First commit
This commit is contained in:
239
src/routes/[orgSlug]/calendar/+page.svelte
Normal file
239
src/routes/[orgSlug]/calendar/+page.svelte
Normal file
@@ -0,0 +1,239 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user