feat(forum): add bottom navigation bar

5 tabs at the bottom of the forum standalone:
  Posts (→ /forum), Spaces, Submit (→ /submit), Search, Alerts

Spaces, Search, and Alerts are dimmed and emit a "coming soon" toast
on tap pointing at the tracking issue:
  - Spaces  → #31 (NIP-72 communities)
  - Search  → #15 (link aggregator search)
  - Alerts  → #32 (per-standalone notifications, hub aggregation)

Mirrors the activities-app bottom-bar pattern (icon + 10px label,
fixed bottom, safe-area-aware) and replaces the previous bare
forum-app shell which had no way to compose a new submission.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-05-02 10:07:50 +02:00
commit a694dc2135

View file

@ -7,7 +7,9 @@ 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'
import {
LogIn, Newspaper, Hash, SquarePen, Search, Bell,
} from 'lucide-vue-next'
const route = useRoute()
const router = useRouter()
@ -17,8 +19,39 @@ const { isAuthenticated } = useAuth()
const showLoginDialog = ref(false)
interface Tab {
name: string
icon: any
path?: string
comingSoon?: { issue: number; label: string }
}
const bottomTabs: Tab[] = [
{ name: 'Posts', icon: Newspaper, path: '/forum' },
{ name: 'Spaces', icon: Hash, comingSoon: { issue: 31, label: 'Spaces' } },
{ name: 'Submit', icon: SquarePen, path: '/submit' },
{ name: 'Search', icon: Search, comingSoon: { issue: 15, label: 'Search' } },
{ name: 'Alerts', icon: Bell, comingSoon: { issue: 32, label: 'Notifications' } },
]
const isLoginPage = computed(() => route.path === '/login')
function isActiveTab(tab: Tab): boolean {
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}`,
})
}
}
async function handleLoginSuccess() {
showLoginDialog.value = false
toast.success('Welcome!')
@ -36,9 +69,33 @@ async function handleLoginSuccess() {
</Button>
</div>
<main class="flex-1">
<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 />