feat(hub): ghost auth-required tiles + dock swaps Profile↔Log in

Two related UX hardening tweaks for the unauthenticated case.

1. Module.authRequired flag

   Tiles for modules with no public view (wallet, chat, castle) are
   now ghosted out for unauthenticated visitors — same visual
   treatment we already apply to "coming soon" tiles (opacity 60,
   cursor not-allowed, non-anchored). This prevents an unauth user
   from clicking through to a standalone that will instantly bounce
   them to /login (per the strict guards in those apps).

   Implementation: hubLink() returns null when authRequired &&
   !isAuthenticated, which already triggers the existing non-link
   render branch. No new visual treatment to design.

   Public modules (forum, market, tasks, activities) and the
   restaurant placeholder are unaffected.

2. Bottom-dock Profile↔Log-in swap

   When logged in, the first dock slot opens the Profile sheet
   (existing behaviour). When logged out it now renders a plain
   Log-In button that pushes /login on the hub itself. Avoids
   showing a "Profile" affordance to a user who has no profile yet.

Both changes localised to src/pages/Hub.vue. No other files
touched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-05-02 14:17:55 +02:00
commit b80ad24ae2

View file

@ -1,5 +1,6 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { useRouter } from 'vue-router'
import { useAuth } from '@/composables/useAuthService'
import { useTheme } from '@/components/theme-provider'
import { useLocale } from '@/composables/useLocale'
@ -7,7 +8,7 @@ import { toast } from 'vue-sonner'
import {
Castle, ListTodo, Newspaper, MessageCircle, Wallet, CalendarDays,
Store, UtensilsCrossed,
User as UserIcon, Sun, Moon, Monitor, Globe, Coins,
User as UserIcon, LogIn, Sun, Moon, Monitor, Globe, Coins,
} from 'lucide-vue-next'
import {
DropdownMenu, DropdownMenuTrigger, DropdownMenuContent,
@ -19,6 +20,7 @@ import {
} from '@/components/ui/sheet'
import ProfileSettings from '@/modules/base/components/ProfileSettings.vue'
const router = useRouter()
const { isAuthenticated } = useAuth()
const { theme, setTheme, currentTheme } = useTheme()
const { currentLocale, locales, setLocale } = useLocale()
@ -31,6 +33,8 @@ interface Module {
glow: string
envKey?: string
status?: string
/** When true, the tile is ghosted out unless the user is logged in. */
authRequired?: boolean
/** Unread count for the corner badge. Wire to real data via #32. */
unread?: number
}
@ -39,12 +43,12 @@ interface Module {
const modules: Module[] = [
{ label: 'Restaurant', chakra: 'Muladhara', icon: UtensilsCrossed, bgClass: '', glow: 'rgba(255,80,80,0.5)', status: 'coming soon' },
{ label: 'Market', chakra: 'Muladhara', icon: Store, bgClass: '', glow: 'rgba(255,80,80,0.5)', envKey: 'VITE_HUB_MARKET_URL', status: 'alpha' },
{ label: 'Wallet', chakra: 'Manipura', icon: Wallet, bgClass: '', glow: 'rgba(255,200,0,0.5)', envKey: 'VITE_HUB_WALLET_URL', status: 'alpha' },
{ label: 'Wallet', chakra: 'Manipura', icon: Wallet, bgClass: '', glow: 'rgba(255,200,0,0.5)', envKey: 'VITE_HUB_WALLET_URL', status: 'alpha', authRequired: true },
{ label: 'Activities', chakra: 'Swadhisthana', icon: CalendarDays, bgClass: '', glow: 'rgba(255,165,0,0.5)', envKey: 'VITE_HUB_ACTIVITIES_URL', status: 'beta' },
{ label: 'Chat', chakra: 'Anahata', icon: MessageCircle, bgClass: '', glow: 'rgba(0,200,80,0.5)', envKey: 'VITE_HUB_CHAT_URL', status: 'alpha' },
{ label: 'Chat', chakra: 'Anahata', icon: MessageCircle, bgClass: '', glow: 'rgba(0,200,80,0.5)', envKey: 'VITE_HUB_CHAT_URL', status: 'alpha', authRequired: true },
{ label: 'Forum', chakra: 'Vishuddha', icon: Newspaper, bgClass: '', glow: 'rgba(60,120,255,0.5)', envKey: 'VITE_HUB_FORUM_URL', status: 'alpha' },
{ label: 'Tasks', chakra: 'Ajna', icon: ListTodo, bgClass: '', glow: 'rgba(99,80,200,0.5)', envKey: 'VITE_HUB_TASKS_URL', status: 'alpha' },
{ label: 'Castle', chakra: 'Sahasrara', icon: Castle, bgClass: '', glow: 'rgba(160,80,220,0.5)', envKey: 'VITE_HUB_CASTLE_URL', status: 'beta' },
{ label: 'Castle', chakra: 'Sahasrara', icon: Castle, bgClass: '', glow: 'rgba(160,80,220,0.5)', envKey: 'VITE_HUB_CASTLE_URL', status: 'beta', authRequired: true },
]
// Crown at top, root at bottom
const orderedModules = computed(() => [...modules].reverse())
@ -53,6 +57,8 @@ const token = computed(() => localStorage.getItem('lnbits_access_token') || '')
function hubLink(m: Module): string | null {
if (!m.envKey) return null
// Auth-only modules (wallet, chat, castle) are ghosted when not logged in.
if (m.authRequired && !isAuthenticated.value) return null
const url = import.meta.env[m.envKey] as string | undefined
if (!url) return null
if (isAuthenticated.value && token.value) {
@ -134,8 +140,8 @@ function notImplemented() {
style="padding-bottom: env(safe-area-inset-bottom)"
>
<div class="flex items-center justify-around h-14 max-w-lg mx-auto">
<!-- Profile -->
<Sheet v-model:open="showProfile">
<!-- Profile (when logged in) / Log in (when not) -->
<Sheet v-if="isAuthenticated" v-model:open="showProfile">
<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" />
@ -152,6 +158,14 @@ function notImplemented() {
</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>