feat(activities): tailor Hosting tab + host detail view for operators
Hosting feed (ActivitiesPage when onlyHosting): - Hide the date picker strip + calendar shortcut and the entire Filters/temporal-pills row; an operator managing their roster doesn't need calendar navigation or temporal narrowing. - Keep the search bar — finding a specific event in a long roster still matters. - Render compact cards via a new `compact` prop on ActivityList + ActivityCard: no hero image, single-line title, no summary, no bookmark, no "Yours" badge (every card is the operator's own), tighter p-3 padding, single-column flex layout. Host detail view (ActivityDetailPage when ownedLnbitsEvent): - Drop the top-bar Scan and Edit buttons. Edit moves into the title row as a prominent filled-primary icon button right of the title; Scan moves into the tickets section. - Render a full-width "Scan tickets" CTA in place of Buy ticket, and hoist it outside the ticketInfo gate so it appears even on hosted events that were published without AIO ticket tags. - Hide BookmarkButton and RSVPButton for the host (favoriting / RSVPing your own event are noise affordances). Detail-page badge row: "Yours" leads the row in the highlighted secondary variant; category and tags drop to outline so the ownership signal stands out.
This commit is contained in:
parent
9a16c2c092
commit
537dd9c588
4 changed files with 111 additions and 69 deletions
|
|
@ -12,6 +12,10 @@ import type { Event } from '../types/event'
|
|||
|
||||
const props = defineProps<{
|
||||
event: Event
|
||||
/** Render a compact row: no hero image, no summary, single-line
|
||||
* title, tighter padding. Used by the Hosting view where the
|
||||
* host already knows what their events look like. */
|
||||
compact?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
|
@ -65,12 +69,12 @@ const isPast = computed(() => {
|
|||
class="overflow-hidden cursor-pointer hover:shadow-lg transition-shadow duration-200 flex flex-col"
|
||||
@click="emit('click', event)"
|
||||
>
|
||||
<!-- Image with overlaid badges. Cards without an image skip the
|
||||
hero area entirely and surface their badges inline at the top
|
||||
of the content block — the solid-color placeholder + calendar
|
||||
glyph wasn't communicating anything the title + details don't
|
||||
already. -->
|
||||
<div v-if="event.image" class="relative aspect-[16/9] overflow-hidden">
|
||||
<!-- Image with overlaid badges. Cards without an image (or in
|
||||
compact mode) skip the hero area entirely and surface their
|
||||
badges inline at the top of the content block — the solid-
|
||||
color placeholder + calendar glyph wasn't communicating
|
||||
anything the title + details don't already. -->
|
||||
<div v-if="event.image && !compact" class="relative aspect-[16/9] overflow-hidden">
|
||||
<img
|
||||
:src="event.image"
|
||||
:alt="event.title"
|
||||
|
|
@ -135,18 +139,21 @@ const isPast = computed(() => {
|
|||
</Badge>
|
||||
</div>
|
||||
|
||||
<CardContent class="p-4 flex-1 flex flex-col gap-2">
|
||||
<!-- Inline badge row (no-image variant). Same badges as the
|
||||
image-overlay set, just stacked horizontally at the top of
|
||||
the content area. -->
|
||||
<div v-if="!event.image" class="flex flex-wrap items-center gap-1.5">
|
||||
<CardContent
|
||||
:class="compact ? 'p-3 flex-1 flex flex-col gap-1.5' : 'p-4 flex-1 flex flex-col gap-2'"
|
||||
>
|
||||
<!-- Inline badge row (no-image variant + compact variant). Same
|
||||
badges as the image-overlay set, stacked horizontally at the
|
||||
top of the content area. The "Yours" chip is dropped in
|
||||
compact mode since every card in the hosting view is owned. -->
|
||||
<div v-if="!event.image || compact" class="flex flex-wrap items-center gap-1.5">
|
||||
<Badge v-if="categoryLabel" variant="secondary" class="text-xs">
|
||||
{{ categoryLabel }}
|
||||
</Badge>
|
||||
<Badge v-if="priceDisplay" class="text-xs">
|
||||
{{ priceDisplay }}
|
||||
</Badge>
|
||||
<Badge v-if="event.isMine" variant="outline" class="text-xs gap-1">
|
||||
<Badge v-if="event.isMine && !compact" variant="outline" class="text-xs gap-1">
|
||||
<User class="w-3 h-3" />
|
||||
Yours
|
||||
</Badge>
|
||||
|
|
@ -167,26 +174,34 @@ const isPast = computed(() => {
|
|||
</Badge>
|
||||
</div>
|
||||
|
||||
<!-- Title + Bookmark -->
|
||||
<!-- Title + Bookmark. Compact mode hides the bookmark (host's
|
||||
own event — bookmarking it would be noise) and clamps the
|
||||
title to a single line. -->
|
||||
<div class="flex items-start gap-1">
|
||||
<h3 class="font-semibold text-foreground line-clamp-2 leading-tight flex-1">
|
||||
<h3
|
||||
:class="[
|
||||
'font-semibold text-foreground leading-tight flex-1',
|
||||
compact ? 'text-sm line-clamp-1' : 'line-clamp-2',
|
||||
]"
|
||||
>
|
||||
{{ event.title }}
|
||||
</h3>
|
||||
<BookmarkButton
|
||||
v-if="!compact"
|
||||
:pubkey="event.organizer.pubkey"
|
||||
:d-tag="event.id"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Summary -->
|
||||
<!-- Summary (hidden in compact mode) -->
|
||||
<p
|
||||
v-if="event.summary"
|
||||
v-if="event.summary && !compact"
|
||||
class="text-sm text-muted-foreground line-clamp-2"
|
||||
>
|
||||
{{ event.summary }}
|
||||
</p>
|
||||
|
||||
<div class="mt-auto space-y-1.5 pt-2">
|
||||
<div :class="compact ? 'space-y-1 text-xs' : 'mt-auto space-y-1.5 pt-2'">
|
||||
<!-- Date/Time -->
|
||||
<div class="flex items-center gap-1.5 text-sm text-muted-foreground">
|
||||
<Calendar class="w-3.5 h-3.5 shrink-0" />
|
||||
|
|
|
|||
|
|
@ -7,6 +7,10 @@ import type { Event } from '../types/event'
|
|||
defineProps<{
|
||||
events: Event[]
|
||||
isLoading?: boolean
|
||||
/** Render compact rows instead of full-image cards. Used by the
|
||||
* Hosting view so an operator can scan their roster of events
|
||||
* without the visual weight of hero images they already recognize. */
|
||||
compact?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
|
@ -47,12 +51,17 @@ const { t } = useI18n()
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Event grid -->
|
||||
<div v-else class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<!-- Event grid — compact mode collapses to a single column of
|
||||
tight rows; default mode is the responsive card grid. -->
|
||||
<div
|
||||
v-else
|
||||
:class="compact ? 'flex flex-col gap-2' : 'grid gap-4 sm:grid-cols-2 lg:grid-cols-3'"
|
||||
>
|
||||
<EventCard
|
||||
v-for="event in events"
|
||||
:key="event.nostrEventId"
|
||||
:event="event"
|
||||
:compact="compact"
|
||||
@click="emit('select', event)"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -170,36 +170,14 @@ function goToMyTickets() {
|
|||
|
||||
<template>
|
||||
<div class="container mx-auto py-6 px-4 max-w-3xl">
|
||||
<!-- Top bar -->
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<!-- Top bar — back-link only. Edit moves into the title row as a
|
||||
prominent icon button; Scan moves into the tickets section
|
||||
where it replaces the Buy-ticket CTA for the host. -->
|
||||
<div class="flex items-center mb-4">
|
||||
<Button variant="ghost" size="sm" class="gap-1.5" @click="goBack">
|
||||
<ArrowLeft class="w-4 h-4" />
|
||||
Back
|
||||
</Button>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<Button
|
||||
v-if="ownedLnbitsEvent"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="gap-1.5"
|
||||
@click="openScannerPage"
|
||||
aria-label="Scan tickets"
|
||||
>
|
||||
<ScanLine class="w-4 h-4" />
|
||||
Scan
|
||||
</Button>
|
||||
<Button
|
||||
v-if="ownedLnbitsEvent"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="gap-1.5"
|
||||
@click="openEditDialog"
|
||||
aria-label="Edit event"
|
||||
>
|
||||
<Pencil class="w-4 h-4" />
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading -->
|
||||
|
|
@ -231,7 +209,17 @@ function goToMyTickets() {
|
|||
<!-- Title + bookmark + captions -->
|
||||
<div>
|
||||
<div class="flex flex-wrap items-start gap-2 mb-2">
|
||||
<Badge v-if="categoryLabel" variant="secondary" class="shrink-0">
|
||||
<!-- "Yours" leads the row in the highlighted variant so the
|
||||
ownership signal stands out against the neutral
|
||||
category/tag chips that follow. -->
|
||||
<Badge
|
||||
v-if="event.isMine"
|
||||
variant="secondary"
|
||||
class="shrink-0"
|
||||
>
|
||||
Yours
|
||||
</Badge>
|
||||
<Badge v-if="categoryLabel" variant="outline" class="shrink-0">
|
||||
{{ categoryLabel }}
|
||||
</Badge>
|
||||
<Badge
|
||||
|
|
@ -241,13 +229,6 @@ function goToMyTickets() {
|
|||
>
|
||||
{{ event.lnbitsStatus === 'rejected' ? 'Rejected' : 'Pending review' }}
|
||||
</Badge>
|
||||
<Badge
|
||||
v-if="event.isMine"
|
||||
variant="outline"
|
||||
class="shrink-0"
|
||||
>
|
||||
Yours
|
||||
</Badge>
|
||||
<div v-for="tag in event.tags.slice(1)" :key="tag">
|
||||
<Badge variant="outline" class="text-xs">{{ tag }}</Badge>
|
||||
</div>
|
||||
|
|
@ -256,11 +237,26 @@ function goToMyTickets() {
|
|||
<h1 class="text-2xl sm:text-3xl font-bold text-foreground">
|
||||
{{ event.title }}
|
||||
</h1>
|
||||
<BookmarkButton
|
||||
:pubkey="event.organizer.pubkey"
|
||||
:d-tag="event.id"
|
||||
class="shrink-0 mt-1"
|
||||
/>
|
||||
<div class="flex items-center gap-1 shrink-0 mt-1">
|
||||
<Button
|
||||
v-if="ownedLnbitsEvent"
|
||||
variant="default"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
:aria-label="t('events.detail.editEvent', 'Edit event')"
|
||||
@click="openEditDialog"
|
||||
>
|
||||
<Pencil class="w-4 h-4" />
|
||||
</Button>
|
||||
<!-- Hosts don't need to favorite their own event — the
|
||||
"Yours" badge already marks it, and the bookmark
|
||||
affordance is meant for discovery, not management. -->
|
||||
<BookmarkButton
|
||||
v-else
|
||||
:pubkey="event.organizer.pubkey"
|
||||
:d-tag="event.id"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="event.summary" class="text-muted-foreground mt-2">
|
||||
{{ event.summary }}
|
||||
|
|
@ -289,24 +285,40 @@ function goToMyTickets() {
|
|||
<p class="whitespace-pre-wrap">{{ event.description }}</p>
|
||||
</div>
|
||||
|
||||
<!-- RSVP -->
|
||||
<!-- RSVP — hidden for the host since RSVPing to your own event
|
||||
is a noise affordance. -->
|
||||
<!-- The NIP-52 RSVP `a` tag must reference the event's actual kind
|
||||
(31922 for date-based, 31923 for time-based). Without this prop the
|
||||
button would default to time-based for every event, leaving RSVPs
|
||||
on date-based events pointing at a non-existent event coord. -->
|
||||
<RSVPButton
|
||||
v-if="!ownedLnbitsEvent"
|
||||
:pubkey="event.organizer.pubkey"
|
||||
:d-tag="event.id"
|
||||
:kind="event.type === 'date' ? NIP52_KINDS.CALENDAR_DATE_EVENT : NIP52_KINDS.CALENDAR_TIME_EVENT"
|
||||
/>
|
||||
|
||||
<!-- Host's primary CTA is to scan tickets at the door. Lives
|
||||
OUTSIDE the ticketInfo gate so it appears even when the
|
||||
event was published without AIO ticket tags — a host always
|
||||
gets to scan attempts. Stays available for past events too
|
||||
so the host can still verify attendance after the fact. -->
|
||||
<Button
|
||||
v-if="ownedLnbitsEvent"
|
||||
class="w-full gap-1.5"
|
||||
size="lg"
|
||||
@click="openScannerPage"
|
||||
>
|
||||
<ScanLine class="w-4 h-4" />
|
||||
{{ t('events.detail.scanTickets', 'Scan tickets') }}
|
||||
</Button>
|
||||
|
||||
<!-- Tickets — gated on the event carrying ticketInfo (set
|
||||
by the calendar→Event converter from the AIO custom
|
||||
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="event.ticketInfo" class="space-y-3">
|
||||
tickets_* tags on the published event). Skipped for the
|
||||
host entirely — they have the Scan CTA above and don't
|
||||
need a Buy CTA for their own event. -->
|
||||
<div v-if="event.ticketInfo && !ownedLnbitsEvent" class="space-y-3">
|
||||
<div
|
||||
v-if="ownedPaidCount > 0"
|
||||
class="bg-primary/15 border border-primary/40 rounded-lg p-4 flex items-center justify-between gap-3"
|
||||
|
|
|
|||
|
|
@ -90,8 +90,10 @@ function openCalendar() {
|
|||
|
||||
<!-- Date picker strip + calendar shortcut. The calendar icon used
|
||||
to be a bottom-nav tab; it now lives on the right of the week
|
||||
strip so the tabs row stays focused on the primary views. -->
|
||||
<div class="mb-3 flex items-center gap-2">
|
||||
strip so the tabs row stays focused on the primary views.
|
||||
Hidden in the Hosting view — operators don't need calendar
|
||||
navigation when they're managing their own roster. -->
|
||||
<div v-if="!onlyHosting" class="mb-3 flex items-center gap-2">
|
||||
<DatePickerStrip
|
||||
class="flex-1 min-w-0"
|
||||
:selected-date="selectedDate"
|
||||
|
|
@ -112,8 +114,9 @@ function openCalendar() {
|
|||
column; only the temporal pills scroll horizontally. The
|
||||
Filters icon (with a count badge when past-events or any
|
||||
categories are active) opens a collapsible that hosts the
|
||||
past-events toggle + category chips below. -->
|
||||
<Collapsible v-model:open="filtersOpen" class="mb-3">
|
||||
past-events toggle + category chips below. Hidden in the
|
||||
Hosting view — the operator's roster doesn't need them. -->
|
||||
<Collapsible v-if="!onlyHosting" v-model:open="filtersOpen" class="mb-3">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="shrink-0 flex flex-col items-center gap-0.5">
|
||||
<CollapsibleTrigger as-child>
|
||||
|
|
@ -185,10 +188,13 @@ function openCalendar() {
|
|||
{{ error }}
|
||||
</div>
|
||||
|
||||
<!-- Event feed -->
|
||||
<!-- Event feed. The Hosting view renders compact rows so the
|
||||
operator can scan their roster without the visual weight of
|
||||
hero images they already recognize. -->
|
||||
<EventList
|
||||
:events="events"
|
||||
:is-loading="isLoading"
|
||||
:compact="onlyHosting"
|
||||
@select="handleSelectEvent"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue