"use client"; import React, { useState, useEffect, useRef, useMemo, useCallback } from "react"; import { signOut } from "next-auth/react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faApple, faGoogle, faMicrosoft, faNotion } from "@fortawesome/free-brands-svg-icons"; import { faServer, faFolder } from "@fortawesome/free-solid-svg-icons"; import CalendarSyncRulesPanel from "./CalendarSyncRulesPanel"; import { ViewStyle, KanbanStage, Task } from "./WeeklyView"; import { AVAILABLE_FONTS, isCustomFont } from "../lib/fontConstants"; import { translations } from "../lib/weeklyViewTranslations"; import { WeatherDisplayKey, WEATHER_DISPLAY_DEFAULTS } from "../lib/weeklyViewConstants"; import { EXPORT_FIELDS, type ExportFieldKey } from "../app/api/user/export/route"; import { ArrowLeftRight, Briefcase, Calendar, CalendarDays, Check, FolderOpen, Globe, Info, Kanban, Link, ListTodo, Palette, Pencil, Play, Plus, Settings, Sparkles, Trash2, User, } from "lucide-react"; import IconPicker from "./IconPicker"; import { allIcons } from "./iconRegistry"; import Icon from "@mdi/react"; // Minimal ProjectIcon — resolves an icon name from the unified registry. function ProjectIcon({ icon, size = 18, color }: { icon?: string | null; size?: number; color?: string }) { if (!icon) return ; const normalised = icon.startsWith("fa") && icon.length > 2 && icon[2] === icon[2].toUpperCase() ? icon.slice(2, 3).toLowerCase() + icon.slice(3) : icon; const found = allIcons.find((i) => i.name === normalised || i.name === icon); if (found) { if (found.type === "fa") { return ; } return ; } return {icon}; } export interface SomedayList { id: string; title: string; tab?: string | null; tasks: Task[]; externalId?: string | null; externalProvider?: string | null; externalListId?: string | null; } export type CellDuration = 15 | 20 | 30 | 60; // Settings Modal Component interface SettingsSidebarProps { onClose: () => void; onSettingsChanged?: (newSettings: { showTimeGrid: boolean; cellDuration: CellDuration; viewStyle: ViewStyle; language: string; dateFormat: string; timeFormat: string; startHour: number; endHour: number; fontSize: "S" | "M" | "L"; showNextTask: boolean; showSomeday: boolean; showAllDayEvents: boolean; showSchedule: boolean; headlineFont: string; headlineFontSize: string; headlineFontWeight: string; goalFontWeight: string; dateFontFamily: string; dateFontSize: string; dateFontWeight: string; timeTaskFontFamily: string; timeTaskFontSize: string; timeTaskFontWeight: string; bodyFont: string; taskFontFamily: string; taskFontSize: string; taskFontWeight: string; fontWeight: string; weekendColorSat: string; weekendColorSun: string; weekdayColor?: string; dateColor?: string; taskColor?: string; todayHighlightColor?: string; dateLayout?: "above" | "below" | "left" | "right" | "hidden"; mobileDateLayout?: "above" | "below" | "left" | "right" | "hidden"; dateAlignment?: "left" | "center" | "right" | "tight"; hourLabelFormat?: "short" | "full"; showSubHourSlots?: boolean; allDayPosition?: "above" | "below"; cwFontFamily?: string; cwFontSize?: string; cwFontWeight?: string; cwColor?: string; yearFontFamily?: string; yearFontSize?: string; yearFontWeight?: string; yearColor?: string; dayHeaderGap?: string; showTaskCheckboxes?: boolean; startDayOffset?: number; quoteSourceUrls: string[]; quoteLanguages: string[]; }) => void; profile: any; setProfile: React.Dispatch>; setCurrentWeekStart: (d: Date) => void; quoteSourceUrls?: string[]; quoteLanguages?: string[]; goal: string; setGoal: (goal: string) => void; saveGoal: (goal: string) => void; connections: any[]; onUpdateConnections: (connections: any[]) => void; onRemoveConnection: (id: string) => void | Promise; focusTimerDuration: number; setFocusTimerDuration: (duration: number) => void; focusBreakDuration: number; setFocusBreakDuration: (duration: number) => void; showNextTask: boolean; setShowNextTask: (show: boolean) => void; protectEventTimes: boolean; setProtectEventTimes: (protect: boolean) => void; goalDefaultSentence?: string; goalFallbackType?: string; importingTasksState: boolean; executeImport: (provider: "google" | "apple" | "outlook") => Promise; onImportLists: (lists: { id: string; title: string }[]) => Promise; importStatusMsg: { type: "success" | "error"; text: string } | null; showTimeGrid: boolean; setShowTimeGrid: (show: boolean) => void; cellDuration: CellDuration; setCellDuration: (duration: CellDuration) => void; weekStartDay: number; setWeekStartDay: (day: number) => void; fontSize: "S" | "M" | "L"; setFontSize: (size: "S" | "M" | "L") => void; headlineFont: string; headlineFontSize: string; headlineFontWeight: string; goalFontWeight: string; dateFontFamily: string; dateFontSize: string; dateFontWeight: string; timeTaskFontFamily: string; timeTaskFontSize: string; timeTaskFontWeight: string; bodyFont: string; taskFontFamily: string; taskFontSize: string; taskFontWeight: string; fontWeight: string; weekendColorSat: string; weekendColorSun: string; viewStyle: ViewStyle; setViewStyle: (style: ViewStyle) => void; showSomeday: boolean; setShowSomeday: (show: boolean) => void; showAllDay: boolean; setShowAllDay: (show: boolean) => void; showSchedule: boolean; setShowSchedule: (show: boolean) => void; dateLayout?: "above" | "below" | "left" | "right" | "hidden"; mobileDateLayout?: "above" | "below" | "left" | "right" | "hidden"; weekdayFormat?: "long" | "short" | "narrow" | "custom"; weekdayCase?: "normal" | "capitalize" | "uppercase"; customWeekdayNames?: string; dateAlignment?: "left" | "center" | "right" | "tight"; hourLabelFormat: "short" | "full"; setHourLabelFormat: (fmt: "short" | "full") => void; showSubHourSlots: boolean; setShowSubHourSlots: (show: boolean) => void; allDayPosition: "above" | "below"; setAllDayPosition: (pos: "above" | "below") => void; saveSetting: (key: string, value: any) => void; availableTaskLists: { [key in "google" | "apple" | "outlook" | "synology"]?: { id: string; title: string }[]; }; isFetchingProviderLists: Record; somedayLists: SomedayList[]; handleToggleTaskList: ( provider: "google" | "apple" | "outlook" | "synology", list: { id: string; title: string }, ) => Promise; unsyncConfirm: { provider: "google" | "apple" | "outlook" | "synology"; list: { id: string; title: string }; } | null; onConfirmUnsync: () => Promise; onCancelUnsync: () => void; handleSyncAll: ( provider: "google" | "outlook" | "synology", lists: { id: string; title: string }[], syncOn: boolean, ) => Promise; fetchAvailableTaskLists: ( provider: "google" | "apple" | "outlook" | "synology", ) => Promise; initialTab?: "calendar" | "general" | "account" | "styling" | "motivation" | "about" | "sync" | "projects"; projects: { id: string; name: string; icon?: string | null; color?: string | null }[]; onProjectsChanged: () => void; kanbanStages: KanbanStage[]; saveKanbanStages: (stages: KanbanStage[]) => Promise; // Quick actions isMobile?: boolean; mobileActions?: { goToPrevWeek: () => void; goToPrevDay: () => void; goToToday: () => void; goToNextDay: () => void; goToNextWeek: () => void; onJumpToDate: () => void; onAddCalendarEvent: () => void; onAddProject: () => void; onRecurringTasks: () => void; onToggleNextTask: () => void; onFocusMode: () => void; onToggleDarkMode: () => void; onSearch: () => void; onUndo: () => void; onRedo: () => void; onRefresh: () => void; darkMode: boolean; showNextTask: boolean; undoCount: number; redoCount: number; viewDays: number; onViewDaysChange: (days: number) => void; showTimeGrid: boolean; cellDuration: CellDuration; onCellDurationChange: (d: CellDuration) => void; viewStyle: string; onViewStyleChange: (style: string) => void; startHour: number; endHour: number; onStartHourChange: (h: number) => void; onEndHourChange: (h: number) => void; }; perView: { saveViewSetting: (key: string, value: any, perView: boolean) => void; getEffective: (key: string, globalVal: any) => any; }; onRunSetupAssistant?: () => void; } function SettingsSidebar({ onClose, onSettingsChanged, viewStyle, setViewStyle, showSomeday, setShowSomeday, showAllDay, setShowAllDay, showSchedule, setShowSchedule, goal, setGoal, saveGoal, connections, onUpdateConnections, onRemoveConnection, focusTimerDuration, setFocusTimerDuration, focusBreakDuration, setFocusBreakDuration, showNextTask, setShowNextTask, protectEventTimes, setProtectEventTimes, goalFallbackType, goalDefaultSentence, importingTasksState, executeImport, onImportLists, importStatusMsg, showTimeGrid, setShowTimeGrid, cellDuration, setCellDuration, weekStartDay, setWeekStartDay, fontSize, setFontSize, headlineFont, headlineFontSize, headlineFontWeight, goalFontWeight, dateFontFamily, dateFontSize, dateFontWeight, timeTaskFontFamily, timeTaskFontSize, timeTaskFontWeight, bodyFont, taskFontFamily, taskFontSize, taskFontWeight, fontWeight, weekendColorSat, weekendColorSun, hourLabelFormat, setHourLabelFormat, showSubHourSlots, setShowSubHourSlots, allDayPosition, setAllDayPosition, saveSetting, availableTaskLists, isFetchingProviderLists, somedayLists, handleToggleTaskList, unsyncConfirm, onConfirmUnsync, onCancelUnsync, handleSyncAll, fetchAvailableTaskLists, initialTab, setCurrentWeekStart, projects, onProjectsChanged, kanbanStages, saveKanbanStages, profile, setProfile, isMobile: isMobileSidebar, mobileActions, perView, onRunSetupAssistant, }: SettingsSidebarProps) { const [activeTab, setActiveTab] = useState< "calendar" | "general" | "account" | "styling" | "motivation" | "about" | "localisation" | "sync" | "projects" >(initialTab || "general"); const [isLoading, setIsLoading] = useState(true); const [isSyncing, setIsSyncing] = useState(false); const [exportStartDate, setExportStartDate] = useState(""); const [exportEndDate, setExportEndDate] = useState(""); const [exportFields, setExportFields] = useState>( () => new Set(EXPORT_FIELDS.filter(f => f.defaultOn).map(f => f.key)) ); const [importMode, setImportMode] = useState<"merge" | "replace">("merge"); const [importFile, setImportFile] = useState(null); const [importMsg, setImportMsg] = useState(""); const [isImporting, setIsImporting] = useState(false); const [isExportingAll, setIsExportingAll] = useState(false); const [passwords, setPasswords] = useState({ new: "", confirm: "" }); const [accountMsg, setAccountMsg] = useState(""); const [isVisible, setIsVisible] = useState(false); // Apple Calendar (CalDAV) State const [showAppleCalendarModal, setShowAppleCalendarModal] = useState(false); const [appleCalEmail, setAppleCalEmail] = useState(""); const [appleCalPassword, setAppleCalPassword] = useState(""); const [isConnectingAppleCal, setIsConnectingAppleCal] = useState(false); const [appleCalError, setAppleCalError] = useState(""); // Synology Calendar State const [showSynologyCalendarModal, setShowSynologyCalendarModal] = useState(false); const [synologyCalServerUrl, setSynologyCalServerUrl] = useState(""); const [synologyCalUsername, setSynologyCalUsername] = useState(""); const [synologyCalPassword, setSynologyCalPassword] = useState(""); const [isConnectingSynologyCal, setIsConnectingSynologyCal] = useState(false); const [synologyCalError, setSynologyCalError] = useState(""); const [disconnectingId, setDisconnectingId] = useState(null); const [confirmDisconnectId, setConfirmDisconnectId] = useState( null, ); const [newProjectName, setNewProjectName] = useState(""); const [newProjectColor, setNewProjectColor] = useState("#3b82f6"); const [newProjectIcon, setNewProjectIcon] = useState("📁"); const [showNewProjectIconPicker, setShowNewProjectIconPicker] = useState(false); const [editingProjectId, setEditingProjectId] = useState(null); const [editProjectName, setEditProjectName] = useState(""); const [editProjectColor, setEditProjectColor] = useState(""); const [editProjectIcon, setEditProjectIcon] = useState(""); const [showEditProjectIconPicker, setShowEditProjectIconPicker] = useState(false); const [weatherSearchResults, setWeatherSearchResults] = useState([]); // Fetch lists when the calendar tab is selected useEffect(() => { if (activeTab === "calendar") { const providersWithAccounts = connections.map((c) => c.provider); if (providersWithAccounts.includes("google")) fetchAvailableTaskLists("google"); if (providersWithAccounts.includes("outlook")) fetchAvailableTaskLists("outlook"); if (providersWithAccounts.includes("synology")) fetchAvailableTaskLists("synology"); } }, [activeTab, connections, fetchAvailableTaskLists]); const [connMsg, setConnMsg] = useState<{ type: "success" | "error"; text: string; } | null>(null); const showConnMsg = (type: "success" | "error", text: string) => { setConnMsg({ type, text }); setTimeout(() => setConnMsg(null), 5000); }; // profile state removed (centralized in parent) const t = translations[profile.language || "en"] || translations["en"]; // --- Auto-save helpers: save settings immediately on change --- // For discrete inputs (checkbox, select, button) — save right away const saveField = (key: string, value: any) => { setProfile((p: any) => ({ ...p, [key]: value })); saveSetting(key, value); }; // For continuous inputs (text, number, color picker) — debounce 500ms const debouncedTimers = useRef>({}); const saveFieldDebounced = (key: string, value: any) => { setProfile((p: any) => ({ ...p, [key]: value })); if (debouncedTimers.current[key]) clearTimeout(debouncedTimers.current[key]); debouncedTimers.current[key] = setTimeout(() => saveSetting(key, value), 500); }; // For standalone state + saveSetting (showSomeday, showTimeGrid, etc.) const saveStateAndSetting = (setter: (v: any) => void, key: string, value: any) => { setter(value); saveSetting(key, value); }; // Load fonts for preview // Font loading moved to top level WeeklyView component useEffect(() => { setIsLoading(false); // Trigger slide-in after mount const timer = setTimeout(() => setIsVisible(true), 10); return () => clearTimeout(timer); }, []); const handleClose = () => { setIsVisible(false); setTimeout(onClose, 300); }; const handleUpdateConnections = async (updatedConnections: any[]) => { onUpdateConnections(updatedConnections); }; const handleRemoveConnection = async (connectionId: string) => { await onRemoveConnection(connectionId); }; const handleGoogleConnect = () => { window.location.href = "/api/calendar/google/start"; }; // --- Apple Calendar (CalDAV) handlers --- const handleAppleCalendarConnect = () => { setShowAppleCalendarModal(true); setAppleCalError(""); setAppleCalEmail(""); setAppleCalPassword(""); }; const submitAppleCalendarConnection = async () => { if (!appleCalEmail || !appleCalPassword) { setAppleCalError("Please enter both email and app-specific password."); return; } setIsConnectingAppleCal(true); setAppleCalError(""); try { const response = await fetch("/api/calendar/apple/connect", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: appleCalEmail, password: appleCalPassword, }), }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || "Failed to connect Apple Calendar"); } setShowAppleCalendarModal(false); showConnMsg("success", "Apple Calendar connected successfully!"); setTimeout(() => { window.location.href = window.location.pathname + "?calendar=apple_connected&openSettings=calendars"; }, 1200); } catch (err: any) { setAppleCalError(err.message || "Connection failed"); } finally { setIsConnectingAppleCal(false); } }; // --- Synology Calendar handlers --- const handleSynologyCalendarConnect = () => { setShowSynologyCalendarModal(true); setSynologyCalError(""); setSynologyCalServerUrl(""); setSynologyCalUsername(""); setSynologyCalPassword(""); }; const submitSynologyCalendarConnection = async () => { if (!synologyCalServerUrl || !synologyCalUsername || !synologyCalPassword) { setSynologyCalError("Please enter Server URL, username, and password."); return; } setIsConnectingSynologyCal(true); setSynologyCalError(""); try { const response = await fetch("/api/calendar/synology/connect", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ serverUrl: synologyCalServerUrl, username: synologyCalUsername, password: synologyCalPassword, }), }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || "Failed to connect Synology Calendar"); } setShowSynologyCalendarModal(false); showConnMsg("success", "Synology Calendar connected successfully!"); setTimeout(() => { window.location.href = window.location.pathname + "?calendar=synology_connected&openSettings=calendars"; }, 1200); } catch (err: any) { setSynologyCalError(err.message || "Connection failed"); } finally { setIsConnectingSynologyCal(false); } }; const handleOutlookConnect = () => { window.location.href = "/api/calendar/outlook/start"; }; const handleNotionConnect = () => { window.location.href = "/api/calendar/notion/start"; }; const handleUpdateCalendar = async ( connectionId: string, calendarId: string, updates: { selected?: boolean; editable?: boolean }, ) => { // Optimistic Update const updatedConnections = connections.map((conn) => { if (conn.id === connectionId && conn.calendars) { return { ...conn, calendars: conn.calendars.map((c: any) => c.id === calendarId ? { ...c, ...updates } : c, ), }; } return conn; }); onUpdateConnections(updatedConnections); // used props instead of setConnections // API Call try { const conn = updatedConnections.find((c) => c.id === connectionId); if (conn) { await fetch("/api/calendar/connections", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id: connectionId, calendars: conn.calendars, }), }); } } catch (error) { console.error("Failed to update calendar selection", error); // Revert on error - tough to do without refetching from parent or keeping prev state } }; const handleUpdateProfile = async (e: React.FormEvent) => { e.preventDefault(); // Only validate password if in Account tab and password field is filled if ( activeTab === "account" && passwords.new && passwords.new !== passwords.confirm ) { setAccountMsg("Passwords do not match"); return; } try { const res = await fetch("/api/user/profile", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ...profile, dateAlignment: profile.dateAlignment, showTimeGrid: showTimeGrid, cellDuration: cellDuration, viewStyle: viewStyle, showNextTask: showNextTask, showSomeday: showSomeday, showAllDayEvents: showAllDay, showSchedule: showSchedule, // The following will be taken from profile if present, // ensuring edited state is saved. // Validate numeric fields to avoid NaN focusBreakDuration: !isNaN(Number(profile.focusBreakDuration)) ? Number(profile.focusBreakDuration) : focusBreakDuration || 5, focusTimerDuration: !isNaN(Number(profile.focusTimerDuration)) ? Number(profile.focusTimerDuration) : focusTimerDuration || 25, password: passwords.new && passwords.new.trim() !== "" ? passwords.new : undefined, }), }); const data = await res.json(); if (res.ok) { setAccountMsg("Profile updated successfully!"); // Update local app state if (onSettingsChanged) { onSettingsChanged({ showTimeGrid: showTimeGrid, cellDuration: cellDuration, viewStyle: viewStyle, language: profile.language || "de", dateFormat: profile.dateFormat || "MM/dd/yyyy", timeFormat: profile.timeFormat || "12h", startHour: profile.startHour || 8, endHour: profile.endHour || 18, fontSize: fontSize, showNextTask: showNextTask, showSomeday: showSomeday, showAllDayEvents: showAllDay, showSchedule: showSchedule, headlineFont: headlineFont, headlineFontSize: headlineFontSize, headlineFontWeight: headlineFontWeight, dateFontFamily: dateFontFamily, dateFontSize: dateFontSize, dateFontWeight: dateFontWeight, timeTaskFontFamily: timeTaskFontFamily, timeTaskFontSize: timeTaskFontSize, timeTaskFontWeight: timeTaskFontWeight, bodyFont: bodyFont, taskFontFamily: taskFontFamily, taskFontSize: taskFontSize, taskFontWeight: taskFontWeight, fontWeight: fontWeight, weekendColorSat: weekendColorSat, weekendColorSun: weekendColorSun, weekdayColor: profile.weekdayColor, dateColor: profile.dateColor, taskColor: profile.taskColor, todayHighlightColor: profile.todayHighlightColor, autoRolling: profile.autoRolling, protectEventTimes: profile.protectEventTimes || protectEventTimes, focusTimerDuration: profile.focusTimerDuration || focusTimerDuration, focusBreakDuration: profile.focusBreakDuration || focusBreakDuration, pastDayColor: profile.pastDayColor, goalScope: profile.goalScope, dateLayout: profile.dateLayout, mobileDateLayout: profile.mobileDateLayout, dateAlignment: profile.dateAlignment, startDayOffset: profile.startDayOffset, } as any); } if (profile.focusTimerDuration && setFocusTimerDuration) { setFocusTimerDuration(profile.focusTimerDuration); } if (profile.focusBreakDuration && setFocusBreakDuration) { setFocusBreakDuration(profile.focusBreakDuration); } // Temporary success message setTimeout(() => setAccountMsg(""), 3000); } else { console.error("Failed to update profile:", data); setAccountMsg( data.details ? `${data.error}: ${data.details}` : data.error || "Failed to update profile", ); } } catch (e) { console.error("Error updating profile:", e); setAccountMsg("Error updating profile"); } }; const handleDownloadData = () => { window.open("/api/user/export", "_blank"); }; const handleExportAllData = async () => { setIsExportingAll(true); try { const res = await fetch("/api/user/export-data"); if (!res.ok) throw new Error("Export failed"); const blob = await res.blob(); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `weekly_todo_backup_${new Date().toISOString().split("T")[0]}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } catch (e) { console.error("Export error:", e); } finally { setIsExportingAll(false); } }; const handleImportData = async () => { if (!importFile) return; if (importMode === "replace") { const confirmed = confirm(t.importConfirmReplace); if (!confirmed) return; } setIsImporting(true); setImportMsg(""); try { const text = await importFile.text(); JSON.parse(text); // validate JSON const res = await fetch(`/api/user/import-data?mode=${importMode}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: text, }); const data = await res.json(); if (!res.ok) { setImportMsg(`❌ ${data.error || "Import failed"}`); return; } const { imported } = data; const parts: string[] = []; if (imported.tasks > 0) parts.push(`${imported.tasks} ${profile.language === "de" ? "Aufgaben" : "tasks"}`); if (imported.somedayLists > 0) parts.push(`${imported.somedayLists} ${profile.language === "de" ? "Listen" : "lists"}`); if (imported.projects > 0) parts.push(`${imported.projects} ${profile.language === "de" ? "Projekte" : "projects"}`); setImportMsg(`✓ ${profile.language === "de" ? "Importiert" : "Imported"}: ${parts.join(", ")}`); setImportFile(null); // Reset file input const fileInput = document.getElementById("import-file-input") as HTMLInputElement; if (fileInput) fileInput.value = ""; // Reload to reflect imported data setTimeout(() => window.location.reload(), 1500); } catch (e) { setImportMsg(`❌ ${profile.language === "de" ? "Ungültige JSON-Datei" : "Invalid JSON file"}`); } finally { setIsImporting(false); } }; const handleDeleteAccount = async () => { if ( !confirm( "Are you sure you want to delete your account? This action cannot be undone.", ) ) return; try { const res = await fetch("/api/user/profile", { method: "DELETE" }); if (res.ok) { window.location.href = "/"; } else { alert("Failed to delete account"); } } catch (e) { alert("Error deleting account"); } }; return ( <>

