From 556955f349873e1c87afae831028706581d564bd Mon Sep 17 00:00:00 2001 From: AlacrisDevs Date: Sat, 7 Feb 2026 10:04:37 +0200 Subject: [PATCH 01/10] feat: Phase 1 - Events entity (migration, API, list page, detail layout with module sidebar, overview page) --- src/lib/api/events.test.ts | 48 ++ src/lib/api/events.ts | 277 +++++++++ src/lib/supabase/types.ts | 96 +++ src/routes/[orgSlug]/+layout.svelte | 13 +- src/routes/[orgSlug]/events/+page.server.ts | 29 + src/routes/[orgSlug]/events/+page.svelte | 499 ++++++++++++++++ .../events/[eventSlug]/+layout.server.ts | 27 + .../events/[eventSlug]/+layout.svelte | 200 +++++++ .../[orgSlug]/events/[eventSlug]/+page.svelte | 563 ++++++++++++++++++ supabase/migrations/022_events.sql | 84 +++ 10 files changed, 1833 insertions(+), 3 deletions(-) create mode 100644 src/lib/api/events.test.ts create mode 100644 src/lib/api/events.ts create mode 100644 src/routes/[orgSlug]/events/+page.server.ts create mode 100644 src/routes/[orgSlug]/events/+page.svelte create mode 100644 src/routes/[orgSlug]/events/[eventSlug]/+layout.server.ts create mode 100644 src/routes/[orgSlug]/events/[eventSlug]/+layout.svelte create mode 100644 src/routes/[orgSlug]/events/[eventSlug]/+page.svelte create mode 100644 supabase/migrations/022_events.sql diff --git a/src/lib/api/events.test.ts b/src/lib/api/events.test.ts new file mode 100644 index 0000000..7365d75 --- /dev/null +++ b/src/lib/api/events.test.ts @@ -0,0 +1,48 @@ +import { describe, it, expect } from 'vitest'; + +// Test the slugify logic (extracted inline since it's not exported) +function slugify(text: string): string { + return text + .toLowerCase() + .replace(/[^\w\s-]/g, '') + .replace(/[\s_]+/g, '-') + .replace(/-+/g, '-') + .replace(/^-|-$/g, '') + .slice(0, 60) || 'event'; +} + +describe('events API - slugify', () => { + it('converts simple name to slug', () => { + expect(slugify('Summer Conference')).toBe('summer-conference'); + }); + + it('handles special characters', () => { + expect(slugify('Music & Arts Festival 2026!')).toBe('music-arts-festival-2026'); + }); + + it('collapses multiple spaces and dashes', () => { + expect(slugify('My Big Event')).toBe('my-big-event'); + }); + + it('trims leading/trailing dashes', () => { + expect(slugify('--hello--')).toBe('hello'); + }); + + it('truncates to 60 characters', () => { + const longName = 'A'.repeat(100); + expect(slugify(longName).length).toBeLessThanOrEqual(60); + }); + + it('returns "event" for empty string', () => { + expect(slugify('')).toBe('event'); + }); + + it('handles unicode characters', () => { + const result = slugify('Ürituse Korraldamine'); + expect(result).toBe('rituse-korraldamine'); + }); + + it('handles numbers', () => { + expect(slugify('Event 2026 Q1')).toBe('event-2026-q1'); + }); +}); diff --git a/src/lib/api/events.ts b/src/lib/api/events.ts new file mode 100644 index 0000000..ed5711f --- /dev/null +++ b/src/lib/api/events.ts @@ -0,0 +1,277 @@ +import type { SupabaseClient } from '@supabase/supabase-js'; +import type { Database } from '$lib/supabase/types'; +import { createLogger } from '$lib/utils/logger'; + +const log = createLogger('api.events'); + +export interface Event { + id: string; + org_id: string; + name: string; + slug: string; + description: string | null; + status: 'planning' | 'active' | 'completed' | 'archived'; + start_date: string | null; + end_date: string | null; + venue_name: string | null; + venue_address: string | null; + cover_image_url: string | null; + color: string | null; + created_by: string | null; + created_at: string; + updated_at: string; +} + +export interface EventMember { + id: string; + event_id: string; + user_id: string; + role: 'lead' | 'manager' | 'member'; + assigned_at: string; +} + +export interface EventWithCounts extends Event { + member_count: number; +} + +function slugify(text: string): string { + return text + .toLowerCase() + .replace(/[^\w\s-]/g, '') + .replace(/[\s_]+/g, '-') + .replace(/-+/g, '-') + .replace(/^-|-$/g, '') + .slice(0, 60) || 'event'; +} + +export async function fetchEvents( + supabase: SupabaseClient, + orgId: string, + status?: string +): Promise { + let query = supabase + .from('events') + .select('*, event_members(count)') + .eq('org_id', orgId) + .order('start_date', { ascending: true, nullsFirst: false }); + + if (status && status !== 'all') { + query = query.eq('status', status); + } + + const { data, error } = await query; + + if (error) { + log.error('fetchEvents failed', { error, data: { orgId } }); + throw error; + } + + const events: EventWithCounts[] = (data ?? []).map((e: any) => ({ + ...e, + member_count: e.event_members?.[0]?.count ?? 0, + event_members: undefined, + })); + + log.debug('fetchEvents ok', { data: { count: events.length } }); + return events; +} + +export async function fetchEvent( + supabase: SupabaseClient, + eventId: string +): Promise { + const { data, error } = await supabase + .from('events') + .select('*') + .eq('id', eventId) + .single(); + + if (error) { + if (error.code === 'PGRST116') return null; + log.error('fetchEvent failed', { error, data: { eventId } }); + throw error; + } + return data as unknown as Event; +} + +export async function fetchEventBySlug( + supabase: SupabaseClient, + orgId: string, + eventSlug: string +): Promise { + const { data, error } = await supabase + .from('events') + .select('*') + .eq('org_id', orgId) + .eq('slug', eventSlug) + .single(); + + if (error) { + if (error.code === 'PGRST116') return null; + log.error('fetchEventBySlug failed', { error, data: { orgId, eventSlug } }); + throw error; + } + return data as unknown as Event; +} + +export async function createEvent( + supabase: SupabaseClient, + orgId: string, + userId: string, + params: { + name: string; + description?: string; + start_date?: string; + end_date?: string; + venue_name?: string; + venue_address?: string; + color?: string; + } +): Promise { + const baseSlug = slugify(params.name); + + // Ensure unique slug within org + const { data: existing } = await supabase + .from('events') + .select('slug') + .eq('org_id', orgId) + .like('slug', `${baseSlug}%`); + + let slug = baseSlug; + if (existing && existing.length > 0) { + const existingSlugs = new Set(existing.map((e: any) => e.slug)); + if (existingSlugs.has(slug)) { + let i = 2; + while (existingSlugs.has(`${baseSlug}-${i}`)) i++; + slug = `${baseSlug}-${i}`; + } + } + + const { data, error } = await supabase + .from('events') + .insert({ + org_id: orgId, + name: params.name, + slug, + description: params.description ?? null, + start_date: params.start_date ?? null, + end_date: params.end_date ?? null, + venue_name: params.venue_name ?? null, + venue_address: params.venue_address ?? null, + color: params.color ?? null, + created_by: userId, + }) + .select() + .single(); + + if (error) { + log.error('createEvent failed', { error, data: { orgId, name: params.name } }); + throw error; + } + log.info('createEvent ok', { data: { id: data.id, name: params.name, slug } }); + return data as unknown as Event; +} + +export async function updateEvent( + supabase: SupabaseClient, + eventId: string, + params: Partial> +): Promise { + const { data, error } = await supabase + .from('events') + .update({ ...params, updated_at: new Date().toISOString() }) + .eq('id', eventId) + .select() + .single(); + + if (error) { + log.error('updateEvent failed', { error, data: { eventId } }); + throw error; + } + log.info('updateEvent ok', { data: { id: data.id } }); + return data as unknown as Event; +} + +export async function deleteEvent( + supabase: SupabaseClient, + eventId: string +): Promise { + const { error } = await supabase + .from('events') + .delete() + .eq('id', eventId); + + if (error) { + log.error('deleteEvent failed', { error, data: { eventId } }); + throw error; + } + log.info('deleteEvent ok', { data: { eventId } }); +} + +export async function fetchEventMembers( + supabase: SupabaseClient, + eventId: string +): Promise<(EventMember & { profile?: { id: string; email: string; full_name: string | null; avatar_url: string | null } })[]> { + const { data: members, error } = await supabase + .from('event_members') + .select('*') + .eq('event_id', eventId) + .order('assigned_at'); + + if (error) { + log.error('fetchEventMembers failed', { error, data: { eventId } }); + throw error; + } + + if (!members || members.length === 0) return []; + + // Fetch profiles separately (same pattern as org_members) + const userIds = members.map((m: any) => m.user_id); + const { data: profiles } = await supabase + .from('profiles') + .select('id, email, full_name, avatar_url') + .in('id', userIds); + + const profileMap = Object.fromEntries((profiles ?? []).map(p => [p.id, p])); + + return members.map((m: any) => ({ + ...m, + profile: profileMap[m.user_id] ?? undefined, + })); +} + +export async function addEventMember( + supabase: SupabaseClient, + eventId: string, + userId: string, + role: 'lead' | 'manager' | 'member' = 'member' +): Promise { + const { data, error } = await supabase + .from('event_members') + .upsert({ event_id: eventId, user_id: userId, role }, { onConflict: 'event_id,user_id' }) + .select() + .single(); + + if (error) { + log.error('addEventMember failed', { error, data: { eventId, userId } }); + throw error; + } + return data as unknown as EventMember; +} + +export async function removeEventMember( + supabase: SupabaseClient, + eventId: string, + userId: string +): Promise { + const { error } = await supabase + .from('event_members') + .delete() + .eq('event_id', eventId) + .eq('user_id', userId); + + if (error) { + log.error('removeEventMember failed', { error, data: { eventId, userId } }); + throw error; + } +} diff --git a/src/lib/supabase/types.ts b/src/lib/supabase/types.ts index 50bac1e..b7df722 100644 --- a/src/lib/supabase/types.ts +++ b/src/lib/supabase/types.ts @@ -360,6 +360,100 @@ export type Database = { }, ] } + event_members: { + Row: { + assigned_at: string | null + event_id: string + id: string + role: string + user_id: string + } + Insert: { + assigned_at?: string | null + event_id: string + id?: string + role?: string + user_id: string + } + Update: { + assigned_at?: string | null + event_id?: string + id?: string + role?: string + user_id?: string + } + Relationships: [ + { + foreignKeyName: "event_members_event_id_fkey" + columns: ["event_id"] + isOneToOne: false + referencedRelation: "events" + referencedColumns: ["id"] + }, + ] + } + events: { + Row: { + color: string | null + cover_image_url: string | null + created_at: string | null + created_by: string | null + description: string | null + end_date: string | null + id: string + name: string + org_id: string + slug: string + start_date: string | null + status: string + updated_at: string | null + venue_address: string | null + venue_name: string | null + } + Insert: { + color?: string | null + cover_image_url?: string | null + created_at?: string | null + created_by?: string | null + description?: string | null + end_date?: string | null + id?: string + name: string + org_id: string + slug: string + start_date?: string | null + status?: string + updated_at?: string | null + venue_address?: string | null + venue_name?: string | null + } + Update: { + color?: string | null + cover_image_url?: string | null + created_at?: string | null + created_by?: string | null + description?: string | null + end_date?: string | null + id?: string + name?: string + org_id?: string + slug?: string + start_date?: string | null + status?: string + updated_at?: string | null + venue_address?: string | null + venue_name?: string | null + } + Relationships: [ + { + foreignKeyName: "events_org_id_fkey" + columns: ["org_id"] + isOneToOne: false + referencedRelation: "organizations" + referencedColumns: ["id"] + }, + ] + } kanban_boards: { Row: { created_at: string | null @@ -1226,3 +1320,5 @@ export type OrgGoogleCalendar = PublicTables['org_google_calendars']['Row'] export type ActivityLog = PublicTables['activity_log']['Row'] export type UserPreferences = PublicTables['user_preferences']['Row'] export type MatrixCredentials = PublicTables['matrix_credentials']['Row'] +export type EventRow = PublicTables['events']['Row'] +export type EventMemberRow = PublicTables['event_members']['Row'] diff --git a/src/routes/[orgSlug]/+layout.svelte b/src/routes/[orgSlug]/+layout.svelte index e091ca9..51a7d42 100644 --- a/src/routes/[orgSlug]/+layout.svelte +++ b/src/routes/[orgSlug]/+layout.svelte @@ -123,6 +123,11 @@ }, ] : []), + { + href: `/${data.org.slug}/events`, + label: "Events", + icon: "celebration", + }, { href: `/${data.org.slug}/chat`, label: "Chat", @@ -349,9 +354,11 @@ ? "files" : target.includes("/calendar") ? "calendar" - : target.includes("/settings") - ? "settings" - : "default"} + : target.includes("/events") + ? "default" + : target.includes("/settings") + ? "settings" + : "default"} {:else} {@render children()} diff --git a/src/routes/[orgSlug]/events/+page.server.ts b/src/routes/[orgSlug]/events/+page.server.ts new file mode 100644 index 0000000..a5f8810 --- /dev/null +++ b/src/routes/[orgSlug]/events/+page.server.ts @@ -0,0 +1,29 @@ +import { error } from '@sveltejs/kit'; +import type { PageServerLoad } from './$types'; +import { fetchEvents } from '$lib/api/events'; +import { createLogger } from '$lib/utils/logger'; + +const log = createLogger('page.events'); + +export const load: PageServerLoad = async ({ params, locals, url }) => { + const { session, user } = await locals.safeGetSession(); + if (!session || !user) error(401, 'Unauthorized'); + + const { data: org } = await locals.supabase + .from('organizations') + .select('id') + .eq('slug', params.orgSlug) + .single(); + + if (!org) error(404, 'Organization not found'); + + const statusFilter = url.searchParams.get('status') || 'all'; + + try { + const events = await fetchEvents(locals.supabase, org.id, statusFilter); + return { events, statusFilter }; + } catch (e: any) { + log.error('Failed to load events', { error: e, data: { orgId: org.id } }); + return { events: [], statusFilter }; + } +}; diff --git a/src/routes/[orgSlug]/events/+page.svelte b/src/routes/[orgSlug]/events/+page.svelte new file mode 100644 index 0000000..da0ff0a --- /dev/null +++ b/src/routes/[orgSlug]/events/+page.svelte @@ -0,0 +1,499 @@ + + + + Events | {data.org.name} + + +
+ +
+
+

