From 150284ad2602362fd4137455f9c28da33f8deeda Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Mon, 28 Jul 2025 22:07:14 +0200 Subject: [PATCH] Add Webcal synchronization feature with modal and backend integration --- sql/16_create_calendar_sync_table.sql | 14 ++ ui/src/components/WebcalModal.tsx | 254 ++++++++++++++++++++++++++ ui/src/hooks/webcal.ts | 65 +++++++ ui/src/pages/planning.tsx | 26 +++ ui/src/utils/helpers.ts | 3 - 5 files changed, 359 insertions(+), 3 deletions(-) create mode 100644 sql/16_create_calendar_sync_table.sql create mode 100644 ui/src/components/WebcalModal.tsx create mode 100644 ui/src/hooks/webcal.ts diff --git a/sql/16_create_calendar_sync_table.sql b/sql/16_create_calendar_sync_table.sql new file mode 100644 index 0000000..3aaa553 --- /dev/null +++ b/sql/16_create_calendar_sync_table.sql @@ -0,0 +1,14 @@ +CREATE TABLE calendar_subscriptions ( + id UUID DEFAULT gen_random_uuid() PRIMARY KEY, + tablo_id TEXT NOT NULL UNIQUE REFERENCES tablos(id) ON DELETE CASCADE, + token TEXT NOT NULL UNIQUE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Create index on token for fast lookups +CREATE INDEX idx_calendar_subscriptions_token ON calendar_subscriptions(token); + +-- Note: Index on tablo_id is automatically created due to UNIQUE constraint + +-- Enable Row Level Security +ALTER TABLE calendar_subscriptions ENABLE ROW LEVEL SECURITY; diff --git a/ui/src/components/WebcalModal.tsx b/ui/src/components/WebcalModal.tsx new file mode 100644 index 0000000..14a55f4 --- /dev/null +++ b/ui/src/components/WebcalModal.tsx @@ -0,0 +1,254 @@ +import { useState } from "react"; +import { useTablosList } from "@ui/hooks/tablos"; +import { useGenerateWebcalToken } from "@ui/hooks/webcal"; +import { + Select, + SelectButton, + SelectPopover, + SelectListBox, + SelectListItem, +} from "@ui/ui-library/select"; +import { toast } from "@ui/ui-library/toast/toast-queue"; + +interface WebcalModalProps { + onClose: () => void; +} + +export const WebcalModal = ({ onClose }: WebcalModalProps) => { + const [selectedTabloId, setSelectedTabloId] = useState(null); + + const { data: tablos, isLoading: tablosLoading } = useTablosList(); + const { + generateWebcalUrl, + isPending, + data: webcalUrl, + } = useGenerateWebcalToken(); + + const copyToClipboard = async (text: string, type: string) => { + try { + await navigator.clipboard.writeText(text); + toast.add( + { + title: "Copié !", + description: `${type} copié dans le presse-papiers`, + type: "success", + }, + { timeout: 2000 } + ); + } catch (error) { + console.error("Failed to copy:", error); + toast.add( + { + title: "Erreur", + description: "Impossible de copier dans le presse-papiers", + type: "error", + }, + { timeout: 3000 } + ); + } + }; + + const getTabloName = (tabloId: string) => { + if (tabloId === null) return "Tous les tablos"; + return tablos?.find((t) => t.id === tabloId)?.name || "Tablo inconnu"; + }; + + return ( +
+
+ {/* Header */} +
+
+

+ Synchronisation de calendrier +

+ +
+
+ Synchronisez vos événements avec votre application de calendrier + préférée +
+
+ + {/* Content */} +
+ {/* Generate new subscription */} +
+

+ Créer un nouvel abonnement +

+ +
+
+ + +
+ + +
+ + {/* Generated webcal URLs */} + {webcalUrl && ( +
+

+ URLs générées pour “ + {getTabloName(selectedTabloId || "")} + ” +

+ +
+ {/* TODO: Add webcal URL */} + {/*
+ +
+ + +
+
*/} + +
+ +
+ + +
+
+
+ +
+

Instructions :

+
    +
  • + Copiez l'URL de souscription et ajoutez-la à votre + application de calendrier +
  • +
  • + Les événements se synchroniseront automatiquement toutes + les heures +
  • +
+
+
+ )} +
+
+ + {/* Footer */} +
+ +
+
+
+ ); +}; diff --git a/ui/src/hooks/webcal.ts b/ui/src/hooks/webcal.ts new file mode 100644 index 0000000..674a8cf --- /dev/null +++ b/ui/src/hooks/webcal.ts @@ -0,0 +1,65 @@ +import { useMutation } from "@tanstack/react-query"; +import { useSession } from "@ui/contexts/SessionContext"; +import { toast } from "@ui/ui-library/toast/toast-queue"; +import { api } from "@ui/lib/api"; + +export interface WebcalToken { + token: string; + metadata: { + tablo_id: string; + }; + created_at: string; +} + +export interface WebcalResponse { + webcal_url: string; + http_url: string; +} + +// Generate a new webcal token +export const useGenerateWebcalToken = () => { + const { session } = useSession(); + + const { mutate, isPending, data } = useMutation({ + mutationFn: async (tablo_id: string | null): Promise => { + const { data } = await api.post( + "/api/v1/tablos/webcal/generate-url", + { tablo_id }, + { + headers: { + Authorization: `Bearer ${session?.access_token}`, + }, + } + ); + return data; + }, + onSuccess: () => { + toast.add( + { + title: "URL de synchronisation générée", + description: + "Vous pouvez maintenant ajouter ce calendrier à votre application", + type: "success", + }, + { + timeout: 4000, + } + ); + }, + onError: (error) => { + console.error("Error generating webcal token:", error); + toast.add( + { + title: "Erreur", + description: "Impossible de générer l'URL de synchronisation", + type: "error", + }, + { + timeout: 4000, + } + ); + }, + }); + + return { generateWebcalUrl: mutate, isPending, data }; +}; diff --git a/ui/src/pages/planning.tsx b/ui/src/pages/planning.tsx index b574955..1646cdb 100644 --- a/ui/src/pages/planning.tsx +++ b/ui/src/pages/planning.tsx @@ -11,6 +11,7 @@ import { import { Outlet, useNavigate, useParams } from "react-router-dom"; import { generateICSFromEvents, downloadICSFile } from "@ui/utils/helpers"; import { ImportICSModal } from "@ui/components/ImportICSModal"; +import { WebcalModal } from "@ui/components/WebcalModal"; type ViewType = "month" | "week" | "day"; @@ -24,6 +25,7 @@ export const PlanningPage = () => { tablo_id || "all" ); const [isImportModalOpen, setIsImportModalOpen] = useState(false); + const [isWebcalModalOpen, setIsWebcalModalOpen] = useState(false); // Fetch tablos const { data: tablos, isLoading: tablosLoading } = useTablosList(); @@ -835,6 +837,26 @@ export const PlanningPage = () => { Exporter +
{(["month", "week", "day"] as ViewType[]).map((view) => (
); }; diff --git a/ui/src/utils/helpers.ts b/ui/src/utils/helpers.ts index 5fd3437..b773768 100644 --- a/ui/src/utils/helpers.ts +++ b/ui/src/utils/helpers.ts @@ -118,8 +118,6 @@ export const generateICSFromEvents = ( events.forEach((event) => { if (!event.start_date || !event.start_time || !event.title) return; - console.log("event", event); - const startDateTime = formatDate(event.start_date, event.start_time); const endDateTime = event.end_time ? formatDate(event.start_date, event.end_time) @@ -156,7 +154,6 @@ export const downloadICSFile = ( icsContent: string, filename: string = "planning.ics" ) => { - console.log("icsContent", icsContent); const blob = new Blob([icsContent], { type: "text/calendar;charset=utf-8" }); const link = document.createElement("a"); link.href = URL.createObjectURL(blob);