{t.settings}

{([ { key: "general", icon: , label: t.general }, { key: "localisation", icon: , label: t.localisation }, { key: "calendar", icon: , label: t.calendar }, { key: "sync", icon: , label: t.calendarSync || "Sync" }, { key: "projects", icon: , label: t.projects || "Projects" }, { key: "account", icon: , label: t.account }, { key: "styling", icon: , label: t.styling }, { key: "motivation", icon: , label: t.motivation }, { key: "about", icon: , label: t.about }, ] as const).map((tab) => ( ))}
{activeTab === "general" ? (
{/* ── General settings (not view-specific) ── */}
{/* Header Display */}
{profile.headerDisplay === "current_day" && (
{ const val = e.target.value; setProfile({ ...profile, headerCurrentDayFormat: val }); saveSetting("headerCurrentDayFormat", val); }} placeholder="DDD, DD. MMMM YYYY" className="w-full px-3 py-1.5 text-sm rounded border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white" />
Tokens: DDDD (Montag), DDD (Mo.), DD (30), MMMM (März), MMM (Mär), MM (03), YYYY (2026)
)} {profile.headerDisplay === "custom" && (
{ const val = e.target.value; setProfile({ ...profile, headerCustomFormat: val }); saveSetting("headerCustomFormat", val); }} placeholder="KW WW | YYYY" className="w-full px-3 py-1.5 text-sm rounded border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white" />
Tokens: WW (KW), YYYY (Jahr), MMMM (März), MM (03), DD (30), [TODAY] (Heute)
)} {/* Mobile Portrait/Landscape overrides */}
{/* Push Notifications */}
{ const enabled = e.target.checked; if (enabled) { try { const { isNotificationSupported, requestNotificationPermission, registerServiceWorker, subscribeToPush, sendSubscriptionToServer } = await import('@/lib/push-notifications'); if (!isNotificationSupported()) { alert('Push notifications are not supported in this browser.'); return; } const permission = await requestNotificationPermission(); if (permission !== 'granted') { alert('Notification permission was denied.'); return; } const registration = await registerServiceWorker(); if (!registration) { alert('Failed to register service worker.'); return; } const subscription = await subscribeToPush(registration); if (!subscription) { alert('Failed to subscribe to push notifications.'); return; } const sent = await sendSubscriptionToServer(subscription); if (!sent) { alert('Failed to save subscription.'); return; } saveField("notificationsEnabled", true); } catch (err) { console.error('Push notification setup failed:', err); alert('Failed to enable notifications.'); } } else { try { const { unsubscribeFromPush } = await import('@/lib/push-notifications'); const registration = await navigator.serviceWorker.ready; await unsubscribeFromPush(registration); } catch (err) { console.error('Unsubscribe failed:', err); } saveField("notificationsEnabled", false); } }} style={{ width: "16px", height: "16px" }} />
{/* ── View Style Tabs ── */}
{([ { key: "simple", label: t.simpleView, icon: }, { key: "calendar", label: t.calendarView, icon: }, { key: "list", label: t.listView, icon: }, { key: "kanban", label: t.kanbanView, icon: }, ] as const).map((tab) => ( ))}
{/* ── View-specific settings ── */}
{/* Time grid settings — for simple & calendar views */} {(viewStyle === "simple" || viewStyle === "calendar") && (
{ perView.saveViewSetting("showSubHourSlots", e.target.checked, true); }} style={{ width: "16px", height: "16px" }} />
{/* Weather Settings */}
{(perView.getEffective("weatherEnabled", profile.weatherEnabled || false) as boolean) && (() => { const recentCities: Array<{ name: string; country: string; admin1?: string; lat: number; lon: number }> = Array.isArray(profile.weatherRecentCities) ? profile.weatherRecentCities : []; const selectCity = (city: { name: string; country: string; admin1?: string; lat: number; lon: number }) => { const locationStr = `${city.name}, ${city.country}`; setProfile({ ...profile, weatherLat: city.lat, weatherLon: city.lon, weatherLocation: locationStr }); saveSetting("weatherLat", city.lat); saveSetting("weatherLon", city.lon); saveSetting("weatherLocation", locationStr); // Add to recent cities (deduplicate by lat+lon, keep max 8) const entry = { name: city.name, country: city.country, ...(city.admin1 ? { admin1: city.admin1 } : {}), lat: city.lat, lon: city.lon }; const filtered = recentCities.filter((c: any) => !(Math.abs(c.lat - city.lat) < 0.01 && Math.abs(c.lon - city.lon) < 0.01)); const updated = [entry, ...filtered].slice(0, 8); setProfile((p: any) => ({ ...p, weatherRecentCities: updated })); saveSetting("weatherRecentCities", updated); setWeatherSearchResults([]); }; return (
{ const q = e.target.value; if (q.length < 2) { setWeatherSearchResults([]); return; } try { const res = await fetch(`/api/weather/geocode?q=${encodeURIComponent(q)}`); const data = await res.json(); setWeatherSearchResults(data.results || []); } catch { setWeatherSearchResults([]); } }} /> {weatherSearchResults.length > 0 && (
{weatherSearchResults.map((r: any, i: number) => ( ))}
)} {profile.weatherLocation && (
📍 {profile.weatherLocation} ({profile.weatherLat?.toFixed(2)}, {profile.weatherLon?.toFixed(2)})
)} {recentCities.length > 0 && (
{profile.language === "de" ? "Letzte Städte" : "Recent cities"}
{recentCities.map((c: any, i: number) => { const isActive = profile.weatherLat && Math.abs(c.lat - profile.weatherLat) < 0.01 && profile.weatherLon && Math.abs(c.lon - profile.weatherLon) < 0.01; return ( ); })}
)} {/* Weather display options */}
{profile.language === "de" ? "Angezeigte Daten" : "Display data"}
{([ { key: "icon" as WeatherDisplayKey, label: profile.language === "de" ? "Wettersymbol" : "Weather icon", icon: "☀️" }, { key: "temp" as WeatherDisplayKey, label: profile.language === "de" ? "Temperatur" : "Temperature", icon: "🌡️" }, { key: "feelsLike" as WeatherDisplayKey, label: profile.language === "de" ? "Gefühlte Temp." : "Feels like", icon: "🤒" }, { key: "wind" as WeatherDisplayKey, label: profile.language === "de" ? "Windgeschwindigkeit" : "Wind speed", icon: "🌬️" }, { key: "gusts" as WeatherDisplayKey, label: profile.language === "de" ? "Windböen" : "Wind gusts", icon: "💨" }, { key: "precipProb" as WeatherDisplayKey, label: profile.language === "de" ? "Regenwahrscheinl." : "Rain probability", icon: "🌧️" }, { key: "precip" as WeatherDisplayKey, label: profile.language === "de" ? "Niederschlag (mm)" : "Precipitation (mm)", icon: "💦" }, { key: "humidity" as WeatherDisplayKey, label: profile.language === "de" ? "Luftfeuchtigkeit" : "Humidity", icon: "💧" }, { key: "uv" as WeatherDisplayKey, label: "UV Index", icon: "☀️" }, ]).map(({ key, label, icon }) => { const current = (perView.getEffective("weatherDisplay", WEATHER_DISPLAY_DEFAULTS) || WEATHER_DISPLAY_DEFAULTS) as WeatherDisplayKey[]; const checked = current.includes(key); return ( ); })}
); })()}
)} {/* Per-view display settings */}
{ perView.saveViewSetting("showSomeday", e.target.checked, true); }} style={{ width: "16px", height: "16px" }} />
{viewStyle !== "kanban" && (
{ perView.saveViewSetting("showAllDayEvents", e.target.checked, true); }} style={{ width: "16px", height: "16px" }} />
)} {viewStyle !== "kanban" && (perView.getEffective("showAllDayEvents", showAllDay) as boolean) && (
)} {viewStyle !== "kanban" && (
saveField("autoRolling", e.target.checked)} style={{ width: "16px", height: "16px" }} />
)}
{ saveField("showTaskCheckboxes", e.target.checked); perView.saveViewSetting("showTaskCheckboxes", e.target.checked, false); }} style={{ width: "16px", height: "16px" }} />
{ saveField("showProjectIcons", e.target.checked); perView.saveViewSetting("showProjectIcons", e.target.checked, false); }} style={{ width: "16px", height: "16px" }} />
{ saveField("showPriorityIcons", e.target.checked); perView.saveViewSetting("showPriorityIcons", e.target.checked, false); }} style={{ width: "16px", height: "16px" }} />
{viewStyle !== "kanban" && (
saveField("protectEventTimes", e.target.checked)} style={{ width: "16px", height: "16px" }} />
)}
{ perView.saveViewSetting("showCompletedTasks", e.target.checked, true); }} style={{ width: "16px", height: "16px" }} />
) : activeTab === "localisation" ? (

{t.localisation || "Localisation"}

{/* Start Week Setting + Start View On */}
{/* Weekday Format */}
{profile.weekdayFormat === "custom" && ( saveFieldDebounced("customWeekdayNames", e.target.value)} placeholder={ weekStartDay === 1 ? (t.customWeekdayNamesMon || translations["en"].customWeekdayNamesMon) : (t.customWeekdayNamesSun || translations["en"].customWeekdayNamesSun) } className="weekly-input" style={{ width: "100%", padding: "8px", fontSize: "0.85rem", border: "1px solid var(--weekly-settings-input-border)", borderRadius: "4px", background: "var(--weekly-settings-input-bg)", color: "var(--weekly-settings-text)" }} /> )}
{/* Weekday Case */}
{Intl.DateTimeFormat().resolvedOptions().timeZone}
) : activeTab === "calendar" ? ( isLoading ? (

Loading connections...

) : ( <>

{t.connectedCalendars}

saveField("showCalendarProviderIcon", e.target.checked)} style={{ width: "16px", height: "16px" }} />
{connMsg && (
{connMsg.text}
)} {connections.length === 0 ? (

{t.noCalendars}

) : (
    {connections.map((conn) => (
  • {conn.provider === "google" ? : conn.provider === "apple" ? : conn.provider === "synology" ? : conn.provider === "notion" ? : } {conn.provider === "google" ? "Google Calendar" : conn.provider === "apple" ? "Apple Calendar" : conn.provider === "synology" ? "Synology Calendar" : conn.provider === "notion" ? "Notion" : "Outlook Calendar"}
    {confirmDisconnectId === conn.id ? (
    Sure?
    ) : ( )}
    {/* Calendar Event Selection List */} {conn.calendars && Array.isArray(conn.calendars) && conn.calendars.length > 0 ? (
    {/* Column Headers */}
    Calendar Display Edit
    {/* Calendar Rows */} {conn.calendars.map((cal: any) => { const isShared = /⚠/.test(cal.title); const cleanTitle = cal.title .replace(/\s*⚠️?\s*/g, "") .trim(); return (
    {/* Calendar Color + Name */} {cleanTitle} {isShared && ( 🔗 )} {cal.isPrimary && ( (Primary) )} {/* Display checkbox */} handleUpdateCalendar(conn.id, cal.id, { selected: e.target.checked, }) } style={{ cursor: "pointer" }} /> {/* Edit checkbox */} handleUpdateCalendar(conn.id, cal.id, { editable: e.target.checked, }) } style={{ cursor: "pointer" }} title="Allow adding/editing events" />
    ); })}
    ) : (
    {conn.provider === "google" ? t.noCalendarsFound : conn.provider === "apple" ? t.noCalendarsApple : conn.provider === "synology" ? t.noCalendarsSynology : conn.provider === "notion" ? t.selectionAfterConnect : t.selectionAfterConnect}
    )}
  • ))}
)}

