From a5f6edd82dc4af8aa8e6703c0bd488fb98fc8784 Mon Sep 17 00:00:00 2001 From: mARTin Date: Mon, 30 Mar 2026 22:03:09 +0200 Subject: [PATCH] perf: fix stale auto-save bug + extract SettingsSidebar to lazy chunk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - saveSetting: add setProfile(p => ({...p, [key]: value})) so profile stays in sync with individual setting changes — prevents auto-save from overwriting fresh settings with stale profile object values - Extract SettingsSidebar (4725 lines) from WeeklyView.tsx into its own file; lazy-load with next/dynamic so the ~4500-line settings panel is deferred until the user opens settings - Extract shared constants/helpers to src/lib/: fontConstants.ts (AVAILABLE_FONTS, FONT_WEIGHTS, isCustomFont) weeklyViewTranslations.ts (translations object, ~1100 lines) weeklyViewConstants.ts (WeatherDisplayKey, WEATHER_DISPLAY_DEFAULTS) - WeeklyView.tsx: 16,209 → 10,364 lines (-36%) v1.80.0 --- package.json | 2 +- src/components/SettingsSidebar.tsx | 4725 ++++++++++++++++++++++ src/components/WeeklyView.tsx | 5861 +--------------------------- src/lib/fontConstants.ts | 28 + src/lib/weeklyViewConstants.ts | 4 + src/lib/weeklyViewTranslations.ts | 1141 ++++++ 6 files changed, 5907 insertions(+), 5854 deletions(-) create mode 100644 src/components/SettingsSidebar.tsx create mode 100644 src/lib/fontConstants.ts create mode 100644 src/lib/weeklyViewConstants.ts create mode 100644 src/lib/weeklyViewTranslations.ts diff --git a/package.json b/package.json index eacc0ea..2a48f23 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "my-weekly-todo-list", - "version": "1.79.0", + "version": "1.80.0", "description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view", "main": "index.js", "scripts": { diff --git a/src/components/SettingsSidebar.tsx b/src/components/SettingsSidebar.tsx new file mode 100644 index 0000000..5d4c69e --- /dev/null +++ b/src/components/SettingsSidebar.tsx @@ -0,0 +1,4725 @@ +"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 } 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 { + ArrowLeftRight, + Calendar, + CalendarDays, + Globe, + Info, + Kanban, + Link, + ListTodo, + Palette, + Play, + Plus, + Settings, + Sparkles, + Trash2, + User, +} from "lucide-react"; + +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: { 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" + >(initialTab || "general"); + const [isLoading, setIsLoading] = useState(true); + const [isSyncing, setIsSyncing] = useState(false); + const [exportStartDate, setExportStartDate] = useState(""); + const [exportEndDate, setExportEndDate] = useState(""); + 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: "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 === "custom" && ( +
+ + { + const val = e.target.value; + setProfile({ ...profile, headerCustomFormat: val }); + saveSetting("headerCustomFormat", val); + }} + placeholder="KW WW | YYYY or DD.MM.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 (Week), YYYY (Year), MMMM (Month Name), MM (Month Num), DD (Day), [TODAY] (Active Date) +
+
+ )} +
+ + {/* 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" }} /> + +
+ + {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)", + }} + /> + +
+ ))} + +
+

+ {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 === "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" + ? "Laden Sie eine CSV-Datei Ihrer erledigten Aufgaben herunter." + : "Download a CSV file of your completed tasks."} +

+
+
+ + 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)", + }} + /> +
+
+ + {profile.language === "de" + ? "Erledigte Aufgaben exportieren (CSV)" + : "Export Completed Tasks (CSV)"} + +
+ + {/* 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; diff --git a/src/components/WeeklyView.tsx b/src/components/WeeklyView.tsx index 0bef9ee..5174ee7 100644 --- a/src/components/WeeklyView.tsx +++ b/src/components/WeeklyView.tsx @@ -98,6 +98,10 @@ import { ImportListModal } from "./ImportListModal"; import OnboardingWizard from "./OnboardingWizard"; import CalendarSyncRulesPanel from "./CalendarSyncRulesPanel"; import { getRandomLocalQuote } from "@/lib/quotes"; +import { AVAILABLE_FONTS, isCustomFont } from "../lib/fontConstants"; +import { translations } from "../lib/weeklyViewTranslations"; +import { WeatherDisplayKey, WEATHER_DISPLAY_DEFAULTS } from "../lib/weeklyViewConstants"; +const SettingsSidebar = dynamic(() => import("./SettingsSidebar"), { ssr: false }); // Cookie helpers for per-device settings const DEVICE_SETTINGS_KEYS = ["viewDays", "cellDuration", "startHour", "endHour"]; @@ -181,7 +185,7 @@ interface CalendarEvent { url?: string; } -interface SomedayList { +export interface SomedayList { id: string; title: string; tasks: Task[]; @@ -192,9 +196,7 @@ interface SomedayList { } // Time grid configuration options -type CellDuration = 15 | 20 | 30 | 60; -type WeatherDisplayKey = "icon" | "temp" | "feelsLike" | "wind" | "gusts" | "precipProb" | "precip" | "humidity" | "uv"; -const WEATHER_DISPLAY_DEFAULTS: WeatherDisplayKey[] = ["icon", "temp"]; +export type CellDuration = 15 | 20 | 30 | 60; const DEFAULT_SOMEDAY_SLOT_COUNT = 5; const getSomedaySlotCount = (tasks: Task[]) => { @@ -210,34 +212,6 @@ const getSomedaySlotCount = (tasks: Task[]) => { return Math.max(DEFAULT_SOMEDAY_SLOT_COUNT, maxIdx + 2); }; -// Font options -const AVAILABLE_FONTS = [ - { name: "Default (Inter)", value: "Inter" }, - { name: "Roboto", value: "Roboto" }, - { name: "Open Sans", value: "Open Sans" }, - { name: "Lato", value: "Lato" }, - { name: "Montserrat", value: "Montserrat" }, - { name: "Oswald", value: "Oswald" }, - { name: "Raleway", value: "Raleway" }, - { name: "Playfair Display", value: "Playfair Display" }, - { name: "Merriweather", value: "Merriweather" }, - { name: "Nunito", value: "Nunito" }, - { name: "Dancing Script", value: "Dancing Script" }, - { name: "Pacifico", value: "Pacifico" }, - { name: "Custom Google Font...", value: "__custom__" }, -]; - -// Check if a font value is a custom (non-preset) font -const isCustomFont = (value: string): boolean => - !!value && value !== "__custom__" && !AVAILABLE_FONTS.slice(0, -1).some((f) => f.value === value); - -const FONT_WEIGHTS = [ - { name: "Light", value: "300" }, - { name: "Normal", value: "400" }, - { name: "Medium", value: "500" }, - { name: "Bold", value: "700" }, -]; - // Helper to load Google Fonts const useGoogleFonts = (fonts: string[]) => { useEffect(() => { @@ -261,1147 +235,6 @@ const useGoogleFonts = (fonts: string[]) => { }, [fonts]); }; -// Translations -const translations: Record = { - en: { - settings: "Settings", - general: "General", - calendar: "Connections", - localisation: "Localisation", - account: "Account", - runningList: "Running List (Auto-roll tasks to today)", - protectEventTimes: "Protect Event Times", - showTimeGrid: "Show Time Grid", - timeSlotDuration: "Time Slot Duration", - viewStyle: "View Style", - simpleView: "Simple", - calendarView: "Calendar", - listView: "List", - weekView: "Week", - kanbanView: "Kanban", - filterByProject: "All Projects", - filterByList: "All Lists", - filterByWeek: "All Weeks", - kanbanStages: "Kanban Stages", - kanbanStagesDesc: "Define the stages for your Kanban board. Drag tasks between columns to change their stage.", - addStage: "Add stage", - stageName: "Stage name", - noStage: "No stage", - headerDisplay: "Header Display", - headerDisplayKW: "Calendar Week (KW)", - headerDisplayMonth: "Month Name - March", - headerDisplayMonthYear: "Month & Year - March | 2026", - headerDisplayDate: "Full Date - 13.03.2026", - headerDisplayCustom: "Custom - Friday - 13. March", - headerDisplayNone: "None", - headerCustomFormatLabel: "Format string (e.g. DD.MM.YYYY)", - language: "Language", - dateFormat: "Date Format", - timeFormat: "Time Format", - saveChanges: "Save Changes", - connectedCalendars: "Connected Calendars", - connectMore: "Connect More", - connectGoogle: "Connect Google Calendar", - connectApple: "Connect Apple Calendar", - appleRemindersNote: "Apple Reminders are not supported. Since iOS 13 / macOS Catalina, Apple no longer provides a CalDAV or public API for Reminders. Only calendar events can be synced.", - connectSynology: "Connect Synology", - connectNotion: "Connect Notion", - noCalendars: "No calendars connected yet.", - dataPrivacy: "Data & Privacy", - downloadData: "Download My Data", - deleteAccount: "Delete Account", - name: "Name", - email: "Email", - timezone: "Timezone", - changePassword: "Change Password", - newPassword: "New Password", - confirmPassword: "Confirm Password", - someday: "SOMEDAY", - lists: "Lists", - newList: "New list", - allTabs: "All", - newTab: "New tab", - newTabName: "New tab name:", - assignTab: "Assign to tab", - noTab: "No tab", - renameTab: "Double-click to rename", - dissolveTab: "Remove tab (keep lists)", - loading: "Loading your tasks...", - sycing: "Syncing...", - synced: "Synced", - localization: "Localization", - allDayEvents: "ALL-DAY EVENTS", - syncCalendar: "Sync Calendar", - showProviderIcon: "Show provider icon on events", - toggleDarkMode: "Toggle Dark Mode", - signOut: "Sign Out", - startHour: "Start of Day", - endHour: "End of Day", - weekAbbr: "W", - goalOfWeek: "Goal of the Week", - goalScope: "Goal Scope", - goalScopeWeek: "Per Week", - goalScopeDay: "Per Day", - goalFallback: "Goal Fallback Type", - defaultGoal: "Custom Default Goal", - showTaskCheckboxes: "Show Checkboxes on Tasks", - showProjectIcons: "Show Icons for Projects", - showSomeday: "Show Someday Section", - showAllDay: "Show All-Day Section", - allDayPosition: "All-Day Events Position", - allDayAbove: "Above", - allDayBelow: "Below", - newPasswordDesc: "Leave blank to keep current password.", - dateAlignment: "Date Alignment", - dateVerticalAlign: "Date Vertical Alignment", - alignTop: "Top", - alignMiddle: "Middle", - alignBottom: "Bottom", - dateLayout: "Date Layout", - alignmentLeft: "Left", - alignmentCenter: "Center", - alignmentRight: "Right", - alignmentTight: "Tight", - backupRestore: "Backup & Restore", - backupRestoreDesc: "Export all your tasks, anyday lists, and projects as a JSON file. You can edit the file and import it back.", - exportAllData: "Export All Data (JSON)", - importData: "Import Data", - importMode: "Import Mode", - importModeMerge: "Merge", - importModeMergeDesc: "Add imported data alongside existing tasks", - importModeReplace: "Replace", - importModeReplaceDesc: "Delete all existing data and replace with imported data", - importReplaceWarning: "Warning: This will permanently delete all your current tasks, lists, and projects!", - importSelectFile: "Select JSON file...", - importButton: "Import", - importing: "Importing...", - exporting: "Exporting...", - projects: "Projects", - projectsDesc: "Organize tasks with color-coded projects", - addProject: "Add Project", - projectName: "Name", - projectColor: "Color", - noProjects: "No projects yet", - assignProject: "Assign project", - removeProject: "Remove project", - weekdayFormat: "Weekday Format", - weekdayFormatFull: "Full Name (Monday)", - weekdayFormatShort: "Short (Mon)", - weekdayFormatNarrow: "Narrow (M)", - weekdayFormatCustom: "Custom", - customWeekdayNamesMon: "Mo; Tu; We; Th; Fr; Sa; Su", - customWeekdayNamesSun: "Su; Mo; Tu; We; Th; Fr; Sa", - weekdayCase: "Weekday Case", - weekdayCaseNormal: "Normal (monday)", - weekdayCaseCapitalize: "Capitalize (Monday)", - weekdayCaseUppercase: "Uppercase (MONDAY)", - styling: "Styling", - motivation: "Motivation", - about: "About", - setupAssistant: "Run Setup Assistant", - calendarSync: "Sync", - calendarSyncTitle: "Calendar Sync", - calendarSyncDesc: "Sync events between your connected calendar providers.", - syncNow: "Sync Now", - syncing: "Syncing…", - syncResults: "Sync Results", - noRulesEnabled: "No enabled rules found.", - syncNeedsTwo: "You need at least two connected calendars to create a sync rule.", - noSyncRules: "No sync rules yet. Add one below.", - addSyncRule: "Add Sync Rule", - editSyncRule: "Edit Rule", - newSyncRule: "New Sync Rule", - syncRuleName: "Rule Name (optional)", - syncRuleNamePlaceholder: "e.g. Work → Personal", - syncDirection: "Direction", - oneWay: "One-way", - twoWay: "Two-way", - sourceCalendar: "Source Calendar", - targetCalendar: "Target Calendar", - titlePrefix: "Title Prefix (optional)", - titlePrefixPlaceholder: "e.g. [Work] ", - syncDescription: "Sync description", - syncLocation: "Sync location", - syncRecurring: "Include recurring events", - createRule: "Create Rule", - updateRule: "Update Rule", - weekStartLabel: "Start week on", - startViewLabel: "Start view on", - monday: "Monday", - sunday: "Sunday", - today: "Today", - yesterday: "Yesterday", - accountId: "Account ID", - accountIdDesc: "Your unique account identifier", - accountNumberLabel: "Account Number", - accountNumberDesc: "Your account number for identification when changing email", - connectOutlook: "Connect Outlook", - syncTasks: "Sync Tasks", - syncTasksDesc: "Sync tasks with Google Tasks or Microsoft To-Do.", - unsyncConfirmMsg: "Stop syncing \"{title}\"? Its tasks will be moved to trash.", - unsyncConfirm: "Stop syncing", - unsyncCancel: "Cancel", - syncAll: "Sync all", - unsyncAll: "Unsync all", - fetchingLists: "(fetching lists...)", - listHeader: "List", - syncHeader: "Sync", - noTaskListsFound: "No task lists found.", - connectProviderAbove: "Connect a provider above to sync task lists.", - noCalendarsFound: "No calendars found or permission denied.", - noCalendarsApple: "No calendars loaded. Please disconnect and reconnect Apple Calendar.", - noCalendarsSynology: "No calendars loaded. Please disconnect and reconnect Synology.", - selectionAfterConnect: "Selection available after connect.", - sharedCalendar: "Shared calendar", - primaryCalendar: "(Primary)", - fontCustomization: "Font Customization", - dateLayoutRight: "Date Right of Weekday", - dateLayoutLeft: "Date Left of Weekday", - dateLayoutAbove: "Date Above Weekday", - dateLayoutBelow: "Date Below Weekday", - dateLayoutHidden: "Date Hidden", - dateLayoutMobile: "Date Layout (Mobile)", - dayWeekdayGap: "Day / Weekday Gap", - weekdayFont: "Weekday Font", - dateFont: "Date Font", - taskFont: "Task Font", - eventFont: "Event Font", - goalFont: "Goal / Quote Font", - cwFont: "Calendar Week Font", - yearFont: "Year Font", - fontPlaceholder: "e.g. Poppins, Bebas Neue...", - fontSizePlaceholder: "Font size (e.g. 1.25rem)", - weightLight: "Light", - weightNormal: "Normal", - weightMedium: "Medium", - weightSemi: "Semi", - weightBold: "Bold", - weightBlack: "Black", - hourLabelFormat: "Hour Label Format", - hourLabelShort: "Short (8, 9, 10)", - hourLabelFull: "Full (8:00, 9:00, 10:00)", - showSubhourLabels: "Show Sub-hour Labels (:15, :30, :45)", - showScheduleCalendar: "Show Schedule / Calendar", - showDoThisNow: 'Show "Do This Now" instead of Motto', - focusTimer: "Focus Timer (min)", - focusBreak: "Focus Break (min)", - goalScopeTitle: "Goal Time Period", - goalFallbackTitle: "Goal Fallback", - motivationalQuote: "Motivational Quote / Holiday Hint", - nextTodo: "Next To-Do", - defaultText: "Default Text", - apiDataSources: "API Data Sources (URLs)", - addSource: "Add Source", - urlFormatHelp: "URL returning JSON quotes", - quoteLanguages: "Quote Languages", - quoteLanguagesDesc: "Choose which languages your quotes appear in. At least one must be selected.", - quoteFallbackDesc: "If no external source responds, curated local quotes in your language are used as fallback.", - defaultGoalPlaceholder: "Enter your goal here...", - saturdayColor: "Saturday", - sundayColor: "Sunday", - todayHighlight: "Today Highlight", - pastDayColor: "Past Day Color", - deleteProjectConfirm: "Delete project", - importConfirmReplace: "This will delete ALL existing tasks, lists, and projects. Continue?", - importSuccess: "Import complete", - importInvalidJson: "Invalid JSON file", - }, - de: { - settings: "Einstellungen", - general: "Allgemein", - calendar: "Verbindungen", - localisation: "Lokalisierung", - account: "Konto", - runningList: "Laufende Liste (Aufgaben automatisch auf heute verschieben)", - protectEventTimes: "Ereigniszeiten schützen", - showTimeGrid: "Zeitplan anzeigen", - timeSlotDuration: "Zeitfensterdauer", - viewStyle: "Ansichtsstil", - simpleView: "Einfach", - calendarView: "Kalender", - kanbanView: "Kanban", - weekView: "Woche", - filterByProject: "Alle Projekte", - filterByList: "Alle Listen", - filterByWeek: "Alle Wochen", - kanbanStages: "Kanban-Phasen", - kanbanStagesDesc: "Definiere die Phasen für dein Kanban-Board. Ziehe Aufgaben zwischen Spalten, um ihre Phase zu ändern.", - addStage: "Phase hinzufügen", - stageName: "Phasenname", - noStage: "Keine Phase", - headerDisplay: "Kopfzeile", - headerDisplayKW: "Kalenderwoche (KW)", - headerDisplayMonth: "Monatsname - März", - headerDisplayMonthYear: "Monat & Jahr - März | 2026", - headerDisplayDate: "Vollständiges Datum - 13.03.2026", - headerDisplayCustom: "Benutzerdefiniert - Freitag - 13. März", - headerDisplayNone: "Nichts", - headerCustomFormatLabel: "Format (z.B. DD.MM.YYYY)", - listView: "Liste", - notes: "Notizen", - notesSidebar: "Notizen-Seitenleiste", - language: "Sprache", - dateFormat: "Datumsformat", - timeFormat: "Zeitformat", - saveChanges: "Änderungen speichern", - connectedCalendars: "Verbundene Kalender", - connectMore: "Mehr verbinden", - connectGoogle: "Google Kalender verbinden", - connectApple: "Apple Kalender verbinden", - appleRemindersNote: "Apple Erinnerungen werden nicht unterstützt. Seit iOS 13 / macOS Catalina bietet Apple keine CalDAV- oder öffentliche API mehr für Erinnerungen an. Nur Kalender-Ereignisse können synchronisiert werden.", - connectNotion: "Notion verbinden", - noCalendars: "Keine Kalender verbunden.", - dataPrivacy: "Daten & Datenschutz", - downloadData: "Meine Daten herunterladen", - deleteAccount: "Konto löschen", - name: "Name", - email: "E-Mail", - timezone: "Zeitzone", - changePassword: "Passwort ändern", - newPassword: "Neues Passwort", - confirmPassword: "Passwort bestätigen", - someday: "IRGENDWANN", - lists: "Listen", - newList: "Neue Liste", - allTabs: "Alle", - newTab: "Neuer Tab", - newTabName: "Neuer Tab-Name:", - assignTab: "Tab zuweisen", - noTab: "Kein Tab", - renameTab: "Doppelklick zum Umbenennen", - dissolveTab: "Tab entfernen (Listen behalten)", - loading: "Lade Aufgaben...", - syncing: "Synchronisiere...", - synced: "Synchronisiert", - localization: "Lokalisierung", - allDayEvents: "GANZTÄGIGE EREIGNISSE", - syncCalendar: "Kalender synchronisieren", - showProviderIcon: "Anbieter-Icon auf Terminen anzeigen", - toggleDarkMode: "Dunkelmodus umschalten", - signOut: "Abmelden", - startHour: "Tagesbeginn", - endHour: "Tagesende", - weekAbbr: "KW", - goalOfWeek: "Ziel der Woche", - goalScope: "Ziel-Zeitraum", - goalScopeWeek: "Pro Woche", - goalScopeDay: "Pro Tag", - goalFallback: "Ziel-Fallback-Typ", - defaultGoal: "Benutzerdefiniertes Standardziel", - showTaskCheckboxes: "Checkboxen bei Aufgaben anzeigen", - showProjectIcons: "Icons für Projekte anzeigen", - showSomeday: "Irgendwann-Bereich anzeigen", - showAllDay: "Ganztägige Ereignisse anzeigen", - allDayPosition: "Position ganztägiger Ereignisse", - allDayAbove: "Oben", - allDayBelow: "Unten", - newPasswordDesc: "Leer lassen, um das aktuelle Passwort zu behalten.", - dateAlignment: "Datums-Ausrichtung", - dateVerticalAlign: "Datums-Vertikalausrichtung", - alignTop: "Oben", - alignMiddle: "Mitte", - alignBottom: "Unten", - dateLayout: "Datumslayout", - alignmentLeft: "Links", - alignmentCenter: "Mitte", - alignmentRight: "Rechts", - alignmentTight: "Eng", - backupRestore: "Sicherung & Wiederherstellung", - backupRestoreDesc: "Exportieren Sie alle Aufgaben, Irgendwann-Listen und Projekte als JSON-Datei. Sie können die Datei bearbeiten und wieder importieren.", - exportAllData: "Alle Daten exportieren (JSON)", - importData: "Daten importieren", - importMode: "Import-Modus", - importModeMerge: "Zusammenführen", - importModeMergeDesc: "Importierte Daten neben bestehenden Aufgaben hinzufügen", - importModeReplace: "Ersetzen", - importModeReplaceDesc: "Alle bestehenden Daten löschen und durch importierte ersetzen", - importReplaceWarning: "Warnung: Dies löscht dauerhaft alle Ihre aktuellen Aufgaben, Listen und Projekte!", - importSelectFile: "JSON-Datei auswählen...", - importButton: "Importieren", - importing: "Importiere...", - exporting: "Exportiere...", - projects: "Projekte", - projectsDesc: "Aufgaben mit farbcodierten Projekten organisieren", - addProject: "Projekt hinzufügen", - projectName: "Name", - projectColor: "Farbe", - noProjects: "Noch keine Projekte", - assignProject: "Projekt zuweisen", - removeProject: "Projekt entfernen", - weekdayFormat: "Wochentag-Format", - weekdayFormatFull: "Vollständiger Name (Montag)", - weekdayFormatShort: "Kurz (Mo)", - weekdayFormatNarrow: "Schmal (M)", - weekdayFormatCustom: "Benutzerdefiniert", - customWeekdayNamesMon: "Mo; Di; Mi; Do; Fr; Sa; So", - customWeekdayNamesSun: "So; Mo; Di; Mi; Do; Fr; Sa", - weekdayCase: "Groß-/Kleinschreibung", - weekdayCaseNormal: "Klein (montag)", - weekdayCaseCapitalize: "Großbuchstabe (Montag)", - weekdayCaseUppercase: "Großbuchstaben (MONTAG)", - styling: "Design", - motivation: "Motivation", - about: "Über", - setupAssistant: "Einrichtungsassistent starten", - calendarSync: "Sync", - calendarSyncTitle: "Kalender-Synchronisation", - calendarSyncDesc: "Ereignisse zwischen verbundenen Kalender-Anbietern synchronisieren.", - syncNow: "Jetzt synchronisieren", - syncResults: "Sync-Ergebnis", - noRulesEnabled: "Keine aktiven Regeln gefunden.", - syncNeedsTwo: "Du benötigst mindestens zwei verbundene Kalender für eine Sync-Regel.", - noSyncRules: "Noch keine Sync-Regeln. Füge eine unten hinzu.", - addSyncRule: "Regel hinzufügen", - editSyncRule: "Regel bearbeiten", - newSyncRule: "Neue Sync-Regel", - syncRuleName: "Regelname (optional)", - syncRuleNamePlaceholder: "z.B. Arbeit → Privat", - syncDirection: "Richtung", - oneWay: "Einseitig", - twoWay: "Beidseitig", - sourceCalendar: "Quellkalender", - targetCalendar: "Zielkalender", - titlePrefix: "Titel-Präfix (optional)", - titlePrefixPlaceholder: "z.B. [Arbeit] ", - syncDescription: "Beschreibung synchronisieren", - syncLocation: "Ort synchronisieren", - syncRecurring: "Wiederkehrende Ereignisse einschließen", - createRule: "Regel erstellen", - updateRule: "Regel aktualisieren", - weekStartLabel: "Woche beginnt am", - startViewLabel: "Ansicht beginnt mit", - monday: "Montag", - sunday: "Sonntag", - today: "Heute", - yesterday: "Gestern", - accountId: "Konto-ID", - accountIdDesc: "Ihre eindeutige Konto-Kennung", - accountNumberLabel: "Kontonummer", - accountNumberDesc: "Ihre Kontonummer zur Identifikation", - connectOutlook: "Outlook verbinden", - syncTasks: "Aufgaben synchronisieren", - syncTasksDesc: "Aufgaben mit Google Tasks oder Microsoft To-Do synchronisieren.", - unsyncConfirmMsg: "Synchronisierung von \"{title}\" beenden? Die Aufgaben werden in den Papierkorb verschoben.", - unsyncConfirm: "Sync beenden", - unsyncCancel: "Abbrechen", - syncAll: "Alle synchronisieren", - unsyncAll: "Alle trennen", - fetchingLists: "(Listen werden geladen...)", - listHeader: "Liste", - syncHeader: "Sync", - noTaskListsFound: "Keine Aufgabenlisten gefunden.", - connectProviderAbove: "Verbinden Sie einen Anbieter oben, um Aufgabenlisten zu synchronisieren.", - noCalendarsFound: "Keine Kalender gefunden oder Zugriff verweigert.", - noCalendarsApple: "Keine Kalender geladen. Bitte trennen und erneut verbinden.", - noCalendarsSynology: "Keine Kalender geladen. Bitte trennen und erneut verbinden.", - selectionAfterConnect: "Auswahl nach Verbindung verfügbar.", - sharedCalendar: "Geteilter Kalender", - primaryCalendar: "(Primär)", - fontCustomization: "Schriftart-Anpassung", - dateLayoutRight: "Datum rechts vom Wochentag", - dateLayoutLeft: "Datum links vom Wochentag", - dateLayoutAbove: "Datum über Wochentag", - dateLayoutBelow: "Datum unter Wochentag", - dateLayoutHidden: "Datum ausgeblendet", - dateLayoutMobile: "Datum-Layout (Mobil)", - dayWeekdayGap: "Tag / Wochentag Abstand", - weekdayFont: "Wochentag-Schrift", - dateFont: "Datum-Schrift", - taskFont: "Aufgaben-Schrift", - eventFont: "Termin-Schrift", - goalFont: "Ziel / Zitat-Schrift", - cwFont: "Kalenderwoche-Schrift", - yearFont: "Jahr-Schrift", - fontPlaceholder: "z.B. Poppins, Bebas Neue...", - fontSizePlaceholder: "Schriftgröße (z.B. 1.25rem)", - weightLight: "Leicht", - weightNormal: "Normal", - weightMedium: "Mittel", - weightSemi: "Halb-fett", - weightBold: "Fett", - weightBlack: "Schwarz", - hourLabelFormat: "Stundenformat", - hourLabelShort: "Kurz (8, 9, 10)", - hourLabelFull: "Voll (8:00, 9:00, 10:00)", - showSubhourLabels: "Viertelstunden anzeigen (:15, :30, :45)", - showScheduleCalendar: "Zeitplan / Kalender anzeigen", - showDoThisNow: '"Jetzt erledigen" statt Motto anzeigen', - focusTimer: "Fokus-Timer (Min)", - focusBreak: "Fokus-Pause (Min)", - goalScopeTitle: "Ziel-Zeitraum", - goalFallbackTitle: "Ziel-Fallback", - motivationalQuote: "Motivationszitat / Feiertags-Hinweis", - nextTodo: "Nächstes To-Do", - defaultText: "Standardtext", - apiDataSources: "API-Datenquellen (URLs)", - addSource: "Quelle hinzufügen", - urlFormatHelp: "URL die JSON-Zitate liefert", - quoteLanguages: "Zitatsprachen", - quoteLanguagesDesc: "Wählen Sie die Sprachen für Ihre Zitate. Mindestens eine muss ausgewählt sein.", - quoteFallbackDesc: "Wenn keine externe Quelle antwortet, werden lokale kuratierte Zitate in Ihrer Sprache verwendet.", - defaultGoalPlaceholder: "Ihr Ziel hier eingeben...", - saturdayColor: "Samstag", - sundayColor: "Sonntag", - todayHighlight: "Heute-Hervorhebung", - pastDayColor: "Vergangene Tage", - deleteProjectConfirm: "Projekt löschen", - importConfirmReplace: "Dies löscht ALLE bestehenden Aufgaben, Listen und Projekte. Fortfahren?", - importSuccess: "Import abgeschlossen", - importInvalidJson: "Ungültige JSON-Datei", - }, - fr: { - settings: "Paramètres", - general: "Général", - calendar: "Connexions", - localisation: "Localisation", - account: "Compte", - runningList: "Liste continue (reporter les tâches à aujourd'hui)", - protectEventTimes: "Protéger les horaires des événements", - showTimeGrid: "Afficher la grille horaire", - timeSlotDuration: "Durée des créneaux horaires", - viewStyle: "Style d'affichage", - simpleView: "Simple", - calendarView: "Calendrier", - listView: "Liste", - weekView: "Semaine", - kanbanView: "Kanban", - filterByProject: "Tous les projets", - filterByList: "Toutes les listes", - filterByWeek: "Toutes les semaines", - kanbanStages: "Étapes Kanban", - kanbanStagesDesc: "Définissez les étapes de votre tableau Kanban. Glissez les tâches entre les colonnes pour changer leur étape.", - addStage: "Ajouter une étape", - stageName: "Nom de l'étape", - noStage: "Aucune étape", - headerDisplay: "Affichage en-tête", - headerDisplayKW: "Semaine calendaire (KW)", - headerDisplayMonth: "Nom du mois - Mars", - headerDisplayMonthYear: "Mois & Année - Mars | 2026", - headerDisplayDate: "Date complète - 13.03.2026", - headerDisplayCustom: "Personnalisé - Vendredi - 13 Mars", - headerDisplayNone: "Aucun", - headerCustomFormatLabel: "Format (ex: DD.MM.YYYY)", - language: "Langue", - dateFormat: "Format de date", - timeFormat: "Format d'heure", - saveChanges: "Enregistrer", - connectedCalendars: "Calendriers connectés", - connectMore: "En connecter d'autres", - connectGoogle: "Connecter Google Agenda", - connectApple: "Connecter le calendrier Apple", - appleRemindersNote: "Les rappels Apple ne sont pas pris en charge. Depuis iOS 13 / macOS Catalina, Apple ne fournit plus de CalDAV ni d'API publique pour les rappels. Seuls les événements de calendrier peuvent être synchronisés.", - connectSynology: "Connecter Synology", - connectNotion: "Connecter Notion", - noCalendars: "Aucun calendrier connecté.", - dataPrivacy: "Données et confidentialité", - downloadData: "Télécharger mes données", - deleteAccount: "Supprimer le compte", - name: "Nom", - email: "E-mail", - timezone: "Fuseau horaire", - changePassword: "Changer le mot de passe", - newPassword: "Nouveau mot de passe", - confirmPassword: "Confirmer le mot de passe", - someday: "UN JOUR", - lists: "Listes", - newList: "Nouvelle liste", - allTabs: "Tous", - newTab: "Nouvel onglet", - newTabName: "Nom du nouvel onglet :", - assignTab: "Assigner à un onglet", - noTab: "Aucun onglet", - renameTab: "Double-cliquez pour renommer", - dissolveTab: "Supprimer l'onglet (garder les listes)", - loading: "Chargement de vos tâches…", - sycing: "Synchronisation…", - synced: "Synchronisé", - localization: "Localisation", - allDayEvents: "ÉVÉNEMENTS JOURNÉE ENTIÈRE", - syncCalendar: "Synchroniser le calendrier", - showProviderIcon: "Afficher l'icône du fournisseur sur les événements", - toggleDarkMode: "Basculer le mode sombre", - signOut: "Se déconnecter", - startHour: "Début de journée", - endHour: "Fin de journée", - weekAbbr: "S", - goalOfWeek: "Objectif de la semaine", - goalScope: "Portée de l'objectif", - goalScopeWeek: "Par semaine", - goalScopeDay: "Par jour", - goalFallback: "Type d'objectif par défaut", - defaultGoal: "Objectif par défaut personnalisé", - showTaskCheckboxes: "Afficher les cases à cocher", - showProjectIcons: "Afficher les icônes de projets", - showSomeday: "Afficher la section Un jour", - showAllDay: "Afficher la section Journée entière", - allDayPosition: "Position des événements journée entière", - allDayAbove: "Au-dessus", - allDayBelow: "En dessous", - newPasswordDesc: "Laisser vide pour conserver le mot de passe actuel.", - dateAlignment: "Alignement de la date", - dateVerticalAlign: "Alignement vertical de la date", - alignTop: "Haut", - alignMiddle: "Milieu", - alignBottom: "Bas", - dateLayout: "Disposition de la date", - alignmentLeft: "Gauche", - alignmentCenter: "Centre", - alignmentRight: "Droite", - alignmentTight: "Compact", - backupRestore: "Sauvegarde et restauration", - backupRestoreDesc: "Exportez toutes vos tâches, listes et projets au format JSON. Vous pouvez modifier le fichier et le réimporter.", - exportAllData: "Exporter toutes les données (JSON)", - importData: "Importer des données", - importMode: "Mode d'importation", - importModeMerge: "Fusionner", - importModeMergeDesc: "Ajouter les données importées aux tâches existantes", - importModeReplace: "Remplacer", - importModeReplaceDesc: "Supprimer toutes les données existantes et les remplacer par les données importées", - importReplaceWarning: "Attention : toutes vos tâches, listes et projets actuels seront définitivement supprimés !", - importSelectFile: "Sélectionner un fichier JSON…", - importButton: "Importer", - importing: "Importation…", - exporting: "Exportation…", - projects: "Projets", - projectsDesc: "Organisez vos tâches avec des projets colorés", - addProject: "Ajouter un projet", - projectName: "Nom", - projectColor: "Couleur", - noProjects: "Aucun projet", - assignProject: "Attribuer un projet", - removeProject: "Retirer le projet", - weekdayFormat: "Format des jours", - weekdayFormatFull: "Nom complet (lundi)", - weekdayFormatShort: "Abrégé (lun.)", - weekdayFormatNarrow: "Étroit (L)", - weekdayFormatCustom: "Personnalisé", - customWeekdayNamesMon: "Lu; Ma; Me; Je; Ve; Sa; Di", - customWeekdayNamesSun: "Di; Lu; Ma; Me; Je; Ve; Sa", - weekdayCase: "Casse des jours", - weekdayCaseNormal: "Normal (lundi)", - weekdayCaseCapitalize: "Majuscule (Lundi)", - weekdayCaseUppercase: "Majuscules (LUNDI)", - styling: "Style", - motivation: "Motivation", - about: "À propos", - setupAssistant: "Lancer l'assistant de configuration", - weekStartLabel: "La semaine commence le", - startViewLabel: "Vue commence par", - monday: "Lundi", - sunday: "Dimanche", - today: "Aujourd'hui", - yesterday: "Hier", - accountId: "ID du compte", - accountIdDesc: "Votre identifiant de compte unique", - accountNumberLabel: "Numéro de compte", - accountNumberDesc: "Votre numéro de compte pour identification", - connectOutlook: "Connecter Outlook", - syncTasks: "Synchroniser les tâches", - syncTasksDesc: "Synchronisez les tâches avec Google Tasks ou Microsoft To-Do.", - unsyncConfirmMsg: "Arrêter la synchronisation de \"{title}\" ? Ses tâches seront mises à la corbeille.", - unsyncConfirm: "Arrêter la sync", - unsyncCancel: "Annuler", - syncAll: "Tout synchroniser", - unsyncAll: "Tout désynchroniser", - fetchingLists: "(chargement des listes...)", - listHeader: "Liste", - syncHeader: "Sync", - noTaskListsFound: "Aucune liste de tâches trouvée.", - connectProviderAbove: "Connectez un fournisseur ci-dessus pour synchroniser les listes.", - noCalendarsFound: "Aucun calendrier trouvé ou accès refusé.", - noCalendarsApple: "Aucun calendrier chargé. Veuillez déconnecter et reconnecter.", - noCalendarsSynology: "Aucun calendrier chargé. Veuillez déconnecter et reconnecter.", - selectionAfterConnect: "Sélection disponible après connexion.", - sharedCalendar: "Calendrier partagé", - primaryCalendar: "(Principal)", - fontCustomization: "Personnalisation des polices", - dateLayoutRight: "Date à droite du jour", - dateLayoutLeft: "Date à gauche du jour", - dateLayoutAbove: "Date au-dessus du jour", - dateLayoutBelow: "Date en dessous du jour", - dateLayoutHidden: "Date masquée", - dateLayoutMobile: "Disposition date (mobile)", - dayWeekdayGap: "Espacement jour / semaine", - weekdayFont: "Police du jour", - dateFont: "Police de la date", - taskFont: "Police des tâches", - eventFont: "Police des événements", - goalFont: "Police objectif / citation", - cwFont: "Police semaine calendaire", - yearFont: "Police de l'année", - fontPlaceholder: "ex. Poppins, Bebas Neue...", - fontSizePlaceholder: "Taille (ex. 1.25rem)", - weightLight: "Léger", - weightNormal: "Normal", - weightMedium: "Moyen", - weightSemi: "Semi-gras", - weightBold: "Gras", - weightBlack: "Noir", - hourLabelFormat: "Format des heures", - hourLabelShort: "Court (8, 9, 10)", - hourLabelFull: "Complet (8:00, 9:00, 10:00)", - showSubhourLabels: "Afficher les quarts d'heure (:15, :30, :45)", - showScheduleCalendar: "Afficher le calendrier", - showDoThisNow: '"Faire maintenant" au lieu de la devise', - focusTimer: "Minuteur Focus (min)", - focusBreak: "Pause Focus (min)", - goalScopeTitle: "Période de l'objectif", - goalFallbackTitle: "Fallback objectif", - motivationalQuote: "Citation motivante / info jour férié", - nextTodo: "Prochaine tâche", - defaultText: "Texte par défaut", - apiDataSources: "Sources de données API (URLs)", - addSource: "Ajouter une source", - urlFormatHelp: "URL retournant des citations JSON", - quoteLanguages: "Langues des citations", - quoteLanguagesDesc: "Choisissez les langues de vos citations. Au moins une doit être sélectionnée.", - quoteFallbackDesc: "Si aucune source externe ne répond, des citations locales dans votre langue sont utilisées.", - defaultGoalPlaceholder: "Entrez votre objectif ici...", - saturdayColor: "Samedi", - sundayColor: "Dimanche", - todayHighlight: "Surbrillance aujourd'hui", - pastDayColor: "Jours passés", - deleteProjectConfirm: "Supprimer le projet", - importConfirmReplace: "Cela supprimera TOUTES les tâches, listes et projets existants. Continuer ?", - importSuccess: "Import terminé", - importInvalidJson: "Fichier JSON invalide", - }, - es: { - settings: "Ajustes", - general: "General", - calendar: "Conexiones", - localisation: "Localización", - account: "Cuenta", - runningList: "Lista continua (pasar tareas a hoy)", - protectEventTimes: "Proteger horarios de eventos", - showTimeGrid: "Mostrar cuadrícula horaria", - timeSlotDuration: "Duración de los intervalos", - viewStyle: "Estilo de vista", - simpleView: "Simple", - calendarView: "Calendario", - listView: "Lista", - weekView: "Semana", - kanbanView: "Kanban", - filterByProject: "Todos los proyectos", - filterByList: "Todas las listas", - filterByWeek: "Todas las semanas", - kanbanStages: "Etapas Kanban", - kanbanStagesDesc: "Define las etapas de tu tablero Kanban. Arrastra tareas entre columnas para cambiar su etapa.", - addStage: "Añadir etapa", - stageName: "Nombre de etapa", - noStage: "Sin etapa", - headerDisplay: "Visualización de encabezado", - headerDisplayKW: "Semana calendario (KW)", - headerDisplayMonth: "Nombre del mes - Marzo", - headerDisplayMonthYear: "Mes y Año - Marzo | 2026", - headerDisplayDate: "Fecha completa - 13.03.2026", - headerDisplayCustom: "Personalizado - Viernes - 13 Marzo", - headerDisplayNone: "Ninguno", - headerCustomFormatLabel: "Formato (ej. DD.MM.YYYY)", - language: "Idioma", - dateFormat: "Formato de fecha", - timeFormat: "Formato de hora", - saveChanges: "Guardar cambios", - connectedCalendars: "Calendarios conectados", - connectMore: "Conectar más", - connectGoogle: "Conectar Google Calendar", - connectApple: "Conectar calendario de Apple", - appleRemindersNote: "Los recordatorios de Apple no son compatibles. Desde iOS 13 / macOS Catalina, Apple ya no ofrece CalDAV ni una API pública para recordatorios. Solo se pueden sincronizar eventos del calendario.", - connectSynology: "Conectar Synology", - connectNotion: "Conectar Notion", - noCalendars: "No hay calendarios conectados.", - dataPrivacy: "Datos y privacidad", - downloadData: "Descargar mis datos", - deleteAccount: "Eliminar cuenta", - name: "Nombre", - email: "Correo electrónico", - timezone: "Zona horaria", - changePassword: "Cambiar contraseña", - newPassword: "Nueva contraseña", - confirmPassword: "Confirmar contraseña", - someday: "ALGÚN DÍA", - lists: "Listas", - newList: "Nueva lista", - allTabs: "Todas", - newTab: "Nueva pestaña", - newTabName: "Nombre de nueva pestaña:", - assignTab: "Asignar a pestaña", - noTab: "Sin pestaña", - renameTab: "Doble clic para renombrar", - dissolveTab: "Eliminar pestaña (mantener listas)", - loading: "Cargando tus tareas…", - sycing: "Sincronizando…", - synced: "Sincronizado", - localization: "Localización", - allDayEvents: "EVENTOS DE TODO EL DÍA", - syncCalendar: "Sincronizar calendario", - showProviderIcon: "Mostrar icono del proveedor en eventos", - toggleDarkMode: "Alternar modo oscuro", - signOut: "Cerrar sesión", - startHour: "Inicio del día", - endHour: "Fin del día", - weekAbbr: "S", - goalOfWeek: "Objetivo de la semana", - goalScope: "Alcance del objetivo", - goalScopeWeek: "Por semana", - goalScopeDay: "Por día", - goalFallback: "Tipo de objetivo por defecto", - defaultGoal: "Objetivo predeterminado personalizado", - showTaskCheckboxes: "Mostrar casillas en las tareas", - showProjectIcons: "Mostrar iconos de proyectos", - showSomeday: "Mostrar sección Algún día", - showAllDay: "Mostrar sección Todo el día", - allDayPosition: "Posición de eventos de todo el día", - allDayAbove: "Arriba", - allDayBelow: "Abajo", - newPasswordDesc: "Dejar en blanco para conservar la contraseña actual.", - dateAlignment: "Alineación de la fecha", - dateVerticalAlign: "Alineación vertical de la fecha", - alignTop: "Arriba", - alignMiddle: "Centro", - alignBottom: "Abajo", - dateLayout: "Disposición de la fecha", - alignmentLeft: "Izquierda", - alignmentCenter: "Centro", - alignmentRight: "Derecha", - alignmentTight: "Compacto", - backupRestore: "Copia de seguridad y restauración", - backupRestoreDesc: "Exporta todas tus tareas, listas y proyectos como archivo JSON. Puedes editar el archivo y volver a importarlo.", - exportAllData: "Exportar todos los datos (JSON)", - importData: "Importar datos", - importMode: "Modo de importación", - importModeMerge: "Combinar", - importModeMergeDesc: "Añadir los datos importados junto a las tareas existentes", - importModeReplace: "Reemplazar", - importModeReplaceDesc: "Eliminar todos los datos existentes y reemplazarlos con los datos importados", - importReplaceWarning: "Advertencia: ¡Se eliminarán permanentemente todas tus tareas, listas y proyectos actuales!", - importSelectFile: "Seleccionar archivo JSON…", - importButton: "Importar", - importing: "Importando…", - exporting: "Exportando…", - projects: "Proyectos", - projectsDesc: "Organiza las tareas con proyectos de colores", - addProject: "Añadir proyecto", - projectName: "Nombre", - projectColor: "Color", - noProjects: "Aún no hay proyectos", - assignProject: "Asignar proyecto", - removeProject: "Quitar proyecto", - weekdayFormat: "Formato de los días", - weekdayFormatFull: "Nombre completo (lunes)", - weekdayFormatShort: "Abreviado (lun.)", - weekdayFormatNarrow: "Estrecho (L)", - weekdayFormatCustom: "Personalizado", - customWeekdayNamesMon: "Lu; Ma; Mi; Ju; Vi; Sá; Do", - customWeekdayNamesSun: "Do; Lu; Ma; Mi; Ju; Vi; Sá", - weekdayCase: "Mayúsculas de los días", - weekdayCaseNormal: "Normal (lunes)", - weekdayCaseCapitalize: "Mayúscula inicial (Lunes)", - weekdayCaseUppercase: "Mayúsculas (LUNES)", - styling: "Estilo", - motivation: "Motivación", - about: "Acerca de", - setupAssistant: "Iniciar asistente de configuración", - weekStartLabel: "La semana empieza el", - startViewLabel: "Vista empieza con", - monday: "Lunes", - sunday: "Domingo", - today: "Hoy", - yesterday: "Ayer", - accountId: "ID de cuenta", - accountIdDesc: "Tu identificador único de cuenta", - accountNumberLabel: "Número de cuenta", - accountNumberDesc: "Tu número de cuenta para identificación", - connectOutlook: "Conectar Outlook", - syncTasks: "Sincronizar tareas", - syncTasksDesc: "Sincroniza tareas con Google Tasks o Microsoft To-Do.", - unsyncConfirmMsg: "¿Dejar de sincronizar \"{title}\"? Sus tareas se moverán a la papelera.", - unsyncConfirm: "Dejar de sincronizar", - unsyncCancel: "Cancelar", - syncAll: "Sincronizar todo", - unsyncAll: "Desincronizar todo", - fetchingLists: "(cargando listas...)", - listHeader: "Lista", - syncHeader: "Sync", - noTaskListsFound: "No se encontraron listas de tareas.", - connectProviderAbove: "Conecta un proveedor arriba para sincronizar listas.", - noCalendarsFound: "No se encontraron calendarios o acceso denegado.", - noCalendarsApple: "No hay calendarios cargados. Desconecta y reconecta.", - noCalendarsSynology: "No hay calendarios cargados. Desconecta y reconecta.", - selectionAfterConnect: "Selección disponible tras conectar.", - sharedCalendar: "Calendario compartido", - primaryCalendar: "(Principal)", - fontCustomization: "Personalización de fuentes", - dateLayoutRight: "Fecha a la derecha del día", - dateLayoutLeft: "Fecha a la izquierda del día", - dateLayoutAbove: "Fecha encima del día", - dateLayoutBelow: "Fecha debajo del día", - dateLayoutHidden: "Fecha oculta", - dateLayoutMobile: "Disposición fecha (móvil)", - dayWeekdayGap: "Espacio día / semana", - weekdayFont: "Fuente del día", - dateFont: "Fuente de la fecha", - taskFont: "Fuente de tareas", - eventFont: "Fuente de eventos", - goalFont: "Fuente objetivo / cita", - cwFont: "Fuente semana calendario", - yearFont: "Fuente del año", - fontPlaceholder: "ej. Poppins, Bebas Neue...", - fontSizePlaceholder: "Tamaño (ej. 1.25rem)", - weightLight: "Ligero", - weightNormal: "Normal", - weightMedium: "Medio", - weightSemi: "Semi-negrita", - weightBold: "Negrita", - weightBlack: "Negro", - hourLabelFormat: "Formato de horas", - hourLabelShort: "Corto (8, 9, 10)", - hourLabelFull: "Completo (8:00, 9:00, 10:00)", - showSubhourLabels: "Mostrar cuartos de hora (:15, :30, :45)", - showScheduleCalendar: "Mostrar calendario", - showDoThisNow: '"Hacer ahora" en vez de lema', - focusTimer: "Temporizador Focus (min)", - focusBreak: "Pausa Focus (min)", - goalScopeTitle: "Periodo del objetivo", - goalFallbackTitle: "Fallback del objetivo", - motivationalQuote: "Cita motivacional / festivo", - nextTodo: "Siguiente tarea", - defaultText: "Texto predeterminado", - apiDataSources: "Fuentes de datos API (URLs)", - addSource: "Añadir fuente", - urlFormatHelp: "URL que devuelve citas JSON", - quoteLanguages: "Idiomas de citas", - quoteLanguagesDesc: "Elige los idiomas de tus citas. Al menos uno debe estar seleccionado.", - quoteFallbackDesc: "Si ninguna fuente externa responde, se usan citas locales en tu idioma.", - defaultGoalPlaceholder: "Ingresa tu objetivo aquí...", - saturdayColor: "Sábado", - sundayColor: "Domingo", - todayHighlight: "Resaltado de hoy", - pastDayColor: "Días pasados", - deleteProjectConfirm: "Eliminar proyecto", - importConfirmReplace: "Esto eliminará TODAS las tareas, listas y proyectos existentes. ¿Continuar?", - importSuccess: "Importación completada", - importInvalidJson: "Archivo JSON inválido", - }, - it: { - settings: "Impostazioni", - general: "Generali", - calendar: "Connessioni", - localisation: "Localizzazione", - account: "Account", - runningList: "Lista continua (sposta le attività a oggi)", - protectEventTimes: "Proteggi gli orari degli eventi", - showTimeGrid: "Mostra griglia oraria", - timeSlotDuration: "Durata degli intervalli", - viewStyle: "Stile di visualizzazione", - simpleView: "Semplice", - calendarView: "Calendario", - listView: "Lista", - weekView: "Settimana", - kanbanView: "Kanban", - filterByProject: "Tutti i progetti", - filterByList: "Tutte le liste", - filterByWeek: "Tutte le settimane", - kanbanStages: "Fasi Kanban", - kanbanStagesDesc: "Definisci le fasi della tua board Kanban. Trascina le attività tra le colonne per cambiare la loro fase.", - addStage: "Aggiungi fase", - stageName: "Nome fase", - noStage: "Nessuna fase", - headerDisplay: "Visualizzazione intestazione", - headerDisplayKW: "Settimana calendario (KW)", - headerDisplayMonth: "Nome del mese - Marzo", - headerDisplayMonthYear: "Mese e Anno - Marzo | 2026", - headerDisplayDate: "Data completa - 13.03.2026", - headerDisplayCustom: "Personalizzato - Venerdì - 13 Marzo", - headerDisplayNone: "Nessuno", - headerCustomFormatLabel: "Formato (es. DD.MM.YYYY)", - language: "Lingua", - dateFormat: "Formato data", - timeFormat: "Formato ora", - saveChanges: "Salva modifiche", - connectedCalendars: "Calendari collegati", - connectMore: "Collega altri", - connectGoogle: "Collega Google Calendar", - connectApple: "Collega il calendario Apple", - appleRemindersNote: "I promemoria Apple non sono supportati. Da iOS 13 / macOS Catalina, Apple non fornisce più CalDAV o un'API pubblica per i promemoria. Solo gli eventi del calendario possono essere sincronizzati.", - connectSynology: "Collega Synology", - connectNotion: "Collega Notion", - noCalendars: "Nessun calendario collegato.", - dataPrivacy: "Dati e privacy", - downloadData: "Scarica i miei dati", - deleteAccount: "Elimina account", - name: "Nome", - email: "E-mail", - timezone: "Fuso orario", - changePassword: "Cambia password", - newPassword: "Nuova password", - confirmPassword: "Conferma password", - someday: "UN GIORNO", - lists: "Liste", - newList: "Nuova lista", - allTabs: "Tutte", - newTab: "Nuova scheda", - newTabName: "Nome nuova scheda:", - assignTab: "Assegna a scheda", - noTab: "Nessuna scheda", - renameTab: "Doppio clic per rinominare", - dissolveTab: "Rimuovi scheda (mantieni liste)", - loading: "Caricamento delle attività…", - sycing: "Sincronizzazione…", - synced: "Sincronizzato", - localization: "Localizzazione", - allDayEvents: "EVENTI GIORNATA INTERA", - syncCalendar: "Sincronizza calendario", - showProviderIcon: "Mostra icona del provider sugli eventi", - toggleDarkMode: "Attiva/disattiva modalità scura", - signOut: "Esci", - startHour: "Inizio giornata", - endHour: "Fine giornata", - weekAbbr: "S", - goalOfWeek: "Obiettivo della settimana", - goalScope: "Ambito dell'obiettivo", - goalScopeWeek: "Per settimana", - goalScopeDay: "Per giorno", - goalFallback: "Tipo di obiettivo predefinito", - defaultGoal: "Obiettivo predefinito personalizzato", - showTaskCheckboxes: "Mostra caselle di spunta sulle attività", - showProjectIcons: "Mostra icone per i progetti", - showSomeday: "Mostra sezione Un giorno", - showAllDay: "Mostra sezione Giornata intera", - allDayPosition: "Posizione eventi giornata intera", - allDayAbove: "Sopra", - allDayBelow: "Sotto", - newPasswordDesc: "Lascia vuoto per mantenere la password attuale.", - dateAlignment: "Allineamento della data", - dateVerticalAlign: "Allineamento verticale della data", - alignTop: "In alto", - alignMiddle: "Al centro", - alignBottom: "In basso", - dateLayout: "Disposizione della data", - alignmentLeft: "Sinistra", - alignmentCenter: "Centro", - alignmentRight: "Destra", - alignmentTight: "Compatto", - backupRestore: "Backup e ripristino", - backupRestoreDesc: "Esporta tutte le attività, le liste e i progetti come file JSON. Puoi modificare il file e reimportarlo.", - exportAllData: "Esporta tutti i dati (JSON)", - importData: "Importa dati", - importMode: "Modalità di importazione", - importModeMerge: "Unisci", - importModeMergeDesc: "Aggiungere i dati importati alle attività esistenti", - importModeReplace: "Sostituisci", - importModeReplaceDesc: "Elimina tutti i dati esistenti e sostituiscili con i dati importati", - importReplaceWarning: "Attenzione: tutte le attività, le liste e i progetti attuali verranno eliminati definitivamente!", - importSelectFile: "Seleziona file JSON…", - importButton: "Importa", - importing: "Importazione…", - exporting: "Esportazione…", - projects: "Progetti", - projectsDesc: "Organizza le attività con progetti colorati", - addProject: "Aggiungi progetto", - projectName: "Nome", - projectColor: "Colore", - noProjects: "Nessun progetto", - assignProject: "Assegna progetto", - removeProject: "Rimuovi progetto", - weekdayFormat: "Formato dei giorni", - weekdayFormatFull: "Nome completo (lunedì)", - weekdayFormatShort: "Abbreviato (lun)", - weekdayFormatNarrow: "Stretto (L)", - weekdayFormatCustom: "Personalizzato", - customWeekdayNamesMon: "Lu; Ma; Me; Gi; Ve; Sa; Do", - customWeekdayNamesSun: "Do; Lu; Ma; Me; Gi; Ve; Sa", - weekdayCase: "Maiuscole dei giorni", - weekdayCaseNormal: "Normale (lunedì)", - weekdayCaseCapitalize: "Iniziale maiuscola (Lunedì)", - weekdayCaseUppercase: "Maiuscolo (LUNEDÌ)", - styling: "Stile", - motivation: "Motivazione", - about: "Info", - setupAssistant: "Assistente di configurazione", - weekStartLabel: "La settimana inizia il", - startViewLabel: "Vista inizia con", - monday: "Lunedì", - sunday: "Domenica", - today: "Oggi", - yesterday: "Ieri", - accountId: "ID account", - accountIdDesc: "Il tuo identificatore account unico", - accountNumberLabel: "Numero account", - accountNumberDesc: "Il tuo numero account per identificazione", - connectOutlook: "Connetti Outlook", - syncTasks: "Sincronizza attività", - syncTasksDesc: "Sincronizza le attività con Google Tasks o Microsoft To-Do.", - unsyncConfirmMsg: "Interrompere la sincronizzazione di \"{title}\"? Le attività verranno spostate nel cestino.", - unsyncConfirm: "Interrompi sync", - unsyncCancel: "Annulla", - syncAll: "Sincronizza tutto", - unsyncAll: "Desincronizza tutto", - fetchingLists: "(caricamento liste...)", - listHeader: "Lista", - syncHeader: "Sync", - noTaskListsFound: "Nessuna lista di attività trovata.", - connectProviderAbove: "Connetti un provider sopra per sincronizzare le liste.", - noCalendarsFound: "Nessun calendario trovato o accesso negato.", - noCalendarsApple: "Nessun calendario caricato. Disconnetti e riconnetti.", - noCalendarsSynology: "Nessun calendario caricato. Disconnetti e riconnetti.", - selectionAfterConnect: "Selezione disponibile dopo la connessione.", - sharedCalendar: "Calendario condiviso", - primaryCalendar: "(Principale)", - fontCustomization: "Personalizzazione caratteri", - dateLayoutRight: "Data a destra del giorno", - dateLayoutLeft: "Data a sinistra del giorno", - dateLayoutAbove: "Data sopra il giorno", - dateLayoutBelow: "Data sotto il giorno", - dateLayoutHidden: "Data nascosta", - dateLayoutMobile: "Layout data (mobile)", - dayWeekdayGap: "Spazio giorno / settimana", - weekdayFont: "Carattere giorno", - dateFont: "Carattere data", - taskFont: "Carattere attività", - eventFont: "Carattere eventi", - goalFont: "Carattere obiettivo / citazione", - cwFont: "Carattere settimana calendario", - yearFont: "Carattere anno", - fontPlaceholder: "es. Poppins, Bebas Neue...", - fontSizePlaceholder: "Dimensione (es. 1.25rem)", - weightLight: "Leggero", - weightNormal: "Normale", - weightMedium: "Medio", - weightSemi: "Semi-grassetto", - weightBold: "Grassetto", - weightBlack: "Nero", - hourLabelFormat: "Formato delle ore", - hourLabelShort: "Breve (8, 9, 10)", - hourLabelFull: "Completo (8:00, 9:00, 10:00)", - showSubhourLabels: "Mostra quarti d'ora (:15, :30, :45)", - showScheduleCalendar: "Mostra calendario", - showDoThisNow: '"Fai ora" invece del motto', - focusTimer: "Timer Focus (min)", - focusBreak: "Pausa Focus (min)", - goalScopeTitle: "Periodo dell'obiettivo", - goalFallbackTitle: "Fallback obiettivo", - motivationalQuote: "Citazione motivazionale / festività", - nextTodo: "Prossima attività", - defaultText: "Testo predefinito", - apiDataSources: "Fonti dati API (URL)", - addSource: "Aggiungi fonte", - urlFormatHelp: "URL che restituisce citazioni JSON", - quoteLanguages: "Lingue delle citazioni", - quoteLanguagesDesc: "Scegli le lingue delle citazioni. Almeno una deve essere selezionata.", - quoteFallbackDesc: "Se nessuna fonte esterna risponde, vengono usate citazioni locali nella tua lingua.", - defaultGoalPlaceholder: "Inserisci il tuo obiettivo qui...", - saturdayColor: "Sabato", - sundayColor: "Domenica", - todayHighlight: "Evidenziazione oggi", - pastDayColor: "Giorni passati", - deleteProjectConfirm: "Elimina progetto", - importConfirmReplace: "Questo eliminerà TUTTE le attività, liste e progetti esistenti. Continuare?", - importSuccess: "Importazione completata", - importInvalidJson: "File JSON non valido", - }, -}; - // Date utilities function getStartOfWeek(date: Date, startDay: number = 0): Date { const d = new Date(date); @@ -3294,6 +2127,8 @@ export default function WeeklyView() { setCookie(`setting_${key}`, String(value)); return; // Don't write to DB — that would overwrite other devices } + // Keep profile object in sync so the debounced auto-save never sends stale values + setProfile((p: any) => ({ ...p, [key]: value })); try { await fetch("/api/user/profile", { method: "PATCH", @@ -11386,204 +10221,6 @@ function ProjectsSidebar({ darkMode, language, projects, onProjectsChanged, onCl ); } -// 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: { 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; -} // Notes Sidebar Component interface NotesSidebarProps { task: Task; @@ -11725,4485 +10362,3 @@ function NotesSidebar({ task, onClose, updateTaskNotes }: NotesSidebarProps) { } -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" - >(initialTab || "general"); - const [isLoading, setIsLoading] = useState(true); - const [isSyncing, setIsSyncing] = useState(false); - const [exportStartDate, setExportStartDate] = useState(""); - const [exportEndDate, setExportEndDate] = useState(""); - 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: "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 === "custom" && ( -
- - { - const val = e.target.value; - setProfile({ ...profile, headerCustomFormat: val }); - saveSetting("headerCustomFormat", val); - }} - placeholder="KW WW | YYYY or DD.MM.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 (Week), YYYY (Year), MMMM (Month Name), MM (Month Num), DD (Day), [TODAY] (Active Date) -
-
- )} -
- - {/* 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" }} /> - -
- - {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)", - }} - /> - -
- ))} - -
-

- {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 === "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" - ? "Laden Sie eine CSV-Datei Ihrer erledigten Aufgaben herunter." - : "Download a CSV file of your completed tasks."} -

-
-
- - 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)", - }} - /> -
-
- - {profile.language === "de" - ? "Erledigte Aufgaben exportieren (CSV)" - : "Export Completed Tasks (CSV)"} - -
- - {/* 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() - } - /> -
-
- -
- - -
-
-
- )} -
-
- - ); -} diff --git a/src/lib/fontConstants.ts b/src/lib/fontConstants.ts new file mode 100644 index 0000000..9e3c472 --- /dev/null +++ b/src/lib/fontConstants.ts @@ -0,0 +1,28 @@ +// Font options shared between WeeklyView and SettingsSidebar + +export const AVAILABLE_FONTS = [ + { name: "Default (Inter)", value: "Inter" }, + { name: "Roboto", value: "Roboto" }, + { name: "Open Sans", value: "Open Sans" }, + { name: "Lato", value: "Lato" }, + { name: "Montserrat", value: "Montserrat" }, + { name: "Oswald", value: "Oswald" }, + { name: "Raleway", value: "Raleway" }, + { name: "Playfair Display", value: "Playfair Display" }, + { name: "Merriweather", value: "Merriweather" }, + { name: "Nunito", value: "Nunito" }, + { name: "Dancing Script", value: "Dancing Script" }, + { name: "Pacifico", value: "Pacifico" }, + { name: "Custom Google Font...", value: "__custom__" }, +]; + +// Check if a font value is a custom (non-preset) font +export const isCustomFont = (value: string): boolean => + !!value && value !== "__custom__" && !AVAILABLE_FONTS.slice(0, -1).some((f) => f.value === value); + +export const FONT_WEIGHTS = [ + { name: "Light", value: "300" }, + { name: "Normal", value: "400" }, + { name: "Medium", value: "500" }, + { name: "Bold", value: "700" }, +]; diff --git a/src/lib/weeklyViewConstants.ts b/src/lib/weeklyViewConstants.ts new file mode 100644 index 0000000..f448bf0 --- /dev/null +++ b/src/lib/weeklyViewConstants.ts @@ -0,0 +1,4 @@ +// Shared constants for WeeklyView and SettingsSidebar + +export type WeatherDisplayKey = "icon" | "temp" | "feelsLike" | "wind" | "gusts" | "precipProb" | "precip" | "humidity" | "uv"; +export const WEATHER_DISPLAY_DEFAULTS: WeatherDisplayKey[] = ["icon", "temp"]; diff --git a/src/lib/weeklyViewTranslations.ts b/src/lib/weeklyViewTranslations.ts new file mode 100644 index 0000000..353770c --- /dev/null +++ b/src/lib/weeklyViewTranslations.ts @@ -0,0 +1,1141 @@ +// Translations for WeeklyView and SettingsSidebar + +export const translations: Record = { + en: { + settings: "Settings", + general: "General", + calendar: "Connections", + localisation: "Localisation", + account: "Account", + runningList: "Running List (Auto-roll tasks to today)", + protectEventTimes: "Protect Event Times", + showTimeGrid: "Show Time Grid", + timeSlotDuration: "Time Slot Duration", + viewStyle: "View Style", + simpleView: "Simple", + calendarView: "Calendar", + listView: "List", + weekView: "Week", + kanbanView: "Kanban", + filterByProject: "All Projects", + filterByList: "All Lists", + filterByWeek: "All Weeks", + kanbanStages: "Kanban Stages", + kanbanStagesDesc: "Define the stages for your Kanban board. Drag tasks between columns to change their stage.", + addStage: "Add stage", + stageName: "Stage name", + noStage: "No stage", + headerDisplay: "Header Display", + headerDisplayKW: "Calendar Week (KW)", + headerDisplayMonth: "Month Name - March", + headerDisplayMonthYear: "Month & Year - March | 2026", + headerDisplayDate: "Full Date - 13.03.2026", + headerDisplayCustom: "Custom - Friday - 13. March", + headerDisplayNone: "None", + headerCustomFormatLabel: "Format string (e.g. DD.MM.YYYY)", + language: "Language", + dateFormat: "Date Format", + timeFormat: "Time Format", + saveChanges: "Save Changes", + connectedCalendars: "Connected Calendars", + connectMore: "Connect More", + connectGoogle: "Connect Google Calendar", + connectApple: "Connect Apple Calendar", + appleRemindersNote: "Apple Reminders are not supported. Since iOS 13 / macOS Catalina, Apple no longer provides a CalDAV or public API for Reminders. Only calendar events can be synced.", + connectSynology: "Connect Synology", + connectNotion: "Connect Notion", + noCalendars: "No calendars connected yet.", + dataPrivacy: "Data & Privacy", + downloadData: "Download My Data", + deleteAccount: "Delete Account", + name: "Name", + email: "Email", + timezone: "Timezone", + changePassword: "Change Password", + newPassword: "New Password", + confirmPassword: "Confirm Password", + someday: "SOMEDAY", + lists: "Lists", + newList: "New list", + allTabs: "All", + newTab: "New tab", + newTabName: "New tab name:", + assignTab: "Assign to tab", + noTab: "No tab", + renameTab: "Double-click to rename", + dissolveTab: "Remove tab (keep lists)", + loading: "Loading your tasks...", + sycing: "Syncing...", + synced: "Synced", + localization: "Localization", + allDayEvents: "ALL-DAY EVENTS", + syncCalendar: "Sync Calendar", + showProviderIcon: "Show provider icon on events", + toggleDarkMode: "Toggle Dark Mode", + signOut: "Sign Out", + startHour: "Start of Day", + endHour: "End of Day", + weekAbbr: "W", + goalOfWeek: "Goal of the Week", + goalScope: "Goal Scope", + goalScopeWeek: "Per Week", + goalScopeDay: "Per Day", + goalFallback: "Goal Fallback Type", + defaultGoal: "Custom Default Goal", + showTaskCheckboxes: "Show Checkboxes on Tasks", + showProjectIcons: "Show Icons for Projects", + showSomeday: "Show Someday Section", + showAllDay: "Show All-Day Section", + allDayPosition: "All-Day Events Position", + allDayAbove: "Above", + allDayBelow: "Below", + newPasswordDesc: "Leave blank to keep current password.", + dateAlignment: "Date Alignment", + dateVerticalAlign: "Date Vertical Alignment", + alignTop: "Top", + alignMiddle: "Middle", + alignBottom: "Bottom", + dateLayout: "Date Layout", + alignmentLeft: "Left", + alignmentCenter: "Center", + alignmentRight: "Right", + alignmentTight: "Tight", + backupRestore: "Backup & Restore", + backupRestoreDesc: "Export all your tasks, anyday lists, and projects as a JSON file. You can edit the file and import it back.", + exportAllData: "Export All Data (JSON)", + importData: "Import Data", + importMode: "Import Mode", + importModeMerge: "Merge", + importModeMergeDesc: "Add imported data alongside existing tasks", + importModeReplace: "Replace", + importModeReplaceDesc: "Delete all existing data and replace with imported data", + importReplaceWarning: "Warning: This will permanently delete all your current tasks, lists, and projects!", + importSelectFile: "Select JSON file...", + importButton: "Import", + importing: "Importing...", + exporting: "Exporting...", + projects: "Projects", + projectsDesc: "Organize tasks with color-coded projects", + addProject: "Add Project", + projectName: "Name", + projectColor: "Color", + noProjects: "No projects yet", + assignProject: "Assign project", + removeProject: "Remove project", + weekdayFormat: "Weekday Format", + weekdayFormatFull: "Full Name (Monday)", + weekdayFormatShort: "Short (Mon)", + weekdayFormatNarrow: "Narrow (M)", + weekdayFormatCustom: "Custom", + customWeekdayNamesMon: "Mo; Tu; We; Th; Fr; Sa; Su", + customWeekdayNamesSun: "Su; Mo; Tu; We; Th; Fr; Sa", + weekdayCase: "Weekday Case", + weekdayCaseNormal: "Normal (monday)", + weekdayCaseCapitalize: "Capitalize (Monday)", + weekdayCaseUppercase: "Uppercase (MONDAY)", + styling: "Styling", + motivation: "Motivation", + about: "About", + setupAssistant: "Run Setup Assistant", + calendarSync: "Sync", + calendarSyncTitle: "Calendar Sync", + calendarSyncDesc: "Sync events between your connected calendar providers.", + syncNow: "Sync Now", + syncing: "Syncing…", + syncResults: "Sync Results", + noRulesEnabled: "No enabled rules found.", + syncNeedsTwo: "You need at least two connected calendars to create a sync rule.", + noSyncRules: "No sync rules yet. Add one below.", + addSyncRule: "Add Sync Rule", + editSyncRule: "Edit Rule", + newSyncRule: "New Sync Rule", + syncRuleName: "Rule Name (optional)", + syncRuleNamePlaceholder: "e.g. Work → Personal", + syncDirection: "Direction", + oneWay: "One-way", + twoWay: "Two-way", + sourceCalendar: "Source Calendar", + targetCalendar: "Target Calendar", + titlePrefix: "Title Prefix (optional)", + titlePrefixPlaceholder: "e.g. [Work] ", + syncDescription: "Sync description", + syncLocation: "Sync location", + syncRecurring: "Include recurring events", + createRule: "Create Rule", + updateRule: "Update Rule", + weekStartLabel: "Start week on", + startViewLabel: "Start view on", + monday: "Monday", + sunday: "Sunday", + today: "Today", + yesterday: "Yesterday", + accountId: "Account ID", + accountIdDesc: "Your unique account identifier", + accountNumberLabel: "Account Number", + accountNumberDesc: "Your account number for identification when changing email", + connectOutlook: "Connect Outlook", + syncTasks: "Sync Tasks", + syncTasksDesc: "Sync tasks with Google Tasks or Microsoft To-Do.", + unsyncConfirmMsg: "Stop syncing \"{title}\"? Its tasks will be moved to trash.", + unsyncConfirm: "Stop syncing", + unsyncCancel: "Cancel", + syncAll: "Sync all", + unsyncAll: "Unsync all", + fetchingLists: "(fetching lists...)", + listHeader: "List", + syncHeader: "Sync", + noTaskListsFound: "No task lists found.", + connectProviderAbove: "Connect a provider above to sync task lists.", + noCalendarsFound: "No calendars found or permission denied.", + noCalendarsApple: "No calendars loaded. Please disconnect and reconnect Apple Calendar.", + noCalendarsSynology: "No calendars loaded. Please disconnect and reconnect Synology.", + selectionAfterConnect: "Selection available after connect.", + sharedCalendar: "Shared calendar", + primaryCalendar: "(Primary)", + fontCustomization: "Font Customization", + dateLayoutRight: "Date Right of Weekday", + dateLayoutLeft: "Date Left of Weekday", + dateLayoutAbove: "Date Above Weekday", + dateLayoutBelow: "Date Below Weekday", + dateLayoutHidden: "Date Hidden", + dateLayoutMobile: "Date Layout (Mobile)", + dayWeekdayGap: "Day / Weekday Gap", + weekdayFont: "Weekday Font", + dateFont: "Date Font", + taskFont: "Task Font", + eventFont: "Event Font", + goalFont: "Goal / Quote Font", + cwFont: "Calendar Week Font", + yearFont: "Year Font", + fontPlaceholder: "e.g. Poppins, Bebas Neue...", + fontSizePlaceholder: "Font size (e.g. 1.25rem)", + weightLight: "Light", + weightNormal: "Normal", + weightMedium: "Medium", + weightSemi: "Semi", + weightBold: "Bold", + weightBlack: "Black", + hourLabelFormat: "Hour Label Format", + hourLabelShort: "Short (8, 9, 10)", + hourLabelFull: "Full (8:00, 9:00, 10:00)", + showSubhourLabels: "Show Sub-hour Labels (:15, :30, :45)", + showScheduleCalendar: "Show Schedule / Calendar", + showDoThisNow: 'Show "Do This Now" instead of Motto', + focusTimer: "Focus Timer (min)", + focusBreak: "Focus Break (min)", + goalScopeTitle: "Goal Time Period", + goalFallbackTitle: "Goal Fallback", + motivationalQuote: "Motivational Quote / Holiday Hint", + nextTodo: "Next To-Do", + defaultText: "Default Text", + apiDataSources: "API Data Sources (URLs)", + addSource: "Add Source", + urlFormatHelp: "URL returning JSON quotes", + quoteLanguages: "Quote Languages", + quoteLanguagesDesc: "Choose which languages your quotes appear in. At least one must be selected.", + quoteFallbackDesc: "If no external source responds, curated local quotes in your language are used as fallback.", + defaultGoalPlaceholder: "Enter your goal here...", + saturdayColor: "Saturday", + sundayColor: "Sunday", + todayHighlight: "Today Highlight", + pastDayColor: "Past Day Color", + deleteProjectConfirm: "Delete project", + importConfirmReplace: "This will delete ALL existing tasks, lists, and projects. Continue?", + importSuccess: "Import complete", + importInvalidJson: "Invalid JSON file", + }, + de: { + settings: "Einstellungen", + general: "Allgemein", + calendar: "Verbindungen", + localisation: "Lokalisierung", + account: "Konto", + runningList: "Laufende Liste (Aufgaben automatisch auf heute verschieben)", + protectEventTimes: "Ereigniszeiten schützen", + showTimeGrid: "Zeitplan anzeigen", + timeSlotDuration: "Zeitfensterdauer", + viewStyle: "Ansichtsstil", + simpleView: "Einfach", + calendarView: "Kalender", + kanbanView: "Kanban", + weekView: "Woche", + filterByProject: "Alle Projekte", + filterByList: "Alle Listen", + filterByWeek: "Alle Wochen", + kanbanStages: "Kanban-Phasen", + kanbanStagesDesc: "Definiere die Phasen für dein Kanban-Board. Ziehe Aufgaben zwischen Spalten, um ihre Phase zu ändern.", + addStage: "Phase hinzufügen", + stageName: "Phasenname", + noStage: "Keine Phase", + headerDisplay: "Kopfzeile", + headerDisplayKW: "Kalenderwoche (KW)", + headerDisplayMonth: "Monatsname - März", + headerDisplayMonthYear: "Monat & Jahr - März | 2026", + headerDisplayDate: "Vollständiges Datum - 13.03.2026", + headerDisplayCustom: "Benutzerdefiniert - Freitag - 13. März", + headerDisplayNone: "Nichts", + headerCustomFormatLabel: "Format (z.B. DD.MM.YYYY)", + listView: "Liste", + notes: "Notizen", + notesSidebar: "Notizen-Seitenleiste", + language: "Sprache", + dateFormat: "Datumsformat", + timeFormat: "Zeitformat", + saveChanges: "Änderungen speichern", + connectedCalendars: "Verbundene Kalender", + connectMore: "Mehr verbinden", + connectGoogle: "Google Kalender verbinden", + connectApple: "Apple Kalender verbinden", + appleRemindersNote: "Apple Erinnerungen werden nicht unterstützt. Seit iOS 13 / macOS Catalina bietet Apple keine CalDAV- oder öffentliche API mehr für Erinnerungen an. Nur Kalender-Ereignisse können synchronisiert werden.", + connectNotion: "Notion verbinden", + noCalendars: "Keine Kalender verbunden.", + dataPrivacy: "Daten & Datenschutz", + downloadData: "Meine Daten herunterladen", + deleteAccount: "Konto löschen", + name: "Name", + email: "E-Mail", + timezone: "Zeitzone", + changePassword: "Passwort ändern", + newPassword: "Neues Passwort", + confirmPassword: "Passwort bestätigen", + someday: "IRGENDWANN", + lists: "Listen", + newList: "Neue Liste", + allTabs: "Alle", + newTab: "Neuer Tab", + newTabName: "Neuer Tab-Name:", + assignTab: "Tab zuweisen", + noTab: "Kein Tab", + renameTab: "Doppelklick zum Umbenennen", + dissolveTab: "Tab entfernen (Listen behalten)", + loading: "Lade Aufgaben...", + syncing: "Synchronisiere...", + synced: "Synchronisiert", + localization: "Lokalisierung", + allDayEvents: "GANZTÄGIGE EREIGNISSE", + syncCalendar: "Kalender synchronisieren", + showProviderIcon: "Anbieter-Icon auf Terminen anzeigen", + toggleDarkMode: "Dunkelmodus umschalten", + signOut: "Abmelden", + startHour: "Tagesbeginn", + endHour: "Tagesende", + weekAbbr: "KW", + goalOfWeek: "Ziel der Woche", + goalScope: "Ziel-Zeitraum", + goalScopeWeek: "Pro Woche", + goalScopeDay: "Pro Tag", + goalFallback: "Ziel-Fallback-Typ", + defaultGoal: "Benutzerdefiniertes Standardziel", + showTaskCheckboxes: "Checkboxen bei Aufgaben anzeigen", + showProjectIcons: "Icons für Projekte anzeigen", + showSomeday: "Irgendwann-Bereich anzeigen", + showAllDay: "Ganztägige Ereignisse anzeigen", + allDayPosition: "Position ganztägiger Ereignisse", + allDayAbove: "Oben", + allDayBelow: "Unten", + newPasswordDesc: "Leer lassen, um das aktuelle Passwort zu behalten.", + dateAlignment: "Datums-Ausrichtung", + dateVerticalAlign: "Datums-Vertikalausrichtung", + alignTop: "Oben", + alignMiddle: "Mitte", + alignBottom: "Unten", + dateLayout: "Datumslayout", + alignmentLeft: "Links", + alignmentCenter: "Mitte", + alignmentRight: "Rechts", + alignmentTight: "Eng", + backupRestore: "Sicherung & Wiederherstellung", + backupRestoreDesc: "Exportieren Sie alle Aufgaben, Irgendwann-Listen und Projekte als JSON-Datei. Sie können die Datei bearbeiten und wieder importieren.", + exportAllData: "Alle Daten exportieren (JSON)", + importData: "Daten importieren", + importMode: "Import-Modus", + importModeMerge: "Zusammenführen", + importModeMergeDesc: "Importierte Daten neben bestehenden Aufgaben hinzufügen", + importModeReplace: "Ersetzen", + importModeReplaceDesc: "Alle bestehenden Daten löschen und durch importierte ersetzen", + importReplaceWarning: "Warnung: Dies löscht dauerhaft alle Ihre aktuellen Aufgaben, Listen und Projekte!", + importSelectFile: "JSON-Datei auswählen...", + importButton: "Importieren", + importing: "Importiere...", + exporting: "Exportiere...", + projects: "Projekte", + projectsDesc: "Aufgaben mit farbcodierten Projekten organisieren", + addProject: "Projekt hinzufügen", + projectName: "Name", + projectColor: "Farbe", + noProjects: "Noch keine Projekte", + assignProject: "Projekt zuweisen", + removeProject: "Projekt entfernen", + weekdayFormat: "Wochentag-Format", + weekdayFormatFull: "Vollständiger Name (Montag)", + weekdayFormatShort: "Kurz (Mo)", + weekdayFormatNarrow: "Schmal (M)", + weekdayFormatCustom: "Benutzerdefiniert", + customWeekdayNamesMon: "Mo; Di; Mi; Do; Fr; Sa; So", + customWeekdayNamesSun: "So; Mo; Di; Mi; Do; Fr; Sa", + weekdayCase: "Groß-/Kleinschreibung", + weekdayCaseNormal: "Klein (montag)", + weekdayCaseCapitalize: "Großbuchstabe (Montag)", + weekdayCaseUppercase: "Großbuchstaben (MONTAG)", + styling: "Design", + motivation: "Motivation", + about: "Über", + setupAssistant: "Einrichtungsassistent starten", + calendarSync: "Sync", + calendarSyncTitle: "Kalender-Synchronisation", + calendarSyncDesc: "Ereignisse zwischen verbundenen Kalender-Anbietern synchronisieren.", + syncNow: "Jetzt synchronisieren", + syncResults: "Sync-Ergebnis", + noRulesEnabled: "Keine aktiven Regeln gefunden.", + syncNeedsTwo: "Du benötigst mindestens zwei verbundene Kalender für eine Sync-Regel.", + noSyncRules: "Noch keine Sync-Regeln. Füge eine unten hinzu.", + addSyncRule: "Regel hinzufügen", + editSyncRule: "Regel bearbeiten", + newSyncRule: "Neue Sync-Regel", + syncRuleName: "Regelname (optional)", + syncRuleNamePlaceholder: "z.B. Arbeit → Privat", + syncDirection: "Richtung", + oneWay: "Einseitig", + twoWay: "Beidseitig", + sourceCalendar: "Quellkalender", + targetCalendar: "Zielkalender", + titlePrefix: "Titel-Präfix (optional)", + titlePrefixPlaceholder: "z.B. [Arbeit] ", + syncDescription: "Beschreibung synchronisieren", + syncLocation: "Ort synchronisieren", + syncRecurring: "Wiederkehrende Ereignisse einschließen", + createRule: "Regel erstellen", + updateRule: "Regel aktualisieren", + weekStartLabel: "Woche beginnt am", + startViewLabel: "Ansicht beginnt mit", + monday: "Montag", + sunday: "Sonntag", + today: "Heute", + yesterday: "Gestern", + accountId: "Konto-ID", + accountIdDesc: "Ihre eindeutige Konto-Kennung", + accountNumberLabel: "Kontonummer", + accountNumberDesc: "Ihre Kontonummer zur Identifikation", + connectOutlook: "Outlook verbinden", + syncTasks: "Aufgaben synchronisieren", + syncTasksDesc: "Aufgaben mit Google Tasks oder Microsoft To-Do synchronisieren.", + unsyncConfirmMsg: "Synchronisierung von \"{title}\" beenden? Die Aufgaben werden in den Papierkorb verschoben.", + unsyncConfirm: "Sync beenden", + unsyncCancel: "Abbrechen", + syncAll: "Alle synchronisieren", + unsyncAll: "Alle trennen", + fetchingLists: "(Listen werden geladen...)", + listHeader: "Liste", + syncHeader: "Sync", + noTaskListsFound: "Keine Aufgabenlisten gefunden.", + connectProviderAbove: "Verbinden Sie einen Anbieter oben, um Aufgabenlisten zu synchronisieren.", + noCalendarsFound: "Keine Kalender gefunden oder Zugriff verweigert.", + noCalendarsApple: "Keine Kalender geladen. Bitte trennen und erneut verbinden.", + noCalendarsSynology: "Keine Kalender geladen. Bitte trennen und erneut verbinden.", + selectionAfterConnect: "Auswahl nach Verbindung verfügbar.", + sharedCalendar: "Geteilter Kalender", + primaryCalendar: "(Primär)", + fontCustomization: "Schriftart-Anpassung", + dateLayoutRight: "Datum rechts vom Wochentag", + dateLayoutLeft: "Datum links vom Wochentag", + dateLayoutAbove: "Datum über Wochentag", + dateLayoutBelow: "Datum unter Wochentag", + dateLayoutHidden: "Datum ausgeblendet", + dateLayoutMobile: "Datum-Layout (Mobil)", + dayWeekdayGap: "Tag / Wochentag Abstand", + weekdayFont: "Wochentag-Schrift", + dateFont: "Datum-Schrift", + taskFont: "Aufgaben-Schrift", + eventFont: "Termin-Schrift", + goalFont: "Ziel / Zitat-Schrift", + cwFont: "Kalenderwoche-Schrift", + yearFont: "Jahr-Schrift", + fontPlaceholder: "z.B. Poppins, Bebas Neue...", + fontSizePlaceholder: "Schriftgröße (z.B. 1.25rem)", + weightLight: "Leicht", + weightNormal: "Normal", + weightMedium: "Mittel", + weightSemi: "Halb-fett", + weightBold: "Fett", + weightBlack: "Schwarz", + hourLabelFormat: "Stundenformat", + hourLabelShort: "Kurz (8, 9, 10)", + hourLabelFull: "Voll (8:00, 9:00, 10:00)", + showSubhourLabels: "Viertelstunden anzeigen (:15, :30, :45)", + showScheduleCalendar: "Zeitplan / Kalender anzeigen", + showDoThisNow: '"Jetzt erledigen" statt Motto anzeigen', + focusTimer: "Fokus-Timer (Min)", + focusBreak: "Fokus-Pause (Min)", + goalScopeTitle: "Ziel-Zeitraum", + goalFallbackTitle: "Ziel-Fallback", + motivationalQuote: "Motivationszitat / Feiertags-Hinweis", + nextTodo: "Nächstes To-Do", + defaultText: "Standardtext", + apiDataSources: "API-Datenquellen (URLs)", + addSource: "Quelle hinzufügen", + urlFormatHelp: "URL die JSON-Zitate liefert", + quoteLanguages: "Zitatsprachen", + quoteLanguagesDesc: "Wählen Sie die Sprachen für Ihre Zitate. Mindestens eine muss ausgewählt sein.", + quoteFallbackDesc: "Wenn keine externe Quelle antwortet, werden lokale kuratierte Zitate in Ihrer Sprache verwendet.", + defaultGoalPlaceholder: "Ihr Ziel hier eingeben...", + saturdayColor: "Samstag", + sundayColor: "Sonntag", + todayHighlight: "Heute-Hervorhebung", + pastDayColor: "Vergangene Tage", + deleteProjectConfirm: "Projekt löschen", + importConfirmReplace: "Dies löscht ALLE bestehenden Aufgaben, Listen und Projekte. Fortfahren?", + importSuccess: "Import abgeschlossen", + importInvalidJson: "Ungültige JSON-Datei", + }, + fr: { + settings: "Paramètres", + general: "Général", + calendar: "Connexions", + localisation: "Localisation", + account: "Compte", + runningList: "Liste continue (reporter les tâches à aujourd'hui)", + protectEventTimes: "Protéger les horaires des événements", + showTimeGrid: "Afficher la grille horaire", + timeSlotDuration: "Durée des créneaux horaires", + viewStyle: "Style d'affichage", + simpleView: "Simple", + calendarView: "Calendrier", + listView: "Liste", + weekView: "Semaine", + kanbanView: "Kanban", + filterByProject: "Tous les projets", + filterByList: "Toutes les listes", + filterByWeek: "Toutes les semaines", + kanbanStages: "Étapes Kanban", + kanbanStagesDesc: "Définissez les étapes de votre tableau Kanban. Glissez les tâches entre les colonnes pour changer leur étape.", + addStage: "Ajouter une étape", + stageName: "Nom de l'étape", + noStage: "Aucune étape", + headerDisplay: "Affichage en-tête", + headerDisplayKW: "Semaine calendaire (KW)", + headerDisplayMonth: "Nom du mois - Mars", + headerDisplayMonthYear: "Mois & Année - Mars | 2026", + headerDisplayDate: "Date complète - 13.03.2026", + headerDisplayCustom: "Personnalisé - Vendredi - 13 Mars", + headerDisplayNone: "Aucun", + headerCustomFormatLabel: "Format (ex: DD.MM.YYYY)", + language: "Langue", + dateFormat: "Format de date", + timeFormat: "Format d'heure", + saveChanges: "Enregistrer", + connectedCalendars: "Calendriers connectés", + connectMore: "En connecter d'autres", + connectGoogle: "Connecter Google Agenda", + connectApple: "Connecter le calendrier Apple", + appleRemindersNote: "Les rappels Apple ne sont pas pris en charge. Depuis iOS 13 / macOS Catalina, Apple ne fournit plus de CalDAV ni d'API publique pour les rappels. Seuls les événements de calendrier peuvent être synchronisés.", + connectSynology: "Connecter Synology", + connectNotion: "Connecter Notion", + noCalendars: "Aucun calendrier connecté.", + dataPrivacy: "Données et confidentialité", + downloadData: "Télécharger mes données", + deleteAccount: "Supprimer le compte", + name: "Nom", + email: "E-mail", + timezone: "Fuseau horaire", + changePassword: "Changer le mot de passe", + newPassword: "Nouveau mot de passe", + confirmPassword: "Confirmer le mot de passe", + someday: "UN JOUR", + lists: "Listes", + newList: "Nouvelle liste", + allTabs: "Tous", + newTab: "Nouvel onglet", + newTabName: "Nom du nouvel onglet :", + assignTab: "Assigner à un onglet", + noTab: "Aucun onglet", + renameTab: "Double-cliquez pour renommer", + dissolveTab: "Supprimer l'onglet (garder les listes)", + loading: "Chargement de vos tâches…", + sycing: "Synchronisation…", + synced: "Synchronisé", + localization: "Localisation", + allDayEvents: "ÉVÉNEMENTS JOURNÉE ENTIÈRE", + syncCalendar: "Synchroniser le calendrier", + showProviderIcon: "Afficher l'icône du fournisseur sur les événements", + toggleDarkMode: "Basculer le mode sombre", + signOut: "Se déconnecter", + startHour: "Début de journée", + endHour: "Fin de journée", + weekAbbr: "S", + goalOfWeek: "Objectif de la semaine", + goalScope: "Portée de l'objectif", + goalScopeWeek: "Par semaine", + goalScopeDay: "Par jour", + goalFallback: "Type d'objectif par défaut", + defaultGoal: "Objectif par défaut personnalisé", + showTaskCheckboxes: "Afficher les cases à cocher", + showProjectIcons: "Afficher les icônes de projets", + showSomeday: "Afficher la section Un jour", + showAllDay: "Afficher la section Journée entière", + allDayPosition: "Position des événements journée entière", + allDayAbove: "Au-dessus", + allDayBelow: "En dessous", + newPasswordDesc: "Laisser vide pour conserver le mot de passe actuel.", + dateAlignment: "Alignement de la date", + dateVerticalAlign: "Alignement vertical de la date", + alignTop: "Haut", + alignMiddle: "Milieu", + alignBottom: "Bas", + dateLayout: "Disposition de la date", + alignmentLeft: "Gauche", + alignmentCenter: "Centre", + alignmentRight: "Droite", + alignmentTight: "Compact", + backupRestore: "Sauvegarde et restauration", + backupRestoreDesc: "Exportez toutes vos tâches, listes et projets au format JSON. Vous pouvez modifier le fichier et le réimporter.", + exportAllData: "Exporter toutes les données (JSON)", + importData: "Importer des données", + importMode: "Mode d'importation", + importModeMerge: "Fusionner", + importModeMergeDesc: "Ajouter les données importées aux tâches existantes", + importModeReplace: "Remplacer", + importModeReplaceDesc: "Supprimer toutes les données existantes et les remplacer par les données importées", + importReplaceWarning: "Attention : toutes vos tâches, listes et projets actuels seront définitivement supprimés !", + importSelectFile: "Sélectionner un fichier JSON…", + importButton: "Importer", + importing: "Importation…", + exporting: "Exportation…", + projects: "Projets", + projectsDesc: "Organisez vos tâches avec des projets colorés", + addProject: "Ajouter un projet", + projectName: "Nom", + projectColor: "Couleur", + noProjects: "Aucun projet", + assignProject: "Attribuer un projet", + removeProject: "Retirer le projet", + weekdayFormat: "Format des jours", + weekdayFormatFull: "Nom complet (lundi)", + weekdayFormatShort: "Abrégé (lun.)", + weekdayFormatNarrow: "Étroit (L)", + weekdayFormatCustom: "Personnalisé", + customWeekdayNamesMon: "Lu; Ma; Me; Je; Ve; Sa; Di", + customWeekdayNamesSun: "Di; Lu; Ma; Me; Je; Ve; Sa", + weekdayCase: "Casse des jours", + weekdayCaseNormal: "Normal (lundi)", + weekdayCaseCapitalize: "Majuscule (Lundi)", + weekdayCaseUppercase: "Majuscules (LUNDI)", + styling: "Style", + motivation: "Motivation", + about: "À propos", + setupAssistant: "Lancer l'assistant de configuration", + weekStartLabel: "La semaine commence le", + startViewLabel: "Vue commence par", + monday: "Lundi", + sunday: "Dimanche", + today: "Aujourd'hui", + yesterday: "Hier", + accountId: "ID du compte", + accountIdDesc: "Votre identifiant de compte unique", + accountNumberLabel: "Numéro de compte", + accountNumberDesc: "Votre numéro de compte pour identification", + connectOutlook: "Connecter Outlook", + syncTasks: "Synchroniser les tâches", + syncTasksDesc: "Synchronisez les tâches avec Google Tasks ou Microsoft To-Do.", + unsyncConfirmMsg: "Arrêter la synchronisation de \"{title}\" ? Ses tâches seront mises à la corbeille.", + unsyncConfirm: "Arrêter la sync", + unsyncCancel: "Annuler", + syncAll: "Tout synchroniser", + unsyncAll: "Tout désynchroniser", + fetchingLists: "(chargement des listes...)", + listHeader: "Liste", + syncHeader: "Sync", + noTaskListsFound: "Aucune liste de tâches trouvée.", + connectProviderAbove: "Connectez un fournisseur ci-dessus pour synchroniser les listes.", + noCalendarsFound: "Aucun calendrier trouvé ou accès refusé.", + noCalendarsApple: "Aucun calendrier chargé. Veuillez déconnecter et reconnecter.", + noCalendarsSynology: "Aucun calendrier chargé. Veuillez déconnecter et reconnecter.", + selectionAfterConnect: "Sélection disponible après connexion.", + sharedCalendar: "Calendrier partagé", + primaryCalendar: "(Principal)", + fontCustomization: "Personnalisation des polices", + dateLayoutRight: "Date à droite du jour", + dateLayoutLeft: "Date à gauche du jour", + dateLayoutAbove: "Date au-dessus du jour", + dateLayoutBelow: "Date en dessous du jour", + dateLayoutHidden: "Date masquée", + dateLayoutMobile: "Disposition date (mobile)", + dayWeekdayGap: "Espacement jour / semaine", + weekdayFont: "Police du jour", + dateFont: "Police de la date", + taskFont: "Police des tâches", + eventFont: "Police des événements", + goalFont: "Police objectif / citation", + cwFont: "Police semaine calendaire", + yearFont: "Police de l'année", + fontPlaceholder: "ex. Poppins, Bebas Neue...", + fontSizePlaceholder: "Taille (ex. 1.25rem)", + weightLight: "Léger", + weightNormal: "Normal", + weightMedium: "Moyen", + weightSemi: "Semi-gras", + weightBold: "Gras", + weightBlack: "Noir", + hourLabelFormat: "Format des heures", + hourLabelShort: "Court (8, 9, 10)", + hourLabelFull: "Complet (8:00, 9:00, 10:00)", + showSubhourLabels: "Afficher les quarts d'heure (:15, :30, :45)", + showScheduleCalendar: "Afficher le calendrier", + showDoThisNow: '"Faire maintenant" au lieu de la devise', + focusTimer: "Minuteur Focus (min)", + focusBreak: "Pause Focus (min)", + goalScopeTitle: "Période de l'objectif", + goalFallbackTitle: "Fallback objectif", + motivationalQuote: "Citation motivante / info jour férié", + nextTodo: "Prochaine tâche", + defaultText: "Texte par défaut", + apiDataSources: "Sources de données API (URLs)", + addSource: "Ajouter une source", + urlFormatHelp: "URL retournant des citations JSON", + quoteLanguages: "Langues des citations", + quoteLanguagesDesc: "Choisissez les langues de vos citations. Au moins une doit être sélectionnée.", + quoteFallbackDesc: "Si aucune source externe ne répond, des citations locales dans votre langue sont utilisées.", + defaultGoalPlaceholder: "Entrez votre objectif ici...", + saturdayColor: "Samedi", + sundayColor: "Dimanche", + todayHighlight: "Surbrillance aujourd'hui", + pastDayColor: "Jours passés", + deleteProjectConfirm: "Supprimer le projet", + importConfirmReplace: "Cela supprimera TOUTES les tâches, listes et projets existants. Continuer ?", + importSuccess: "Import terminé", + importInvalidJson: "Fichier JSON invalide", + }, + es: { + settings: "Ajustes", + general: "General", + calendar: "Conexiones", + localisation: "Localización", + account: "Cuenta", + runningList: "Lista continua (pasar tareas a hoy)", + protectEventTimes: "Proteger horarios de eventos", + showTimeGrid: "Mostrar cuadrícula horaria", + timeSlotDuration: "Duración de los intervalos", + viewStyle: "Estilo de vista", + simpleView: "Simple", + calendarView: "Calendario", + listView: "Lista", + weekView: "Semana", + kanbanView: "Kanban", + filterByProject: "Todos los proyectos", + filterByList: "Todas las listas", + filterByWeek: "Todas las semanas", + kanbanStages: "Etapas Kanban", + kanbanStagesDesc: "Define las etapas de tu tablero Kanban. Arrastra tareas entre columnas para cambiar su etapa.", + addStage: "Añadir etapa", + stageName: "Nombre de etapa", + noStage: "Sin etapa", + headerDisplay: "Visualización de encabezado", + headerDisplayKW: "Semana calendario (KW)", + headerDisplayMonth: "Nombre del mes - Marzo", + headerDisplayMonthYear: "Mes y Año - Marzo | 2026", + headerDisplayDate: "Fecha completa - 13.03.2026", + headerDisplayCustom: "Personalizado - Viernes - 13 Marzo", + headerDisplayNone: "Ninguno", + headerCustomFormatLabel: "Formato (ej. DD.MM.YYYY)", + language: "Idioma", + dateFormat: "Formato de fecha", + timeFormat: "Formato de hora", + saveChanges: "Guardar cambios", + connectedCalendars: "Calendarios conectados", + connectMore: "Conectar más", + connectGoogle: "Conectar Google Calendar", + connectApple: "Conectar calendario de Apple", + appleRemindersNote: "Los recordatorios de Apple no son compatibles. Desde iOS 13 / macOS Catalina, Apple ya no ofrece CalDAV ni una API pública para recordatorios. Solo se pueden sincronizar eventos del calendario.", + connectSynology: "Conectar Synology", + connectNotion: "Conectar Notion", + noCalendars: "No hay calendarios conectados.", + dataPrivacy: "Datos y privacidad", + downloadData: "Descargar mis datos", + deleteAccount: "Eliminar cuenta", + name: "Nombre", + email: "Correo electrónico", + timezone: "Zona horaria", + changePassword: "Cambiar contraseña", + newPassword: "Nueva contraseña", + confirmPassword: "Confirmar contraseña", + someday: "ALGÚN DÍA", + lists: "Listas", + newList: "Nueva lista", + allTabs: "Todas", + newTab: "Nueva pestaña", + newTabName: "Nombre de nueva pestaña:", + assignTab: "Asignar a pestaña", + noTab: "Sin pestaña", + renameTab: "Doble clic para renombrar", + dissolveTab: "Eliminar pestaña (mantener listas)", + loading: "Cargando tus tareas…", + sycing: "Sincronizando…", + synced: "Sincronizado", + localization: "Localización", + allDayEvents: "EVENTOS DE TODO EL DÍA", + syncCalendar: "Sincronizar calendario", + showProviderIcon: "Mostrar icono del proveedor en eventos", + toggleDarkMode: "Alternar modo oscuro", + signOut: "Cerrar sesión", + startHour: "Inicio del día", + endHour: "Fin del día", + weekAbbr: "S", + goalOfWeek: "Objetivo de la semana", + goalScope: "Alcance del objetivo", + goalScopeWeek: "Por semana", + goalScopeDay: "Por día", + goalFallback: "Tipo de objetivo por defecto", + defaultGoal: "Objetivo predeterminado personalizado", + showTaskCheckboxes: "Mostrar casillas en las tareas", + showProjectIcons: "Mostrar iconos de proyectos", + showSomeday: "Mostrar sección Algún día", + showAllDay: "Mostrar sección Todo el día", + allDayPosition: "Posición de eventos de todo el día", + allDayAbove: "Arriba", + allDayBelow: "Abajo", + newPasswordDesc: "Dejar en blanco para conservar la contraseña actual.", + dateAlignment: "Alineación de la fecha", + dateVerticalAlign: "Alineación vertical de la fecha", + alignTop: "Arriba", + alignMiddle: "Centro", + alignBottom: "Abajo", + dateLayout: "Disposición de la fecha", + alignmentLeft: "Izquierda", + alignmentCenter: "Centro", + alignmentRight: "Derecha", + alignmentTight: "Compacto", + backupRestore: "Copia de seguridad y restauración", + backupRestoreDesc: "Exporta todas tus tareas, listas y proyectos como archivo JSON. Puedes editar el archivo y volver a importarlo.", + exportAllData: "Exportar todos los datos (JSON)", + importData: "Importar datos", + importMode: "Modo de importación", + importModeMerge: "Combinar", + importModeMergeDesc: "Añadir los datos importados junto a las tareas existentes", + importModeReplace: "Reemplazar", + importModeReplaceDesc: "Eliminar todos los datos existentes y reemplazarlos con los datos importados", + importReplaceWarning: "Advertencia: ¡Se eliminarán permanentemente todas tus tareas, listas y proyectos actuales!", + importSelectFile: "Seleccionar archivo JSON…", + importButton: "Importar", + importing: "Importando…", + exporting: "Exportando…", + projects: "Proyectos", + projectsDesc: "Organiza las tareas con proyectos de colores", + addProject: "Añadir proyecto", + projectName: "Nombre", + projectColor: "Color", + noProjects: "Aún no hay proyectos", + assignProject: "Asignar proyecto", + removeProject: "Quitar proyecto", + weekdayFormat: "Formato de los días", + weekdayFormatFull: "Nombre completo (lunes)", + weekdayFormatShort: "Abreviado (lun.)", + weekdayFormatNarrow: "Estrecho (L)", + weekdayFormatCustom: "Personalizado", + customWeekdayNamesMon: "Lu; Ma; Mi; Ju; Vi; Sá; Do", + customWeekdayNamesSun: "Do; Lu; Ma; Mi; Ju; Vi; Sá", + weekdayCase: "Mayúsculas de los días", + weekdayCaseNormal: "Normal (lunes)", + weekdayCaseCapitalize: "Mayúscula inicial (Lunes)", + weekdayCaseUppercase: "Mayúsculas (LUNES)", + styling: "Estilo", + motivation: "Motivación", + about: "Acerca de", + setupAssistant: "Iniciar asistente de configuración", + weekStartLabel: "La semana empieza el", + startViewLabel: "Vista empieza con", + monday: "Lunes", + sunday: "Domingo", + today: "Hoy", + yesterday: "Ayer", + accountId: "ID de cuenta", + accountIdDesc: "Tu identificador único de cuenta", + accountNumberLabel: "Número de cuenta", + accountNumberDesc: "Tu número de cuenta para identificación", + connectOutlook: "Conectar Outlook", + syncTasks: "Sincronizar tareas", + syncTasksDesc: "Sincroniza tareas con Google Tasks o Microsoft To-Do.", + unsyncConfirmMsg: "¿Dejar de sincronizar \"{title}\"? Sus tareas se moverán a la papelera.", + unsyncConfirm: "Dejar de sincronizar", + unsyncCancel: "Cancelar", + syncAll: "Sincronizar todo", + unsyncAll: "Desincronizar todo", + fetchingLists: "(cargando listas...)", + listHeader: "Lista", + syncHeader: "Sync", + noTaskListsFound: "No se encontraron listas de tareas.", + connectProviderAbove: "Conecta un proveedor arriba para sincronizar listas.", + noCalendarsFound: "No se encontraron calendarios o acceso denegado.", + noCalendarsApple: "No hay calendarios cargados. Desconecta y reconecta.", + noCalendarsSynology: "No hay calendarios cargados. Desconecta y reconecta.", + selectionAfterConnect: "Selección disponible tras conectar.", + sharedCalendar: "Calendario compartido", + primaryCalendar: "(Principal)", + fontCustomization: "Personalización de fuentes", + dateLayoutRight: "Fecha a la derecha del día", + dateLayoutLeft: "Fecha a la izquierda del día", + dateLayoutAbove: "Fecha encima del día", + dateLayoutBelow: "Fecha debajo del día", + dateLayoutHidden: "Fecha oculta", + dateLayoutMobile: "Disposición fecha (móvil)", + dayWeekdayGap: "Espacio día / semana", + weekdayFont: "Fuente del día", + dateFont: "Fuente de la fecha", + taskFont: "Fuente de tareas", + eventFont: "Fuente de eventos", + goalFont: "Fuente objetivo / cita", + cwFont: "Fuente semana calendario", + yearFont: "Fuente del año", + fontPlaceholder: "ej. Poppins, Bebas Neue...", + fontSizePlaceholder: "Tamaño (ej. 1.25rem)", + weightLight: "Ligero", + weightNormal: "Normal", + weightMedium: "Medio", + weightSemi: "Semi-negrita", + weightBold: "Negrita", + weightBlack: "Negro", + hourLabelFormat: "Formato de horas", + hourLabelShort: "Corto (8, 9, 10)", + hourLabelFull: "Completo (8:00, 9:00, 10:00)", + showSubhourLabels: "Mostrar cuartos de hora (:15, :30, :45)", + showScheduleCalendar: "Mostrar calendario", + showDoThisNow: '"Hacer ahora" en vez de lema', + focusTimer: "Temporizador Focus (min)", + focusBreak: "Pausa Focus (min)", + goalScopeTitle: "Periodo del objetivo", + goalFallbackTitle: "Fallback del objetivo", + motivationalQuote: "Cita motivacional / festivo", + nextTodo: "Siguiente tarea", + defaultText: "Texto predeterminado", + apiDataSources: "Fuentes de datos API (URLs)", + addSource: "Añadir fuente", + urlFormatHelp: "URL que devuelve citas JSON", + quoteLanguages: "Idiomas de citas", + quoteLanguagesDesc: "Elige los idiomas de tus citas. Al menos uno debe estar seleccionado.", + quoteFallbackDesc: "Si ninguna fuente externa responde, se usan citas locales en tu idioma.", + defaultGoalPlaceholder: "Ingresa tu objetivo aquí...", + saturdayColor: "Sábado", + sundayColor: "Domingo", + todayHighlight: "Resaltado de hoy", + pastDayColor: "Días pasados", + deleteProjectConfirm: "Eliminar proyecto", + importConfirmReplace: "Esto eliminará TODAS las tareas, listas y proyectos existentes. ¿Continuar?", + importSuccess: "Importación completada", + importInvalidJson: "Archivo JSON inválido", + }, + it: { + settings: "Impostazioni", + general: "Generali", + calendar: "Connessioni", + localisation: "Localizzazione", + account: "Account", + runningList: "Lista continua (sposta le attività a oggi)", + protectEventTimes: "Proteggi gli orari degli eventi", + showTimeGrid: "Mostra griglia oraria", + timeSlotDuration: "Durata degli intervalli", + viewStyle: "Stile di visualizzazione", + simpleView: "Semplice", + calendarView: "Calendario", + listView: "Lista", + weekView: "Settimana", + kanbanView: "Kanban", + filterByProject: "Tutti i progetti", + filterByList: "Tutte le liste", + filterByWeek: "Tutte le settimane", + kanbanStages: "Fasi Kanban", + kanbanStagesDesc: "Definisci le fasi della tua board Kanban. Trascina le attività tra le colonne per cambiare la loro fase.", + addStage: "Aggiungi fase", + stageName: "Nome fase", + noStage: "Nessuna fase", + headerDisplay: "Visualizzazione intestazione", + headerDisplayKW: "Settimana calendario (KW)", + headerDisplayMonth: "Nome del mese - Marzo", + headerDisplayMonthYear: "Mese e Anno - Marzo | 2026", + headerDisplayDate: "Data completa - 13.03.2026", + headerDisplayCustom: "Personalizzato - Venerdì - 13 Marzo", + headerDisplayNone: "Nessuno", + headerCustomFormatLabel: "Formato (es. DD.MM.YYYY)", + language: "Lingua", + dateFormat: "Formato data", + timeFormat: "Formato ora", + saveChanges: "Salva modifiche", + connectedCalendars: "Calendari collegati", + connectMore: "Collega altri", + connectGoogle: "Collega Google Calendar", + connectApple: "Collega il calendario Apple", + appleRemindersNote: "I promemoria Apple non sono supportati. Da iOS 13 / macOS Catalina, Apple non fornisce più CalDAV o un'API pubblica per i promemoria. Solo gli eventi del calendario possono essere sincronizzati.", + connectSynology: "Collega Synology", + connectNotion: "Collega Notion", + noCalendars: "Nessun calendario collegato.", + dataPrivacy: "Dati e privacy", + downloadData: "Scarica i miei dati", + deleteAccount: "Elimina account", + name: "Nome", + email: "E-mail", + timezone: "Fuso orario", + changePassword: "Cambia password", + newPassword: "Nuova password", + confirmPassword: "Conferma password", + someday: "UN GIORNO", + lists: "Liste", + newList: "Nuova lista", + allTabs: "Tutte", + newTab: "Nuova scheda", + newTabName: "Nome nuova scheda:", + assignTab: "Assegna a scheda", + noTab: "Nessuna scheda", + renameTab: "Doppio clic per rinominare", + dissolveTab: "Rimuovi scheda (mantieni liste)", + loading: "Caricamento delle attività…", + sycing: "Sincronizzazione…", + synced: "Sincronizzato", + localization: "Localizzazione", + allDayEvents: "EVENTI GIORNATA INTERA", + syncCalendar: "Sincronizza calendario", + showProviderIcon: "Mostra icona del provider sugli eventi", + toggleDarkMode: "Attiva/disattiva modalità scura", + signOut: "Esci", + startHour: "Inizio giornata", + endHour: "Fine giornata", + weekAbbr: "S", + goalOfWeek: "Obiettivo della settimana", + goalScope: "Ambito dell'obiettivo", + goalScopeWeek: "Per settimana", + goalScopeDay: "Per giorno", + goalFallback: "Tipo di obiettivo predefinito", + defaultGoal: "Obiettivo predefinito personalizzato", + showTaskCheckboxes: "Mostra caselle di spunta sulle attività", + showProjectIcons: "Mostra icone per i progetti", + showSomeday: "Mostra sezione Un giorno", + showAllDay: "Mostra sezione Giornata intera", + allDayPosition: "Posizione eventi giornata intera", + allDayAbove: "Sopra", + allDayBelow: "Sotto", + newPasswordDesc: "Lascia vuoto per mantenere la password attuale.", + dateAlignment: "Allineamento della data", + dateVerticalAlign: "Allineamento verticale della data", + alignTop: "In alto", + alignMiddle: "Al centro", + alignBottom: "In basso", + dateLayout: "Disposizione della data", + alignmentLeft: "Sinistra", + alignmentCenter: "Centro", + alignmentRight: "Destra", + alignmentTight: "Compatto", + backupRestore: "Backup e ripristino", + backupRestoreDesc: "Esporta tutte le attività, le liste e i progetti come file JSON. Puoi modificare il file e reimportarlo.", + exportAllData: "Esporta tutti i dati (JSON)", + importData: "Importa dati", + importMode: "Modalità di importazione", + importModeMerge: "Unisci", + importModeMergeDesc: "Aggiungere i dati importati alle attività esistenti", + importModeReplace: "Sostituisci", + importModeReplaceDesc: "Elimina tutti i dati esistenti e sostituiscili con i dati importati", + importReplaceWarning: "Attenzione: tutte le attività, le liste e i progetti attuali verranno eliminati definitivamente!", + importSelectFile: "Seleziona file JSON…", + importButton: "Importa", + importing: "Importazione…", + exporting: "Esportazione…", + projects: "Progetti", + projectsDesc: "Organizza le attività con progetti colorati", + addProject: "Aggiungi progetto", + projectName: "Nome", + projectColor: "Colore", + noProjects: "Nessun progetto", + assignProject: "Assegna progetto", + removeProject: "Rimuovi progetto", + weekdayFormat: "Formato dei giorni", + weekdayFormatFull: "Nome completo (lunedì)", + weekdayFormatShort: "Abbreviato (lun)", + weekdayFormatNarrow: "Stretto (L)", + weekdayFormatCustom: "Personalizzato", + customWeekdayNamesMon: "Lu; Ma; Me; Gi; Ve; Sa; Do", + customWeekdayNamesSun: "Do; Lu; Ma; Me; Gi; Ve; Sa", + weekdayCase: "Maiuscole dei giorni", + weekdayCaseNormal: "Normale (lunedì)", + weekdayCaseCapitalize: "Iniziale maiuscola (Lunedì)", + weekdayCaseUppercase: "Maiuscolo (LUNEDÌ)", + styling: "Stile", + motivation: "Motivazione", + about: "Info", + setupAssistant: "Assistente di configurazione", + weekStartLabel: "La settimana inizia il", + startViewLabel: "Vista inizia con", + monday: "Lunedì", + sunday: "Domenica", + today: "Oggi", + yesterday: "Ieri", + accountId: "ID account", + accountIdDesc: "Il tuo identificatore account unico", + accountNumberLabel: "Numero account", + accountNumberDesc: "Il tuo numero account per identificazione", + connectOutlook: "Connetti Outlook", + syncTasks: "Sincronizza attività", + syncTasksDesc: "Sincronizza le attività con Google Tasks o Microsoft To-Do.", + unsyncConfirmMsg: "Interrompere la sincronizzazione di \"{title}\"? Le attività verranno spostate nel cestino.", + unsyncConfirm: "Interrompi sync", + unsyncCancel: "Annulla", + syncAll: "Sincronizza tutto", + unsyncAll: "Desincronizza tutto", + fetchingLists: "(caricamento liste...)", + listHeader: "Lista", + syncHeader: "Sync", + noTaskListsFound: "Nessuna lista di attività trovata.", + connectProviderAbove: "Connetti un provider sopra per sincronizzare le liste.", + noCalendarsFound: "Nessun calendario trovato o accesso negato.", + noCalendarsApple: "Nessun calendario caricato. Disconnetti e riconnetti.", + noCalendarsSynology: "Nessun calendario caricato. Disconnetti e riconnetti.", + selectionAfterConnect: "Selezione disponibile dopo la connessione.", + sharedCalendar: "Calendario condiviso", + primaryCalendar: "(Principale)", + fontCustomization: "Personalizzazione caratteri", + dateLayoutRight: "Data a destra del giorno", + dateLayoutLeft: "Data a sinistra del giorno", + dateLayoutAbove: "Data sopra il giorno", + dateLayoutBelow: "Data sotto il giorno", + dateLayoutHidden: "Data nascosta", + dateLayoutMobile: "Layout data (mobile)", + dayWeekdayGap: "Spazio giorno / settimana", + weekdayFont: "Carattere giorno", + dateFont: "Carattere data", + taskFont: "Carattere attività", + eventFont: "Carattere eventi", + goalFont: "Carattere obiettivo / citazione", + cwFont: "Carattere settimana calendario", + yearFont: "Carattere anno", + fontPlaceholder: "es. Poppins, Bebas Neue...", + fontSizePlaceholder: "Dimensione (es. 1.25rem)", + weightLight: "Leggero", + weightNormal: "Normale", + weightMedium: "Medio", + weightSemi: "Semi-grassetto", + weightBold: "Grassetto", + weightBlack: "Nero", + hourLabelFormat: "Formato delle ore", + hourLabelShort: "Breve (8, 9, 10)", + hourLabelFull: "Completo (8:00, 9:00, 10:00)", + showSubhourLabels: "Mostra quarti d'ora (:15, :30, :45)", + showScheduleCalendar: "Mostra calendario", + showDoThisNow: '"Fai ora" invece del motto', + focusTimer: "Timer Focus (min)", + focusBreak: "Pausa Focus (min)", + goalScopeTitle: "Periodo dell'obiettivo", + goalFallbackTitle: "Fallback obiettivo", + motivationalQuote: "Citazione motivazionale / festività", + nextTodo: "Prossima attività", + defaultText: "Testo predefinito", + apiDataSources: "Fonti dati API (URL)", + addSource: "Aggiungi fonte", + urlFormatHelp: "URL che restituisce citazioni JSON", + quoteLanguages: "Lingue delle citazioni", + quoteLanguagesDesc: "Scegli le lingue delle citazioni. Almeno una deve essere selezionata.", + quoteFallbackDesc: "Se nessuna fonte esterna risponde, vengono usate citazioni locali nella tua lingua.", + defaultGoalPlaceholder: "Inserisci il tuo obiettivo qui...", + saturdayColor: "Sabato", + sundayColor: "Domenica", + todayHighlight: "Evidenziazione oggi", + pastDayColor: "Giorni passati", + deleteProjectConfirm: "Elimina progetto", + importConfirmReplace: "Questo eliminerà TUTTE le attività, liste e progetti esistenti. Continuare?", + importSuccess: "Importazione completata", + importInvalidJson: "File JSON non valido", + }, +};