Redesign interface across member dashboard and events pages

The changes involve a comprehensive interface redesign across multiple
pages, including:

- Updated peer support badge with shield design
- Switched privacy toggle to use USwitch component
- Added light/dark mode support throughout
- Enhanced layout and spacing in default template
- Added series details page with timeline view
- Improved event cards and status indicators
- Refreshed member profile styles for better readability
- Introduced global cursor styling for interactive elements
This commit is contained in:
Jennie Robinson Faber 2025-10-09 16:25:57 +01:00
parent e8e3b84276
commit 896ad0336c
12 changed files with 915 additions and 360 deletions

446
app/pages/series/[id].vue Normal file
View file

@ -0,0 +1,446 @@
<template>
<div>
<div v-if="pending" class="min-h-screen flex items-center justify-center">
<div class="text-center">
<div
class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto mb-4"
></div>
<p class="text-[--ui-text-muted]">Loading series details...</p>
</div>
</div>
<div
v-else-if="error"
class="min-h-screen flex items-center justify-center"
>
<div class="text-center">
<h2 class="text-2xl font-bold text-[--ui-text] mb-2">
Series Not Found
</h2>
<p class="text-[--ui-text-muted] mb-6">
The event series you're looking for doesn't exist.
</p>
<NuxtLink to="/series" class="text-primary hover:underline">
Back to Event Series
</NuxtLink>
</div>
</div>
<div v-else>
<!-- Page Header -->
<PageHeader
:title="series.title"
:subtitle="series.description"
theme="purple"
size="large"
/>
<!-- Series Meta -->
<section class="py-20 bg-[--ui-bg]">
<UContainer>
<div class="max-w-4xl mx-auto">
<div class="flex items-center gap-4 mb-8 flex-wrap">
<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-3 py-1 rounded text-sm font-medium',
getSeriesStatusClass(),
]"
>
{{ getSeriesStatusText() }}
</span>
</div>
<!-- Series Stats -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-6 mb-12">
<div>
<div class="text-3xl font-bold text-[--ui-text] mb-1">
{{ series.statistics.totalEvents }}
</div>
<div class="text-sm text-[--ui-text-muted]">Total Events</div>
</div>
<div>
<div class="text-3xl font-bold text-[--ui-text] mb-1">
{{ series.statistics.completedEvents }}
</div>
<div class="text-sm text-[--ui-text-muted]">Completed</div>
</div>
<div>
<div class="text-3xl font-bold text-[--ui-text] mb-1">
{{ series.statistics.upcomingEvents }}
</div>
<div class="text-sm text-[--ui-text-muted]">Upcoming</div>
</div>
<div v-if="series.statistics.totalRegistrations">
<div class="text-3xl font-bold text-[--ui-text] mb-1">
{{ series.statistics.totalRegistrations }}
</div>
<div class="text-sm text-[--ui-text-muted]">Registrations</div>
</div>
</div>
<!-- Series Date Range -->
<div
v-if="series.startDate && series.endDate"
class="flex items-center gap-2 text-[--ui-text-muted] mb-8"
>
<Icon name="heroicons:calendar-days" class="w-5 h-5" />
<span>
Series runs from
{{ formatDateRange(series.startDate, series.endDate) }}
</span>
</div>
<!-- Status Message -->
<div
v-if="series.statistics.isOngoing"
class="p-4 bg-green-500/10 border border-green-500/30 rounded mb-8"
>
<p class="text-green-600 dark:text-green-400 font-semibold mb-1">
This series is currently ongoing!
</p>
<p class="text-sm text-[--ui-text-muted]">
Register for upcoming events to join the learning journey.
</p>
</div>
<div
v-else-if="series.statistics.isUpcoming"
class="p-4 bg-blue-500/10 border border-blue-500/30 rounded mb-8"
>
<p class="text-blue-600 dark:text-blue-400 font-semibold mb-1">
This series is starting soon!
</p>
<p class="text-sm text-[--ui-text-muted]">
Mark your calendar and register for the events.
</p>
</div>
<div
v-else-if="series.statistics.isCompleted"
class="p-4 bg-gray-500/10 border border-gray-500/30 rounded mb-8"
>
<p class="text-[--ui-text] font-semibold mb-1">
This series has concluded.
</p>
<p class="text-sm text-[--ui-text-muted]">
Check out our other event series for more opportunities to learn
and connect.
</p>
</div>
</div>
</UContainer>
</section>
<!-- Events Timeline -->
<section class="py-20 bg-[--ui-bg-elevated]">
<UContainer>
<div class="max-w-4xl mx-auto">
<h2 class="text-2xl font-bold text-[--ui-text] mb-8">
Event Schedule
</h2>
<div class="space-y-4">
<div
v-for="(event, index) in series.events"
:key="event.id"
class="group"
>
<div class="flex items-start gap-4">
<!-- Position indicator -->
<div class="flex flex-col items-center flex-shrink-0">
<div
:class="[
'w-10 h-10 rounded-full flex items-center justify-center text-sm font-bold border',
getEventTimelineColor(event),
]"
>
{{ event.series?.position || index + 1 }}
</div>
<div
v-if="index < series.events.length - 1"
class="w-0.5 h-12 bg-[--ui-border]"
></div>
</div>
<!-- Event Card -->
<NuxtLink
:to="`/events/${event.slug || event.id}`"
class="flex-1 border border-[--ui-border] rounded p-4 hover:border-primary transition-colors"
>
<div
class="flex flex-col md:flex-row md:items-start md:justify-between gap-3"
>
<!-- Event Info -->
<div class="flex-1 min-w-0">
<div class="flex items-start gap-2 mb-2 flex-wrap">
<h3
class="text-lg font-semibold text-[--ui-text] group-hover:text-primary transition-colors"
>
{{ event.title }}
</h3>
<span
:class="[
'inline-flex items-center px-2 py-0.5 rounded text-xs font-medium flex-shrink-0',
getEventStatusClass(event),
]"
>
{{ getEventStatus(event) }}
</span>
</div>
<p
v-if="event.description"
class="text-[--ui-text-muted] mb-3 line-clamp-2"
>
{{ event.description }}
</p>
<div
class="flex flex-wrap items-center gap-3 text-sm text-[--ui-text-muted]"
>
<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.location"
class="flex items-center gap-1"
>
<Icon name="heroicons:map-pin" class="w-4 h-4" />
{{ event.location }}
</div>
<div
v-if="event.registeredCount"
class="flex items-center gap-1"
>
<Icon name="heroicons:users" class="w-4 h-4" />
{{ event.registeredCount }} registered
</div>
</div>
</div>
<!-- Arrow -->
<div class="flex items-center md:pt-1">
<Icon
name="heroicons:arrow-right"
class="w-5 h-5 text-[--ui-text-muted] group-hover:text-primary group-hover:translate-x-1 transition-all"
/>
</div>
</div>
</NuxtLink>
</div>
</div>
</div>
</div>
</UContainer>
</section>
<!-- Questions -->
<section class="py-20 bg-[--ui-bg]">
<UContainer>
<div class="max-w-4xl mx-auto">
<h3 class="text-xl font-bold text-[--ui-text] mb-3">
Questions About This Series?
</h3>
<p class="text-[--ui-text-muted] mb-4">
If you have any questions about this event series, please reach
out to us.
</p>
<a
href="mailto:events@ghostguild.org"
class="text-primary hover:underline"
>
events@ghostguild.org
</a>
<div class="mt-8">
<NuxtLink to="/series" class="text-primary hover:underline">
Back to all event series
</NuxtLink>
</div>
</div>
</UContainer>
</section>
</div>
</div>
</template>
<script setup>
const route = useRoute();
// Fetch series data from API
const {
data: series,
pending,
error,
} = await useFetch(`/api/series/${route.params.id}`);
// Handle series not found
if (error.value?.statusCode === 404) {
throw createError({
statusCode: 404,
statusMessage: "Event series not found",
});
}
// Helper functions
const formatSeriesType = (type) => {
const types = {
workshop_series: "Workshop Series",
recurring_meetup: "Recurring Meetup",
multi_day: "Multi-Day Event",
course: "Course",
tournament: "Tournament",
};
return types[type] || type;
};
const getSeriesTypeBadgeClass = (type) => {
const classes = {
workshop_series:
"bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-500/30",
recurring_meetup:
"bg-blue-500/10 text-blue-600 dark:text-blue-400 border border-blue-500/30",
multi_day:
"bg-purple-500/10 text-purple-600 dark:text-purple-400 border border-purple-500/30",
course:
"bg-amber-500/10 text-amber-600 dark:text-amber-400 border border-amber-500/30",
tournament:
"bg-red-500/10 text-red-600 dark:text-red-400 border border-red-500/30",
};
return (
classes[type] ||
"bg-gray-500/10 text-gray-600 dark:text-gray-400 border border-gray-500/30"
);
};
const getSeriesStatusText = () => {
if (series.value.statistics.isOngoing) return "Ongoing";
if (series.value.statistics.isUpcoming) return "Starting Soon";
if (series.value.statistics.isCompleted) return "Completed";
return "Active";
};
const getSeriesStatusClass = () => {
if (series.value.statistics.isOngoing)
return "bg-green-500/10 text-green-600 dark:text-green-400 border border-green-500/30";
if (series.value.statistics.isUpcoming)
return "bg-blue-500/10 text-blue-600 dark:text-blue-400 border border-blue-500/30";
if (series.value.statistics.isCompleted)
return "bg-gray-500/10 text-gray-600 dark:text-gray-400 border border-gray-500/30";
return "bg-purple-500/10 text-purple-600 dark:text-purple-400 border border-purple-500/30";
};
const formatEventDate = (date) => {
return new Date(date).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
};
const formatEventTime = (date) => {
return new Date(date).toLocaleTimeString("en-US", {
hour: "numeric",
minute: "2-digit",
hour12: true,
});
};
const formatDateRange = (startDate, endDate) => {
const start = new Date(startDate);
const end = new Date(endDate);
const formatter = new Intl.DateTimeFormat("en-US", {
month: "long",
day: "numeric",
year: "numeric",
});
return `${formatter.format(start)} to ${formatter.format(end)}`;
};
const getEventStatus = (event) => {
const now = new Date();
const startDate = new Date(event.startDate);
const endDate = new Date(event.endDate);
if (now < startDate) return "Upcoming";
if (now >= startDate && now <= endDate) return "Ongoing";
return "Completed";
};
const getEventStatusClass = (event) => {
const status = getEventStatus(event);
const classes = {
Upcoming:
"bg-blue-500/10 text-blue-600 dark:text-blue-400 border border-blue-500/30",
Ongoing:
"bg-green-500/10 text-green-600 dark:text-green-400 border border-green-500/30",
Completed:
"bg-gray-500/10 text-gray-600 dark:text-gray-400 border border-gray-500/30",
};
return (
classes[status] ||
"bg-gray-500/10 text-gray-600 dark:text-gray-400 border border-gray-500/30"
);
};
const getEventTimelineColor = (event) => {
const status = getEventStatus(event);
const classes = {
Upcoming:
"bg-blue-500/10 text-blue-600 dark:text-blue-400 border-blue-500/30",
Ongoing:
"bg-green-500/10 text-green-600 dark:text-green-400 border-green-500/30",
Completed:
"bg-purple-500/10 text-purple-600 dark:text-purple-400 border-purple-500/30",
};
return (
classes[status] ||
"bg-gray-500/10 text-gray-600 dark:text-gray-400 border-gray-500/30"
);
};
// SEO Meta
useHead(() => ({
title: series.value
? `${series.value.title} - Event Series - Ghost Guild`
: "Event Series - Ghost Guild",
meta: [
{
name: "description",
content:
series.value?.description ||
"Explore our multi-event series designed for learning and growth",
},
],
}));
</script>
<style scoped>
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>