{t.connectMore}

{t.syncTasks}

{t.syncTasksDesc}

{connections .filter((c) => ["google", "outlook", "synology"].includes(c.provider)) .map((conn) => { const providerLists = availableTaskLists[ conn.provider as "google" | "outlook" | "synology" ] || []; const isFetching = isFetchingProviderLists[conn.provider]; return (
{conn.provider === "google" ? : conn.provider === "synology" ? : } {conn.provider === "google" ? "Google Tasks" : conn.provider === "synology" ? "Synology Tasks" : "Microsoft To-Do"} {isFetching && ( (fetching lists...) )}
{/* Column header with sync all / unsync all */} {providerLists.length > 0 && (() => { const allSynced = providerLists.every( (list: { id: string; title: string }) => somedayLists.some( (sl: SomedayList) => sl.externalId === list.id && sl.externalProvider === conn.provider, ), ); const noneSynced = providerLists.every( (list: { id: string; title: string }) => !somedayLists.some( (sl: SomedayList) => sl.externalId === list.id && sl.externalProvider === conn.provider, ), ); return (
List {!allSynced && ( )} {!noneSynced && ( )} Sync
); })()} {/* Inline unsync confirmation */} {unsyncConfirm && unsyncConfirm.provider === conn.provider && (
{t.unsyncConfirmMsg.replace("{title}", unsyncConfirm.list.title)}
)} {providerLists.map((list: { id: string; title: string }) => { const isSynced = somedayLists.some( (sl: SomedayList) => sl.externalId === list.id && sl.externalProvider === conn.provider, ); return (
{list.title} handleToggleTaskList( conn.provider as | "google" | "outlook" | "synology", list, ) } disabled={importingTasksState} />
); })} {!isFetching && providerLists.length === 0 && (
No task lists found.
)}
); })} {connections.filter((c) => ["google", "outlook", "synology"].includes(c.provider), ).length === 0 && (
Connect a provider above to sync task lists.
)} {importStatusMsg && (
{importStatusMsg.text}
)}
) ) : activeTab === "styling" ? (
{/* Mobile Font Scale */}
{[ { label: "75%", value: 0.75 }, { label: "85%", value: 0.85 }, { label: "100%", value: 1.0 }, { label: "115%", value: 1.15 }, { label: "130%", value: 1.3 }, ].map(opt => ( ))}
{(profile.language || "en") === "de" ? "Skaliert alle Schriften auf Mobilgeräten (< 768px)" : "Scales all fonts on mobile devices (< 768px)"}
{/* Typography Settings */}
{/* Date Layout & Alignment side-by-side */}
{/* Day / Weekday Gap */}
saveFieldDebounced("dayHeaderGap", e.target.value) } placeholder="0.35em" className="weekly-input" style={{ width: "100%", padding: "8px", fontSize: "0.9rem", border: "1px solid var(--weekly-settings-input-border)", borderRadius: "4px", background: "var(--weekly-settings-input-bg)", color: "var(--weekly-settings-text)", }} />
{/* Day Names */}
saveFieldDebounced("weekdayColor", e.target.value)} style={{ width: "28px", height: "28px", border: "1px solid var(--weekly-settings-input-border)", borderRadius: "4px", cursor: "pointer", padding: 0, flexShrink: 0 }} />
{(isCustomFont(profile.headlineFont || "") || profile.headlineFont === "__custom__") && ( saveFieldDebounced("headlineFont", e.target.value || "__custom__")} placeholder={t.fontPlaceholder} className="weekly-input" style={{ width: "100%", padding: "8px", fontSize: "0.85rem", border: "1px solid var(--weekly-settings-input-border)", borderRadius: "4px", background: "var(--weekly-settings-input-bg)", color: "var(--weekly-settings-text)" }} /> )}
saveFieldDebounced("headlineFontSize", e.target.value)} placeholder={t.fontSizePlaceholder} className="weekly-input" style={{ flex: 1, padding: "8px", fontSize: "0.85rem", border: "1px solid var(--weekly-settings-input-border)", borderRadius: "4px", background: "var(--weekly-settings-input-bg)", color: "var(--weekly-settings-text)" }} />
{/* Dates */}
saveFieldDebounced("dateColor", e.target.value)} style={{ width: "28px", height: "28px", border: "1px solid var(--weekly-settings-input-border)", borderRadius: "4px", cursor: "pointer", padding: 0, flexShrink: 0 }} />
{(isCustomFont(profile.dateFontFamily || "") || profile.dateFontFamily === "__custom__") && ( saveFieldDebounced("dateFontFamily", e.target.value || "__custom__")} placeholder={t.fontPlaceholder} className="weekly-input" style={{ width: "100%", padding: "8px", fontSize: "0.85rem", border: "1px solid var(--weekly-settings-input-border)", borderRadius: "4px", background: "var(--weekly-settings-input-bg)", color: "var(--weekly-settings-text)" }} /> )}
saveFieldDebounced("dateFontSize", e.target.value)} placeholder="0.65rem" className="weekly-input" style={{ flex: 1, padding: "8px", fontSize: "0.9rem", border: "1px solid var(--weekly-settings-input-border)", borderRadius: "4px", background: "var(--weekly-settings-input-bg)", color: "var(--weekly-settings-text)" }} />
{/* Tasks */}
saveFieldDebounced("taskColor", e.target.value)} style={{ width: "28px", height: "28px", border: "1px solid var(--weekly-settings-input-border)", borderRadius: "4px", cursor: "pointer", padding: 0, flexShrink: 0 }} />
{(isCustomFont(profile.taskFontFamily || "") || profile.taskFontFamily === "__custom__") && ( { const val = e.target.value || "__custom__"; saveFieldDebounced("taskFontFamily", val); saveFieldDebounced("timeTaskFontFamily", val); }} placeholder={t.fontPlaceholder} className="weekly-input" style={{ width: "100%", padding: "8px", fontSize: "0.85rem", border: "1px solid var(--weekly-settings-input-border)", borderRadius: "4px", background: "var(--weekly-settings-input-bg)", color: "var(--weekly-settings-text)" }} /> )}
{ saveFieldDebounced("taskFontSize", e.target.value); saveFieldDebounced("timeTaskFontSize", e.target.value); }} placeholder="0.9rem" className="weekly-input" style={{ flex: 1, padding: "8px", fontSize: "0.9rem", border: "1px solid var(--weekly-settings-input-border)", borderRadius: "4px", background: "var(--weekly-settings-input-bg)", color: "var(--weekly-settings-text)" }} />
{/* Calendar Event Font */}
{(isCustomFont(profile.eventFontFamily || "") || profile.eventFontFamily === "__custom__") && ( saveFieldDebounced("eventFontFamily", e.target.value || "__custom__")} placeholder={t.fontPlaceholder} className="weekly-input" style={{ width: "100%", padding: "8px", fontSize: "0.85rem", border: "1px solid var(--weekly-settings-input-border)", borderRadius: "4px", background: "var(--weekly-settings-input-bg)", color: "var(--weekly-settings-text)" }} /> )}
saveFieldDebounced("eventFontSize", e.target.value)} placeholder="0.85rem" className="weekly-input" style={{ flex: 1, padding: "8px", fontSize: "0.9rem", border: "1px solid var(--weekly-settings-input-border)", borderRadius: "4px", background: "var(--weekly-settings-input-bg)", color: "var(--weekly-settings-text)" }} />
{/* Goal Font */}
{(isCustomFont(profile.goalFontFamily || "") || profile.goalFontFamily === "__custom__") && ( saveFieldDebounced("goalFontFamily", e.target.value || "__custom__")} placeholder={t.fontPlaceholder} className="weekly-input" style={{ width: "100%", padding: "8px", fontSize: "0.85rem", border: "1px solid var(--weekly-settings-input-border)", borderRadius: "4px", background: "var(--weekly-settings-input-bg)", color: "var(--weekly-settings-text)" }} /> )}
saveFieldDebounced("goalFontSize", e.target.value)} placeholder="1rem" className="weekly-input" style={{ flex: 1, padding: "8px", fontSize: "0.9rem", border: "1px solid var(--weekly-settings-input-border)", borderRadius: "4px", background: "var(--weekly-settings-input-bg)", color: "var(--weekly-settings-text)" }} />
{/* Calendar Week Font */}
saveFieldDebounced("cwColor", e.target.value)} style={{ width: "28px", height: "28px", border: "1px solid var(--weekly-settings-input-border)", borderRadius: "4px", cursor: "pointer", padding: 0, flexShrink: 0 }} />
{(isCustomFont(profile.cwFontFamily || "") || profile.cwFontFamily === "__custom__") && ( saveFieldDebounced("cwFontFamily", e.target.value || "__custom__")} placeholder={t.fontPlaceholder} className="weekly-input" style={{ width: "100%", padding: "8px", fontSize: "0.85rem", border: "1px solid var(--weekly-settings-input-border)", borderRadius: "4px", background: "var(--weekly-settings-input-bg)", color: "var(--weekly-settings-text)" }} /> )}
saveFieldDebounced("cwFontSize", e.target.value)} placeholder="1.125rem" className="weekly-input" style={{ flex: 1, padding: "8px", fontSize: "0.9rem", border: "1px solid var(--weekly-settings-input-border)", borderRadius: "4px", background: "var(--weekly-settings-input-bg)", color: "var(--weekly-settings-text)" }} />
{/* Year Font */}
saveFieldDebounced("yearColor", e.target.value)} style={{ width: "28px", height: "28px", border: "1px solid var(--weekly-settings-input-border)", borderRadius: "4px", cursor: "pointer", padding: 0, flexShrink: 0 }} />
{(isCustomFont(profile.yearFontFamily || "") || profile.yearFontFamily === "__custom__") && ( saveFieldDebounced("yearFontFamily", e.target.value || "__custom__")} placeholder={t.fontPlaceholder} className="weekly-input" style={{ width: "100%", padding: "8px", fontSize: "0.85rem", border: "1px solid var(--weekly-settings-input-border)", borderRadius: "4px", background: "var(--weekly-settings-input-bg)", color: "var(--weekly-settings-text)" }} /> )}
saveFieldDebounced("yearFontSize", e.target.value)} placeholder="1.125rem" className="weekly-input" style={{ flex: 1, padding: "8px", fontSize: "0.9rem", border: "1px solid var(--weekly-settings-input-border)", borderRadius: "4px", background: "var(--weekly-settings-input-bg)", color: "var(--weekly-settings-text)" }} />
{/* Terminal Theme Import/Export */}