Events

+

+ Organize and manage your events +

+
+ {#if isEditor} + + {/if} +
+ + +
+ {#each statusTabs as tab} + + {/each} +
+ + +
+ {#if data.events.length === 0} +
+ celebration +

No events yet

+

+ Create your first event to get started +

+ {#if isEditor} + + {/if} +
+ {:else} + + {/if} +
+
+ + +{#if showCreateModal} + +
e.key === "Escape" && (showCreateModal = false)} + onclick={(e) => e.target === e.currentTarget && (showCreateModal = false)} + role="dialog" + aria-modal="true" + aria-label="Create Event" + > +
+
+

Create Event

+ +
+ +
{ + e.preventDefault(); + handleCreate(); + }} + > + +
+ + +
+ + +
+ + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+ + +
+ +
+ {#each presetColors as color} + + {/each} +
+
+ + +
+ + +
+
+
+
+{/if} diff --git a/src/routes/[orgSlug]/events/[eventSlug]/+layout.server.ts b/src/routes/[orgSlug]/events/[eventSlug]/+layout.server.ts new file mode 100644 index 0000000..2b45be1 --- /dev/null +++ b/src/routes/[orgSlug]/events/[eventSlug]/+layout.server.ts @@ -0,0 +1,27 @@ +import { error } from '@sveltejs/kit'; +import type { LayoutServerLoad } from './$types'; +import { fetchEventBySlug, fetchEventMembers } from '$lib/api/events'; +import { createLogger } from '$lib/utils/logger'; + +const log = createLogger('page.event-detail'); + +export const load: LayoutServerLoad = async ({ params, locals, parent }) => { + const { session, user } = await locals.safeGetSession(); + if (!session || !user) error(401, 'Unauthorized'); + + const parentData = await parent() as { org: { id: string; name: string; slug: string } }; + const orgId = parentData.org.id; + + try { + const event = await fetchEventBySlug(locals.supabase, orgId, params.eventSlug); + if (!event) error(404, 'Event not found'); + + const members = await fetchEventMembers(locals.supabase, event.id); + + return { event, eventMembers: members }; + } catch (e: any) { + if (e?.status === 404) throw e; + log.error('Failed to load event', { error: e, data: { orgId, eventSlug: params.eventSlug } }); + error(500, 'Failed to load event'); + } +}; diff --git a/src/routes/[orgSlug]/events/[eventSlug]/+layout.svelte b/src/routes/[orgSlug]/events/[eventSlug]/+layout.svelte new file mode 100644 index 0000000..b7fdfda --- /dev/null +++ b/src/routes/[orgSlug]/events/[eventSlug]/+layout.svelte @@ -0,0 +1,200 @@ + + +
+ + + + +
+ {@render children()} +
+
diff --git a/src/routes/[orgSlug]/events/[eventSlug]/+page.svelte b/src/routes/[orgSlug]/events/[eventSlug]/+page.svelte new file mode 100644 index 0000000..0e3aff2 --- /dev/null +++ b/src/routes/[orgSlug]/events/[eventSlug]/+page.svelte @@ -0,0 +1,563 @@ + + + + {data.event.name} | {data.org.name} + + +
+ +
+
+
+ {#if editing} + + {:else} +
+
+

+ {data.event.name} +

+
+ {/if} + +
+ {#if editing} + + {:else} + + {statusOptions.find( + (s) => s.value === data.event.status, + )?.icon ?? "help"} + {data.event.status} + + {/if} + + {#if data.event.start_date && !editing} + + calendar_today + {formatDate(data.event.start_date)} + + {@const countdown = daysUntilEvent()} + {#if countdown} + {countdown} + {/if} + {/if} + + {#if data.event.venue_name && !editing} + + location_on + {data.event.venue_name} + + {/if} +
+
+ + {#if isEditor} +
+ {#if editing} + + + {:else} + + + {/if} +
+ {/if} +
+
+ + + {#if editing} +
+ +
+ + + + +
+
+ {/if} + + +
+ + {#if data.event.description && !editing} +

+ {data.event.description} +

+ {/if} + + +

Modules

+
+ {#each moduleCards as mod} + +
+ {mod.icon} +
+

+ {mod.label} +

+

{mod.description}

+
+ {/each} +
+ + +
+ +
+

+ Event Details +

+
+
+ calendar_today +
+

+ Start Date +

+

+ {formatDate(data.event.start_date)} +

+
+
+
+ event +
+

End Date

+

+ {formatDate(data.event.end_date)} +

+
+
+ {#if data.event.venue_name} +
+ location_on +
+

Venue

+

+ {data.event.venue_name} +

+ {#if data.event.venue_address} +

+ {data.event.venue_address} +

+ {/if} +
+
+ {/if} +
+
+ + +
+
+

+ Team ({data.eventMembers.length}) +

+ Manage +
+
+ {#each data.eventMembers.slice(0, 6) as member} +
+ +
+

+ {member.profile?.full_name || + member.profile?.email || + "Unknown"} +

+

+ {member.role} +

+
+
+ {/each} + {#if data.eventMembers.length > 6} + + +{data.eventMembers.length - 6} more + + {/if} + {#if data.eventMembers.length === 0} +

+ No team members assigned yet +

+ {/if} +
+
+
+
+
+ + +{#if showDeleteConfirm} + +
e.key === "Escape" && (showDeleteConfirm = false)} + onclick={(e) => + e.target === e.currentTarget && (showDeleteConfirm = false)} + role="dialog" + aria-modal="true" + aria-label="Delete Event" + > +
+

Delete Event?

+

+ This will permanently delete {data.event.name} + and all its data. This action cannot be undone. +

+
+ + +
+
+
+{/if} diff --git a/supabase/migrations/022_events.sql b/supabase/migrations/022_events.sql new file mode 100644 index 0000000..44813e8 --- /dev/null +++ b/supabase/migrations/022_events.sql @@ -0,0 +1,84 @@ +-- Events: the core project entity within an organization +-- Each event represents a project (conference, festival, meetup, etc.) + +CREATE TABLE events ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, + name TEXT NOT NULL, + slug TEXT NOT NULL, + description TEXT, + status TEXT NOT NULL DEFAULT 'planning' CHECK (status IN ('planning', 'active', 'completed', 'archived')), + start_date DATE, + end_date DATE, + venue_name TEXT, + venue_address TEXT, + cover_image_url TEXT, + color TEXT, + created_by UUID REFERENCES auth.users(id), + created_at TIMESTAMPTZ DEFAULT now(), + updated_at TIMESTAMPTZ DEFAULT now(), + UNIQUE(org_id, slug) +); + +-- Event members: subset of org members assigned to an event +CREATE TABLE event_members ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + event_id UUID NOT NULL REFERENCES events(id) ON DELETE CASCADE, + user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, + role TEXT NOT NULL DEFAULT 'member' CHECK (role IN ('lead', 'manager', 'member')), + assigned_at TIMESTAMPTZ DEFAULT now(), + UNIQUE(event_id, user_id) +); + +-- Indexes +CREATE INDEX idx_events_org ON events(org_id); +CREATE INDEX idx_events_status ON events(org_id, status); +CREATE INDEX idx_events_dates ON events(start_date, end_date); +CREATE INDEX idx_event_members_event ON event_members(event_id); +CREATE INDEX idx_event_members_user ON event_members(user_id); + +-- RLS +ALTER TABLE events ENABLE ROW LEVEL SECURITY; +ALTER TABLE event_members ENABLE ROW LEVEL SECURITY; + +-- Events: org members can view, editors+ can manage +CREATE POLICY "Org members can view events" ON events FOR SELECT + USING (EXISTS (SELECT 1 FROM org_members WHERE org_id = events.org_id AND user_id = auth.uid())); + +CREATE POLICY "Editors can manage events" ON events FOR ALL + USING (EXISTS (SELECT 1 FROM org_members WHERE org_id = events.org_id AND user_id = auth.uid() AND role IN ('owner', 'admin', 'editor'))); + +-- Event members: org members can view, editors+ can manage +CREATE POLICY "Org members can view event members" ON event_members FOR SELECT + USING (EXISTS ( + SELECT 1 FROM events e + JOIN org_members om ON e.org_id = om.org_id + WHERE e.id = event_members.event_id AND om.user_id = auth.uid() + )); + +CREATE POLICY "Editors can manage event members" ON event_members FOR ALL + USING (EXISTS ( + SELECT 1 FROM events e + JOIN org_members om ON e.org_id = om.org_id + WHERE e.id = event_members.event_id AND om.user_id = auth.uid() AND om.role IN ('owner', 'admin', 'editor') + )); + +-- Enable realtime +ALTER PUBLICATION supabase_realtime ADD TABLE events; + +-- Auto-add creator as event lead +CREATE OR REPLACE FUNCTION public.handle_new_event() +RETURNS TRIGGER AS $$ +BEGIN + IF NEW.created_by IS NOT NULL THEN + INSERT INTO public.event_members (event_id, user_id, role) + VALUES (NEW.id, NEW.created_by, 'lead') + ON CONFLICT (event_id, user_id) DO NOTHING; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +CREATE TRIGGER on_event_created + AFTER INSERT ON events + FOR EACH ROW EXECUTE FUNCTION public.handle_new_event(); From 36496e8cdb8a4a952b6114a1db2d18e1077426fc Mon Sep 17 00:00:00 2001 From: AlacrisDevs Date: Sat, 7 Feb 2026 10:09:00 +0200 Subject: [PATCH 02/10] fix: event detail SSR children guard, state_referenced_locally warnings, a11y warnings --- src/routes/[orgSlug]/events/+page.svelte | 4 +++ .../events/[eventSlug]/+layout.svelte | 4 ++- .../[orgSlug]/events/[eventSlug]/+page.svelte | 28 ++++++++++++++----- 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/routes/[orgSlug]/events/+page.svelte b/src/routes/[orgSlug]/events/+page.svelte index da0ff0a..e1d0e80 100644 --- a/src/routes/[orgSlug]/events/+page.svelte +++ b/src/routes/[orgSlug]/events/+page.svelte @@ -339,6 +339,7 @@ {#if showCreateModal} +
e.key === "Escape" && (showCreateModal = false)} @@ -356,6 +357,7 @@ type="button" class="text-light/40 hover:text-white transition-colors" onclick={() => (showCreateModal = false)} + aria-label="Close" >
+ @@ -466,6 +469,7 @@ : 'border-transparent hover:border-light/30'}" style="background-color: {color}" onclick={() => (newEventColor = color)} + aria-label="Select color {color}" > {/each}
diff --git a/src/routes/[orgSlug]/events/[eventSlug]/+layout.svelte b/src/routes/[orgSlug]/events/[eventSlug]/+layout.svelte index b7fdfda..006bbde 100644 --- a/src/routes/[orgSlug]/events/[eventSlug]/+layout.svelte +++ b/src/routes/[orgSlug]/events/[eventSlug]/+layout.svelte @@ -195,6 +195,8 @@
- {@render children()} + {#if children} + {@render children()} + {/if}
diff --git a/src/routes/[orgSlug]/events/[eventSlug]/+page.svelte b/src/routes/[orgSlug]/events/[eventSlug]/+page.svelte index 0e3aff2..619c30a 100644 --- a/src/routes/[orgSlug]/events/[eventSlug]/+page.svelte +++ b/src/routes/[orgSlug]/events/[eventSlug]/+page.svelte @@ -33,15 +33,28 @@ // Edit mode let editing = $state(false); - let editName = $state(data.event.name); - let editDescription = $state(data.event.description ?? ""); - let editStatus = $state(data.event.status); - let editStartDate = $state(data.event.start_date ?? ""); - let editEndDate = $state(data.event.end_date ?? ""); - let editVenueName = $state(data.event.venue_name ?? ""); - let editVenueAddress = $state(data.event.venue_address ?? ""); + let editName = $state(""); + let editDescription = $state(""); + let editStatus = $state("planning"); + let editStartDate = $state(""); + let editEndDate = $state(""); + let editVenueName = $state(""); + let editVenueAddress = $state(""); let saving = $state(false); + // Sync edit fields when data changes or edit mode opens + $effect(() => { + if (editing) { + editName = data.event.name; + editDescription = data.event.description ?? ""; + editStatus = data.event.status; + editStartDate = data.event.start_date ?? ""; + editEndDate = data.event.end_date ?? ""; + editVenueName = data.event.venue_name ?? ""; + editVenueAddress = data.event.venue_address ?? ""; + } + }); + // Delete confirmation let showDeleteConfirm = $state(false); let deleting = $state(false); @@ -522,6 +535,7 @@ {#if showDeleteConfirm} +
e.key === "Escape" && (showDeleteConfirm = false)} From fe6ec6e0afc9e48fddc7308290f8a42c34127226 Mon Sep 17 00:00:00 2001 From: AlacrisDevs Date: Sat, 7 Feb 2026 10:16:13 +0200 Subject: [PATCH 03/10] i18n: add Paraglide messages for all events pages (EN + ET) --- messages/en.json | 71 +++++++++++++- messages/et.json | 71 +++++++++++++- src/routes/[orgSlug]/+layout.svelte | 2 +- src/routes/[orgSlug]/events/+page.svelte | 63 ++++++------ .../events/[eventSlug]/+layout.svelte | 21 ++-- .../[orgSlug]/events/[eventSlug]/+page.svelte | 98 +++++++++---------- 6 files changed, 232 insertions(+), 94 deletions(-) diff --git a/messages/en.json b/messages/en.json index bdd2a21..2f9bed3 100644 --- a/messages/en.json +++ b/messages/en.json @@ -251,5 +251,74 @@ "entity_kanban_column": "column", "entity_member": "member", "entity_role": "role", - "entity_invite": "invite" + "entity_invite": "invite", + "entity_event": "event", + "nav_events": "Events", + "events_title": "Events", + "events_subtitle": "Organize and manage your events", + "events_new": "New Event", + "events_create": "Create Event", + "events_empty_title": "No events yet", + "events_empty_desc": "Create your first event to get started", + "events_no_dates": "No dates set", + "events_tab_all": "All Events", + "events_tab_planning": "Planning", + "events_tab_active": "Active", + "events_tab_completed": "Completed", + "events_tab_archived": "Archived", + "events_status_planning": "Planning", + "events_status_active": "Active", + "events_status_completed": "Completed", + "events_status_archived": "Archived", + "events_form_name": "Event Name", + "events_form_name_placeholder": "e.g., Summer Conference 2026", + "events_form_description": "Description", + "events_form_description_placeholder": "Brief description of the event...", + "events_form_start_date": "Start Date", + "events_form_end_date": "End Date", + "events_form_venue": "Venue", + "events_form_venue_placeholder": "e.g., Convention Center", + "events_form_venue_address_placeholder": "Venue address", + "events_form_color": "Color", + "events_form_select_color": "Select color {color}", + "events_creating": "Creating...", + "events_saving": "Saving...", + "events_deleting": "Deleting...", + "events_updated": "Event updated", + "events_created": "Event \"{name}\" created", + "events_deleted": "Event deleted", + "events_delete_title": "Delete Event?", + "events_delete_desc": "This will permanently delete {name} and all its data. This action cannot be undone.", + "events_delete_confirm": "Delete Event", + "events_days_ago": "{count} days ago", + "events_today": "Today!", + "events_tomorrow": "Tomorrow", + "events_in_days": "In {count} days", + "events_overview": "Overview", + "events_modules": "Modules", + "events_details": "Event Details", + "events_start_date": "Start Date", + "events_end_date": "End Date", + "events_venue": "Venue", + "events_not_set": "Not set", + "events_all_events": "All Events", + "events_team": "Team", + "events_team_count": "Team ({count})", + "events_team_manage": "Manage", + "events_team_empty": "No team members assigned yet", + "events_more_members": "+{count} more", + "events_mod_tasks": "Tasks", + "events_mod_tasks_desc": "Manage tasks, milestones, and progress", + "events_mod_files": "Files", + "events_mod_files_desc": "Documents, contracts, and media", + "events_mod_schedule": "Schedule", + "events_mod_schedule_desc": "Event timeline and program", + "events_mod_budget": "Budget", + "events_mod_budget_desc": "Income, expenses, and tracking", + "events_mod_guests": "Guests", + "events_mod_guests_desc": "Guest list and registration", + "events_mod_team": "Team", + "events_mod_team_desc": "Team members and shift scheduling", + "events_mod_sponsors": "Sponsors", + "events_mod_sponsors_desc": "Sponsors, partners, and deliverables" } \ No newline at end of file diff --git a/messages/et.json b/messages/et.json index deba226..b3be0d3 100644 --- a/messages/et.json +++ b/messages/et.json @@ -251,5 +251,74 @@ "entity_kanban_column": "veeru", "entity_member": "liikme", "entity_role": "rolli", - "entity_invite": "kutse" + "entity_invite": "kutse", + "entity_event": "ürituse", + "nav_events": "Üritused", + "events_title": "Üritused", + "events_subtitle": "Korralda ja halda oma üritusi", + "events_new": "Uus üritus", + "events_create": "Loo üritus", + "events_empty_title": "Üritusi pole veel", + "events_empty_desc": "Loo oma esimene üritus alustamiseks", + "events_no_dates": "Kuupäevad määramata", + "events_tab_all": "Kõik üritused", + "events_tab_planning": "Planeerimisel", + "events_tab_active": "Aktiivne", + "events_tab_completed": "Lõpetatud", + "events_tab_archived": "Arhiveeritud", + "events_status_planning": "Planeerimisel", + "events_status_active": "Aktiivne", + "events_status_completed": "Lõpetatud", + "events_status_archived": "Arhiveeritud", + "events_form_name": "Ürituse nimi", + "events_form_name_placeholder": "nt Suvekonverents 2026", + "events_form_description": "Kirjeldus", + "events_form_description_placeholder": "Ürituse lühikirjeldus...", + "events_form_start_date": "Alguskuupäev", + "events_form_end_date": "Lõppkuupäev", + "events_form_venue": "Toimumiskoht", + "events_form_venue_placeholder": "nt Konverentsikeskus", + "events_form_venue_address_placeholder": "Toimumiskoha aadress", + "events_form_color": "Värv", + "events_form_select_color": "Vali värv {color}", + "events_creating": "Loomine...", + "events_saving": "Salvestamine...", + "events_deleting": "Kustutamine...", + "events_updated": "Üritus uuendatud", + "events_created": "Üritus \"{name}\" loodud", + "events_deleted": "Üritus kustutatud", + "events_delete_title": "Kustuta üritus?", + "events_delete_desc": "See kustutab jäädavalt ürituse {name} ja kõik selle andmed. Seda toimingut ei saa tagasi võtta.", + "events_delete_confirm": "Kustuta üritus", + "events_days_ago": "{count} päeva tagasi", + "events_today": "Täna!", + "events_tomorrow": "Homme", + "events_in_days": "{count} päeva pärast", + "events_overview": "Ülevaade", + "events_modules": "Moodulid", + "events_details": "Ürituse andmed", + "events_start_date": "Alguskuupäev", + "events_end_date": "Lõppkuupäev", + "events_venue": "Toimumiskoht", + "events_not_set": "Määramata", + "events_all_events": "Kõik üritused", + "events_team": "Meeskond", + "events_team_count": "Meeskond ({count})", + "events_team_manage": "Halda", + "events_team_empty": "Meeskonnaliikmeid pole veel määratud", + "events_more_members": "+{count} veel", + "events_mod_tasks": "Ülesanded", + "events_mod_tasks_desc": "Halda ülesandeid, verstaposte ja edenemist", + "events_mod_files": "Failid", + "events_mod_files_desc": "Dokumendid, lepingud ja meedia", + "events_mod_schedule": "Ajakava", + "events_mod_schedule_desc": "Ürituse ajakava ja programm", + "events_mod_budget": "Eelarve", + "events_mod_budget_desc": "Tulud, kulud ja jälgimine", + "events_mod_guests": "Külalised", + "events_mod_guests_desc": "Külaliste nimekiri ja registreerimine", + "events_mod_team": "Meeskond", + "events_mod_team_desc": "Meeskonnaliikmed ja vahetuste planeerimine", + "events_mod_sponsors": "Sponsorid", + "events_mod_sponsors_desc": "Sponsorid, partnerid ja kohustused" } \ No newline at end of file diff --git a/src/routes/[orgSlug]/+layout.svelte b/src/routes/[orgSlug]/+layout.svelte index 51a7d42..9cffd9e 100644 --- a/src/routes/[orgSlug]/+layout.svelte +++ b/src/routes/[orgSlug]/+layout.svelte @@ -125,7 +125,7 @@ : []), { href: `/${data.org.slug}/events`, - label: "Events", + label: m.nav_events(), icon: "celebration", }, { diff --git a/src/routes/[orgSlug]/events/+page.svelte b/src/routes/[orgSlug]/events/+page.svelte index e1d0e80..16bd286 100644 --- a/src/routes/[orgSlug]/events/+page.svelte +++ b/src/routes/[orgSlug]/events/+page.svelte @@ -6,6 +6,7 @@ import type { SupabaseClient } from "@supabase/supabase-js"; import type { Database } from "$lib/supabase/types"; import { toasts } from "$lib/stores/ui"; + import * as m from "$lib/paraglide/messages"; interface EventItem { id: string; @@ -48,13 +49,13 @@ let newEventColor = $state("#00A3E0"); let creating = $state(false); - const statusTabs = [ - { value: "all", label: "All Events", icon: "apps" }, - { value: "planning", label: "Planning", icon: "edit_note" }, - { value: "active", label: "Active", icon: "play_circle" }, - { value: "completed", label: "Completed", icon: "check_circle" }, - { value: "archived", label: "Archived", icon: "archive" }, - ]; + const statusTabs = $derived([ + { value: "all", label: m.events_tab_all(), icon: "apps" }, + { value: "planning", label: m.events_tab_planning(), icon: "edit_note" }, + { value: "active", label: m.events_tab_active(), icon: "play_circle" }, + { value: "completed", label: m.events_tab_completed(), icon: "check_circle" }, + { value: "archived", label: m.events_tab_archived(), icon: "archive" }, + ]); const presetColors = [ "#00A3E0", @@ -100,7 +101,7 @@ start: string | null, end: string | null, ): string { - if (!start && !end) return "No dates set"; + 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)}`; @@ -134,7 +135,7 @@ if (error) throw error; - toasts.success(`Event "${created.name}" created`); + toasts.success(m.events_created({ name: created.name })); showCreateModal = false; resetForm(); goto(`/${data.org.slug}/events/${created.slug}`); @@ -166,7 +167,7 @@ - Events | {data.org.name} + {m.events_title()} | {data.org.name}
@@ -175,9 +176,9 @@ class="flex items-center justify-between px-6 py-5 border-b border-light/5" >
-

Events

+

{m.events_title()}

- Organize and manage your events + {m.events_subtitle()}

{#if isEditor} @@ -191,7 +192,7 @@ style="font-size: 20px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20;" >add - New Event + {m.events_new()} {/if} @@ -228,9 +229,9 @@ style="font-size: 64px; font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 48;" >celebration -

No events yet

+

{m.events_empty_title()}

- Create your first event to get started + {m.events_empty_desc()}

{#if isEditor} {/if}
@@ -346,18 +347,18 @@ onclick={(e) => e.target === e.currentTarget && (showCreateModal = false)} role="dialog" aria-modal="true" - aria-label="Create Event" + aria-label={m.events_create()} >
-

Create Event

+

{m.events_create()}

@@ -457,7 +458,7 @@
{m.events_form_color()}
{#each presetColors as color} @@ -469,7 +470,7 @@ : 'border-transparent hover:border-light/30'}" style="background-color: {color}" onclick={() => (newEventColor = color)} - aria-label="Select color {color}" + aria-label={m.events_form_select_color({ color })} > {/each}
@@ -487,14 +488,14 @@ resetForm(); }} > - Cancel + {m.btn_cancel()}
diff --git a/src/routes/[orgSlug]/events/[eventSlug]/+layout.svelte b/src/routes/[orgSlug]/events/[eventSlug]/+layout.svelte index 006bbde..702a6bb 100644 --- a/src/routes/[orgSlug]/events/[eventSlug]/+layout.svelte +++ b/src/routes/[orgSlug]/events/[eventSlug]/+layout.svelte @@ -3,6 +3,7 @@ import { Avatar } from "$lib/components/ui"; import type { Snippet } from "svelte"; import type { Event, EventMember } from "$lib/api/events"; + import * as m from "$lib/paraglide/messages"; interface Props { data: { @@ -30,43 +31,43 @@ const modules = $derived([ { href: basePath, - label: "Overview", + label: m.events_overview(), icon: "dashboard", exact: true, }, { href: `${basePath}/tasks`, - label: "Tasks", + label: m.events_mod_tasks(), icon: "task_alt", }, { href: `${basePath}/files`, - label: "Files", + label: m.events_mod_files(), icon: "folder", }, { href: `${basePath}/schedule`, - label: "Schedule", + label: m.events_mod_schedule(), icon: "calendar_today", }, { href: `${basePath}/budget`, - label: "Budget", + label: m.events_mod_budget(), icon: "account_balance_wallet", }, { href: `${basePath}/guests`, - label: "Guests", + label: m.events_mod_guests(), icon: "groups", }, { href: `${basePath}/team`, - label: "Team", + label: m.events_mod_team(), icon: "badge", }, { href: `${basePath}/sponsors`, - label: "Sponsors", + label: m.events_mod_sponsors(), icon: "handshake", }, ]); @@ -155,7 +156,7 @@

- Team ({data.eventMembers.length}) + {m.events_team_count({ count: String(data.eventMembers.length) })}

{#each data.eventMembers.slice(0, 8) as member} @@ -189,7 +190,7 @@ style="font-size: 16px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 16;" >arrow_back - All Events + {m.events_all_events()} diff --git a/src/routes/[orgSlug]/events/[eventSlug]/+page.svelte b/src/routes/[orgSlug]/events/[eventSlug]/+page.svelte index 619c30a..a2832a3 100644 --- a/src/routes/[orgSlug]/events/[eventSlug]/+page.svelte +++ b/src/routes/[orgSlug]/events/[eventSlug]/+page.svelte @@ -6,6 +6,7 @@ import type { Database } from "$lib/supabase/types"; import { toasts } from "$lib/stores/ui"; import type { Event, EventMember } from "$lib/api/events"; + import * as m from "$lib/paraglide/messages"; interface Props { data: { @@ -63,74 +64,74 @@ `/${data.org.slug}/events/${data.event.slug}`, ); - const statusOptions = [ - { value: "planning", label: "Planning", icon: "edit_note", color: "text-amber-400" }, - { value: "active", label: "Active", icon: "play_circle", color: "text-emerald-400" }, - { value: "completed", label: "Completed", icon: "check_circle", color: "text-blue-400" }, - { value: "archived", label: "Archived", icon: "archive", color: "text-light/40" }, - ]; + const statusOptions = $derived([ + { value: "planning", label: m.events_status_planning(), icon: "edit_note", color: "text-amber-400" }, + { value: "active", label: m.events_status_active(), icon: "play_circle", color: "text-emerald-400" }, + { value: "completed", label: m.events_status_completed(), icon: "check_circle", color: "text-blue-400" }, + { value: "archived", label: m.events_status_archived(), icon: "archive", color: "text-light/40" }, + ]); const moduleCards = $derived([ { href: `${basePath}/tasks`, - label: "Tasks", + label: m.events_mod_tasks(), icon: "task_alt", - description: "Manage tasks, milestones, and progress", + description: m.events_mod_tasks_desc(), color: "text-emerald-400", bg: "bg-emerald-400/10", }, { href: `${basePath}/files`, - label: "Files", + label: m.events_mod_files(), icon: "folder", - description: "Documents, contracts, and media", + description: m.events_mod_files_desc(), color: "text-blue-400", bg: "bg-blue-400/10", }, { href: `${basePath}/schedule`, - label: "Schedule", + label: m.events_mod_schedule(), icon: "calendar_today", - description: "Event timeline and program", + description: m.events_mod_schedule_desc(), color: "text-purple-400", bg: "bg-purple-400/10", }, { href: `${basePath}/budget`, - label: "Budget", + label: m.events_mod_budget(), icon: "account_balance_wallet", - description: "Income, expenses, and tracking", + description: m.events_mod_budget_desc(), color: "text-amber-400", bg: "bg-amber-400/10", }, { href: `${basePath}/guests`, - label: "Guests", + label: m.events_mod_guests(), icon: "groups", - description: "Guest list and registration", + description: m.events_mod_guests_desc(), color: "text-pink-400", bg: "bg-pink-400/10", }, { href: `${basePath}/team`, - label: "Team", + label: m.events_mod_team(), icon: "badge", - description: "Team members and shift scheduling", + description: m.events_mod_team_desc(), color: "text-teal-400", bg: "bg-teal-400/10", }, { href: `${basePath}/sponsors`, - label: "Sponsors", + label: m.events_mod_sponsors(), icon: "handshake", - description: "Sponsors, partners, and deliverables", + description: m.events_mod_sponsors_desc(), color: "text-orange-400", bg: "bg-orange-400/10", }, ]); function formatDate(dateStr: string | null): string { - if (!dateStr) return "Not set"; + if (!dateStr) return m.events_not_set(); return new Date(dateStr).toLocaleDateString(undefined, { weekday: "short", month: "long", @@ -146,10 +147,10 @@ const diff = Math.ceil( (start.getTime() - now.getTime()) / (1000 * 60 * 60 * 24), ); - if (diff < 0) return `${Math.abs(diff)} days ago`; - if (diff === 0) return "Today!"; - if (diff === 1) return "Tomorrow"; - return `In ${diff} days`; + if (diff < 0) return m.events_days_ago({ count: String(Math.abs(diff)) }); + if (diff === 0) return m.events_today(); + if (diff === 1) return m.events_tomorrow(); + return m.events_in_days({ count: String(diff) }); } async function handleSave() { @@ -171,7 +172,7 @@ if (error) throw error; - toasts.success("Event updated"); + toasts.success(m.events_updated()); editing = false; // Refresh the page data goto(`/${data.org.slug}/events/${data.event.slug}`, { @@ -194,7 +195,7 @@ if (error) throw error; - toasts.success("Event deleted"); + toasts.success(m.events_deleted()); goto(`/${data.org.slug}/events`); } catch (e: any) { toasts.error(e.message || "Failed to delete event"); @@ -299,7 +300,7 @@ class="px-3 py-1.5 text-body-sm text-light/60 hover:text-white transition-colors" onclick={() => (editing = false)} > - Cancel + {m.btn_cancel()} {:else}
@@ -388,7 +389,7 @@ {/if} -

Modules

+

{m.events_modules()}

@@ -421,7 +422,7 @@

- Event Details + {m.events_details()}

@@ -432,7 +433,7 @@ >

- Start Date + {m.events_start_date()}

{formatDate(data.event.start_date)} @@ -446,7 +447,7 @@ >event

-

End Date

+

{m.events_end_date()}

{formatDate(data.event.end_date)}

@@ -460,7 +461,7 @@ >location_on
-

Venue

+

{m.events_venue()}

{data.event.venue_name}

@@ -479,12 +480,12 @@

- Team ({data.eventMembers.length}) + {m.events_team_count({ count: String(data.eventMembers.length) })}

Manage{m.events_team_manage()}
@@ -518,12 +519,12 @@ href="{basePath}/team" class="text-body-sm text-primary hover:underline text-center pt-1" > - +{data.eventMembers.length - 6} more + {m.events_more_members({ count: String(data.eventMembers.length - 6) })} {/if} {#if data.eventMembers.length === 0}

- No team members assigned yet + {m.events_team_empty()}

{/if}
@@ -543,17 +544,14 @@ e.target === e.currentTarget && (showDeleteConfirm = false)} role="dialog" aria-modal="true" - aria-label="Delete Event" + aria-label={m.events_delete_title()} >
-

Delete Event?

+

{m.events_delete_title()}

- This will permanently delete {data.event.name} - and all its data. This action cannot be undone. + {m.events_delete_desc({ name: data.event.name })}

From 2913912cb856e1f5e200eedf03ae6af778a84aee Mon Sep 17 00:00:00 2001 From: AlacrisDevs Date: Sat, 7 Feb 2026 10:44:53 +0200 Subject: [PATCH 04/10] 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 --- messages/en.json | 8 +- messages/et.json | 8 +- src/lib/components/ui/ActivityFeed.svelte | 132 ++++++ src/lib/components/ui/ContentSkeleton.svelte | 109 +++++ src/lib/components/ui/EventCard.svelte | 116 +++++ src/lib/components/ui/MemberList.svelte | 71 +++ src/lib/components/ui/ModuleCard.svelte | 40 ++ src/lib/components/ui/PageHeader.svelte | 46 ++ src/lib/components/ui/QuickLinkGrid.svelte | 30 ++ src/lib/components/ui/SectionCard.svelte | 41 ++ src/lib/components/ui/StatCard.svelte | 58 +++ src/lib/components/ui/StatusBadge.svelte | 30 ++ src/lib/components/ui/TabBar.svelte | 37 ++ src/lib/components/ui/index.ts | 11 + src/routes/[orgSlug]/+layout.server.ts | 19 +- src/routes/[orgSlug]/+layout.svelte | 24 +- src/routes/[orgSlug]/+page.svelte | 440 +++++++----------- src/routes/[orgSlug]/account/+layout.svelte | 36 ++ src/routes/[orgSlug]/account/+page.svelte | 12 +- src/routes/[orgSlug]/calendar/+layout.svelte | 31 ++ src/routes/[orgSlug]/calendar/+page.svelte | 16 +- src/routes/[orgSlug]/documents/+layout.svelte | 31 ++ src/routes/[orgSlug]/documents/+page.svelte | 2 +- src/routes/[orgSlug]/events/+layout.svelte | 49 ++ src/routes/[orgSlug]/events/+page.svelte | 217 ++------- .../[orgSlug]/events/[eventSlug]/+page.svelte | 64 +-- src/routes/[orgSlug]/kanban/+layout.svelte | 31 ++ src/routes/[orgSlug]/kanban/+page.svelte | 58 +-- src/routes/[orgSlug]/settings/+layout.svelte | 35 ++ src/routes/[orgSlug]/settings/+page.svelte | 42 +- 30 files changed, 1240 insertions(+), 604 deletions(-) create mode 100644 src/lib/components/ui/ActivityFeed.svelte create mode 100644 src/lib/components/ui/ContentSkeleton.svelte create mode 100644 src/lib/components/ui/EventCard.svelte create mode 100644 src/lib/components/ui/MemberList.svelte create mode 100644 src/lib/components/ui/ModuleCard.svelte create mode 100644 src/lib/components/ui/PageHeader.svelte create mode 100644 src/lib/components/ui/QuickLinkGrid.svelte create mode 100644 src/lib/components/ui/SectionCard.svelte create mode 100644 src/lib/components/ui/StatCard.svelte create mode 100644 src/lib/components/ui/StatusBadge.svelte create mode 100644 src/lib/components/ui/TabBar.svelte create mode 100644 src/routes/[orgSlug]/account/+layout.svelte create mode 100644 src/routes/[orgSlug]/calendar/+layout.svelte create mode 100644 src/routes/[orgSlug]/documents/+layout.svelte create mode 100644 src/routes/[orgSlug]/events/+layout.svelte create mode 100644 src/routes/[orgSlug]/kanban/+layout.svelte create mode 100644 src/routes/[orgSlug]/settings/+layout.svelte diff --git a/messages/en.json b/messages/en.json index 2f9bed3..23d83fd 100644 --- a/messages/en.json +++ b/messages/en.json @@ -320,5 +320,11 @@ "events_mod_team": "Team", "events_mod_team_desc": "Team members and shift scheduling", "events_mod_sponsors": "Sponsors", - "events_mod_sponsors_desc": "Sponsors, partners, and deliverables" + "events_mod_sponsors_desc": "Sponsors, partners, and deliverables", + "overview_subtitle": "Welcome back. Here's what's happening.", + "overview_stat_events": "Events", + "overview_upcoming_events": "Upcoming Events", + "overview_upcoming_empty": "No upcoming events. Create one to get started.", + "overview_view_all_events": "View all events", + "overview_more_members": "+{count} more" } \ No newline at end of file diff --git a/messages/et.json b/messages/et.json index b3be0d3..228d7cc 100644 --- a/messages/et.json +++ b/messages/et.json @@ -320,5 +320,11 @@ "events_mod_team": "Meeskond", "events_mod_team_desc": "Meeskonnaliikmed ja vahetuste planeerimine", "events_mod_sponsors": "Sponsorid", - "events_mod_sponsors_desc": "Sponsorid, partnerid ja kohustused" + "events_mod_sponsors_desc": "Sponsorid, partnerid ja kohustused", + "overview_subtitle": "Tere tagasi. Siin on ülevaade toimuvast.", + "overview_stat_events": "Üritused", + "overview_upcoming_events": "Tulevased üritused", + "overview_upcoming_empty": "Tulevasi üritusi pole. Loo üks alustamiseks.", + "overview_view_all_events": "Vaata kõiki üritusi", + "overview_more_members": "+{count} veel" } \ No newline at end of file diff --git a/src/lib/components/ui/ActivityFeed.svelte b/src/lib/components/ui/ActivityFeed.svelte new file mode 100644 index 0000000..1f2db1d --- /dev/null +++ b/src/lib/components/ui/ActivityFeed.svelte @@ -0,0 +1,132 @@ + + +{#if entries.length === 0} +
+ history +

{emptyLabel ?? m.activity_empty()}

+
+{:else} +
+ {#each entries as entry} +
+ {getActivityIcon(entry.action)} +
+

+ {getDescription(entry)} +

+
+ {formatTimeAgo(entry.created_at)} +
+ {/each} +
+{/if} diff --git a/src/lib/components/ui/ContentSkeleton.svelte b/src/lib/components/ui/ContentSkeleton.svelte new file mode 100644 index 0000000..e6e59da --- /dev/null +++ b/src/lib/components/ui/ContentSkeleton.svelte @@ -0,0 +1,109 @@ + + +
+ {#if variant === "kanban"} +
+ {#each Array(3) as _} +
+
+ + +
+ {#each Array(3) as __} + + {/each} +
+ {/each} +
+ {:else if variant === "files"} +
+ +
+ + +
+
+ {#each Array(12) as _} + + {/each} +
+ {:else if variant === "calendar"} +
+ + + +
+ +
+
+ {#each Array(7) as _} + + {/each} + {#each Array(35) as _} + + {/each} +
+ {:else if variant === "settings"} +
+ + + + +
+ {:else if variant === "list"} +
+ {#each Array(4) as _} + + {/each} +
+
+ {#each Array(5) as _} + + {/each} +
+ {:else if variant === "detail"} +
+
+ + +
+
+ + +
+
+ {:else} +
+ {#each Array(4) as _} + + {/each} +
+
+
+ +
+
+ + +
+
+ {/if} +
+ + diff --git a/src/lib/components/ui/EventCard.svelte b/src/lib/components/ui/EventCard.svelte new file mode 100644 index 0000000..4285283 --- /dev/null +++ b/src/lib/components/ui/EventCard.svelte @@ -0,0 +1,116 @@ + + +{#if compact} + + +
+
+

+ {name} +

+
+ {#if startDate} + {formatDate(startDate)}{endDate + ? ` — ${formatDate(endDate)}` + : ""} + {/if} + {#if venueName} + · {venueName} + {/if} +
+
+ +
+{:else} + + +
+
+
+

+ {name} +

+
+ +
+ +
+ {#if startDate} + + calendar_today + {formatDate(startDate)}{endDate + ? ` — ${formatDate(endDate)}` + : ""} + + {/if} + {#if venueName} + + location_on + {venueName} + + {/if} +
+
+{/if} diff --git a/src/lib/components/ui/MemberList.svelte b/src/lib/components/ui/MemberList.svelte new file mode 100644 index 0000000..97878a0 --- /dev/null +++ b/src/lib/components/ui/MemberList.svelte @@ -0,0 +1,71 @@ + + +
+ {#each visible as member} +
+ +
+

+ {member.profiles?.full_name || + member.profiles?.email || + "Unknown"} +

+

+ {member.role} +

+
+
+ {/each} + {#if remaining > 0 && moreHref && moreLabel} + + {moreLabel} + + {/if} + {#if members.length === 0 && emptyLabel} +

+ {emptyLabel} +

+ {/if} +
diff --git a/src/lib/components/ui/ModuleCard.svelte b/src/lib/components/ui/ModuleCard.svelte new file mode 100644 index 0000000..daec6e7 --- /dev/null +++ b/src/lib/components/ui/ModuleCard.svelte @@ -0,0 +1,40 @@ + + + +
+ {icon} +
+

+ {label} +

+

{description}

+
diff --git a/src/lib/components/ui/PageHeader.svelte b/src/lib/components/ui/PageHeader.svelte new file mode 100644 index 0000000..034b157 --- /dev/null +++ b/src/lib/components/ui/PageHeader.svelte @@ -0,0 +1,46 @@ + + +
+
+ {#if icon} + {icon} + {/if} +
+

{title}

+ {#if subtitle} +

{subtitle}

+ {/if} +
+
+ {#if actions} +
+ {@render actions()} +
+ {/if} +
diff --git a/src/lib/components/ui/QuickLinkGrid.svelte b/src/lib/components/ui/QuickLinkGrid.svelte new file mode 100644 index 0000000..40707e6 --- /dev/null +++ b/src/lib/components/ui/QuickLinkGrid.svelte @@ -0,0 +1,30 @@ + + +
+ {#each links as link} + + {link.icon} + {link.label} + + {/each} +
diff --git a/src/lib/components/ui/SectionCard.svelte b/src/lib/components/ui/SectionCard.svelte new file mode 100644 index 0000000..bdb4e27 --- /dev/null +++ b/src/lib/components/ui/SectionCard.svelte @@ -0,0 +1,41 @@ + + +
+ {#if title || titleRight} +
+ {#if title} +

{title}

+ {/if} + {#if titleRight} + {@render titleRight()} + {/if} +
+ {/if} + {@render children()} +
diff --git a/src/lib/components/ui/StatCard.svelte b/src/lib/components/ui/StatCard.svelte new file mode 100644 index 0000000..54f06ea --- /dev/null +++ b/src/lib/components/ui/StatCard.svelte @@ -0,0 +1,58 @@ + + +{#if href} + +
+ {icon} +
+
+

{value}

+

{label}

+
+
+{:else} +
+
+ {icon} +
+
+

{value}

+

{label}

+
+
+{/if} diff --git a/src/lib/components/ui/StatusBadge.svelte b/src/lib/components/ui/StatusBadge.svelte new file mode 100644 index 0000000..7e45307 --- /dev/null +++ b/src/lib/components/ui/StatusBadge.svelte @@ -0,0 +1,30 @@ + + +{status} diff --git a/src/lib/components/ui/TabBar.svelte b/src/lib/components/ui/TabBar.svelte new file mode 100644 index 0000000..fa033cf --- /dev/null +++ b/src/lib/components/ui/TabBar.svelte @@ -0,0 +1,37 @@ + + +
+ {#each tabs as tab} + + {/each} +
diff --git a/src/lib/components/ui/index.ts b/src/lib/components/ui/index.ts index eb32b1c..305a794 100644 --- a/src/lib/components/ui/index.ts +++ b/src/lib/components/ui/index.ts @@ -26,6 +26,17 @@ export { default as Icon } from './Icon.svelte'; export { default as AssigneePicker } from './AssigneePicker.svelte'; export { default as ContextMenu } from './ContextMenu.svelte'; export { default as PageSkeleton } from './PageSkeleton.svelte'; +export { default as PageHeader } from './PageHeader.svelte'; +export { default as SectionCard } from './SectionCard.svelte'; +export { default as StatCard } from './StatCard.svelte'; +export { default as StatusBadge } from './StatusBadge.svelte'; +export { default as TabBar } from './TabBar.svelte'; +export { default as MemberList } from './MemberList.svelte'; +export { default as ActivityFeed } from './ActivityFeed.svelte'; +export { default as EventCard } from './EventCard.svelte'; +export { default as ContentSkeleton } from './ContentSkeleton.svelte'; +export { default as QuickLinkGrid } from './QuickLinkGrid.svelte'; +export { default as ModuleCard } from './ModuleCard.svelte'; export { default as ImagePreviewModal } from './ImagePreviewModal.svelte'; export { default as Twemoji } from './Twemoji.svelte'; export { default as EmojiPicker } from './EmojiPicker.svelte'; diff --git a/src/routes/[orgSlug]/+layout.server.ts b/src/routes/[orgSlug]/+layout.server.ts index e2345fa..153acd2 100644 --- a/src/routes/[orgSlug]/+layout.server.ts +++ b/src/routes/[orgSlug]/+layout.server.ts @@ -20,7 +20,7 @@ export const load: LayoutServerLoad = async ({ params, locals }) => { } // Now fetch membership, members, activity, and user profile in parallel (all depend on org.id) - const [membershipResult, membersResult, activityResult, profileResult, docCountResult, folderCountResult, kanbanCountResult] = await Promise.all([ + const [membershipResult, membersResult, activityResult, profileResult, docCountResult, folderCountResult, kanbanCountResult, eventCountResult] = await Promise.all([ locals.supabase .from('org_members') .select('role, role_id') @@ -68,7 +68,11 @@ export const load: LayoutServerLoad = async ({ params, locals }) => { .from('documents') .select('id', { count: 'exact', head: true }) .eq('org_id', org.id) - .eq('type', 'kanban') + .eq('type', 'kanban'), + locals.supabase + .from('events') + .select('id', { count: 'exact', head: true }) + .eq('org_id', org.id) ]); const { data: membership } = membershipResult; @@ -81,6 +85,7 @@ export const load: LayoutServerLoad = async ({ params, locals }) => { documentCount: docCountResult.count ?? 0, folderCount: folderCountResult.count ?? 0, kanbanCount: kanbanCountResult.count ?? 0, + eventCount: eventCountResult.count ?? 0, }; if (!membership) { @@ -121,6 +126,15 @@ export const load: LayoutServerLoad = async ({ params, locals }) => { profiles: (m.user_id ? memberProfilesMap[m.user_id] : null) ?? null })); + // Fetch upcoming events for the overview + const { data: upcomingEvents } = await locals.supabase + .from('events') + .select('id, name, slug, status, start_date, end_date, color, venue_name') + .eq('org_id', org.id) + .in('status', ['planning', 'active']) + .order('start_date', { ascending: true, nullsFirst: false }) + .limit(5); + return { org, userRole: membership.role, @@ -128,6 +142,7 @@ export const load: LayoutServerLoad = async ({ params, locals }) => { members, recentActivity: recentActivity ?? [], stats, + upcomingEvents: upcomingEvents ?? [], user, profile: profile ?? { id: user.id, email: user.email ?? '', full_name: null, avatar_url: null } }; diff --git a/src/routes/[orgSlug]/+layout.svelte b/src/routes/[orgSlug]/+layout.svelte index 9cffd9e..6da719f 100644 --- a/src/routes/[orgSlug]/+layout.svelte +++ b/src/routes/[orgSlug]/+layout.svelte @@ -1,10 +1,10 @@ {data.org.name} | Root -
- -
-

{data.org.name}

-

{m.overview_title()}

-
- - -
- {#each statCards as stat} - {#if stat.href} +
+ + {#snippet actions()} + {#if isEditor} -
- - {stat.icon} - -
-
-

- {stat.value} -

-

{stat.label}

-
-
- {:else} -
-
- - {stat.icon} - -
-
-

- {stat.value} -

-

{stat.label}

-
-
- {/if} - {/each} -
- -
- -
-

- {m.activity_title()} -

- - {#if recentActivity.length === 0} -
celebration - history - -

{m.activity_empty()}

-
- {:else} -
- {#each recentActivity as entry} -
- - {getActivityIcon(entry.action)} - -
-

- {getActivityDescription(entry)} -

-

- {formatTimeAgo(entry.created_at)} -

-
-
- {/each} -
+ {m.nav_events()} + {/if} + {/snippet} + + +
+ +
+ + + +
- -
- -
-

- {m.overview_quick_links()} -

-
- {#each quickLinks as link} +
+ +
+ + + {#snippet titleRight()} {m.overview_view_all_events()} + {/snippet} + + {#if upcomingEvents.length === 0} +
celebration - {link.icon} - - {link.label} - - {/each} -
+

{m.overview_upcoming_empty()}

+
+ {:else} +
+ {#each upcomingEvents as event} + + {/each} +
+ {/if} + + + + + +
- -
-
-

- {m.overview_stat_members()} -

- {stats.memberCount} +
+ + + + + + {#snippet titleRight()} + {stats.memberCount} + {/snippet} + + + + {#if isAdmin} + -
-
diff --git a/src/routes/[orgSlug]/account/+layout.svelte b/src/routes/[orgSlug]/account/+layout.svelte new file mode 100644 index 0000000..ed00bc3 --- /dev/null +++ b/src/routes/[orgSlug]/account/+layout.svelte @@ -0,0 +1,36 @@ + + +
+ + + {#if isNavigatingHere} + + {:else} +
+ {@render children()} +
+ {/if} +
diff --git a/src/routes/[orgSlug]/account/+page.svelte b/src/routes/[orgSlug]/account/+page.svelte index d05b6ce..9aba7c9 100644 --- a/src/routes/[orgSlug]/account/+page.svelte +++ b/src/routes/[orgSlug]/account/+page.svelte @@ -227,16 +227,8 @@ Account Settings | Root -
- -
-

{m.account_title()}

-

- {m.account_subtitle()} -

-
- -
+
+

diff --git a/src/routes/[orgSlug]/calendar/+layout.svelte b/src/routes/[orgSlug]/calendar/+layout.svelte new file mode 100644 index 0000000..76cd2eb --- /dev/null +++ b/src/routes/[orgSlug]/calendar/+layout.svelte @@ -0,0 +1,31 @@ + + +
+ + + {#if isNavigatingHere} + + {:else} +
+ {@render children()} +
+ {/if} +
diff --git a/src/routes/[orgSlug]/calendar/+page.svelte b/src/routes/[orgSlug]/calendar/+page.svelte index bda44a0..a33f42b 100644 --- a/src/routes/[orgSlug]/calendar/+page.svelte +++ b/src/routes/[orgSlug]/calendar/+page.svelte @@ -456,13 +456,11 @@ Calendar - {data.org.name} | Root -
- -
-

- {m.calendar_title()} -

- -
+
-
+
+ import type { Snippet } from "svelte"; + import { navigating } from "$app/stores"; + import { PageHeader, ContentSkeleton } from "$lib/components/ui"; + import * as m from "$lib/paraglide/messages"; + + interface Props { + data: { + org: { id: string; name: string; slug: string }; + }; + children: Snippet; + } + + let { data, children }: Props = $props(); + + const isNavigatingHere = $derived( + $navigating?.to?.url.pathname.includes("/documents") && !$navigating?.to?.url.pathname.includes("/events"), + ); + + +
+ + + {#if isNavigatingHere} + + {:else} +
+ {@render children()} +
+ {/if} +
diff --git a/src/routes/[orgSlug]/documents/+page.svelte b/src/routes/[orgSlug]/documents/+page.svelte index 892ad33..3091165 100644 --- a/src/routes/[orgSlug]/documents/+page.svelte +++ b/src/routes/[orgSlug]/documents/+page.svelte @@ -22,7 +22,7 @@ Files - {data.org.name} | Root -
+
+ import type { Snippet } from "svelte"; + import { navigating, page } from "$app/stores"; + import { PageHeader, ContentSkeleton } from "$lib/components/ui"; + import * as m from "$lib/paraglide/messages"; + + interface Props { + data: { + org: { id: string; name: string; slug: string }; + userRole: string; + }; + children: Snippet; + } + + let { data, children }: Props = $props(); + + // Only show the events list header when on the events list page itself, + // not on event detail pages (which have their own layout) + const isEventsList = $derived( + $page.url.pathname === `/${data.org.slug}/events`, + ); + + const isNavigatingToList = $derived( + $navigating?.to?.url.pathname === `/${data.org.slug}/events`, + ); + + const showListLayout = $derived(isEventsList || isNavigatingToList); + + +{#if showListLayout} +
+ + + {#if isNavigatingToList && !isEventsList} + + {:else} +
+ {@render children()} +
+ {/if} +
+{:else} + {@render children()} +{/if} diff --git a/src/routes/[orgSlug]/events/+page.svelte b/src/routes/[orgSlug]/events/+page.svelte index 16bd286..3d60936 100644 --- a/src/routes/[orgSlug]/events/+page.svelte +++ b/src/routes/[orgSlug]/events/+page.svelte @@ -1,7 +1,7 @@ + +
+ + + {#if isNavigatingHere} + + {:else} +
+ {@render children()} +
+ {/if} +
diff --git a/src/routes/[orgSlug]/kanban/+page.svelte b/src/routes/[orgSlug]/kanban/+page.svelte index 866b5ef..f2774cc 100644 --- a/src/routes/[orgSlug]/kanban/+page.svelte +++ b/src/routes/[orgSlug]/kanban/+page.svelte @@ -494,13 +494,13 @@ > -
- -
+
+ +
{#if isRenamingBoard && selectedBoard} { if (e.key === "Enter") confirmBoardRename(); @@ -509,12 +509,30 @@ onblur={confirmBoardRename} autofocus /> - {:else} -

- {selectedBoard ? selectedBoard.name : m.kanban_title()} -

+ {:else if selectedBoard} +

{selectedBoard.name}

{/if} - + {/each} +
+ {/if} + +
+ + -
- - - {#if boards.length > 1} -
- {#each boards as board} - - {/each} -
- {/if} +
-
+
{#if selectedBoard} + import type { Snippet } from "svelte"; + import { navigating } from "$app/stores"; + import { PageHeader, ContentSkeleton } from "$lib/components/ui"; + import * as m from "$lib/paraglide/messages"; + + interface Props { + data: { + org: { id: string; name: string; slug: string }; + }; + children: Snippet; + } + + let { data, children }: Props = $props(); + + const isNavigatingHere = $derived( + $navigating?.to?.url.pathname.includes("/settings") ?? false, + ); + + +
+ + + {#if isNavigatingHere} + + {:else} +
+ {@render children()} +
+ {/if} +
diff --git a/src/routes/[orgSlug]/settings/+page.svelte b/src/routes/[orgSlug]/settings/+page.svelte index 5a83189..52158aa 100644 --- a/src/routes/[orgSlug]/settings/+page.svelte +++ b/src/routes/[orgSlug]/settings/+page.svelte @@ -274,33 +274,24 @@ Settings - {data.org.name} | Root -
- -
-
- -

- {m.settings_title()} -

- - - -
- - -
- {#each tabs as tab} - - {/each} -
+
+ +
+ {#each tabs as tab} + + {/each}
+
+ {#if activeTab === "general"} {/if} +
From 819d5b876a7ba90f142428250bf7ca666f2cbd2d Mon Sep 17 00:00:00 2001 From: AlacrisDevs Date: Sat, 7 Feb 2026 11:03:58 +0200 Subject: [PATCH 05/10] ui: overhaul files, kanban, calendar, settings, chat modules - FileBrowser: modernize breadcrumbs, toolbar, list/grid items, empty states - KanbanColumn: remove fixed height, border-based styling, compact header - KanbanCard: cleaner border styling, smaller tags, compact footer - Calendar: compact nav bar, border grid, today circle indicator, day view empty state - DocumentViewer: remove bg-night rounded-[32px], border-b header pattern - Settings tags: inline border/rounded-xl cards, icon action buttons - Chat: create +layout.svelte with PageHeader, overhaul sidebar and main area - Chat i18n: add nav_chat, chat_title, chat_subtitle keys (en + et) svelte-check: 0 errors, vitest: 112/112 passed --- messages/en.json | 3 + messages/et.json | 3 + src/lib/components/calendar/Calendar.svelte | 137 +++++------- .../documents/DocumentViewer.svelte | 35 ++- .../components/documents/FileBrowser.svelte | 201 +++++++++--------- src/lib/components/kanban/KanbanCard.svelte | 61 ++---- src/lib/components/ui/KanbanColumn.svelte | 31 ++- src/routes/[orgSlug]/chat/+layout.svelte | 36 ++++ src/routes/[orgSlug]/chat/+page.svelte | 180 ++++++++-------- src/routes/[orgSlug]/settings/+page.svelte | 99 +++++---- 10 files changed, 376 insertions(+), 410 deletions(-) create mode 100644 src/routes/[orgSlug]/chat/+layout.svelte diff --git a/messages/en.json b/messages/en.json index 23d83fd..f620aa8 100644 --- a/messages/en.json +++ b/messages/en.json @@ -254,6 +254,9 @@ "entity_invite": "invite", "entity_event": "event", "nav_events": "Events", + "nav_chat": "Chat", + "chat_title": "Chat", + "chat_subtitle": "Team messaging and communication", "events_title": "Events", "events_subtitle": "Organize and manage your events", "events_new": "New Event", diff --git a/messages/et.json b/messages/et.json index 228d7cc..f206b28 100644 --- a/messages/et.json +++ b/messages/et.json @@ -254,6 +254,9 @@ "entity_invite": "kutse", "entity_event": "ürituse", "nav_events": "Üritused", + "nav_chat": "Vestlus", + "chat_title": "Vestlus", + "chat_subtitle": "Meeskonna sõnumid ja suhtlus", "events_title": "Üritused", "events_subtitle": "Korralda ja halda oma üritusi", "events_new": "Uus üritus", diff --git a/src/lib/components/calendar/Calendar.svelte b/src/lib/components/calendar/Calendar.svelte index f2bad56..20061cc 100644 --- a/src/lib/components/calendar/Calendar.svelte +++ b/src/lib/components/calendar/Calendar.svelte @@ -123,63 +123,63 @@ }); -
+
-
-
+
+
{headerTitle}
-
+
@@ -187,48 +187,40 @@ {#if currentView === "month"} -
+
-
+
{#each weekDayHeaders as day} -
- {day} +
+ {day}
{/each}
-
+
{#each weeks as week} -
+
{#each week as day} {@const dayEvents = getEventsForDay(day)} {@const isToday = isSameDay(day, today)} {@const inMonth = isCurrentMonth(day)} -
onDateClick?.(day)} > {day.getDate()} {#each dayEvents.slice(0, 2) as event} {/each} {#if dayEvents.length > 2} - +{dayEvents.length - 2} more + +{dayEvents.length - 2} {/if} -
+ {/each}
{/each} @@ -253,40 +242,25 @@ {#if currentView === "week"} -
-
+
+
{#each weekDates as day} {@const dayEvents = getEventsForDay(day)} {@const isToday = isSameDay(day, today)} -
-
-
+
+
+
{weekDayHeaders[(day.getDay() + 6) % 7]}
-
+
{day.getDate()}
-
+
{#each dayEvents as event} + {:else if mode === "edit"} - {:else} - + {/if} - +
-
+
diff --git a/src/lib/components/documents/FileBrowser.svelte b/src/lib/components/documents/FileBrowser.svelte index 8e44fee..fae3729 100644 --- a/src/lib/components/documents/FileBrowser.svelte +++ b/src/lib/components/documents/FileBrowser.svelte @@ -5,9 +5,6 @@ Button, Modal, Input, - Avatar, - IconButton, - Icon, } from "$lib/components/ui"; import { DocumentViewer } from "$lib/components/documents"; import { createLogger } from "$lib/utils/logger"; @@ -490,97 +487,101 @@ } -
+
-
- -
- -

{title}

- - - - -
- - - + +
-
+
{#if viewMode === "list"}
{#if currentFolderItems.length === 0} -
-

- No files yet. Drag files here or create a new - one. -

+
+ folder_open +

{m.files_empty()}

{:else} {#each currentFolderItems as item} @@ -678,7 +671,7 @@ {#if selectedDoc} -
+
- delete + close {/if} {#if card.tags && card.tags.length > 0} -
+
{#each card.tags as tag} {tag.name} @@ -95,55 +95,40 @@ {/if} -

+

{card.title}

{#if hasFooter} -
-
- +
+
{#if card.due_date} -
+ - calendar_today - - - {formatDueDate(card.due_date)} - -
+ class="material-symbols-rounded" + style="font-size: 14px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 14;" + >calendar_today + {formatDueDate(card.due_date)} + {/if} - {#if (card.checklist_total ?? 0) > 0} -
+ - check_box - - - {card.checklist_done ?? 0}/{card.checklist_total} - -
+ class="material-symbols-rounded" + style="font-size: 14px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 14;" + >check_box + {card.checklist_done ?? 0}/{card.checklist_total} + {/if}
- {#if card.assignee_id} {/if}
diff --git a/src/lib/components/ui/KanbanColumn.svelte b/src/lib/components/ui/KanbanColumn.svelte index f604b1b..990988e 100644 --- a/src/lib/components/ui/KanbanColumn.svelte +++ b/src/lib/components/ui/KanbanColumn.svelte @@ -14,28 +14,25 @@
-
+
- {title} -
- {count} -
+ {title} + {count}
{#if onMore} +
+ +
{/if}
diff --git a/src/routes/[orgSlug]/chat/+layout.svelte b/src/routes/[orgSlug]/chat/+layout.svelte new file mode 100644 index 0000000..0022c62 --- /dev/null +++ b/src/routes/[orgSlug]/chat/+layout.svelte @@ -0,0 +1,36 @@ + + +
+ + + {#if isNavigatingHere && !$navigating?.from?.url.pathname?.includes(`/${data.org.slug}/chat`)} + + {:else} +
+ {@render children()} +
+ {/if} +
diff --git a/src/routes/[orgSlug]/chat/+page.svelte b/src/routes/[orgSlug]/chat/+page.svelte index 41e1da2..56638e3 100644 --- a/src/routes/[orgSlug]/chat/+page.svelte +++ b/src/routes/[orgSlug]/chat/+page.svelte @@ -392,9 +392,9 @@ {#if showMatrixLogin}
-
-

Connect to Chat

-

+

+

Connect to Chat

+

Enter your Matrix credentials to enable messaging.

@@ -410,12 +410,12 @@ placeholder="@user:matrix.org" />
- + { if (e.key === "Enter") handleMatrixLogin(); }} @@ -438,9 +438,9 @@
-

+

{#if isInitializing} Connecting to Matrix... {:else if $syncState === "CATCHUP"} @@ -460,56 +460,50 @@ {:else if matrixClient} {#snippet children()} -

+
-