View file

@ -1,60 +1,76 @@
<template>
<div>
<!-- Hero Section -->
<div class="bg-gradient-to-br from-purple-600 via-blue-600 to-emerald-500 py-16">
<UContainer>
<div class="text-center">
<h1 class="text-4xl md:text-6xl font-bold text-white mb-6">
Event Series
</h1>
<p class="text-xl md:text-2xl text-purple-100 max-w-3xl mx-auto">
Discover our multi-event series designed to take you on a journey of learning and growth
</p>
</div>
</UContainer>
</div>
<!-- Page Header -->
<PageHeader
title="Event Series"
subtitle="Discover our multi-event series designed to take you on a journey of learning and growth"
theme="purple"
size="large"
/>
<!-- Series Grid -->
<div class="py-16 bg-gray-50">
<section class="py-20 bg-[--ui-bg]">
<UContainer>
<div v-if="pending" class="text-center py-12">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-500 mx-auto mb-4"></div>
<p class="text-gray-600">Loading series...</p>
<div
class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"
></div>
<p class="text-[--ui-text-muted]">Loading series...</p>
</div>
<div v-else-if="filteredSeries.length > 0" class="space-y-8">
<div
v-for="series in filteredSeries"
<div
v-else-if="filteredSeries.length > 0"
class="max-w-4xl mx-auto space-y-6"
>
<div
v-for="series in filteredSeries"
:key="series.id"
class="bg-white rounded-xl shadow-lg overflow-hidden hover:shadow-xl transition-shadow duration-300"
class="border border-[--ui-border] rounded overflow-hidden hover:border-primary transition-colors"
>
<!-- Series Header -->
<div class="p-6 border-b border-gray-200">
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div class="p-6 border-b border-[--ui-border]">
<div
class="flex flex-col md:flex-row md:items-start md:justify-between gap-4"
>
<div class="flex-1">
<div class="flex items-center gap-3 mb-2">
<div :class="[
'inline-flex items-center px-3 py-1 rounded-full text-sm font-medium',
getSeriesTypeBadgeClass(series.type)
]">
<div class="flex items-center gap-3 mb-3 flex-wrap">
<span
:class="[
'inline-flex items-center px-3 py-1 rounded text-sm font-medium',
getSeriesTypeBadgeClass(series.type),
]"
>
{{ formatSeriesType(series.type) }}
</div>
<span :class="[
'inline-flex items-center px-2 py-1 rounded-full text-xs font-medium',
series.status === 'active' ? 'bg-green-100 text-green-700' :
series.status === 'upcoming' ? 'bg-blue-100 text-blue-700' :
'bg-gray-100 text-gray-700'
]">
</span>
<span
:class="[
'inline-flex items-center px-2 py-1 rounded text-xs font-medium',
series.status === 'active'
? 'bg-green-500/10 text-green-600 dark:text-green-400 border border-green-500/30'
: series.status === 'upcoming'
? 'bg-blue-500/10 text-blue-600 dark:text-blue-400 border border-blue-500/30'
: 'bg-gray-500/10 text-gray-600 dark:text-gray-400 border border-gray-500/30',
]"
>
{{ series.status }}
</span>
</div>
<h2 class="text-2xl font-bold text-gray-900 mb-2">{{ series.title }}</h2>
<p class="text-gray-600 leading-relaxed">{{ series.description }}</p>
<h2 class="text-2xl 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">
<div class="text-3xl font-bold text-purple-600 mb-1">{{ series.eventCount }}</div>
<div class="text-sm text-gray-500">Events</div>
<div v-if="series.totalEvents" class="text-xs text-gray-400 mt-1">
<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>
@ -62,47 +78,61 @@
</div>
<!-- Events List -->
<div class="divide-y divide-gray-100">
<div
v-for="event in series.events"
<div class="divide-y divide-[--ui-border]">
<div
v-for="event in series.events"
:key="event.id"
class="p-4 hover:bg-gray-50 transition-colors duration-200"
class="p-4 hover:bg-[--ui-bg-elevated] transition-colors"
>
<div class="flex items-center justify-between">
<div class="flex items-center gap-4 flex-1">
<div class="w-10 h-10 bg-purple-100 text-purple-600 rounded-full flex items-center justify-center text-sm font-semibold">
{{ event.series?.position || '?' }}
<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-purple-500/10 text-purple-600 dark:text-purple-400 rounded-full flex items-center justify-center text-sm font-semibold flex-shrink-0 border border-purple-500/30"
>
{{ event.series?.position || "?" }}
</div>
<div class="flex-1">
<h3 class="font-medium text-gray-900 mb-1">{{ event.title }}</h3>
<div class="flex items-center gap-4 text-sm text-gray-500">
<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" />
<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">
<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">
<span :class="[
'inline-flex items-center px-2 py-1 rounded-full text-xs font-medium',
getEventStatusClass(event)
]">
<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
<NuxtLink
:to="`/events/${event.slug || event.id}`"
class="inline-flex items-center px-3 py-1 bg-purple-600 text-white text-sm rounded-lg hover:bg-purple-700 transition-colors"
class="inline-flex items-center px-3 py-1 bg-primary text-white text-sm rounded hover:bg-primary/90 transition-colors"
>
View Event
View
</NuxtLink>
</div>
</div>
@ -110,125 +140,167 @@
</div>
<!-- Series Footer -->
<div v-if="series.startDate && series.endDate" class="px-6 py-4 bg-gray-50 border-t border-gray-200">
<div class="flex items-center justify-between text-sm text-gray-500">
<div class="flex items-center gap-1">
<Icon name="heroicons:calendar-days" class="w-4 h-4" />
Series runs from {{ 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
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 v-else class="text-center py-16">
<Icon name="heroicons:squares-2x2" class="w-16 h-16 text-gray-300 mx-auto mb-4" />
<h3 class="text-xl font-semibold text-gray-900 mb-2">No Event Series Available</h3>
<p class="text-gray-600 max-w-md mx-auto">
We're currently planning exciting event series. Check back soon for multi-event learning journeys!
<Icon
name="heroicons:squares-2x2"
class="w-16 h-16 text-[--ui-text-muted] mx-auto mb-4 opacity-50"
/>
<h3 class="text-xl font-semibold text-[--ui-text] mb-2">
No Event Series Available
</h3>
<p class="text-[--ui-text-muted] max-w-md mx-auto">
We're currently planning exciting event series. Check back soon for
multi-event learning journeys!
</p>
</div>
</UContainer>
</div>
</section>
</div>
</template>
<script setup>
// SEO
useHead({
title: 'Event Series - Ghost Guild',
title: "Event Series - Ghost Guild",
meta: [
{ name: 'description', content: 'Discover our multi-event series designed to take you on a journey of learning and growth in cooperative game development and community building.' }
]
})
{
name: "description",
content:
"Discover our multi-event series designed to take you on a journey of learning and growth in cooperative game development and community building.",
},
],
});
// Fetch series data
const { data: seriesData, pending } = await useFetch('/api/series', {
query: { includeHidden: false }
})
const { data: seriesData, pending } = await useFetch("/api/series", {
query: { includeHidden: false },
});
// Filter for active and upcoming series only
const filteredSeries = computed(() => {
if (!seriesData.value) return []
return seriesData.value.filter(series =>
series.status === 'active' || series.status === 'upcoming'
)
})
if (!seriesData.value) return [];
return seriesData.value.filter(
(series) => series.status === "active" || series.status === "upcoming",
);
});
// Helper functions
const formatSeriesType = (type) => {
const types = {
'workshop_series': 'Workshop Series',
'recurring_meetup': 'Recurring Meetup',
'multi_day': 'Multi-Day Event',
'course': 'Course',
'tournament': 'Tournament'
}
return types[type] || type
}
workshop_series: "Workshop Series",
recurring_meetup: "Recurring Meetup",
multi_day: "Multi-Day Event",
course: "Course",
tournament: "Tournament",
};
return types[type] || type;
};
const getSeriesTypeBadgeClass = (type) => {
const classes = {
'workshop_series': 'bg-emerald-100 text-emerald-700',
'recurring_meetup': 'bg-blue-100 text-blue-700',
'multi_day': 'bg-purple-100 text-purple-700',
'course': 'bg-amber-100 text-amber-700',
'tournament': 'bg-red-100 text-red-700'
}
return classes[type] || 'bg-gray-100 text-gray-700'
}
workshop_series:
"bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-500/30",
recurring_meetup:
"bg-blue-500/10 text-blue-600 dark:text-blue-400 border border-blue-500/30",
multi_day:
"bg-purple-500/10 text-purple-600 dark:text-purple-400 border border-purple-500/30",
course:
"bg-amber-500/10 text-amber-600 dark:text-amber-400 border border-amber-500/30",
tournament:
"bg-red-500/10 text-red-600 dark:text-red-400 border border-red-500/30",
};
return (
classes[type] ||
"bg-gray-500/10 text-gray-600 dark:text-gray-400 border border-gray-500/30"
);
};
const formatEventDate = (date) => {
return new Date(date).toLocaleDateString('en-US', {
weekday: 'long',
month: 'long',
day: 'numeric',
year: 'numeric'
})
}
return new Date(date).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
};
const formatEventTime = (date) => {
return new Date(date).toLocaleTimeString('en-US', {
hour: 'numeric',
minute: '2-digit',
hour12: true
})
}
return new Date(date).toLocaleTimeString("en-US", {
hour: "numeric",
minute: "2-digit",
hour12: true,
});
};
const formatDateRange = (startDate, endDate) => {
const start = new Date(startDate)
const end = new Date(endDate)
const formatter = new Intl.DateTimeFormat('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric'
})
return `${formatter.format(start)} to ${formatter.format(end)}`
}
const start = new Date(startDate);
const end = new Date(endDate);
const formatter = new Intl.DateTimeFormat("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
return `${formatter.format(start)} to ${formatter.format(end)}`;
};
const getEventStatus = (event) => {
const now = new Date()
const startDate = new Date(event.startDate)
const endDate = new Date(event.endDate)
if (now < startDate) return 'Upcoming'
if (now >= startDate && now <= endDate) return 'Ongoing'
return 'Completed'
}
const now = new Date();
const startDate = new Date(event.startDate);
const endDate = new Date(event.endDate);
if (now < startDate) return "Upcoming";
if (now >= startDate && now <= endDate) return "Ongoing";
return "Completed";
};
const getEventStatusClass = (event) => {
const status = getEventStatus(event)
const status = getEventStatus(event);
const classes = {
'Upcoming': 'bg-blue-100 text-blue-700',
'Ongoing': 'bg-green-100 text-green-700',
'Completed': 'bg-gray-100 text-gray-700'
}
return classes[status] || 'bg-gray-100 text-gray-700'
}
</script>
Upcoming:
"bg-blue-500/10 text-blue-600 dark:text-blue-400 border border-blue-500/30",
Ongoing:
"bg-green-500/10 text-green-600 dark:text-green-400 border border-green-500/30",
Completed:
"bg-gray-500/10 text-gray-600 dark:text-gray-400 border border-gray-500/30",
};
return (
classes[status] ||
"bg-gray-500/10 text-gray-600 dark:text-gray-400 border border-gray-500/30"
);
};
</script>