795 lines
22 KiB
Svelte
795 lines
22 KiB
Svelte
<script lang="ts">
|
|
import { getContext, onMount, onDestroy } from "svelte";
|
|
import { createLogger } from "$lib/utils/logger";
|
|
import {
|
|
Button,
|
|
Modal,
|
|
Avatar,
|
|
ContextMenu,
|
|
Input,
|
|
Textarea,
|
|
} from "$lib/components/ui";
|
|
import { Calendar } from "$lib/components/calendar";
|
|
import {
|
|
getCalendarSubscribeUrl,
|
|
type GoogleCalendarEvent,
|
|
} from "$lib/api/google-calendar";
|
|
import type { CalendarEvent } from "$lib/supabase/types";
|
|
import type { SupabaseClient } from "@supabase/supabase-js";
|
|
import type { Database } from "$lib/supabase/types";
|
|
import * as m from "$lib/paraglide/messages";
|
|
|
|
interface Props {
|
|
data: {
|
|
org: { id: string; name: string; slug: string };
|
|
events: CalendarEvent[];
|
|
user: { id: string } | null;
|
|
userRole?: string;
|
|
};
|
|
}
|
|
|
|
let { data }: Props = $props();
|
|
|
|
const supabase = getContext<SupabaseClient<Database>>("supabase");
|
|
const log = createLogger("page.calendar");
|
|
|
|
// svelte-ignore state_referenced_locally
|
|
let events = $state(data.events);
|
|
$effect(() => {
|
|
events = data.events;
|
|
});
|
|
let googleEvents = $state<CalendarEvent[]>([]);
|
|
let isOrgCalendarConnected = $state(false);
|
|
let isLoadingGoogle = $state(false);
|
|
let orgCalendarId = $state<string | null>(null);
|
|
let orgCalendarName = $state<string | null>(null);
|
|
// Track Google event IDs that are pending deletion to prevent ghost re-appearance
|
|
let deletedGoogleEventIds = $state<Set<string>>(new Set());
|
|
|
|
const isAdmin = $derived(
|
|
data.userRole === "owner" || data.userRole === "admin",
|
|
);
|
|
|
|
// Deduplicate: exclude Google Calendar events that already exist locally (synced events)
|
|
// Also exclude events that are pending deletion
|
|
const allEvents = $derived.by(() => {
|
|
const localGoogleIds = new Set(
|
|
events
|
|
.filter((e) => e.google_event_id)
|
|
.map((e) => e.google_event_id),
|
|
);
|
|
const filteredGoogle = googleEvents.filter((ge) => {
|
|
const rawId = ge.id.replace("google-", "");
|
|
if (localGoogleIds.has(rawId)) return false;
|
|
if (deletedGoogleEventIds.has(rawId)) return false;
|
|
return true;
|
|
});
|
|
return [...events, ...filteredGoogle];
|
|
});
|
|
let showEventModal = $state(false);
|
|
let showEventFormModal = $state(false);
|
|
let eventFormMode = $state<"create" | "edit">("create");
|
|
let isDeleting = $state(false);
|
|
let isSavingEvent = $state(false);
|
|
let selectedEvent = $state<CalendarEvent | null>(null);
|
|
|
|
// Event form state
|
|
let eventTitle = $state("");
|
|
let eventDescription = $state("");
|
|
let eventDate = $state("");
|
|
let eventStartTime = $state("09:00");
|
|
let eventEndTime = $state("10:00");
|
|
let eventAllDay = $state(false);
|
|
let eventColor = $state("#7986cb");
|
|
let syncToGoogleCal = $state(true);
|
|
|
|
// Google Calendar official event colors (colorId → hex)
|
|
const GCAL_COLORS: Record<string, string> = {
|
|
"1": "#7986cb", // Lavender
|
|
"2": "#33b679", // Sage
|
|
"3": "#8e24aa", // Grape
|
|
"4": "#e67c73", // Flamingo
|
|
"5": "#f6bf26", // Banana
|
|
"6": "#f4511e", // Tangerine
|
|
"7": "#039be5", // Peacock
|
|
"8": "#616161", // Graphite
|
|
"9": "#3f51b5", // Blueberry
|
|
"10": "#0b8043", // Basil
|
|
"11": "#d50000", // Tomato
|
|
};
|
|
|
|
// Reverse map: hex → colorId (for pushing to Google)
|
|
const HEX_TO_COLOR_ID: Record<string, string> = Object.fromEntries(
|
|
Object.entries(GCAL_COLORS).map(([id, hex]) => [hex, id]),
|
|
);
|
|
|
|
const EVENT_COLORS = Object.values(GCAL_COLORS);
|
|
|
|
function toLocalDateString(date: Date): string {
|
|
const y = date.getFullYear();
|
|
const m = String(date.getMonth() + 1).padStart(2, "0");
|
|
const d = String(date.getDate()).padStart(2, "0");
|
|
return `${y}-${m}-${d}`;
|
|
}
|
|
|
|
function handleDateClick(date: Date) {
|
|
eventFormMode = "create";
|
|
eventTitle = "";
|
|
eventDescription = "";
|
|
eventDate = toLocalDateString(date);
|
|
eventStartTime = "09:00";
|
|
eventEndTime = "10:00";
|
|
eventAllDay = false;
|
|
eventColor = "#7986cb";
|
|
syncToGoogleCal = isOrgCalendarConnected;
|
|
showEventFormModal = true;
|
|
}
|
|
|
|
function handleEventClick(event: CalendarEvent) {
|
|
selectedEvent = event;
|
|
showEventModal = true;
|
|
}
|
|
|
|
function openEditEvent() {
|
|
if (!selectedEvent || selectedEvent.id.startsWith("google-")) return;
|
|
eventFormMode = "edit";
|
|
eventTitle = selectedEvent.title;
|
|
eventDescription = selectedEvent.description ?? "";
|
|
const start = new Date(selectedEvent.start_time);
|
|
const end = new Date(selectedEvent.end_time);
|
|
eventDate = toLocalDateString(start);
|
|
eventStartTime = start.toTimeString().slice(0, 5);
|
|
eventEndTime = end.toTimeString().slice(0, 5);
|
|
eventAllDay = selectedEvent.all_day ?? false;
|
|
eventColor = selectedEvent.color ?? "#7986cb";
|
|
showEventModal = false;
|
|
showEventFormModal = true;
|
|
}
|
|
|
|
/**
|
|
* Push event to Google Calendar in the background.
|
|
* Does not block the UI — updates google_event_id on success.
|
|
*/
|
|
async function syncToGoogle(
|
|
action: "create" | "update" | "delete",
|
|
eventData: {
|
|
id?: string;
|
|
google_event_id?: string | null;
|
|
title?: string;
|
|
description?: string | null;
|
|
start_time?: string;
|
|
end_time?: string;
|
|
all_day?: boolean;
|
|
color?: string;
|
|
},
|
|
) {
|
|
const colorId = eventData.color
|
|
? (HEX_TO_COLOR_ID[eventData.color] ?? undefined)
|
|
: undefined;
|
|
|
|
try {
|
|
if (action === "create") {
|
|
const res = await fetch("/api/google-calendar/push", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
org_id: data.org.id,
|
|
title: eventData.title,
|
|
description: eventData.description,
|
|
start_time: eventData.start_time,
|
|
end_time: eventData.end_time,
|
|
all_day: eventData.all_day,
|
|
color_id: colorId,
|
|
}),
|
|
});
|
|
if (res.ok) {
|
|
const { google_event_id } = await res.json();
|
|
if (google_event_id && eventData.id) {
|
|
// Store the Google event ID back to Supabase
|
|
await supabase
|
|
.from("calendar_events")
|
|
.update({
|
|
google_event_id,
|
|
synced_at: new Date().toISOString(),
|
|
})
|
|
.eq("id", eventData.id);
|
|
// Update local state
|
|
events = events.map((e) =>
|
|
e.id === eventData.id
|
|
? {
|
|
...e,
|
|
google_event_id,
|
|
synced_at: new Date().toISOString(),
|
|
}
|
|
: e,
|
|
);
|
|
}
|
|
}
|
|
} else if (action === "update" && eventData.google_event_id) {
|
|
await fetch("/api/google-calendar/push", {
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
org_id: data.org.id,
|
|
google_event_id: eventData.google_event_id,
|
|
title: eventData.title,
|
|
description: eventData.description,
|
|
start_time: eventData.start_time,
|
|
end_time: eventData.end_time,
|
|
all_day: eventData.all_day,
|
|
color_id: colorId,
|
|
}),
|
|
});
|
|
} else if (action === "delete" && eventData.google_event_id) {
|
|
await fetch("/api/google-calendar/push", {
|
|
method: "DELETE",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
org_id: data.org.id,
|
|
google_event_id: eventData.google_event_id,
|
|
}),
|
|
});
|
|
}
|
|
} catch (e) {
|
|
log.error("Google Calendar sync failed", {
|
|
error: e,
|
|
data: { action },
|
|
});
|
|
}
|
|
}
|
|
|
|
async function handleSaveEvent() {
|
|
if (!eventTitle.trim() || !eventDate) return;
|
|
isSavingEvent = true;
|
|
|
|
const startTime = eventAllDay
|
|
? `${eventDate}T00:00:00`
|
|
: `${eventDate}T${eventStartTime}:00`;
|
|
const endTime = eventAllDay
|
|
? `${eventDate}T23:59:59`
|
|
: `${eventDate}T${eventEndTime}:00`;
|
|
|
|
if (eventFormMode === "edit" && selectedEvent) {
|
|
const { error } = await supabase
|
|
.from("calendar_events")
|
|
.update({
|
|
title: eventTitle.trim(),
|
|
description: eventDescription.trim() || null,
|
|
start_time: startTime,
|
|
end_time: endTime,
|
|
all_day: eventAllDay,
|
|
color: eventColor,
|
|
})
|
|
.eq("id", selectedEvent.id);
|
|
|
|
if (!error) {
|
|
events = events.map((e) =>
|
|
e.id === selectedEvent!.id
|
|
? {
|
|
...e,
|
|
title: eventTitle.trim(),
|
|
description: eventDescription.trim() || null,
|
|
start_time: startTime,
|
|
end_time: endTime,
|
|
all_day: eventAllDay,
|
|
color: eventColor,
|
|
}
|
|
: e,
|
|
);
|
|
// Push update to Google Calendar in background
|
|
syncToGoogle("update", {
|
|
google_event_id: selectedEvent.google_event_id,
|
|
title: eventTitle.trim(),
|
|
description: eventDescription.trim() || null,
|
|
start_time: startTime,
|
|
end_time: endTime,
|
|
all_day: eventAllDay,
|
|
color: eventColor,
|
|
});
|
|
}
|
|
} else {
|
|
const { data: newEvent, error } = await supabase
|
|
.from("calendar_events")
|
|
.insert({
|
|
org_id: data.org.id,
|
|
title: eventTitle.trim(),
|
|
description: eventDescription.trim() || null,
|
|
start_time: startTime,
|
|
end_time: endTime,
|
|
all_day: eventAllDay,
|
|
color: eventColor,
|
|
created_by: data.user?.id,
|
|
})
|
|
.select()
|
|
.single();
|
|
|
|
if (!error && newEvent) {
|
|
events = [...events, newEvent as CalendarEvent];
|
|
// Push new event to Google Calendar if sync is enabled
|
|
if (syncToGoogleCal && isOrgCalendarConnected) {
|
|
syncToGoogle("create", {
|
|
id: newEvent.id,
|
|
title: eventTitle.trim(),
|
|
description: eventDescription.trim() || null,
|
|
start_time: startTime,
|
|
end_time: endTime,
|
|
all_day: eventAllDay,
|
|
color: eventColor,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
showEventFormModal = false;
|
|
selectedEvent = null;
|
|
isSavingEvent = false;
|
|
}
|
|
|
|
async function handleDeleteEvent() {
|
|
if (!selectedEvent || selectedEvent.id.startsWith("google-")) return;
|
|
|
|
isDeleting = true;
|
|
try {
|
|
const googleEventId = selectedEvent.google_event_id;
|
|
const { error } = await supabase
|
|
.from("calendar_events")
|
|
.delete()
|
|
.eq("id", selectedEvent.id);
|
|
|
|
if (!error) {
|
|
events = events.filter((e) => e.id !== selectedEvent?.id);
|
|
if (googleEventId) {
|
|
// Immediately exclude from Google events display
|
|
deletedGoogleEventIds = new Set([
|
|
...deletedGoogleEventIds,
|
|
googleEventId,
|
|
]);
|
|
googleEvents = googleEvents.filter(
|
|
(ge) => ge.id !== `google-${googleEventId}`,
|
|
);
|
|
// Await Google delete so it completes before any refresh
|
|
await syncToGoogle("delete", {
|
|
google_event_id: googleEventId,
|
|
});
|
|
}
|
|
showEventModal = false;
|
|
selectedEvent = null;
|
|
}
|
|
} catch (e) {
|
|
log.error("Failed to delete event", { error: e });
|
|
}
|
|
isDeleting = false;
|
|
}
|
|
|
|
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" })}`;
|
|
}
|
|
|
|
let pollInterval: ReturnType<typeof setInterval> | null = null;
|
|
|
|
function handleWindowFocus() {
|
|
if (isOrgCalendarConnected) {
|
|
loadGoogleCalendarEvents();
|
|
}
|
|
}
|
|
|
|
onMount(async () => {
|
|
await loadGoogleCalendarEvents();
|
|
|
|
// Re-fetch when user returns to the tab (e.g. after editing in Google Calendar)
|
|
window.addEventListener("focus", handleWindowFocus);
|
|
|
|
// Poll every 60s for changes made in Google Calendar
|
|
pollInterval = setInterval(() => {
|
|
if (isOrgCalendarConnected && !document.hidden) {
|
|
loadGoogleCalendarEvents();
|
|
}
|
|
}, 60_000);
|
|
});
|
|
|
|
onDestroy(() => {
|
|
if (typeof window !== "undefined") {
|
|
window.removeEventListener("focus", handleWindowFocus);
|
|
}
|
|
if (pollInterval) clearInterval(pollInterval);
|
|
});
|
|
|
|
async function loadGoogleCalendarEvents() {
|
|
isLoadingGoogle = true;
|
|
try {
|
|
const res = await fetch(
|
|
`/api/google-calendar/events?org_id=${data.org.id}`,
|
|
);
|
|
const result = await res.json();
|
|
|
|
// Check if calendar is connected (even if no events)
|
|
if (res.ok && result.calendar_id) {
|
|
isOrgCalendarConnected = true;
|
|
orgCalendarId = result.calendar_id;
|
|
orgCalendarName = result.calendar_name;
|
|
|
|
const fetchedEvents = result.events ?? [];
|
|
googleEvents = fetchedEvents.map((ge: GoogleCalendarEvent) => ({
|
|
id: `google-${ge.id}`,
|
|
org_id: data.org.id,
|
|
title: ge.summary || "(No title)",
|
|
description: ge.description ?? null,
|
|
start_time:
|
|
ge.start.dateTime || `${ge.start.date}T00:00:00`,
|
|
end_time: ge.end.dateTime || `${ge.end.date}T23:59:59`,
|
|
all_day: !ge.start.dateTime,
|
|
color: ge.colorId
|
|
? (GCAL_COLORS[ge.colorId] ?? "#7986cb")
|
|
: "#7986cb",
|
|
recurrence: null,
|
|
created_by: data.user?.id ?? "",
|
|
created_at: new Date().toISOString(),
|
|
})) as CalendarEvent[];
|
|
// Clear deleted IDs that Google has confirmed are gone
|
|
const fetchedIds = new Set(
|
|
fetchedEvents.map((ge: GoogleCalendarEvent) => ge.id),
|
|
);
|
|
deletedGoogleEventIds = new Set(
|
|
[...deletedGoogleEventIds].filter((id) =>
|
|
fetchedIds.has(id),
|
|
),
|
|
);
|
|
} else if (result.error) {
|
|
log.error("Calendar API error", { error: result.error });
|
|
}
|
|
} catch (e) {
|
|
log.error("Failed to load Google events", { error: e });
|
|
}
|
|
isLoadingGoogle = false;
|
|
}
|
|
|
|
function subscribeToCalendar() {
|
|
if (!orgCalendarId) return;
|
|
const url = getCalendarSubscribeUrl(orgCalendarId);
|
|
window.open(url, "_blank");
|
|
}
|
|
</script>
|
|
|
|
<svelte:head>
|
|
<title>Calendar - {data.org.name} | Root</title>
|
|
</svelte:head>
|
|
|
|
<div class="flex flex-col h-full">
|
|
<!-- Toolbar -->
|
|
<div class="flex items-center gap-2 px-6 py-3 border-b border-light/5 shrink-0">
|
|
<div class="flex-1"></div>
|
|
<Button size="sm" onclick={() => handleDateClick(new Date())}
|
|
>{m.btn_new()}</Button
|
|
>
|
|
<ContextMenu
|
|
items={[
|
|
...(isOrgCalendarConnected
|
|
? [
|
|
{
|
|
label: m.calendar_subscribe(),
|
|
icon: "add",
|
|
onclick: subscribeToCalendar,
|
|
},
|
|
]
|
|
: []),
|
|
{
|
|
label: m.calendar_refresh(),
|
|
icon: "refresh",
|
|
onclick: () => {
|
|
loadGoogleCalendarEvents();
|
|
},
|
|
},
|
|
...(isAdmin
|
|
? [
|
|
{
|
|
label: "",
|
|
icon: "",
|
|
onclick: () => {},
|
|
divider: true,
|
|
},
|
|
{
|
|
label: m.calendar_settings(),
|
|
icon: "settings",
|
|
onclick: () => {
|
|
window.location.href = `/${data.org.slug}/settings?tab=integrations`;
|
|
},
|
|
},
|
|
]
|
|
: []),
|
|
]}
|
|
/>
|
|
</div>
|
|
|
|
<!-- Calendar Grid -->
|
|
<div class="flex-1 overflow-auto p-4">
|
|
<Calendar
|
|
events={allEvents}
|
|
onDateClick={handleDateClick}
|
|
onEventClick={handleEventClick}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<Modal
|
|
isOpen={showEventModal}
|
|
onClose={() => (showEventModal = false)}
|
|
title={selectedEvent?.title ?? "Event"}
|
|
>
|
|
{#if selectedEvent}
|
|
<div class="space-y-4">
|
|
<!-- Date and Time -->
|
|
<div class="flex items-start gap-3">
|
|
<div
|
|
class="w-8 h-8 rounded-lg bg-light/10 flex items-center justify-center flex-shrink-0"
|
|
>
|
|
<svg
|
|
class="w-4 h-4 text-light/70"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
>
|
|
<rect
|
|
x="3"
|
|
y="4"
|
|
width="18"
|
|
height="18"
|
|
rx="2"
|
|
ry="2"
|
|
/>
|
|
<line x1="16" y1="2" x2="16" y2="6" />
|
|
<line x1="8" y1="2" x2="8" y2="6" />
|
|
<line x1="3" y1="10" x2="21" y2="10" />
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<p class="text-light font-medium">
|
|
{new Date(selectedEvent.start_time).toLocaleDateString(
|
|
undefined,
|
|
{
|
|
weekday: "long",
|
|
year: "numeric",
|
|
month: "long",
|
|
day: "numeric",
|
|
},
|
|
)}
|
|
</p>
|
|
<p class="text-light/60 text-sm">
|
|
{formatEventTime(selectedEvent)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Description -->
|
|
{#if selectedEvent.description}
|
|
<div class="flex items-start gap-3">
|
|
<div
|
|
class="w-8 h-8 rounded-lg bg-light/10 flex items-center justify-center flex-shrink-0"
|
|
>
|
|
<svg
|
|
class="w-4 h-4 text-light/70"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
>
|
|
<line x1="17" y1="10" x2="3" y2="10" />
|
|
<line x1="21" y1="6" x2="3" y2="6" />
|
|
<line x1="21" y1="14" x2="3" y2="14" />
|
|
<line x1="17" y1="18" x2="3" y2="18" />
|
|
</svg>
|
|
</div>
|
|
<p class="text-light/80 text-sm whitespace-pre-wrap">
|
|
{selectedEvent.description}
|
|
</p>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Color indicator -->
|
|
<div class="flex items-center gap-3">
|
|
<div
|
|
class="w-8 h-8 rounded-lg bg-light/10 flex items-center justify-center flex-shrink-0"
|
|
>
|
|
<div
|
|
class="w-4 h-4 rounded-full"
|
|
style="background-color: {selectedEvent.color ??
|
|
'#6366f1'}"
|
|
></div>
|
|
</div>
|
|
<span class="text-light/60 text-sm">
|
|
{selectedEvent.id.startsWith("google-")
|
|
? "Google Calendar Event"
|
|
: selectedEvent.google_event_id
|
|
? "Synced to Google Calendar"
|
|
: "Local Event"}
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Google Calendar link -->
|
|
{#if (selectedEvent.id.startsWith("google-") ? selectedEvent.id.replace("google-", "") : selectedEvent.google_event_id) && orgCalendarId}
|
|
{@const googleId = selectedEvent.id.startsWith("google-")
|
|
? selectedEvent.id.replace("google-", "")
|
|
: selectedEvent.google_event_id}
|
|
<div class="pt-3 border-t border-light/10">
|
|
<a
|
|
href="https://calendar.google.com/calendar/u/0/r/eventedit/{googleId}?cid={encodeURIComponent(
|
|
orgCalendarId,
|
|
)}"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
class="inline-flex items-center gap-2 px-4 py-2 text-sm bg-blue-500/20 text-blue-400 rounded-lg hover:bg-blue-500/30 transition-colors"
|
|
>
|
|
<svg class="w-4 h-4" viewBox="0 0 24 24">
|
|
<path
|
|
fill="currentColor"
|
|
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
|
/>
|
|
<path
|
|
fill="currentColor"
|
|
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
|
/>
|
|
<path
|
|
fill="currentColor"
|
|
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
|
/>
|
|
<path
|
|
fill="currentColor"
|
|
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
|
/>
|
|
</svg>
|
|
Open in Google Calendar
|
|
</a>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Edit/Delete local event -->
|
|
{#if !selectedEvent.id.startsWith("google-")}
|
|
<div
|
|
class="pt-3 border-t border-light/10 flex items-center justify-between"
|
|
>
|
|
<Button
|
|
variant="danger"
|
|
onclick={handleDeleteEvent}
|
|
loading={isDeleting}
|
|
>
|
|
Delete Event
|
|
</Button>
|
|
<Button onclick={openEditEvent}>Edit Event</Button>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
</Modal>
|
|
|
|
<!-- Event Create/Edit Form Modal -->
|
|
<Modal
|
|
isOpen={showEventFormModal}
|
|
onClose={() => (showEventFormModal = false)}
|
|
title={eventFormMode === "edit"
|
|
? m.calendar_edit_event()
|
|
: m.calendar_create_event()}
|
|
>
|
|
<div class="space-y-4">
|
|
<Input
|
|
label={m.calendar_event_title()}
|
|
bind:value={eventTitle}
|
|
placeholder={m.calendar_event_title_placeholder()}
|
|
/>
|
|
|
|
<Input
|
|
type="date"
|
|
label={m.calendar_event_date()}
|
|
bind:value={eventDate}
|
|
/>
|
|
|
|
<label
|
|
class="flex items-center gap-2 text-sm text-light cursor-pointer"
|
|
>
|
|
<input type="checkbox" bind:checked={eventAllDay} class="rounded" />
|
|
All day event
|
|
</label>
|
|
|
|
{#if !eventAllDay}
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<div class="flex flex-col gap-1">
|
|
<span class="px-3 font-bold font-body text-body text-white"
|
|
>Start</span
|
|
>
|
|
<input
|
|
type="time"
|
|
class="w-full p-3 bg-background text-white rounded-[32px] font-medium font-input text-body focus:outline-none focus:ring-2 focus:ring-primary"
|
|
bind:value={eventStartTime}
|
|
/>
|
|
</div>
|
|
<div class="flex flex-col gap-1">
|
|
<span class="px-3 font-bold font-body text-body text-white"
|
|
>End</span
|
|
>
|
|
<input
|
|
type="time"
|
|
class="w-full p-3 bg-background text-white rounded-[32px] font-medium font-input text-body focus:outline-none focus:ring-2 focus:ring-primary"
|
|
bind:value={eventEndTime}
|
|
/>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<Textarea
|
|
label={m.calendar_event_desc()}
|
|
bind:value={eventDescription}
|
|
placeholder="Add a description..."
|
|
rows={3}
|
|
/>
|
|
|
|
<div>
|
|
<span class="block text-sm font-medium text-light mb-2">Color</span>
|
|
<div class="flex gap-2">
|
|
{#each EVENT_COLORS as color}
|
|
<button
|
|
type="button"
|
|
class="w-7 h-7 rounded-full transition-transform {eventColor ===
|
|
color
|
|
? 'ring-2 ring-white scale-110'
|
|
: ''}"
|
|
style="background-color: {color}"
|
|
onclick={() => (eventColor = color)}
|
|
aria-label="Color {color}"
|
|
></button>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
|
|
{#if isOrgCalendarConnected && eventFormMode === "create"}
|
|
<label
|
|
class="flex items-center gap-3 text-sm text-light cursor-pointer p-3 rounded-lg bg-light/5 border border-light/10"
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
bind:checked={syncToGoogleCal}
|
|
class="rounded"
|
|
/>
|
|
<div class="flex items-center gap-2">
|
|
<svg class="w-4 h-4 text-blue-400" viewBox="0 0 24 24">
|
|
<path
|
|
fill="currentColor"
|
|
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
|
/>
|
|
<path
|
|
fill="currentColor"
|
|
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
|
/>
|
|
<path
|
|
fill="currentColor"
|
|
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
|
/>
|
|
<path
|
|
fill="currentColor"
|
|
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
|
/>
|
|
</svg>
|
|
<span>Sync to Google Calendar</span>
|
|
</div>
|
|
</label>
|
|
{/if}
|
|
|
|
<div class="flex justify-end gap-2 pt-2">
|
|
<Button
|
|
variant="tertiary"
|
|
onclick={() => (showEventFormModal = false)}
|
|
>{m.btn_cancel()}</Button
|
|
>
|
|
<Button
|
|
onclick={handleSaveEvent}
|
|
loading={isSavingEvent}
|
|
disabled={!eventTitle.trim() || !eventDate}
|
|
>{eventFormMode === "edit"
|
|
? m.btn_save()
|
|
: m.btn_create()}</Button
|
|
>
|
|
</div>
|
|
</div>
|
|
</Modal>
|