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
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>
|
|
|