Import or Export standard Terminal 16-color JSON themes (e.g. Gogh, terminal.sexy) to completely change the app colors.

{/* Light Theme */}
{((profile as any).lightTheme) && ( )} {/* Color 16-grid Preview */} {(profile as any).lightTheme && (
{[...Array(16)].map((_, i) => (
))}
)}
{/* Dark Theme */}
{((profile as any).darkTheme) && ( )} {/* Color 16-grid Preview */} {(profile as any).darkTheme && (
{[...Array(16)].map((_, i) => (
))}
)}
{/* Element Colors */}
saveFieldDebounced("todayHighlightColor", e.target.value) } style={{ width: "100%", height: "30px", cursor: "pointer", border: "none", background: "transparent", }} />
saveFieldDebounced("pastDayColor", e.target.value) } style={{ width: "100%", height: "30px", cursor: "pointer", border: "none", background: "transparent", }} />
{/* Weekend Colors */}
saveFieldDebounced("weekendColorSat", e.target.value) } style={{ width: "100%", height: "30px", cursor: "pointer", border: "none", background: "transparent", }} />
saveFieldDebounced("weekendColorSun", e.target.value) } style={{ width: "100%", height: "30px", cursor: "pointer", border: "none", background: "transparent", }} />
{/* All styling settings auto-save */}
) : activeTab === "motivation" ? (
{/* Replaced Goal of the Week settings block */} {/* "Do This Now" Toggle */}
{ const newVal = e.target.checked; setShowNextTask(newVal); saveSetting("showNextTask", newVal); }} style={{ width: "20px", height: "20px", cursor: "pointer" }} />
{/* Focus Timer Settings moved here */}
saveFieldDebounced("focusTimerDuration", parseInt(e.target.value) || 25)} className="weekly-input" style={{ width: "100%", padding: "12px", border: "1px solid var(--weekly-border)", borderRadius: "6px", fontSize: "1rem", }} />
saveFieldDebounced("focusBreakDuration", parseInt(e.target.value) || 5)} className="weekly-input" style={{ width: "100%", padding: "12px", border: "1px solid var(--weekly-border)", borderRadius: "6px", fontSize: "1rem", }} />
{/* Goal Scope Redesign */}

