From e822285b99d2f2831c44f7d221895063e2e4c6ca Mon Sep 17 00:00:00 2001 From: Padreug Date: Thu, 4 Jun 2026 17:32:16 +0200 Subject: [PATCH 01/47] feat(activities): restructure event detail page layout - Move bookmark heart from top bar to the right of the title. - Replace the When/Where info cards with caption-style lines directly under the title (calendar + map-pin icons + muted text). - Move description above the organizer so it sits right under the title/info separator; push the organizer card to the bottom. - Promote the "you own N tickets" CTA (filled primary "View" button) and demote "Buy another ticket" to outline when the user already owns tickets, so the My-Tickets path is what jumps out. - Tighten ticket availability against the buy button: standalone strip removed, count rendered as an xs muted caption directly under the buy CTA. --- src/modules/events/views/EventDetailPage.vue | 123 +++++++++---------- 1 file changed, 56 insertions(+), 67 deletions(-) diff --git a/src/modules/events/views/EventDetailPage.vue b/src/modules/events/views/EventDetailPage.vue index 1839775..8fedfeb 100644 --- a/src/modules/events/views/EventDetailPage.vue +++ b/src/modules/events/views/EventDetailPage.vue @@ -199,11 +199,6 @@ function goToMyTickets() { Edit - @@ -233,23 +228,23 @@ function goToMyTickets() { /> - +
-
- +
+ {{ categoryLabel }} {{ event.lnbitsStatus === 'rejected' ? 'Rejected' : 'Pending review' }} Yours @@ -257,38 +252,41 @@ function goToMyTickets() { {{ tag }}
-

- {{ event.title }} -

+
+

+ {{ event.title }} +

+ +

{{ event.summary }}

+ + +
+
+ + + {{ dateDisplay }} + ({{ event.timezone }}) + +
+
+ + {{ event.location }} +
+
- -
- -
-
- - {{ t('events.detail.when') }} -
-

{{ dateDisplay }}

-

- {{ event.timezone }} -

-
- - -
-
- - {{ t('events.detail.location') }} -
-

{{ event.location }}

-
+ +
+

{{ event.description }}

@@ -304,33 +302,20 @@ function goToMyTickets() { + tickets_* tags on the published event). When the user + already owns tickets, the "you have N tickets / view" + card is promoted (filled primary CTA) and the buy CTA + is demoted (outline). -->
-
- - - {{ t('events.detail.unlimitedTickets', 'Unlimited tickets') }} - - - {{ t('events.detail.ticketsAvailable', { count: event.ticketInfo.available }) }} - - - {{ t('events.detail.soldOut') }} - -
-
{{ t('events.detail.ticketsOwned', { count: ownedPaidCount }, ownedPaidCount) }}
- @@ -343,10 +328,11 @@ function goToMyTickets() { {{ t('events.detail.pastEvent', 'This event has already happened') }}
-
+
+

+ + {{ t('events.detail.unlimitedTickets', 'Unlimited tickets') }} + + + {{ t('events.detail.ticketsAvailable', { count: event.ticketInfo.available }) }} + +

+ +

+ +
+ + +

@@ -382,18 +383,6 @@ function goToMyTickets() {

- - - - -
-

{{ event.description }}

-
- - -
- -
From 39678126c92a651efcda4425172b7015ba6e34bc Mon Sep 17 00:00:00 2001 From: Padreug Date: Thu, 4 Jun 2026 21:51:45 +0200 Subject: [PATCH 02/47] feat(webapp): replace HubPill with hamburger sidebar menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The top-right "Back to hub" pill in each standalone is replaced by a hamburger button that opens a right-side sheet reusing the existing ProfileSheetContent (identity card, back-to-hub link, theme/lang/ currency prefs, profile settings or log-in CTA). The redundant Profile entry is removed from BottomNav and its loggedOutOpensSheet plumbing (BottomNav → AppShell) is dropped — Hub.vue still mounts ProfileSheetTrigger directly so it's unaffected. ProfileSheetContent gains an `app-nav` slot so standalones can inject app-specific nav items above the cross-app section. AppShell exposes a new optional `sidebarNav` prop that forwards items to the menu; unset on non-activities standalones, those still get the hamburger menu showing just the shared profile/preferences content. Activities passes "My tickets" (routes to /my-tickets) and "Hosting" (toggles the onlyHosting feed filter and lands on /activities), so those entries leave the inline filter chip row on ActivitiesPage and live in the sidebar instead. The "Past events" chip stays inline — it doesn't require auth and pairs visually with the temporal filters. --- src/components/layout/AppShell.vue | 19 ++--- src/components/layout/BottomNav.vue | 11 +-- src/components/layout/HubPill.vue | 24 ------ src/components/layout/ProfileSheetContent.vue | 3 + src/components/layout/StandaloneMenu.vue | 81 +++++++++++++++++++ src/events-app/App.vue | 34 +++++++- src/i18n/locales/en.ts | 1 + src/i18n/locales/es.ts | 1 + src/i18n/locales/fr.ts | 1 + src/i18n/types.ts | 1 + src/modules/events/views/EventsPage.vue | 38 ++------- 11 files changed, 135 insertions(+), 79 deletions(-) delete mode 100644 src/components/layout/HubPill.vue create mode 100644 src/components/layout/StandaloneMenu.vue 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..26667b1 100644 --- a/src/components/layout/ProfileSheetContent.vue +++ b/src/components/layout/ProfileSheetContent.vue @@ -53,6 +53,9 @@ function goLogin() { + + +
+import { ref, type Component } from 'vue' +import { useI18n } from 'vue-i18n' +import { useRouter } from 'vue-router' +import { Menu } from 'lucide-vue-next' +import { + Sheet, + SheetContent, + SheetTrigger, +} from '@/components/ui/sheet' +import { Separator } from '@/components/ui/separator' +import ProfileSheetContent from './ProfileSheetContent.vue' + +export interface SidebarNavItem { + /** Display label. */ + name: string + /** Lucide (or any) component to render as the leading icon. */ + icon: Component + /** Optional route to navigate to on click. */ + path?: string + /** Optional click handler. Runs after navigation if both are set. */ + onClick?: () => void + /** Visual-only "active" predicate for highlight state. */ + isActive?: () => boolean +} + +interface Props { + /** App-specific nav items rendered at the top of the sheet. */ + items?: SidebarNavItem[] +} + +const props = withDefaults(defineProps(), { items: () => [] }) + +const { t } = useI18n() +const router = useRouter() +const open = ref(false) + +function handleClick(item: SidebarNavItem) { + if (item.path) router.push(item.path) + item.onClick?.() + open.value = false +} + + + diff --git a/src/events-app/App.vue b/src/events-app/App.vue index 2540dad..8609648 100644 --- a/src/events-app/App.vue +++ b/src/events-app/App.vue @@ -3,9 +3,10 @@ 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 { CalendarDays, Map, Heart, Search, Plus, Ticket, Megaphone } from 'lucide-vue-next' import AppShell from '@/components/layout/AppShell.vue' import type { BottomTab } from '@/components/layout/BottomNav.vue' +import type { SidebarNavItem } from '@/components/layout/StandaloneMenu.vue' import { useAuth } from '@/composables/useAuthService' import { useEventsStore } from '@/modules/events/stores/events' import { useEvents } from '@/modules/events/composables/useEvents' @@ -24,7 +25,9 @@ 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() +// The hosting filter also lives on the events composable; the +// sidebar entry below mirrors what the old in-page chip used to do. +const { loadOwnEvents, onlyHosting, toggleHosting } = 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 @@ -77,6 +80,31 @@ const tabs = computed(() => [ }, ]) +// Sidebar entries shown to authed users only. "My tickets" routes to +// the dedicated /my-tickets page; "Hosting" toggles the feed filter +// (no dedicated page yet) and lands on /events so the user can +// see the filtered list. +const sidebarNav = computed(() => { + if (!isAuthenticated.value) return [] + return [ + { + name: t('events.filters.myTickets', 'My tickets'), + icon: Ticket, + path: '/my-tickets', + isActive: () => route.path.startsWith('/my-tickets'), + }, + { + name: t('events.filters.hosting', 'Hosting'), + icon: Megaphone, + onClick: () => { + if (!onlyHosting.value) toggleHosting() + if (!route.path.startsWith('/events')) router.push('/events') + }, + isActive: () => onlyHosting.value, + }, + ] +}) + // 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 { @@ -123,7 +151,7 @@ function handleDialogOpenChange(open: boolean) {