diff --git a/src/components/layout/AppShell.vue b/src/components/layout/AppShell.vue index c59b54c..20affdd 100644 --- a/src/components/layout/AppShell.vue +++ b/src/components/layout/AppShell.vue @@ -4,24 +4,23 @@ import { useRoute } from 'vue-router' import { Toaster } from '@/components/ui/sonner' import { useTheme } from '@/components/theme-provider' import BottomNav, { type BottomTab } from './BottomNav.vue' -import HubPill from './HubPill.vue' +import StandaloneMenu, { type SidebarNavItem } from './StandaloneMenu.vue' interface Props { /** App-specific tabs displayed before the constant Profile entry. */ tabs: BottomTab[] /** Active-tab matcher. Forwarded to BottomNav. */ isActive: (path: string) => boolean - /** Hide the top-right HubPill — only true when this shell is rendering - * the hub itself. Standalones leave this false (default). */ + /** Hide the top-right standalone menu — only true when this shell is + * rendering the hub itself. Standalones leave this false (default). */ hideHub?: boolean - /** Forwarded to BottomNav. Hub passes true so logged-out users can still - * reach prefs from the sheet. Standalones leave it false. */ - loggedOutOpensSheet?: boolean + /** App-specific nav items rendered at the top of the standalone menu. */ + sidebarNav?: SidebarNavItem[] } const props = withDefaults(defineProps(), { hideHub: false, - loggedOutOpensSheet: false, + sidebarNav: () => [], }) const route = useRoute() @@ -45,11 +44,13 @@ const isLoginPage = computed(() => route.path === '/login') v-if="!isLoginPage" :tabs="props.tabs" :is-active="props.isActive" - :logged-out-opens-sheet="props.loggedOutOpensSheet" /> - + - diff --git a/src/components/layout/HubPill.vue b/src/components/layout/HubPill.vue deleted file mode 100644 index dbe4f5a..0000000 --- a/src/components/layout/HubPill.vue +++ /dev/null @@ -1,24 +0,0 @@ - - - diff --git a/src/components/layout/ProfileSheetContent.vue b/src/components/layout/ProfileSheetContent.vue index d08735a..5bf0122 100644 --- a/src/components/layout/ProfileSheetContent.vue +++ b/src/components/layout/ProfileSheetContent.vue @@ -1,38 +1,122 @@ diff --git a/src/components/layout/ProfileSheetTrigger.vue b/src/components/layout/ProfileSheetTrigger.vue index 9e1b61b..88e4754 100644 --- a/src/components/layout/ProfileSheetTrigger.vue +++ b/src/components/layout/ProfileSheetTrigger.vue @@ -50,7 +50,7 @@ const open = ref(false) - + diff --git a/src/components/layout/StandaloneMenu.vue b/src/components/layout/StandaloneMenu.vue new file mode 100644 index 0000000..fa8353a --- /dev/null +++ b/src/components/layout/StandaloneMenu.vue @@ -0,0 +1,81 @@ + + + diff --git a/src/components/ui/avatar/index.ts b/src/components/ui/avatar/index.ts index 5367952..0cc8926 100644 --- a/src/components/ui/avatar/index.ts +++ b/src/components/ui/avatar/index.ts @@ -5,7 +5,7 @@ export { default as AvatarFallback } from './AvatarFallback.vue' export { default as AvatarImage } from './AvatarImage.vue' export const avatarVariant = cva( - 'inline-flex items-center justify-center font-normal text-foreground select-none shrink-0 bg-secondary overflow-hidden', + 'inline-flex items-center justify-center font-normal text-secondary-foreground select-none shrink-0 bg-secondary overflow-hidden', { variants: { size: { diff --git a/src/events-app/App.vue b/src/events-app/App.vue index 2540dad..53b8299 100644 --- a/src/events-app/App.vue +++ b/src/events-app/App.vue @@ -3,7 +3,7 @@ import { computed } from 'vue' import { useRoute, useRouter } from 'vue-router' import { useI18n } from 'vue-i18n' import { toast } from 'vue-sonner' -import { CalendarDays, Map, Heart, Search, Plus } from 'lucide-vue-next' +import { Home, Map, Heart, Ticket, Megaphone } from 'lucide-vue-next' import AppShell from '@/components/layout/AppShell.vue' import type { BottomTab } from '@/components/layout/BottomNav.vue' import { useAuth } from '@/composables/useAuthService' @@ -23,37 +23,72 @@ const eventsStore = useEventsStore() const { isAdmin, autoApprove } = useApprovalState() // Used to merge own LNbits drafts into the events feed right after // the user creates or edits an event — otherwise the new draft only -// surfaces on the next EventsPage subscribe cycle. -const { loadOwnEvents } = useEvents() +// surfaces on the next EventsPage subscribe cycle. `onlyHosting` +// is the feed filter that backs the Hosting bottom-nav tab — tapping +// it toggles the filter on; Home tab toggles it off. +const { loadOwnEvents, onlyHosting, toggleHosting } = useEvents() + +// True for /events and its sub-routes (incl. detail pages) but +// not for the routes owned by other tabs (map/favorites). Used by +// both Home and Hosting active-state predicates so the highlight +// only shifts based on the onlyHosting flag while you're in the feed. +function inFeedRoute(): boolean { + if (route.path.startsWith('/events/map')) return false + if (route.path.startsWith('/events/favorites')) return false + return route.path === '/events' || route.path.startsWith('/events/') +} -// Settings dropped — theme/lang/currency now live in the shared profile sheet. -// Create lives in the bottom nav: when logged out, tapping it shows an -// auth-prompt toast (mirroring BookmarkButton/RSVPButton) instead of -// opening the dialog. Per-app placement deliberation tracked at #53. const tabs = computed(() => [ - { name: t('events.nav.feed'), icon: Search, path: '/events' }, - { name: t('events.nav.calendar'), icon: CalendarDays, path: '/events/calendar' }, { - name: t('events.createNew'), - icon: Plus, + name: t('events.nav.feed'), + icon: Home, + onClick: () => { + // Tapping Home clears the hosting filter so the feed always + // returns to the unfiltered view, regardless of where the + // user just came from. + if (onlyHosting.value) toggleHosting() + if (route.path !== '/events') router.push('/events') + }, + isActive: () => inFeedRoute() && !onlyHosting.value, + }, + { name: t('events.nav.map'), icon: Map, path: '/events/map' }, + { + name: t('events.filters.myTickets'), + icon: Ticket, + path: '/my-tickets', onClick: () => { if (!isAuthenticated.value) { - toast.info('Log in to create an event', { + toast.info(t('events.detail.loginToBuyTickets'), { action: { - label: 'Log in', + label: t('events.detail.logIn'), onClick: () => router.push('/login'), }, }) return } - // Defensively clear any lingering edit selection so the Create - // tap always opens in Create mode regardless of a prior Edit. - eventsStore.editingEvent = null - eventsStore.showCreateDialog = true + router.push('/my-tickets') }, disabled: !isAuthenticated.value, }, - { name: t('events.nav.map'), icon: Map, path: '/events/map' }, + { + name: t('events.filters.hosting'), + icon: Megaphone, + onClick: () => { + if (!isAuthenticated.value) { + toast.info(t('events.hosting.loginPrompt', 'Log in to manage your hosted events'), { + action: { + label: t('events.favorites.logIn'), + onClick: () => router.push('/login'), + }, + }) + return + } + if (!onlyHosting.value) toggleHosting() + if (route.path !== '/events') router.push('/events') + }, + isActive: () => inFeedRoute() && onlyHosting.value, + disabled: !isAuthenticated.value, + }, { name: t('events.nav.favorites'), icon: Heart, @@ -77,18 +112,8 @@ const tabs = computed(() => [ }, ]) -// Feed tab is active for the bare /events route AND all sub-paths that -// aren't owned by another tab (e.g. /events/ detail pages). +// Path-based fallback for tabs that don't carry their own `isActive`. function isActive(path: string): boolean { - if (path === '/events') { - return ( - route.path === '/events' || - (route.path.startsWith('/events/') && - !route.path.startsWith('/events/calendar') && - !route.path.startsWith('/events/map') && - !route.path.startsWith('/events/favorites')) - ) - } return route.path.startsWith(path) } diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 52fc2e0..331c5d4 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -22,6 +22,7 @@ const messages: LocaleMessages = { profileDescription: 'Your Nostr identity and display name.', profileLoggedOutDescription: 'Sign in or change your preferences.', login: 'Log in', + menu: 'Menu', backToHub: 'Back to hub', hub: 'Hub', theme: 'Theme', @@ -68,6 +69,8 @@ const messages: LocaleMessages = { hosting: 'Hosting', pastEvents: 'Past events', past: 'Past', + filters: 'Filters', + clearAll: 'Clear all', }, categories: { concert: 'Concert', @@ -98,9 +101,6 @@ const messages: LocaleMessages = { }, detail: { getTicket: 'Get Ticket', - going: 'Going', - maybe: 'Maybe', - notGoing: 'Not Going', contactOrganizer: 'Contact Organizer', organizer: 'Organizer', location: 'Location', @@ -127,7 +127,7 @@ const messages: LocaleMessages = { registered: 'Registered', }, nav: { - feed: 'Feed', + feed: 'Home', calendar: 'Calendar', map: 'Map', favorites: 'Favorites', diff --git a/src/i18n/locales/es.ts b/src/i18n/locales/es.ts index 9e5407d..6b42a69 100644 --- a/src/i18n/locales/es.ts +++ b/src/i18n/locales/es.ts @@ -22,6 +22,7 @@ const messages: LocaleMessages = { profileDescription: 'Tu identidad Nostr y nombre de visualización.', profileLoggedOutDescription: 'Inicia sesión o cambia tus preferencias.', login: 'Iniciar sesión', + menu: 'Menú', backToHub: 'Volver al hub', hub: 'Hub', theme: 'Tema', @@ -68,6 +69,8 @@ const messages: LocaleMessages = { hosting: 'Organizo', pastEvents: 'Eventos pasados', past: 'Pasado', + filters: 'Filtros', + clearAll: 'Limpiar todo', }, categories: { concert: 'Concierto', @@ -98,9 +101,6 @@ const messages: LocaleMessages = { }, detail: { getTicket: 'Obtener boleto', - going: 'Voy', - maybe: 'Tal vez', - notGoing: 'No voy', contactOrganizer: 'Contactar organizador', organizer: 'Organizador', location: 'Ubicación', diff --git a/src/i18n/locales/fr.ts b/src/i18n/locales/fr.ts index 4c9a4f1..ff76d79 100644 --- a/src/i18n/locales/fr.ts +++ b/src/i18n/locales/fr.ts @@ -22,6 +22,7 @@ const messages: LocaleMessages = { profileDescription: 'Votre identité Nostr et votre nom d\u2019affichage.', profileLoggedOutDescription: 'Connectez-vous ou modifiez vos préférences.', login: 'Se connecter', + menu: 'Menu', backToHub: 'Retour au hub', hub: 'Hub', theme: 'Thème', @@ -68,6 +69,8 @@ const messages: LocaleMessages = { hosting: 'J\'organise', pastEvents: 'Événements passés', past: 'Passé', + filters: 'Filtres', + clearAll: 'Tout effacer', }, categories: { concert: 'Concert', @@ -98,9 +101,6 @@ const messages: LocaleMessages = { }, detail: { getTicket: 'Obtenir un billet', - going: 'Présent', - maybe: 'Peut-être', - notGoing: 'Absent', contactOrganizer: "Contacter l'organisateur", organizer: 'Organisateur', location: 'Lieu', @@ -127,7 +127,7 @@ const messages: LocaleMessages = { registered: 'Enregistré', }, nav: { - feed: 'Fil', + feed: 'Accueil', calendar: 'Calendrier', map: 'Carte', favorites: 'Favoris', diff --git a/src/i18n/types.ts b/src/i18n/types.ts index 81ef71e..63e8fc3 100644 --- a/src/i18n/types.ts +++ b/src/i18n/types.ts @@ -21,6 +21,7 @@ export interface LocaleMessages { profileDescription: string profileLoggedOutDescription: string login: string + menu: string backToHub: string hub: string theme: string @@ -69,13 +70,12 @@ export interface LocaleMessages { hosting: string pastEvents: string past: string + filters: string + clearAll: string } categories: Record detail: { getTicket: string - going: string - maybe: string - notGoing: string contactOrganizer: string organizer: string location: string diff --git a/src/modules/base/components/ProfileSettings.vue b/src/modules/base/components/ProfileSettings.vue index 1ad99ef..7a3386a 100644 --- a/src/modules/base/components/ProfileSettings.vue +++ b/src/modules/base/components/ProfileSettings.vue @@ -1,14 +1,5 @@ @@ -173,8 +121,7 @@ import { toTypedSchema } from '@vee-validate/zod' import * as z from 'zod' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' -import { Separator } from '@/components/ui/separator' -import { User, Zap, Hash } from 'lucide-vue-next' +import { User } from 'lucide-vue-next' import { FormControl, FormDescription, @@ -184,27 +131,13 @@ import { FormMessage, } from '@/components/ui/form' import ImageUpload from './ImageUpload.vue' -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from '@/components/ui/alert-dialog' -import { LogOut } from 'lucide-vue-next' import { useAuth } from '@/composables/useAuthService' -import { useRouter } from 'vue-router' import { injectService, SERVICE_TOKENS } from '@/core/di-container' import type { ImageUploadService } from '../services/ImageUploadService' import { useToast } from '@/core/composables/useToast' // Services -const { user, updateProfile, logout } = useAuth() -const router = useRouter() +const { user, updateProfile } = useAuth() const imageService = injectService(SERVICE_TOKENS.IMAGE_UPLOAD_SERVICE) const toast = useToast() @@ -224,14 +157,14 @@ const lightningDomain = computed(() => import.meta.env.VITE_LIGHTNING_DOMAIN || window.location.hostname ) -// Computed previews +// Live preview of the user's NIP-05 / Lightning address — shown in the +// username field's helper text so the consequence of a future rename is +// visible inline. const nip05Preview = computed(() => { const username = form.values.username || currentUsername.value || 'username' return `${username}@${lightningDomain.value}` }) -const lightningAddress = computed(() => nip05Preview.value) - // Form schema const profileFormSchema = toTypedSchema(z.object({ username: z.string() @@ -327,17 +260,4 @@ const updateUserProfile = async (formData: any) => { isUpdating.value = false } } - -// Log out + redirect to /login on this app's origin. -const onLogout = async () => { - try { - await logout() - toast.success('Logged out') - router.push('/login') - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Failed to log out' - console.error('Error logging out:', error) - toast.error(`Logout failed: ${errorMessage}`) - } -} diff --git a/src/modules/events/components/CreateEventDialog.vue b/src/modules/events/components/CreateEventDialog.vue index 5c9d216..3f50231 100644 --- a/src/modules/events/components/CreateEventDialog.vue +++ b/src/modules/events/components/CreateEventDialog.vue @@ -432,7 +432,7 @@ const handleOpenChange = (open: boolean) => { diff --git a/src/modules/events/components/RSVPButton.vue b/src/modules/events/components/RSVPButton.vue deleted file mode 100644 index 0cda9bf..0000000 --- a/src/modules/events/components/RSVPButton.vue +++ /dev/null @@ -1,81 +0,0 @@ - - - diff --git a/src/modules/events/components/TemporalFilterBar.vue b/src/modules/events/components/TemporalFilterBar.vue index 1b84ff9..f66ce10 100644 --- a/src/modules/events/components/TemporalFilterBar.vue +++ b/src/modules/events/components/TemporalFilterBar.vue @@ -1,14 +1,17 @@