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.
This commit is contained in:
Padreug 2026-06-04 17:32:16 +02:00
commit 4924e70fe8

View file

@ -199,11 +199,6 @@ function goToMyTickets() {
<Pencil class="w-4 h-4" />
Edit
</Button>
<BookmarkButton
v-if="activity"
:pubkey="activity.organizer.pubkey"
:d-tag="activity.id"
/>
</div>
</div>
@ -233,23 +228,23 @@ function goToMyTickets() {
/>
</div>
<!-- Title + Category -->
<!-- Title + bookmark + captions -->
<div>
<div class="flex items-start gap-2 mb-2">
<Badge v-if="categoryLabel" variant="secondary" class="shrink-0 mt-1">
<div class="flex flex-wrap items-start gap-2 mb-2">
<Badge v-if="categoryLabel" variant="secondary" class="shrink-0">
{{ categoryLabel }}
</Badge>
<Badge
v-if="activity.lnbitsStatus && activity.lnbitsStatus !== 'approved'"
:variant="activity.lnbitsStatus === 'rejected' ? 'destructive' : 'secondary'"
class="shrink-0 mt-1 capitalize"
class="shrink-0 capitalize"
>
{{ activity.lnbitsStatus === 'rejected' ? 'Rejected' : 'Pending review' }}
</Badge>
<Badge
v-if="activity.isMine"
variant="outline"
class="shrink-0 mt-1"
class="shrink-0"
>
Yours
</Badge>
@ -257,38 +252,41 @@ function goToMyTickets() {
<Badge variant="outline" class="text-xs">{{ tag }}</Badge>
</div>
</div>
<h1 class="text-2xl sm:text-3xl font-bold text-foreground">
{{ activity.title }}
</h1>
<div class="flex items-start justify-between gap-3">
<h1 class="text-2xl sm:text-3xl font-bold text-foreground">
{{ activity.title }}
</h1>
<BookmarkButton
:pubkey="activity.organizer.pubkey"
:d-tag="activity.id"
class="shrink-0 mt-1"
/>
</div>
<p v-if="activity.summary" class="text-muted-foreground mt-2">
{{ activity.summary }}
</p>
<!-- When + Where captions -->
<div class="mt-3 space-y-1 text-sm text-muted-foreground">
<div class="flex items-start gap-1.5">
<Calendar class="w-4 h-4 shrink-0 mt-0.5" />
<span>
{{ dateDisplay }}
<span v-if="activity.timezone" class="opacity-70">({{ activity.timezone }})</span>
</span>
</div>
<div v-if="activity.location" class="flex items-start gap-1.5">
<MapPin class="w-4 h-4 shrink-0 mt-0.5" />
<span>{{ activity.location }}</span>
</div>
</div>
</div>
<Separator />
<!-- Info section -->
<div class="grid gap-4 sm:grid-cols-2">
<!-- When -->
<div class="bg-muted/50 rounded-lg p-4 space-y-1">
<div class="flex items-center gap-2 text-sm font-medium text-foreground">
<Calendar class="w-4 h-4" />
{{ t('activities.detail.when') }}
</div>
<p class="text-sm text-muted-foreground">{{ dateDisplay }}</p>
<p v-if="activity.timezone" class="text-xs text-muted-foreground/70">
{{ activity.timezone }}
</p>
</div>
<!-- Where -->
<div v-if="activity.location" class="bg-muted/50 rounded-lg p-4 space-y-1">
<div class="flex items-center gap-2 text-sm font-medium text-foreground">
<MapPin class="w-4 h-4" />
{{ t('activities.detail.location') }}
</div>
<p class="text-sm text-muted-foreground">{{ activity.location }}</p>
</div>
<!-- Description -->
<div class="prose prose-sm max-w-none text-foreground">
<p class="whitespace-pre-wrap">{{ activity.description }}</p>
</div>
<!-- RSVP -->
@ -304,33 +302,20 @@ function goToMyTickets() {
<!-- Tickets gated on the activity carrying ticketInfo (set
by the calendarActivity converter from the AIO custom
tickets_* tags on the published event). Sections render
bottom-up: availability count, then existing owned
tickets (when count > 0) above a Purchase CTA (when
capacity remains). -->
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). -->
<div v-if="activity.ticketInfo" class="space-y-3">
<div class="flex items-center justify-center gap-1.5 text-sm text-muted-foreground">
<Ticket class="w-4 h-4 shrink-0" />
<span v-if="activity.ticketInfo.available === undefined">
{{ t('activities.detail.unlimitedTickets', 'Unlimited tickets') }}
</span>
<span v-else-if="activity.ticketInfo.available > 0">
{{ t('activities.detail.ticketsAvailable', { count: activity.ticketInfo.available }) }}
</span>
<span v-else class="text-destructive font-medium">
{{ t('activities.detail.soldOut') }}
</span>
</div>
<div
v-if="ownedPaidCount > 0"
class="bg-primary/10 border border-primary/30 rounded-lg p-4 flex items-center justify-between gap-3"
class="bg-primary/15 border border-primary/40 rounded-lg p-4 flex items-center justify-between gap-3"
>
<div class="flex items-center gap-2 text-sm font-medium text-foreground">
<CheckCircle2 class="w-4 h-4 text-primary shrink-0" />
{{ t('activities.detail.ticketsOwned', { count: ownedPaidCount }, ownedPaidCount) }}
</div>
<Button variant="outline" size="sm" class="gap-1.5 shrink-0" @click="goToMyTickets">
<Button size="sm" class="gap-1.5 shrink-0" @click="goToMyTickets">
<Ticket class="w-4 h-4" />
{{ t('activities.detail.viewMyTickets', 'View in My Tickets') }}
</Button>
@ -343,10 +328,11 @@ function goToMyTickets() {
<History class="w-4 h-4 shrink-0" />
{{ t('activities.detail.pastEvent', 'This event has already happened') }}
</div>
<div v-else-if="canBuyTicket">
<div v-else-if="canBuyTicket" class="space-y-1">
<Button
class="w-full gap-1.5"
size="lg"
:variant="ownedPaidCount > 0 ? 'outline' : 'default'"
@click="openPurchaseDialog"
>
<Ticket class="w-4 h-4" />
@ -359,6 +345,14 @@ function goToMyTickets() {
: `${activity.ticketInfo.price} ${activity.ticketInfo.currency}` }}
</span>
</Button>
<p class="text-xs text-muted-foreground text-center">
<span v-if="activity.ticketInfo.available === undefined">
{{ t('activities.detail.unlimitedTickets', 'Unlimited tickets') }}
</span>
<span v-else>
{{ t('activities.detail.ticketsAvailable', { count: activity.ticketInfo.available }) }}
</span>
</p>
</div>
<p
v-else-if="ownedPaidCount === 0"
@ -375,6 +369,13 @@ function goToMyTickets() {
@update:is-open="showPurchaseDialog = $event"
/>
<!-- External references -->
<div v-if="activity.tags.length > 0" class="space-y-2">
<!-- References would go here in future phases -->
</div>
<Separator />
<!-- Organizer -->
<div class="bg-muted/50 rounded-lg p-4">
<p class="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-2">
@ -382,18 +383,6 @@ function goToMyTickets() {
</p>
<OrganizerCard :pubkey="activity.organizer.pubkey" />
</div>
<Separator />
<!-- Description -->
<div class="prose prose-sm max-w-none text-foreground">
<p class="whitespace-pre-wrap">{{ activity.description }}</p>
</div>
<!-- External references -->
<div v-if="activity.tags.length > 0" class="space-y-2">
<!-- References would go here in future phases -->
</div>
</div>
</div>
</template>