Mega push vol 5, working on messaging now
This commit is contained in:
@@ -1,6 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { getContext, onMount } from "svelte";
|
||||
import { Button, Modal, Avatar } from "$lib/components/ui";
|
||||
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,
|
||||
@@ -9,6 +17,7 @@
|
||||
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: {
|
||||
@@ -22,6 +31,7 @@
|
||||
let { data }: Props = $props();
|
||||
|
||||
const supabase = getContext<SupabaseClient<Database>>("supabase");
|
||||
const log = createLogger("page.calendar");
|
||||
|
||||
let events = $state(data.events);
|
||||
$effect(() => {
|
||||
@@ -32,17 +42,86 @@
|
||||
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",
|
||||
);
|
||||
|
||||
const allEvents = $derived([...events, ...googleEvents]);
|
||||
// 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);
|
||||
function handleDateClick(_date: Date) {
|
||||
// Event creation disabled
|
||||
|
||||
// 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) {
|
||||
@@ -50,11 +129,207 @@
|
||||
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()
|
||||
@@ -62,11 +337,25 @@
|
||||
|
||||
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) {
|
||||
console.error("Failed to delete event:", e);
|
||||
log.error("Failed to delete event", { error: e });
|
||||
}
|
||||
isDeleting = false;
|
||||
}
|
||||
@@ -78,8 +367,33 @@
|
||||
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() {
|
||||
@@ -96,31 +410,37 @@
|
||||
orgCalendarId = result.calendar_id;
|
||||
orgCalendarName = result.calendar_name;
|
||||
|
||||
if (result.events && result.events.length > 0) {
|
||||
googleEvents = result.events.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: "#4285f4",
|
||||
recurrence: null,
|
||||
created_by: data.user?.id ?? "",
|
||||
created_at: new Date().toISOString(),
|
||||
}),
|
||||
) as CalendarEvent[];
|
||||
}
|
||||
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) {
|
||||
console.error("Calendar API error:", result.error);
|
||||
log.error("Calendar API error", { error: result.error });
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to load Google events:", e);
|
||||
log.error("Failed to load Google events", { error: e });
|
||||
}
|
||||
isLoadingGoogle = false;
|
||||
}
|
||||
@@ -139,36 +459,49 @@
|
||||
<div class="flex flex-col h-full p-4 lg:p-5 gap-4">
|
||||
<!-- Header -->
|
||||
<header class="flex items-center gap-2 p-1">
|
||||
<Avatar name="Calendar" size="md" />
|
||||
<h1 class="flex-1 font-heading text-h1 text-white">Calendar</h1>
|
||||
{#if isOrgCalendarConnected}
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center gap-2 px-3 py-1.5 text-sm bg-primary/10 text-primary rounded-[32px] hover:bg-primary/20 transition-colors"
|
||||
onclick={subscribeToCalendar}
|
||||
title="Add to your Google Calendar"
|
||||
>
|
||||
<span
|
||||
class="material-symbols-rounded"
|
||||
style="font-size: 16px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 16;"
|
||||
>
|
||||
add
|
||||
</span>
|
||||
Subscribe
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
type="button"
|
||||
class="p-1 hover:bg-dark rounded-lg transition-colors"
|
||||
aria-label="More options"
|
||||
<h1 class="flex-1 font-heading text-h1 text-white">
|
||||
{m.calendar_title()}
|
||||
</h1>
|
||||
<Button size="md" onclick={() => handleDateClick(new Date())}
|
||||
>{m.btn_new()}</Button
|
||||
>
|
||||
<span
|
||||
class="material-symbols-rounded text-light"
|
||||
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
|
||||
>
|
||||
more_horiz
|
||||
</span>
|
||||
</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`;
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]}
|
||||
/>
|
||||
</header>
|
||||
|
||||
<!-- Calendar Grid -->
|
||||
@@ -270,18 +603,22 @@
|
||||
<span class="text-light/60 text-sm">
|
||||
{selectedEvent.id.startsWith("google-")
|
||||
? "Google Calendar Event"
|
||||
: "Local Event"}
|
||||
: selectedEvent.google_event_id
|
||||
? "Synced to Google Calendar"
|
||||
: "Local Event"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Google Calendar link -->
|
||||
{#if selectedEvent.id.startsWith("google-") && orgCalendarId}
|
||||
{#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/{selectedEvent.id.replace(
|
||||
'google-',
|
||||
'',
|
||||
)}?cid={encodeURIComponent(orgCalendarId)}"
|
||||
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"
|
||||
@@ -306,15 +643,14 @@
|
||||
</svg>
|
||||
Open in Google Calendar
|
||||
</a>
|
||||
<p class="text-xs text-light/40 mt-2">
|
||||
Edit this event directly in Google Calendar
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Delete local event -->
|
||||
<!-- Edit/Delete local event -->
|
||||
{#if !selectedEvent.id.startsWith("google-")}
|
||||
<div class="pt-3 border-t border-light/10">
|
||||
<div
|
||||
class="pt-3 border-t border-light/10 flex items-center justify-between"
|
||||
>
|
||||
<Button
|
||||
variant="danger"
|
||||
onclick={handleDeleteEvent}
|
||||
@@ -322,8 +658,137 @@
|
||||
>
|
||||
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)}
|
||||
></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>
|
||||
|
||||
Reference in New Issue
Block a user