feat(hub): toast "<module> requires login" on ghosted tile click

Adds active feedback to the auth-required ghosting introduced in
b80ad24. Previously a ghosted tile (wallet/chat/castle/tasks for an
unauth user) was a non-clickable <div> with no signal beyond opacity-60
+ cursor-not-allowed. Users had no way to discover *why* it was
disabled.

Now ghosted auth-required tiles render as <button>, click triggers
toast.info("<Module> requires login") with an inline "Log in" action
that pushes /login on the hub. "Coming soon" tiles (no envKey, no
authRequired) remain truly inert.

Cursor switches to pointer for ghosted-but-clickable tiles, stays
not-allowed for coming-soon tiles, so the cursor matches whether
clicking does anything.

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

View file

@ -57,7 +57,7 @@ const token = computed(() => localStorage.getItem('lnbits_access_token') || '')
function hubLink(m: Module): string | null { function hubLink(m: Module): string | null {
if (!m.envKey) return null if (!m.envKey) return null
// Auth-only modules (wallet, chat, castle) are ghosted when not logged in. // Auth-only modules (wallet, chat, castle, tasks) are ghosted when not logged in.
if (m.authRequired && !isAuthenticated.value) return null if (m.authRequired && !isAuthenticated.value) return null
const url = import.meta.env[m.envKey] as string | undefined const url = import.meta.env[m.envKey] as string | undefined
if (!url) return null if (!url) return null
@ -68,6 +68,24 @@ function hubLink(m: Module): string | null {
return url return url
} }
function isAuthGated(m: Module): boolean {
return !!(m.authRequired && !isAuthenticated.value)
}
function onTileClick(m: Module, event: Event) {
// Ghosted auth-required tiles aren't anchors; intercept and toast.
if (isAuthGated(m)) {
event.preventDefault()
toast.info(`${m.label} requires login`, {
action: {
label: 'Log in',
onClick: () => router.push('/login'),
},
})
}
// "Coming soon" tiles (no envKey, no authRequired) silently do nothing.
}
const showProfile = ref(false) const showProfile = ref(false)
function notImplemented() { function notImplemented() {
@ -107,14 +125,17 @@ function notImplemented() {
<component <component
v-for="m in orderedModules" v-for="m in orderedModules"
:key="m.label" :key="m.label"
:is="hubLink(m) ? 'a' : 'div'" :is="hubLink(m) ? 'a' : (isAuthGated(m) ? 'button' : 'div')"
:href="hubLink(m) || undefined" :href="hubLink(m) || undefined"
class="relative flex flex-col items-center justify-center gap-1 rounded-xl border backdrop-blur-sm transition-all p-2 min-h-0" class="relative flex flex-col items-center justify-center gap-1 rounded-xl border backdrop-blur-sm transition-all p-2 min-h-0"
:class="[ :class="[
hubLink(m) hubLink(m)
? 'bg-card/60 hover:bg-card/40 border-white/10 hover:border-white/25 cursor-pointer' ? 'bg-card/60 hover:bg-card/40 border-white/10 hover:border-white/25 cursor-pointer'
: 'bg-card/70 border-white/5 opacity-60 cursor-not-allowed', : isAuthGated(m)
? 'bg-card/70 hover:bg-card/55 border-white/5 opacity-60 cursor-pointer'
: 'bg-card/70 border-white/5 opacity-60 cursor-not-allowed',
]" ]"
@click="onTileClick(m, $event)"
> >
<component :is="m.icon" class="h-7 w-7 text-foreground/90" :style="{ filter: `drop-shadow(0 0 8px ${m.glow})` }" /> <component :is="m.icon" class="h-7 w-7 text-foreground/90" :style="{ filter: `drop-shadow(0 0 8px ${m.glow})` }" />
<div class="text-center leading-tight"> <div class="text-center leading-tight">