feat: UI overhaul - component library + route layouts with instant headers
- Created 11 reusable UI components: PageHeader, SectionCard, StatCard, StatusBadge, TabBar, MemberList, ActivityFeed, EventCard, ContentSkeleton, QuickLinkGrid, ModuleCard - Created route-specific +layout.svelte for documents, calendar, kanban, events, settings, account - Each layout renders PageHeader instantly from parent data, shows ContentSkeleton during navigation - Removed full-page PageSkeleton from parent layout - Refactored all pages to use new components instead of inline markup - Overview page: uses StatCard, SectionCard, EventCard, ActivityFeed, MemberList, QuickLinkGrid - Events list: uses EventCard, Button components - Event detail: uses ModuleCard, SectionCard - Settings/Account/Calendar/Kanban: headers in layouts, toolbars in pages - Added i18n keys for overview page (EN + ET) - 0 errors, 112 tests pass
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import { page } from "$app/stores";
|
||||
import { Avatar } from "$lib/components/ui";
|
||||
import { EventCard, TabBar, Button } from "$lib/components/ui";
|
||||
import { getContext } from "svelte";
|
||||
import type { SupabaseClient } from "@supabase/supabase-js";
|
||||
import type { Database } from "$lib/supabase/types";
|
||||
@@ -68,45 +68,6 @@
|
||||
"#14B8A6",
|
||||
];
|
||||
|
||||
function getStatusColor(status: string): string {
|
||||
const map: Record<string, string> = {
|
||||
planning: "text-amber-400 bg-amber-400/10",
|
||||
active: "text-emerald-400 bg-emerald-400/10",
|
||||
completed: "text-blue-400 bg-blue-400/10",
|
||||
archived: "text-light/40 bg-light/5",
|
||||
};
|
||||
return map[status] ?? "text-light/40 bg-light/5";
|
||||
}
|
||||
|
||||
function getStatusIcon(status: string): string {
|
||||
const map: Record<string, string> = {
|
||||
planning: "edit_note",
|
||||
active: "play_circle",
|
||||
completed: "check_circle",
|
||||
archived: "archive",
|
||||
};
|
||||
return map[status] ?? "help";
|
||||
}
|
||||
|
||||
function formatDate(dateStr: string | null): string {
|
||||
if (!dateStr) return "";
|
||||
return new Date(dateStr).toLocaleDateString(undefined, {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
function formatDateRange(
|
||||
start: string | null,
|
||||
end: string | null,
|
||||
): string {
|
||||
if (!start && !end) return m.events_no_dates();
|
||||
if (start && !end) return formatDate(start);
|
||||
if (!start && end) return `Until ${formatDate(end)}`;
|
||||
return `${formatDate(start)} — ${formatDate(end)}`;
|
||||
}
|
||||
|
||||
async function handleCreate() {
|
||||
if (!newEventName.trim()) return;
|
||||
creating = true;
|
||||
@@ -171,166 +132,66 @@
|
||||
</svelte:head>
|
||||
|
||||
<div class="flex flex-col h-full">
|
||||
<!-- Header -->
|
||||
<header
|
||||
class="flex items-center justify-between px-6 py-5 border-b border-light/5"
|
||||
>
|
||||
<div>
|
||||
<h1 class="text-h1 font-heading text-white">{m.events_title()}</h1>
|
||||
<p class="text-body-sm text-light/50 mt-1">
|
||||
{m.events_subtitle()}
|
||||
</p>
|
||||
<!-- Toolbar: Status Tabs + Create Button -->
|
||||
<div class="flex items-center justify-between px-6 py-3 border-b border-light/5 shrink-0">
|
||||
<div class="flex items-center gap-1">
|
||||
{#each statusTabs as tab}
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-body-sm font-body transition-colors {data.statusFilter ===
|
||||
tab.value
|
||||
? 'bg-primary text-background'
|
||||
: 'text-light/50 hover:text-white hover:bg-dark/50'}"
|
||||
onclick={() => switchStatus(tab.value)}
|
||||
>
|
||||
<span
|
||||
class="material-symbols-rounded"
|
||||
style="font-size: 16px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 16;"
|
||||
>{tab.icon}</span
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{#if isEditor}
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center gap-2 px-4 py-2 bg-primary text-background rounded-xl font-body text-body-sm hover:bg-primary-hover transition-colors"
|
||||
onclick={() => (showCreateModal = true)}
|
||||
>
|
||||
<span
|
||||
class="material-symbols-rounded"
|
||||
style="font-size: 20px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20;"
|
||||
>add</span
|
||||
>
|
||||
<Button size="sm" icon="add" onclick={() => (showCreateModal = true)}>
|
||||
{m.events_new()}
|
||||
</button>
|
||||
</Button>
|
||||
{/if}
|
||||
</header>
|
||||
|
||||
<!-- Status Tabs -->
|
||||
<div class="flex items-center gap-1 px-6 py-3 border-b border-light/5">
|
||||
{#each statusTabs as tab}
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-body-sm font-body transition-colors {data.statusFilter ===
|
||||
tab.value
|
||||
? 'bg-primary text-background'
|
||||
: 'text-light/60 hover:text-white hover:bg-dark/50'}"
|
||||
onclick={() => switchStatus(tab.value)}
|
||||
>
|
||||
<span
|
||||
class="material-symbols-rounded"
|
||||
style="font-size: 16px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 16;"
|
||||
>{tab.icon}</span
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Events Grid -->
|
||||
<div class="flex-1 overflow-auto p-6">
|
||||
{#if data.events.length === 0}
|
||||
<div
|
||||
class="flex flex-col items-center justify-center h-full text-light/40"
|
||||
>
|
||||
<div class="flex flex-col items-center justify-center h-full text-light/40">
|
||||
<span
|
||||
class="material-symbols-rounded mb-4"
|
||||
style="font-size: 64px; font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 48;"
|
||||
>celebration</span
|
||||
>
|
||||
<p class="text-h3 font-heading mb-2">{m.events_empty_title()}</p>
|
||||
<p class="text-body text-light/30">
|
||||
{m.events_empty_desc()}
|
||||
</p>
|
||||
<p class="text-body text-light/30">{m.events_empty_desc()}</p>
|
||||
{#if isEditor}
|
||||
<button
|
||||
type="button"
|
||||
class="mt-4 flex items-center gap-2 px-4 py-2 bg-primary text-background rounded-xl font-body text-body-sm hover:bg-primary-hover transition-colors"
|
||||
onclick={() => (showCreateModal = true)}
|
||||
>
|
||||
<span
|
||||
class="material-symbols-rounded"
|
||||
style="font-size: 20px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20;"
|
||||
>add</span
|
||||
>
|
||||
{m.events_create()}
|
||||
</button>
|
||||
<div class="mt-4">
|
||||
<Button icon="add" onclick={() => (showCreateModal = true)}>
|
||||
{m.events_create()}
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||
{#each data.events as event}
|
||||
<a
|
||||
<EventCard
|
||||
name={event.name}
|
||||
slug={event.slug}
|
||||
status={event.status}
|
||||
startDate={event.start_date}
|
||||
endDate={event.end_date}
|
||||
color={event.color}
|
||||
venueName={event.venue_name}
|
||||
href="/{data.org.slug}/events/{event.slug}"
|
||||
class="group bg-dark/30 hover:bg-dark/60 border border-light/5 hover:border-light/10 rounded-2xl p-5 flex flex-col gap-3 transition-all"
|
||||
>
|
||||
<!-- Color bar + Status -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
class="w-3 h-3 rounded-full"
|
||||
style="background-color: {event.color ||
|
||||
'#00A3E0'}"
|
||||
></div>
|
||||
<h3
|
||||
class="text-body font-heading text-white group-hover:text-primary transition-colors truncate"
|
||||
>
|
||||
{event.name}
|
||||
</h3>
|
||||
</div>
|
||||
<span
|
||||
class="text-[11px] font-body px-2 py-0.5 rounded-full capitalize {getStatusColor(
|
||||
event.status,
|
||||
)}"
|
||||
>
|
||||
{event.status}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
{#if event.description}
|
||||
<p
|
||||
class="text-body-sm text-light/50 line-clamp-2"
|
||||
>
|
||||
{event.description}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<!-- Meta row -->
|
||||
<div
|
||||
class="flex items-center gap-4 text-[12px] text-light/40 mt-auto pt-2"
|
||||
>
|
||||
<!-- Date -->
|
||||
<div class="flex items-center gap-1">
|
||||
<span
|
||||
class="material-symbols-rounded"
|
||||
style="font-size: 14px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 14;"
|
||||
>calendar_today</span
|
||||
>
|
||||
<span
|
||||
>{formatDateRange(
|
||||
event.start_date,
|
||||
event.end_date,
|
||||
)}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Venue -->
|
||||
{#if event.venue_name}
|
||||
<div class="flex items-center gap-1">
|
||||
<span
|
||||
class="material-symbols-rounded"
|
||||
style="font-size: 14px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 14;"
|
||||
>location_on</span
|
||||
>
|
||||
<span class="truncate max-w-[120px]"
|
||||
>{event.venue_name}</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Members -->
|
||||
<div class="flex items-center gap-1 ml-auto">
|
||||
<span
|
||||
class="material-symbols-rounded"
|
||||
style="font-size: 14px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 14;"
|
||||
>group</span
|
||||
>
|
||||
<span>{event.member_count}</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user