diff --git a/src/components/layout/AppShell.vue b/src/components/layout/AppShell.vue index 20affdd..c59b54c 100644 --- a/src/components/layout/AppShell.vue +++ b/src/components/layout/AppShell.vue @@ -4,23 +4,24 @@ 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 StandaloneMenu, { type SidebarNavItem } from './StandaloneMenu.vue' +import HubPill from './HubPill.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 standalone menu — only true when this shell is - * rendering the hub itself. Standalones leave this false (default). */ + /** Hide the top-right HubPill — only true when this shell is rendering + * the hub itself. Standalones leave this false (default). */ hideHub?: boolean - /** App-specific nav items rendered at the top of the standalone menu. */ - sidebarNav?: SidebarNavItem[] + /** Forwarded to BottomNav. Hub passes true so logged-out users can still + * reach prefs from the sheet. Standalones leave it false. */ + loggedOutOpensSheet?: boolean } const props = withDefaults(defineProps(), { hideHub: false, - sidebarNav: () => [], + loggedOutOpensSheet: false, }) const route = useRoute() @@ -44,13 +45,11 @@ 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 new file mode 100644 index 0000000..dbe4f5a --- /dev/null +++ b/src/components/layout/HubPill.vue @@ -0,0 +1,24 @@ + + + diff --git a/src/components/layout/ProfileSheetContent.vue b/src/components/layout/ProfileSheetContent.vue index 5bf0122..d08735a 100644 --- a/src/components/layout/ProfileSheetContent.vue +++ b/src/components/layout/ProfileSheetContent.vue @@ -1,122 +1,38 @@ diff --git a/src/components/layout/ProfileSheetTrigger.vue b/src/components/layout/ProfileSheetTrigger.vue index 88e4754..9e1b61b 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 deleted file mode 100644 index fa8353a..0000000 --- a/src/components/layout/StandaloneMenu.vue +++ /dev/null @@ -1,81 +0,0 @@ - - - diff --git a/src/components/ui/avatar/index.ts b/src/components/ui/avatar/index.ts index 0cc8926..5367952 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-secondary-foreground select-none shrink-0 bg-secondary overflow-hidden', + 'inline-flex items-center justify-center font-normal text-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 53b8299..2540dad 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 { Home, Map, Heart, Ticket, Megaphone } from 'lucide-vue-next' +import { CalendarDays, Map, Heart, Search, Plus } 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,72 +23,37 @@ 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. `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/') -} +// surfaces on the next EventsPage subscribe cycle. +const { loadOwnEvents } = useEvents() +// 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.nav.feed'), - icon: Home, + name: t('events.createNew'), + icon: Plus, 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') + if (!isAuthenticated.value) { + toast.info('Log in to create an event', { + action: { + label: 'Log in', + 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 }, - isActive: () => inFeedRoute() && !onlyHosting.value, + disabled: !isAuthenticated.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(t('events.detail.loginToBuyTickets'), { - action: { - label: t('events.detail.logIn'), - onClick: () => router.push('/login'), - }, - }) - return - } - router.push('/my-tickets') - }, - disabled: !isAuthenticated.value, - }, - { - 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, @@ -112,8 +77,18 @@ const tabs = computed(() => [ }, ]) -// Path-based fallback for tabs that don't carry their own `isActive`. +// 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). 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 331c5d4..52fc2e0 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -22,7 +22,6 @@ 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', @@ -69,8 +68,6 @@ const messages: LocaleMessages = { hosting: 'Hosting', pastEvents: 'Past events', past: 'Past', - filters: 'Filters', - clearAll: 'Clear all', }, categories: { concert: 'Concert', @@ -101,6 +98,9 @@ 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: 'Home', + feed: 'Feed', calendar: 'Calendar', map: 'Map', favorites: 'Favorites', diff --git a/src/i18n/locales/es.ts b/src/i18n/locales/es.ts index 6b42a69..9e5407d 100644 --- a/src/i18n/locales/es.ts +++ b/src/i18n/locales/es.ts @@ -22,7 +22,6 @@ 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', @@ -69,8 +68,6 @@ const messages: LocaleMessages = { hosting: 'Organizo', pastEvents: 'Eventos pasados', past: 'Pasado', - filters: 'Filtros', - clearAll: 'Limpiar todo', }, categories: { concert: 'Concierto', @@ -101,6 +98,9 @@ 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 ff76d79..4c9a4f1 100644 --- a/src/i18n/locales/fr.ts +++ b/src/i18n/locales/fr.ts @@ -22,7 +22,6 @@ 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', @@ -69,8 +68,6 @@ const messages: LocaleMessages = { hosting: 'J\'organise', pastEvents: 'Événements passés', past: 'Passé', - filters: 'Filtres', - clearAll: 'Tout effacer', }, categories: { concert: 'Concert', @@ -101,6 +98,9 @@ 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: 'Accueil', + feed: 'Fil', calendar: 'Calendrier', map: 'Carte', favorites: 'Favoris', diff --git a/src/i18n/types.ts b/src/i18n/types.ts index 63e8fc3..81ef71e 100644 --- a/src/i18n/types.ts +++ b/src/i18n/types.ts @@ -21,7 +21,6 @@ export interface LocaleMessages { profileDescription: string profileLoggedOutDescription: string login: string - menu: string backToHub: string hub: string theme: string @@ -70,12 +69,13 @@ 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 7a3386a..1ad99ef 100644 --- a/src/modules/base/components/ProfileSettings.vue +++ b/src/modules/base/components/ProfileSettings.vue @@ -1,5 +1,14 @@ @@ -121,7 +173,8 @@ import { toTypedSchema } from '@vee-validate/zod' import * as z from 'zod' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' -import { User } from 'lucide-vue-next' +import { Separator } from '@/components/ui/separator' +import { User, Zap, Hash } from 'lucide-vue-next' import { FormControl, FormDescription, @@ -131,13 +184,27 @@ 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 } = useAuth() +const { user, updateProfile, logout } = useAuth() +const router = useRouter() const imageService = injectService(SERVICE_TOKENS.IMAGE_UPLOAD_SERVICE) const toast = useToast() @@ -157,14 +224,14 @@ const lightningDomain = computed(() => import.meta.env.VITE_LIGHTNING_DOMAIN || window.location.hostname ) -// 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. +// Computed previews 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() @@ -260,4 +327,17 @@ 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 3f50231..5c9d216 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 new file mode 100644 index 0000000..0cda9bf --- /dev/null +++ b/src/modules/events/components/RSVPButton.vue @@ -0,0 +1,81 @@ + + + diff --git a/src/modules/events/components/TemporalFilterBar.vue b/src/modules/events/components/TemporalFilterBar.vue index f66ce10..1b84ff9 100644 --- a/src/modules/events/components/TemporalFilterBar.vue +++ b/src/modules/events/components/TemporalFilterBar.vue @@ -1,17 +1,14 @@