feat(layout): adopt unified AppShell across hub + 7 standalones (Phase B) #52
13 changed files with 142 additions and 576 deletions
feat(layout): adopt unified AppShell across hub + 7 standalones (Phase B)
Refactor every entry point to consume the Phase A primitives. Each App.vue collapses from 47-127 lines of shell boilerplate into a thin AppShell consumer that declares its own BottomTab[] and active-path matcher; Hub.vue now reuses ProfileSheetTrigger + PreferencesRow instead of inlining its own bottom row. The Settings tab is dropped from activities and libra (theme/lang/currency now live in the shared profile sheet — see #50 for cleanup of orphaned SettingsPage.vue routes). The redundant top-right LogIn icon is dropped from every standalone (the bottom-nav profile slot covers that affordance). BottomNav.vue gains optional `onClick`, `disabled`, and an optional `path` on BottomTab so consumers can express coming-soon toasts (forum's Spaces/ Search/Alerts) and auth-gated tabs (market's "My Store" → toast when logged out) without reaching back into shell internals. While in here: remove the page-level Refresh buttons in ActivitiesPage, EventsPage, MyTicketsPage, and NostrFeed. The relay-subscription + VisibilityService reconnect path keeps these views fresh; manual refresh was redundant and was now visually colliding with the new top-right HubPill. Net: -434 lines.
commit
c80a8461ac
|
|
@ -1,94 +1,29 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { Toaster } from '@/components/ui/sonner'
|
import { PlusCircle, List, Scale, Wallet } from 'lucide-vue-next'
|
||||||
import LoginDialog from '@/components/auth/LoginDialog.vue'
|
import AppShell from '@/components/layout/AppShell.vue'
|
||||||
import { useTheme } from '@/components/theme-provider'
|
import type { BottomTab } from '@/components/layout/BottomNav.vue'
|
||||||
import { toast } from 'vue-sonner'
|
|
||||||
import { useAuth } from '@/composables/useAuthService'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import {
|
|
||||||
PlusCircle, List, Scale, Wallet, Settings, LogIn,
|
|
||||||
} from 'lucide-vue-next'
|
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
useTheme()
|
// Settings dropped — theme/lang/currency now live in the shared profile sheet.
|
||||||
const { isAuthenticated } = useAuth()
|
const tabs = computed<BottomTab[]>(() => [
|
||||||
|
|
||||||
const showLoginDialog = ref(false)
|
|
||||||
|
|
||||||
// Bottom navigation tabs
|
|
||||||
const bottomTabs = computed(() => [
|
|
||||||
{ name: t('libra.nav.record'), icon: PlusCircle, path: '/record' },
|
{ name: t('libra.nav.record'), icon: PlusCircle, path: '/record' },
|
||||||
{ name: t('libra.nav.transactions'), icon: List, path: '/expenses/transactions' },
|
{ name: t('libra.nav.transactions'), icon: List, path: '/expenses/transactions' },
|
||||||
{ name: t('libra.nav.balance'), icon: Scale, path: '/balance' },
|
{ name: t('libra.nav.balance'), icon: Scale, path: '/balance' },
|
||||||
{ name: t('libra.nav.wallet'), icon: Wallet, path: '/wallet' },
|
{ name: t('libra.nav.wallet'), icon: Wallet, path: '/wallet' },
|
||||||
{ name: t('libra.nav.settings'), icon: Settings, path: '/settings' },
|
|
||||||
])
|
])
|
||||||
|
|
||||||
const isLoginPage = computed(() => route.path === '/login')
|
function isActive(path: string): boolean {
|
||||||
|
if (path === '/record') return route.path === '/record'
|
||||||
function isActiveTab(path: string): boolean {
|
if (path === '/expenses/transactions') return route.path === '/expenses/transactions'
|
||||||
if (path === '/record') {
|
|
||||||
return route.path === '/record'
|
|
||||||
}
|
|
||||||
if (path === '/expenses/transactions') {
|
|
||||||
return route.path === '/expenses/transactions'
|
|
||||||
}
|
|
||||||
return route.path.startsWith(path)
|
return route.path.startsWith(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleLoginSuccess() {
|
|
||||||
showLoginDialog.value = false
|
|
||||||
toast.success('Welcome!')
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="min-h-dvh bg-background font-sans antialiased">
|
<AppShell :tabs="tabs" :is-active="isActive" />
|
||||||
<div class="relative flex min-h-dvh flex-col"
|
|
||||||
style="padding-top: env(safe-area-inset-top); padding-bottom: env(safe-area-inset-bottom)">
|
|
||||||
|
|
||||||
<!-- Top bar with login -->
|
|
||||||
<div v-if="!isLoginPage && !isAuthenticated" class="fixed top-0 right-0 z-50 p-3" style="padding-top: env(safe-area-inset-top)">
|
|
||||||
<Button variant="ghost" size="icon" class="h-8 w-8" @click="router.push('/login')">
|
|
||||||
<LogIn class="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Main content (with bottom padding for nav bar) -->
|
|
||||||
<main class="flex-1" :class="{ 'pb-16': !isLoginPage }">
|
|
||||||
<router-view />
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<!-- Bottom navigation bar -->
|
|
||||||
<nav
|
|
||||||
v-if="!isLoginPage"
|
|
||||||
class="fixed bottom-0 left-0 right-0 z-50 border-t bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60"
|
|
||||||
style="padding-bottom: env(safe-area-inset-bottom)"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-around h-14 max-w-lg mx-auto">
|
|
||||||
<button
|
|
||||||
v-for="tab in bottomTabs"
|
|
||||||
:key="tab.path"
|
|
||||||
class="flex flex-col items-center justify-center gap-0.5 flex-1 h-full transition-colors"
|
|
||||||
:class="isActiveTab(tab.path)
|
|
||||||
? 'text-primary'
|
|
||||||
: 'text-muted-foreground hover:text-foreground'"
|
|
||||||
@click="router.push(tab.path)"
|
|
||||||
>
|
|
||||||
<component :is="tab.icon" class="w-5 h-5" />
|
|
||||||
<span class="text-[10px] font-medium">{{ tab.name }}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Toaster />
|
|
||||||
<LoginDialog v-model:is-open="showLoginDialog" @success="handleLoginSuccess" />
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,84 +1,38 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { Toaster } from '@/components/ui/sonner'
|
import { CalendarDays, Map, Heart, Search } from 'lucide-vue-next'
|
||||||
import LoginDialog from '@/components/auth/LoginDialog.vue'
|
import AppShell from '@/components/layout/AppShell.vue'
|
||||||
import { useTheme } from '@/components/theme-provider'
|
import type { BottomTab } from '@/components/layout/BottomNav.vue'
|
||||||
import { toast } from 'vue-sonner'
|
|
||||||
import {
|
|
||||||
CalendarDays, Map, Heart, Settings, Search,
|
|
||||||
} from 'lucide-vue-next'
|
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
useTheme()
|
// Settings dropped — theme/lang/currency now live in the shared profile sheet.
|
||||||
|
const tabs = computed<BottomTab[]>(() => [
|
||||||
const showLoginDialog = ref(false)
|
|
||||||
|
|
||||||
// Bottom navigation tabs (p'a semana style)
|
|
||||||
const bottomTabs = computed(() => [
|
|
||||||
{ name: t('activities.nav.feed'), icon: Search, path: '/activities' },
|
{ name: t('activities.nav.feed'), icon: Search, path: '/activities' },
|
||||||
{ name: t('activities.nav.calendar'), icon: CalendarDays, path: '/activities/calendar' },
|
{ name: t('activities.nav.calendar'), icon: CalendarDays, path: '/activities/calendar' },
|
||||||
{ name: t('activities.nav.map'), icon: Map, path: '/activities/map' },
|
{ name: t('activities.nav.map'), icon: Map, path: '/activities/map' },
|
||||||
{ name: t('activities.nav.favorites'), icon: Heart, path: '/activities/favorites' },
|
{ name: t('activities.nav.favorites'), icon: Heart, path: '/activities/favorites' },
|
||||||
{ name: t('activities.nav.settings'), icon: Settings, path: '/settings' },
|
|
||||||
])
|
])
|
||||||
|
|
||||||
const isLoginPage = computed(() => route.path === '/login')
|
// Feed tab is active for the bare /activities route AND all sub-paths that
|
||||||
|
// aren't owned by another tab (e.g. /activities/<id> detail pages).
|
||||||
function isActiveTab(path: string): boolean {
|
function isActive(path: string): boolean {
|
||||||
if (path === '/activities') {
|
if (path === '/activities') {
|
||||||
return route.path === '/activities' || route.path.startsWith('/activities/') &&
|
return (
|
||||||
!route.path.startsWith('/activities/calendar') &&
|
route.path === '/activities' ||
|
||||||
!route.path.startsWith('/activities/map') &&
|
(route.path.startsWith('/activities/') &&
|
||||||
!route.path.startsWith('/activities/favorites')
|
!route.path.startsWith('/activities/calendar') &&
|
||||||
|
!route.path.startsWith('/activities/map') &&
|
||||||
|
!route.path.startsWith('/activities/favorites'))
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return route.path.startsWith(path)
|
return route.path.startsWith(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleLoginSuccess() {
|
|
||||||
showLoginDialog.value = false
|
|
||||||
toast.success('Welcome!')
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="min-h-dvh bg-background font-sans antialiased">
|
<AppShell :tabs="tabs" :is-active="isActive" />
|
||||||
<div class="relative flex min-h-dvh flex-col"
|
|
||||||
style="padding-top: env(safe-area-inset-top); padding-bottom: env(safe-area-inset-bottom)">
|
|
||||||
|
|
||||||
<!-- Main content (with bottom padding for nav bar) -->
|
|
||||||
<main class="flex-1" :class="{ 'pb-16': !isLoginPage }">
|
|
||||||
<router-view />
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<!-- Bottom navigation bar (p'a semana style) -->
|
|
||||||
<nav
|
|
||||||
v-if="!isLoginPage"
|
|
||||||
class="fixed bottom-0 left-0 right-0 z-50 border-t bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60"
|
|
||||||
style="padding-bottom: env(safe-area-inset-bottom)"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-around h-14 max-w-lg mx-auto">
|
|
||||||
<button
|
|
||||||
v-for="tab in bottomTabs"
|
|
||||||
:key="tab.path"
|
|
||||||
class="flex flex-col items-center justify-center gap-0.5 flex-1 h-full transition-colors"
|
|
||||||
:class="isActiveTab(tab.path)
|
|
||||||
? 'text-primary'
|
|
||||||
: 'text-muted-foreground hover:text-foreground'"
|
|
||||||
@click="router.push(tab.path)"
|
|
||||||
>
|
|
||||||
<component :is="tab.icon" class="w-5 h-5" />
|
|
||||||
<span class="text-[10px] font-medium">{{ tab.name }}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Toaster />
|
|
||||||
<LoginDialog v-model:is-open="showLoginDialog" @success="handleLoginSuccess" />
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,47 +1,16 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue'
|
import AppShell from '@/components/layout/AppShell.vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import type { BottomTab } from '@/components/layout/BottomNav.vue'
|
||||||
import { Toaster } from '@/components/ui/sonner'
|
|
||||||
import LoginDialog from '@/components/auth/LoginDialog.vue'
|
|
||||||
import { useTheme } from '@/components/theme-provider'
|
|
||||||
import { toast } from 'vue-sonner'
|
|
||||||
import { useAuth } from '@/composables/useAuthService'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { LogIn } from 'lucide-vue-next'
|
|
||||||
|
|
||||||
const route = useRoute()
|
// Chat owns its in-page navigation today; the shell only contributes the
|
||||||
const router = useRouter()
|
// always-on Profile entry on the right of the bottom row.
|
||||||
|
const tabs: BottomTab[] = []
|
||||||
|
|
||||||
useTheme()
|
function isActive(_path: string): boolean {
|
||||||
const { isAuthenticated } = useAuth()
|
return false
|
||||||
|
|
||||||
const showLoginDialog = ref(false)
|
|
||||||
|
|
||||||
const isLoginPage = computed(() => route.path === '/login')
|
|
||||||
|
|
||||||
async function handleLoginSuccess() {
|
|
||||||
showLoginDialog.value = false
|
|
||||||
toast.success('Welcome!')
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="min-h-dvh bg-background font-sans antialiased">
|
<AppShell :tabs="tabs" :is-active="isActive" />
|
||||||
<div class="relative flex min-h-dvh flex-col"
|
|
||||||
style="padding-top: env(safe-area-inset-top); padding-bottom: env(safe-area-inset-bottom)">
|
|
||||||
|
|
||||||
<div v-if="!isLoginPage && !isAuthenticated" class="fixed top-0 right-0 z-50 p-3" style="padding-top: env(safe-area-inset-top)">
|
|
||||||
<Button variant="ghost" size="icon" class="h-8 w-8" @click="router.push('/login')">
|
|
||||||
<LogIn class="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<main class="flex-1">
|
|
||||||
<router-view />
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Toaster />
|
|
||||||
<LoginDialog v-model:is-open="showLoginDialog" @success="handleLoginSuccess" />
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,16 @@ export interface BottomTab {
|
||||||
name: string
|
name: string
|
||||||
/** lucide-vue-next icon component. */
|
/** lucide-vue-next icon component. */
|
||||||
icon: Component
|
icon: Component
|
||||||
/** Router path to push on click. */
|
/** Router path to push on click. Optional — coming-soon entries omit it. */
|
||||||
path: string
|
path?: string
|
||||||
/** Optional unread/cart badge count. Falsy values hide the badge. */
|
/** Optional unread/cart badge count. Falsy values hide the badge. */
|
||||||
badge?: number | null
|
badge?: number | null
|
||||||
|
/** Click override. When provided, replaces the default router.push(path).
|
||||||
|
* Consumers use this for coming-soon toasts or auth-gated CTAs. */
|
||||||
|
onClick?: () => void
|
||||||
|
/** Render the entry ghosted (opacity-reduced). Used for coming-soon and
|
||||||
|
* for auth-required tabs when the user is logged out. */
|
||||||
|
disabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
@ -28,6 +34,14 @@ interface Props {
|
||||||
const props = withDefaults(defineProps<Props>(), { loggedOutOpensSheet: false })
|
const props = withDefaults(defineProps<Props>(), { loggedOutOpensSheet: false })
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
function onTabClick(tab: BottomTab) {
|
||||||
|
if (tab.onClick) {
|
||||||
|
tab.onClick()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (tab.path) router.push(tab.path)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -39,13 +53,16 @@ const router = useRouter()
|
||||||
<!-- App-specific tabs (consumer-provided) -->
|
<!-- App-specific tabs (consumer-provided) -->
|
||||||
<button
|
<button
|
||||||
v-for="tab in props.tabs"
|
v-for="tab in props.tabs"
|
||||||
:key="tab.path"
|
:key="tab.name"
|
||||||
class="relative flex flex-col items-center justify-center gap-0.5 flex-1 h-full transition-colors"
|
class="relative flex flex-col items-center justify-center gap-0.5 flex-1 h-full transition-colors"
|
||||||
:class="props.isActive(tab.path)
|
:class="[
|
||||||
? 'text-primary'
|
tab.path && props.isActive(tab.path)
|
||||||
: 'text-muted-foreground hover:text-foreground'"
|
? 'text-primary'
|
||||||
:aria-current="props.isActive(tab.path) ? 'page' : undefined"
|
: 'text-muted-foreground hover:text-foreground',
|
||||||
@click="router.push(tab.path)"
|
tab.disabled ? 'opacity-50' : '',
|
||||||
|
]"
|
||||||
|
:aria-current="tab.path && props.isActive(tab.path) ? 'page' : undefined"
|
||||||
|
@click="onTabClick(tab)"
|
||||||
>
|
>
|
||||||
<component :is="tab.icon" class="w-5 h-5" />
|
<component :is="tab.icon" class="w-5 h-5" />
|
||||||
<span class="text-[10px] font-medium">{{ tab.name }}</span>
|
<span class="text-[10px] font-medium">{{ tab.name }}</span>
|
||||||
|
|
|
||||||
|
|
@ -1,104 +1,35 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { Toaster } from '@/components/ui/sonner'
|
|
||||||
import LoginDialog from '@/components/auth/LoginDialog.vue'
|
|
||||||
import { useTheme } from '@/components/theme-provider'
|
|
||||||
import { toast } from 'vue-sonner'
|
import { toast } from 'vue-sonner'
|
||||||
import { useAuth } from '@/composables/useAuthService'
|
import { Newspaper, Hash, SquarePen, Search, Bell } from 'lucide-vue-next'
|
||||||
import { Button } from '@/components/ui/button'
|
import AppShell from '@/components/layout/AppShell.vue'
|
||||||
import {
|
import type { BottomTab } from '@/components/layout/BottomNav.vue'
|
||||||
LogIn, Newspaper, Hash, SquarePen, Search, Bell,
|
|
||||||
} from 'lucide-vue-next'
|
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
useTheme()
|
function comingSoon(label: string, issue: number) {
|
||||||
const { isAuthenticated } = useAuth()
|
toast.info(`${label} — coming soon`, {
|
||||||
|
description: `Tracked on issue #${issue}`,
|
||||||
const showLoginDialog = ref(false)
|
})
|
||||||
|
|
||||||
interface Tab {
|
|
||||||
name: string
|
|
||||||
icon: any
|
|
||||||
path?: string
|
|
||||||
comingSoon?: { issue: number; label: string }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const bottomTabs: Tab[] = [
|
const tabs = computed<BottomTab[]>(() => [
|
||||||
{ name: 'Posts', icon: Newspaper, path: '/forum' },
|
{ name: 'Posts', icon: Newspaper, path: '/forum' },
|
||||||
{ name: 'Spaces', icon: Hash, comingSoon: { issue: 31, label: 'Spaces' } },
|
{ name: 'Spaces', icon: Hash, disabled: true, onClick: () => comingSoon('Spaces', 31) },
|
||||||
{ name: 'Submit', icon: SquarePen, path: '/submit' },
|
{ name: 'Submit', icon: SquarePen, path: '/submit' },
|
||||||
{ name: 'Search', icon: Search, comingSoon: { issue: 15, label: 'Search' } },
|
{ name: 'Search', icon: Search, disabled: true, onClick: () => comingSoon('Search', 15) },
|
||||||
{ name: 'Alerts', icon: Bell, comingSoon: { issue: 32, label: 'Notifications' } },
|
{ name: 'Alerts', icon: Bell, disabled: true, onClick: () => comingSoon('Notifications', 32) },
|
||||||
]
|
])
|
||||||
|
|
||||||
const isLoginPage = computed(() => route.path === '/login')
|
function isActive(path: string): boolean {
|
||||||
|
if (path === '/forum') {
|
||||||
function isActiveTab(tab: Tab): boolean {
|
return route.path === '/forum' || route.path.startsWith('/submission/')
|
||||||
if (!tab.path) return false
|
|
||||||
if (tab.path === '/forum') return route.path === '/forum' || route.path.startsWith('/submission/')
|
|
||||||
return route.path.startsWith(tab.path)
|
|
||||||
}
|
|
||||||
|
|
||||||
function onTabClick(tab: Tab) {
|
|
||||||
if (tab.path) {
|
|
||||||
router.push(tab.path)
|
|
||||||
} else if (tab.comingSoon) {
|
|
||||||
toast.info(`${tab.comingSoon.label} — coming soon`, {
|
|
||||||
description: `Tracked on issue #${tab.comingSoon.issue}`,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
return route.path.startsWith(path)
|
||||||
|
|
||||||
async function handleLoginSuccess() {
|
|
||||||
showLoginDialog.value = false
|
|
||||||
toast.success('Welcome!')
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="min-h-dvh bg-background font-sans antialiased">
|
<AppShell :tabs="tabs" :is-active="isActive" />
|
||||||
<div class="relative flex min-h-dvh flex-col"
|
|
||||||
style="padding-top: env(safe-area-inset-top); padding-bottom: env(safe-area-inset-bottom)">
|
|
||||||
|
|
||||||
<div v-if="!isLoginPage && !isAuthenticated" class="fixed top-0 right-0 z-50 p-3" style="padding-top: env(safe-area-inset-top)">
|
|
||||||
<Button variant="ghost" size="icon" class="h-8 w-8" @click="router.push('/login')">
|
|
||||||
<LogIn class="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<main class="flex-1" :class="{ 'pb-16': !isLoginPage }">
|
|
||||||
<router-view />
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<nav
|
|
||||||
v-if="!isLoginPage"
|
|
||||||
class="fixed bottom-0 left-0 right-0 z-50 border-t bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60"
|
|
||||||
style="padding-bottom: env(safe-area-inset-bottom)"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-around h-14 max-w-lg mx-auto">
|
|
||||||
<button
|
|
||||||
v-for="tab in bottomTabs"
|
|
||||||
:key="tab.name"
|
|
||||||
class="flex flex-col items-center justify-center gap-0.5 flex-1 h-full transition-colors"
|
|
||||||
:class="[
|
|
||||||
isActiveTab(tab)
|
|
||||||
? 'text-primary'
|
|
||||||
: 'text-muted-foreground hover:text-foreground',
|
|
||||||
tab.comingSoon ? 'opacity-50' : '',
|
|
||||||
]"
|
|
||||||
@click="onTabClick(tab)"
|
|
||||||
>
|
|
||||||
<component :is="tab.icon" class="w-5 h-5" />
|
|
||||||
<span class="text-[10px] font-medium">{{ tab.name }}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Toaster />
|
|
||||||
<LoginDialog v-model:is-open="showLoginDialog" @success="handleLoginSuccess" />
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,114 +1,55 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { Toaster } from '@/components/ui/sonner'
|
|
||||||
import LoginDialog from '@/components/auth/LoginDialog.vue'
|
|
||||||
import { useTheme } from '@/components/theme-provider'
|
|
||||||
import { toast } from 'vue-sonner'
|
import { toast } from 'vue-sonner'
|
||||||
|
import { Store, ShoppingCart, Package } from 'lucide-vue-next'
|
||||||
|
import AppShell from '@/components/layout/AppShell.vue'
|
||||||
|
import type { BottomTab } from '@/components/layout/BottomNav.vue'
|
||||||
import { useAuth } from '@/composables/useAuthService'
|
import { useAuth } from '@/composables/useAuthService'
|
||||||
import { useMarketStore } from '@/modules/market/stores/market'
|
import { useMarketStore } from '@/modules/market/stores/market'
|
||||||
import {
|
|
||||||
Store, ShoppingCart, Package, LogIn, User as UserIcon,
|
|
||||||
} from 'lucide-vue-next'
|
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
useTheme()
|
|
||||||
const { isAuthenticated } = useAuth()
|
const { isAuthenticated } = useAuth()
|
||||||
const marketStore = useMarketStore()
|
const marketStore = useMarketStore()
|
||||||
|
|
||||||
const showLoginDialog = ref(false)
|
const tabs = computed<BottomTab[]>(() => [
|
||||||
|
|
||||||
interface Tab {
|
|
||||||
name: string
|
|
||||||
icon: any
|
|
||||||
path?: string
|
|
||||||
authRequired?: boolean
|
|
||||||
badge?: () => number
|
|
||||||
onClick?: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const bottomTabs = computed<Tab[]>(() => [
|
|
||||||
{ name: 'Browse', icon: Store, path: '/market' },
|
{ name: 'Browse', icon: Store, path: '/market' },
|
||||||
{ name: 'Cart', icon: ShoppingCart, path: '/cart', badge: () => marketStore.totalCartItems },
|
{
|
||||||
{ name: 'My Store', icon: Package, path: '/market/dashboard', authRequired: true },
|
name: 'Cart',
|
||||||
isAuthenticated.value
|
icon: ShoppingCart,
|
||||||
? { name: 'Profile', icon: UserIcon, path: '/profile' }
|
path: '/cart',
|
||||||
: { name: 'Log in', icon: LogIn, path: '/login' },
|
badge: marketStore.totalCartItems || null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'My Store',
|
||||||
|
icon: Package,
|
||||||
|
path: isAuthenticated.value ? '/market/dashboard' : undefined,
|
||||||
|
disabled: !isAuthenticated.value,
|
||||||
|
onClick: !isAuthenticated.value
|
||||||
|
? () =>
|
||||||
|
toast.info('My Store requires login', {
|
||||||
|
action: { label: 'Log in', onClick: () => router.push('/login') },
|
||||||
|
})
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
const isLoginPage = computed(() => route.path === '/login')
|
function isActive(path: string): boolean {
|
||||||
|
if (path === '/market') {
|
||||||
function isActiveTab(tab: Tab): boolean {
|
return (
|
||||||
if (!tab.path) return false
|
route.path === '/market' ||
|
||||||
if (tab.path === '/market') {
|
route.path.startsWith('/market/stall/') ||
|
||||||
return route.path === '/market' || route.path.startsWith('/market/stall/') || route.path.startsWith('/market/product/')
|
route.path.startsWith('/market/product/')
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (tab.path === '/cart') return route.path === '/cart' || route.path.startsWith('/checkout/')
|
if (path === '/cart') {
|
||||||
return route.path.startsWith(tab.path)
|
return route.path === '/cart' || route.path.startsWith('/checkout/')
|
||||||
}
|
|
||||||
|
|
||||||
function onTabClick(tab: Tab) {
|
|
||||||
if (tab.authRequired && !isAuthenticated.value) {
|
|
||||||
toast.info(`${tab.name} requires login`, {
|
|
||||||
action: {
|
|
||||||
label: 'Log in',
|
|
||||||
onClick: () => router.push('/login'),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
if (tab.path) router.push(tab.path)
|
return route.path.startsWith(path)
|
||||||
}
|
|
||||||
|
|
||||||
async function handleLoginSuccess() {
|
|
||||||
showLoginDialog.value = false
|
|
||||||
toast.success('Welcome!')
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="min-h-dvh bg-background font-sans antialiased">
|
<AppShell :tabs="tabs" :is-active="isActive" />
|
||||||
<div class="relative flex min-h-dvh flex-col"
|
|
||||||
style="padding-top: env(safe-area-inset-top); padding-bottom: env(safe-area-inset-bottom)">
|
|
||||||
|
|
||||||
<main class="flex-1" :class="{ 'pb-16': !isLoginPage }">
|
|
||||||
<router-view />
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<nav
|
|
||||||
v-if="!isLoginPage"
|
|
||||||
class="fixed bottom-0 left-0 right-0 z-50 border-t bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60"
|
|
||||||
style="padding-bottom: env(safe-area-inset-bottom)"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-around h-14 max-w-lg mx-auto">
|
|
||||||
<button
|
|
||||||
v-for="tab in bottomTabs"
|
|
||||||
:key="tab.name"
|
|
||||||
class="flex flex-col items-center justify-center gap-0.5 flex-1 h-full transition-colors"
|
|
||||||
:class="[
|
|
||||||
isActiveTab(tab)
|
|
||||||
? 'text-primary'
|
|
||||||
: 'text-muted-foreground hover:text-foreground',
|
|
||||||
tab.authRequired && !isAuthenticated ? 'opacity-50' : '',
|
|
||||||
]"
|
|
||||||
@click="onTabClick(tab)"
|
|
||||||
>
|
|
||||||
<span class="relative inline-flex">
|
|
||||||
<component :is="tab.icon" class="w-5 h-5" />
|
|
||||||
<span
|
|
||||||
v-if="tab.badge && tab.badge() > 0"
|
|
||||||
class="absolute -top-1.5 -right-2 min-w-[16px] h-4 px-1 rounded-full bg-primary text-primary-foreground text-[10px] font-semibold leading-4 text-center"
|
|
||||||
>{{ tab.badge() }}</span>
|
|
||||||
</span>
|
|
||||||
<span class="text-[10px] font-medium">{{ tab.name }}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Toaster />
|
|
||||||
<LoginDialog v-model:is-open="showLoginDialog" @success="handleLoginSuccess" />
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import {
|
||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
} from '@/components/ui/collapsible'
|
} from '@/components/ui/collapsible'
|
||||||
import { RefreshCw, SlidersHorizontal, ChevronDown, Plus, LogIn } from 'lucide-vue-next'
|
import { SlidersHorizontal, ChevronDown, Plus } from 'lucide-vue-next'
|
||||||
import { useAuth } from '@/composables/useAuthService'
|
import { useAuth } from '@/composables/useAuthService'
|
||||||
import { useActivities } from '../composables/useActivities'
|
import { useActivities } from '../composables/useActivities'
|
||||||
import CreateEventDialog from '../components/CreateEventDialog.vue'
|
import CreateEventDialog from '../components/CreateEventDialog.vue'
|
||||||
|
|
@ -86,18 +86,6 @@ async function handleCreateEvent(eventData: CreateEventRequest) {
|
||||||
<Plus class="w-4 h-4 mr-1.5" />
|
<Plus class="w-4 h-4 mr-1.5" />
|
||||||
<span class="hidden sm:inline">{{ t('activities.createNew') }}</span>
|
<span class="hidden sm:inline">{{ t('activities.createNew') }}</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="icon" class="h-8 w-8" @click="handleRefresh" :disabled="isLoading">
|
|
||||||
<RefreshCw class="w-4 h-4" :class="{ 'animate-spin': isLoading }" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
v-if="!isAuthenticated"
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
class="h-8 w-8"
|
|
||||||
@click="$router.push('/login')"
|
|
||||||
>
|
|
||||||
<LogIn class="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import { Badge } from '@/components/ui/badge'
|
||||||
import { format } from 'date-fns'
|
import { format } from 'date-fns'
|
||||||
import PurchaseTicketDialog from '../components/PurchaseTicketDialog.vue'
|
import PurchaseTicketDialog from '../components/PurchaseTicketDialog.vue'
|
||||||
import CreateEventDialog from '../components/CreateEventDialog.vue'
|
import CreateEventDialog from '../components/CreateEventDialog.vue'
|
||||||
import { RefreshCw, User, LogIn, Plus } from 'lucide-vue-next'
|
import { User, LogIn, Plus } from 'lucide-vue-next'
|
||||||
import { formatEventPrice } from '@/lib/utils/formatting'
|
import { formatEventPrice } from '@/lib/utils/formatting'
|
||||||
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
||||||
import type { TicketApiService } from '../services/TicketApiService'
|
import type { TicketApiService } from '../services/TicketApiService'
|
||||||
|
|
@ -84,10 +84,6 @@ function handleEventCreated() {
|
||||||
<Plus class="w-4 h-4" />
|
<Plus class="w-4 h-4" />
|
||||||
<span class="ml-2">Create Event</span>
|
<span class="ml-2">Create Event</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="secondary" size="sm" @click="refresh" :disabled="isLoading" class="flex-1 sm:flex-none">
|
|
||||||
<RefreshCw class="w-4 h-4" :class="{ 'animate-spin': isLoading }" />
|
|
||||||
<span class="ml-2">Refresh</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -131,10 +131,6 @@ onMounted(async () => {
|
||||||
<span>Please log in to view your tickets</span>
|
<span>Please log in to view your tickets</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="secondary" size="sm" @click="refresh" :disabled="isLoading">
|
|
||||||
<span v-if="isLoading" class="animate-spin mr-2">⚡</span>
|
|
||||||
Refresh
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!isAuthenticated" class="text-center py-12">
|
<div v-if="!isAuthenticated" class="text-center py-12">
|
||||||
|
|
|
||||||
|
|
@ -465,16 +465,6 @@ function cancelDelete() {
|
||||||
<p class="text-xs md:text-sm text-muted-foreground">{{ feedDescription }}</p>
|
<p class="text-xs md:text-sm text-muted-foreground">{{ feedDescription }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
@click="refreshFeed"
|
|
||||||
:disabled="isLoading"
|
|
||||||
class="gap-2 md:h-10 md:px-4 hover:bg-accent transition-colors"
|
|
||||||
>
|
|
||||||
<RefreshCw :class="{ 'animate-spin': isLoading }" class="h-4 w-4" />
|
|
||||||
<span class="hidden md:inline">Refresh</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,17 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useAuth } from '@/composables/useAuthService'
|
import { useAuth } from '@/composables/useAuthService'
|
||||||
import { useTheme } from '@/components/theme-provider'
|
|
||||||
import { useLocale } from '@/composables/useLocale'
|
|
||||||
import { toast } from 'vue-sonner'
|
import { toast } from 'vue-sonner'
|
||||||
import {
|
import {
|
||||||
Scale, ListTodo, Newspaper, MessageCircle, Wallet, CalendarDays,
|
Scale, ListTodo, Newspaper, MessageCircle, Wallet, CalendarDays,
|
||||||
Store, UtensilsCrossed,
|
Store, UtensilsCrossed,
|
||||||
User as UserIcon, LogIn, Sun, Moon, Monitor, Globe, Coins,
|
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
import {
|
import ProfileSheetTrigger from '@/components/layout/ProfileSheetTrigger.vue'
|
||||||
DropdownMenu, DropdownMenuTrigger, DropdownMenuContent,
|
import PreferencesRow from '@/components/layout/PreferencesRow.vue'
|
||||||
DropdownMenuRadioGroup, DropdownMenuRadioItem,
|
|
||||||
DropdownMenuLabel, DropdownMenuSeparator,
|
|
||||||
} from '@/components/ui/dropdown-menu'
|
|
||||||
import {
|
|
||||||
Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetTrigger,
|
|
||||||
} from '@/components/ui/sheet'
|
|
||||||
import ProfileSettings from '@/modules/base/components/ProfileSettings.vue'
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { isAuthenticated } = useAuth()
|
const { isAuthenticated } = useAuth()
|
||||||
const { theme, setTheme, currentTheme } = useTheme()
|
|
||||||
const { currentLocale, locales, setLocale } = useLocale()
|
|
||||||
|
|
||||||
interface Module {
|
interface Module {
|
||||||
label: string
|
label: string
|
||||||
|
|
@ -85,14 +73,6 @@ function onTileClick(m: Module, event: Event) {
|
||||||
}
|
}
|
||||||
// "Coming soon" tiles (no envKey, no authRequired) silently do nothing.
|
// "Coming soon" tiles (no envKey, no authRequired) silently do nothing.
|
||||||
}
|
}
|
||||||
|
|
||||||
const showProfile = ref(false)
|
|
||||||
|
|
||||||
function notImplemented() {
|
|
||||||
toast.info('Currency picker — coming soon', {
|
|
||||||
description: 'A preferred-currency setting (sats/USD/EUR) is on the roadmap.',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -154,85 +134,17 @@ function notImplemented() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Bottom bar: profile & user preferences -->
|
<!-- Bottom bar: profile + user preferences. Profile entry is identical to
|
||||||
|
every standalone (avatar from kind-0, sheet has back-to-hub stub
|
||||||
|
removed since this IS the hub). Logged-out users still get the
|
||||||
|
sheet so they can change theme/lang without signing in. -->
|
||||||
<nav
|
<nav
|
||||||
class="relative z-10 border-t bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60"
|
class="relative z-10 border-t bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60"
|
||||||
style="padding-bottom: env(safe-area-inset-bottom)"
|
style="padding-bottom: env(safe-area-inset-bottom)"
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-around h-14 max-w-lg mx-auto">
|
<div class="flex items-center justify-around h-14 max-w-lg mx-auto">
|
||||||
<!-- Profile (when logged in) / Log in (when not) -->
|
<ProfileSheetTrigger :logged-out-opens-sheet="true" />
|
||||||
<Sheet v-if="isAuthenticated" v-model:open="showProfile">
|
<PreferencesRow layout="row" />
|
||||||
<SheetTrigger as-child>
|
|
||||||
<button class="flex flex-col items-center justify-center gap-0.5 flex-1 h-full text-muted-foreground hover:text-foreground transition-colors">
|
|
||||||
<UserIcon class="w-5 h-5" />
|
|
||||||
<span class="text-[10px] font-medium">Profile</span>
|
|
||||||
</button>
|
|
||||||
</SheetTrigger>
|
|
||||||
<SheetContent side="bottom" class="h-[90vh] overflow-y-auto">
|
|
||||||
<SheetHeader>
|
|
||||||
<SheetTitle>Profile</SheetTitle>
|
|
||||||
<SheetDescription>Your Nostr identity and display name.</SheetDescription>
|
|
||||||
</SheetHeader>
|
|
||||||
<div class="mt-4">
|
|
||||||
<ProfileSettings />
|
|
||||||
</div>
|
|
||||||
</SheetContent>
|
|
||||||
</Sheet>
|
|
||||||
<button
|
|
||||||
v-else
|
|
||||||
class="flex flex-col items-center justify-center gap-0.5 flex-1 h-full text-muted-foreground hover:text-foreground transition-colors"
|
|
||||||
@click="router.push('/login')"
|
|
||||||
>
|
|
||||||
<LogIn class="w-5 h-5" />
|
|
||||||
<span class="text-[10px] font-medium">Log in</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Theme -->
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger as-child>
|
|
||||||
<button class="flex flex-col items-center justify-center gap-0.5 flex-1 h-full text-muted-foreground hover:text-foreground transition-colors">
|
|
||||||
<component :is="currentTheme === 'dark' ? Moon : Sun" class="w-5 h-5" />
|
|
||||||
<span class="text-[10px] font-medium">Theme</span>
|
|
||||||
</button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="center" class="w-40">
|
|
||||||
<DropdownMenuLabel>Theme</DropdownMenuLabel>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuRadioGroup :model-value="theme" @update:model-value="(v: string) => setTheme(v as 'dark' | 'light' | 'system')">
|
|
||||||
<DropdownMenuRadioItem value="light"><Sun class="w-4 h-4 mr-2" />Light</DropdownMenuRadioItem>
|
|
||||||
<DropdownMenuRadioItem value="dark"><Moon class="w-4 h-4 mr-2" />Dark</DropdownMenuRadioItem>
|
|
||||||
<DropdownMenuRadioItem value="system"><Monitor class="w-4 h-4 mr-2" />System</DropdownMenuRadioItem>
|
|
||||||
</DropdownMenuRadioGroup>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
|
|
||||||
<!-- Language -->
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger as-child>
|
|
||||||
<button class="flex flex-col items-center justify-center gap-0.5 flex-1 h-full text-muted-foreground hover:text-foreground transition-colors">
|
|
||||||
<Globe class="w-5 h-5" />
|
|
||||||
<span class="text-[10px] font-medium">Language</span>
|
|
||||||
</button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="center" class="w-44">
|
|
||||||
<DropdownMenuLabel>Language</DropdownMenuLabel>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuRadioGroup :model-value="currentLocale" @update:model-value="(v: string) => setLocale(v)">
|
|
||||||
<DropdownMenuRadioItem v-for="l in locales" :key="l.code" :value="l.code">
|
|
||||||
<span class="mr-2">{{ l.flag }}</span>{{ l.name }}
|
|
||||||
</DropdownMenuRadioItem>
|
|
||||||
</DropdownMenuRadioGroup>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
|
|
||||||
<!-- Currency (placeholder) -->
|
|
||||||
<button
|
|
||||||
class="flex flex-col items-center justify-center gap-0.5 flex-1 h-full text-muted-foreground hover:text-foreground transition-colors opacity-50"
|
|
||||||
@click="notImplemented"
|
|
||||||
>
|
|
||||||
<Coins class="w-5 h-5" />
|
|
||||||
<span class="text-[10px] font-medium">Currency</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,47 +1,16 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue'
|
import AppShell from '@/components/layout/AppShell.vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import type { BottomTab } from '@/components/layout/BottomNav.vue'
|
||||||
import { Toaster } from '@/components/ui/sonner'
|
|
||||||
import LoginDialog from '@/components/auth/LoginDialog.vue'
|
|
||||||
import { useTheme } from '@/components/theme-provider'
|
|
||||||
import { toast } from 'vue-sonner'
|
|
||||||
import { useAuth } from '@/composables/useAuthService'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { LogIn } from 'lucide-vue-next'
|
|
||||||
|
|
||||||
const route = useRoute()
|
// Tasks owns its in-page navigation; the shell only contributes the
|
||||||
const router = useRouter()
|
// always-on Profile entry on the right of the bottom row.
|
||||||
|
const tabs: BottomTab[] = []
|
||||||
|
|
||||||
useTheme()
|
function isActive(_path: string): boolean {
|
||||||
const { isAuthenticated } = useAuth()
|
return false
|
||||||
|
|
||||||
const showLoginDialog = ref(false)
|
|
||||||
|
|
||||||
const isLoginPage = computed(() => route.path === '/login')
|
|
||||||
|
|
||||||
async function handleLoginSuccess() {
|
|
||||||
showLoginDialog.value = false
|
|
||||||
toast.success('Welcome!')
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="min-h-dvh bg-background font-sans antialiased">
|
<AppShell :tabs="tabs" :is-active="isActive" />
|
||||||
<div class="relative flex min-h-dvh flex-col"
|
|
||||||
style="padding-top: env(safe-area-inset-top); padding-bottom: env(safe-area-inset-bottom)">
|
|
||||||
|
|
||||||
<div v-if="!isLoginPage && !isAuthenticated" class="fixed top-0 right-0 z-50 p-3" style="padding-top: env(safe-area-inset-top)">
|
|
||||||
<Button variant="ghost" size="icon" class="h-8 w-8" @click="router.push('/login')">
|
|
||||||
<LogIn class="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<main class="flex-1">
|
|
||||||
<router-view />
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Toaster />
|
|
||||||
<LoginDialog v-model:is-open="showLoginDialog" @success="handleLoginSuccess" />
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,48 +1,16 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue'
|
import AppShell from '@/components/layout/AppShell.vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import type { BottomTab } from '@/components/layout/BottomNav.vue'
|
||||||
import { Toaster } from '@/components/ui/sonner'
|
|
||||||
import LoginDialog from '@/components/auth/LoginDialog.vue'
|
|
||||||
import { useTheme } from '@/components/theme-provider'
|
|
||||||
import { toast } from 'vue-sonner'
|
|
||||||
import { useAuth } from '@/composables/useAuthService'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { LogIn } from 'lucide-vue-next'
|
|
||||||
|
|
||||||
const route = useRoute()
|
// Wallet owns its in-page navigation; the shell only contributes the
|
||||||
const router = useRouter()
|
// always-on Profile entry on the right of the bottom row.
|
||||||
|
const tabs: BottomTab[] = []
|
||||||
|
|
||||||
useTheme()
|
function isActive(_path: string): boolean {
|
||||||
const { isAuthenticated } = useAuth()
|
return false
|
||||||
|
|
||||||
const showLoginDialog = ref(false)
|
|
||||||
|
|
||||||
const isLoginPage = computed(() => route.path === '/login')
|
|
||||||
|
|
||||||
async function handleLoginSuccess() {
|
|
||||||
showLoginDialog.value = false
|
|
||||||
toast.success('Welcome!')
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="min-h-dvh bg-background font-sans antialiased">
|
<AppShell :tabs="tabs" :is-active="isActive" />
|
||||||
<div class="relative flex min-h-dvh flex-col"
|
|
||||||
style="padding-top: env(safe-area-inset-top); padding-bottom: env(safe-area-inset-bottom)">
|
|
||||||
|
|
||||||
<!-- Top bar with login -->
|
|
||||||
<div v-if="!isLoginPage && !isAuthenticated" class="fixed top-0 right-0 z-50 p-3" style="padding-top: env(safe-area-inset-top)">
|
|
||||||
<Button variant="ghost" size="icon" class="h-8 w-8" @click="router.push('/login')">
|
|
||||||
<LogIn class="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<main class="flex-1">
|
|
||||||
<router-view />
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Toaster />
|
|
||||||
<LoginDialog v-model:is-open="showLoginDialog" @success="handleLoginSuccess" />
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue