feat: Phase 1 - Events entity (migration, API, list page, detail layout with module sidebar, overview page)
This commit is contained in:
200
src/routes/[orgSlug]/events/[eventSlug]/+layout.svelte
Normal file
200
src/routes/[orgSlug]/events/[eventSlug]/+layout.svelte
Normal file
@@ -0,0 +1,200 @@
|
||||
<script lang="ts">
|
||||
import { page } from "$app/stores";
|
||||
import { Avatar } from "$lib/components/ui";
|
||||
import type { Snippet } from "svelte";
|
||||
import type { Event, EventMember } from "$lib/api/events";
|
||||
|
||||
interface Props {
|
||||
data: {
|
||||
org: { id: string; name: string; slug: string };
|
||||
userRole: string;
|
||||
event: Event;
|
||||
eventMembers: (EventMember & {
|
||||
profile?: {
|
||||
id: string;
|
||||
email: string;
|
||||
full_name: string | null;
|
||||
avatar_url: string | null;
|
||||
};
|
||||
})[];
|
||||
};
|
||||
children: Snippet;
|
||||
}
|
||||
|
||||
let { data, children }: Props = $props();
|
||||
|
||||
const basePath = $derived(
|
||||
`/${data.org.slug}/events/${data.event.slug}`,
|
||||
);
|
||||
|
||||
const modules = $derived([
|
||||
{
|
||||
href: basePath,
|
||||
label: "Overview",
|
||||
icon: "dashboard",
|
||||
exact: true,
|
||||
},
|
||||
{
|
||||
href: `${basePath}/tasks`,
|
||||
label: "Tasks",
|
||||
icon: "task_alt",
|
||||
},
|
||||
{
|
||||
href: `${basePath}/files`,
|
||||
label: "Files",
|
||||
icon: "folder",
|
||||
},
|
||||
{
|
||||
href: `${basePath}/schedule`,
|
||||
label: "Schedule",
|
||||
icon: "calendar_today",
|
||||
},
|
||||
{
|
||||
href: `${basePath}/budget`,
|
||||
label: "Budget",
|
||||
icon: "account_balance_wallet",
|
||||
},
|
||||
{
|
||||
href: `${basePath}/guests`,
|
||||
label: "Guests",
|
||||
icon: "groups",
|
||||
},
|
||||
{
|
||||
href: `${basePath}/team`,
|
||||
label: "Team",
|
||||
icon: "badge",
|
||||
},
|
||||
{
|
||||
href: `${basePath}/sponsors`,
|
||||
label: "Sponsors",
|
||||
icon: "handshake",
|
||||
},
|
||||
]);
|
||||
|
||||
function isModuleActive(href: string, exact?: boolean): boolean {
|
||||
if (exact) return $page.url.pathname === href;
|
||||
return $page.url.pathname.startsWith(href);
|
||||
}
|
||||
|
||||
function getStatusColor(status: string): string {
|
||||
const map: Record<string, string> = {
|
||||
planning: "bg-amber-400",
|
||||
active: "bg-emerald-400",
|
||||
completed: "bg-blue-400",
|
||||
archived: "bg-light/40",
|
||||
};
|
||||
return map[status] ?? "bg-light/40";
|
||||
}
|
||||
|
||||
function formatDateCompact(dateStr: string | null): string {
|
||||
if (!dateStr) return "";
|
||||
return new Date(dateStr).toLocaleDateString(undefined, {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex h-full">
|
||||
<!-- Event Module Sidebar -->
|
||||
<aside
|
||||
class="w-56 shrink-0 bg-dark/30 border-r border-light/5 flex flex-col overflow-hidden"
|
||||
>
|
||||
<!-- Event Header -->
|
||||
<div class="p-4 border-b border-light/5">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<div
|
||||
class="w-2.5 h-2.5 rounded-full shrink-0 {getStatusColor(
|
||||
data.event.status,
|
||||
)}"
|
||||
></div>
|
||||
<h2
|
||||
class="text-body font-heading text-white truncate"
|
||||
title={data.event.name}
|
||||
>
|
||||
{data.event.name}
|
||||
</h2>
|
||||
</div>
|
||||
{#if data.event.start_date}
|
||||
<p class="text-[11px] text-light/40 flex items-center gap-1">
|
||||
<span
|
||||
class="material-symbols-rounded"
|
||||
style="font-size: 12px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 12;"
|
||||
>calendar_today</span
|
||||
>
|
||||
{formatDateCompact(data.event.start_date)}{data.event
|
||||
.end_date
|
||||
? ` — ${formatDateCompact(data.event.end_date)}`
|
||||
: ""}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Module Navigation -->
|
||||
<nav class="flex-1 flex flex-col gap-0.5 p-2 overflow-auto">
|
||||
{#each modules as mod}
|
||||
<a
|
||||
href={mod.href}
|
||||
class="flex items-center gap-2.5 px-3 py-2 rounded-xl text-body-sm font-body transition-colors {isModuleActive(
|
||||
mod.href,
|
||||
mod.exact,
|
||||
)
|
||||
? 'bg-primary text-background'
|
||||
: 'text-light/60 hover:text-white hover:bg-dark/50'}"
|
||||
>
|
||||
<span
|
||||
class="material-symbols-rounded"
|
||||
style="font-size: 18px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 18;"
|
||||
>{mod.icon}</span
|
||||
>
|
||||
{mod.label}
|
||||
</a>
|
||||
{/each}
|
||||
</nav>
|
||||
|
||||
<!-- Event Team Preview -->
|
||||
<div class="p-3 border-t border-light/5">
|
||||
<p class="text-[11px] text-light/40 mb-2 px-1">
|
||||
Team ({data.eventMembers.length})
|
||||
</p>
|
||||
<div class="flex flex-wrap gap-1 px-1">
|
||||
{#each data.eventMembers.slice(0, 8) as member}
|
||||
<div title={member.profile?.full_name || member.profile?.email || "Member"}>
|
||||
<Avatar
|
||||
name={member.profile?.full_name ||
|
||||
member.profile?.email ||
|
||||
"?"}
|
||||
src={member.profile?.avatar_url}
|
||||
size="xs"
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
{#if data.eventMembers.length > 8}
|
||||
<div
|
||||
class="w-6 h-6 rounded-full bg-dark flex items-center justify-center text-[10px] text-light/50"
|
||||
>
|
||||
+{data.eventMembers.length - 8}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Back link -->
|
||||
<a
|
||||
href="/{data.org.slug}/events"
|
||||
class="flex items-center gap-2 px-4 py-3 border-t border-light/5 text-body-sm text-light/40 hover:text-white transition-colors"
|
||||
>
|
||||
<span
|
||||
class="material-symbols-rounded"
|
||||
style="font-size: 16px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 16;"
|
||||
>arrow_back</span
|
||||
>
|
||||
All Events
|
||||
</a>
|
||||
</aside>
|
||||
|
||||
<!-- Module Content -->
|
||||
<div class="flex-1 overflow-auto">
|
||||
{@render children()}
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user