feat: map shapes, image persistence, grab tool, layer rename/delete, i18n, page metadata
This commit is contained in:
@@ -1,12 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import { page } from "$app/stores";
|
||||
import { EventCard, TabBar, Button } from "$lib/components/ui";
|
||||
import {
|
||||
EventCard,
|
||||
TabBar,
|
||||
Button,
|
||||
Input,
|
||||
Textarea,
|
||||
} from "$lib/components/ui";
|
||||
import { getContext } from "svelte";
|
||||
import type { SupabaseClient } from "@supabase/supabase-js";
|
||||
import type { Database } from "$lib/supabase/types";
|
||||
import { toasts } from "$lib/stores/ui";
|
||||
import { getErrorMessage } from "$lib/utils/logger";
|
||||
import { createEventFolder, ensureFinanceFolder } from "$lib/api/documents";
|
||||
import * as m from "$lib/paraglide/messages";
|
||||
|
||||
interface EventItem {
|
||||
@@ -25,7 +32,12 @@
|
||||
|
||||
interface Props {
|
||||
data: {
|
||||
org: { id: string; name: string; slug: string };
|
||||
org: {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
default_event_color: string;
|
||||
};
|
||||
userRole: string;
|
||||
events: EventItem[];
|
||||
statusFilter: string;
|
||||
@@ -47,14 +59,22 @@
|
||||
let newEventStartDate = $state("");
|
||||
let newEventEndDate = $state("");
|
||||
let newEventVenue = $state("");
|
||||
let newEventColor = $state("#00A3E0");
|
||||
let newEventColor = $state(data.org.default_event_color || "#00A3E0");
|
||||
let creating = $state(false);
|
||||
|
||||
const statusTabs = $derived([
|
||||
{ value: "all", label: m.events_tab_all(), icon: "apps" },
|
||||
{ value: "planning", label: m.events_tab_planning(), icon: "edit_note" },
|
||||
{
|
||||
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: "completed",
|
||||
label: m.events_tab_completed(),
|
||||
icon: "check_circle",
|
||||
},
|
||||
{ value: "archived", label: m.events_tab_archived(), icon: "archive" },
|
||||
]);
|
||||
|
||||
@@ -73,36 +93,56 @@
|
||||
if (!newEventName.trim()) return;
|
||||
creating = true;
|
||||
try {
|
||||
const userId = (await supabase.auth.getUser()).data.user?.id;
|
||||
const { data: created, error } = await supabase
|
||||
.from("events")
|
||||
.insert({
|
||||
org_id: data.org.id,
|
||||
name: newEventName.trim(),
|
||||
slug: newEventName
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^\w\s-]/g, "")
|
||||
.replace(/[\s_]+/g, "-")
|
||||
.replace(/-+/g, "-")
|
||||
.slice(0, 60) || "event",
|
||||
slug:
|
||||
newEventName
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^\w\s-]/g, "")
|
||||
.replace(/[\s_]+/g, "-")
|
||||
.replace(/-+/g, "-")
|
||||
.slice(0, 60) || "event",
|
||||
description: newEventDescription.trim() || null,
|
||||
start_date: newEventStartDate || null,
|
||||
end_date: newEventEndDate || null,
|
||||
venue_name: newEventVenue.trim() || null,
|
||||
color: newEventColor,
|
||||
created_by: (await supabase.auth.getUser()).data.user?.id,
|
||||
created_by: userId,
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
// Auto-create Events > [EventName] folder structure + Finance folder
|
||||
if (userId) {
|
||||
createEventFolder(
|
||||
supabase,
|
||||
data.org.id,
|
||||
userId,
|
||||
created.id,
|
||||
created.name,
|
||||
).catch(() => {});
|
||||
ensureFinanceFolder(
|
||||
supabase,
|
||||
data.org.id,
|
||||
userId,
|
||||
created.id,
|
||||
created.name,
|
||||
).catch(() => {});
|
||||
}
|
||||
|
||||
toasts.success(m.events_created({ name: created.name }));
|
||||
showCreateModal = false;
|
||||
resetForm();
|
||||
goto(`/${data.org.slug}/events/${created.slug}`);
|
||||
} catch (e: unknown) {
|
||||
toasts.error(getErrorMessage(e, 'Failed to create event'));
|
||||
toasts.error(getErrorMessage(e, "Failed to create event"));
|
||||
} finally {
|
||||
creating = false;
|
||||
}
|
||||
@@ -114,7 +154,7 @@
|
||||
newEventStartDate = "";
|
||||
newEventEndDate = "";
|
||||
newEventVenue = "";
|
||||
newEventColor = "#00A3E0";
|
||||
newEventColor = data.org.default_event_color || "#00A3E0";
|
||||
}
|
||||
|
||||
function switchStatus(status: string) {
|
||||
@@ -134,7 +174,9 @@
|
||||
|
||||
<div class="flex flex-col h-full">
|
||||
<!-- 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 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
|
||||
@@ -155,7 +197,11 @@
|
||||
{/each}
|
||||
</div>
|
||||
{#if isEditor}
|
||||
<Button size="sm" icon="add" onclick={() => (showCreateModal = true)}>
|
||||
<Button
|
||||
size="sm"
|
||||
icon="add"
|
||||
onclick={() => (showCreateModal = true)}
|
||||
>
|
||||
{m.events_new()}
|
||||
</Button>
|
||||
{/if}
|
||||
@@ -164,17 +210,24 @@
|
||||
<!-- 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-h3 font-heading mb-2">
|
||||
{m.events_empty_title()}
|
||||
</p>
|
||||
<p class="text-body text-light/30">{m.events_empty_desc()}</p>
|
||||
{#if isEditor}
|
||||
<div class="mt-4">
|
||||
<Button icon="add" onclick={() => (showCreateModal = true)}>
|
||||
<Button
|
||||
icon="add"
|
||||
onclick={() => (showCreateModal = true)}
|
||||
>
|
||||
{m.events_create()}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -206,7 +259,8 @@
|
||||
<div
|
||||
class="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4"
|
||||
onkeydown={(e) => e.key === "Escape" && (showCreateModal = false)}
|
||||
onclick={(e) => e.target === e.currentTarget && (showCreateModal = false)}
|
||||
onclick={(e) =>
|
||||
e.target === e.currentTarget && (showCreateModal = false)}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={m.events_create()}
|
||||
@@ -214,8 +268,12 @@
|
||||
<div
|
||||
class="bg-night rounded-2xl w-full max-w-lg shadow-2xl border border-light/10"
|
||||
>
|
||||
<div class="flex items-center justify-between p-5 border-b border-light/5">
|
||||
<h2 class="text-h3 font-heading text-white">{m.events_create()}</h2>
|
||||
<div
|
||||
class="flex items-center justify-between p-5 border-b border-light/5"
|
||||
>
|
||||
<h2 class="text-h3 font-heading text-white">
|
||||
{m.events_create()}
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
class="text-light/40 hover:text-white transition-colors"
|
||||
@@ -238,83 +296,47 @@
|
||||
}}
|
||||
>
|
||||
<!-- Name -->
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label
|
||||
for="event-name"
|
||||
class="text-body-sm text-light/60 font-body"
|
||||
>{m.events_form_name()}</label
|
||||
>
|
||||
<input
|
||||
id="event-name"
|
||||
type="text"
|
||||
bind:value={newEventName}
|
||||
placeholder={m.events_form_name_placeholder()}
|
||||
class="bg-dark border border-light/10 rounded-xl px-3 py-2 text-body text-white placeholder:text-light/30 focus:outline-none focus:border-primary"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
variant="compact"
|
||||
label={m.events_form_name()}
|
||||
bind:value={newEventName}
|
||||
placeholder={m.events_form_name_placeholder()}
|
||||
required
|
||||
/>
|
||||
|
||||
<!-- Description -->
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label
|
||||
for="event-desc"
|
||||
class="text-body-sm text-light/60 font-body"
|
||||
>{m.events_form_description()}</label
|
||||
>
|
||||
<textarea
|
||||
id="event-desc"
|
||||
bind:value={newEventDescription}
|
||||
placeholder={m.events_form_description_placeholder()}
|
||||
rows="2"
|
||||
class="bg-dark border border-light/10 rounded-xl px-3 py-2 text-body text-white placeholder:text-light/30 focus:outline-none focus:border-primary resize-none"
|
||||
></textarea>
|
||||
</div>
|
||||
<Textarea
|
||||
variant="compact"
|
||||
label={m.events_form_description()}
|
||||
bind:value={newEventDescription}
|
||||
placeholder={m.events_form_description_placeholder()}
|
||||
rows={2}
|
||||
resize="none"
|
||||
/>
|
||||
|
||||
<!-- Dates -->
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label
|
||||
for="event-start"
|
||||
class="text-body-sm text-light/60 font-body"
|
||||
>{m.events_form_start_date()}</label
|
||||
>
|
||||
<input
|
||||
id="event-start"
|
||||
type="date"
|
||||
bind:value={newEventStartDate}
|
||||
class="bg-dark border border-light/10 rounded-xl px-3 py-2 text-body text-white focus:outline-none focus:border-primary"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label
|
||||
for="event-end"
|
||||
class="text-body-sm text-light/60 font-body"
|
||||
>{m.events_form_end_date()}</label
|
||||
>
|
||||
<input
|
||||
id="event-end"
|
||||
type="date"
|
||||
bind:value={newEventEndDate}
|
||||
class="bg-dark border border-light/10 rounded-xl px-3 py-2 text-body text-white focus:outline-none focus:border-primary"
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
variant="compact"
|
||||
type="date"
|
||||
label={m.events_form_start_date()}
|
||||
bind:value={newEventStartDate}
|
||||
/>
|
||||
<Input
|
||||
variant="compact"
|
||||
type="date"
|
||||
label={m.events_form_end_date()}
|
||||
bind:value={newEventEndDate}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Venue -->
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label
|
||||
for="event-venue"
|
||||
class="text-body-sm text-light/60 font-body"
|
||||
>{m.events_form_venue()}</label
|
||||
>
|
||||
<input
|
||||
id="event-venue"
|
||||
type="text"
|
||||
bind:value={newEventVenue}
|
||||
placeholder={m.events_form_venue_placeholder()}
|
||||
class="bg-dark border border-light/10 rounded-xl px-3 py-2 text-body text-white placeholder:text-light/30 focus:outline-none focus:border-primary"
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
variant="compact"
|
||||
label={m.events_form_venue()}
|
||||
bind:value={newEventVenue}
|
||||
placeholder={m.events_form_venue_placeholder()}
|
||||
/>
|
||||
|
||||
<!-- Color -->
|
||||
<div class="flex flex-col gap-1.5">
|
||||
@@ -332,7 +354,9 @@
|
||||
: 'border-transparent hover:border-light/30'}"
|
||||
style="background-color: {color}"
|
||||
onclick={() => (newEventColor = color)}
|
||||
aria-label={m.events_form_select_color({ color })}
|
||||
aria-label={m.events_form_select_color({
|
||||
color,
|
||||
})}
|
||||
></button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user