- 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>
1150 lines
64 KiB
TypeScript
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" }}>✓</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>
|
|
);
|
|
}
|