{t.goalScopeTitle}

{/* Fallback Section */}

{t.goalFallbackTitle}

{(!profile.goalFallbackType || profile.goalFallbackType === "quote") && (
{(profile.quoteSourceUrls || [profile.quoteSourceUrl || ""]).map((url: string, idx: number) => (
{ const newUrls = [...(profile.quoteSourceUrls || [profile.quoteSourceUrl || ""])]; newUrls[idx] = e.target.value; setProfile((p: any) => ({ ...p, quoteSourceUrls: newUrls })); if (debouncedTimers.current["quoteSourceUrls"]) clearTimeout(debouncedTimers.current["quoteSourceUrls"]); debouncedTimers.current["quoteSourceUrls"] = setTimeout(() => saveSetting("quoteSourceUrls", newUrls), 500); }} className="weekly-input" placeholder="https://..." style={{ flex: 1, padding: "10px", fontSize: "0.95rem", borderRadius: "6px", border: "1px solid var(--weekly-border)", background: "var(--weekly-bg)", }} />
))} {/* Preset sources */}

{profile.language === "de" ? "Bekannte Quellen (klicken zum Hinzufügen):" : "Known sources (click to add):"}

{[ { label: "ZenQuotes (EN)", url: "https://zenquotes.io/api/random" }, { label: "Stoic Quotes (EN)", url: "https://stoic.tekloon.net/stoic-quote" }, { label: "Quotable (EN)", url: "https://api.quotable.io/quotes/random" }, { label: "Advice Slip (EN)", url: "https://api.adviceslip.com/advice" }, { label: "Zitat-Service (DE)", url: "https://api.zitat-service.de/v1/quote?language=de" }, { label: "Zitat-Service (EN)", url: "https://api.zitat-service.de/v1/quote?language=en" }, { label: "Zitat-Service (ES)", url: "https://api.zitat-service.de/v1/quote?language=es" }, ].map(({ label, url }) => { const current: string[] = profile.quoteSourceUrls || []; const already = current.includes(url); return ( ); })}

{profile.language === "de" ? "DE/EN/ES: auch über Zitat-Service API verfügbar. FR/IT: kuratierte lokale Sammlung." : "DE/EN/ES: also available via Zitat-Service API. FR/IT: curated local collection."}

{t.urlFormatHelp}

{t.quoteFallbackDesc}

{[ { code: "en", label: "English" }, { code: "de", label: "Deutsch" }, { code: "fr", label: "Français" }, { code: "es", label: "Español" }, { code: "it", label: "Italiano" }, ].map((lang) => { const selected = (profile.quoteLanguages || ["en", "de"]).includes(lang.code); return ( ); })}

{t.quoteLanguagesDesc}

)} {profile.goalFallbackType === "default" && (
saveFieldDebounced("goalDefaultSentence", e.target.value)} className="weekly-input" style={{ width: "100%", padding: "12px", borderRadius: "6px", border: "1px solid var(--weekly-border)", }} placeholder={t.defaultGoalPlaceholder} />
)}
) : activeTab === "sync" ? ( ) : activeTab === "projects" ? (

{t.projects || "Projects"}

{t.projectsDesc || "Organize tasks with color-coded projects"}

{/* Existing projects list */} {projects.length === 0 ? (

{t.noProjects || "No projects yet"}

) : (
{projects.map((p) => (
{editingProjectId === p.id ? (
{showEditProjectIconPicker && (
{ setEditProjectIcon(name); setShowEditProjectIconPicker(false); }} darkMode={false} />
)}
setEditProjectName(e.target.value)} className="weekly-input" style={{ flex: 1, padding: "8px 12px", fontSize: "0.9rem" }} onKeyDown={(e) => { if (e.key === "Enter") { fetch("/api/projects", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id: p.id, name: editProjectName, color: editProjectColor, icon: editProjectIcon }) }) .then(() => { onProjectsChanged(); setEditingProjectId(null); }); } if (e.key === "Escape") setEditingProjectId(null); }} autoFocus />
setEditProjectColor(e.target.value)} style={{ width: "30px", height: "30px", border: "none", cursor: "pointer", padding: 0, borderRadius: "6px" }} /> {profile.language === "de" ? "Farbe" : "Color"}
) : ( <>
{p.name}
)}
))}
)} {/* Add new project form */}

