chore(visual): Phase 4 audit polish on event/series surface
Migrates event/series UI from Tailwind/Nuxt UI form components to the zine pattern (dashed borders, CSS-var palette, native inputs). Restructures single-event and series detail/index pages to the two-column grid pattern matching about.vue and member/dashboard.vue. Touches: - app/components/EventSeriesTicketCard.vue — phantom-palette → CSS-var migration (--candle, --ember, --surface), color="gray" → "neutral" - app/components/EventTicketCard.vue — --candle-faint border var - app/components/EventTicketPurchase.vue — accent-color: var(--candle) - app/pages/events/[slug].vue — page-fill flex chain, .event-body grid - app/pages/events/index.vue — series link uses series.id (was _id) - app/pages/series/[id].vue — two-column layout (1fr/280px) + sidebar - app/pages/series/index.vue — full rewrite to dashed-border zine pattern Per docs/specs/events-visual-audit-findings.md Phase 4. Behavior unchanged.
This commit is contained in:
parent
8f0648de57
commit
0f2f1d1cbf
7 changed files with 376 additions and 337 deletions
|
|
@ -1,37 +1,27 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div class="series-ticket-card" style="border: 1px solid var(--border); overflow: hidden">
|
||||||
class="series-ticket-card border border-guild-600 dark:border-guild-600 rounded-xl overflow-hidden"
|
|
||||||
>
|
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div
|
<div class="p-6" style="background: var(--candle); color: var(--parch-text)">
|
||||||
class="bg-gradient-to-br from-candlelight-500 to-candlelight-700 dark:from-candlelight-600 dark:to-candlelight-800 p-6"
|
|
||||||
>
|
|
||||||
<div class="flex items-start justify-between gap-4">
|
<div class="flex items-start justify-between gap-4">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="flex items-center gap-2 mb-2">
|
<div class="flex items-center gap-2 mb-2">
|
||||||
<Icon
|
<Icon name="heroicons:ticket" class="w-5 h-5" style="color: var(--parch-text)" />
|
||||||
name="heroicons:ticket"
|
<span class="text-sm font-semibold" style="color: var(--parch-text)">
|
||||||
class="w-5 h-5 text-candlelight-900 dark:text-candlelight-200"
|
|
||||||
/>
|
|
||||||
<span class="text-sm font-semibold text-candlelight-900 dark:text-candlelight-200">
|
|
||||||
Series Pass
|
Series Pass
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-xl font-bold text-white mb-1">
|
<h3 class="font-display text-xl font-bold mb-1" style="color: var(--parch-text)">
|
||||||
{{ ticket.name }}
|
{{ ticket.name }}
|
||||||
</h3>
|
</h3>
|
||||||
<p v-if="ticket.description" class="text-sm text-candlelight-900 dark:text-candlelight-200">
|
<p v-if="ticket.description" class="text-sm" style="color: var(--parch-text); opacity: 0.85">
|
||||||
{{ ticket.description }}
|
{{ ticket.description }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-right flex-shrink-0">
|
<div class="text-right flex-shrink-0">
|
||||||
<div class="text-3xl font-bold text-white text-ui-mono">
|
<div class="text-3xl font-bold" style="color: var(--parch-text)">
|
||||||
{{ formatPrice(ticket.price, ticket.currency) }}
|
{{ formatPrice(ticket.price, ticket.currency) }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div v-if="ticket.isEarlyBird" class="text-xs mt-1" style="color: var(--parch-text); opacity: 0.85">
|
||||||
v-if="ticket.isEarlyBird"
|
|
||||||
class="text-xs text-candlelight-900 dark:text-candlelight-200 mt-1"
|
|
||||||
>
|
|
||||||
Early Bird Price
|
Early Bird Price
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -39,29 +29,23 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Body -->
|
<!-- Body -->
|
||||||
<div class="p-6 bg-guild-800/50 dark:bg-guild-700/30">
|
<div class="p-6" style="background: var(--surface)">
|
||||||
<!-- What's Included -->
|
<!-- What's Included -->
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<h4 class="text-sm font-semibold text-guild-200 dark:text-guild-200 mb-3 uppercase tracking-wide">
|
<h4 class="text-sm font-semibold mb-3 uppercase tracking-wide" style="color: var(--text-faint)">
|
||||||
What's Included
|
What's Included
|
||||||
</h4>
|
</h4>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div class="flex items-center gap-2 text-guild-300 dark:text-guild-300">
|
<div class="flex items-center gap-2" style="color: var(--text)">
|
||||||
<Icon name="heroicons:check-circle" class="w-5 h-5 text-candlelight-400" />
|
<Icon name="heroicons:check-circle" class="w-5 h-5" style="color: var(--candle)" />
|
||||||
<span>Access to all {{ totalEvents }} events in the series</span>
|
<span>Access to all {{ totalEvents }} events in the series</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div v-if="ticket.isFree && !isMember" class="flex items-center gap-2" style="color: var(--text)">
|
||||||
v-if="ticket.isFree && !isMember"
|
<Icon name="heroicons:check-circle" class="w-5 h-5" style="color: var(--candle)" />
|
||||||
class="flex items-center gap-2 text-guild-300 dark:text-guild-300"
|
|
||||||
>
|
|
||||||
<Icon name="heroicons:check-circle" class="w-5 h-5 text-candlelight-400" />
|
|
||||||
<span>Automatic registration for all sessions</span>
|
<span>Automatic registration for all sessions</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div v-if="memberSavings > 0" class="flex items-center gap-2" style="color: var(--text)">
|
||||||
v-if="memberSavings > 0"
|
<Icon name="heroicons:check-circle" class="w-5 h-5" style="color: var(--candle)" />
|
||||||
class="flex items-center gap-2 text-guild-300 dark:text-guild-300"
|
|
||||||
>
|
|
||||||
<Icon name="heroicons:check-circle" class="w-5 h-5 text-candlelight-400" />
|
|
||||||
<span>Save {{ formatPrice(memberSavings, ticket.currency) }} as a member</span>
|
<span>Save {{ formatPrice(memberSavings, ticket.currency) }} as a member</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -69,33 +53,31 @@
|
||||||
|
|
||||||
<!-- Events List Preview -->
|
<!-- Events List Preview -->
|
||||||
<div v-if="events && events.length > 0" class="mb-6">
|
<div v-if="events && events.length > 0" class="mb-6">
|
||||||
<h4 class="text-sm font-semibold text-guild-200 dark:text-guild-200 mb-3 uppercase tracking-wide">
|
<h4 class="text-sm font-semibold mb-3 uppercase tracking-wide" style="color: var(--text-faint)">
|
||||||
Series Schedule
|
Series Schedule
|
||||||
</h4>
|
</h4>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div
|
<div
|
||||||
v-for="(event, index) in events.slice(0, 3)"
|
v-for="(event, index) in events.slice(0, 3)"
|
||||||
:key="event.id"
|
:key="event.id"
|
||||||
class="flex items-start gap-3 p-3 bg-guild-700/50 dark:bg-guild-600/30 rounded-lg"
|
class="flex items-start gap-3 p-3"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="w-8 h-8 rounded-full bg-candlelight-600/20 border border-candlelight-500/30 flex items-center justify-center flex-shrink-0"
|
class="w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0"
|
||||||
|
style="background: color-mix(in srgb, var(--candle) 15%, transparent); border: 1px solid var(--candle)"
|
||||||
>
|
>
|
||||||
<span class="text-sm font-bold text-candlelight-300">{{ index + 1 }}</span>
|
<span class="text-sm font-bold" style="color: var(--candle)">{{ index + 1 }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div class="font-medium text-guild-100 dark:text-guild-100 text-sm">
|
<div class="font-medium text-sm" style="color: var(--text)">
|
||||||
{{ event.title }}
|
{{ event.title }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs text-guild-400 dark:text-guild-400 mt-1">
|
<div class="text-xs mt-1" style="color: var(--text-faint)">
|
||||||
{{ formatEventDate(event.startDate) }}
|
{{ formatEventDate(event.startDate) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div v-if="events.length > 3" class="text-center text-sm pt-2" style="color: var(--text-faint)">
|
||||||
v-if="events.length > 3"
|
|
||||||
class="text-center text-sm text-guild-400 dark:text-guild-400 pt-2"
|
|
||||||
>
|
|
||||||
+ {{ events.length - 3 }} more event{{ events.length - 3 !== 1 ? 's' : '' }}
|
+ {{ events.length - 3 }} more event{{ events.length - 3 !== 1 ? 's' : '' }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -104,13 +86,14 @@
|
||||||
<!-- Member Benefit Callout -->
|
<!-- Member Benefit Callout -->
|
||||||
<div
|
<div
|
||||||
v-if="ticket.isFree && isMember"
|
v-if="ticket.isFree && isMember"
|
||||||
class="p-4 bg-candlelight-900/20 border border-candlelight-700/30 rounded-lg mb-6"
|
class="p-4 mb-6"
|
||||||
|
style="background: color-mix(in srgb, var(--candle) 15%, transparent); border: 1px solid var(--candle)"
|
||||||
>
|
>
|
||||||
<div class="flex items-start gap-3">
|
<div class="flex items-start gap-3">
|
||||||
<Icon name="heroicons:sparkles" class="w-5 h-5 text-candlelight-400 flex-shrink-0 mt-0.5" />
|
<Icon name="heroicons:sparkles" class="w-5 h-5 flex-shrink-0 mt-0.5" style="color: var(--candle)" />
|
||||||
<div>
|
<div>
|
||||||
<div class="font-semibold text-candlelight-300 mb-1">Member Benefit</div>
|
<div class="font-semibold mb-1" style="color: var(--candle)">Member Benefit</div>
|
||||||
<div class="text-sm text-candlelight-400">
|
<div class="text-sm" style="color: var(--candle)">
|
||||||
This series pass is free for Ghost Guild members! Complete registration to secure your spot.
|
This series pass is free for Ghost Guild members! Complete registration to secure your spot.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -120,13 +103,14 @@
|
||||||
<!-- Public vs Member Pricing -->
|
<!-- Public vs Member Pricing -->
|
||||||
<div
|
<div
|
||||||
v-if="!ticket.isFree && publicPrice && ticket.type === 'member'"
|
v-if="!ticket.isFree && publicPrice && ticket.type === 'member'"
|
||||||
class="p-4 bg-candlelight-900/20 border border-candlelight-700/30 rounded-lg mb-6"
|
class="p-4 mb-6"
|
||||||
|
style="background: color-mix(in srgb, var(--candle) 15%, transparent); border: 1px solid var(--candle)"
|
||||||
>
|
>
|
||||||
<div class="flex items-start gap-3">
|
<div class="flex items-start gap-3">
|
||||||
<Icon name="heroicons:tag" class="w-5 h-5 text-candlelight-400 flex-shrink-0 mt-0.5" />
|
<Icon name="heroicons:tag" class="w-5 h-5 flex-shrink-0 mt-0.5" style="color: var(--candle)" />
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="font-semibold text-candlelight-300 mb-1">Member Savings</div>
|
<div class="font-semibold mb-1" style="color: var(--candle)">Member Savings</div>
|
||||||
<div class="text-sm text-candlelight-400">
|
<div class="text-sm" style="color: var(--candle)">
|
||||||
You're saving {{ formatPrice(memberSavings, ticket.currency) }} as a member.
|
You're saving {{ formatPrice(memberSavings, ticket.currency) }} as a member.
|
||||||
Public price: {{ formatPrice(publicPrice, ticket.currency) }}
|
Public price: {{ formatPrice(publicPrice, ticket.currency) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -136,22 +120,15 @@
|
||||||
|
|
||||||
<!-- Availability -->
|
<!-- Availability -->
|
||||||
<div v-if="availability" class="mb-6">
|
<div v-if="availability" class="mb-6">
|
||||||
<div
|
<div v-if="!availability.unlimited && availability.remaining !== null" class="flex items-center gap-2">
|
||||||
v-if="!availability.unlimited && availability.remaining !== null"
|
|
||||||
class="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<Icon
|
<Icon
|
||||||
:name="availability.remaining > 5 ? 'heroicons:check-circle' : 'heroicons:exclamation-triangle'"
|
:name="availability.remaining > 5 ? 'heroicons:check-circle' : 'heroicons:exclamation-triangle'"
|
||||||
:class="[
|
class="w-5 h-5"
|
||||||
'w-5 h-5',
|
:style="{ color: availability.remaining > 5 ? 'var(--candle)' : 'var(--ember)' }"
|
||||||
availability.remaining > 5 ? 'text-candlelight-400' : 'text-ember-400'
|
|
||||||
]"
|
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
:class="[
|
class="text-sm font-medium"
|
||||||
'text-sm font-medium',
|
:style="{ color: availability.remaining > 5 ? 'var(--candle)' : 'var(--ember)' }"
|
||||||
availability.remaining > 5 ? 'text-candlelight-300' : 'text-ember-300'
|
|
||||||
]"
|
|
||||||
>
|
>
|
||||||
{{ availability.remaining }} series pass{{ availability.remaining !== 1 ? 'es' : '' }} remaining
|
{{ availability.remaining }} series pass{{ availability.remaining !== 1 ? 'es' : '' }} remaining
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -160,12 +137,12 @@
|
||||||
|
|
||||||
<!-- Sold Out / Waitlist -->
|
<!-- Sold Out / Waitlist -->
|
||||||
<div v-if="!available" class="space-y-3">
|
<div v-if="!available" class="space-y-3">
|
||||||
<div class="p-4 bg-ember-900/20 border border-ember-700/30 rounded-lg">
|
<div class="p-4" style="background: var(--ember-bg); border: 1px solid var(--ember)">
|
||||||
<div class="flex items-start gap-3">
|
<div class="flex items-start gap-3">
|
||||||
<Icon name="heroicons:exclamation-circle" class="w-5 h-5 text-ember-400 flex-shrink-0 mt-0.5" />
|
<Icon name="heroicons:exclamation-circle" class="w-5 h-5 flex-shrink-0 mt-0.5" style="color: var(--ember)" />
|
||||||
<div>
|
<div>
|
||||||
<div class="font-semibold text-ember-300 mb-1">Series Pass Sold Out</div>
|
<div class="font-semibold mb-1" style="color: var(--ember)">Series Pass Sold Out</div>
|
||||||
<div class="text-sm text-ember-400">
|
<div class="text-sm" style="color: var(--ember)">
|
||||||
All series passes have been claimed.
|
All series passes have been claimed.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -174,7 +151,7 @@
|
||||||
<UButton
|
<UButton
|
||||||
v-if="availability?.waitlistAvailable"
|
v-if="availability?.waitlistAvailable"
|
||||||
block
|
block
|
||||||
color="gray"
|
color="neutral"
|
||||||
size="lg"
|
size="lg"
|
||||||
@click="$emit('join-waitlist')"
|
@click="$emit('join-waitlist')"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -199,7 +199,7 @@ const formatPrice = (amount) => {
|
||||||
|
|
||||||
.early-bird {
|
.early-bird {
|
||||||
color: var(--candle-dim);
|
color: var(--candle-dim);
|
||||||
border-color: rgba(122, 90, 16, 0.35);
|
border-color: var(--candle-faint);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ticket-savings {
|
.ticket-savings {
|
||||||
|
|
|
||||||
|
|
@ -463,6 +463,7 @@ const formatEventDate = (date) => {
|
||||||
.consent-field input[type="checkbox"] {
|
.consent-field input[type="checkbox"] {
|
||||||
margin-top: 3px;
|
margin-top: 3px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
accent-color: var(--candle);
|
||||||
}
|
}
|
||||||
.consent-hint {
|
.consent-hint {
|
||||||
margin-bottom: 14px;
|
margin-bottom: 14px;
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
<NuxtLink to="/events">← Back to Events</NuxtLink>
|
<NuxtLink to="/events">← Back to Events</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else>
|
<div v-else class="page-fill">
|
||||||
<!-- EVENT HEADER -->
|
<!-- EVENT HEADER -->
|
||||||
<div class="event-header">
|
<div class="event-header">
|
||||||
<h1>{{ event.title }}</h1>
|
<h1>{{ event.title }}</h1>
|
||||||
|
|
@ -48,7 +48,7 @@
|
||||||
<img
|
<img
|
||||||
:src="event.featureImage.url"
|
:src="event.featureImage.url"
|
||||||
:alt="event.featureImage.alt || event.title"
|
:alt="event.featureImage.alt || event.title"
|
||||||
/>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- TWO-COLUMN BODY -->
|
<!-- TWO-COLUMN BODY -->
|
||||||
|
|
@ -294,10 +294,19 @@ useHead(() => ({
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- PAGE FILL (aside border reaches viewport bottom) ---- */
|
||||||
|
.page-fill {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- TWO-COLUMN BODY ---- */
|
/* ---- TWO-COLUMN BODY ---- */
|
||||||
.event-body {
|
.event-body {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 280px;
|
grid-template-columns: 1fr 280px;
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
.event-main {
|
.event-main {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
|
||||||
|
|
@ -88,8 +88,8 @@
|
||||||
<div class="series-grid">
|
<div class="series-grid">
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
v-for="series in activeSeries"
|
v-for="series in activeSeries"
|
||||||
:key="series._id"
|
:key="series.id"
|
||||||
:to="`/series/${series._id}`"
|
:to="`/series/${series.id}`"
|
||||||
class="series-box"
|
class="series-box"
|
||||||
>
|
>
|
||||||
<h2>{{ series.title }}</h2>
|
<h2>{{ series.title }}</h2>
|
||||||
|
|
@ -107,6 +107,11 @@
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
<div
|
||||||
|
v-if="activeSeries.length % 2"
|
||||||
|
class="series-box series-box-filler"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -368,14 +373,21 @@ const isAlmostFull = (event) => {
|
||||||
}
|
}
|
||||||
.series-box {
|
.series-box {
|
||||||
padding: 20px 24px;
|
padding: 20px 24px;
|
||||||
border-right: 1px dashed var(--border);
|
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
transition: background 0.15s;
|
transition: background 0.15s;
|
||||||
|
border-right: 1px dashed var(--border);
|
||||||
|
border-bottom: 1px dashed var(--border);
|
||||||
}
|
}
|
||||||
.series-box:last-child {
|
.series-box:nth-child(2n) {
|
||||||
border-right: none;
|
border-right: none;
|
||||||
}
|
}
|
||||||
.series-box:hover {
|
.series-box:nth-last-child(-n + 2) {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.series-box-filler {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.series-box:not(.series-box-filler):hover {
|
||||||
background: var(--surface-hover);
|
background: var(--surface-hover);
|
||||||
}
|
}
|
||||||
.series-box h2 {
|
.series-box h2 {
|
||||||
|
|
@ -467,8 +479,17 @@ const isAlmostFull = (event) => {
|
||||||
border-right: none;
|
border-right: none;
|
||||||
border-bottom: 1px dashed var(--border);
|
border-bottom: 1px dashed var(--border);
|
||||||
}
|
}
|
||||||
|
.series-box:nth-child(2n) {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
.series-box:nth-last-child(-n + 2) {
|
||||||
|
border-bottom: 1px dashed var(--border);
|
||||||
|
}
|
||||||
.series-box:last-child {
|
.series-box:last-child {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
.series-box-filler {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="page-fill">
|
||||||
<div v-if="pending" class="loading">Loading series details...</div>
|
<div v-if="pending" class="loading">Loading series details...</div>
|
||||||
|
|
||||||
<div v-else-if="error" class="loading">
|
<div v-else-if="error" class="loading">
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
<NuxtLink to="/events">← Back to Events</NuxtLink>
|
<NuxtLink to="/events">← Back to Events</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else>
|
<div v-else class="page-fill">
|
||||||
<!-- BACK LINK -->
|
<!-- BACK LINK -->
|
||||||
<div class="back-link">
|
<div class="back-link">
|
||||||
<NuxtLink to="/events">← Back to Events</NuxtLink>
|
<NuxtLink to="/events">← Back to Events</NuxtLink>
|
||||||
|
|
@ -26,46 +26,59 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- DESCRIPTION -->
|
<!-- TWO-COLUMN BODY -->
|
||||||
<div v-if="series.description" class="section">
|
<div class="series-body" :class="{ 'has-aside': series.tickets?.enabled }">
|
||||||
<p>{{ series.description }}</p>
|
<!-- LEFT: MAIN CONTENT -->
|
||||||
</div>
|
<div class="series-main">
|
||||||
|
<div v-if="series.description" class="section description">
|
||||||
|
<p>{{ series.description }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- EVENT LIST -->
|
<div class="section" :class="{ 'section-flush': series.events?.length }">
|
||||||
<div class="section">
|
<div class="section-label">Sessions</div>
|
||||||
<div class="section-label">Sessions</div>
|
<div v-if="series.events?.length" class="sessions-box">
|
||||||
<div v-if="series.events?.length">
|
<div v-for="(event, index) in series.events" :key="event._id || index" class="event-row">
|
||||||
<div v-for="(event, index) in series.events" :key="event._id || index" class="event-row">
|
<span class="event-num">{{ String(index + 1).padStart(2, '0') }}</span>
|
||||||
<span class="event-num">{{ String(index + 1).padStart(2, '0') }}</span>
|
<span class="event-date">{{ formatDate(event.startDate) }}</span>
|
||||||
<span class="event-date">{{ formatDate(event.startDate) }}</span>
|
<div class="event-info">
|
||||||
<div class="event-info">
|
<div class="event-info-head">
|
||||||
<NuxtLink :to="`/events/${event.slug || event._id || event.id}`" class="event-title-link">
|
<NuxtLink :to="`/events/${event.slug || event._id || event.id}`" class="event-title-link">
|
||||||
{{ event.title }}
|
{{ event.title }}
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<span class="event-status">{{ getEventStatus(event) }}</span>
|
<span class="event-status">{{ getEventStatus(event) }}</span>
|
||||||
|
</div>
|
||||||
|
<p v-if="event.description" class="event-description">{{ event.description }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<p v-else class="empty">No sessions scheduled yet.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Questions (inline when no sidebar) -->
|
||||||
|
<div v-if="!series.tickets?.enabled" class="section">
|
||||||
|
<div class="section-label">Questions?</div>
|
||||||
|
<p>If you have questions about this series, reach out to us.</p>
|
||||||
|
<a href="mailto:events@ghostguild.org">events@ghostguild.org</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p v-else class="empty">No sessions scheduled yet.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- PASS PURCHASE -->
|
<!-- RIGHT: SIDEBAR -->
|
||||||
<div v-if="series.tickets?.enabled" class="section">
|
<aside v-if="series.tickets?.enabled" class="series-aside">
|
||||||
<SeriesPassPurchase
|
<SeriesPassPurchase
|
||||||
:series-id="series.id"
|
:series-id="series.id"
|
||||||
:series-info="{ id: series.id, title: series.title, totalEvents: series.totalEvents || series.events?.length || 0, type: series.type }"
|
:series-info="{ id: series.id, title: series.title, totalEvents: series.totalEvents || series.events?.length || 0, type: series.type }"
|
||||||
:series-events="series.events || []"
|
:series-events="series.events || []"
|
||||||
:user-email="memberData?.email"
|
:user-email="memberData?.email"
|
||||||
:user-name="memberData?.name"
|
:user-name="memberData?.name"
|
||||||
@purchase-success="handlePurchaseSuccess"
|
@purchase-success="handlePurchaseSuccess"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- QUESTIONS -->
|
<div class="aside-panel">
|
||||||
<div class="section">
|
<div class="box-title">Questions?</div>
|
||||||
<div class="section-label">Questions?</div>
|
<p class="aside-detail">Drop us a line.</p>
|
||||||
<p>If you have questions about this series, reach out to us.</p>
|
<a class="aside-link" href="mailto:events@ghostguild.org">events@ghostguild.org</a>
|
||||||
<a href="mailto:events@ghostguild.org">events@ghostguild.org</a>
|
</div>
|
||||||
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -137,28 +150,105 @@ useHead(() => ({
|
||||||
}
|
}
|
||||||
.meta-text { color: var(--text-faint); }
|
.meta-text { color: var(--text-faint); }
|
||||||
|
|
||||||
|
/* ---- PAGE FILL (aside border reaches viewport bottom) ---- */
|
||||||
|
.page-fill {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- TWO-COLUMN BODY ---- */
|
||||||
|
.series-body {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.series-body.has-aside {
|
||||||
|
grid-template-columns: 1fr 280px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.series-main { min-width: 0; }
|
||||||
|
.series-aside {
|
||||||
|
border-left: 1px dashed var(--border);
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.section {
|
.section {
|
||||||
padding: 24px 32px;
|
padding: 24px 32px;
|
||||||
border-bottom: 1px dashed var(--border);
|
border-bottom: 1px dashed var(--border);
|
||||||
}
|
}
|
||||||
|
.series-main .section:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
.section p { font-size: 12px; color: var(--text-dim); line-height: 1.7; max-width: 560px; margin-bottom: 8px; }
|
.section p { font-size: 12px; color: var(--text-dim); line-height: 1.7; max-width: 560px; margin-bottom: 8px; }
|
||||||
.section a { font-size: 12px; color: var(--candle); }
|
.section a { font-size: 12px; color: var(--candle); }
|
||||||
|
.section.description p { font-size: 14px; color: var(--text); }
|
||||||
|
|
||||||
|
.section-flush { padding-bottom: 0; }
|
||||||
|
.sessions-box {
|
||||||
|
border-top: 1px dashed var(--border);
|
||||||
|
margin: 10px -32px 0;
|
||||||
|
}
|
||||||
.event-row {
|
.event-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 32px 80px 1fr;
|
grid-template-columns: 32px auto 1fr;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
padding: 10px 0;
|
padding: 10px 32px;
|
||||||
border-bottom: 1px dashed var(--border);
|
border-bottom: 1px dashed var(--border);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
.event-row:last-child { border-bottom: none; }
|
.event-row:last-child { border-bottom: none; }
|
||||||
.event-num { color: var(--text-faint); font-size: 11px; }
|
.event-num { color: var(--text-faint); font-size: 11px; }
|
||||||
.event-date { color: var(--text-faint); }
|
.event-date { color: var(--text-faint); white-space: nowrap; }
|
||||||
|
.event-info { min-width: 0; }
|
||||||
.event-title-link { color: var(--text); text-decoration: none; font-size: 13px; }
|
.event-title-link { color: var(--text); text-decoration: none; font-size: 13px; }
|
||||||
.event-title-link:hover { color: var(--candle); }
|
.event-title-link:hover { color: var(--candle); }
|
||||||
.event-status { font-size: 10px; color: var(--text-faint); margin-left: 8px; }
|
.event-status { font-size: 10px; color: var(--text-faint); margin-left: 8px; }
|
||||||
|
.event-description {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
line-height: 1.5;
|
||||||
|
margin: 4px 0 0;
|
||||||
|
max-width: 560px;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.empty { font-size: 12px; color: var(--text-faint); }
|
.empty { font-size: 12px; color: var(--text-faint); }
|
||||||
|
|
||||||
|
/* ---- ASIDE PANELS ---- */
|
||||||
|
.aside-panel {
|
||||||
|
padding: 20px 24px;
|
||||||
|
border-bottom: 1px dashed var(--border);
|
||||||
|
}
|
||||||
|
.aside-panel:last-child { border-bottom: none; }
|
||||||
|
.box-title {
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--text-faint);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.aside-detail {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.aside-link {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--candle);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.series-body.has-aside {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.series-aside {
|
||||||
|
border-left: none;
|
||||||
|
border-top: 1px dashed var(--border);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,197 +1,77 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<!-- Page Header -->
|
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="Event Series"
|
title="Event Series"
|
||||||
subtitle="Multi-session events on cooperative topics"
|
subtitle="Multi-session events on cooperative topics"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Series Grid -->
|
<div v-if="pending" class="state-msg">Loading series...</div>
|
||||||
<section class="py-20 bg-[--ui-bg]">
|
|
||||||
<UContainer>
|
<div v-else-if="!filteredSeries.length" class="state-msg">
|
||||||
<div v-if="pending" class="text-center py-12">
|
<p>
|
||||||
<div
|
No series right now. Check back later or browse
|
||||||
class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"
|
<NuxtLink to="/events">upcoming events</NuxtLink>.
|
||||||
></div>
|
</p>
|
||||||
<p class="text-[--ui-text-muted]">Loading series...</p>
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<section
|
||||||
|
v-for="series in filteredSeries"
|
||||||
|
:key="series.id"
|
||||||
|
class="series-section"
|
||||||
|
>
|
||||||
|
<div class="series-head">
|
||||||
|
<h2>{{ series.title }}</h2>
|
||||||
|
<div class="series-meta-row">
|
||||||
|
<span v-if="series.type" class="badge all">{{ formatSeriesType(series.type) }}</span>
|
||||||
|
<span class="meta-text">
|
||||||
|
{{ series.eventCount }} sessions<template v-if="series.totalEvents"> of {{ series.totalEvents }} planned</template>
|
||||||
|
</span>
|
||||||
|
<span v-if="series.startDate && series.endDate" class="meta-text">
|
||||||
|
{{ formatDateRange(series.startDate, series.endDate) }}
|
||||||
|
</span>
|
||||||
|
<span v-if="series.totalRegistrations" class="meta-text">
|
||||||
|
{{ series.totalRegistrations }} registered
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p v-if="series.description" class="series-desc">
|
||||||
|
{{ series.description }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div v-if="series.events?.length" class="sessions">
|
||||||
v-else-if="filteredSeries.length > 0"
|
|
||||||
class="max-w-4xl mx-auto space-y-6"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
v-for="series in filteredSeries"
|
v-for="(event, index) in series.events"
|
||||||
:key="series.id"
|
:key="event.id"
|
||||||
class="border border-[--ui-border] rounded overflow-hidden hover:border-primary transition-colors"
|
class="event-row"
|
||||||
>
|
>
|
||||||
<!-- Series Header -->
|
<span class="event-num">
|
||||||
<div class="p-6 border-b border-[--ui-border]">
|
{{ String(event.series?.position || index + 1).padStart(2, '0') }}
|
||||||
<div
|
</span>
|
||||||
class="flex flex-col md:flex-row md:items-start md:justify-between gap-4"
|
<span class="event-date">{{ formatEventDate(event.startDate) }}</span>
|
||||||
|
<div class="event-info">
|
||||||
|
<NuxtLink
|
||||||
|
:to="`/events/${event.slug || event.id}`"
|
||||||
|
class="event-title-link"
|
||||||
>
|
>
|
||||||
<div class="flex-1">
|
{{ event.title }}
|
||||||
<div class="flex items-center gap-3 mb-3 flex-wrap">
|
</NuxtLink>
|
||||||
<span
|
<span class="event-status">{{ getEventStatus(event) }}</span>
|
||||||
:class="[
|
|
||||||
'inline-flex items-center px-3 py-1 rounded text-sm font-medium',
|
|
||||||
getSeriesTypeBadgeClass(series.type),
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
{{ formatSeriesType(series.type) }}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
:class="[
|
|
||||||
'inline-flex items-center px-2 py-1 rounded text-xs font-medium',
|
|
||||||
series.status === 'active'
|
|
||||||
? 'bg-candlelight-900/20 text-candlelight-500 dark:text-candlelight-400 border border-candlelight-700/30'
|
|
||||||
: series.status === 'upcoming'
|
|
||||||
? 'bg-guild-500/10 text-guild-300 dark:text-guild-300 border border-guild-500/30'
|
|
||||||
: 'bg-guild-500/10 text-guild-400 dark:text-guild-400 border border-guild-500/30',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
{{ series.status }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<h2 class="text-display-sm font-bold text-[--ui-text] mb-2">
|
|
||||||
{{ series.title }}
|
|
||||||
</h2>
|
|
||||||
<p class="text-[--ui-text-muted] leading-relaxed">
|
|
||||||
{{ series.description }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="text-center md:text-right flex-shrink-0">
|
|
||||||
<div class="text-3xl font-bold text-[--ui-text] mb-1">
|
|
||||||
{{ series.eventCount }}
|
|
||||||
</div>
|
|
||||||
<div class="text-sm text-[--ui-text-muted]">Events</div>
|
|
||||||
<div
|
|
||||||
v-if="series.totalEvents"
|
|
||||||
class="text-xs text-[--ui-text-muted] mt-1"
|
|
||||||
>
|
|
||||||
of {{ series.totalEvents }} planned
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Events List -->
|
|
||||||
<div class="divide-y divide-[--ui-border]">
|
|
||||||
<div
|
|
||||||
v-for="event in series.events"
|
|
||||||
:key="event.id"
|
|
||||||
class="p-4 hover:bg-[--ui-bg-elevated] transition-colors"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-between gap-4">
|
|
||||||
<div class="flex items-center gap-4 flex-1 min-w-0">
|
|
||||||
<div
|
|
||||||
class="w-8 h-8 bg-candlelight-900/20 text-candlelight-500 dark:text-candlelight-400 rounded-full flex items-center justify-center text-sm font-semibold flex-shrink-0 border border-candlelight-700/30"
|
|
||||||
>
|
|
||||||
{{ event.series?.position || "?" }}
|
|
||||||
</div>
|
|
||||||
<div class="flex-1 min-w-0">
|
|
||||||
<h3 class="font-medium text-[--ui-text] mb-1">
|
|
||||||
{{ event.title }}
|
|
||||||
</h3>
|
|
||||||
<div
|
|
||||||
class="flex items-center gap-4 text-sm text-[--ui-text-muted] flex-wrap"
|
|
||||||
>
|
|
||||||
<div class="flex items-center gap-1">
|
|
||||||
<Icon
|
|
||||||
name="heroicons:calendar-days"
|
|
||||||
class="w-4 h-4"
|
|
||||||
/>
|
|
||||||
{{ formatEventDate(event.startDate) }}
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-1">
|
|
||||||
<Icon name="heroicons:clock" class="w-4 h-4" />
|
|
||||||
{{ formatEventTime(event.startDate) }}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="event.registrations?.length"
|
|
||||||
class="flex items-center gap-1"
|
|
||||||
>
|
|
||||||
<Icon name="heroicons:users" class="w-4 h-4" />
|
|
||||||
{{ event.registrations.length }} registered
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-3 flex-shrink-0">
|
|
||||||
<span
|
|
||||||
:class="[
|
|
||||||
'inline-flex items-center px-2 py-1 rounded text-xs font-medium',
|
|
||||||
getEventStatusClass(event),
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
{{ getEventStatus(event) }}
|
|
||||||
</span>
|
|
||||||
<NuxtLink
|
|
||||||
:to="`/events/${event.slug || event.id}`"
|
|
||||||
class="inline-flex items-center px-3 py-1 bg-primary text-white text-sm rounded hover:bg-primary/90 transition-colors"
|
|
||||||
>
|
|
||||||
View
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Series Footer -->
|
|
||||||
<div
|
|
||||||
class="px-6 py-4 bg-[--ui-bg-elevated] border-t border-[--ui-border]"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-between gap-4">
|
|
||||||
<div
|
|
||||||
class="flex items-center gap-4 text-sm text-[--ui-text-muted] flex-wrap"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-if="series.startDate && series.endDate"
|
|
||||||
class="flex items-center gap-1"
|
|
||||||
>
|
|
||||||
<Icon name="heroicons:calendar-days" class="w-4 h-4" />
|
|
||||||
{{ formatDateRange(series.startDate, series.endDate) }}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="series.totalRegistrations"
|
|
||||||
class="flex items-center gap-1"
|
|
||||||
>
|
|
||||||
<Icon name="heroicons:users" class="w-4 h-4" />
|
|
||||||
{{ series.totalRegistrations }} total registrations
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<NuxtLink
|
|
||||||
:to="`/series/${series.id}`"
|
|
||||||
class="inline-flex items-center px-4 py-2 bg-primary text-white text-sm font-medium rounded hover:bg-primary/90 transition-colors"
|
|
||||||
>
|
|
||||||
View Series
|
|
||||||
<Icon name="heroicons:arrow-right" class="w-4 h-4 ml-2" />
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="text-center py-16">
|
<div class="series-foot">
|
||||||
<Icon
|
<NuxtLink :to="`/series/${series.id}`" class="view-link">
|
||||||
name="heroicons:squares-2x2"
|
View series →
|
||||||
class="w-16 h-16 text-[--ui-text-muted] mx-auto mb-4 opacity-50"
|
</NuxtLink>
|
||||||
/>
|
|
||||||
<h3 class="text-display-sm font-semibold text-[--ui-text] mb-2">
|
|
||||||
No series right now
|
|
||||||
</h3>
|
|
||||||
<p class="text-[--ui-text-muted] max-w-md mx-auto">
|
|
||||||
Check back later or browse
|
|
||||||
<NuxtLink to="/events" class="text-primary">upcoming events</NuxtLink>.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</UContainer>
|
</section>
|
||||||
</section>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
// SEO
|
|
||||||
useHead({
|
useHead({
|
||||||
title: "Event Series - Ghost Guild",
|
title: "Event Series - Ghost Guild",
|
||||||
meta: [
|
meta: [
|
||||||
|
|
@ -203,12 +83,10 @@ useHead({
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch series data
|
|
||||||
const { data: seriesData, pending } = await useFetch("/api/series", {
|
const { data: seriesData, pending } = await useFetch("/api/series", {
|
||||||
query: { includeHidden: false },
|
query: { includeHidden: false },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Filter for active and upcoming series only
|
|
||||||
const filteredSeries = computed(() => {
|
const filteredSeries = computed(() => {
|
||||||
if (!seriesData.value) return [];
|
if (!seriesData.value) return [];
|
||||||
return seriesData.value.filter(
|
return seriesData.value.filter(
|
||||||
|
|
@ -216,7 +94,6 @@ const filteredSeries = computed(() => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Helper functions
|
|
||||||
const formatSeriesType = (type) => {
|
const formatSeriesType = (type) => {
|
||||||
const types = {
|
const types = {
|
||||||
workshop_series: "Workshop Series",
|
workshop_series: "Workshop Series",
|
||||||
|
|
@ -228,25 +105,6 @@ const formatSeriesType = (type) => {
|
||||||
return types[type] || type;
|
return types[type] || type;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSeriesTypeBadgeClass = (type) => {
|
|
||||||
const classes = {
|
|
||||||
workshop_series:
|
|
||||||
"bg-candlelight-900/20 text-candlelight-500 dark:text-candlelight-400 border border-candlelight-700/30",
|
|
||||||
recurring_meetup:
|
|
||||||
"bg-guild-500/10 text-guild-300 dark:text-guild-300 border border-guild-500/30",
|
|
||||||
multi_day:
|
|
||||||
"bg-ember-900/20 text-ember-500 dark:text-ember-400 border border-ember-700/30",
|
|
||||||
course:
|
|
||||||
"bg-candlelight-900/20 text-candlelight-500 dark:text-candlelight-400 border border-candlelight-700/30",
|
|
||||||
tournament:
|
|
||||||
"bg-ember-900/20 text-ember-500 dark:text-ember-400 border border-ember-700/30",
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
classes[type] ||
|
|
||||||
"bg-earth-900/20 text-earth-400 dark:text-earth-400 border border-earth-700/30"
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatEventDate = (date) => {
|
const formatEventDate = (date) => {
|
||||||
return new Date(date).toLocaleDateString("en-US", {
|
return new Date(date).toLocaleDateString("en-US", {
|
||||||
month: "short",
|
month: "short",
|
||||||
|
|
@ -255,50 +113,133 @@ const formatEventDate = (date) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatEventTime = (date) => {
|
|
||||||
return new Date(date).toLocaleTimeString("en-US", {
|
|
||||||
hour: "numeric",
|
|
||||||
minute: "2-digit",
|
|
||||||
hour12: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatDateRange = (startDate, endDate) => {
|
const formatDateRange = (startDate, endDate) => {
|
||||||
const start = new Date(startDate);
|
const start = new Date(startDate);
|
||||||
const end = new Date(endDate);
|
const end = new Date(endDate);
|
||||||
|
|
||||||
const formatter = new Intl.DateTimeFormat("en-US", {
|
const formatter = new Intl.DateTimeFormat("en-US", {
|
||||||
month: "short",
|
month: "short",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
});
|
});
|
||||||
|
return `${formatter.format(start)} – ${formatter.format(end)}`;
|
||||||
return `${formatter.format(start)} to ${formatter.format(end)}`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getEventStatus = (event) => {
|
const getEventStatus = (event) => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const startDate = new Date(event.startDate);
|
const startDate = new Date(event.startDate);
|
||||||
const endDate = new Date(event.endDate);
|
const endDate = new Date(event.endDate);
|
||||||
|
|
||||||
if (now < startDate) return "Upcoming";
|
if (now < startDate) return "Upcoming";
|
||||||
if (now >= startDate && now <= endDate) return "Ongoing";
|
if (now >= startDate && now <= endDate) return "Ongoing";
|
||||||
return "Completed";
|
return "Completed";
|
||||||
};
|
};
|
||||||
|
|
||||||
const getEventStatusClass = (event) => {
|
|
||||||
const status = getEventStatus(event);
|
|
||||||
const classes = {
|
|
||||||
Upcoming:
|
|
||||||
"bg-guild-500/10 text-guild-300 dark:text-guild-300 border border-guild-500/30",
|
|
||||||
Ongoing:
|
|
||||||
"bg-candlelight-900/20 text-candlelight-500 dark:text-candlelight-400 border border-candlelight-700/30",
|
|
||||||
Completed:
|
|
||||||
"bg-guild-500/10 text-guild-400 dark:text-guild-400 border border-guild-500/30",
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
classes[status] ||
|
|
||||||
"bg-guild-500/10 text-guild-400 dark:text-guild-400 border border-guild-500/30"
|
|
||||||
);
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.state-msg {
|
||||||
|
padding: 32px 28px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.state-msg p { max-width: 560px; }
|
||||||
|
|
||||||
|
.series-section {
|
||||||
|
border-bottom: 1px dashed var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.series-head {
|
||||||
|
padding: 24px 28px 16px;
|
||||||
|
}
|
||||||
|
.series-head h2 {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-bright);
|
||||||
|
line-height: 1.2;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.series-meta-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.meta-text {
|
||||||
|
color: var(--text-faint);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.series-desc {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
line-height: 1.7;
|
||||||
|
max-width: 640px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sessions {
|
||||||
|
border-top: 1px dashed var(--border);
|
||||||
|
border-bottom: 1px dashed var(--border);
|
||||||
|
}
|
||||||
|
.event-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 10px 28px;
|
||||||
|
border-bottom: 1px dashed var(--border);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.event-row:last-child { border-bottom: none; }
|
||||||
|
.event-num {
|
||||||
|
flex: 0 0 24px;
|
||||||
|
color: var(--text-faint);
|
||||||
|
font-size: 11px;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
.event-date {
|
||||||
|
flex: 0 0 110px;
|
||||||
|
color: var(--text-faint);
|
||||||
|
white-space: nowrap;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
.event-info { flex: 1 1 0; min-width: 0; }
|
||||||
|
.event-title-link {
|
||||||
|
color: var(--text);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.event-title-link:hover { color: var(--candle); }
|
||||||
|
.event-status {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-faint);
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.series-foot {
|
||||||
|
padding: 14px 28px 24px;
|
||||||
|
}
|
||||||
|
.view-link {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--candle);
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
.view-link:hover { text-decoration: underline; }
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.series-head,
|
||||||
|
.series-foot {
|
||||||
|
padding-left: 20px;
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
.event-row {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding-left: 20px;
|
||||||
|
padding-right: 20px;
|
||||||
|
row-gap: 2px;
|
||||||
|
}
|
||||||
|
.event-info {
|
||||||
|
flex-basis: 100%;
|
||||||
|
margin-left: 36px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue