You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

239 lines
5.7 KiB

<script lang="ts">
import { getContext } from "svelte";
import { Button, Modal, Input, Textarea } from "$lib/components/ui";
import { Calendar } from "$lib/components/calendar";
import { createEvent } from "$lib/api/calendar";
import type { CalendarEvent } from "$lib/supabase/types";
import type { SupabaseClient } from "@supabase/supabase-js";
import type { Database } from "$lib/supabase/types";
interface Props {
data: {
org: { id: string; name: string; slug: string };
events: CalendarEvent[];
user: { id: string } | null;
};
}
let { data }: Props = $props();
const supabase = getContext<SupabaseClient<Database>>("supabase");
let events = $state(data.events);
let showCreateModal = $state(false);
let showEventModal = $state(false);
let selectedEvent = $state<CalendarEvent | null>(null);
let selectedDate = $state<Date | null>(null);
let newEvent = $state({
title: "",
description: "",
date: "",
startTime: "09:00",
endTime: "10:00",
allDay: false,
color: "#6366f1",
});
const colorOptions = [
{ value: "#6366f1", label: "Indigo" },
{ value: "#ec4899", label: "Pink" },
{ value: "#10b981", label: "Green" },
{ value: "#f59e0b", label: "Amber" },
{ value: "#ef4444", label: "Red" },
{ value: "#8b5cf6", label: "Purple" },
];
function handleDateClick(date: Date) {
selectedDate = date;
newEvent.date = date.toISOString().split("T")[0];
showCreateModal = true;
}
function handleEventClick(event: CalendarEvent) {
selectedEvent = event;
showEventModal = true;
}
async function handleCreateEvent() {
if (!newEvent.title.trim() || !newEvent.date || !data.user) return;
const startTime = newEvent.allDay
? `${newEvent.date}T00:00:00`
: `${newEvent.date}T${newEvent.startTime}:00`;
const endTime = newEvent.allDay
? `${newEvent.date}T23:59:59`
: `${newEvent.date}T${newEvent.endTime}:00`;
const created = await createEvent(
supabase,
data.org.id,
{
title: newEvent.title,
description: newEvent.description || undefined,
start_time: startTime,
end_time: endTime,
all_day: newEvent.allDay,
color: newEvent.color,
},
data.user.id,
);
events = [...events, created];
resetForm();
}
function resetForm() {
showCreateModal = false;
newEvent = {
title: "",
description: "",
date: "",
startTime: "09:00",
endTime: "10:00",
allDay: false,
color: "#6366f1",
};
selectedDate = null;
}
function formatEventTime(event: CalendarEvent): string {
if (event.all_day) return "All day";
const start = new Date(event.start_time);
const end = new Date(event.end_time);
return `${start.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })} - ${end.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}`;
}
</script>
<div class="p-6 h-full overflow-auto">
<header class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold text-light">Calendar</h1>
<Button onclick={() => (showCreateModal = true)}>
<svg
class="w-4 h-4 mr-2"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<line x1="12" y1="5" x2="12" y2="19" />
<line x1="5" y1="12" x2="19" y2="12" />
</svg>
New Event
</Button>
</header>
<Calendar
{events}
onDateClick={handleDateClick}
onEventClick={handleEventClick}
/>
</div>
<Modal isOpen={showCreateModal} onClose={resetForm} title="Create Event">
<div class="space-y-4">
<Input
label="Title"
bind:value={newEvent.title}
placeholder="Event title"
/>
<Textarea
label="Description"
bind:value={newEvent.description}
placeholder="Optional description"
rows={2}
/>
<Input label="Date" type="date" bind:value={newEvent.date} />
<label class="flex items-center gap-2 text-sm text-light">
<input
type="checkbox"
bind:checked={newEvent.allDay}
class="rounded"
/>
All day event
</label>
{#if !newEvent.allDay}
<div class="grid grid-cols-2 gap-4">
<Input
label="Start Time"
type="time"
bind:value={newEvent.startTime}
/>
<Input
label="End Time"
type="time"
bind:value={newEvent.endTime}
/>
</div>
{/if}
<div>
<label class="block text-sm font-medium text-light mb-2"
>Color</label
>
<div class="flex gap-2">
{#each colorOptions as color}
<button
type="button"
class="w-8 h-8 rounded-full transition-transform"
class:ring-2={newEvent.color === color.value}
class:ring-white={newEvent.color === color.value}
class:scale-110={newEvent.color === color.value}
style="background-color: {color.value}"
onclick={() => (newEvent.color = color.value)}
aria-label={color.label}
></button>
{/each}
</div>
</div>
<div class="flex justify-end gap-2 pt-2">
<Button variant="ghost" onclick={resetForm}>Cancel</Button>
<Button
onclick={handleCreateEvent}
disabled={!newEvent.title.trim() || !newEvent.date}
>Create</Button
>
</div>
</div>
</Modal>
<Modal
isOpen={showEventModal}
onClose={() => (showEventModal = false)}
title={selectedEvent?.title ?? "Event"}
>
{#if selectedEvent}
<div class="space-y-3">
<div class="flex items-center gap-2">
<div
class="w-3 h-3 rounded-full"
style="background-color: {selectedEvent.color ?? '#6366f1'}"
></div>
<span class="text-light/70"
>{formatEventTime(selectedEvent)}</span
>
</div>
{#if selectedEvent.description}
<p class="text-light/80">{selectedEvent.description}</p>
{/if}
<p class="text-xs text-light/40">
{new Date(selectedEvent.start_time).toLocaleDateString(
undefined,
{
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
},
)}
</p>
</div>
{/if}
</Modal>