{profile.language === "de" ? "Projekt hinzufügen" : "Add Project"}

{showNewProjectIconPicker && (
{ setNewProjectIcon(name); setShowNewProjectIconPicker(false); }} darkMode={false} />
)}
setNewProjectColor(e.target.value)} style={{ width: "30px", height: "30px", border: "none", cursor: "pointer", padding: 0, borderRadius: "6px" }} /> setNewProjectName(e.target.value)} placeholder={profile.language === "de" ? "Name" : "Name"} className="weekly-input" style={{ flex: 1, padding: "8px 12px", fontSize: "0.9rem" }} onKeyDown={(e) => { if (e.key === "Enter" && newProjectName.trim()) { fetch("/api/projects", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: newProjectName.trim(), color: newProjectColor, icon: newProjectIcon }) }) .then(() => { onProjectsChanged(); setNewProjectName(""); setNewProjectIcon("folder"); }); } }} />
) : activeTab === "about" ? (

My Weekly To-Do List

Version {process.env.NEXT_PUBLIC_APP_VERSION || "1.8.0"}

) : ( /* Account Tab */
saveFieldDebounced("name", e.target.value)} className="weekly-input" style={{ width: "100%", padding: "8px", border: "1px solid #ddd", borderRadius: "4px", }} />
{profile.id && (
(e.target as HTMLInputElement).select()} className="weekly-input" style={{ width: "100%", padding: "8px", border: "1px solid #eee", borderRadius: "4px", background: "#f5f5f5", color: "#555", fontSize: "0.85rem", fontFamily: "monospace", cursor: "text", }} /> {t.accountIdDesc}
)} {profile.accountNumber && (
(e.target as HTMLInputElement).select()} className="weekly-input" style={{ width: "100%", padding: "8px", border: "1px solid #eee", borderRadius: "4px", background: "#f5f5f5", color: "#555", fontSize: "0.85rem", fontFamily: "monospace", cursor: "text", }} /> {t.accountNumberDesc}
)}
setPasswords({ ...passwords, new: e.target.value }) } style={{ width: "100%", padding: "8px", border: "1px solid #ddd", borderRadius: "4px", marginBottom: "8px", }} /> setPasswords({ ...passwords, confirm: e.target.value }) } style={{ width: "100%", padding: "8px", border: "1px solid #ddd", borderRadius: "4px", }} /> {translations[profile.language || "en"]?.newPasswordDesc || translations["en"].newPasswordDesc}
{accountMsg && ( {accountMsg} )}
{/* Data Export Section */}

