My-Weekly-ToDo-List/src/components/OnboardingWizard.tsx
mARTin ee4b540558 feat: add Notion provider integration + Apple Reminders deprecation note
- Notion OAuth integration: start/callback routes, calendar-events dispatch,
  CRUD operations (create/update/delete via Notion API)
- New notion-calendar.ts provider library with database discovery
- Onboarding wizard: expanded to 6 steps (header/tasks/display design),
  improved preview fidelity, universal dummy content, Notion in connect step
- Apple Calendar: label changed to "events only", added warning that
  Reminders are unsupported since iOS 13 (no CalDAV/API from Apple)
- Fixed 12h time format on now-line, wizard settings apply on completion

v1.57.0

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 21:02:25 +01:00

1150 lines
64 KiB
TypeScript

"use client";
import React, { useState, useEffect } from "react";
import { ChevronRight, ChevronLeft, X, Sun, Moon, Check } from "lucide-react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faGoogle, faApple, faMicrosoft, faNotion } from "@fortawesome/free-brands-svg-icons";
import { faServer } from "@fortawesome/free-solid-svg-icons";
import FontPicker from "./FontPicker";
interface OnboardingWizardProps {
profile: any;
darkMode: boolean;
language: string;
connections: any[];
onComplete: () => void;
onSkip: () => void;
saveSetting: (key: string, value: any) => Promise<void>;
onLanguageChange: (lang: string) => void;
onDarkModeToggle: () => void;
onConnectProvider?: (provider: string) => void;
}
const STEPS = ["welcome", "language", "connect", "header", "tasks", "display"] as const;
const translations: Record<string, Record<string, string>> = {
en: {
welcomeTitle: "Welcome to My Weekly ToDo List",
welcomeSubtitle: "Let's set up your workspace in under a minute",
getStarted: "Get Started",
skipSetup: "Skip setup",
next: "Next",
back: "Back",
finish: "Start Using My Weekly ToDo List",
langTitle: "Language & Region",
langSubtitle: "Choose your language, formats and week start",
timezone: "Timezone",
dateFormat: "Date Format",
timeFormat: "Time Format",
weekStartLabel: "Start week on",
monday: "Monday",
sunday: "Sunday",
connectTitle: "Connect Your Services",
connectSubtitle: "Sync calendars and tasks from your favorite services",
connectGoogle: "Google Calendar & Tasks",
connectApple: "Apple Calendar (events only)",
connectOutlook: "Microsoft Outlook",
connectSynology: "Synology Calendar",
connectNotion: "Notion",
connected: "Connected",
connect: "Connect",
skipConnect: "Skip for now — you can connect later in Settings",
// Header step
headerTitle: "Day Headers",
headerSubtitle: "Style the weekday names and dates at the top of each column",
weekdayFont: "Weekday Font",
dateSection: "Date",
datePosition: "Position",
dateAlignment: "Alignment",
datePositionRight: "Right",
datePositionLeft: "Left",
datePositionAbove: "Above",
datePositionBelow: "Below",
datePositionHidden: "Hidden",
alignLeft: "Left",
alignCenter: "Center",
alignRight: "Right",
alignTight: "Tight",
weekdayFormat: "Weekday Format",
weekdayFull: "Full (Monday)",
weekdayShort: "Short (Mon)",
weekdayNarrow: "Narrow (M)",
// Tasks step
tasksTitle: "Tasks & Events",
tasksSubtitle: "Choose how your tasks and calendar events look",
taskFont: "Task Font",
eventFont: "Calendar Event Font",
fontSize: "Base Font Size",
// Display step
displayTitle: "Display Options",
displaySubtitle: "Toggle which areas to show and pick your theme",
showAllDay: "All-Day Events",
showSomeday: "Someday Area",
showTimeGrid: "Time Grid",
showCheckboxes: "Task Checkboxes",
darkMode: "Dark Mode",
somedayLabel: "Someday",
color: "Color",
},
de: {
welcomeTitle: "Willkommen bei My Weekly ToDo List",
welcomeSubtitle: "Richte deinen Arbeitsbereich in unter einer Minute ein",
getStarted: "Los geht's",
skipSetup: "Setup überspringen",
next: "Weiter",
back: "Zurück",
finish: "My Weekly ToDo List starten",
langTitle: "Sprache & Region",
langSubtitle: "Wähle Sprache, Formate und Wochenstart",
timezone: "Zeitzone",
dateFormat: "Datumsformat",
timeFormat: "Zeitformat",
weekStartLabel: "Woche beginnt am",
monday: "Montag",
sunday: "Sonntag",
connectTitle: "Dienste verbinden",
connectSubtitle: "Synchronisiere Kalender und Aufgaben",
connectGoogle: "Google Kalender & Aufgaben",
connectApple: "Apple Kalender (nur Ereignisse)",
connectOutlook: "Microsoft Outlook",
connectSynology: "Synology Kalender",
connectNotion: "Notion",
connected: "Verbunden",
connect: "Verbinden",
skipConnect: "Jetzt überspringen — du kannst später in den Einstellungen verbinden",
headerTitle: "Tagesüberschriften",
headerSubtitle: "Gestalte die Wochentage und Datumsanzeige über den Spalten",
weekdayFont: "Wochentag-Schrift",
dateSection: "Datum",
datePosition: "Position",
dateAlignment: "Ausrichtung",
datePositionRight: "Rechts",
datePositionLeft: "Links",
datePositionAbove: "Darüber",
datePositionBelow: "Darunter",
datePositionHidden: "Ausblenden",
alignLeft: "Links",
alignCenter: "Zentriert",
alignRight: "Rechts",
alignTight: "Eng",
weekdayFormat: "Wochentag-Format",
weekdayFull: "Voll (Montag)",
weekdayShort: "Kurz (Mo)",
weekdayNarrow: "Schmal (M)",
tasksTitle: "Aufgaben & Termine",
tasksSubtitle: "Wähle das Aussehen deiner Aufgaben und Kalendereinträge",
taskFont: "Aufgaben-Schrift",
eventFont: "Kalenderevent-Schrift",
fontSize: "Basis-Schriftgröße",
displayTitle: "Anzeige-Optionen",
displaySubtitle: "Wähle welche Bereiche sichtbar sein sollen und dein Farbschema",
showAllDay: "Ganztags-Termine",
showSomeday: "Someday-Bereich",
showTimeGrid: "Zeitraster",
showCheckboxes: "Aufgaben-Checkboxen",
darkMode: "Dunkler Modus",
somedayLabel: "Someday",
color: "Farbe",
},
fr: {
welcomeTitle: "Bienvenue sur My Weekly ToDo List",
welcomeSubtitle: "Configurez votre espace en moins d'une minute",
getStarted: "Commencer",
skipSetup: "Passer la configuration",
next: "Suivant",
back: "Retour",
finish: "Commencer à utiliser My Weekly ToDo List",
langTitle: "Langue & Région",
langSubtitle: "Choisissez langue, formats et début de semaine",
timezone: "Fuseau horaire",
dateFormat: "Format de date",
timeFormat: "Format d'heure",
weekStartLabel: "Semaine commence le",
monday: "Lundi",
sunday: "Dimanche",
connectTitle: "Connectez vos services",
connectSubtitle: "Synchronisez calendriers et tâches",
connectGoogle: "Google Agenda & Tâches",
connectApple: "Calendrier Apple (événements uniquement)",
connectOutlook: "Microsoft Outlook",
connectSynology: "Calendrier Synology",
connectNotion: "Notion",
connected: "Connecté",
connect: "Connecter",
skipConnect: "Passer pour l'instant — vous pourrez connecter plus tard",
headerTitle: "En-têtes de jour",
headerSubtitle: "Personnalisez les noms et dates en haut de chaque colonne",
weekdayFont: "Police jour",
dateSection: "Date",
datePosition: "Position",
dateAlignment: "Alignement",
datePositionRight: "Droite",
datePositionLeft: "Gauche",
datePositionAbove: "Au-dessus",
datePositionBelow: "En-dessous",
datePositionHidden: "Masqué",
alignLeft: "Gauche",
alignCenter: "Centre",
alignRight: "Droite",
alignTight: "Serré",
weekdayFormat: "Format jour",
weekdayFull: "Complet (Lundi)",
weekdayShort: "Court (Lun)",
weekdayNarrow: "Étroit (L)",
tasksTitle: "Tâches & Événements",
tasksSubtitle: "Choisissez l'apparence de vos tâches et événements",
taskFont: "Police tâches",
eventFont: "Police événements",
fontSize: "Taille de base",
displayTitle: "Options d'affichage",
displaySubtitle: "Activez les zones à afficher et choisissez votre thème",
showAllDay: "Événements journée entière",
showSomeday: "Zone Someday",
showTimeGrid: "Grille horaire",
showCheckboxes: "Cases à cocher",
darkMode: "Mode sombre",
somedayLabel: "Someday",
color: "Couleur",
},
es: {
welcomeTitle: "Bienvenido a My Weekly ToDo List",
welcomeSubtitle: "Configura tu espacio en menos de un minuto",
getStarted: "Empezar",
skipSetup: "Omitir configuración",
next: "Siguiente",
back: "Atrás",
finish: "Empezar a usar My Weekly ToDo List",
langTitle: "Idioma y Región",
langSubtitle: "Elige idioma, formatos e inicio de semana",
timezone: "Zona horaria",
dateFormat: "Formato de fecha",
timeFormat: "Formato de hora",
weekStartLabel: "Semana empieza el",
monday: "Lunes",
sunday: "Domingo",
connectTitle: "Conecta tus servicios",
connectSubtitle: "Sincroniza calendarios y tareas",
connectGoogle: "Google Calendar y Tareas",
connectApple: "Calendario Apple (solo eventos)",
connectOutlook: "Microsoft Outlook",
connectSynology: "Calendario Synology",
connectNotion: "Notion",
connected: "Conectado",
connect: "Conectar",
skipConnect: "Omitir por ahora — puedes conectar después en Ajustes",
headerTitle: "Encabezados de día",
headerSubtitle: "Personaliza los nombres y fechas de cada columna",
weekdayFont: "Fuente día",
dateSection: "Fecha",
datePosition: "Posición",
dateAlignment: "Alineación",
datePositionRight: "Derecha",
datePositionLeft: "Izquierda",
datePositionAbove: "Arriba",
datePositionBelow: "Abajo",
datePositionHidden: "Oculta",
alignLeft: "Izquierda",
alignCenter: "Centro",
alignRight: "Derecha",
alignTight: "Ajustado",
weekdayFormat: "Formato día",
weekdayFull: "Completo (Lunes)",
weekdayShort: "Corto (Lun)",
weekdayNarrow: "Estrecho (L)",
tasksTitle: "Tareas y Eventos",
tasksSubtitle: "Elige cómo se ven tus tareas y eventos del calendario",
taskFont: "Fuente tareas",
eventFont: "Fuente eventos",
fontSize: "Tamaño base",
displayTitle: "Opciones de pantalla",
displaySubtitle: "Elige qué áreas mostrar y tu tema de color",
showAllDay: "Eventos de todo el día",
showSomeday: "Área Someday",
showTimeGrid: "Rejilla horaria",
showCheckboxes: "Casillas de verificación",
darkMode: "Modo oscuro",
somedayLabel: "Someday",
color: "Color",
},
};
const COMMON_TIMEZONES = [
"Europe/Berlin", "Europe/London", "Europe/Paris", "Europe/Madrid",
"Europe/Rome", "Europe/Amsterdam", "Europe/Zurich", "Europe/Vienna",
"America/New_York", "America/Chicago", "America/Denver", "America/Los_Angeles",
"America/Toronto", "America/Sao_Paulo", "America/Mexico_City",
"Asia/Tokyo", "Asia/Shanghai", "Asia/Kolkata", "Asia/Singapore", "Asia/Dubai",
"Australia/Sydney", "Pacific/Auckland", "UTC"
];
const DATE_FORMATS = [
{ value: "DD.MM.YYYY", example: "25.03.2026" },
{ value: "MM/DD/YYYY", example: "03/25/2026" },
{ value: "YYYY-MM-DD", example: "2026-03-25" },
{ value: "DD/MM/YYYY", example: "25/03/2026" },
];
const LANGUAGES = [
{ code: "de", label: "Deutsch", flag: "DE" },
{ code: "en", label: "English", flag: "EN" },
{ code: "fr", label: "Français", flag: "FR" },
{ code: "es", label: "Español", flag: "ES" },
];
const WEIGHT_OPTS = [
{ value: "300", label: "Light" },
{ value: "400", label: "Normal" },
{ value: "500", label: "Medium" },
{ value: "600", label: "Semi" },
{ value: "700", label: "Bold" },
{ value: "900", label: "Black" },
];
export default function OnboardingWizard({
profile,
darkMode,
language,
connections,
onComplete,
onSkip,
saveSetting,
onLanguageChange,
onDarkModeToggle,
onConnectProvider,
}: OnboardingWizardProps) {
const [currentStep, setCurrentStep] = useState(0);
const [direction, setDirection] = useState<"forward" | "backward">("forward");
const [animKey, setAnimKey] = useState(0);
// Language step
const [selectedLang, setSelectedLang] = useState(language || "de");
const [selectedTimezone, setSelectedTimezone] = useState(profile.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC");
const [selectedDateFormat, setSelectedDateFormat] = useState(profile.dateFormat || "DD.MM.YYYY");
const [selectedTimeFormat, setSelectedTimeFormat] = useState(profile.timeFormat || "24H");
const [selectedWeekStart, setSelectedWeekStart] = useState(profile.weekStartDay ?? 1);
// Header step
const [selectedHeadlineFont, setSelectedHeadlineFont] = useState(profile.headlineFont || "Oswald");
const [selectedHeadlineFontSize, setSelectedHeadlineFontSize] = useState(profile.headlineFontSize || "1.25rem");
const [selectedHeadlineFontWeight, setSelectedHeadlineFontWeight] = useState(profile.headlineFontWeight || "900");
const [selectedWeekdayColor, setSelectedWeekdayColor] = useState(profile.weekdayColor || (darkMode ? "#d1d5db" : "#555555"));
const [selectedDateFontSize, setSelectedDateFontSize] = useState(profile.dateFontSize || "0.65rem");
const [selectedDateFontWeight, setSelectedDateFontWeight] = useState(profile.dateFontWeight || "400");
const [selectedDateColor, setSelectedDateColor] = useState(profile.dateColor || (darkMode ? "#9ca3af" : "#888888"));
const [selectedDateLayout, setSelectedDateLayout] = useState(profile.dateLayout || "right");
const [selectedDateAlignment, setSelectedDateAlignment] = useState(profile.dateAlignment || "center");
const [selectedWeekdayFormat, setSelectedWeekdayFormat] = useState(profile.weekdayFormat || "full");
// Tasks step
const [selectedBodyFont, setSelectedBodyFont] = useState(profile.bodyFont || "Inter");
const [selectedTaskFontSize, setSelectedTaskFontSize] = useState(profile.taskFontSize || "0.9rem");
const [selectedTaskFontWeight, setSelectedTaskFontWeight] = useState(profile.taskFontWeight || "400");
const [selectedTaskColor, setSelectedTaskColor] = useState(profile.taskColor || (darkMode ? "#e5e7eb" : "#333333"));
const [selectedEventFont, setSelectedEventFont] = useState(profile.eventFontFamily || "Inter");
const [selectedEventFontSize, setSelectedEventFontSize] = useState(profile.eventFontSize || "0.85rem");
const [selectedEventFontWeight, setSelectedEventFontWeight] = useState(profile.eventFontWeight || "400");
const [selectedFontSize, setSelectedFontSize] = useState(profile.fontSize || "M");
// Display step
const [selectedShowSomeday, setSelectedShowSomeday] = useState(profile.showSomeday !== false);
const [selectedShowAllDay, setSelectedShowAllDay] = useState(profile.showAllDayEvents !== false);
const [selectedShowTimeGrid, setSelectedShowTimeGrid] = useState(profile.showTimeGrid !== false);
const [selectedShowCheckboxes, setSelectedShowCheckboxes] = useState(profile.showTaskCheckboxes === true);
useEffect(() => {
const savedStep = localStorage.getItem("onboarding_step");
if (savedStep) {
const step = parseInt(savedStep, 10);
setCurrentStep(Math.min(step, STEPS.length - 1));
localStorage.removeItem("onboarding_step");
}
}, []);
const t = translations[selectedLang] || translations.en;
const goNext = async () => {
await saveCurrentStep();
setDirection("forward");
setAnimKey((k) => k + 1);
setCurrentStep((s) => Math.min(s + 1, STEPS.length - 1));
};
const goBack = () => {
setDirection("backward");
setAnimKey((k) => k + 1);
setCurrentStep((s) => Math.max(s - 1, 0));
};
const saveCurrentStep = async () => {
const step = STEPS[currentStep];
if (step === "language") {
await saveSetting("language", selectedLang);
await saveSetting("timezone", selectedTimezone);
await saveSetting("dateFormat", selectedDateFormat);
await saveSetting("timeFormat", selectedTimeFormat);
await saveSetting("weekStartDay", selectedWeekStart);
onLanguageChange(selectedLang);
} else if (step === "header") {
await saveSetting("headlineFont", selectedHeadlineFont);
await saveSetting("headlineFontSize", selectedHeadlineFontSize);
await saveSetting("headlineFontWeight", selectedHeadlineFontWeight);
await saveSetting("weekdayColor", selectedWeekdayColor);
await saveSetting("dateFontSize", selectedDateFontSize);
await saveSetting("dateFontWeight", selectedDateFontWeight);
await saveSetting("dateColor", selectedDateColor);
await saveSetting("dateLayout", selectedDateLayout);
await saveSetting("dateAlignment", selectedDateAlignment);
await saveSetting("weekdayFormat", selectedWeekdayFormat);
} else if (step === "tasks") {
await saveSetting("bodyFont", selectedBodyFont);
await saveSetting("taskFontFamily", selectedBodyFont);
await saveSetting("timeTaskFontFamily", selectedBodyFont);
await saveSetting("taskFontSize", selectedTaskFontSize);
await saveSetting("timeTaskFontSize", selectedTaskFontSize);
await saveSetting("taskFontWeight", selectedTaskFontWeight);
await saveSetting("timeTaskFontWeight", selectedTaskFontWeight);
await saveSetting("taskColor", selectedTaskColor);
await saveSetting("eventFontFamily", selectedEventFont);
await saveSetting("eventFontSize", selectedEventFontSize);
await saveSetting("eventFontWeight", selectedEventFontWeight);
await saveSetting("fontSize", selectedFontSize);
} else if (step === "display") {
await saveSetting("showSomeday", selectedShowSomeday);
await saveSetting("showAllDayEvents", selectedShowAllDay);
await saveSetting("showTimeGrid", selectedShowTimeGrid);
await saveSetting("showTaskCheckboxes", selectedShowCheckboxes);
}
};
const handleFinish = async () => {
await saveCurrentStep();
onComplete();
};
const connectedProviders = new Set(connections.map((c: any) => c.provider));
const handleOAuthConnect = (url: string) => {
localStorage.setItem("onboarding_step", String(currentStep));
window.location.href = url;
};
const handleProviderConnect = (provider: string) => {
if (onConnectProvider) {
localStorage.setItem("onboarding_step", String(currentStep));
onConnectProvider(provider);
}
};
const bg = darkMode ? "#1f2937" : "#ffffff";
const textColor = darkMode ? "#e5e7eb" : "#333333";
const mutedColor = darkMode ? "#9ca3af" : "#6b7280";
const borderColor = darkMode ? "#374151" : "#e5e7eb";
const accentColor = "#0d9488";
const inputBg = darkMode ? "#111827" : "#fff";
const itemBg = darkMode ? "#111827" : "#f9fafb";
const btnStyle: React.CSSProperties = {
display: "inline-flex", alignItems: "center", gap: "6px",
padding: "10px 24px", borderRadius: "10px", border: "none",
fontSize: "0.95rem", fontWeight: 600, cursor: "pointer",
transition: "all 0.15s",
};
const primaryBtn: React.CSSProperties = { ...btnStyle, background: accentColor, color: "#fff" };
const secondaryBtn: React.CSSProperties = { ...btnStyle, background: darkMode ? "#374151" : "#f3f4f6", color: textColor };
const labelStyle: React.CSSProperties = { display: "block", fontSize: "0.78rem", fontWeight: 600, color: textColor, marginBottom: "3px" };
const selectStyle: React.CSSProperties = {
width: "100%", padding: "5px 8px", borderRadius: "6px", border: `1px solid ${borderColor}`,
background: inputBg, color: textColor, fontSize: "0.8rem",
};
const smallInputStyle: React.CSSProperties = { ...selectStyle, padding: "5px 8px" };
const colorPickerStyle: React.CSSProperties = {
width: "26px", height: "26px", border: `1px solid ${borderColor}`,
borderRadius: "4px", cursor: "pointer", padding: 0, flexShrink: 0,
};
const isLastStep = currentStep === STEPS.length - 1;
const isWideStep = ["header", "tasks", "display"].includes(STEPS[currentStep]);
// ─── Preview ───
const renderPreview = () => {
const sizeScale = selectedFontSize === "S" ? 0.85 : selectedFontSize === "L" ? 1.15 : 1.0;
const pvBg = darkMode ? "#1a1a2e" : "#ffffff";
const pvBorder = darkMode ? "#334155" : "#eee";
const pvMuted = darkMode ? "#64748b" : "#a6a6a7";
const pvTodayBg = darkMode ? "rgba(13,148,136,0.06)" : "rgba(13,148,136,0.03)";
const dayNamesMap: Record<string, Record<string, string[]>> = {
full: { en: ["SUNDAY", "MONDAY"], de: ["SONNTAG", "MONTAG"], fr: ["DIMANCHE", "LUNDI"], es: ["DOMINGO", "LUNES"] },
short: { en: ["SUN", "MON"], de: ["SO", "MO"], fr: ["DIM", "LUN"], es: ["DOM", "LUN"] },
narrow: { en: ["S", "M"], de: ["S", "M"], fr: ["D", "L"], es: ["D", "L"] },
};
const cols = (dayNamesMap[selectedWeekdayFormat] || dayNamesMap.full)[selectedLang] || dayNamesMap.full.en;
const colDates = ["22. MÄR", "23. MÄR"];
if (selectedLang === "en") { colDates[0] = "MAR 22"; colDates[1] = "MAR 23"; }
if (selectedLang === "fr") { colDates[0] = "22 MARS"; colDates[1] = "23 MARS"; }
if (selectedLang === "es") { colDates[0] = "22 MAR"; colDates[1] = "23 MAR"; }
const allDayEvents = [
[
{ title: selectedLang === "de" ? "Omas Geburtstag" : "Grandma's Birthday", color: "#ec4899" },
{ title: selectedLang === "de" ? "Pizza-Abend" : "Pizza Night", color: "#f59e0b" },
],
[
{ title: selectedLang === "de" ? "Netflix & Snacks" : "Netflix & Snacks", color: "#6366f1" },
],
];
const taskLists = [
[
{ title: selectedLang === "de" ? "Endlich den Keller aufräumen" : "Finally clean the basement", color: "#8b5cf6" },
{ title: selectedLang === "de" ? "Herausfinden warum die Pflanze traurig aussieht" : "Figure out why the plant looks sad", color: "#10b981" },
{ title: "", color: "" },
{ title: selectedLang === "de" ? "Steuererklärung (diesmal wirklich)" : "Tax return (for real this time)", color: "#ef4444" },
{ title: selectedLang === "de" ? "Geburtstagsgeschenk für Mama besorgen" : "Get birthday gift for Mom", color: "#f59e0b" },
],
[
{ title: selectedLang === "de" ? "Arzttermin nicht vergessen!" : "Don't forget doctor appointment!", color: "#3b82f6" },
{ title: "", color: "" },
{ title: selectedLang === "de" ? "Kühlschrank ausmisten (riecht komisch)" : "Clean out fridge (smells weird)", color: "#ef4444" },
{ title: selectedLang === "de" ? "10 Min joggen (oder 10 Min drüber nachdenken)" : "10 min jog (or 10 min thinking about it)", color: "#8b5cf6" },
{ title: "", color: "" },
{ title: selectedLang === "de" ? "WLAN-Passwort endlich aufschreiben" : "Finally write down the WiFi password", color: "#f59e0b" },
],
];
const somedayLists = [
{ name: selectedLang === "de" ? "Irgendwann" : "Someday", count: 5, active: true },
{ name: selectedLang === "de" ? "Träume" : "Dreams", count: 2, active: false },
];
const somedayTasks = [
selectedLang === "de" ? "Buch schreiben" : "Write a book",
selectedLang === "de" ? "Spanisch lernen" : "Learn Spanish",
];
const headerAlign = selectedDateAlignment === "left" ? "flex-start"
: selectedDateAlignment === "right" ? "flex-end" : "center";
const renderDayHeader = (dayName: string, date: string, isToday: boolean) => {
const dayStyle: React.CSSProperties = {
fontFamily: `'${selectedHeadlineFont}', sans-serif`,
fontSize: `calc(${selectedHeadlineFontSize} * ${sizeScale})`,
fontWeight: parseInt(selectedHeadlineFontWeight) as any,
color: isToday ? accentColor : selectedWeekdayColor,
letterSpacing: "-0.02em",
lineHeight: 1.2,
margin: 0,
};
const dStyle: React.CSSProperties = {
fontFamily: `'${selectedBodyFont}', sans-serif`,
fontSize: `calc(${selectedDateFontSize} * ${sizeScale})`,
fontWeight: parseInt(selectedDateFontWeight) as any,
color: selectedDateColor,
textTransform: "uppercase" as const,
letterSpacing: "0.1em",
lineHeight: 1.2,
};
const isVertical = selectedDateLayout === "above" || selectedDateLayout === "below";
const pad: React.CSSProperties = { padding: "12px 12px 8px" };
if (selectedDateLayout === "hidden") return <div style={{ ...pad, display: "flex", justifyContent: headerAlign }}><span style={dayStyle}>{dayName}</span></div>;
if (isVertical) return (
<div style={{ ...pad, display: "flex", flexDirection: selectedDateLayout === "above" ? "column-reverse" : "column", alignItems: headerAlign === "center" ? "center" : headerAlign === "flex-end" ? "flex-end" : "flex-start" }}>
<span style={dayStyle}>{dayName}</span>
<span style={dStyle}>{date}</span>
</div>
);
if (selectedDateLayout === "left") return <div style={{ ...pad, display: "flex", alignItems: "baseline", gap: selectedDateAlignment === "tight" ? "2px" : "0.35em", justifyContent: headerAlign }}><span style={dStyle}>{date}</span><span style={dayStyle}>{dayName}</span></div>;
return <div style={{ ...pad, display: "flex", alignItems: "baseline", gap: selectedDateAlignment === "tight" ? "2px" : "0.35em", justifyContent: headerAlign }}><span style={dayStyle}>{dayName}</span><span style={dStyle}>{date}</span></div>;
};
return (
<div style={{
borderRadius: "10px", overflow: "hidden", border: `1px solid ${pvBorder}`,
background: pvBg, height: "100%", display: "flex", flexDirection: "column",
}}>
{/* All-day events */}
{selectedShowAllDay && (
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr" }}>
{allDayEvents.map((evts, colIdx) => (
<div key={colIdx} style={{
padding: "4px 4px 2px",
borderLeft: colIdx > 0 ? `1px solid ${pvBorder}` : "none",
}}>
{evts.map((ev) => (
<div key={ev.title} style={{
background: ev.color, color: "#fff",
borderRadius: "4px", padding: "3px 8px", marginBottom: "3px",
fontFamily: `'${selectedEventFont}', sans-serif`,
fontSize: `calc(${selectedEventFontSize} * ${sizeScale})`,
fontWeight: parseInt(selectedEventFontWeight) as any,
whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis",
}}>
{ev.title}
</div>
))}
</div>
))}
</div>
)}
{/* Day headers */}
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr" }}>
{cols.map((day, i) => (
<div key={day + i} style={{ borderLeft: i > 0 ? `1px solid ${pvBorder}` : "none" }}>
{renderDayHeader(day, colDates[i], i === 0)}
</div>
))}
</div>
{/* Task columns */}
<div style={{ flex: 1, overflow: "auto", display: "grid", gridTemplateColumns: "1fr 1fr" }}>
{[0, 1].map((colIdx) => (
<div key={colIdx} style={{
borderLeft: colIdx > 0 ? `1px solid ${pvBorder}` : "none",
background: colIdx === 0 ? pvTodayBg : "transparent",
position: "relative",
}}>
{taskLists[colIdx].map((task, tIdx) => (
task.title ? (
<div key={tIdx} style={{
padding: "4px 12px",
display: "flex", alignItems: "flex-start", gap: "0px",
borderBottom: `1px solid ${pvBorder}`,
minHeight: "28px", lineHeight: 1.4,
}}>
{selectedShowCheckboxes && (
<div style={{
width: "13px", height: "13px", border: `1px solid ${pvBorder}`,
borderRadius: "3px", marginRight: "6px", marginTop: "3px",
flexShrink: 0,
}} />
)}
<div style={{
width: "3px", minHeight: "16px", alignSelf: "stretch",
background: task.color, borderRadius: "2px",
marginRight: "8px", flexShrink: 0,
}} />
<div style={{
fontFamily: `'${selectedBodyFont}', sans-serif`,
fontSize: `calc(${selectedTaskFontSize} * ${sizeScale})`,
fontWeight: parseInt(selectedTaskFontWeight) as any,
color: selectedTaskColor, lineHeight: 1.4,
}}>
{task.title}
</div>
</div>
) : (
<div key={tIdx} style={{ borderBottom: `1px solid ${pvBorder}`, height: "28px" }} />
)
))}
{Array.from({ length: 4 }).map((_, i) => (
<div key={`empty-${i}`} style={{ borderBottom: `1px solid ${pvBorder}`, height: "28px" }} />
))}
{colIdx === 0 && (
<div style={{
position: "absolute", bottom: "30px", left: 0, right: 0,
borderTop: "2px solid #ef4444", zIndex: 3,
}}>
<div style={{
width: "8px", height: "8px", borderRadius: "50%",
background: "#ef4444", position: "absolute",
top: "-5px", left: "-4px",
}} />
<span style={{
position: "absolute", top: "-8px", left: "14px",
fontSize: "0.6rem", color: "#ef4444", fontWeight: 600,
fontFamily: `'${selectedBodyFont}', sans-serif`,
}}>19:21</span>
</div>
)}
</div>
))}
</div>
{/* Someday area */}
{selectedShowSomeday && (
<div style={{ borderTop: `1px solid ${pvBorder}`, background: darkMode ? "#111827" : "#fafafa" }}>
<div style={{ display: "flex", gap: "0px", padding: "0 8px", borderBottom: `1px solid ${pvBorder}` }}>
{somedayLists.map((list) => (
<div key={list.name} style={{
padding: "5px 10px",
fontFamily: `'${selectedBodyFont}', sans-serif`,
fontSize: `calc(${selectedTaskFontSize} * ${sizeScale} * 0.8)`,
fontWeight: list.active ? 700 : 400,
color: list.active ? "#fff" : pvMuted,
background: list.active ? accentColor : "transparent",
borderRadius: list.active ? "6px 6px 0 0" : "0",
cursor: "pointer", display: "flex", alignItems: "center", gap: "4px",
}}>
{list.name}
<span style={{
fontSize: "0.65rem",
background: list.active ? "rgba(255,255,255,0.3)" : (darkMode ? "#334155" : "#e2e8f0"),
borderRadius: "8px", padding: "0 5px", fontWeight: 600,
}}>{list.count}</span>
</div>
))}
</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", padding: "6px 8px 8px", gap: "8px" }}>
{somedayTasks.map((task) => (
<div key={task} style={{
fontFamily: `'${selectedHeadlineFont}', sans-serif`,
fontSize: `calc(${selectedHeadlineFontSize} * ${sizeScale} * 0.65)`,
fontWeight: parseInt(selectedHeadlineFontWeight) as any,
color: selectedWeekdayColor,
}}>
{task}
<div style={{
fontFamily: `'${selectedBodyFont}', sans-serif`,
fontSize: `calc(${selectedTaskFontSize} * ${sizeScale} * 0.85)`,
color: pvMuted, fontWeight: 400, marginTop: "2px",
}}>
{selectedLang === "de" ? "Aufgabe hinzufügen..." : "Add task..."}
</div>
</div>
))}
</div>
</div>
)}
</div>
);
};
// ─── Render Steps ───
const renderStep = () => {
const step = STEPS[currentStep];
if (step === "welcome") {
return (
<div style={{ textAlign: "center", padding: "40px 30px" }}>
<div style={{ fontSize: "3rem", marginBottom: "16px" }}>&#10003;</div>
<h1 style={{ fontSize: "1.5rem", fontWeight: 700, margin: "0 0 12px", color: textColor }}>
{t.welcomeTitle}
</h1>
<p style={{ color: mutedColor, fontSize: "1rem", margin: "0 0 32px", lineHeight: 1.5 }}>
{t.welcomeSubtitle}
</p>
<button onClick={goNext} style={primaryBtn}>
{t.getStarted} <ChevronRight size={16} />
</button>
<div style={{ marginTop: "16px" }}>
<button onClick={onSkip} style={{ background: "none", border: "none", color: mutedColor, cursor: "pointer", fontSize: "0.85rem", textDecoration: "underline" }}>
{t.skipSetup}
</button>
</div>
</div>
);
}
if (step === "language") {
return (
<div style={{ padding: "30px" }}>
<h2 style={{ fontSize: "1.3rem", fontWeight: 700, margin: "0 0 6px", color: textColor }}>{t.langTitle}</h2>
<p style={{ color: mutedColor, margin: "0 0 20px", fontSize: "0.9rem" }}>{t.langSubtitle}</p>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "10px", marginBottom: "20px" }}>
{LANGUAGES.map((lang) => (
<button
key={lang.code}
onClick={() => setSelectedLang(lang.code)}
style={{
padding: "14px 16px", borderRadius: "10px", border: `2px solid ${selectedLang === lang.code ? accentColor : borderColor}`,
background: selectedLang === lang.code ? (darkMode ? "rgba(13,148,136,0.15)" : "rgba(13,148,136,0.06)") : "transparent",
cursor: "pointer", textAlign: "left", display: "flex", alignItems: "center", gap: "10px",
color: textColor, fontSize: "0.95rem", fontWeight: selectedLang === lang.code ? 600 : 400,
transition: "all 0.15s",
}}
>
<span style={{ fontWeight: 700, fontSize: "0.75rem", background: darkMode ? "#374151" : "#e5e7eb", padding: "2px 6px", borderRadius: "4px" }}>{lang.flag}</span>
{lang.label}
{selectedLang === lang.code && <Check size={16} style={{ marginLeft: "auto", color: accentColor }} />}
</button>
))}
</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "12px", marginBottom: "16px" }}>
<div>
<label style={{ display: "block", fontSize: "0.85rem", fontWeight: 600, color: textColor, marginBottom: "6px" }}>{t.dateFormat}</label>
<select
value={selectedDateFormat}
onChange={(e) => setSelectedDateFormat(e.target.value)}
style={{ width: "100%", padding: "10px 12px", borderRadius: "8px", border: `1px solid ${borderColor}`, background: inputBg, color: textColor, fontSize: "0.9rem" }}
>
{DATE_FORMATS.map((fmt) => (
<option key={fmt.value} value={fmt.value}>{fmt.example}</option>
))}
</select>
</div>
<div>
<label style={{ display: "block", fontSize: "0.85rem", fontWeight: 600, color: textColor, marginBottom: "6px" }}>{t.timeFormat}</label>
<div style={{ display: "flex", gap: "6px" }}>
{(["24H", "12H"] as const).map((fmt) => (
<button
key={fmt}
onClick={() => setSelectedTimeFormat(fmt)}
style={{
flex: 1, padding: "10px", borderRadius: "8px",
border: `2px solid ${selectedTimeFormat === fmt ? accentColor : borderColor}`,
background: selectedTimeFormat === fmt ? (darkMode ? "rgba(13,148,136,0.15)" : "rgba(13,148,136,0.06)") : "transparent",
color: textColor, fontSize: "0.9rem", fontWeight: 600, cursor: "pointer", transition: "all 0.15s",
}}
>{fmt}</button>
))}
</div>
</div>
</div>
<div style={{ marginBottom: "16px" }}>
<label style={{ display: "block", fontSize: "0.85rem", fontWeight: 600, color: textColor, marginBottom: "6px" }}>{t.weekStartLabel}</label>
<div style={{ display: "flex", gap: "8px" }}>
{([{ value: 1, label: t.monday }, { value: 0, label: t.sunday }] as const).map((opt) => (
<button
key={opt.value}
onClick={() => setSelectedWeekStart(opt.value)}
style={{
flex: 1, padding: "10px", borderRadius: "8px",
border: `2px solid ${selectedWeekStart === opt.value ? accentColor : borderColor}`,
background: selectedWeekStart === opt.value ? (darkMode ? "rgba(13,148,136,0.15)" : "rgba(13,148,136,0.06)") : "transparent",
color: textColor, fontSize: "0.9rem", fontWeight: 600, cursor: "pointer", transition: "all 0.15s",
}}
>{opt.label}</button>
))}
</div>
</div>
<label style={{ display: "block", fontSize: "0.85rem", fontWeight: 600, color: textColor, marginBottom: "6px" }}>{t.timezone}</label>
<select
value={selectedTimezone}
onChange={(e) => setSelectedTimezone(e.target.value)}
style={{ width: "100%", padding: "10px 12px", borderRadius: "8px", border: `1px solid ${borderColor}`, background: inputBg, color: textColor, fontSize: "0.9rem" }}
>
{COMMON_TIMEZONES.map((tz) => (
<option key={tz} value={tz}>{tz.replace(/_/g, " ")}</option>
))}
</select>
</div>
);
}
if (step === "connect") {
const providers = [
{ key: "google", label: t.connectGoogle, icon: faGoogle, color: "#4285F4", action: () => handleOAuthConnect("/api/calendar/google/start") },
{ key: "apple", label: t.connectApple, icon: faApple, color: darkMode ? "#ccc" : "#333", action: () => handleProviderConnect("apple") },
{ key: "outlook", label: t.connectOutlook, icon: faMicrosoft, color: "#0078D4", action: () => handleOAuthConnect("/api/calendar/outlook/start") },
{ key: "synology", label: t.connectSynology, icon: faServer, color: "#007AFF", action: () => handleProviderConnect("synology") },
{ key: "notion", label: t.connectNotion, icon: faNotion, color: darkMode ? "#fff" : "#000", action: () => handleOAuthConnect("/api/calendar/notion/start") },
];
return (
<div style={{ padding: "30px" }}>
<h2 style={{ fontSize: "1.3rem", fontWeight: 700, margin: "0 0 6px", color: textColor }}>{t.connectTitle}</h2>
<p style={{ color: mutedColor, margin: "0 0 24px", fontSize: "0.9rem" }}>{t.connectSubtitle}</p>
<div style={{ display: "flex", flexDirection: "column", gap: "10px" }}>
{providers.map((p) => {
const isConnected = connectedProviders.has(p.key);
return (
<div key={p.key} style={{
display: "flex", alignItems: "center", gap: "12px",
padding: "14px 16px", borderRadius: "10px",
border: `1px solid ${isConnected ? accentColor : borderColor}`,
background: isConnected ? (darkMode ? "rgba(13,148,136,0.1)" : "rgba(13,148,136,0.04)") : "transparent",
}}>
<FontAwesomeIcon icon={p.icon} style={{ width: 20, height: 20, color: p.color }} />
<span style={{ flex: 1, fontSize: "0.95rem", color: textColor, fontWeight: 500 }}>{p.label}</span>
{isConnected ? (
<span style={{ fontSize: "0.8rem", color: accentColor, fontWeight: 600, display: "flex", alignItems: "center", gap: "4px" }}>
<Check size={14} /> {t.connected}
</span>
) : (
<button onClick={p.action} style={{
padding: "6px 16px", borderRadius: "6px", border: `1px solid ${borderColor}`,
background: "transparent", color: textColor, fontSize: "0.8rem", fontWeight: 500, cursor: "pointer",
}}>{t.connect}</button>
)}
</div>
);
})}
</div>
<p style={{ color: mutedColor, fontSize: "0.8rem", marginTop: "16px", textAlign: "center" }}>{t.skipConnect}</p>
</div>
);
}
// ─── HEADER step ───
if (step === "header") {
return (
<div className="onboarding-wide-step" style={{ display: "flex", gap: "20px", padding: "20px 24px", minHeight: "460px" }}>
<div className="onboarding-settings-panel" style={{ width: "320px", flexShrink: 0, overflowY: "auto", maxHeight: "520px", display: "flex", flexDirection: "column", gap: "10px", paddingRight: "4px" }}>
<div>
<h2 style={{ fontSize: "1.15rem", fontWeight: 700, margin: "0 0 2px", color: textColor }}>{t.headerTitle}</h2>
<p style={{ color: mutedColor, margin: 0, fontSize: "0.75rem", lineHeight: 1.3 }}>{t.headerSubtitle}</p>
</div>
{/* Weekday Font */}
<div style={{ background: itemBg, padding: "8px 10px", borderRadius: "8px" }}>
<label style={labelStyle}>{t.weekdayFont}</label>
<div style={{ display: "flex", gap: "5px", alignItems: "center", marginBottom: "5px" }}>
<input type="color" value={selectedWeekdayColor} onChange={(e) => setSelectedWeekdayColor(e.target.value)} style={colorPickerStyle} />
<div style={{ flex: 1 }}>
<FontPicker value={selectedHeadlineFont} onChange={setSelectedHeadlineFont} darkMode={darkMode} />
</div>
</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "5px" }}>
<input type="text" value={selectedHeadlineFontSize} onChange={(e) => setSelectedHeadlineFontSize(e.target.value)} placeholder="1.25rem" style={smallInputStyle} />
<select value={selectedHeadlineFontWeight} onChange={(e) => setSelectedHeadlineFontWeight(e.target.value)} style={selectStyle}>
{WEIGHT_OPTS.map((w) => <option key={w.value} value={w.value}>{w.label}</option>)}
</select>
</div>
</div>
{/* Weekday Format */}
<div style={{ background: itemBg, padding: "8px 10px", borderRadius: "8px" }}>
<label style={labelStyle}>{t.weekdayFormat}</label>
<div style={{ display: "flex", gap: "4px" }}>
{([
{ value: "full", label: t.weekdayFull },
{ value: "short", label: t.weekdayShort },
{ value: "narrow", label: t.weekdayNarrow },
] as const).map((opt) => (
<button key={opt.value} onClick={() => setSelectedWeekdayFormat(opt.value)}
style={{
flex: 1, padding: "5px 4px", borderRadius: "6px", fontSize: "0.72rem", fontWeight: 600,
border: `2px solid ${selectedWeekdayFormat === opt.value ? accentColor : borderColor}`,
background: selectedWeekdayFormat === opt.value ? (darkMode ? "rgba(13,148,136,0.15)" : "rgba(13,148,136,0.06)") : "transparent",
color: textColor, cursor: "pointer", transition: "all 0.15s",
}}
>{opt.label}</button>
))}
</div>
</div>
{/* Date */}
<div style={{ background: itemBg, padding: "8px 10px", borderRadius: "8px" }}>
<label style={labelStyle}>{t.dateSection}</label>
<div style={{ display: "flex", gap: "5px", alignItems: "center", marginBottom: "5px" }}>
<input type="color" value={selectedDateColor} onChange={(e) => setSelectedDateColor(e.target.value)} style={colorPickerStyle} />
<input type="text" value={selectedDateFontSize} onChange={(e) => setSelectedDateFontSize(e.target.value)} placeholder="0.65rem" style={{ ...smallInputStyle, flex: 1 }} />
<select value={selectedDateFontWeight} onChange={(e) => setSelectedDateFontWeight(e.target.value)} style={{ ...selectStyle, flex: 1 }}>
{WEIGHT_OPTS.slice(0, 5).map((w) => <option key={w.value} value={w.value}>{w.label}</option>)}
</select>
</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "5px" }}>
<div>
<label style={{ ...labelStyle, fontSize: "0.7rem" }}>{t.datePosition}</label>
<select value={selectedDateLayout} onChange={(e) => setSelectedDateLayout(e.target.value)} style={selectStyle}>
<option value="right">{t.datePositionRight}</option>
<option value="left">{t.datePositionLeft}</option>
<option value="above">{t.datePositionAbove}</option>
<option value="below">{t.datePositionBelow}</option>
<option value="hidden">{t.datePositionHidden}</option>
</select>
</div>
<div>
<label style={{ ...labelStyle, fontSize: "0.7rem" }}>{t.dateAlignment}</label>
<select value={selectedDateAlignment} onChange={(e) => setSelectedDateAlignment(e.target.value)} style={selectStyle}>
<option value="left">{t.alignLeft}</option>
<option value="center">{t.alignCenter}</option>
<option value="right">{t.alignRight}</option>
<option value="tight">{t.alignTight}</option>
</select>
</div>
</div>
</div>
</div>
{/* Preview */}
<div className="onboarding-preview-panel" style={{ flex: 1, minWidth: 0 }}>{renderPreview()}</div>
</div>
);
}
// ─── TASKS step ───
if (step === "tasks") {
return (
<div className="onboarding-wide-step" style={{ display: "flex", gap: "20px", padding: "20px 24px", minHeight: "460px" }}>
<div className="onboarding-settings-panel" style={{ width: "320px", flexShrink: 0, overflowY: "auto", maxHeight: "520px", display: "flex", flexDirection: "column", gap: "10px", paddingRight: "4px" }}>
<div>
<h2 style={{ fontSize: "1.15rem", fontWeight: 700, margin: "0 0 2px", color: textColor }}>{t.tasksTitle}</h2>
<p style={{ color: mutedColor, margin: 0, fontSize: "0.75rem", lineHeight: 1.3 }}>{t.tasksSubtitle}</p>
</div>
{/* Task Font */}
<div style={{ background: itemBg, padding: "8px 10px", borderRadius: "8px" }}>
<label style={labelStyle}>{t.taskFont}</label>
<div style={{ display: "flex", gap: "5px", alignItems: "center", marginBottom: "5px" }}>
<input type="color" value={selectedTaskColor} onChange={(e) => setSelectedTaskColor(e.target.value)} style={colorPickerStyle} />
<div style={{ flex: 1 }}>
<FontPicker value={selectedBodyFont} onChange={setSelectedBodyFont} darkMode={darkMode} />
</div>
</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "5px" }}>
<input type="text" value={selectedTaskFontSize} onChange={(e) => setSelectedTaskFontSize(e.target.value)} placeholder="0.9rem" style={smallInputStyle} />
<select value={selectedTaskFontWeight} onChange={(e) => setSelectedTaskFontWeight(e.target.value)} style={selectStyle}>
{WEIGHT_OPTS.map((w) => <option key={w.value} value={w.value}>{w.label}</option>)}
</select>
</div>
</div>
{/* Event Font */}
<div style={{ background: itemBg, padding: "8px 10px", borderRadius: "8px" }}>
<label style={labelStyle}>{t.eventFont}</label>
<div style={{ display: "flex", gap: "5px", alignItems: "center", marginBottom: "5px" }}>
<div style={{ flex: 1 }}>
<FontPicker value={selectedEventFont} onChange={setSelectedEventFont} darkMode={darkMode} />
</div>
</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "5px" }}>
<input type="text" value={selectedEventFontSize} onChange={(e) => setSelectedEventFontSize(e.target.value)} placeholder="0.85rem" style={smallInputStyle} />
<select value={selectedEventFontWeight} onChange={(e) => setSelectedEventFontWeight(e.target.value)} style={selectStyle}>
{WEIGHT_OPTS.slice(0, 5).map((w) => <option key={w.value} value={w.value}>{w.label}</option>)}
</select>
</div>
</div>
{/* Font size S/M/L */}
<div style={{ background: itemBg, padding: "8px 10px", borderRadius: "8px" }}>
<label style={labelStyle}>{t.fontSize}</label>
<div style={{ display: "flex", gap: "4px" }}>
{(["S", "M", "L"] as const).map((size) => (
<button key={size} onClick={() => setSelectedFontSize(size)}
style={{
flex: 1, padding: "5px", borderRadius: "6px",
border: `2px solid ${selectedFontSize === size ? accentColor : borderColor}`,
background: selectedFontSize === size ? (darkMode ? "rgba(13,148,136,0.15)" : "rgba(13,148,136,0.06)") : "transparent",
color: textColor, fontSize: "0.8rem", fontWeight: 600, cursor: "pointer", transition: "all 0.15s",
}}
>{size}</button>
))}
</div>
</div>
</div>
{/* Preview */}
<div className="onboarding-preview-panel" style={{ flex: 1, minWidth: 0 }}>{renderPreview()}</div>
</div>
);
}
// ─── DISPLAY step ───
if (step === "display") {
const toggles = [
{ label: t.showAllDay, value: selectedShowAllDay, setter: setSelectedShowAllDay },
{ label: t.showSomeday, value: selectedShowSomeday, setter: setSelectedShowSomeday },
{ label: t.showTimeGrid, value: selectedShowTimeGrid, setter: setSelectedShowTimeGrid },
{ label: t.showCheckboxes, value: selectedShowCheckboxes, setter: setSelectedShowCheckboxes },
{ label: t.darkMode, value: darkMode, setter: () => {}, isDarkMode: true },
];
return (
<div className="onboarding-wide-step" style={{ display: "flex", gap: "20px", padding: "20px 24px", minHeight: "460px" }}>
<div className="onboarding-settings-panel" style={{ width: "320px", flexShrink: 0, overflowY: "auto", maxHeight: "520px", display: "flex", flexDirection: "column", gap: "8px", paddingRight: "4px" }}>
<div>
<h2 style={{ fontSize: "1.15rem", fontWeight: 700, margin: "0 0 2px", color: textColor }}>{t.displayTitle}</h2>
<p style={{ color: mutedColor, margin: "0 0 4px", fontSize: "0.75rem", lineHeight: 1.3 }}>{t.displaySubtitle}</p>
</div>
{toggles.map((toggle) => (
<div key={toggle.label} style={{
display: "flex", alignItems: "center", justifyContent: "space-between",
padding: "8px 10px", borderRadius: "8px", border: `1px solid ${borderColor}`,
background: itemBg,
}}>
<span style={{ fontSize: "0.85rem", fontWeight: 500, color: textColor }}>{toggle.label}</span>
<button
onClick={() => {
if (toggle.isDarkMode) onDarkModeToggle();
else toggle.setter(!toggle.value);
}}
style={{
width: "38px", height: "22px", borderRadius: "11px", border: "none",
background: toggle.value ? accentColor : "#d1d5db", cursor: "pointer",
position: "relative", transition: "background 0.2s", flexShrink: 0,
}}
>
<div style={{
width: "16px", height: "16px", borderRadius: "50%",
background: "#fff", position: "absolute", top: "3px",
left: toggle.value ? "19px" : "3px", transition: "left 0.2s",
display: "flex", alignItems: "center", justifyContent: "center",
}}>
{toggle.isDarkMode && (toggle.value ? <Moon size={9} color="#333" /> : <Sun size={9} color="#333" />)}
</div>
</button>
</div>
))}
</div>
{/* Preview */}
<div className="onboarding-preview-panel" style={{ flex: 1, minWidth: 0 }}>{renderPreview()}</div>
</div>
);
}
return null;
};
return (
<div className="onboarding-overlay">
<div
className="onboarding-card"
style={{
background: bg, color: textColor,
maxWidth: isWideStep ? "1060px" : "560px",
transition: "max-width 0.3s ease",
}}
>
{/* Close button */}
{currentStep > 0 && (
<button
onClick={onSkip}
style={{
position: "absolute", top: "12px", right: "12px", background: "none", border: "none",
color: mutedColor, cursor: "pointer", padding: "4px", zIndex: 10,
}}
title="Close"
>
<X size={18} />
</button>
)}
{/* Step content with animation */}
<div key={animKey} className={direction === "forward" ? "onboarding-step-forward" : "onboarding-step-backward"}>
{renderStep()}
</div>
{/* Footer with progress + nav (not on welcome) */}
{currentStep > 0 && (
<div style={{
display: "flex", alignItems: "center", justifyContent: "space-between",
padding: "16px 30px 24px", borderTop: `1px solid ${borderColor}`,
}}>
<button onClick={goBack} style={secondaryBtn}>
<ChevronLeft size={16} /> {t.back}
</button>
{/* Progress dots */}
<div style={{ display: "flex", gap: "6px" }}>
{STEPS.map((_, i) => (
<div
key={i}
style={{
width: i === currentStep ? "20px" : "8px", height: "8px",
borderRadius: "4px", transition: "all 0.3s",
background: i === currentStep ? accentColor : i < currentStep ? accentColor : (darkMode ? "#4b5563" : "#d1d5db"),
opacity: i < currentStep ? 0.5 : 1,
}}
/>
))}
</div>
{isLastStep ? (
<button onClick={handleFinish} style={primaryBtn}>
{t.finish}
</button>
) : (
<button onClick={goNext} style={primaryBtn}>
{t.next} <ChevronRight size={16} />
</button>
)}
</div>
)}
</div>
</div>
);
}