My-Weekly-ToDo-List/src/components/SettingsSidebar.tsx
mARTin-B78 02b2e40649 feat: add customizable CSS variables and custom CSS to Styling settings
Adds a "CSS Variables" panel to the Styling tab with a color picker and
description for every color token in globals.css (base, sidebar, design
token aliases, event colors, all-day chips), plus a "Custom CSS" textarea
that loads after the default stylesheet and can override any rule.

Overrides are stored per-user (customCssVars, customCss) and applied on
the root container's inline style, so they take precedence over themes
and per-field settings.

v1.113.0
2026-07-08 19:57:21 +02:00

5581 lines
359 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import React, { useState, useEffect, useRef, useMemo, useCallback } from "react";
import { signOut } from "next-auth/react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faApple, faGoogle, faMicrosoft, faNotion } from "@fortawesome/free-brands-svg-icons";
import { faServer, faFolder } from "@fortawesome/free-solid-svg-icons";
import CalendarSyncRulesPanel from "./CalendarSyncRulesPanel";
import { ViewStyle, KanbanStage, Task } from "./WeeklyView";
import { AVAILABLE_FONTS, isCustomFont } from "../lib/fontConstants";
import { translations } from "../lib/weeklyViewTranslations";
import { WeatherDisplayKey, WEATHER_DISPLAY_DEFAULTS } from "../lib/weeklyViewConstants";
import { EXPORT_FIELDS, type ExportFieldKey } from "../app/api/user/export/route";
import {
ArrowLeftRight,
Briefcase,
Calendar,
CalendarDays,
Check,
FolderOpen,
Globe,
Info,
Kanban,
Link,
ListTodo,
Palette,
Pencil,
Play,
Plus,
Settings,
Sparkles,
Trash2,
User,
} from "lucide-react";
import IconPicker from "./IconPicker";
import { allIcons } from "./iconRegistry";
import Icon from "@mdi/react";
import FlagIcon from "./FlagIcon";
import SearchableDropdown from "./SearchableDropdown";
import { TIMEZONE_OPTIONS, formatTimezone, getOffsetMinutes, formatOffset } from "../lib/timezones";
// Punkt 6 — manage a user's extra timezones (for cross-team scheduling).
// Stored in user.viewSettings.extraTimezones to avoid an extra migration.
function ExtraTimezonesEditor({
profile,
setProfile,
saveSetting,
}: {
profile: any;
setProfile: React.Dispatch<React.SetStateAction<any>>;
saveSetting: (key: string, value: any) => void;
}) {
const de = profile.language === "de";
const extras: string[] = (profile.viewSettings && profile.viewSettings.extraTimezones) || [];
const [picker, setPicker] = useState("");
const persist = (next: string[]) => {
const newViewSettings = { ...(profile.viewSettings || {}), extraTimezones: next };
setProfile((p: any) => ({ ...p, viewSettings: newViewSettings }));
saveSetting("viewSettings", newViewSettings);
};
const add = (zone: string) => {
if (!zone || extras.includes(zone) || zone === profile.timezone) return;
persist([...extras, zone]);
setPicker("");
};
const remove = (zone: string) => {
persist(extras.filter((z) => z !== zone));
};
return (
<div style={{ marginTop: "10px" }}>
<label style={{ display: "block", fontSize: "0.8rem", fontWeight: 600, color: "var(--weekly-settings-label)", marginBottom: "4px" }}>
{de ? "Zusätzliche Zeitzonen" : "Additional Timezones"}
</label>
<p style={{ fontSize: "0.72rem", color: "var(--weekly-settings-label)", margin: "0 0 6px", opacity: 0.8 }}>
{de
? "Hilfreich, wenn du mit Teammitgliedern in anderen Zeitzonen arbeitest. Zeiten werden im Tageskopf angezeigt."
: "Useful when collaborating with people in other timezones. Times are shown in the day header."}
</p>
{extras.length > 0 && (
<div style={{ display: "flex", flexDirection: "column", gap: "4px", marginBottom: "6px" }}>
{extras.map((z) => {
const opt = TIMEZONE_OPTIONS.find((o) => o.zone === z);
return (
<div key={z} style={{
display: "flex", alignItems: "center", gap: "8px",
padding: "4px 8px", borderRadius: "6px",
background: "var(--weekly-bg-soft, #f9fafb)",
border: "1px solid var(--weekly-border, #e5e7eb)",
}}>
<span style={{ fontSize: "0.8rem", flex: 1 }}>
{opt ? formatTimezone(opt) : z}
</span>
<button
onClick={() => remove(z)}
title={de ? "Entfernen" : "Remove"}
style={{ background: "none", border: "none", cursor: "pointer", color: "#ef4444", padding: "2px" }}
>
<Trash2 size={14} />
</button>
</div>
);
})}
</div>
)}
<div style={{ display: "flex", gap: "6px" }}>
<div style={{ flex: 1, minWidth: 0 }}>
<SearchableDropdown
value={picker}
onChange={(v) => setPicker(v)}
searchable
placeholder={de ? "Zeitzone auswählen…" : "Select a timezone…"}
searchPlaceholder={de ? "Stadt, Land, UTC, CET…" : "City, country, UTC, CET…"}
emptyText={de ? "Keine Treffer" : "No matches"}
options={TIMEZONE_OPTIONS
.filter((o) => !extras.includes(o.zone) && o.zone !== profile.timezone)
.sort((a, b) => {
const oa = getOffsetMinutes(a.zone);
const ob = getOffsetMinutes(b.zone);
if (oa !== ob) return oa - ob;
return a.code.localeCompare(b.code);
})
.map((opt) => {
const off = formatOffset(getOffsetMinutes(opt.zone));
return {
value: opt.zone,
label: `${opt.code}${opt.label}`,
secondary: `UTC${off}`,
searchHaystack: `${opt.zone} ${opt.code} ${opt.label} utc${off} utc${off.replace(":00", "")} gmt${off}`,
};
})}
/>
</div>
<button
onClick={() => add(picker)}
disabled={!picker}
className="weekly-btn-primary"
style={{ padding: "6px 10px", fontSize: "0.85rem", display: "inline-flex", alignItems: "center", gap: "4px", opacity: picker ? 1 : 0.5 }}
>
<Plus size={14} /> {de ? "Hinzufügen" : "Add"}
</button>
</div>
</div>
);
}
// All customizable color custom-properties defined in globals.css :root, grouped for the
// "Styling" tab's CSS Variables editor. Overrides are stored as a flat { "--var-name": "#hex" }
// map in profile.customCssVars and applied on top of containerStyle in WeeklyView.
const CSS_VARIABLE_GROUPS: { title: string; vars: { name: string; label: string; default: string }[] }[] = [
{
title: "Base Colors",
vars: [
{ name: "--weekly-teal", label: "Accent", default: "#009a9a" },
{ name: "--weekly-bg", label: "Background", default: "#ffffff" },
{ name: "--weekly-text", label: "Text", default: "#000000" },
{ name: "--weekly-text-light", label: "Text (light)", default: "#767676" },
{ name: "--weekly-border", label: "Border", default: "#cccccc" },
{ name: "--weekly-completed", label: "Completed task", default: "#cccccc" },
],
},
{
title: "Settings Sidebar",
vars: [
{ name: "--weekly-settings-bg", label: "Background", default: "#ffffff" },
{ name: "--weekly-settings-item-bg", label: "Item background", default: "#f8fafc" },
{ name: "--weekly-settings-label", label: "Label", default: "#475569" },
{ name: "--weekly-settings-title", label: "Title", default: "#111827" },
{ name: "--weekly-settings-input-bg", label: "Input background", default: "#ffffff" },
{ name: "--weekly-settings-input-border", label: "Input border", default: "#dddddd" },
{ name: "--weekly-settings-text", label: "Text", default: "#111827" },
{ name: "--weekly-settings-toggle-bg", label: "Toggle background", default: "#f3f4f6" },
{ name: "--weekly-settings-toggle-active-bg", label: "Toggle active background", default: "#ffffff" },
{ name: "--weekly-settings-toggle-text", label: "Toggle text", default: "#6b7280" },
{ name: "--weekly-settings-toggle-active-text", label: "Toggle active text", default: "#111827" },
],
},
{
title: "Design Token Aliases",
vars: [
{ name: "--bg", label: "Background (alias)", default: "#ffffff" },
{ name: "--paper", label: "Paper (alias)", default: "#ffffff" },
{ name: "--ink", label: "Text (alias)", default: "#000000" },
{ name: "--ink-2", label: "Text variant 2", default: "#2a2a28" },
{ name: "--ink-3", label: "Text light (alias)", default: "#767676" },
{ name: "--ink-4", label: "Text variant 4", default: "#a8a8a2" },
{ name: "--ink-5", label: "Text variant 5", default: "#c8c8c2" },
{ name: "--accent", label: "Accent (alias)", default: "#009a9a" },
{ name: "--line", label: "Border (alias)", default: "#cccccc" },
{ name: "--line-soft", label: "Border (soft)", default: "#f3f3f0" },
{ name: "--weekend", label: "Weekend", default: "#dc2626" },
{ name: "--tasks-bg", label: "Tasks background", default: "#f4f2ee" },
],
},
{
title: "Event Colors — Default",
vars: [
{ name: "--ev-default-bg", label: "Background", default: "#eef2f7" },
{ name: "--ev-default-border", label: "Border", default: "#6c87a8" },
{ name: "--ev-default-title", label: "Title", default: "#2b3f57" },
{ name: "--ev-default-meta", label: "Meta", default: "#6c87a8" },
],
},
{
title: "Event Colors — Family",
vars: [
{ name: "--ev-fam-bg", label: "Background", default: "#fbeef0" },
{ name: "--ev-fam-border", label: "Border", default: "#b85a6a" },
{ name: "--ev-fam-title", label: "Title", default: "#5a2530" },
{ name: "--ev-fam-meta", label: "Meta", default: "#99536a" },
],
},
{
title: "Event Colors — Finance",
vars: [
{ name: "--ev-fin-bg", label: "Background", default: "#f6f0e2" },
{ name: "--ev-fin-border", label: "Border", default: "#a88a3c" },
{ name: "--ev-fin-title", label: "Title", default: "#4a3a14" },
{ name: "--ev-fin-meta", label: "Meta", default: "#8a6f2a" },
],
},
{
title: "Event Colors — Development",
vars: [
{ name: "--ev-dev-bg", label: "Background", default: "#ecf3ed" },
{ name: "--ev-dev-border", label: "Border", default: "#5a7a4a" },
{ name: "--ev-dev-title", label: "Title", default: "#2c4527" },
{ name: "--ev-dev-meta", label: "Meta", default: "#5a7a4a" },
],
},
{
title: "All-Day Chips",
vars: [
{ name: "--chip-default-bg", label: "Default", default: "#b8b8b0" },
{ name: "--chip-fam-bg", label: "Family", default: "#e88a8a" },
{ name: "--chip-special-bg", label: "Special background", default: "#f4d588" },
{ name: "--chip-special-text", label: "Special text", default: "#6b4f10" },
],
},
];
// Lets the user override any globals.css :root color variable individually. Overrides are
// merged into profile.customCssVars (a flat map) and spread onto the root container's inline
// style in WeeklyView, so they take precedence over every other color source (theme, per-field
// settings, etc.).
function CssVariablesEditor({
profile,
setProfile,
saveSetting,
}: {
profile: any;
setProfile: React.Dispatch<React.SetStateAction<any>>;
saveSetting: (key: string, value: any) => void;
}) {
const de = profile.language === "de";
const vars: Record<string, string> = profile.customCssVars || {};
const debounceTimer = useRef<NodeJS.Timeout | null>(null);
const setVar = (name: string, value: string) => {
const next = { ...(profile.customCssVars || {}), [name]: value };
setProfile((p: any) => ({ ...p, customCssVars: next }));
if (debounceTimer.current) clearTimeout(debounceTimer.current);
debounceTimer.current = setTimeout(() => saveSetting("customCssVars", next), 500);
};
const reset = () => {
setProfile((p: any) => ({ ...p, customCssVars: {} }));
saveSetting("customCssVars", {});
};
return (
<div
style={{
background: "var(--weekly-settings-item-bg)",
padding: "12px",
borderRadius: "8px",
marginBottom: "12px",
}}
>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "8px" }}>
<label style={{ fontSize: "0.85rem", fontWeight: 600, color: "var(--weekly-settings-label)" }}>
{de ? "CSS-Variablen" : "CSS Variables"}
</label>
<button
type="button"
onClick={reset}
style={{
fontSize: "0.7rem",
background: "none",
border: "1px solid var(--weekly-settings-input-border)",
borderRadius: "6px",
padding: "3px 8px",
cursor: "pointer",
color: "var(--weekly-settings-label)",
}}
>
{de ? "Zurücksetzen" : "Reset"}
</button>
</div>
<p style={{ fontSize: "0.72rem", color: "var(--weekly-settings-label)", margin: "0 0 10px", opacity: 0.8 }}>
{de
? "Passe jede Design-Farbvariable des Stylesheets einzeln an."
: "Fine-tune every design-token color used by the stylesheet."}
</p>
{CSS_VARIABLE_GROUPS.map((group) => (
<div key={group.title} style={{ marginBottom: "12px" }}>
<div
style={{
fontSize: "0.7rem",
fontWeight: 600,
color: "var(--weekly-settings-title)",
marginBottom: "6px",
textTransform: "uppercase",
letterSpacing: "0.02em",
}}
>
{group.title}
</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "6px 12px" }}>
{group.vars.map((v) => (
<div
key={v.name}
title={v.name}
style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: "8px" }}
>
<span
style={{
fontSize: "0.72rem",
color: "var(--weekly-settings-label)",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{v.label}
</span>
<input
type="color"
value={vars[v.name] || v.default}
onChange={(e) => setVar(v.name, e.target.value)}
style={{
width: "32px",
height: "22px",
cursor: "pointer",
border: "1px solid var(--weekly-settings-input-border)",
borderRadius: "4px",
background: "transparent",
flexShrink: 0,
}}
/>
</div>
))}
</div>
</div>
))}
</div>
);
}
// Minimal ProjectIcon — resolves an icon name from the unified registry.
function ProjectIcon({ icon, size = 18, color }: { icon?: string | null; size?: number; color?: string }) {
if (!icon) return <FontAwesomeIcon icon={faFolder} style={{ fontSize: size, color }} />;
const normalised = icon.startsWith("fa") && icon.length > 2 && icon[2] === icon[2].toUpperCase()
? icon.slice(2, 3).toLowerCase() + icon.slice(3)
: icon;
const found = allIcons.find((i) => i.name === normalised || i.name === icon);
if (found) {
if (found.type === "fa") {
return <FontAwesomeIcon icon={found.icon as any} style={{ fontSize: size, color }} />;
}
return <Icon path={found.icon as string} size={size / 24} color={color} />;
}
return <span style={{ fontSize: size, lineHeight: 1 }}>{icon}</span>;
}
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<React.SetStateAction<any>>;
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<void>;
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<void>;
onImportLists: (lists: { id: string; title: string }[]) => Promise<void>;
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<string, boolean>;
somedayLists: SomedayList[];
handleToggleTaskList: (
provider: "google" | "apple" | "outlook" | "synology",
list: { id: string; title: string },
) => Promise<void>;
unsyncConfirm: {
provider: "google" | "apple" | "outlook" | "synology";
list: { id: string; title: string };
} | null;
onConfirmUnsync: () => Promise<void>;
onCancelUnsync: () => void;
handleSyncAll: (
provider: "google" | "outlook" | "synology",
lists: { id: string; title: string }[],
syncOn: boolean,
) => Promise<void>;
fetchAvailableTaskLists: (
provider: "google" | "apple" | "outlook" | "synology",
) => Promise<void>;
initialTab?: "calendar" | "general" | "account" | "styling" | "motivation" | "about" | "sync" | "projects";
projects: { id: string; name: string; icon?: string | null; color?: string | null }[];
onProjectsChanged: () => void;
kanbanStages: KanbanStage[];
saveKanbanStages: (stages: KanbanStage[]) => Promise<void>;
// Quick actions
isMobile?: boolean;
mobileActions?: {
goToPrevWeek: () => void;
goToPrevDay: () => void;
goToToday: () => void;
goToNextDay: () => void;
goToNextWeek: () => void;
onJumpToDate: () => void;
onAddCalendarEvent: () => void;
onAddProject: () => void;
onRecurringTasks: () => void;
onToggleNextTask: () => void;
onFocusMode: () => void;
onToggleDarkMode: () => void;
onSearch: () => void;
onUndo: () => void;
onRedo: () => void;
onRefresh: () => void;
darkMode: boolean;
showNextTask: boolean;
undoCount: number;
redoCount: number;
viewDays: number;
onViewDaysChange: (days: number) => void;
showTimeGrid: boolean;
cellDuration: CellDuration;
onCellDurationChange: (d: CellDuration) => void;
viewStyle: string;
onViewStyleChange: (style: string) => void;
startHour: number;
endHour: number;
onStartHourChange: (h: number) => void;
onEndHourChange: (h: number) => void;
};
perView: {
saveViewSetting: (key: string, value: any, perView: boolean) => void;
getEffective: (key: string, globalVal: any) => any;
};
onRunSetupAssistant?: () => void;
}
function SettingsSidebar({
onClose,
onSettingsChanged,
viewStyle,
setViewStyle,
showSomeday,
setShowSomeday,
showAllDay,
setShowAllDay,
showSchedule,
setShowSchedule,
goal,
setGoal,
saveGoal,
connections,
onUpdateConnections,
onRemoveConnection,
focusTimerDuration,
setFocusTimerDuration,
focusBreakDuration,
setFocusBreakDuration,
showNextTask,
setShowNextTask,
protectEventTimes,
setProtectEventTimes,
goalFallbackType,
goalDefaultSentence,
importingTasksState,
executeImport,
onImportLists,
importStatusMsg,
showTimeGrid,
setShowTimeGrid,
cellDuration,
setCellDuration,
weekStartDay,
setWeekStartDay,
fontSize,
setFontSize,
headlineFont,
headlineFontSize,
headlineFontWeight,
goalFontWeight,
dateFontFamily,
dateFontSize,
dateFontWeight,
timeTaskFontFamily,
timeTaskFontSize,
timeTaskFontWeight,
bodyFont,
taskFontFamily,
taskFontSize,
taskFontWeight,
fontWeight,
weekendColorSat,
weekendColorSun,
hourLabelFormat,
setHourLabelFormat,
showSubHourSlots,
setShowSubHourSlots,
allDayPosition,
setAllDayPosition,
saveSetting,
availableTaskLists,
isFetchingProviderLists,
somedayLists,
handleToggleTaskList,
unsyncConfirm,
onConfirmUnsync,
onCancelUnsync,
handleSyncAll,
fetchAvailableTaskLists,
initialTab,
setCurrentWeekStart,
projects,
onProjectsChanged,
kanbanStages,
saveKanbanStages,
profile,
setProfile,
isMobile: isMobileSidebar,
mobileActions,
perView,
onRunSetupAssistant,
}: SettingsSidebarProps) {
const [activeTab, setActiveTab] = useState<
"calendar" | "general" | "account" | "styling" | "motivation" | "about" | "localisation" | "sync" | "projects"
>(initialTab || "general");
const [isLoading, setIsLoading] = useState(true);
const [isSyncing, setIsSyncing] = useState(false);
const [exportStartDate, setExportStartDate] = useState("");
const [exportEndDate, setExportEndDate] = useState("");
const [exportFields, setExportFields] = useState<Set<ExportFieldKey>>(
() => new Set(EXPORT_FIELDS.filter(f => f.defaultOn).map(f => f.key))
);
const [importMode, setImportMode] = useState<"merge" | "replace">("merge");
const [importFile, setImportFile] = useState<File | null>(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<string | null>(null);
const [confirmDisconnectId, setConfirmDisconnectId] = useState<string | null>(
null,
);
const [newProjectName, setNewProjectName] = useState("");
const [newProjectColor, setNewProjectColor] = useState("#3b82f6");
const [newProjectIcon, setNewProjectIcon] = useState("📁");
const [showNewProjectIconPicker, setShowNewProjectIconPicker] = useState(false);
const [editingProjectId, setEditingProjectId] = useState<string | null>(null);
const [editProjectName, setEditProjectName] = useState("");
const [editProjectColor, setEditProjectColor] = useState("");
const [editProjectIcon, setEditProjectIcon] = useState("");
const [showEditProjectIconPicker, setShowEditProjectIconPicker] = useState(false);
const [weatherSearchResults, setWeatherSearchResults] = useState<any[]>([]);
// 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<Record<string, NodeJS.Timeout>>({});
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 (
<>
<div
className={`weekly-settings-overlay ${isVisible ? "show" : ""}`}
onClick={handleClose}
style={{ zIndex: 1999 }}
/>
<div className={`weekly-settings-sidebar ${isVisible ? "open" : ""}`}>
<header className="weekly-settings-header">
<h2 className="weekly-settings-title">{t.settings}</h2>
<button className="weekly-settings-close" onClick={handleClose}>
×
</button>
</header>
<div
className="weekly-settings-tabs"
style={{
display: "flex",
justifyContent: "space-around",
flexWrap: "nowrap",
gap: "0",
borderBottom: "1px solid var(--weekly-border, #eee)",
padding: "0",
}}
>
{([
{ key: "general", icon: <Settings size={18} />, label: t.general },
{ key: "localisation", icon: <Globe size={18} />, label: t.localisation },
{ key: "calendar", icon: <Link size={18} />, label: t.calendar },
{ key: "sync", icon: <ArrowLeftRight size={18} />, label: t.calendarSync || "Sync" },
{ key: "projects", icon: <FolderOpen size={18} />, label: t.projects || "Projects" },
{ key: "account", icon: <User size={18} />, label: t.account },
{ key: "styling", icon: <Palette size={18} />, label: t.styling },
{ key: "motivation", icon: <Sparkles size={18} />, label: t.motivation },
{ key: "about", icon: <Info size={18} />, label: t.about },
] as const).map((tab) => (
<button
key={tab.key}
onClick={() => setActiveTab(tab.key as any)}
title={tab.label}
className="settings-tab-btn"
style={{
padding: "10px 8px",
borderBottom:
activeTab === tab.key
? "2px solid var(--weekly-text, black)"
: "2px solid transparent",
background: "none",
border: "none",
borderBottomStyle: "solid",
borderBottomWidth: "2px",
borderBottomColor:
activeTab === tab.key
? "var(--weekly-text, black)"
: "transparent",
cursor: "pointer",
opacity: activeTab === tab.key ? 1 : 0.5,
color: "var(--weekly-text, #333)",
display: "flex",
alignItems: "center",
justifyContent: "center",
transition: "opacity 0.15s, border-color 0.15s",
position: "relative",
}}
>
{tab.icon}
</button>
))}
</div>
<div
className="weekly-settings-content"
style={{ flex: 1, overflowY: "auto", padding: "24px" }}
>
{activeTab === "general" ? (
<div
style={{ display: "flex", flexDirection: "column", gap: "16px" }}
>
{/* ── General settings (not view-specific) ── */}
<div style={{ display: "flex", flexDirection: "column", gap: "12px", paddingBottom: "16px", borderBottom: "1px solid var(--weekly-border, #e5e7eb)" }}>
{/* Menu Position */}
<div>
<label style={{ fontSize: "0.85rem", fontWeight: 600, color: "var(--weekly-settings-label)" }}>
{profile.language === "de" ? "Menüposition" : "Menu Position"}
</label>
<div className="mt-1 flex gap-2">
{[
{ value: "left", label: profile.language === "de" ? "Links (Seitenleiste)" : "Left (Sidebar)" },
{ value: "top", label: profile.language === "de" ? "Oben (Kopfleiste)" : "Top (Header)" },
].map(({ value, label }) => (
<button
key={value}
onClick={() => {
setProfile((p: any) => ({ ...p, menuPosition: value }));
saveSetting("menuPosition", value);
}}
style={{
flex: 1,
padding: "6px 10px",
fontSize: "0.8rem",
borderRadius: "6px",
border: "1px solid var(--weekly-border, #e5e7eb)",
cursor: "pointer",
fontWeight: (profile.menuPosition || "left") === value ? 700 : 400,
background: (profile.menuPosition || "left") === value ? "var(--weekly-text, #333)" : "transparent",
color: (profile.menuPosition || "left") === value ? "#fff" : "var(--weekly-settings-label)",
}}
>
{label}
</button>
))}
</div>
<div style={{ marginTop: "10px" }}>
<label style={{ fontSize: "0.78rem", fontWeight: 600, color: "var(--weekly-settings-label)" }}>
{profile.language === "de" ? "Steuerung in der Kopfleiste" : "Header Controls"}
</label>
<div className="mt-1 flex gap-2">
{[
{ value: true, label: profile.language === "de" ? "Anzeigen" : "Show" },
{ value: false, label: profile.language === "de" ? "Ausblenden" : "Hide" },
].map(({ value, label }) => {
const isActive = (profile.showHeaderControls !== false) === value;
return (
<button
key={String(value)}
onClick={() => {
setProfile((p: any) => ({ ...p, showHeaderControls: value }));
saveSetting("showHeaderControls", value);
}}
style={{
flex: 1,
padding: "6px 10px",
fontSize: "0.8rem",
borderRadius: "6px",
border: "1px solid var(--weekly-border, #e5e7eb)",
cursor: "pointer",
fontWeight: isActive ? 700 : 400,
background: isActive ? "var(--weekly-text, #333)" : "transparent",
color: isActive ? "#fff" : "var(--weekly-settings-label)",
}}
>
{label}
</button>
);
})}
</div>
</div>
</div>
{/* Header Display */}
<div>
<label style={{ fontSize: "0.85rem", fontWeight: 600, color: "var(--weekly-settings-label)" }}>
{t.headerDisplay}
</label>
<div className="mt-1">
<select
value={profile.headerDisplay || "kw"}
onChange={(e) => {
const val = e.target.value as any;
setProfile({ ...profile, headerDisplay: val });
saveSetting("headerDisplay", val);
}}
className="weekly-input w-full p-2 border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white rounded"
>
<option value="kw">{t.headerDisplayKW}</option>
<option value="month">{t.headerDisplayMonth}</option>
<option value="month_year">{t.headerDisplayMonthYear}</option>
<option value="date">{t.headerDisplayDate}</option>
<option value="current_day">{t.headerDisplayCurrentDay}</option>
<option value="custom">{t.headerDisplayCustom}</option>
<option value="none">{t.headerDisplayNone}</option>
</select>
</div>
{profile.headerDisplay === "current_day" && (
<div className="mt-2">
<label style={{ fontSize: "0.75rem", color: "var(--weekly-settings-label)", display: "block", marginBottom: "4px" }}>
{t.headerCurrentDayFormatLabel}
</label>
<input
type="text"
value={profile.headerCurrentDayFormat || ""}
onChange={(e) => {
const val = e.target.value;
setProfile({ ...profile, headerCurrentDayFormat: val });
saveSetting("headerCurrentDayFormat", val);
}}
placeholder="DDD, DD. MMMM YYYY"
className="w-full px-3 py-1.5 text-sm rounded border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
/>
<div className="mt-1 text-[10px] text-gray-500 leading-tight">
Tokens: DDDD (Montag), DDD (Mo.), DD (30), MMMM (März), MMM (Mär), MM (03), YYYY (2026)
</div>
</div>
)}
{profile.headerDisplay === "custom" && (
<div className="mt-2">
<label style={{ fontSize: "0.75rem", color: "var(--weekly-settings-label)", display: "block", marginBottom: "4px" }}>
{t.headerCustomFormatLabel}
</label>
<input
type="text"
value={profile.headerCustomFormat || ""}
onChange={(e) => {
const val = e.target.value;
setProfile({ ...profile, headerCustomFormat: val });
saveSetting("headerCustomFormat", val);
}}
placeholder="KW WW | YYYY"
className="w-full px-3 py-1.5 text-sm rounded border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
/>
<div className="mt-1 text-[10px] text-gray-500 leading-tight">
Tokens: WW (KW), YYYY (Jahr), MMMM (März), MM (03), DD (30), [TODAY] (Heute)
</div>
</div>
)}
{/* Mobile Portrait/Landscape overrides */}
<div className="mt-3 grid grid-cols-2 gap-2">
<div>
<label style={{ fontSize: "0.75rem", color: "var(--weekly-settings-label)", display: "block", marginBottom: "4px" }}>
{t.headerMobilePortrait}
</label>
<select
value={profile.mobilePortraitHeaderDisplay || "current_day"}
onChange={(e) => {
const val = e.target.value as any;
setProfile({ ...profile, mobilePortraitHeaderDisplay: val });
saveSetting("mobilePortraitHeaderDisplay", val);
}}
className="weekly-input w-full p-1.5 text-xs border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white rounded"
>
<option value="kw">{t.headerDisplayKW}</option>
<option value="month">{t.headerDisplayMonth}</option>
<option value="month_year">{t.headerDisplayMonthYear}</option>
<option value="date">{t.headerDisplayDate}</option>
<option value="current_day">{t.headerDisplayCurrentDay}</option>
<option value="custom">{t.headerDisplayCustom}</option>
<option value="none">{t.headerDisplayNone}</option>
</select>
</div>
<div>
<label style={{ fontSize: "0.75rem", color: "var(--weekly-settings-label)", display: "block", marginBottom: "4px" }}>
{t.headerMobileLandscape}
</label>
<select
value={profile.mobileLandscapeHeaderDisplay || "kw"}
onChange={(e) => {
const val = e.target.value as any;
setProfile({ ...profile, mobileLandscapeHeaderDisplay: val });
saveSetting("mobileLandscapeHeaderDisplay", val);
}}
className="weekly-input w-full p-1.5 text-xs border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white rounded"
>
<option value="kw">{t.headerDisplayKW}</option>
<option value="month">{t.headerDisplayMonth}</option>
<option value="month_year">{t.headerDisplayMonthYear}</option>
<option value="date">{t.headerDisplayDate}</option>
<option value="current_day">{t.headerDisplayCurrentDay}</option>
<option value="custom">{t.headerDisplayCustom}</option>
<option value="none">{t.headerDisplayNone}</option>
</select>
</div>
</div>
</div>
{/* Push Notifications */}
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<input
type="checkbox"
id="notificationsEnabled"
checked={profile.notificationsEnabled || false}
onChange={async (e) => {
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" }}
/>
<label htmlFor="notificationsEnabled" style={{ fontSize: "0.9rem", fontWeight: 600 }}>
{(profile.language || "en") === "de" ? "Push-Benachrichtigungen" : "Push Notifications"}
</label>
</div>
</div>
{/* ── View Style Tabs ── */}
<div>
<div style={{
display: "flex",
borderBottom: "2px solid var(--weekly-border, #e5e7eb)",
gap: "0",
}}>
{([
{ key: "simple", label: t.simpleView, icon: <Calendar size={14} /> },
{ key: "calendar", label: t.calendarView, icon: <CalendarDays size={14} /> },
{ key: "list", label: t.listView, icon: <ListTodo size={14} /> },
{ key: "kanban", label: t.kanbanView, icon: <Kanban size={14} /> },
] as const).map((tab) => (
<button
key={tab.key}
onClick={() => {
setViewStyle(tab.key);
saveSetting("viewStyle", tab.key);
if (tab.key === "simple" || tab.key === "calendar") {
setShowTimeGrid(true);
saveSetting("showTimeGrid", true);
} else if (tab.key === "list") {
setShowTimeGrid(false);
saveSetting("showTimeGrid", false);
}
}}
style={{
flex: 1,
padding: "10px 8px",
fontSize: "0.8rem",
fontWeight: viewStyle === tab.key ? 700 : 400,
color: viewStyle === tab.key ? "var(--weekly-text, #333)" : "var(--weekly-text-light, #9ca3af)",
background: "none",
border: "none",
borderBottom: viewStyle === tab.key ? "2px solid var(--weekly-text, #333)" : "2px solid transparent",
marginBottom: "-2px",
cursor: "pointer",
transition: "all 0.15s",
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: "4px",
}}
>
{tab.icon}
{tab.label}
</button>
))}
</div>
</div>
{/* ── View-specific settings ── */}
<div style={{ display: "flex", flexDirection: "column", gap: "12px" }}>
{/* Time grid settings — for simple & calendar views */}
{(viewStyle === "simple" || viewStyle === "calendar") && (
<div style={{ display: "flex", flexDirection: "column", gap: "10px" }}>
<div>
<label style={{ display: "block", fontSize: "0.9rem", fontWeight: 600, marginBottom: "4px" }}>
{t.timeSlotDuration}
</label>
<select
value={perView.getEffective("cellDuration", cellDuration) as number}
onChange={(e) => {
const val = Number(e.target.value) as CellDuration;
perView.saveViewSetting("cellDuration", val, true);
}}
className="weekly-input"
style={{ width: "100%", padding: "8px", border: "1px solid #ddd", borderRadius: "4px" }}
>
<option value={15}>15 min</option>
<option value={20}>20 min</option>
<option value={30}>30 min</option>
<option value={60}>1 hour</option>
</select>
</div>
<div>
<label style={{ display: "block", fontSize: "0.9rem", fontWeight: 600, marginBottom: "4px" }}>
{t.hourLabelFormat}
</label>
<select
value={perView.getEffective("hourLabelFormat", hourLabelFormat) as string}
onChange={(e) => {
const fmt = e.target.value as "short" | "full";
perView.saveViewSetting("hourLabelFormat", fmt, true);
}}
className="weekly-input"
style={{ width: "100%", padding: "8px", border: "1px solid #ddd", borderRadius: "4px" }}
>
<option value="short">{t.hourLabelShort}</option>
<option value="full">{t.hourLabelFull}</option>
</select>
</div>
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<input
type="checkbox" id="showSubHourSlots"
checked={perView.getEffective("showSubHourSlots", showSubHourSlots) as boolean}
onChange={(e) => {
perView.saveViewSetting("showSubHourSlots", e.target.checked, true);
}}
style={{ width: "16px", height: "16px" }}
/>
<label htmlFor="showSubHourSlots" style={{ fontSize: "0.9rem", fontWeight: 600 }}>
{t.showSubhourLabels}
</label>
</div>
{/* Weather Settings */}
<div style={{ marginTop: "8px", borderTop: "1px solid var(--border-color, #e5e7eb)", paddingTop: "12px" }}>
<label style={{ display: "flex", alignItems: "center", gap: "8px", marginBottom: "8px", cursor: "pointer" }}>
<input
type="checkbox"
checked={perView.getEffective("weatherEnabled", profile.weatherEnabled || false) as boolean}
onChange={(e) => {
setProfile({ ...profile, weatherEnabled: e.target.checked });
saveSetting("weatherEnabled", e.target.checked);
perView.saveViewSetting("weatherEnabled", e.target.checked, true);
}}
style={{ width: "16px", height: "16px" }}
/>
<span style={{ fontSize: "0.9rem", fontWeight: 600 }}>
{profile.language === "de" ? "Wetter anzeigen" : "Show weather"}
</span>
</label>
{(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 (
<div style={{ display: "flex", flexDirection: "column", gap: "8px", marginLeft: "24px" }}>
<input
type="text"
placeholder={profile.language === "de" ? "Stadt suchen..." : "Search city..."}
className="weekly-input"
style={{ padding: "6px 10px", fontSize: "0.85rem" }}
onChange={async (e) => {
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 && (
<div style={{ border: "1px solid var(--border-color, #e5e7eb)", borderRadius: "8px", overflow: "hidden" }}>
{weatherSearchResults.map((r: any, i: number) => (
<button
key={i}
onClick={() => selectCity(r)}
style={{ display: "block", width: "100%", padding: "8px 12px", textAlign: "left", border: "none", borderBottom: "1px solid var(--border-color, #e5e7eb)", background: "var(--bg-secondary, #f9fafb)", cursor: "pointer", fontSize: "0.85rem" }}
>
{r.name}{r.admin1 ? `, ${r.admin1}` : ""}, {r.country} <span style={{ color: "#888", fontSize: "0.75rem" }}>({r.lat.toFixed(2)}, {r.lon.toFixed(2)})</span>
</button>
))}
</div>
)}
{profile.weatherLocation && (
<div style={{ display: "flex", alignItems: "center", gap: "8px", padding: "8px 12px", borderRadius: "8px", background: "var(--bg-secondary, #f9fafb)" }}>
<span style={{ fontSize: "0.85rem", fontWeight: 500 }}>📍 {profile.weatherLocation}</span>
<span style={{ fontSize: "0.75rem", color: "#888" }}>({profile.weatherLat?.toFixed(2)}, {profile.weatherLon?.toFixed(2)})</span>
</div>
)}
{recentCities.length > 0 && (
<div>
<div style={{ fontSize: "0.75rem", fontWeight: 600, color: "var(--weekly-text-light, #9ca3af)", marginBottom: "4px" }}>
{profile.language === "de" ? "Letzte Städte" : "Recent cities"}
</div>
<div style={{ display: "flex", flexWrap: "wrap", gap: "4px" }}>
{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 (
<button
key={i}
onClick={() => selectCity(c)}
style={{
padding: "4px 10px", fontSize: "0.8rem", borderRadius: "999px",
border: isActive ? "1.5px solid var(--weekly-accent, #0ea5e9)" : "1px solid var(--weekly-border, #e5e7eb)",
background: isActive ? "var(--weekly-accent, #0ea5e9)" : "var(--bg-secondary, #f9fafb)",
color: isActive ? "#fff" : "var(--weekly-text, #333)",
cursor: "pointer", fontWeight: isActive ? 600 : 400,
}}
>
{c.name}
</button>
);
})}
</div>
</div>
)}
{/* Weather display options */}
<div style={{ marginTop: "8px", borderTop: "1px solid var(--border-color, #e5e7eb)", paddingTop: "8px" }}>
<div style={{ fontSize: "0.8rem", fontWeight: 600, marginBottom: "6px", color: "var(--weekly-text-light, #666)" }}>
{profile.language === "de" ? "Angezeigte Daten" : "Display data"}
</div>
{([
{ 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 (
<label key={key} style={{ display: "flex", alignItems: "center", gap: "6px", cursor: "pointer", padding: "3px 0", fontSize: "0.82rem" }}>
<input
type="checkbox"
checked={checked}
onChange={() => {
const updated = checked ? current.filter(k => k !== key) : [...current, key];
perView.saveViewSetting("weatherDisplay", updated.length > 0 ? updated : ["icon"], true);
}}
style={{ width: "14px", height: "14px" }}
/>
<span>{icon}</span>
<span>{label}</span>
</label>
);
})}
</div>
</div>
);
})()}
</div>
</div>
)}
{/* Per-view display settings */}
<div style={{ display: "flex", flexDirection: "column", gap: "10px", borderTop: (viewStyle === "simple" || viewStyle === "calendar") ? "1px solid var(--weekly-border, #e5e7eb)" : "none", paddingTop: (viewStyle === "simple" || viewStyle === "calendar") ? "12px" : "0" }}>
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<input type="checkbox" id="showSomeday"
checked={perView.getEffective("showSomeday", showSomeday) as boolean}
onChange={(e) => {
perView.saveViewSetting("showSomeday", e.target.checked, true);
}}
style={{ width: "16px", height: "16px" }} />
<label htmlFor="showSomeday" style={{ fontSize: "0.9rem", fontWeight: 600 }}>{t.showSomeday}</label>
</div>
{viewStyle !== "kanban" && (
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<input type="checkbox" id="showAllDay"
checked={perView.getEffective("showAllDayEvents", showAllDay) as boolean}
onChange={(e) => {
perView.saveViewSetting("showAllDayEvents", e.target.checked, true);
}}
style={{ width: "16px", height: "16px" }} />
<label htmlFor="showAllDay" style={{ fontSize: "0.9rem", fontWeight: 600 }}>{t.showAllDay}</label>
</div>
)}
{viewStyle !== "kanban" && (perView.getEffective("showAllDayEvents", showAllDay) as boolean) && (
<div style={{ marginLeft: "24px" }}>
<select
value={perView.getEffective("allDayPosition", allDayPosition) as string}
onChange={(e) => {
const pos = e.target.value as "above" | "below";
perView.saveViewSetting("allDayPosition", pos, true);
}}
className="weekly-input"
style={{ width: "100%", padding: "8px", border: "1px solid #ddd", borderRadius: "4px" }}
>
<option value="above">{t.allDayAbove}</option>
<option value="below">{t.allDayBelow}</option>
</select>
</div>
)}
{viewStyle !== "kanban" && (
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<input type="checkbox" id="autoRolling" checked={profile.autoRolling || false}
onChange={(e) => saveField("autoRolling", e.target.checked)}
style={{ width: "16px", height: "16px" }} />
<label htmlFor="autoRolling" style={{ fontSize: "0.9rem", fontWeight: 600 }}>{t.runningList}</label>
</div>
)}
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<input type="checkbox" id="showTaskCheckboxes"
checked={profile.showTaskCheckboxes || false}
onChange={(e) => {
saveField("showTaskCheckboxes", e.target.checked);
perView.saveViewSetting("showTaskCheckboxes", e.target.checked, false);
}}
style={{ width: "16px", height: "16px" }} />
<label htmlFor="showTaskCheckboxes" style={{ fontSize: "0.9rem", fontWeight: 600 }}>{t.showTaskCheckboxes}</label>
</div>
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<input type="checkbox" id="showProjectIcons"
checked={profile.showProjectIcons || false}
onChange={(e) => {
saveField("showProjectIcons", e.target.checked);
perView.saveViewSetting("showProjectIcons", e.target.checked, false);
}}
style={{ width: "16px", height: "16px" }} />
<label htmlFor="showProjectIcons" style={{ fontSize: "0.9rem", fontWeight: 600 }}>{t.showProjectIcons}</label>
</div>
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<input type="checkbox" id="showPriorityIcons"
checked={profile.showPriorityIcons !== false}
onChange={(e) => {
saveField("showPriorityIcons", e.target.checked);
perView.saveViewSetting("showPriorityIcons", e.target.checked, false);
}}
style={{ width: "16px", height: "16px" }} />
<label htmlFor="showPriorityIcons" style={{ fontSize: "0.9rem", fontWeight: 600 }}>
{profile.language === "de" ? "Prioritäts-Icons anzeigen" : "Show Priority Icons"}
</label>
</div>
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<label htmlFor="priorityStyle" style={{ fontSize: "0.9rem", fontWeight: 600 }}>
{profile.language === "de" ? "Prioritäts-Stil" : "Priority Style"}
</label>
<select
id="priorityStyle"
value={profile.priorityStyle || "eisenhower"}
onChange={(e) => saveField("priorityStyle", e.target.value)}
className="weekly-input"
style={{ padding: "4px 8px", fontSize: "0.85rem", borderRadius: "6px", border: "1px solid var(--weekly-border, #e5e7eb)" }}
>
<option value="eisenhower">Eisenhower</option>
<option value="abcde">ABCDE</option>
<option value="ivylee">Ivy Lee</option>
<option value="pareto">80/20 (Pareto)</option>
</select>
</div>
{viewStyle !== "kanban" && (
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<input type="checkbox" id="protectEventTimes" checked={profile.protectEventTimes || false}
onChange={(e) => saveField("protectEventTimes", e.target.checked)}
style={{ width: "16px", height: "16px" }} />
<label htmlFor="protectEventTimes" style={{ fontSize: "0.9rem", fontWeight: 600 }}>{t.protectEventTimes}</label>
</div>
)}
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<input type="checkbox" id="showCompletedTasks"
checked={perView.getEffective("showCompletedTasks", profile.showCompletedTasks !== false) as boolean}
onChange={(e) => {
perView.saveViewSetting("showCompletedTasks", e.target.checked, true);
}}
style={{ width: "16px", height: "16px" }} />
<label htmlFor="showCompletedTasks" style={{ fontSize: "0.9rem", fontWeight: 600 }}>
{profile.language === "de" ? "Erledigte Aufgaben anzeigen" : "Show completed tasks"}
</label>
</div>
</div>
</div>
</div>
) : activeTab === "localisation" ? (
<div
style={{ display: "flex", flexDirection: "column", gap: "20px" }}
>
<h3 style={{ fontSize: "1rem", fontWeight: 600, margin: 0 }}>
{t.localisation || "Localisation"}
</h3>
{/* Start Week Setting + Start View On */}
<div style={{ display: "flex", gap: "24px", flexWrap: "wrap" }}>
<div>
<label
style={{
display: "block",
fontSize: "0.9rem",
fontWeight: 600,
marginBottom: "4px",
}}
>
{t.weekStartLabel}
</label>
<div style={{ display: "flex", gap: "8px" }}>
<button
className={`px-3 py-2 rounded text-sm ${weekStartDay === 1 ? "bg-blue-600 text-white" : "bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300"}`}
onClick={() => { setWeekStartDay(1); saveSetting("weekStartDay", 1); }}
>
{t.monday}
</button>
<button
className={`px-3 py-2 rounded text-sm ${weekStartDay === 0 ? "bg-blue-600 text-white" : "bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300"}`}
onClick={() => { setWeekStartDay(0); saveSetting("weekStartDay", 0); }}
>
{t.sunday}
</button>
</div>
</div>
<div>
<label
style={{
display: "block",
fontSize: "0.9rem",
fontWeight: 600,
marginBottom: "4px",
}}
>
{t.startViewLabel}
</label>
<div style={{ display: "flex", gap: "8px" }}>
<button
className={`px-3 py-2 rounded text-sm ${(profile.startDayOffset || 0) === 0 ? "bg-blue-600 text-white" : "bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300"}`}
onClick={() => {
setProfile({ ...profile, startDayOffset: 0 });
saveSetting("startDayOffset", 0);
const d = new Date();
d.setHours(0, 0, 0, 0);
setCurrentWeekStart(d);
}}
>
{t.today}
</button>
<button
className={`px-3 py-2 rounded text-sm ${profile.startDayOffset === -1 ? "bg-blue-600 text-white" : "bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300"}`}
onClick={() => {
setProfile({ ...profile, startDayOffset: -1 });
saveSetting("startDayOffset", -1);
const d = new Date();
d.setHours(0, 0, 0, 0);
d.setDate(d.getDate() - 1);
setCurrentWeekStart(d);
}}
>
{t.yesterday}
</button>
</div>
</div>
</div>
{/* Weekday Format */}
<div style={{ marginTop: "8px" }}>
<label
style={{
display: "block",
fontSize: "0.9rem",
fontWeight: 600,
marginBottom: "8px",
}}
>
{t.weekdayFormat || translations["en"].weekdayFormat}
</label>
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
<select
value={profile.weekdayFormat || "long"}
onChange={(e) => saveField("weekdayFormat", e.target.value)}
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)" }}
>
<option value="long">{t.weekdayFormatFull || translations["en"].weekdayFormatFull}</option>
<option value="short">{t.weekdayFormatShort || translations["en"].weekdayFormatShort}</option>
<option value="narrow">{t.weekdayFormatNarrow || translations["en"].weekdayFormatNarrow}</option>
<option value="custom">{t.weekdayFormatCustom || translations["en"].weekdayFormatCustom}</option>
</select>
{profile.weekdayFormat === "custom" && (
<input
type="text"
value={profile.customWeekdayNames || ""}
onChange={(e) => 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)" }}
/>
)}
</div>
</div>
{/* Weekday Case */}
<div style={{ marginTop: "8px" }}>
<label
style={{
display: "block",
fontSize: "0.9rem",
fontWeight: 600,
marginBottom: "8px",
}}
>
{t.weekdayCase || "Weekday Case"}
</label>
<select
value={profile.weekdayCase || "capitalize"}
onChange={(e) => saveField("weekdayCase", e.target.value)}
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)" }}
>
<option value="normal">{t.weekdayCaseNormal || "Normal (monday)"}</option>
<option value="capitalize">{t.weekdayCaseCapitalize || "Capitalize (Monday)"}</option>
<option value="uppercase">{t.weekdayCaseUppercase || "Uppercase (MONDAY)"}</option>
</select>
</div>
<div style={{ marginTop: "8px" }}>
<label
style={{
display: "block",
fontSize: "0.9rem",
fontWeight: 600,
marginBottom: "4px",
}}
>
{t.language}
</label>
<SearchableDropdown
value={profile.language || "de"}
onChange={(v) => saveField("language", v)}
options={[
{ value: "en", label: "English", leading: <FlagIcon code="en" width={20} height={14} /> },
{ value: "de", label: "Deutsch", leading: <FlagIcon code="de" width={20} height={14} /> },
{ value: "fr", label: "Français", leading: <FlagIcon code="fr" width={20} height={14} /> },
{ value: "es", label: "Español", leading: <FlagIcon code="es" width={20} height={14} /> },
{ value: "it", label: "Italiano", leading: <FlagIcon code="it" width={20} height={14} /> },
]}
/>
</div>
<div>
<label
style={{
display: "block",
fontSize: "0.9rem",
fontWeight: 600,
marginBottom: "4px",
}}
>
{t.timezone}
</label>
<SearchableDropdown
value={profile.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone}
onChange={(v) => saveField("timezone", v)}
searchable
searchPlaceholder={profile.language === "de" ? "Stadt, Land, UTC, CET…" : "City, country, UTC, CET…"}
emptyText={profile.language === "de" ? "Keine Treffer" : "No matches"}
options={[...TIMEZONE_OPTIONS]
.sort((a, b) => {
const oa = getOffsetMinutes(a.zone);
const ob = getOffsetMinutes(b.zone);
if (oa !== ob) return oa - ob;
return a.code.localeCompare(b.code);
})
.map((opt) => {
const off = formatOffset(getOffsetMinutes(opt.zone));
return {
value: opt.zone,
label: `${opt.code}${opt.label}`,
secondary: `UTC${off}`,
// Match against IANA name (zone), code, label,
// and the offset in several styles.
searchHaystack: `${opt.zone} ${opt.code} ${opt.label} utc${off} utc${off.replace(":00", "")} gmt${off}`,
};
})}
/>
{/* Multi-timezone (Punkt 6) */}
<ExtraTimezonesEditor
profile={profile}
setProfile={setProfile}
saveSetting={saveSetting}
/>
</div>
<div>
<label
style={{
display: "block",
fontSize: "0.9rem",
fontWeight: 600,
marginBottom: "4px",
}}
>
{t.dateFormat}
</label>
<select
value={profile.dateFormat}
onChange={(e) => saveField("dateFormat", e.target.value)}
className="weekly-input"
style={{
width: "100%",
padding: "8px",
border: "1px solid #ddd",
borderRadius: "4px",
}}
>
<option value="MM/dd/yyyy">MM/DD/YYYY</option>
<option value="dd/MM/yyyy">DD/MM/YYYY</option>
<option value="yyyy-MM-dd">YYYY-MM-DD</option>
</select>
</div>
<div>
<label
style={{
display: "block",
fontSize: "0.9rem",
fontWeight: 600,
marginBottom: "4px",
}}
>
{t.timeFormat}
</label>
<select
value={profile.timeFormat}
onChange={(e) => saveField("timeFormat", e.target.value)}
className="weekly-input"
style={{
width: "100%",
padding: "8px",
border: "1px solid #ddd",
borderRadius: "4px",
}}
>
<option value="12h">12h AM/PM</option>
<option value="24h">24H</option>
</select>
</div>
<div />
</div>
) : activeTab === "calendar" ? (
isLoading ? (
<p>Loading connections...</p>
) : (
<>
<h3
style={{
marginBottom: "1rem",
fontSize: "1rem",
fontWeight: 600,
}}
>
{t.connectedCalendars}
</h3>
<div style={{ display: "flex", alignItems: "center", gap: "8px", marginBottom: "12px" }}>
<input type="checkbox" id="showCalendarProviderIcon"
checked={profile.showCalendarProviderIcon || false}
onChange={(e) => saveField("showCalendarProviderIcon", e.target.checked)}
style={{ width: "16px", height: "16px" }} />
<label htmlFor="showCalendarProviderIcon" style={{ fontSize: "0.9rem", fontWeight: 600 }}>
{t.showProviderIcon}
</label>
</div>
{connMsg && (
<div
style={{
padding: "8px 12px",
borderRadius: "4px",
marginBottom: "12px",
fontSize: "0.875rem",
background:
connMsg.type === "success"
? "rgba(16, 185, 129, 0.1)"
: "rgba(239, 68, 68, 0.1)",
color: connMsg.type === "success" ? "#059669" : "#dc2626",
border: `1px solid ${connMsg.type === "success" ? "#10b981" : "#ef4444"}`,
}}
>
{connMsg.text}
</div>
)}
{connections.length === 0 ? (
<p
style={{
color: "var(--weekly-text-light)",
marginBottom: "1.5rem",
}}
>
{t.noCalendars}
</p>
) : (
<ul
style={{
marginBottom: "1.5rem",
listStyle: "none",
padding: 0,
}}
>
{connections.map((conn) => (
<li
key={conn.id}
style={{
padding: "1rem 0",
borderBottom: "1px solid var(--weekly-border)",
}}
>
<div
style={{
marginBottom: "0.5rem",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<div
style={{
fontWeight: 600,
display: "flex",
alignItems: "center",
gap: "8px",
}}
>
<span>
{conn.provider === "google"
? <FontAwesomeIcon icon={faGoogle} />
: conn.provider === "apple"
? <FontAwesomeIcon icon={faApple} />
: conn.provider === "synology"
? <FontAwesomeIcon icon={faServer} />
: conn.provider === "notion"
? <FontAwesomeIcon icon={faNotion} />
: <FontAwesomeIcon icon={faMicrosoft} />}
</span>
{conn.provider === "google"
? "Google Calendar"
: conn.provider === "apple"
? "Apple Calendar"
: conn.provider === "synology"
? "Synology Calendar"
: conn.provider === "notion"
? "Notion"
: "Outlook Calendar"}
</div>
{confirmDisconnectId === conn.id ? (
<div
style={{
display: "flex",
alignItems: "center",
gap: "6px",
}}
>
<span
style={{
fontSize: "0.8rem",
color: "var(--weekly-text)",
}}
>
Sure?
</span>
<button
type="button"
onClick={async (e) => {
e.preventDefault();
e.stopPropagation();
setConfirmDisconnectId(null);
setDisconnectingId(conn.id);
try {
await onRemoveConnection(conn.id);
showConnMsg(
"success",
"Calendar disconnected.",
);
} catch (err: any) {
console.error("Failed to disconnect:", err);
showConnMsg(
"error",
err.message ||
"Failed to disconnect calendar",
);
} finally {
setDisconnectingId(null);
}
}}
style={{
padding: "3px 8px",
fontSize: "0.8rem",
background: "#dc2626",
color: "white",
border: "none",
borderRadius: "4px",
cursor: "pointer",
}}
>
Yes
</button>
<button
type="button"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setConfirmDisconnectId(null);
}}
style={{
padding: "3px 8px",
fontSize: "0.8rem",
background: "#e5e7eb",
color: "#374151",
border: "none",
borderRadius: "4px",
cursor: "pointer",
}}
>
No
</button>
</div>
) : (
<button
type="button"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setConfirmDisconnectId(conn.id);
}}
disabled={disconnectingId === conn.id}
style={{
padding: "4px 8px",
fontSize: "0.8rem",
color:
disconnectingId === conn.id
? "#999"
: "#dc2626",
background: "none",
border: `1px solid ${disconnectingId === conn.id ? "#999" : "#dc2626"}`,
borderRadius: "4px",
cursor:
disconnectingId === conn.id
? "not-allowed"
: "pointer",
opacity: disconnectingId === conn.id ? 0.7 : 1,
}}
>
{disconnectingId === conn.id
? "Disconnecting..."
: "Disconnect"}
</button>
)}
</div>
{/* Calendar Event Selection List */}
{conn.calendars &&
Array.isArray(conn.calendars) &&
conn.calendars.length > 0 ? (
<div style={{ paddingLeft: "8px" }}>
{/* Column Headers */}
<div
style={{
display: "flex",
alignItems: "center",
gap: "8px",
marginBottom: "6px",
paddingBottom: "4px",
borderBottom: "1px solid var(--weekly-border, #eee)",
}}
>
<span style={{ flex: 1, fontSize: "0.75rem", color: "var(--weekly-text-light)", fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.5px" }}>
Calendar
</span>
<span style={{ width: "50px", textAlign: "center", fontSize: "0.75rem", color: "var(--weekly-text-light)", fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.5px" }}>
Display
</span>
<span style={{ width: "50px", textAlign: "center", fontSize: "0.75rem", color: "var(--weekly-text-light)", fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.5px" }}>
Edit
</span>
</div>
{/* Calendar Rows */}
{conn.calendars.map((cal: any) => {
const isShared = /⚠/.test(cal.title);
const cleanTitle = cal.title
.replace(/\s*⚠️?\s*/g, "")
.trim();
return (
<div
key={cal.id}
style={{
display: "flex",
alignItems: "center",
gap: "8px",
padding: "3px 0",
}}
>
{/* Calendar Color + Name */}
<span
style={{
flex: 1,
fontSize: "0.9rem",
color: "var(--weekly-text)",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
display: "flex",
alignItems: "center",
gap: "6px",
}}
>
<span style={{
width: "10px", height: "10px", borderRadius: "50%", flexShrink: 0,
backgroundColor: cal.backgroundColor || cal.color || "#3b82f6",
}} />
{cleanTitle}
{isShared && (
<span title="Shared calendar" style={{ marginLeft: "4px", fontSize: "0.75rem", opacity: 0.5 }}>
🔗
</span>
)}
{cal.isPrimary && (
<span style={{ fontSize: "0.8em", color: "var(--weekly-text-light)", marginLeft: "4px" }}>
(Primary)
</span>
)}
</span>
{/* Display checkbox */}
<span style={{ width: "50px", textAlign: "center" }}>
<input
type="checkbox"
checked={cal.selected !== false}
onChange={(e) =>
handleUpdateCalendar(conn.id, cal.id, {
selected: e.target.checked,
})
}
style={{ cursor: "pointer" }}
/>
</span>
{/* Edit checkbox */}
<span style={{ width: "50px", textAlign: "center" }}>
<input
type="checkbox"
checked={cal.editable === true}
onChange={(e) =>
handleUpdateCalendar(conn.id, cal.id, {
editable: e.target.checked,
})
}
style={{ cursor: "pointer" }}
title="Allow adding/editing events"
/>
</span>
</div>
);
})}
</div>
) : (
<div
style={{
fontSize: "0.85rem",
color: "#888",
paddingLeft: "24px",
}}
>
{conn.provider === "google"
? t.noCalendarsFound
: conn.provider === "apple"
? t.noCalendarsApple
: conn.provider === "synology"
? t.noCalendarsSynology
: conn.provider === "notion"
? t.selectionAfterConnect
: t.selectionAfterConnect}
</div>
)}
</li>
))}
</ul>
)}
<h3
style={{
marginBottom: "1rem",
fontSize: "1rem",
fontWeight: 600,
}}
>
{t.connectMore}
</h3>
<div style={{ display: "flex", gap: "1rem", flexWrap: "wrap" }}>
<button
onClick={handleGoogleConnect}
className="calendar-connect-btn"
>
<FontAwesomeIcon icon={faGoogle} className="mr-2" /> {t.connectGoogle}
</button>
<button
onClick={handleAppleCalendarConnect}
className="calendar-connect-btn"
>
<FontAwesomeIcon icon={faApple} className="mr-2" /> {t.connectApple}
</button>
<button
onClick={handleOutlookConnect}
className="calendar-connect-btn"
>
<FontAwesomeIcon icon={faMicrosoft} className="mr-2" /> {t.connectOutlook}
</button>
<button
onClick={handleSynologyCalendarConnect}
className="calendar-connect-btn"
>
<FontAwesomeIcon icon={faServer} className="mr-2" /> {t.connectSynology || "Connect Synology"}
</button>
<button
onClick={handleNotionConnect}
className="calendar-connect-btn"
>
<FontAwesomeIcon icon={faNotion} className="mr-2" /> {t.connectNotion || "Connect Notion"}
</button>
</div>
<h3
style={{
marginBottom: "1rem",
fontSize: "1rem",
fontWeight: 600,
marginTop: "2rem",
}}
>
{t.syncTasks}
</h3>
<p
style={{
fontSize: "0.9rem",
color: "var(--weekly-text-light)",
marginBottom: "1rem",
}}
>
{t.syncTasksDesc}
</p>
<div
style={{
display: "flex",
flexDirection: "column",
gap: "1.5rem",
}}
>
{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 (
<div key={conn.id}>
<div
style={{
display: "flex",
alignItems: "center",
gap: "8px",
marginBottom: "0.5rem",
fontWeight: 600,
fontSize: "0.9rem",
}}
>
<span>
{conn.provider === "google" ? <FontAwesomeIcon icon={faGoogle} /> : conn.provider === "synology" ? <FontAwesomeIcon icon={faServer} /> : <FontAwesomeIcon icon={faMicrosoft} />}
</span>
{conn.provider === "google"
? "Google Tasks"
: conn.provider === "synology"
? "Synology Tasks"
: "Microsoft To-Do"}
{isFetching && (
<span
style={{
fontSize: "0.75rem",
fontWeight: 400,
color: "#888",
}}
>
(fetching lists...)
</span>
)}
</div>
<div
style={{
display: "flex",
flexDirection: "column",
gap: "4px",
}}
>
{/* 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 (
<div style={{
display: "flex",
alignItems: "center",
padding: "2px 8px",
fontSize: "0.75rem",
fontWeight: 600,
color: "var(--weekly-text-light, #888)",
textTransform: "uppercase",
letterSpacing: "0.05em",
}}>
<span style={{ flex: 1 }}>List</span>
{!allSynced && (
<button
onClick={() => handleSyncAll(
conn.provider as "google" | "outlook" | "synology",
providerLists,
true,
)}
disabled={importingTasksState}
style={{
background: "none",
border: "none",
cursor: "pointer",
fontSize: "0.7rem",
color: "var(--weekly-accent, #6366f1)",
padding: "2px 6px",
fontWeight: 500,
textTransform: "none",
}}
>{t.syncAll}</button>
)}
{!noneSynced && (
<button
onClick={() => handleSyncAll(
conn.provider as "google" | "outlook" | "synology",
providerLists,
false,
)}
disabled={importingTasksState}
style={{
background: "none",
border: "none",
cursor: "pointer",
fontSize: "0.7rem",
color: "#ef4444",
padding: "2px 6px",
fontWeight: 500,
textTransform: "none",
}}
>{t.unsyncAll}</button>
)}
<span style={{ width: "50px", textAlign: "center" }}>Sync</span>
</div>
);
})()}
{/* Inline unsync confirmation */}
{unsyncConfirm && unsyncConfirm.provider === conn.provider && (
<div style={{
display: "flex",
alignItems: "center",
gap: "8px",
padding: "8px 10px",
borderRadius: "6px",
background: "#fef2f2",
border: "1px solid #fecaca",
fontSize: "0.82rem",
color: "#991b1b",
}}>
<span style={{ flex: 1 }}>
{t.unsyncConfirmMsg.replace("{title}", unsyncConfirm.list.title)}
</span>
<button
onClick={onConfirmUnsync}
style={{
background: "#ef4444",
color: "#fff",
border: "none",
borderRadius: "4px",
padding: "4px 10px",
cursor: "pointer",
fontSize: "0.8rem",
fontWeight: 600,
whiteSpace: "nowrap",
}}
>{t.unsyncConfirm}</button>
<button
onClick={onCancelUnsync}
style={{
background: "none",
border: "1px solid #d1d5db",
borderRadius: "4px",
padding: "4px 10px",
cursor: "pointer",
fontSize: "0.8rem",
color: "#666",
whiteSpace: "nowrap",
}}
>{t.unsyncCancel}</button>
</div>
)}
{providerLists.map((list: { id: string; title: string }) => {
const isSynced = somedayLists.some(
(sl: SomedayList) =>
sl.externalId === list.id &&
sl.externalProvider === conn.provider,
);
return (
<div
key={list.id}
style={{
display: "flex",
alignItems: "center",
padding: "4px 8px",
borderRadius: "4px",
background: "rgba(0,0,0,0.02)",
}}
>
<span style={{ flex: 1, fontSize: "0.9rem" }}>
{list.title}
</span>
<span style={{ width: "50px", textAlign: "center" }}>
<input
type="checkbox"
checked={isSynced}
onChange={() =>
handleToggleTaskList(
conn.provider as
| "google"
| "outlook"
| "synology",
list,
)
}
disabled={importingTasksState}
/>
</span>
</div>
);
})}
{!isFetching && providerLists.length === 0 && (
<div
style={{
fontSize: "0.85rem",
color: "#888",
paddingLeft: "24px",
}}
>
No task lists found.
</div>
)}
</div>
</div>
);
})}
{connections.filter((c) =>
["google", "outlook", "synology"].includes(c.provider),
).length === 0 && (
<div
style={{
fontSize: "0.9rem",
color: "#888",
fontStyle: "italic",
}}
>
Connect a provider above to sync task lists.
</div>
)}
{importStatusMsg && (
<div
style={{
padding: "8px 12px",
borderRadius: "4px",
fontSize: "0.9rem",
background:
importStatusMsg.type === "success"
? "rgba(16, 185, 129, 0.1)"
: "rgba(239, 68, 68, 0.1)",
color:
importStatusMsg.type === "success"
? "#059669"
: "#dc2626",
border: `1px solid ${importStatusMsg.type === "success" ? "#10b981" : "#ef4444"}`,
}}
>
{importStatusMsg.text}
</div>
)}
</div>
</>
)
) : activeTab === "styling" ? (
<div
style={{ display: "flex", flexDirection: "column", gap: "24px" }}
>
{/* Mobile Font Scale */}
<div style={{
background: "var(--weekly-settings-item-bg)",
padding: "12px",
borderRadius: "8px",
}}>
<label style={{
display: "block", fontSize: "0.85rem", fontWeight: 600,
marginBottom: "8px", color: "var(--weekly-settings-label)",
}}>
{(profile.language || "en") === "de" ? "Mobile Schriftgröße" : "Mobile Font Scale"}
</label>
<div style={{ display: "flex", gap: "6px" }}>
{[
{ 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 => (
<button
key={opt.value}
onClick={() => {
setProfile({ ...profile, mobileFontScale: opt.value });
saveSetting("mobileFontScale", opt.value);
}}
style={{
flex: 1, padding: "6px 4px", borderRadius: "6px",
border: (profile.mobileFontScale || 1.0) === opt.value
? "2px solid var(--weekly-teal, #0ea5e9)"
: "1px solid var(--weekly-border, #ddd)",
background: (profile.mobileFontScale || 1.0) === opt.value
? "var(--weekly-teal, #0ea5e9)" : "transparent",
color: (profile.mobileFontScale || 1.0) === opt.value
? "white" : "var(--weekly-text)",
fontSize: "0.8rem", fontWeight: 600, cursor: "pointer",
}}
>
{opt.label}
</button>
))}
</div>
<span style={{ fontSize: "0.7rem", color: "var(--weekly-text-light)", marginTop: "4px", display: "block" }}>
{(profile.language || "en") === "de"
? "Skaliert alle Schriften auf Mobilgeräten (< 768px)"
: "Scales all fonts on mobile devices (< 768px)"}
</span>
</div>
{/* Typography Settings */}
<div
style={{
marginBottom: "1.5rem",
borderBottom: "1px solid var(--weekly-border)",
paddingBottom: "1rem",
}}
>
<label
style={{
display: "block",
fontSize: "1rem",
fontWeight: 700,
marginBottom: "12px",
color: "var(--weekly-settings-title)",
}}
>
{t.fontCustomization}
</label>
{/* Date Layout & Alignment side-by-side */}
<div
style={{
display: "grid",
gridTemplateColumns: "1fr 1fr",
gap: "16px",
marginBottom: "1.5rem",
background: "var(--weekly-settings-item-bg)",
padding: "12px",
borderRadius: "8px",
}}
>
<div>
<label
style={{
display: "block",
fontSize: "0.85rem",
fontWeight: 600,
color: "var(--weekly-settings-label)",
marginBottom: "8px",
}}
>
{t.dateLayout}
</label>
<select
value={profile.dateLayout || "right"}
onChange={(e) =>
saveField("dateLayout", e.target.value)
}
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)",
}}
>
<option value="right">{t.dateLayoutRight}</option>
<option value="left">{t.dateLayoutLeft}</option>
<option value="above">{t.dateLayoutAbove}</option>
<option value="below">{t.dateLayoutBelow}</option>
<option value="hidden">{t.dateLayoutHidden}</option>
</select>
</div>
<div>
<label
style={{
display: "block",
fontSize: "0.85rem",
fontWeight: 600,
color: "var(--weekly-settings-label)",
marginBottom: "8px",
}}
>
{t.dateLayoutMobile}
</label>
<select
value={profile.mobileDateLayout || "below"}
onChange={(e) =>
saveField("mobileDateLayout", e.target.value)
}
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)",
}}
>
<option value="right">{t.dateLayoutRight}</option>
<option value="left">{t.dateLayoutLeft}</option>
<option value="above">{t.dateLayoutAbove}</option>
<option value="below">{t.dateLayoutBelow}</option>
<option value="hidden">{t.dateLayoutHidden}</option>
</select>
</div>
<div>
<label
style={{
display: "block",
fontSize: "0.85rem",
fontWeight: 600,
color: "var(--weekly-settings-label)",
marginBottom: "8px",
}}
>
{t.dateAlignment || "Date Alignment"}
</label>
<select
value={profile.dateAlignment || "center"}
onChange={(e) =>
saveField("dateAlignment", e.target.value)
}
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)",
}}
>
<option value="left">{t.alignmentLeft || "Left"}</option>
<option value="center">{t.alignmentCenter || "Center"}</option>
<option value="right">{t.alignmentRight || "Right"}</option>
<option value="tight">{t.alignmentTight || "Tight"}</option>
</select>
</div>
<div>
<label
style={{
display: "block",
fontSize: "0.85rem",
fontWeight: 600,
color: "var(--weekly-settings-label)",
marginBottom: "8px",
}}
>
{t.dateVerticalAlign || "Date Vertical Alignment"}
</label>
<select
value={profile.dateVerticalAlign || "middle"}
onChange={(e) =>
saveField("dateVerticalAlign", e.target.value)
}
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)",
}}
>
<option value="top">{t.alignTop || "Top"}</option>
<option value="middle">{t.alignMiddle || "Middle"}</option>
<option value="bottom">{t.alignBottom || "Bottom"}</option>
</select>
</div>
</div>
{/* Day / Weekday Gap */}
<div
style={{
background: "var(--weekly-settings-item-bg)",
padding: "12px",
borderRadius: "8px",
marginBottom: "12px",
}}
>
<label
style={{
display: "block",
fontSize: "0.85rem",
fontWeight: 600,
color: "var(--weekly-settings-label)",
marginBottom: "8px",
}}
>
{t.dayWeekdayGap}
</label>
<input
type="text"
value={profile.dayHeaderGap || "0.35em"}
onChange={(e) =>
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)",
}}
/>
</div>
{/* Day Names */}
<div
style={{
background: "var(--weekly-settings-item-bg)",
padding: "12px",
borderRadius: "8px",
marginBottom: "12px",
}}
>
<label
style={{
display: "block",
fontSize: "0.85rem",
fontWeight: 600,
color: "var(--weekly-settings-label)",
marginBottom: "8px",
}}
>
{t.weekdayFont}
</label>
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
<div style={{ display: "flex", gap: "8px", alignItems: "center" }}>
<input
type="color"
value={profile.weekdayColor || "#888888"}
onChange={(e) => 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 }}
/>
<select
value={isCustomFont(profile.headlineFont || "") || profile.headlineFont === "__custom__" ? "__custom__" : (profile.headlineFont || "Inter")}
onChange={(e) => {
const val = e.target.value === "__custom__" ? "__custom__" : e.target.value;
saveField("headlineFont", val);
}}
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)" }}
>
{AVAILABLE_FONTS.map((font) => (
<option key={font.value} value={font.value} style={{ fontFamily: font.value }}>{font.name}</option>
))}
</select>
</div>
{(isCustomFont(profile.headlineFont || "") || profile.headlineFont === "__custom__") && (
<input
type="text"
value={profile.headlineFont === "__custom__" ? "" : (profile.headlineFont || "")}
onChange={(e) => 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)" }}
/>
)}
<div style={{ display: "flex", gap: "8px" }}>
<input
type="text"
value={profile.headlineFontSize || "1.25rem"}
onChange={(e) => 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)" }}
/>
<select
value={profile.headlineFontWeight || "900"}
onChange={(e) => saveField("headlineFontWeight", e.target.value)}
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)" }}
>
<option value="300">{t.weightLight}</option>
<option value="400">{t.weightNormal}</option>
<option value="500">{t.weightMedium}</option>
<option value="600">{t.weightSemi}</option>
<option value="700">{t.weightBold}</option>
<option value="900">{t.weightBlack}</option>
</select>
</div>
</div>
</div>
{/* Dates */}
<div
style={{
background: "var(--weekly-settings-item-bg)",
padding: "12px",
borderRadius: "8px",
marginBottom: "12px",
}}
>
<label
style={{
display: "block",
fontSize: "0.85rem",
fontWeight: 600,
color: "var(--weekly-settings-label)",
marginBottom: "8px",
}}
>
{t.dateFont}
</label>
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
<div style={{ display: "flex", gap: "8px", alignItems: "center" }}>
<input
type="color"
value={profile.dateColor || "#888888"}
onChange={(e) => 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 }}
/>
<select
value={isCustomFont(profile.dateFontFamily || "") || profile.dateFontFamily === "__custom__" ? "__custom__" : (profile.dateFontFamily || "Inter")}
onChange={(e) => {
const val = e.target.value === "__custom__" ? "__custom__" : e.target.value;
saveField("dateFontFamily", val);
}}
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)" }}
>
{AVAILABLE_FONTS.map((font) => (
<option key={font.value} value={font.value} style={{ fontFamily: font.value }}>{font.name}</option>
))}
</select>
</div>
{(isCustomFont(profile.dateFontFamily || "") || profile.dateFontFamily === "__custom__") && (
<input
type="text"
value={profile.dateFontFamily === "__custom__" ? "" : (profile.dateFontFamily || "")}
onChange={(e) => 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)" }}
/>
)}
<div style={{ display: "flex", gap: "8px" }}>
<input
type="text"
value={profile.dateFontSize || "0.65rem"}
onChange={(e) => 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)" }}
/>
<select
value={profile.dateFontWeight || "400"}
onChange={(e) => saveField("dateFontWeight", e.target.value)}
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)" }}
>
<option value="300">{t.weightLight}</option>
<option value="400">{t.weightNormal}</option>
<option value="500">{t.weightMedium}</option>
<option value="600">{t.weightSemi}</option>
<option value="700">{t.weightBold}</option>
</select>
</div>
</div>
</div>
{/* Tasks */}
<div
style={{
background: "var(--weekly-settings-item-bg)",
padding: "12px",
borderRadius: "8px",
marginBottom: "12px",
}}
>
<label
style={{
display: "block",
fontSize: "0.85rem",
fontWeight: 600,
color: "var(--weekly-settings-label)",
marginBottom: "8px",
}}
>
{t.taskFont}
</label>
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
<div style={{ display: "flex", gap: "8px", alignItems: "center" }}>
<input
type="color"
value={profile.taskColor || "#333333"}
onChange={(e) => 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 }}
/>
<select
value={isCustomFont(profile.taskFontFamily || "") || profile.taskFontFamily === "__custom__" ? "__custom__" : (profile.taskFontFamily || "Inter")}
onChange={(e) => {
const val = e.target.value === "__custom__" ? "__custom__" : e.target.value;
saveField("taskFontFamily", val);
saveField("timeTaskFontFamily", val);
}}
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)" }}
>
{AVAILABLE_FONTS.map((font) => (
<option key={font.value} value={font.value} style={{ fontFamily: font.value }}>{font.name}</option>
))}
</select>
</div>
{(isCustomFont(profile.taskFontFamily || "") || profile.taskFontFamily === "__custom__") && (
<input
type="text"
value={profile.taskFontFamily === "__custom__" ? "" : (profile.taskFontFamily || "")}
onChange={(e) => {
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)" }}
/>
)}
<div style={{ display: "flex", gap: "8px" }}>
<input
type="text"
value={profile.taskFontSize || "0.9rem"}
onChange={(e) => {
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)" }}
/>
<select
value={profile.taskFontWeight || "400"}
onChange={(e) => {
saveField("taskFontWeight", e.target.value);
saveField("timeTaskFontWeight", e.target.value);
}}
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)" }}
>
<option value="300">{t.weightLight}</option>
<option value="400">{t.weightNormal}</option>
<option value="500">{t.weightMedium}</option>
<option value="600">{t.weightSemi}</option>
<option value="700">{t.weightBold}</option>
</select>
</div>
</div>
</div>
{/* Calendar Event Font */}
<div
style={{
background: "var(--weekly-settings-item-bg)",
padding: "12px",
borderRadius: "8px",
marginBottom: "12px",
}}
>
<label
style={{
display: "block",
fontSize: "0.85rem",
fontWeight: 600,
color: "var(--weekly-settings-label)",
marginBottom: "8px",
}}
>
Calendar Event Font
</label>
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
<select
value={isCustomFont(profile.eventFontFamily || "") || profile.eventFontFamily === "__custom__" ? "__custom__" : (profile.eventFontFamily || "Inter")}
onChange={(e) => {
const val = e.target.value === "__custom__" ? "__custom__" : e.target.value;
saveField("eventFontFamily", val);
}}
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)" }}
>
{AVAILABLE_FONTS.map((font) => (
<option key={font.value} value={font.value} style={{ fontFamily: font.value }}>{font.name}</option>
))}
</select>
{(isCustomFont(profile.eventFontFamily || "") || profile.eventFontFamily === "__custom__") && (
<input
type="text"
value={profile.eventFontFamily === "__custom__" ? "" : (profile.eventFontFamily || "")}
onChange={(e) => 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)" }}
/>
)}
<div style={{ display: "flex", gap: "8px" }}>
<input
type="text"
value={profile.eventFontSize || "0.85rem"}
onChange={(e) => 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)" }}
/>
<select
value={profile.eventFontWeight || "400"}
onChange={(e) => saveField("eventFontWeight", e.target.value)}
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)" }}
>
<option value="300">{t.weightLight}</option>
<option value="400">{t.weightNormal}</option>
<option value="500">{t.weightMedium}</option>
<option value="600">{t.weightSemi}</option>
<option value="700">{t.weightBold}</option>
</select>
</div>
</div>
</div>
{/* Goal Font */}
<div
style={{
background: "var(--weekly-settings-item-bg)",
padding: "12px",
borderRadius: "8px",
marginBottom: "12px",
}}
>
<label
style={{
display: "block",
fontSize: "0.85rem",
fontWeight: 600,
color: "var(--weekly-settings-label)",
marginBottom: "8px",
}}
>
Goal Font
</label>
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
<select
value={isCustomFont(profile.goalFontFamily || "") || profile.goalFontFamily === "__custom__" ? "__custom__" : (profile.goalFontFamily || "Inter")}
onChange={(e) => {
const val = e.target.value === "__custom__" ? "__custom__" : e.target.value;
saveField("goalFontFamily", val);
}}
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)" }}
>
{AVAILABLE_FONTS.map((font) => (
<option key={font.value} value={font.value} style={{ fontFamily: font.value }}>{font.name}</option>
))}
</select>
{(isCustomFont(profile.goalFontFamily || "") || profile.goalFontFamily === "__custom__") && (
<input
type="text"
value={profile.goalFontFamily === "__custom__" ? "" : (profile.goalFontFamily || "")}
onChange={(e) => 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)" }}
/>
)}
<div style={{ display: "flex", gap: "8px" }}>
<input
type="text"
value={profile.goalFontSize || "1rem"}
onChange={(e) => 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)" }}
/>
<select
value={profile.goalFontWeight || "400"}
onChange={(e) => saveField("goalFontWeight", e.target.value)}
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)" }}
>
<option value="300">{t.weightLight}</option>
<option value="400">{t.weightNormal}</option>
<option value="500">{t.weightMedium}</option>
<option value="600">{t.weightSemi}</option>
<option value="700">{t.weightBold}</option>
</select>
</div>
</div>
</div>
{/* Calendar Week Font */}
<div
style={{
background: "var(--weekly-settings-item-bg)",
padding: "12px",
borderRadius: "8px",
marginBottom: "12px",
}}
>
<label
style={{
display: "block",
fontSize: "0.85rem",
fontWeight: 600,
color: "var(--weekly-settings-label)",
marginBottom: "8px",
}}
>
Calendar Week (KW)
</label>
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
<div style={{ display: "flex", gap: "8px", alignItems: "center" }}>
<input
type="color"
value={profile.cwColor || "#333333"}
onChange={(e) => 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 }}
/>
<select
value={isCustomFont(profile.cwFontFamily || "") || profile.cwFontFamily === "__custom__" ? "__custom__" : (profile.cwFontFamily || "Inter")}
onChange={(e) => {
const val = e.target.value === "__custom__" ? "__custom__" : e.target.value;
saveField("cwFontFamily", val);
}}
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)" }}
>
{AVAILABLE_FONTS.map((font) => (
<option key={font.value} value={font.value} style={{ fontFamily: font.value }}>{font.name}</option>
))}
</select>
</div>
{(isCustomFont(profile.cwFontFamily || "") || profile.cwFontFamily === "__custom__") && (
<input
type="text"
value={profile.cwFontFamily === "__custom__" ? "" : (profile.cwFontFamily || "")}
onChange={(e) => 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)" }}
/>
)}
<div style={{ display: "flex", gap: "8px" }}>
<input
type="text"
value={profile.cwFontSize || "1.125rem"}
onChange={(e) => 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)" }}
/>
<select
value={profile.cwFontWeight || "700"}
onChange={(e) => saveField("cwFontWeight", e.target.value)}
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)" }}
>
<option value="300">{t.weightLight}</option>
<option value="400">{t.weightNormal}</option>
<option value="500">{t.weightMedium}</option>
<option value="600">{t.weightSemi}</option>
<option value="700">{t.weightBold}</option>
<option value="900">{t.weightBlack}</option>
</select>
</div>
</div>
</div>
{/* Year Font */}
<div
style={{
background: "var(--weekly-settings-item-bg)",
padding: "12px",
borderRadius: "8px",
marginBottom: "12px",
}}
>
<label
style={{
display: "block",
fontSize: "0.85rem",
fontWeight: 600,
color: "var(--weekly-settings-label)",
marginBottom: "8px",
}}
>
Year
</label>
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
<div style={{ display: "flex", gap: "8px", alignItems: "center" }}>
<input
type="color"
value={profile.yearColor || "#333333"}
onChange={(e) => 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 }}
/>
<select
value={isCustomFont(profile.yearFontFamily || "") || profile.yearFontFamily === "__custom__" ? "__custom__" : (profile.yearFontFamily || "Inter")}
onChange={(e) => {
const val = e.target.value === "__custom__" ? "__custom__" : e.target.value;
saveField("yearFontFamily", val);
}}
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)" }}
>
{AVAILABLE_FONTS.map((font) => (
<option key={font.value} value={font.value} style={{ fontFamily: font.value }}>{font.name}</option>
))}
</select>
</div>
{(isCustomFont(profile.yearFontFamily || "") || profile.yearFontFamily === "__custom__") && (
<input
type="text"
value={profile.yearFontFamily === "__custom__" ? "" : (profile.yearFontFamily || "")}
onChange={(e) => 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)" }}
/>
)}
<div style={{ display: "flex", gap: "8px" }}>
<input
type="text"
value={profile.yearFontSize || "1.125rem"}
onChange={(e) => 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)" }}
/>
<select
value={profile.yearFontWeight || "700"}
onChange={(e) => saveField("yearFontWeight", e.target.value)}
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)" }}
>
<option value="300">{t.weightLight}</option>
<option value="400">{t.weightNormal}</option>
<option value="500">{t.weightMedium}</option>
<option value="600">{t.weightSemi}</option>
<option value="700">{t.weightBold}</option>
<option value="900">{t.weightBlack}</option>
</select>
</div>
</div>
</div>
</div>
{/* Terminal Theme Import/Export */}
<div
style={{
background: "var(--weekly-settings-item-bg)",
padding: "16px",
borderRadius: "8px",
marginBottom: "12px",
border: "1px solid var(--weekly-border)",
}}
>
<label
style={{
display: "block",
fontSize: "0.95rem",
fontWeight: 700,
color: "var(--weekly-settings-title)",
marginBottom: "8px",
}}
>
Terminal Color Theme
</label>
<p style={{ fontSize: "0.8rem", color: "var(--weekly-settings-label)", marginBottom: "16px" }}>
Import or Export standard Terminal 16-color JSON themes (e.g. Gogh, terminal.sexy) to completely change the app colors.
</p>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "16px" }}>
{/* Light Theme */}
<div style={{ display: "flex", flexDirection: "column", gap: "8px", padding: "12px", background: "rgba(0,0,0,0.03)", borderRadius: "6px" }}>
<label style={{ fontSize: "0.85rem", fontWeight: 600, color: "var(--weekly-settings-title)" }}> Light Mode Theme</label>
<div style={{ display: "flex", gap: "8px" }}>
<label className="weekly-btn-secondary" style={{ flex: 1, textAlign: "center", cursor: "pointer", fontSize: "0.8rem", padding: "6px" }}>
Import JSON
<input
type="file"
accept=".json"
style={{ display: "none" }}
onChange={(e) => {
const file = e.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = async (evt) => {
try {
const text = evt.target?.result as string;
let json = JSON.parse(text);
// Normalizing standard terminal formats to a consistent internal format
const normalized: any = {
background: json.background,
foreground: json.foreground,
cursorColor: json.cursorColor || json.cursor,
};
// Handle Tabby-style colors array
if (Array.isArray(json.colors)) {
json.colors.forEach((c: string, i: number) => {
normalized[`color${i}`] = c;
});
} else {
// Handle Gogh/terminal.sexy color0...color15 keys
for (let i = 0; i < 16; i++) {
if (json[`color${i}`]) normalized[`color${i}`] = json[`color${i}`];
}
}
saveField("lightTheme", normalized);
} catch (err) {
alert("Invalid JSON format.");
}
};
reader.readAsText(file);
e.target.value = '';
}}
/>
</label>
<button
className="weekly-btn-outline"
onClick={() => {
// Fix: access profile from state properly
const currentTheme = (profile as any).lightTheme || {};
const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(currentTheme, null, 2));
const anchor = document.createElement("a");
anchor.href = dataStr;
anchor.download = "light-theme.json";
anchor.click();
}}
style={{ flex: 1, fontSize: "0.8rem", padding: "6px" }}
>
Export
</button>
</div>
{((profile as any).lightTheme) && (
<button
onClick={() => saveField("lightTheme", null)}
style={{ fontSize: "0.75rem", color: "#dc2626", background: "none", border: "none", cursor: "pointer", textAlign: "left", marginTop: "4px" }}
>
Clear Light Theme
</button>
)}
{/* Color 16-grid Preview */}
{(profile as any).lightTheme && (
<div style={{ display: "grid", gridTemplateColumns: "repeat(8, 1fr)", gap: "2px", marginTop: "8px" }}>
{[...Array(16)].map((_, i) => (
<div key={i} title={`color${i}`} style={{ width: "100%", height: "12px", background: (profile as any).lightTheme[`color${i}`] || "#ccc", borderRadius: "2px" }} />
))}
</div>
)}
</div>
{/* Dark Theme */}
<div style={{ display: "flex", flexDirection: "column", gap: "8px", padding: "12px", background: "rgba(0,0,0,0.2)", borderRadius: "6px" }}>
<label style={{ fontSize: "0.85rem", fontWeight: 600, color: "var(--weekly-settings-title)" }}>🌙 Dark Mode Theme</label>
<div style={{ display: "flex", gap: "8px" }}>
<label className="weekly-btn-secondary" style={{ flex: 1, textAlign: "center", cursor: "pointer", fontSize: "0.8rem", padding: "6px" }}>
Import JSON
<input
type="file"
accept=".json"
style={{ display: "none" }}
onChange={(e) => {
const file = e.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = async (evt) => {
try {
const text = evt.target?.result as string;
let json = JSON.parse(text);
const normalized: any = {
background: json.background,
foreground: json.foreground,
cursorColor: json.cursorColor || json.cursor,
};
if (Array.isArray(json.colors)) {
json.colors.forEach((c: string, i: number) => {
normalized[`color${i}`] = c;
});
} else {
for (let i = 0; i < 16; i++) {
if (json[`color${i}`]) normalized[`color${i}`] = json[`color${i}`];
}
}
saveField("darkTheme", normalized);
} catch (err) {
alert("Invalid JSON format.");
}
};
reader.readAsText(file);
e.target.value = '';
}}
/>
</label>
<button
className="weekly-btn-outline"
onClick={() => {
const currentTheme = (profile as any).darkTheme || {};
const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(currentTheme, null, 2));
const anchor = document.createElement("a");
anchor.href = dataStr;
anchor.download = "dark-theme.json";
anchor.click();
}}
style={{ flex: 1, fontSize: "0.8rem", padding: "6px" }}
>
Export
</button>
</div>
{((profile as any).darkTheme) && (
<button
onClick={() => saveField("darkTheme", null)}
style={{ fontSize: "0.75rem", color: "#ef4444", background: "none", border: "none", cursor: "pointer", textAlign: "left", marginTop: "4px" }}
>
Clear Dark Theme
</button>
)}
{/* Color 16-grid Preview */}
{(profile as any).darkTheme && (
<div style={{ display: "grid", gridTemplateColumns: "repeat(8, 1fr)", gap: "2px", marginTop: "8px" }}>
{[...Array(16)].map((_, i) => (
<div key={i} title={`color${i}`} style={{ width: "100%", height: "12px", background: (profile as any).darkTheme[`color${i}`] || "#ccc", borderRadius: "2px" }} />
))}
</div>
)}
</div>
</div>
</div>
{/* Element Colors */}
<div
style={{
background: "var(--weekly-settings-item-bg)",
padding: "12px",
borderRadius: "8px",
marginBottom: "12px",
}}
>
<label
style={{
display: "block",
fontSize: "0.85rem",
fontWeight: 600,
color: "var(--weekly-settings-label)",
marginBottom: "8px",
}}
>
Element Colors
</label>
<div
style={{
display: "grid",
gridTemplateColumns: "1fr 1fr",
gap: "12px",
}}
>
<div>
<label
style={{
display: "block",
fontSize: "0.75rem",
color: "var(--weekly-settings-label)",
marginBottom: "4px",
}}
>
Today Highlight
</label>
<input
type="color"
value={profile.todayHighlightColor || "#f0fafa"}
onChange={(e) =>
saveFieldDebounced("todayHighlightColor", e.target.value)
}
style={{
width: "100%",
height: "30px",
cursor: "pointer",
border: "none",
background: "transparent",
}}
/>
</div>
<div>
<label
style={{
display: "block",
fontSize: "0.75rem",
color: "var(--weekly-settings-label)",
marginBottom: "4px",
}}
>
Past Days
</label>
<input
type="color"
value={profile.pastDayColor || "#a6a6a7"}
onChange={(e) =>
saveFieldDebounced("pastDayColor", e.target.value)
}
style={{
width: "100%",
height: "30px",
cursor: "pointer",
border: "none",
background: "transparent",
}}
/>
</div>
</div>
</div>
{/* Weekend Colors */}
<div
style={{
background: "var(--weekly-settings-item-bg)",
padding: "12px",
borderRadius: "8px",
marginBottom: "12px",
}}
>
<label
style={{
display: "block",
fontSize: "0.85rem",
fontWeight: 600,
color: "var(--weekly-settings-label)",
marginBottom: "8px",
}}
>
Weekend Highlight Colors
</label>
<div
style={{
display: "grid",
gridTemplateColumns: "1fr 1fr",
gap: "12px",
}}
>
<div>
<label
style={{
display: "block",
fontSize: "0.75rem",
color: "var(--weekly-settings-label)",
marginBottom: "4px",
}}
>
Saturday
</label>
<input
type="color"
value={profile.weekendColorSat || "#666666"}
onChange={(e) =>
saveFieldDebounced("weekendColorSat", e.target.value)
}
style={{
width: "100%",
height: "30px",
cursor: "pointer",
border: "none",
background: "transparent",
}}
/>
</div>
<div>
<label
style={{
display: "block",
fontSize: "0.75rem",
color: "var(--weekly-settings-label)",
marginBottom: "4px",
}}
>
Sunday
</label>
<input
type="color"
value={profile.weekendColorSun || "#dc2626"}
onChange={(e) =>
saveFieldDebounced("weekendColorSun", e.target.value)
}
style={{
width: "100%",
height: "30px",
cursor: "pointer",
border: "none",
background: "transparent",
}}
/>
</div>
</div>
</div>
{/* CSS Variables */}
<CssVariablesEditor profile={profile} setProfile={setProfile} saveSetting={saveSetting} />
{/* Custom CSS */}
<div
style={{
background: "var(--weekly-settings-item-bg)",
padding: "12px",
borderRadius: "8px",
marginBottom: "12px",
}}
>
<label
style={{
display: "block",
fontSize: "0.85rem",
fontWeight: 600,
color: "var(--weekly-settings-label)",
marginBottom: "4px",
}}
>
{profile.language === "de" ? "Eigenes CSS" : "Custom CSS"}
</label>
<p style={{ fontSize: "0.72rem", color: "var(--weekly-settings-label)", margin: "0 0 8px", opacity: 0.8 }}>
{profile.language === "de"
? "Wird nach dem Stylesheet geladen und kann jede Regel überschreiben."
: "Loaded after the stylesheet — can override any rule."}
</p>
<textarea
value={profile.customCss || ""}
onChange={(e) => saveFieldDebounced("customCss", e.target.value)}
rows={10}
spellCheck={false}
placeholder={".weekly-task-item.completed .weekly-task-text {\n color: #cccccc;\n}"}
style={{
width: "100%",
fontFamily: "monospace",
fontSize: "0.75rem",
background: "var(--weekly-settings-input-bg)",
color: "var(--weekly-settings-text)",
border: "1px solid var(--weekly-settings-input-border)",
borderRadius: "6px",
padding: "8px",
resize: "vertical",
boxSizing: "border-box",
}}
/>
</div>
{/* All styling settings auto-save */}
</div>
) : activeTab === "motivation" ? (
<div
style={{ display: "flex", flexDirection: "column", gap: "24px" }}
>
{/* Replaced Goal of the Week settings block */}
{/* "Do This Now" Toggle */}
<div
style={{
display: "flex",
alignItems: "center",
gap: "8px",
marginBottom: "8px",
}}
>
<input
type="checkbox"
id="showNextTaskMotivation"
checked={showNextTask}
onChange={(e) => {
const newVal = e.target.checked;
setShowNextTask(newVal);
saveSetting("showNextTask", newVal);
}}
style={{ width: "20px", height: "20px", cursor: "pointer" }}
/>
<label
htmlFor="showNextTaskMotivation"
style={{
fontSize: "1rem",
fontWeight: 500,
color: "var(--weekly-settings-title)",
cursor: "pointer",
}}
>
{t.showDoThisNow}
</label>
</div>
{/* Focus Timer Settings moved here */}
<div style={{ display: "flex", gap: "16px" }}>
<div style={{ flex: 1 }}>
<label
style={{
display: "block",
marginBottom: "8px",
fontSize: "1rem",
fontWeight: 600,
color: "var(--weekly-settings-label)",
}}
>
{t.focusTimer}
</label>
<input
type="number"
min="1"
max="120"
value={profile.focusTimerDuration || 25}
onChange={(e) => 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",
}}
/>
</div>
<div style={{ flex: 1 }}>
<label
style={{
display: "block",
marginBottom: "8px",
fontSize: "1rem",
fontWeight: 600,
color: "var(--weekly-settings-label)",
}}
>
{t.focusBreak}
</label>
<input
type="number"
min="1"
max="60"
value={profile.focusBreakDuration || 5}
onChange={(e) => 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",
}}
/>
</div>
</div>
{/* Goal Scope Redesign */}
<div
style={{
background: "var(--weekly-settings-item-bg)",
padding: "20px",
borderRadius: "12px",
border: "1px solid var(--weekly-border)",
}}
>
<h3
style={{
fontSize: "1rem",
fontWeight: 600,
marginBottom: "16px",
color: "var(--weekly-settings-title)",
}}
>
{t.goalScopeTitle}
</h3>
<div
style={{
display: "flex",
background: "var(--weekly-bg)",
padding: "4px",
borderRadius: "8px",
border: "1px solid var(--weekly-border)",
}}
>
<button
type="button"
onClick={() => saveField("goalScope", "week")}
style={{
flex: 1,
padding: "10px",
borderRadius: "6px",
border: profile.goalScope === "week" ? "2px solid var(--weekly-teal)" : "none",
background:
profile.goalScope === "week"
? "var(--weekly-settings-toggle-active-bg)"
: "transparent",
color:
profile.goalScope === "week"
? "var(--weekly-settings-toggle-active-text)"
: "var(--weekly-settings-text)",
fontWeight: 700,
boxShadow: profile.goalScope === "week" ? "0 2px 4px rgba(0,0,154,0.1)" : "none",
cursor: "pointer",
transition: "all 0.2s",
}}
>
{t.goalScopeWeek}
</button>
<button
type="button"
onClick={() => saveField("goalScope", "day")}
style={{
flex: 1,
padding: "10px",
borderRadius: "6px",
border: profile.goalScope === "day" ? "2px solid var(--weekly-teal)" : "none",
background:
profile.goalScope === "day"
? "var(--weekly-settings-toggle-active-bg)"
: "transparent",
color:
profile.goalScope === "day"
? "var(--weekly-settings-toggle-active-text)"
: "var(--weekly-settings-text)",
fontWeight: 700,
boxShadow: profile.goalScope === "day" ? "0 2px 4px rgba(0,154,154,0.1)" : "none",
cursor: "pointer",
transition: "all 0.2s",
}}
>
{t.goalScopeDay}
</button>
</div>
</div>
{/* Fallback Section */}
<div
style={{
background: "var(--weekly-settings-item-bg)",
padding: "20px",
borderRadius: "12px",
border: "1px solid var(--weekly-border)",
}}
>
<h3
style={{
fontSize: "1rem",
fontWeight: 600,
marginBottom: "8px",
color: "var(--weekly-settings-title)",
}}
>
{t.goalFallbackTitle}
</h3>
<div style={{ marginBottom: "12px" }}>
<label
style={{
display: "block",
fontSize: "0.85rem",
color: "var(--weekly-settings-label)",
marginBottom: "8px",
}}
>
{t.goalFallback}
</label>
<select
value={profile.goalFallbackType || "quote"}
onChange={(e) => saveField("goalFallbackType", e.target.value)}
className="weekly-input"
style={{
width: "100%",
padding: "12px",
borderRadius: "6px",
border: "1px solid var(--weekly-border)",
background: "var(--weekly-bg)",
fontSize: "1rem",
}}
>
<option value="quote">
{t.motivationalQuote}
</option>
<option value="next_todo">{t.nextTodo}</option>
<option value="default">{t.defaultText}</option>
</select>
</div>
{(!profile.goalFallbackType || profile.goalFallbackType === "quote") && (
<div style={{ marginTop: "12px" }}>
<label
style={{
display: "block",
fontSize: "0.9rem",
marginBottom: "4px",
color: "var(--weekly-settings-label)",
}}
>
{t.apiDataSources}
</label>
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
{(profile.quoteSourceUrls || [profile.quoteSourceUrl || ""]).map((url: string, idx: number) => (
<div key={idx} style={{ display: "flex", gap: "8px" }}>
<input
type="text"
value={url}
onChange={(e) => {
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)",
}}
/>
<button
type="button"
onClick={() => {
const newUrls = (profile.quoteSourceUrls || [profile.quoteSourceUrl || ""]).filter((_val: string, i: number) => i !== idx);
saveField("quoteSourceUrls", newUrls);
}}
style={{
padding: "8px",
background: "#fee2e2",
color: "#ef4444",
border: "none",
borderRadius: "6px",
cursor: "pointer"
}}
>
<Trash2 size={16} />
</button>
</div>
))}
<button
type="button"
onClick={() => {
const newUrls = [...(profile.quoteSourceUrls || [profile.quoteSourceUrl || ""]), ""];
saveField("quoteSourceUrls", newUrls);
}}
style={{
alignSelf: "flex-start",
marginTop: "4px",
padding: "6px 12px",
fontSize: "0.85rem",
background: "var(--weekly-teal)",
color: "white",
border: "none",
borderRadius: "6px",
cursor: "pointer",
display: "flex",
alignItems: "center",
gap: "4px"
}}
>
<Plus size={14} /> {t.addSource}
</button>
{/* Preset sources */}
<div style={{ marginTop: "8px" }}>
<p style={{ fontSize: "0.75rem", color: "var(--weekly-text-light)", marginBottom: "6px" }}>
{profile.language === "de" ? "Bekannte Quellen (klicken zum Hinzufügen):" : "Known sources (click to add):"}
</p>
<div style={{ display: "flex", flexWrap: "wrap", gap: "6px" }}>
{[
{ label: "ZenQuotes (EN)", url: "https://zenquotes.io/api/random" },
{ label: "Stoic Quotes (EN)", url: "https://stoic.tekloon.net/stoic-quote" },
{ label: "Quotable (EN)", url: "https://api.quotable.io/quotes/random" },
{ label: "Advice Slip (EN)", url: "https://api.adviceslip.com/advice" },
{ label: "Zitat-Service (DE)", url: "https://api.zitat-service.de/v1/quote?language=de" },
{ label: "Zitat-Service (EN)", url: "https://api.zitat-service.de/v1/quote?language=en" },
{ label: "Zitat-Service (ES)", url: "https://api.zitat-service.de/v1/quote?language=es" },
].map(({ label, url }) => {
const current: string[] = profile.quoteSourceUrls || [];
const already = current.includes(url);
return (
<button
key={url}
type="button"
disabled={already}
onClick={() => {
if (already) return;
const newUrls = [...current, url];
saveField("quoteSourceUrls", newUrls);
}}
style={{
padding: "3px 8px",
fontSize: "0.72rem",
borderRadius: "12px",
border: "1px solid var(--weekly-border)",
background: already ? "var(--weekly-teal)" : "var(--weekly-bg)",
color: already ? "white" : "var(--weekly-text)",
cursor: already ? "default" : "pointer",
opacity: already ? 0.7 : 1,
}}
>
{already ? "✓ " : "+ "}{label}
</button>
);
})}
</div>
<p style={{ fontSize: "0.72rem", color: "var(--weekly-text-light)", marginTop: "6px", fontStyle: "italic" }}>
{profile.language === "de"
? "DE/EN/ES: auch über Zitat-Service API verfügbar. FR/IT: kuratierte lokale Sammlung."
: "DE/EN/ES: also available via Zitat-Service API. FR/IT: curated local collection."}
</p>
</div>
</div>
<p style={{ fontSize: "0.75rem", color: "var(--weekly-text-light)", marginTop: "4px" }}>
{t.urlFormatHelp}
</p>
<p style={{ fontSize: "0.75rem", color: "var(--weekly-settings-label)", marginTop: "8px", fontStyle: "italic" }}>
{t.quoteFallbackDesc}
</p>
<div style={{ marginTop: "12px" }}>
<label
style={{
display: "block",
fontSize: "0.9rem",
marginBottom: "6px",
color: "var(--weekly-settings-label)",
}}
>
{t.quoteLanguages}
</label>
<div style={{ display: "flex", flexWrap: "wrap", gap: "8px" }}>
{[
{ 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 (
<label
key={lang.code}
style={{
display: "flex",
alignItems: "center",
gap: "4px",
fontSize: "0.85rem",
cursor: "pointer",
padding: "4px 10px",
borderRadius: "6px",
border: selected ? "1px solid var(--weekly-teal)" : "1px solid var(--weekly-border)",
background: selected ? "var(--weekly-teal)" : "transparent",
color: selected ? "white" : "var(--weekly-text)",
transition: "all 0.15s ease",
}}
>
<input
type="checkbox"
checked={selected}
onChange={() => {
const current = profile.quoteLanguages || ["en", "de"];
const updated = selected
? current.filter((c: string) => c !== lang.code)
: [...current, lang.code];
if (updated.length > 0) {
saveField("quoteLanguages", updated);
}
}}
style={{ display: "none" }}
/>
{lang.label}
</label>
);
})}
</div>
<p style={{ fontSize: "0.75rem", color: "var(--weekly-text-light)", marginTop: "4px" }}>
{t.quoteLanguagesDesc}
</p>
</div>
</div>
)}
{profile.goalFallbackType === "default" && (
<div style={{ marginTop: "12px" }}>
<label
style={{
display: "block",
fontSize: "0.9rem",
marginBottom: "4px",
color: "var(--weekly-settings-label)",
}}
>
{t.defaultText}
</label>
<input
type="text"
value={profile.goalDefaultSentence || ""}
onChange={(e) => saveFieldDebounced("goalDefaultSentence", e.target.value)}
className="weekly-input"
style={{
width: "100%",
padding: "12px",
borderRadius: "6px",
border: "1px solid var(--weekly-border)",
}}
placeholder={t.defaultGoalPlaceholder}
/>
</div>
)}
</div>
</div>
) : activeTab === "sync" ? (
<CalendarSyncRulesPanel
connections={connections}
t={t}
/>
) : activeTab === "projects" ? (
<div style={{ display: "flex", flexDirection: "column", gap: "16px" }}>
<div>
<h3 style={{ fontSize: "1rem", fontWeight: 600, margin: 0, display: "flex", alignItems: "center", gap: "8px" }}>
<FolderOpen size={18} /> {t.projects || "Projects"}
</h3>
<p style={{ fontSize: "0.8rem", color: "var(--weekly-settings-label)", marginTop: "4px" }}>
{t.projectsDesc || "Organize tasks with color-coded projects"}
</p>
</div>
{/* Existing projects list */}
{projects.length === 0 ? (
<div style={{ textAlign: "center", padding: "24px 12px", border: "1px dashed var(--weekly-border, #e5e7eb)", borderRadius: "10px" }}>
<ProjectIcon icon="folder" size={28} color="#aaa" />
<p style={{ fontSize: "0.85rem", color: "#aaa", fontStyle: "italic", marginTop: "8px" }}>
{t.noProjects || "No projects yet"}
</p>
</div>
) : (
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
{projects.map((p) => (
<div key={p.id} style={{
display: "flex", alignItems: "center", gap: "10px",
padding: "10px 14px", borderRadius: "10px",
background: "var(--weekly-bg-soft, #f9fafb)",
borderLeft: `4px solid ${p.color || "#999"}`,
}}>
{editingProjectId === p.id ? (
<div style={{ display: "flex", flexDirection: "column", gap: "8px", width: "100%" }}>
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<div style={{ position: "relative" }}>
<button onClick={() => setShowEditProjectIconPicker(!showEditProjectIconPicker)} style={{ width: "38px", height: "38px", borderRadius: "8px", border: "1px solid var(--weekly-border, #e5e7eb)", background: "var(--weekly-bg, #fff)", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center" }}>
<ProjectIcon icon={editProjectIcon} size={16} color="#555" />
</button>
{showEditProjectIconPicker && (
<div style={{ position: "absolute", top: "100%", left: 0, marginTop: "4px", zIndex: 50 }}>
<IconPicker selectedIcon={editProjectIcon} onSelect={(name) => { setEditProjectIcon(name); setShowEditProjectIconPicker(false); }} darkMode={false} />
</div>
)}
</div>
<input
type="text"
value={editProjectName}
onChange={(e) => setEditProjectName(e.target.value)}
className="weekly-input"
style={{ flex: 1, padding: "8px 12px", fontSize: "0.9rem" }}
onKeyDown={(e) => {
if (e.key === "Enter") {
fetch("/api/projects", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id: p.id, name: editProjectName, color: editProjectColor, icon: editProjectIcon }) })
.then(() => { onProjectsChanged(); setEditingProjectId(null); });
}
if (e.key === "Escape") setEditingProjectId(null);
}}
autoFocus
/>
</div>
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<input type="color" value={editProjectColor} onChange={(e) => setEditProjectColor(e.target.value)} style={{ width: "30px", height: "30px", border: "none", cursor: "pointer", padding: 0, borderRadius: "6px" }} />
<span style={{ fontSize: "0.8rem", color: "var(--weekly-settings-label)" }}>{profile.language === "de" ? "Farbe" : "Color"}</span>
<div style={{ flex: 1 }} />
<button onClick={() => setEditingProjectId(null)} style={{ padding: "6px 12px", fontSize: "0.8rem", background: "none", border: "1px solid var(--weekly-border, #ddd)", borderRadius: "8px", cursor: "pointer" }}>
{profile.language === "de" ? "Abbrechen" : "Cancel"}
</button>
<button
onClick={() => {
fetch("/api/projects", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id: p.id, name: editProjectName, color: editProjectColor, icon: editProjectIcon }) })
.then(() => { onProjectsChanged(); setEditingProjectId(null); });
}}
className="weekly-btn-primary"
style={{ padding: "6px 12px", fontSize: "0.8rem", display: "inline-flex", alignItems: "center", gap: "4px" }}
>
<Check size={14} /> {profile.language === "de" ? "Speichern" : "Save"}
</button>
</div>
</div>
) : (
<>
<ProjectIcon icon={p.icon} size={20} color={p.color || "#999"} />
<div style={{ flex: 1, minWidth: 0 }}>
<span style={{ fontSize: "0.9rem", fontWeight: 600, display: "block" }}>{p.name}</span>
</div>
<button
onClick={() => {
setEditingProjectId(p.id);
setEditProjectName(p.name);
setEditProjectColor(p.color || "#3b82f6");
setEditProjectIcon(p.icon || "folder");
setShowEditProjectIconPicker(false);
}}
style={{ padding: "6px", opacity: 0.6, cursor: "pointer", background: "none", border: "none", borderRadius: "6px" }}
title={profile.language === "de" ? "Bearbeiten" : "Edit"}
>
<Pencil size={14} />
</button>
<button
onClick={() => {
const msg = profile.language === "de" ? `Projekt "${p.name}" löschen?` : `Delete project "${p.name}"?`;
if (confirm(msg)) {
fetch(`/api/projects?id=${p.id}`, { method: "DELETE" }).then(() => onProjectsChanged());
}
}}
style={{ padding: "6px", opacity: 0.6, cursor: "pointer", color: "#ef4444", background: "none", border: "none", borderRadius: "6px" }}
title={profile.language === "de" ? "Löschen" : "Delete"}
>
<Trash2 size={14} />
</button>
</>
)}
</div>
))}
</div>
)}
{/* Add new project form */}
<div style={{ borderTop: "1px solid var(--weekly-border, #e5e7eb)", paddingTop: "16px" }}>
<p style={{ fontSize: "0.8rem", fontWeight: 600, color: "var(--weekly-settings-label)", marginBottom: "10px" }}>
{profile.language === "de" ? "Projekt hinzufügen" : "Add Project"}
</p>
<div style={{ display: "flex", gap: "8px", alignItems: "center" }}>
<div style={{ position: "relative" }}>
<button onClick={() => setShowNewProjectIconPicker(!showNewProjectIconPicker)} style={{ width: "38px", height: "38px", borderRadius: "8px", border: "1px solid var(--weekly-border, #e5e7eb)", background: "var(--weekly-bg, #fff)", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center" }}>
<ProjectIcon icon={newProjectIcon} size={16} color="#555" />
</button>
{showNewProjectIconPicker && (
<div style={{ position: "absolute", bottom: "100%", left: 0, marginBottom: "4px", zIndex: 50 }}>
<IconPicker selectedIcon={newProjectIcon} onSelect={(name) => { setNewProjectIcon(name); setShowNewProjectIconPicker(false); }} darkMode={false} />
</div>
)}
</div>
<input type="color" value={newProjectColor} onChange={(e) => setNewProjectColor(e.target.value)} style={{ width: "30px", height: "30px", border: "none", cursor: "pointer", padding: 0, borderRadius: "6px" }} />
<input
type="text"
value={newProjectName}
onChange={(e) => setNewProjectName(e.target.value)}
placeholder={profile.language === "de" ? "Name" : "Name"}
className="weekly-input"
style={{ flex: 1, padding: "8px 12px", fontSize: "0.9rem" }}
onKeyDown={(e) => {
if (e.key === "Enter" && newProjectName.trim()) {
fetch("/api/projects", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: newProjectName.trim(), color: newProjectColor, icon: newProjectIcon }) })
.then(() => { onProjectsChanged(); setNewProjectName(""); setNewProjectIcon("folder"); });
}
}}
/>
<button
onClick={() => {
if (!newProjectName.trim()) return;
fetch("/api/projects", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: newProjectName.trim(), color: newProjectColor, icon: newProjectIcon }) })
.then(() => { onProjectsChanged(); setNewProjectName(""); setNewProjectIcon("folder"); });
}}
className="weekly-btn-primary"
style={{ padding: "8px 14px", fontSize: "0.85rem", display: "inline-flex", alignItems: "center", gap: "4px", whiteSpace: "nowrap" }}
>
<Plus size={14} /> {profile.language === "de" ? "Hinzufügen" : "Add"}
</button>
</div>
</div>
</div>
) : activeTab === "about" ? (
<div
style={{ display: "flex", flexDirection: "column", gap: "20px" }}
>
<div style={{ textAlign: "center", marginBottom: "20px" }}>
<h3
style={{
fontSize: "1.2rem",
fontWeight: 700,
marginBottom: "8px",
color: "var(--weekly-settings-title)",
}}
>
My Weekly To-Do List
</h3>
<p
style={{
fontSize: "0.9rem",
color: "var(--weekly-settings-label)",
}}
>
Version {process.env.NEXT_PUBLIC_APP_VERSION || "1.8.0"}
</p>
</div>
<button
onClick={() => { onRunSetupAssistant?.(); }}
style={{
display: "flex", alignItems: "center", justifyContent: "center", gap: "8px",
width: "100%", padding: "12px 20px", borderRadius: "10px",
border: "1px solid var(--weekly-border, #e5e7eb)",
background: "var(--weekly-bg, #fff)",
color: "var(--weekly-text, #333)",
fontSize: "0.9rem", fontWeight: 500, cursor: "pointer",
transition: "all 0.15s",
}}
onMouseEnter={(e) => { e.currentTarget.style.background = "var(--weekly-hover, #f3f4f6)"; }}
onMouseLeave={(e) => { e.currentTarget.style.background = "var(--weekly-bg, #fff)"; }}
>
<Play size={16} />
{t.setupAssistant}
</button>
</div>
) : (
/* Account Tab */
<div
style={{ display: "flex", flexDirection: "column", gap: "20px" }}
>
<form
onSubmit={handleUpdateProfile}
style={{
display: "flex",
flexDirection: "column",
gap: "12px",
}}
>
<div>
<label
style={{
display: "block",
fontSize: "0.9rem",
fontWeight: 600,
marginBottom: "4px",
}}
>
{t.name}
</label>
<input
type="text"
value={profile.name}
onChange={(e) => saveFieldDebounced("name", e.target.value)}
className="weekly-input"
style={{
width: "100%",
padding: "8px",
border: "1px solid #ddd",
borderRadius: "4px",
}}
/>
</div>
<div>
<label
style={{
display: "block",
fontSize: "0.9rem",
fontWeight: 600,
marginBottom: "4px",
}}
>
{t.email}
</label>
<input
type="email"
value={profile.email}
disabled
style={{
width: "100%",
padding: "8px",
border: "1px solid #eee",
borderRadius: "4px",
background: "#f5f5f5",
color: "#555",
}}
/>
</div>
{profile.id && (
<div>
<label
style={{
display: "block",
fontSize: "0.9rem",
fontWeight: 600,
marginBottom: "4px",
}}
>
{t.accountId}
</label>
<input
type="text"
value={profile.id}
readOnly
onClick={(e) => (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",
}}
/>
<span style={{ fontSize: "0.75rem", color: "var(--weekly-settings-label)", opacity: 0.7 }}>
{t.accountIdDesc}
</span>
</div>
)}
{profile.accountNumber && (
<div>
<label
style={{
display: "block",
fontSize: "0.9rem",
fontWeight: 600,
marginBottom: "4px",
}}
>
{t.accountNumberLabel}
</label>
<input
type="text"
value={`#${profile.accountNumber}`}
readOnly
onClick={(e) => (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",
}}
/>
<span style={{ fontSize: "0.75rem", color: "var(--weekly-settings-label)", opacity: 0.7 }}>
{t.accountNumberDesc}
</span>
</div>
)}
<div>
<label
style={{
display: "block",
fontSize: "0.9rem",
fontWeight: 600,
marginBottom: "4px",
}}
>
{t.timezone}
</label>
<select
value={profile.timezone}
onChange={(e) => saveField("timezone", e.target.value)}
style={{
width: "100%",
padding: "8px",
border: "1px solid #ddd",
borderRadius: "4px",
}}
>
<option value="UTC">UTC</option>
<option value="Europe/Berlin">Europe/Berlin</option>
<option value="America/New_York">America/New_York</option>
<option value="Asia/Tokyo">Asia/Tokyo</option>
</select>
</div>
<div
style={{
borderTop: "1px solid #eee",
paddingTop: "12px",
marginTop: "8px",
}}
>
<label
style={{
display: "block",
fontSize: "0.9rem",
fontWeight: 600,
marginBottom: "4px",
}}
>
{t.changePassword}
</label>
<input
type="password"
placeholder={t.newPassword}
value={passwords.new}
onChange={(e) =>
setPasswords({ ...passwords, new: e.target.value })
}
style={{
width: "100%",
padding: "8px",
border: "1px solid #ddd",
borderRadius: "4px",
marginBottom: "8px",
}}
/>
<input
type="password"
placeholder={t.confirmPassword}
value={passwords.confirm}
onChange={(e) =>
setPasswords({ ...passwords, confirm: e.target.value })
}
style={{
width: "100%",
padding: "8px",
border: "1px solid #ddd",
borderRadius: "4px",
}}
/>
<small
className="help-text"
style={{
fontSize: "0.75rem",
color: "#666",
marginTop: "4px",
display: "block",
}}
>
{translations[profile.language || "en"]?.newPasswordDesc ||
translations["en"].newPasswordDesc}
</small>
</div>
<div
style={{
marginTop: "16px",
display: "flex",
alignItems: "center",
gap: "12px",
}}
>
<button
type="submit"
className="weekly-btn-primary"
style={{ padding: "10px 20px" }}
>
{t.saveChanges}
</button>
{accountMsg && (
<span
style={{
fontSize: "0.9rem",
color: accountMsg.toLowerCase().includes("success")
? "#059669"
: "#dc2626",
fontWeight: 600,
}}
>
{accountMsg}
</span>
)}
</div>
</form>
{/* Data Export Section */}
<div
style={{
marginTop: "20px",
paddingTop: "20px",
borderTop: "1px solid var(--weekly-border)",
}}
>
<h3
style={{
marginBottom: "10px",
fontSize: "1rem",
fontWeight: 600,
color: "var(--weekly-settings-title)",
}}
>
{profile.language === "de" ? "Datenexport" : "Data Export"}
</h3>
<p style={{ fontSize: "0.85rem", color: "var(--weekly-settings-label)", marginBottom: "14px" }}>
{profile.language === "de"
? "CSV-Arbeitsbericht erledigter Aufgaben — nach Kalenderwochen gruppiert. Wähle Felder, Zeitraum und lade die Datei herunter."
: "CSV work report of completed tasks grouped by calendar week. Choose fields, date range and download."}
</p>
{/* Date range */}
<div style={{ display: "flex", gap: "10px", marginBottom: "14px" }}>
<div style={{ flex: 1 }}>
<label style={{ display: "block", fontSize: "0.78rem", fontWeight: 600, marginBottom: "4px", color: "var(--weekly-settings-label)" }}>
{profile.language === "de" ? "Von" : "From"}
</label>
<input
type="date"
value={exportStartDate}
onChange={(e) => 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)" }}
/>
</div>
<div style={{ flex: 1 }}>
<label style={{ display: "block", fontSize: "0.78rem", fontWeight: 600, marginBottom: "4px", color: "var(--weekly-settings-label)" }}>
{profile.language === "de" ? "Bis" : "To"}
</label>
<input
type="date"
value={exportEndDate}
onChange={(e) => 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)" }}
/>
</div>
</div>
{/* Quick date presets */}
<div style={{ display: "flex", gap: "6px", flexWrap: "wrap", marginBottom: "16px" }}>
{[
{ label: profile.language === "de" ? "Diese Woche" : "This week", days: 7 },
{ label: profile.language === "de" ? "Dieser Monat" : "This month", days: 30 },
{ label: profile.language === "de" ? "Letzter Monat" : "Last month", days: 60, offset: 30 },
{ label: profile.language === "de" ? "Dieses Jahr" : "This year", days: 365 },
].map(({ label, days, offset }) => (
<button
key={label}
onClick={() => {
const end = new Date();
if (offset) end.setDate(end.getDate() - offset);
const start = new Date(end);
start.setDate(start.getDate() - days + (offset ? 0 : 0));
// Simpler: this month = first day of month
if (label.includes("Monat") || label.includes("month")) {
if (offset) {
// last month
const now = new Date();
const s = new Date(now.getFullYear(), now.getMonth() - 1, 1);
const e = new Date(now.getFullYear(), now.getMonth(), 0);
setExportStartDate(s.toISOString().split("T")[0]);
setExportEndDate(e.toISOString().split("T")[0]);
return;
} else {
const now = new Date();
const s = new Date(now.getFullYear(), now.getMonth(), 1);
setExportStartDate(s.toISOString().split("T")[0]);
setExportEndDate(new Date().toISOString().split("T")[0]);
return;
}
}
if (label.includes("Jahr") || label.includes("year")) {
const now = new Date();
setExportStartDate(`${now.getFullYear()}-01-01`);
setExportEndDate(now.toISOString().split("T")[0]);
return;
}
// This week
const now = new Date();
const day = now.getDay() || 7;
const mon = new Date(now); mon.setDate(now.getDate() - day + 1);
setExportStartDate(mon.toISOString().split("T")[0]);
setExportEndDate(now.toISOString().split("T")[0]);
}}
style={{ fontSize: "0.75rem", padding: "3px 9px", borderRadius: "5px", border: "1px solid var(--weekly-settings-input-border)", background: "var(--weekly-settings-input-bg)", color: "var(--weekly-settings-label)", cursor: "pointer" }}
>
{label}
</button>
))}
</div>
{/* Field picker */}
<div style={{ marginBottom: "14px" }}>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "8px" }}>
<span style={{ fontSize: "0.78rem", fontWeight: 600, color: "var(--weekly-settings-label)" }}>
{profile.language === "de" ? "Spalten auswählen:" : "Select columns:"}
</span>
<div style={{ display: "flex", gap: "6px" }}>
<button
onClick={() => setExportFields(new Set(EXPORT_FIELDS.map(f => f.key)))}
style={{ fontSize: "0.72rem", padding: "2px 7px", borderRadius: "4px", border: "1px solid var(--weekly-settings-input-border)", background: "transparent", color: "var(--weekly-settings-label)", cursor: "pointer" }}
>
{profile.language === "de" ? "Alle" : "All"}
</button>
<button
onClick={() => setExportFields(new Set(EXPORT_FIELDS.filter(f => f.defaultOn).map(f => f.key)))}
style={{ fontSize: "0.72rem", padding: "2px 7px", borderRadius: "4px", border: "1px solid var(--weekly-settings-input-border)", background: "transparent", color: "var(--weekly-settings-label)", cursor: "pointer" }}
>
{profile.language === "de" ? "Standard" : "Default"}
</button>
</div>
</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "5px 12px" }}>
{EXPORT_FIELDS.map(f => {
const checked = exportFields.has(f.key);
const label = profile.language === "de" ? f.labelDe : f.labelEn;
return (
<label
key={f.key}
style={{ display: "flex", alignItems: "center", gap: "7px", fontSize: "0.82rem", color: "var(--weekly-settings-text)", cursor: "pointer", padding: "3px 0" }}
>
<input
type="checkbox"
checked={checked}
onChange={() => {
setExportFields(prev => {
const next = new Set(prev);
if (next.has(f.key)) next.delete(f.key);
else next.add(f.key);
return next;
});
}}
style={{ accentColor: "#6366f1", width: "14px", height: "14px", flexShrink: 0 }}
/>
{label}
</label>
);
})}
</div>
</div>
{/* Download buttons */}
<div style={{ display: "flex", gap: "8px" }}>
<a
href={`/api/user/export?startDate=${exportStartDate}&endDate=${exportEndDate}&lang=${profile.language || 'de'}&fields=${Array.from(exportFields).join(',')}`}
target="_blank"
style={{ flex: 1, display: "inline-flex", alignItems: "center", justifyContent: "center", gap: "6px", textDecoration: "none", background: "var(--weekly-settings-item-bg)", border: "1px solid var(--weekly-settings-input-border)", color: "var(--weekly-settings-text)", padding: "9px 12px", borderRadius: "4px", fontWeight: 500, fontSize: "0.85rem", transition: "background-color 0.2s" }}
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="12" y1="18" x2="12" y2="12"/><line x1="9" y1="15" x2="15" y2="15"/></svg>
CSV
</a>
<a
href={`/api/user/export-pdf?startDate=${exportStartDate}&endDate=${exportEndDate}&lang=${profile.language || 'de'}&fields=${Array.from(exportFields).join(',')}`}
target="_blank"
style={{ flex: 1, display: "inline-flex", alignItems: "center", justifyContent: "center", gap: "6px", textDecoration: "none", background: "#6366f1", border: "none", color: "#fff", padding: "9px 12px", borderRadius: "4px", fontWeight: 600, fontSize: "0.85rem", transition: "background-color 0.2s" }}
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><rect x="8" y="13" width="8" height="6" rx="1"/></svg>
PDF
</a>
</div>
</div>
{/* Backup & Restore Section */}
<div
style={{
marginTop: "20px",
paddingTop: "20px",
borderTop: "1px solid var(--weekly-border)",
}}
>
<h3
style={{
marginBottom: "10px",
fontSize: "1rem",
fontWeight: 600,
color: "var(--weekly-settings-title)",
}}
>
{t.backupRestore}
</h3>
<p
style={{
fontSize: "0.9rem",
color: "var(--weekly-settings-label)",
marginBottom: "15px",
}}
>
{t.backupRestoreDesc}
</p>
{/* Export All Data */}
<button
onClick={handleExportAllData}
disabled={isExportingAll}
className="weekly-auth-button w-full justify-center"
style={{
display: "inline-flex",
width: "100%",
padding: "10px",
background: "var(--weekly-settings-item-bg)",
border: "1px solid var(--weekly-settings-input-border)",
color: "var(--weekly-settings-text)",
borderRadius: "4px",
fontWeight: 500,
cursor: isExportingAll ? "wait" : "pointer",
transition: "background-color 0.2s",
marginBottom: "15px",
opacity: isExportingAll ? 0.7 : 1,
}}
>
{isExportingAll ? t.exporting : t.exportAllData}
</button>
{/* Import Section */}
<div
style={{
padding: "12px",
border: "1px solid var(--weekly-border)",
borderRadius: "6px",
background: "var(--weekly-settings-item-bg)",
}}
>
<label
style={{
display: "block",
fontSize: "0.9rem",
fontWeight: 600,
marginBottom: "10px",
color: "var(--weekly-settings-text)",
}}
>
{t.importData}
</label>
{/* Import Mode Toggle */}
<div style={{ marginBottom: "10px" }}>
<label
style={{
display: "block",
fontSize: "0.8rem",
fontWeight: 600,
marginBottom: "6px",
color: "var(--weekly-settings-label)",
}}
>
{t.importMode}
</label>
<div style={{ display: "flex", gap: "8px" }}>
<button
onClick={() => setImportMode("merge")}
style={{
flex: 1,
padding: "8px",
borderRadius: "4px",
border: importMode === "merge"
? "2px solid var(--weekly-teal)"
: "1px solid var(--weekly-settings-input-border)",
background: importMode === "merge"
? "rgba(20, 184, 166, 0.1)"
: "var(--weekly-settings-input-bg)",
color: "var(--weekly-settings-text)",
cursor: "pointer",
fontSize: "0.85rem",
fontWeight: importMode === "merge" ? 600 : 400,
transition: "all 0.2s",
}}
>
<div>{t.importModeMerge}</div>
<div style={{ fontSize: "0.75rem", opacity: 0.7, marginTop: "2px" }}>
{t.importModeMergeDesc}
</div>
</button>
<button
onClick={() => setImportMode("replace")}
style={{
flex: 1,
padding: "8px",
borderRadius: "4px",
border: importMode === "replace"
? "2px solid #ef4444"
: "1px solid var(--weekly-settings-input-border)",
background: importMode === "replace"
? "rgba(239, 68, 68, 0.1)"
: "var(--weekly-settings-input-bg)",
color: importMode === "replace" ? "#ef4444" : "var(--weekly-settings-text)",
cursor: "pointer",
fontSize: "0.85rem",
fontWeight: importMode === "replace" ? 600 : 400,
transition: "all 0.2s",
}}
>
<div>{t.importModeReplace}</div>
<div style={{ fontSize: "0.75rem", opacity: 0.7, marginTop: "2px" }}>
{t.importModeReplaceDesc}
</div>
</button>
</div>
</div>
{importMode === "replace" && (
<div
style={{
padding: "8px 10px",
marginBottom: "10px",
borderRadius: "4px",
background: "rgba(239, 68, 68, 0.08)",
border: "1px solid rgba(239, 68, 68, 0.3)",
fontSize: "0.8rem",
color: "#ef4444",
fontWeight: 500,
}}
>
{t.importReplaceWarning}
</div>
)}
{/* File Input */}
<input
id="import-file-input"
type="file"
accept=".json"
onChange={(e) => {
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",
}}
/>
<button
onClick={handleImportData}
disabled={!importFile || isImporting}
className="weekly-auth-button w-full justify-center"
style={{
display: "inline-flex",
width: "100%",
padding: "10px",
background: !importFile || isImporting
? "var(--weekly-settings-input-bg)"
: "var(--weekly-teal)",
border: "1px solid var(--weekly-settings-input-border)",
color: !importFile || isImporting
? "var(--weekly-settings-label)"
: "#fff",
borderRadius: "4px",
fontWeight: 600,
cursor: !importFile || isImporting ? "not-allowed" : "pointer",
transition: "background-color 0.2s",
opacity: !importFile || isImporting ? 0.6 : 1,
}}
>
{isImporting ? t.importing : t.importButton}
</button>
{importMsg && (
<p
style={{
marginTop: "10px",
fontSize: "0.85rem",
fontWeight: 600,
color: importMsg.startsWith("✓")
? "#059669"
: "#dc2626",
}}
>
{importMsg}
</p>
)}
</div>
</div>
<div
className="account-danger-zone"
style={{
marginTop: "20px",
paddingTop: "20px",
borderTop: "1px solid var(--weekly-border)",
}}
>
{/* Sign Out Button - accessible on mobile */}
<button
onClick={() => signOut()}
className="weekly-auth-button w-full justify-center"
style={{
marginBottom: "18px",
padding: "10px",
border: "1px solid var(--weekly-settings-input-border)",
background: "var(--weekly-settings-item-bg)",
color: "var(--weekly-settings-text)",
borderRadius: "4px",
cursor: "pointer",
fontWeight: 500,
transition: "background-color 0.2s",
display: "flex",
alignItems: "center",
gap: "8px",
width: "100%",
}}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path>
<polyline points="16 17 21 12 16 7"></polyline>
<line x1="21" y1="12" x2="9" y2="12"></line>
</svg>
{t.signOut}
</button>
<h3
style={{
fontSize: "1rem",
fontWeight: 600,
marginBottom: "10px",
color: "var(--weekly-settings-title)",
}}
>
{t.dataPrivacy}
</h3>
<div
style={{
display: "flex",
flexDirection: "column",
gap: "10px",
}}
>
<button
onClick={handleDownloadData}
className="weekly-auth-button w-full justify-center"
style={{
padding: "10px",
border: "1px solid var(--weekly-settings-input-border)",
background: "var(--weekly-settings-item-bg)",
color: "var(--weekly-settings-text)",
borderRadius: "4px",
cursor: "pointer",
fontWeight: 500,
transition: "background-color 0.2s",
}}
>
{t.downloadData}
</button>
<button
onClick={handleDeleteAccount}
className="weekly-auth-button w-full justify-center"
style={{
padding: "10px",
border: "1px solid #ef4444",
background: "rgba(239, 68, 68, 0.05)",
color: "#ef4444",
borderRadius: "4px",
cursor: "pointer",
fontWeight: 600,
transition: "background-color 0.2s",
}}
onMouseOver={(e) =>
(e.currentTarget.style.background =
"rgba(239, 68, 68, 0.1)")
}
onMouseOut={(e) =>
(e.currentTarget.style.background =
"rgba(239, 68, 68, 0.05)")
}
>
{t.deleteAccount}
</button>
</div>
</div>
</div>
)}
{/* Apple Calendar (CalDAV) Connection Modal */}
{showAppleCalendarModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[2000]">
<div className="bg-white rounded-lg p-6 max-w-md w-full shadow-xl">
<h3 className="text-xl font-bold mb-4">
Connect Apple Calendar
</h3>
<div className="bg-blue-50 border border-blue-200 rounded p-3 mb-4 text-sm text-blue-800">
<p style={{ marginBottom: "6px" }}>
Connect your iCloud Calendar events via CalDAV.
</p>
<p style={{ fontSize: "0.8rem", opacity: 0.85 }}>
This requires an{" "}
<a
href="https://support.apple.com/en-us/102654"
target="_blank"
rel="noopener noreferrer"
style={{ textDecoration: "underline" }}
>
app-specific password
</a>{" "}
generated at appleid.apple.com.
</p>
</div>
<div className="bg-amber-50 border border-amber-200 rounded p-3 mb-4 text-sm text-amber-800" style={{ display: "flex", gap: "8px", alignItems: "flex-start" }}>
<span style={{ fontSize: "1rem", flexShrink: 0 }}></span>
<p style={{ margin: 0, fontSize: "0.8rem", lineHeight: 1.4 }}>
{t.appleRemindersNote}
</p>
</div>
{appleCalError && (
<div className="bg-red-50 text-red-600 p-3 rounded mb-4 text-sm">
{appleCalError}
</div>
)}
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Apple ID (Email)
</label>
<input
type="email"
value={appleCalEmail}
onChange={(e) => 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"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
App-Specific Password
</label>
<input
type="password"
value={appleCalPassword}
onChange={(e) => 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()
}
/>
</div>
</div>
<div className="flex justify-end gap-3 mt-6">
<button
onClick={() => {
setShowAppleCalendarModal(false);
setAppleCalError("");
}}
className="px-4 py-2 text-gray-600 hover:text-gray-800"
disabled={isConnectingAppleCal}
>
Cancel
</button>
<button
onClick={submitAppleCalendarConnection}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:bg-blue-300 flex items-center"
disabled={isConnectingAppleCal}
>
{isConnectingAppleCal ? (
<>
<div className="weekly-spinner weekly-spinner-white -ml-1 mr-2" aria-hidden="true"></div>
Connecting...
</>
) : (
"Connect"
)}
</button>
</div>
</div>
</div>
)}
{/* Synology Calendar Connection Modal */}
{showSynologyCalendarModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[2000]">
<div className="bg-white rounded-lg p-6 max-w-md w-full shadow-xl">
<h3 className="text-xl font-bold mb-4">
Connect Synology Calendar
</h3>
<div className="bg-blue-50 border border-blue-200 rounded p-3 mb-4 text-sm text-blue-800">
<p style={{ marginBottom: "6px" }}>
Connect your Synology NAS Calendar events.
</p>
<p style={{ fontSize: "0.8rem", opacity: 0.85 }}>
Make sure Synology Calendar is installed and the CalDAV URL is reachable over HTTPS.
</p>
</div>
{synologyCalError && (
<div className="bg-red-50 text-red-600 p-3 rounded mb-4 text-sm">
{synologyCalError}
</div>
)}
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Server URL (CalDAV)
</label>
<input
type="url"
value={synologyCalServerUrl}
onChange={(e) => 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"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Username
</label>
<input
type="text"
value={synologyCalUsername}
onChange={(e) => 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"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Password
</label>
<input
type="password"
value={synologyCalPassword}
onChange={(e) => 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()
}
/>
</div>
</div>
<div className="flex justify-end gap-3 mt-6">
<button
onClick={() => {
setShowSynologyCalendarModal(false);
setSynologyCalError("");
}}
className="px-4 py-2 text-gray-600 hover:text-gray-800"
disabled={isConnectingSynologyCal}
>
Cancel
</button>
<button
onClick={submitSynologyCalendarConnection}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:bg-blue-300 flex items-center"
disabled={isConnectingSynologyCal}
>
{isConnectingSynologyCal ? (
<>
<div className="weekly-spinner weekly-spinner-white -ml-1 mr-2" aria-hidden="true"></div>
Connecting...
</>
) : (
"Connect"
)}
</button>
</div>
</div>
</div>
)}
</div>
</div>
</>
);
}
export default SettingsSidebar;