{profile.language === "de" ? "Datenexport" : "Data Export"}

{profile.language === "de" ? "CSV-Arbeitsbericht erledigter Aufgaben — nach Kalenderwochen gruppiert. Wähle Felder, Zeitraum und lade die Datei herunter." : "CSV work report of completed tasks grouped by calendar week. Choose fields, date range and download."}

{/* Date range */}
setExportStartDate(e.target.value)} className="weekly-input" style={{ width: "100%", padding: "6px", border: "1px solid var(--weekly-settings-input-border)", borderRadius: "4px", background: "var(--weekly-settings-input-bg)", color: "var(--weekly-settings-text)" }} />
setExportEndDate(e.target.value)} className="weekly-input" style={{ width: "100%", padding: "6px", border: "1px solid var(--weekly-settings-input-border)", borderRadius: "4px", background: "var(--weekly-settings-input-bg)", color: "var(--weekly-settings-text)" }} />
{/* Quick date presets */}
{[ { label: profile.language === "de" ? "Diese Woche" : "This week", days: 7 }, { label: profile.language === "de" ? "Dieser Monat" : "This month", days: 30 }, { label: profile.language === "de" ? "Letzter Monat" : "Last month", days: 60, offset: 30 }, { label: profile.language === "de" ? "Dieses Jahr" : "This year", days: 365 }, ].map(({ label, days, offset }) => ( ))}
{/* Field picker */}
{profile.language === "de" ? "Spalten auswählen:" : "Select columns:"}
{EXPORT_FIELDS.map(f => { const checked = exportFields.has(f.key); const label = profile.language === "de" ? f.labelDe : f.labelEn; return ( ); })}
{/* Download buttons */}
{/* Backup & Restore Section */}

{t.backupRestore}

{t.backupRestoreDesc}

{/* Export All Data */} {/* Import Section */}
{/* Import Mode Toggle */}
{importMode === "replace" && (
{t.importReplaceWarning}
)} {/* File Input */} { setImportFile(e.target.files?.[0] || null); setImportMsg(""); }} className="weekly-input" style={{ width: "100%", padding: "6px", border: "1px solid var(--weekly-settings-input-border)", borderRadius: "4px", background: "var(--weekly-settings-input-bg)", color: "var(--weekly-settings-text)", marginBottom: "10px", fontSize: "0.85rem", }} /> {importMsg && (

{importMsg}

)}
{/* Sign Out Button - accessible on mobile */}

{t.dataPrivacy}

)} {/* Apple Calendar (CalDAV) Connection Modal */} {showAppleCalendarModal && (

Connect Apple Calendar

Connect your iCloud Calendar events via CalDAV.

This requires an{" "} app-specific password {" "} generated at appleid.apple.com.

⚠️

{t.appleRemindersNote}

{appleCalError && (
{appleCalError}
)}
setAppleCalEmail(e.target.value)} className="w-full border border-gray-300 rounded px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:outline-none" placeholder="name@icloud.com" />
setAppleCalPassword(e.target.value)} className="w-full border border-gray-300 rounded px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:outline-none" placeholder="xxxx-xxxx-xxxx-xxxx" onKeyDown={(e) => e.key === "Enter" && submitAppleCalendarConnection() } />
)} {/* Synology Calendar Connection Modal */} {showSynologyCalendarModal && (

Connect Synology Calendar

Connect your Synology NAS Calendar events.

Make sure Synology Calendar is installed and the CalDAV URL is reachable over HTTPS.

{synologyCalError && (
{synologyCalError}
)}
setSynologyCalServerUrl(e.target.value)} className="w-full border border-gray-300 rounded px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:outline-none" placeholder="https://your-synology-nas:5001" />
setSynologyCalUsername(e.target.value)} className="w-full border border-gray-300 rounded px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:outline-none" placeholder="admin" />
setSynologyCalPassword(e.target.value)} className="w-full border border-gray-300 rounded px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:outline-none" onKeyDown={(e) => e.key === "Enter" && submitSynologyCalendarConnection() } />
)}
); } export default SettingsSidebar;