feat: reorganize mobile UX — projects sidebar, left quick actions, sticky fix

- Create dedicated Projects sidebar (separate from settings) with full
  CRUD, emoji picker, and dark mode support
- Move all quick actions (nav, search, jump to date, calendar event,
  recurring tasks, goal, focus, dark mode, view/days/slot/hours,
  undo/redo/refresh) from right settings sidebar to left quick settings
- Remove projects tab from settings — all project management via sidebar
- Fix iPhone sticky headers: time-column-header no longer sticky on
  mobile, weekly-day-header remains sticky

v1.45.0

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
mARTin 2026-03-17 22:57:00 +01:00
parent 62257ff089
commit 183fd6ea48
3 changed files with 326 additions and 450 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "my-weekly-todo-list", "name": "my-weekly-todo-list",
"version": "1.44.1", "version": "1.45.0",
"description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view", "description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {

View File

@ -4358,10 +4358,15 @@ h3 {
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.time-grid-on .weekly-day-header, .time-grid-on .weekly-day-header {
.time-grid-on .time-column-header {
top: calc(48px + env(safe-area-inset-top, 0px)) !important; top: calc(48px + env(safe-area-inset-top, 0px)) !important;
} }
/* On mobile, time-column-header should NOT be sticky — it wastes vertical space */
.time-grid-on .time-column-header {
position: relative !important;
top: auto !important;
z-index: auto !important;
}
} }
/* Provide a solid background for the top-left empty corner above the time column */ /* Provide a solid background for the top-left empty corner above the time column */

View File

@ -1807,7 +1807,7 @@ export default function WeeklyView() {
// Moved state definitions to the top // Moved state definitions to the top
const [showSettings, setShowSettings] = useState(false); const [showSettings, setShowSettings] = useState(false);
const [activeTab, setActiveTab] = useState< const [activeTab, setActiveTab] = useState<
"calendar" | "general" | "account" | "styling" | "motivation" | "about" | "projects" "calendar" | "general" | "account" | "styling" | "motivation" | "about"
>("general"); >("general");
const [exportStartDate, setExportStartDate] = useState(""); const [exportStartDate, setExportStartDate] = useState("");
const [exportEndDate, setExportEndDate] = useState(""); const [exportEndDate, setExportEndDate] = useState("");
@ -1991,6 +1991,7 @@ export default function WeeklyView() {
const [showDatePicker, setShowDatePicker] = useState(false); const [showDatePicker, setShowDatePicker] = useState(false);
const datePickerBtnRef = useRef<HTMLDivElement>(null); const datePickerBtnRef = useRef<HTMLDivElement>(null);
const [showQuickSettings, setShowQuickSettings] = useState(false); const [showQuickSettings, setShowQuickSettings] = useState(false);
const [showProjectsSidebar, setShowProjectsSidebar] = useState(false);
const [focusTimerDuration, setFocusTimerDuration] = useState(25); const [focusTimerDuration, setFocusTimerDuration] = useState(25);
const [fontSize, setFontSize] = useState<"S" | "M" | "L">("M"); const [fontSize, setFontSize] = useState<"S" | "M" | "L">("M");
@ -5279,6 +5280,18 @@ export default function WeeklyView() {
const activeDateLayout = isMobile ? (profile.mobileDateLayout || "below") : (profile.dateLayout || "right"); const activeDateLayout = isMobile ? (profile.mobileDateLayout || "below") : (profile.dateLayout || "right");
// Quick settings sidebar button styles
const qsBtnStyle = (dm: boolean): React.CSSProperties => ({
padding: "6px", borderRadius: "6px", border: "none", cursor: "pointer",
display: "flex", alignItems: "center", justifyContent: "center",
background: dm ? "#1f2937" : "#e5e7eb", color: dm ? "#9ca3af" : "#6b7280",
});
const qsActionStyle = (dm: boolean): React.CSSProperties => ({
display: "flex", alignItems: "center", gap: "8px", padding: "7px 8px",
fontSize: "0.8rem", borderRadius: "6px", border: "none", cursor: "pointer",
background: "none", color: dm ? "#d1d5db" : "#333", textAlign: "left" as const,
});
// All-Day Events Section (reusable for above/below positioning) // All-Day Events Section (reusable for above/below positioning)
const allDaySection = (() => { const allDaySection = (() => {
if (!showAllDay) return null; if (!showAllDay) return null;
@ -5471,166 +5484,167 @@ export default function WeeklyView() {
left: 0, left: 0,
top: 0, top: 0,
bottom: 0, bottom: 0,
width: "220px", width: "240px",
background: darkMode ? "#1a1a2e" : "#fafafa", background: darkMode ? "#1a1a2e" : "#fafafa",
borderRight: `1px solid ${darkMode ? "#333" : "#e5e7eb"}`, borderRight: `1px solid ${darkMode ? "#333" : "#e5e7eb"}`,
zIndex: 1000, zIndex: 1000,
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
padding: "20px 16px", padding: "16px 14px",
gap: "18px", gap: "10px",
overflowY: "auto", overflowY: "auto",
boxShadow: "2px 0 12px rgba(0,0,0,0.08)", boxShadow: "2px 0 12px rgba(0,0,0,0.08)",
}} }}
> >
<div style={{ fontSize: "0.85rem", fontWeight: 700, color: darkMode ? "#e5e7eb" : "#333", marginBottom: "4px" }}> <div style={{ fontSize: "0.85rem", fontWeight: 700, color: darkMode ? "#e5e7eb" : "#333", marginBottom: "2px" }}>
{language === "de" ? "Einstellungen" : "Preferences"} {language === "de" ? "Einstellungen" : "Preferences"}
</div> </div>
{/* Columns */} {/* Navigation Row */}
<div style={{ display: "flex", justifyContent: "center", gap: "4px", padding: "4px 0", borderBottom: `1px solid ${darkMode ? "#333" : "#e5e7eb"}`, paddingBottom: "10px" }}>
<button onClick={() => { goToPrevWeek(); setShowQuickSettings(false); }} style={{ ...qsBtnStyle(darkMode), minWidth: "36px" }} title="Previous Week"><ChevronsLeft size={16} /></button>
<button onClick={() => { goToPrevDay(); setShowQuickSettings(false); }} style={{ ...qsBtnStyle(darkMode), minWidth: "36px" }} title="Previous Day"><ChevronLeft size={16} /></button>
<button onClick={() => { goToToday(); setShowQuickSettings(false); }} style={{ ...qsBtnStyle(darkMode), minWidth: "56px", fontWeight: 700, fontSize: "0.7rem" }} title="Today">{language === "de" ? "Heute" : "Today"}</button>
<button onClick={() => { goToNextDay(); setShowQuickSettings(false); }} style={{ ...qsBtnStyle(darkMode), minWidth: "36px" }} title="Next Day"><ChevronRight size={16} /></button>
<button onClick={() => { goToNextWeek(); setShowQuickSettings(false); }} style={{ ...qsBtnStyle(darkMode), minWidth: "36px" }} title="Next Week"><ChevronsRight size={16} /></button>
</div>
{/* Quick Actions */}
<div style={{ display: "flex", flexDirection: "column", gap: "1px" }}>
<button onClick={() => { setIsSearchOpen(true); setShowQuickSettings(false); }} style={qsActionStyle(darkMode)}><Search size={16} /> <span>{language === "de" ? "Suche" : "Search"}</span></button>
<button onClick={() => { setShowDatePicker(true); setShowQuickSettings(false); }} style={qsActionStyle(darkMode)}><Calendar size={16} /> <span>{language === "de" ? "Datum wählen" : "Jump to date"}</span></button>
<button onClick={() => { const now = new Date(); setCalendarEventModal({ isOpen: true, event: undefined, initialDate: now, initialStartTime: `${String(now.getHours()).padStart(2, "0")}:00` }); setShowQuickSettings(false); }} style={qsActionStyle(darkMode)}><Plus size={16} /> <span>{language === "de" ? "Kalendereintrag" : "Calendar Event"}</span></button>
<button onClick={() => { setShowProjectsSidebar(true); setShowQuickSettings(false); }} style={qsActionStyle(darkMode)}><FolderPlus size={16} /> <span>{language === "de" ? "Projekte" : "Projects"}</span></button>
<button onClick={() => { setIsRecurringTasksOpen(true); setShowQuickSettings(false); }} style={qsActionStyle(darkMode)}><Repeat size={16} /> <span>{language === "de" ? "Wiederkehrend" : "Recurring Tasks"}</span></button>
<button onClick={() => { const nv = !showNextTask; setShowNextTask(nv); saveSetting("showNextTask", nv); }} style={qsActionStyle(darkMode)}>
{showNextTask ? <Play size={16} /> : <Target size={16} />}
<span>{showNextTask ? (language === "de" ? "Nächste Aufgabe" : "Next Task") : (language === "de" ? "Ziel" : "Goal")}</span>
</button>
<button onClick={() => { setShowFocusMode(true); setShowQuickSettings(false); }} style={qsActionStyle(darkMode)}><Zap size={16} /> <span>{language === "de" ? "Fokus" : "Focus"}</span></button>
</div>
{/* Separator */}
<div style={{ borderTop: `1px solid ${darkMode ? "#333" : "#e5e7eb"}`, paddingTop: "8px" }} />
{/* View Style */}
<div style={{ display: "flex", flexDirection: "column", gap: "6px" }}> <div style={{ display: "flex", flexDirection: "column", gap: "6px" }}>
<span style={{ fontSize: "0.75rem", fontWeight: 600, color: darkMode ? "#9ca3af" : "#6b7280" }}> <span style={{ fontSize: "0.7rem", fontWeight: 600, color: darkMode ? "#9ca3af" : "#6b7280" }}>
{language === "de" ? "Spalten" : "Columns"} {language === "de" ? "Ansicht" : "View"}
</span>
<div style={{ display: "flex", gap: "4px" }}>
{[
{ key: "simple", icon: <CalendarDays size={13} /> },
{ key: "calendar", icon: <Calendar size={13} /> },
{ key: "list", icon: <ListTodo size={13} /> },
{ key: "kanban", icon: <Kanban size={13} /> },
].map((v) => (
<button key={v.key} onClick={() => { setViewStyle(v.key as any); saveSetting("viewStyle", v.key); }}
style={{ flex: 1, padding: "5px 0", borderRadius: "6px", border: "none", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", fontWeight: viewStyle === v.key ? 700 : 400, background: viewStyle === v.key ? "#0ea5e9" : (darkMode ? "#1f2937" : "#e5e7eb"), color: viewStyle === v.key ? "#fff" : (darkMode ? "#9ca3af" : "#6b7280") }}>
{v.icon}
</button>
))}
</div>
</div>
{/* Columns / Days */}
<div style={{ display: "flex", flexDirection: "column", gap: "6px" }}>
<span style={{ fontSize: "0.7rem", fontWeight: 600, color: darkMode ? "#9ca3af" : "#6b7280" }}>
{language === "de" ? "Spalten" : "Days"}
</span> </span>
<div style={{ display: "flex", gap: "4px" }}> <div style={{ display: "flex", gap: "4px" }}>
{[1, 2, 3, 5, 7].map((num) => ( {[1, 2, 3, 5, 7].map((num) => (
<button <button key={num} onClick={() => { setViewDays(num); savedViewDaysRef.current = num; saveSetting("viewDays", num); }}
key={num} style={{ flex: 1, padding: "5px 0", fontSize: "0.8rem", borderRadius: "6px", border: "none", cursor: "pointer", fontWeight: viewDays === num ? 700 : 400, background: viewDays === num ? (darkMode ? "#374151" : "#333") : (darkMode ? "#1f2937" : "#e5e7eb"), color: viewDays === num ? "#fff" : (darkMode ? "#9ca3af" : "#6b7280") }}>
onClick={() => { setViewDays(num); savedViewDaysRef.current = num; saveSetting("viewDays", num); }}
style={{
padding: "4px 10px",
fontSize: "0.8rem",
borderRadius: "6px",
border: "none",
cursor: "pointer",
fontWeight: viewDays === num ? 700 : 400,
background: viewDays === num ? (darkMode ? "#374151" : "#333") : (darkMode ? "#1f2937" : "#e5e7eb"),
color: viewDays === num ? "#fff" : (darkMode ? "#9ca3af" : "#6b7280"),
}}
>
{num} {num}
</button> </button>
))} ))}
</div> </div>
</div> </div>
{/* Slot Duration (only with time grid) */}
{showTimeGrid && (
<div style={{ display: "flex", flexDirection: "column", gap: "6px" }}>
<span style={{ fontSize: "0.7rem", fontWeight: 600, color: darkMode ? "#9ca3af" : "#6b7280" }}>
{language === "de" ? "Zeitfenster" : "Slot"}
</span>
<div style={{ display: "flex", gap: "4px" }}>
{[15, 30, 60].map((d) => (
<button key={d} onClick={() => { setCellDuration(d as CellDuration); saveSetting("cellDuration", d); }}
style={{ flex: 1, padding: "5px 0", fontSize: "0.8rem", borderRadius: "6px", border: "none", cursor: "pointer", fontWeight: cellDuration === d ? 700 : 400, background: cellDuration === d ? (darkMode ? "#374151" : "#333") : (darkMode ? "#1f2937" : "#e5e7eb"), color: cellDuration === d ? "#fff" : (darkMode ? "#9ca3af" : "#6b7280") }}>
{d}m
</button>
))}
</div>
</div>
)}
{/* Visible Hours (only with time grid) */}
{showTimeGrid && (
<div style={{ display: "flex", alignItems: "center", gap: "6px" }}>
<Clock size={14} style={{ color: darkMode ? "#9ca3af" : "#6b7280", flexShrink: 0 }} />
<input type="number" min={0} max={23} value={startHour} onChange={(e) => { const h = Math.max(0, Math.min(23, parseInt(e.target.value) || 0)); setStartHour(h); saveSetting("startHour", h); }}
style={{ width: "44px", padding: "4px 4px", borderRadius: "6px", border: `1px solid ${darkMode ? "#555" : "#e5e7eb"}`, background: darkMode ? "#1f2937" : "#f3f4f6", color: darkMode ? "#e5e7eb" : "#333", fontSize: "0.8rem", textAlign: "center", outline: "none" }} />
<span style={{ fontSize: "0.7rem", color: darkMode ? "#9ca3af" : "#6b7280" }}></span>
<input type="number" min={1} max={24} value={endHour} onChange={(e) => { const h = Math.max(1, Math.min(24, parseInt(e.target.value) || 24)); setEndHour(h); saveSetting("endHour", h); }}
style={{ width: "44px", padding: "4px 4px", borderRadius: "6px", border: `1px solid ${darkMode ? "#555" : "#e5e7eb"}`, background: darkMode ? "#1f2937" : "#f3f4f6", color: darkMode ? "#e5e7eb" : "#333", fontSize: "0.8rem", textAlign: "center", outline: "none" }} />
<span style={{ fontSize: "0.7rem", color: darkMode ? "#9ca3af" : "#6b7280" }}>h</span>
</div>
)}
{/* Text size */} {/* Text size */}
<div style={{ display: "flex", flexDirection: "column", gap: "6px" }}> <div style={{ display: "flex", flexDirection: "column", gap: "6px" }}>
<span style={{ fontSize: "0.75rem", fontWeight: 600, color: darkMode ? "#9ca3af" : "#6b7280" }}> <span style={{ fontSize: "0.7rem", fontWeight: 600, color: darkMode ? "#9ca3af" : "#6b7280" }}>
{language === "de" ? "Textgröße" : "Text size"} {language === "de" ? "Textgröße" : "Text size"}
</span> </span>
<div style={{ display: "flex", gap: "4px" }}> <div style={{ display: "flex", gap: "4px" }}>
{(["S", "M", "L"] as const).map((size) => ( {(["S", "M", "L"] as const).map((size) => (
<button <button key={size} onClick={() => { setFontSize(size); saveSetting("fontSize", size); }}
key={size} style={{ padding: "4px 10px", fontSize: "0.8rem", borderRadius: "6px", border: "none", cursor: "pointer", fontWeight: fontSize === size ? 700 : 400, background: fontSize === size ? (darkMode ? "#374151" : "#333") : (darkMode ? "#1f2937" : "#e5e7eb"), color: fontSize === size ? "#fff" : (darkMode ? "#9ca3af" : "#6b7280") }}>
onClick={() => { setFontSize(size); saveSetting("fontSize", size); }}
style={{
padding: "4px 10px",
fontSize: "0.8rem",
borderRadius: "6px",
border: "none",
cursor: "pointer",
fontWeight: fontSize === size ? 700 : 400,
background: fontSize === size ? (darkMode ? "#374151" : "#333") : (darkMode ? "#1f2937" : "#e5e7eb"),
color: fontSize === size ? "#fff" : (darkMode ? "#9ca3af" : "#6b7280"),
}}
>
{size} {size}
</button> </button>
))} ))}
</div> </div>
</div> </div>
{/* Someday section */} {/* Separator */}
<div style={{ borderTop: `1px solid ${darkMode ? "#333" : "#e5e7eb"}`, paddingTop: "4px" }} />
{/* Toggle switches */}
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}> <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<span style={{ fontSize: "0.75rem", fontWeight: 600, color: darkMode ? "#9ca3af" : "#6b7280" }}> <span style={{ fontSize: "0.75rem", fontWeight: 600, color: darkMode ? "#9ca3af" : "#6b7280" }}>{language === "de" ? "Irgendwann" : "Someday"}</span>
{language === "de" ? "Irgendwann" : "Someday"} <button onClick={() => { const v = !showSomeday; setShowSomeday(v); saveSetting("showSomeday", v); }} style={{ background: "none", border: "none", cursor: "pointer", padding: "2px", color: darkMode ? "#9ca3af" : "#6b7280" }}>
</span>
<button
onClick={() => { const v = !showSomeday; setShowSomeday(v); saveSetting("showSomeday", v); }}
style={{ background: "none", border: "none", cursor: "pointer", padding: "2px", color: darkMode ? "#9ca3af" : "#6b7280" }}
>
{showSomeday ? <Eye size={16} /> : <EyeOff size={16} />} {showSomeday ? <Eye size={16} /> : <EyeOff size={16} />}
</button> </button>
</div> </div>
{/* Schedule / Time Grid */}
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}> <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<span style={{ fontSize: "0.75rem", fontWeight: 600, color: darkMode ? "#9ca3af" : "#6b7280" }}> <span style={{ fontSize: "0.75rem", fontWeight: 600, color: darkMode ? "#9ca3af" : "#6b7280" }}>{language === "de" ? "Zeitplan" : "Schedule"}</span>
{language === "de" ? "Zeitplan" : "Schedule"} <button onClick={() => { const v = !showTimeGrid; setShowTimeGrid(v); saveSetting("showTimeGrid", v); }} style={{ background: "none", border: "none", cursor: "pointer", padding: "2px", color: darkMode ? "#9ca3af" : "#6b7280" }}>
</span>
<button
onClick={() => { const v = !showTimeGrid; setShowTimeGrid(v); saveSetting("showTimeGrid", v); }}
style={{ background: "none", border: "none", cursor: "pointer", padding: "2px", color: darkMode ? "#9ca3af" : "#6b7280" }}
>
{showTimeGrid ? <Eye size={16} /> : <EyeOff size={16} />} {showTimeGrid ? <Eye size={16} /> : <EyeOff size={16} />}
</button> </button>
</div> </div>
{/* All-day events */}
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}> <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<span style={{ fontSize: "0.75rem", fontWeight: 600, color: darkMode ? "#9ca3af" : "#6b7280" }}> <span style={{ fontSize: "0.75rem", fontWeight: 600, color: darkMode ? "#9ca3af" : "#6b7280" }}>{language === "de" ? "Ganztägig" : "All-day"}</span>
{language === "de" ? "Ganztägig" : "All-day"} <button onClick={() => { const v = !showAllDay; setShowAllDay(v); saveSetting("showAllDayEvents", v); }} style={{ background: "none", border: "none", cursor: "pointer", padding: "2px", color: darkMode ? "#9ca3af" : "#6b7280" }}>
</span>
<button
onClick={() => { const v = !showAllDay; setShowAllDay(v); saveSetting("showAllDayEvents", v); }}
style={{ background: "none", border: "none", cursor: "pointer", padding: "2px", color: darkMode ? "#9ca3af" : "#6b7280" }}
>
{showAllDay ? <Eye size={16} /> : <EyeOff size={16} />} {showAllDay ? <Eye size={16} /> : <EyeOff size={16} />}
</button> </button>
</div> </div>
{/* Checkboxes */}
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}> <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<span style={{ fontSize: "0.75rem", fontWeight: 600, color: darkMode ? "#9ca3af" : "#6b7280" }}> <span style={{ fontSize: "0.75rem", fontWeight: 600, color: darkMode ? "#9ca3af" : "#6b7280" }}>{language === "de" ? "Checkboxen" : "Checkboxes"}</span>
{language === "de" ? "Checkboxen" : "Checkboxes"} <button onClick={() => { const v = !profile.showTaskCheckboxes; setProfile({ ...profile, showTaskCheckboxes: v }); saveSetting("showTaskCheckboxes", v); }} style={{ background: "none", border: "none", cursor: "pointer", padding: "2px", color: darkMode ? "#9ca3af" : "#6b7280" }}>
</span>
<button
onClick={() => {
const v = !profile.showTaskCheckboxes;
setProfile({ ...profile, showTaskCheckboxes: v });
saveSetting("showTaskCheckboxes", v);
}}
style={{ background: "none", border: "none", cursor: "pointer", padding: "2px", color: darkMode ? "#9ca3af" : "#6b7280" }}
>
{profile.showTaskCheckboxes ? <Eye size={16} /> : <EyeOff size={16} />} {profile.showTaskCheckboxes ? <Eye size={16} /> : <EyeOff size={16} />}
</button> </button>
</div> </div>
{/* Start on */} {/* Start on */}
<div style={{ display: "flex", flexDirection: "column", gap: "6px" }}> <div style={{ display: "flex", flexDirection: "column", gap: "6px" }}>
<span style={{ fontSize: "0.75rem", fontWeight: 600, color: darkMode ? "#9ca3af" : "#6b7280" }}> <span style={{ fontSize: "0.7rem", fontWeight: 600, color: darkMode ? "#9ca3af" : "#6b7280" }}>{language === "de" ? "Starten mit" : "Start on"}</span>
{language === "de" ? "Starten mit" : "Start on"}
</span>
<div style={{ display: "flex", gap: "4px" }}> <div style={{ display: "flex", gap: "4px" }}>
<button <button onClick={() => { setProfile({ ...profile, startDayOffset: 0 }); saveSetting("startDayOffset", 0); const d = new Date(); d.setHours(0, 0, 0, 0); setCurrentWeekStart(d); }}
onClick={() => { style={{ padding: "4px 10px", fontSize: "0.75rem", borderRadius: "6px", border: "none", cursor: "pointer", fontWeight: (profile.startDayOffset || 0) === 0 ? 700 : 400, background: (profile.startDayOffset || 0) === 0 ? (darkMode ? "#374151" : "#333") : (darkMode ? "#1f2937" : "#e5e7eb"), color: (profile.startDayOffset || 0) === 0 ? "#fff" : (darkMode ? "#9ca3af" : "#6b7280") }}>
setProfile({ ...profile, startDayOffset: 0 });
saveSetting("startDayOffset", 0);
const d = new Date(); d.setHours(0, 0, 0, 0); setCurrentWeekStart(d);
}}
style={{
padding: "4px 10px", fontSize: "0.75rem", borderRadius: "6px", border: "none", cursor: "pointer",
fontWeight: (profile.startDayOffset || 0) === 0 ? 700 : 400,
background: (profile.startDayOffset || 0) === 0 ? (darkMode ? "#374151" : "#333") : (darkMode ? "#1f2937" : "#e5e7eb"),
color: (profile.startDayOffset || 0) === 0 ? "#fff" : (darkMode ? "#9ca3af" : "#6b7280"),
}}
>
{language === "de" ? "Heute" : "Today"} {language === "de" ? "Heute" : "Today"}
</button> </button>
<button <button 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); }}
onClick={() => { style={{ padding: "4px 10px", fontSize: "0.75rem", borderRadius: "6px", border: "none", cursor: "pointer", fontWeight: profile.startDayOffset === -1 ? 700 : 400, background: profile.startDayOffset === -1 ? (darkMode ? "#374151" : "#333") : (darkMode ? "#1f2937" : "#e5e7eb"), color: profile.startDayOffset === -1 ? "#fff" : (darkMode ? "#9ca3af" : "#6b7280") }}>
setProfile({ ...profile, startDayOffset: -1 });
saveSetting("startDayOffset", -1);
const d = new Date(); d.setHours(0, 0, 0, 0); d.setDate(d.getDate() - 1); setCurrentWeekStart(d);
}}
style={{
padding: "4px 10px", fontSize: "0.75rem", borderRadius: "6px", border: "none", cursor: "pointer",
fontWeight: profile.startDayOffset === -1 ? 700 : 400,
background: profile.startDayOffset === -1 ? (darkMode ? "#374151" : "#333") : (darkMode ? "#1f2937" : "#e5e7eb"),
color: profile.startDayOffset === -1 ? "#fff" : (darkMode ? "#9ca3af" : "#6b7280"),
}}
>
{language === "de" ? "Gestern" : "Yesterday"} {language === "de" ? "Gestern" : "Yesterday"}
</button> </button>
</div> </div>
@ -5638,49 +5652,36 @@ export default function WeeklyView() {
{/* Display mode */} {/* Display mode */}
<div style={{ display: "flex", flexDirection: "column", gap: "6px" }}> <div style={{ display: "flex", flexDirection: "column", gap: "6px" }}>
<span style={{ fontSize: "0.75rem", fontWeight: 600, color: darkMode ? "#9ca3af" : "#6b7280" }}> <span style={{ fontSize: "0.7rem", fontWeight: 600, color: darkMode ? "#9ca3af" : "#6b7280" }}>{language === "de" ? "Anzeige" : "Display"}</span>
{language === "de" ? "Anzeige" : "Display"}
</span>
<div style={{ display: "flex", gap: "4px" }}> <div style={{ display: "flex", gap: "4px" }}>
<button <button onClick={() => setDarkMode(false)}
onClick={() => setDarkMode(false)} style={{ padding: "4px 10px", fontSize: "0.75rem", borderRadius: "6px", border: "none", cursor: "pointer", display: "flex", alignItems: "center", gap: "4px", fontWeight: !darkMode ? 700 : 400, background: !darkMode ? "#333" : (darkMode ? "#1f2937" : "#e5e7eb"), color: !darkMode ? "#fff" : (darkMode ? "#9ca3af" : "#6b7280") }}>
style={{
padding: "4px 10px", fontSize: "0.75rem", borderRadius: "6px", border: "none", cursor: "pointer",
display: "flex", alignItems: "center", gap: "4px",
fontWeight: !darkMode ? 700 : 400,
background: !darkMode ? (darkMode ? "#374151" : "#333") : (darkMode ? "#1f2937" : "#e5e7eb"),
color: !darkMode ? "#fff" : (darkMode ? "#9ca3af" : "#6b7280"),
}}
>
<Sun size={12} /> {language === "de" ? "Hell" : "Light"} <Sun size={12} /> {language === "de" ? "Hell" : "Light"}
</button> </button>
<button <button onClick={() => setDarkMode(true)}
onClick={() => setDarkMode(true)} style={{ padding: "4px 10px", fontSize: "0.75rem", borderRadius: "6px", border: "none", cursor: "pointer", display: "flex", alignItems: "center", gap: "4px", fontWeight: darkMode ? 700 : 400, background: darkMode ? "#374151" : "#e5e7eb", color: darkMode ? "#fff" : "#6b7280" }}>
style={{
padding: "4px 10px", fontSize: "0.75rem", borderRadius: "6px", border: "none", cursor: "pointer",
display: "flex", alignItems: "center", gap: "4px",
fontWeight: darkMode ? 700 : 400,
background: darkMode ? "#374151" : "#e5e7eb",
color: darkMode ? "#fff" : "#6b7280",
}}
>
<Moon size={12} /> {language === "de" ? "Dunkel" : "Dark"} <Moon size={12} /> {language === "de" ? "Dunkel" : "Dark"}
</button> </button>
</div> </div>
</div> </div>
{/* Separator */}
<div style={{ borderTop: `1px solid ${darkMode ? "#333" : "#e5e7eb"}`, paddingTop: "4px" }} />
{/* Undo / Redo / Refresh */}
<div style={{ display: "flex", justifyContent: "center", gap: "8px", padding: "2px 0" }}>
<button onClick={handleUndo} disabled={undoCount === 0} style={{ ...qsBtnStyle(darkMode), opacity: undoCount === 0 ? 0.3 : 1 }} title="Undo"><Undo2 size={15} /></button>
<button onClick={handleRedo} disabled={redoCount === 0} style={{ ...qsBtnStyle(darkMode), opacity: redoCount === 0 ? 0.3 : 1 }} title="Redo"><Redo2 size={15} /></button>
<button onClick={() => { fetchCalendarEvents(true); fetchTasks(); setShowQuickSettings(false); }} style={qsBtnStyle(darkMode)} title="Refresh"><RefreshCcw size={15} /></button>
</div>
{/* Spacer */} {/* Spacer */}
<div style={{ flex: 1 }} /> <div style={{ flex: 1 }} />
{/* Close button at bottom */} {/* Close button at bottom */}
<button <button
onClick={() => setShowQuickSettings(false)} onClick={() => setShowQuickSettings(false)}
style={{ style={{ display: "flex", alignItems: "center", gap: "6px", background: "none", border: "none", cursor: "pointer", fontSize: "0.75rem", color: darkMode ? "#6b7280" : "#9ca3af", padding: "4px 0" }}
display: "flex", alignItems: "center", gap: "6px",
background: "none", border: "none", cursor: "pointer",
fontSize: "0.75rem", color: darkMode ? "#6b7280" : "#9ca3af",
padding: "4px 0",
}}
> >
<PanelLeftClose size={16} /> <PanelLeftClose size={16} />
{language === "de" ? "Ausblenden" : "Hide"} {language === "de" ? "Ausblenden" : "Hide"}
@ -5689,6 +5690,17 @@ export default function WeeklyView() {
</> </>
)} )}
{/* Projects Sidebar */}
{showProjectsSidebar && (
<ProjectsSidebar
darkMode={darkMode}
language={language}
projects={projects}
onProjectsChanged={fetchProjects}
onClose={() => setShowProjectsSidebar(false)}
/>
)}
{/* Quick Settings toggle button (bottom-left) */} {/* Quick Settings toggle button (bottom-left) */}
{!showQuickSettings && ( {!showQuickSettings && (
<button <button
@ -6141,7 +6153,7 @@ export default function WeeklyView() {
<button onClick={() => { const nv = !showNextTask; setShowNextTask(nv); saveSetting("showNextTask", nv); setShowHeaderMore(false); }}>{showNextTask ? <Play size={16} /> : <Target size={16} />} <span>{showNextTask ? "Next Task" : "Goal"}</span></button> <button onClick={() => { const nv = !showNextTask; setShowNextTask(nv); saveSetting("showNextTask", nv); setShowHeaderMore(false); }}>{showNextTask ? <Play size={16} /> : <Target size={16} />} <span>{showNextTask ? "Next Task" : "Goal"}</span></button>
<button onClick={() => { setShowFocusMode(true); setShowHeaderMore(false); }}><Zap size={16} /> <span>{language === "de" ? "Fokus" : "Focus"}</span></button> <button onClick={() => { setShowFocusMode(true); setShowHeaderMore(false); }}><Zap size={16} /> <span>{language === "de" ? "Fokus" : "Focus"}</span></button>
<button onClick={() => { setDarkMode(!darkMode); setShowHeaderMore(false); }}>{darkMode ? <Sun size={16} /> : <Moon size={16} />} <span>{darkMode ? "Light" : "Dark"}</span></button> <button onClick={() => { setDarkMode(!darkMode); setShowHeaderMore(false); }}>{darkMode ? <Sun size={16} /> : <Moon size={16} />} <span>{darkMode ? "Light" : "Dark"}</span></button>
<button onClick={() => { setShowSettings(true); setActiveTab("projects"); setShowHeaderMore(false); }}><FolderPlus size={16} /> <span>{language === "de" ? "Neues Projekt" : "New Project"}</span></button> <button onClick={() => { setShowProjectsSidebar(true); setShowHeaderMore(false); }}><FolderPlus size={16} /> <span>{language === "de" ? "Neues Projekt" : "New Project"}</span></button>
<div style={{ borderTop: "1px solid var(--border-color, #e5e7eb)", margin: "2px 0" }} /> <div style={{ borderTop: "1px solid var(--border-color, #e5e7eb)", margin: "2px 0" }} />
<div style={{ padding: "6px 12px", display: "flex", alignItems: "center", gap: "6px", flexWrap: "wrap" }}> <div style={{ padding: "6px 12px", display: "flex", alignItems: "center", gap: "6px", flexWrap: "wrap" }}>
<span style={{ fontSize: "12px", color: "#888", marginRight: "4px" }}>{language === "de" ? "Tage" : "Days"}:</span> <span style={{ fontSize: "12px", color: "#888", marginRight: "4px" }}>{language === "de" ? "Tage" : "Days"}:</span>
@ -6176,7 +6188,7 @@ export default function WeeklyView() {
{/* Desktop only: Recurring Tasks + New Project */} {/* Desktop only: Recurring Tasks + New Project */}
<button className="weekly-btn-icon header-desktop-only" onClick={() => setIsRecurringTasksOpen(true)} title="Recurring Tasks"><Repeat size={17} /></button> <button className="weekly-btn-icon header-desktop-only" onClick={() => setIsRecurringTasksOpen(true)} title="Recurring Tasks"><Repeat size={17} /></button>
<button className="weekly-btn-icon header-desktop-only" onClick={() => { setShowSettings(true); setActiveTab("projects"); }} title="New Project"><FolderPlus size={17} /></button> <button className="weekly-btn-icon header-desktop-only" onClick={() => setShowProjectsSidebar(true)} title="New Project"><FolderPlus size={17} /></button>
{/* User Menu */} {/* User Menu */}
<UserMenu <UserMenu
@ -8315,7 +8327,7 @@ export default function WeeklyView() {
const now = new Date(); const now = new Date();
setCalendarEventModal({ isOpen: true, event: undefined, initialDate: now, initialStartTime: `${String(now.getHours()).padStart(2, "0")}:00` }); setCalendarEventModal({ isOpen: true, event: undefined, initialDate: now, initialStartTime: `${String(now.getHours()).padStart(2, "0")}:00` });
}, },
onAddProject: () => { setShowSettings(true); setActiveTab("projects"); }, onAddProject: () => { setShowProjectsSidebar(true); },
onRecurringTasks: () => setIsRecurringTasksOpen(true), onRecurringTasks: () => setIsRecurringTasksOpen(true),
onToggleNextTask: () => { const newVal = !showNextTask; setShowNextTask(newVal); saveSetting("showNextTask", newVal); }, onToggleNextTask: () => { const newVal = !showNextTask; setShowNextTask(newVal); saveSetting("showNextTask", newVal); },
onFocusMode: () => setShowFocusMode(true), onFocusMode: () => setShowFocusMode(true),
@ -8401,7 +8413,7 @@ export default function WeeklyView() {
<div className="mobile-fab-menu-icon" style={{ background: "#10b981" }}><Search size={18} /></div> <div className="mobile-fab-menu-icon" style={{ background: "#10b981" }}><Search size={18} /></div>
<span>{language === "de" ? "Suche" : "Search"}</span> <span>{language === "de" ? "Suche" : "Search"}</span>
</button> </button>
<button className="mobile-fab-menu-item" onClick={() => { setShowMobileFabMenu(false); setShowSettings(true); setActiveTab("projects"); }}> <button className="mobile-fab-menu-item" onClick={() => { setShowMobileFabMenu(false); setShowProjectsSidebar(true); }}>
<div className="mobile-fab-menu-icon" style={{ background: "#6366f1" }}><FolderPlus size={18} /></div> <div className="mobile-fab-menu-icon" style={{ background: "#6366f1" }}><FolderPlus size={18} /></div>
<span>{language === "de" ? "Projekt" : "Project"}</span> <span>{language === "de" ? "Projekt" : "Project"}</span>
</button> </button>
@ -9487,6 +9499,172 @@ function TaskItem({
); );
} }
// Projects Sidebar Component
const projectEmojisGlobal = [
"📁", "📂", "💼", "🎯", "🚀", "⭐", "💡", "🔥", "🎨", "🎵",
"📱", "💻", "🌐", "🏠", "🏢", "📊", "📈", "🔧", "⚡", "🎮",
"📝", "📖", "🎓", "🧪", "🔬", "🏋️", "🍽️", "✈️", "🌿", "❤️",
"🛒", "💰", "🎁", "📸", "🎬", "🧹", "🐾", "🌍", "🔒", "✅",
];
function ProjectsSidebar({ darkMode, language, projects, onProjectsChanged, onClose }: {
darkMode: boolean;
language: string;
projects: { id: string; name: string; icon?: string | null; color?: string | null }[];
onProjectsChanged: () => void;
onClose: () => void;
}) {
const [newProjectName, setNewProjectName] = useState("");
const [newProjectColor, setNewProjectColor] = useState("#3b82f6");
const [newProjectIcon, setNewProjectIcon] = useState("📁");
const [showNewIconPicker, setShowNewIconPicker] = useState(false);
const [editingId, setEditingId] = useState<string | null>(null);
const [editName, setEditName] = useState("");
const [editColor, setEditColor] = useState("");
const [editIcon, setEditIcon] = useState("");
const [showEditIconPicker, setShowEditIconPicker] = useState(false);
const t = {
projects: language === "de" ? "Projekte" : "Projects",
desc: language === "de" ? "Aufgaben mit farbcodierten Projekten organisieren" : "Organize tasks with color-coded projects",
noProjects: language === "de" ? "Noch keine Projekte erstellt" : "No projects yet",
addProject: language === "de" ? "Projekt hinzufügen" : "Add Project",
name: language === "de" ? "Name" : "Name",
cancel: language === "de" ? "Abbrechen" : "Cancel",
save: language === "de" ? "Speichern" : "Save",
color: language === "de" ? "Farbe" : "Color",
deleteConfirm: (name: string) => language === "de" ? `Projekt "${name}" löschen?` : `Delete project "${name}"?`,
};
const createProject = () => {
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("📁"); });
};
const updateProject = (id: string) => {
fetch("/api/projects", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id, name: editName, color: editColor, icon: editIcon }) })
.then(() => { onProjectsChanged(); setEditingId(null); });
};
const deleteProject = (id: string, name: string) => {
if (confirm(t.deleteConfirm(name))) {
fetch(`/api/projects?id=${id}`, { method: "DELETE" }).then(() => onProjectsChanged());
}
};
return (
<>
<div onClick={onClose} style={{ position: "fixed", inset: 0, zIndex: 999, background: "rgba(0,0,0,0.3)" }} />
<div style={{
position: "fixed", right: 0, top: 0, bottom: 0, width: "min(380px, 90vw)", zIndex: 1000,
background: darkMode ? "#1e1e2e" : "#fff",
borderLeft: `1px solid ${darkMode ? "#333" : "#e5e7eb"}`,
display: "flex", flexDirection: "column",
boxShadow: "-4px 0 24px rgba(0,0,0,0.12)",
}}>
{/* Header */}
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "16px 20px", borderBottom: `1px solid ${darkMode ? "#333" : "#e5e7eb"}` }}>
<h2 style={{ fontSize: "1.1rem", fontWeight: 700, display: "flex", alignItems: "center", gap: "8px", color: darkMode ? "#f0f0f0" : "#333" }}>
<FolderOpen size={20} /> {t.projects}
</h2>
<button onClick={onClose} style={{ background: "none", border: "none", cursor: "pointer", padding: "6px", color: darkMode ? "#9ca3af" : "#666", borderRadius: "6px" }}>
<X size={20} />
</button>
</div>
{/* Scrollable content */}
<div style={{ flex: 1, overflowY: "auto", padding: "16px 20px", display: "flex", flexDirection: "column", gap: "12px" }}>
<p style={{ fontSize: "0.8rem", color: darkMode ? "#9ca3af" : "#888" }}>{t.desc}</p>
{projects.length === 0 ? (
<div style={{ textAlign: "center", padding: "32px 16px" }}>
<div style={{ width: "56px", height: "56px", borderRadius: "50%", background: darkMode ? "#2a2a3a" : "#f3f4f6", display: "flex", alignItems: "center", justifyContent: "center", margin: "0 auto 12px", fontSize: "24px" }}>📁</div>
<p style={{ fontSize: "0.9rem", color: "#aaa", fontStyle: "italic" }}>{t.noProjects}</p>
</div>
) : (
projects.map((p) => (
<div key={p.id} style={{ display: "flex", alignItems: "center", gap: "10px", padding: "10px 14px", borderRadius: "12px", background: darkMode ? "#2a2a3a" : "#f9fafb", borderLeft: `4px solid ${p.color || "#999"}` }}>
{editingId === p.id ? (
<div style={{ display: "flex", flexDirection: "column", gap: "10px", width: "100%" }}>
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<div style={{ position: "relative" }}>
<button onClick={() => setShowEditIconPicker(!showEditIconPicker)} style={{ width: "40px", height: "40px", borderRadius: "10px", border: `1px solid ${darkMode ? "#555" : "#e5e7eb"}`, background: darkMode ? "#1e1e2e" : "#fff", cursor: "pointer", fontSize: "20px", display: "flex", alignItems: "center", justifyContent: "center" }}>
{editIcon || "📁"}
</button>
{showEditIconPicker && (
<div style={{ position: "absolute", top: "100%", left: 0, marginTop: "4px", zIndex: 50, background: darkMode ? "#2a2a3a" : "#fff", border: `1px solid ${darkMode ? "#555" : "#e5e7eb"}`, borderRadius: "12px", padding: "10px", boxShadow: "0 8px 24px rgba(0,0,0,0.2)", width: "240px" }}>
<div style={{ display: "grid", gridTemplateColumns: "repeat(8, 1fr)", gap: "4px" }}>
{projectEmojisGlobal.map((emoji) => (
<button key={emoji} onClick={() => { setEditIcon(emoji); setShowEditIconPicker(false); }} style={{ width: "28px", height: "28px", border: "none", background: editIcon === emoji ? (darkMode ? "#374151" : "#f3f4f6") : "transparent", borderRadius: "6px", cursor: "pointer", fontSize: "16px", display: "flex", alignItems: "center", justifyContent: "center" }}>
{emoji}
</button>
))}
</div>
</div>
)}
</div>
<input type="text" value={editName} onChange={(e) => setEditName(e.target.value)} className="weekly-input" style={{ flex: 1, padding: "8px 12px", fontSize: "0.9rem" }} onKeyDown={(e) => { if (e.key === "Enter") updateProject(p.id); if (e.key === "Escape") setEditingId(null); }} autoFocus />
</div>
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<input type="color" value={editColor} onChange={(e) => setEditColor(e.target.value)} style={{ width: "32px", height: "32px", border: "none", cursor: "pointer", padding: 0, borderRadius: "8px" }} />
<span style={{ fontSize: "0.8rem", color: darkMode ? "#9ca3af" : "#888" }}>{t.color}</span>
<div style={{ flex: 1 }} />
<button onClick={() => setEditingId(null)} style={{ padding: "6px 14px", fontSize: "0.8rem", background: "none", border: `1px solid ${darkMode ? "#555" : "#ddd"}`, borderRadius: "8px", cursor: "pointer", color: darkMode ? "#9ca3af" : "#666" }}>{t.cancel}</button>
<button onClick={() => updateProject(p.id)} className="weekly-btn-primary" style={{ padding: "6px 14px", fontSize: "0.8rem" }}><Check size={14} /> {t.save}</button>
</div>
</div>
) : (
<>
<span style={{ fontSize: "22px", lineHeight: 1 }}>{p.icon || "📁"}</span>
<div style={{ flex: 1, minWidth: 0 }}>
<span style={{ fontSize: "0.9rem", fontWeight: 600, display: "block", color: darkMode ? "#f0f0f0" : "#333" }}>{p.name}</span>
</div>
<button onClick={() => { setEditingId(p.id); setEditName(p.name); setEditColor(p.color || "#999"); setEditIcon(p.icon || "📁"); setShowEditIconPicker(false); }} style={{ padding: "6px", opacity: 0.5, cursor: "pointer", background: "none", border: "none", borderRadius: "6px", color: darkMode ? "#ccc" : "#333" }} title="Edit">
<Pencil size={15} />
</button>
<button onClick={() => deleteProject(p.id, p.name)} style={{ padding: "6px", opacity: 0.5, cursor: "pointer", color: "#ef4444", background: "none", border: "none", borderRadius: "6px" }} title="Delete">
<Trash2 size={15} />
</button>
</>
)}
</div>
))
)}
</div>
{/* Add project form — fixed at bottom */}
<div style={{ padding: "16px 20px", borderTop: `1px solid ${darkMode ? "#333" : "#e5e7eb"}`, background: darkMode ? "#1e1e2e" : "#fff" }}>
<p style={{ fontSize: "0.8rem", fontWeight: 600, color: darkMode ? "#9ca3af" : "#888", marginBottom: "10px" }}>{t.addProject}</p>
<div style={{ display: "flex", gap: "8px", alignItems: "center" }}>
<div style={{ position: "relative" }}>
<button onClick={() => setShowNewIconPicker(!showNewIconPicker)} style={{ width: "40px", height: "40px", borderRadius: "10px", border: `1px solid ${darkMode ? "#555" : "#e5e7eb"}`, background: darkMode ? "#2a2a3a" : "#fff", cursor: "pointer", fontSize: "20px", display: "flex", alignItems: "center", justifyContent: "center" }}>
{newProjectIcon}
</button>
{showNewIconPicker && (
<div style={{ position: "absolute", bottom: "100%", left: 0, marginBottom: "4px", zIndex: 50, background: darkMode ? "#2a2a3a" : "#fff", border: `1px solid ${darkMode ? "#555" : "#e5e7eb"}`, borderRadius: "12px", padding: "10px", boxShadow: "0 8px 24px rgba(0,0,0,0.2)", width: "240px" }}>
<div style={{ display: "grid", gridTemplateColumns: "repeat(8, 1fr)", gap: "4px" }}>
{projectEmojisGlobal.map((emoji) => (
<button key={emoji} onClick={() => { setNewProjectIcon(emoji); setShowNewIconPicker(false); }} style={{ width: "28px", height: "28px", border: "none", background: newProjectIcon === emoji ? (darkMode ? "#374151" : "#f3f4f6") : "transparent", borderRadius: "6px", cursor: "pointer", fontSize: "16px", display: "flex", alignItems: "center", justifyContent: "center" }}>
{emoji}
</button>
))}
</div>
</div>
)}
</div>
<input type="color" value={newProjectColor} onChange={(e) => setNewProjectColor(e.target.value)} style={{ width: "32px", height: "32px", border: "none", cursor: "pointer", padding: 0, borderRadius: "8px" }} />
<input type="text" value={newProjectName} onChange={(e) => setNewProjectName(e.target.value)} placeholder={t.name} className="weekly-input" style={{ flex: 1, padding: "8px 12px", fontSize: "0.9rem" }} onKeyDown={(e) => { if (e.key === "Enter") createProject(); }} />
<button onClick={createProject} className="weekly-btn-primary" style={{ padding: "8px 16px", fontSize: "0.85rem", whiteSpace: "nowrap" }}>
<Plus size={14} /> {t.addProject}
</button>
</div>
</div>
</div>
</>
);
}
// Settings Modal Component // Settings Modal Component
interface SettingsSidebarProps { interface SettingsSidebarProps {
onClose: () => void; onClose: () => void;
@ -9639,7 +9817,7 @@ interface SettingsSidebarProps {
fetchAvailableTaskLists: ( fetchAvailableTaskLists: (
provider: "google" | "apple" | "outlook" | "synology", provider: "google" | "apple" | "outlook" | "synology",
) => Promise<void>; ) => Promise<void>;
initialTab?: "calendar" | "general" | "account" | "styling" | "motivation" | "about" | "projects"; initialTab?: "calendar" | "general" | "account" | "styling" | "motivation" | "about";
projects: { id: string; name: string; icon?: string | null; color?: string | null }[]; projects: { id: string; name: string; icon?: string | null; color?: string | null }[];
onProjectsChanged: () => void; onProjectsChanged: () => void;
kanbanStages: KanbanStage[]; kanbanStages: KanbanStage[];
@ -9905,7 +10083,7 @@ function SettingsSidebar({
mobileActions, mobileActions,
}: SettingsSidebarProps) { }: SettingsSidebarProps) {
const [activeTab, setActiveTab] = useState< const [activeTab, setActiveTab] = useState<
"calendar" | "general" | "account" | "styling" | "motivation" | "about" | "localisation" | "projects" "calendar" | "general" | "account" | "styling" | "motivation" | "about" | "localisation"
>(initialTab || "general"); >(initialTab || "general");
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [isSyncing, setIsSyncing] = useState(false); const [isSyncing, setIsSyncing] = useState(false);
@ -10375,193 +10553,6 @@ function SettingsSidebar({
</button> </button>
</header> </header>
{/* Quick Actions Panel */}
{mobileActions && (
<div className="mobile-quick-actions" style={{
padding: "12px 16px",
borderBottom: "1px solid var(--weekly-border, #eee)",
display: "flex",
flexDirection: "column",
gap: "8px",
}}>
{/* Navigation Row */}
<div style={{ display: "flex", justifyContent: "center", gap: "4px", padding: "4px 0" }}>
<button className="mobile-quick-nav-btn" onClick={() => { mobileActions.goToPrevWeek(); handleClose(); }} title="Previous Week">
<ChevronsLeft size={18} />
</button>
<button className="mobile-quick-nav-btn" onClick={() => { mobileActions.goToPrevDay(); handleClose(); }} title="Previous Day">
<ChevronLeft size={18} />
</button>
<button className="mobile-quick-nav-btn" onClick={() => { mobileActions.goToToday(); handleClose(); }} title="Today"
style={{ fontWeight: 700, fontSize: "0.75rem", minWidth: 64 }}>
{t.today || "Today"}
</button>
<button className="mobile-quick-nav-btn" onClick={() => { mobileActions.goToNextDay(); handleClose(); }} title="Next Day">
<ChevronRight size={18} />
</button>
<button className="mobile-quick-nav-btn" onClick={() => { mobileActions.goToNextWeek(); handleClose(); }} title="Next Week">
<ChevronsRight size={18} />
</button>
</div>
{/* Quick Action Buttons */}
<div style={{ display: "flex", flexDirection: "column", gap: "2px" }}>
<button className="mobile-quick-action-btn" onClick={() => { mobileActions.onSearch(); handleClose(); }}>
<Search size={18} /> <span>{t.search || "Search"}</span>
</button>
<button className="mobile-quick-action-btn" onClick={() => { mobileActions.onJumpToDate(); handleClose(); }}>
<Calendar size={18} /> <span>{t.jumpToDate || "Jump to date"}</span>
</button>
<button className="mobile-quick-action-btn" onClick={() => { mobileActions.onAddCalendarEvent(); handleClose(); }}>
<Plus size={18} /> <span>{t.addCalendarEvent || "Add Calendar Event"}</span>
</button>
<button className="mobile-quick-action-btn" onClick={() => { mobileActions.onAddProject(); }}>
<FolderPlus size={18} /> <span>{t.addProject || "New Project"}</span>
</button>
<button className="mobile-quick-action-btn" onClick={() => { mobileActions.onRecurringTasks(); handleClose(); }}>
<Repeat size={18} /> <span>{t.recurringTasks || "Recurring Tasks"}</span>
</button>
<button className="mobile-quick-action-btn" onClick={() => { mobileActions.onToggleNextTask(); }}>
{mobileActions.showNextTask ? <Play size={18} className="text-teal-600" /> : <Target size={18} />}
<span>{mobileActions.showNextTask ? (t.showingNextTask || "Showing Next Task") : (t.showingGoal || "Showing Goal")}</span>
</button>
<button className="mobile-quick-action-btn" onClick={() => { mobileActions.onFocusMode(); handleClose(); }}>
<Zap size={18} /> <span>{t.focusMode || "Focus Mode"}</span>
</button>
<button className="mobile-quick-action-btn" onClick={() => { mobileActions.onToggleDarkMode(); }}>
{mobileActions.darkMode ? <Sun size={18} className="text-yellow-500" /> : <Moon size={18} />}
<span>{mobileActions.darkMode ? (t.lightMode || "Light Mode") : (t.darkMode || "Dark Mode")}</span>
</button>
</div>
{/* View Style */}
<div style={{ display: "flex", flexDirection: "column", gap: "8px", padding: "4px 0" }}>
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<span style={{ fontSize: "0.75rem", color: "var(--weekly-text-light, #9ca3af)", minWidth: "40px" }}>{t.view || "View"}</span>
<div style={{ display: "flex", gap: "4px", flex: 1 }}>
{[
{ key: "simple", icon: <CalendarDays size={14} />, label: "Simple" },
{ key: "calendar", icon: <Calendar size={14} />, label: "Calendar" },
{ key: "list", icon: <ListTodo size={14} />, label: "List" },
{ key: "kanban", icon: <Kanban size={14} />, label: "Kanban" },
].map((v) => (
<button
key={v.key}
onClick={() => { mobileActions.onViewStyleChange(v.key); }}
title={v.label}
style={{
flex: 1, padding: "6px 0", borderRadius: "6px", border: "none", cursor: "pointer",
display: "flex", alignItems: "center", justifyContent: "center",
fontWeight: mobileActions.viewStyle === v.key ? 700 : 400,
background: mobileActions.viewStyle === v.key ? "#0ea5e9" : "var(--weekly-settings-toggle-bg, #f3f4f6)",
color: mobileActions.viewStyle === v.key ? "#fff" : "var(--weekly-text, #333)",
}}
>
{v.icon}
</button>
))}
</div>
</div>
{/* Days */}
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<span style={{ fontSize: "0.75rem", color: "var(--weekly-text-light, #9ca3af)", minWidth: "40px" }}>{t.days || "Days"}</span>
<div style={{ display: "flex", gap: "4px", flex: 1 }}>
{[1, 2, 3, 5, 7].map((num) => (
<button
key={num}
onClick={() => { mobileActions.onViewDaysChange(num); }}
style={{
flex: 1, padding: "6px 0", borderRadius: "6px", border: "none", cursor: "pointer",
fontSize: "0.8rem", fontWeight: mobileActions.viewDays === num ? 700 : 400,
background: mobileActions.viewDays === num ? "#0ea5e9" : "var(--weekly-settings-toggle-bg, #f3f4f6)",
color: mobileActions.viewDays === num ? "#fff" : "var(--weekly-text, #333)",
}}
>
{num}
</button>
))}
</div>
</div>
{/* Slot Duration */}
{mobileActions.showTimeGrid && (
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<span style={{ fontSize: "0.75rem", color: "var(--weekly-text-light, #9ca3af)", minWidth: "40px" }}>{t.slot || "Slot"}</span>
<div style={{ display: "flex", gap: "4px", flex: 1 }}>
{[15, 30, 60].map((d) => (
<button
key={d}
onClick={() => { mobileActions.onCellDurationChange(d as CellDuration); }}
style={{
flex: 1, padding: "6px 0", borderRadius: "6px", border: "none", cursor: "pointer",
fontSize: "0.8rem", fontWeight: mobileActions.cellDuration === d ? 700 : 400,
background: mobileActions.cellDuration === d ? "#0ea5e9" : "var(--weekly-settings-toggle-bg, #f3f4f6)",
color: mobileActions.cellDuration === d ? "#fff" : "var(--weekly-text, #333)",
}}
>
{d}m
</button>
))}
</div>
</div>
)}
{/* Visible Hours */}
{mobileActions.showTimeGrid && (
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<span style={{ fontSize: "0.75rem", color: "var(--weekly-text-light, #9ca3af)", minWidth: "40px" }}>
<Clock size={14} />
</span>
<div style={{ display: "flex", alignItems: "center", gap: "4px", flex: 1 }}>
<input
type="number"
min={0}
max={23}
value={mobileActions.startHour}
onChange={(e) => mobileActions.onStartHourChange(Math.max(0, Math.min(23, parseInt(e.target.value) || 0)))}
style={{
width: "48px", padding: "4px 6px", borderRadius: "6px", border: "1px solid var(--weekly-border, #e5e7eb)",
background: "var(--weekly-settings-toggle-bg, #f3f4f6)", color: "var(--weekly-text, #333)",
fontSize: "0.8rem", textAlign: "center", outline: "none",
}}
/>
<span style={{ fontSize: "0.75rem", color: "var(--weekly-text-light, #9ca3af)" }}></span>
<input
type="number"
min={1}
max={24}
value={mobileActions.endHour}
onChange={(e) => mobileActions.onEndHourChange(Math.max(1, Math.min(24, parseInt(e.target.value) || 24)))}
style={{
width: "48px", padding: "4px 6px", borderRadius: "6px", border: "1px solid var(--weekly-border, #e5e7eb)",
background: "var(--weekly-settings-toggle-bg, #f3f4f6)", color: "var(--weekly-text, #333)",
fontSize: "0.8rem", textAlign: "center", outline: "none",
}}
/>
<span style={{ fontSize: "0.7rem", color: "var(--weekly-text-light, #9ca3af)" }}>h</span>
</div>
</div>
)}
</div>
{/* Utility Row */}
<div style={{ display: "flex", justifyContent: "center", gap: "8px", padding: "4px 0", borderTop: "1px solid var(--weekly-border, #eee)", paddingTop: "8px" }}>
<button className="mobile-quick-nav-btn" onClick={() => { mobileActions.onUndo(); }} disabled={mobileActions.undoCount === 0}
style={{ opacity: mobileActions.undoCount === 0 ? 0.3 : 1 }} title="Undo">
<Undo2 size={16} />
</button>
<button className="mobile-quick-nav-btn" onClick={() => { mobileActions.onRedo(); }} disabled={mobileActions.redoCount === 0}
style={{ opacity: mobileActions.redoCount === 0 ? 0.3 : 1 }} title="Redo">
<Redo2 size={16} />
</button>
<button className="mobile-quick-nav-btn" onClick={() => { mobileActions.onRefresh(); handleClose(); }} title="Refresh">
<RefreshCcw size={16} />
</button>
</div>
</div>
)}
<div <div
className="weekly-settings-tabs" className="weekly-settings-tabs"
style={{ style={{
@ -10575,7 +10566,6 @@ function SettingsSidebar({
> >
{([ {([
{ key: "general", icon: <Settings size={18} />, label: t.general }, { key: "general", icon: <Settings size={18} />, label: t.general },
{ key: "projects", icon: <FolderOpen size={18} />, label: t.projects },
{ key: "localisation", icon: <Globe size={18} />, label: t.localisation }, { key: "localisation", icon: <Globe size={18} />, label: t.localisation },
{ key: "calendar", icon: <Link size={18} />, label: t.calendar }, { key: "calendar", icon: <Link size={18} />, label: t.calendar },
{ key: "account", icon: <User size={18} />, label: t.account }, { key: "account", icon: <User size={18} />, label: t.account },
@ -11526,125 +11516,6 @@ function SettingsSidebar({
<div /> <div />
</div> </div>
) : activeTab === "projects" ? (
<div style={{ display: "flex", flexDirection: "column", gap: "20px" }}>
<div>
<h3 style={{ fontSize: "1.1rem", fontWeight: 700, marginBottom: "4px", display: "flex", alignItems: "center", gap: "8px" }}>
<FolderOpen size={20} /> {t.projects}
</h3>
<p style={{ fontSize: "0.8rem", color: "#888", marginBottom: "16px" }}>{t.projectsDesc}</p>
</div>
{projects.length === 0 ? (
<div style={{ textAlign: "center", padding: "32px 16px" }}>
<div style={{ width: "56px", height: "56px", borderRadius: "50%", background: "var(--bg-secondary, #f3f4f6)", display: "flex", alignItems: "center", justifyContent: "center", margin: "0 auto 12px", fontSize: "24px" }}>📁</div>
<p style={{ fontSize: "0.9rem", color: "#aaa", fontStyle: "italic" }}>{t.noProjects}</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: "12px", background: "var(--bg-secondary, #f9fafb)", borderLeft: `4px solid ${p.color || "#999"}`, transition: "box-shadow 0.15s" }}>
{editingProjectId === p.id ? (
<div style={{ display: "flex", flexDirection: "column", gap: "10px", width: "100%" }}>
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<div style={{ position: "relative" }}>
<button
onClick={() => setShowEditProjectIconPicker(!showEditProjectIconPicker)}
style={{ width: "40px", height: "40px", borderRadius: "10px", border: "1px solid var(--border-color, #e5e7eb)", background: "var(--bg-primary, #fff)", cursor: "pointer", fontSize: "20px", display: "flex", alignItems: "center", justifyContent: "center" }}
title="Change icon"
>
{editProjectIcon || "📁"}
</button>
{showEditProjectIconPicker && (
<div style={{ position: "absolute", top: "100%", left: 0, marginTop: "4px", zIndex: 50, background: "var(--bg-primary, #fff)", border: "1px solid var(--border-color, #e5e7eb)", borderRadius: "12px", padding: "10px", boxShadow: "0 8px 24px rgba(0,0,0,0.12)", width: "240px" }}>
<div style={{ display: "grid", gridTemplateColumns: "repeat(8, 1fr)", gap: "4px" }}>
{projectEmojis.map((emoji) => (
<button
key={emoji}
onClick={() => { setEditProjectIcon(emoji); setShowEditProjectIconPicker(false); }}
style={{ width: "28px", height: "28px", border: "none", background: editProjectIcon === emoji ? "var(--bg-secondary, #f3f4f6)" : "transparent", borderRadius: "6px", cursor: "pointer", fontSize: "16px", display: "flex", alignItems: "center", justifyContent: "center" }}
>
{emoji}
</button>
))}
</div>
</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: "32px", height: "32px", border: "none", cursor: "pointer", padding: 0, borderRadius: "8px" }} />
<span style={{ fontSize: "0.8rem", color: "#888" }}>{profile.language === "de" ? "Farbe" : "Color"}</span>
<div style={{ flex: 1 }} />
<button onClick={() => setEditingProjectId(null)} style={{ padding: "6px 14px", fontSize: "0.8rem", background: "none", border: "1px solid var(--border-color, #ddd)", borderRadius: "8px", cursor: "pointer", color: "var(--text-secondary, #666)" }}>
{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 14px", fontSize: "0.8rem" }}>
<Check size={14} /> {profile.language === "de" ? "Speichern" : "Save"}
</button>
</div>
</div>
) : (
<>
<span style={{ fontSize: "22px", lineHeight: 1 }}>{p.icon || "📁"}</span>
<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 || "#999"); setEditProjectIcon(p.icon || "📁"); setShowEditProjectIconPicker(false); }} style={{ padding: "6px", opacity: 0.5, cursor: "pointer", background: "none", border: "none", borderRadius: "6px" }} title="Edit">
<Pencil size={15} />
</button>
<button onClick={() => { if (confirm(profile.language === "de" ? `Projekt "${p.name}" löschen?` : `Delete project "${p.name}"?`)) { fetch(`/api/projects?id=${p.id}`, { method: "DELETE" }).then(() => onProjectsChanged()); } }} style={{ padding: "6px", opacity: 0.5, cursor: "pointer", color: "#ef4444", background: "none", border: "none", borderRadius: "6px" }} title="Delete">
<Trash2 size={15} />
</button>
</>
)}
</div>
))}
</div>
)}
{/* Add new project */}
<div style={{ padding: "14px", borderRadius: "12px", border: "2px dashed var(--border-color, #d1d5db)", background: "var(--bg-secondary, #f9fafb)" }}>
<p style={{ fontSize: "0.8rem", fontWeight: 600, color: "#888", marginBottom: "10px" }}>{t.addProject}</p>
<div style={{ display: "flex", gap: "8px", alignItems: "center" }}>
<div style={{ position: "relative" }}>
<button onClick={() => setShowNewProjectIconPicker(!showNewProjectIconPicker)} style={{ width: "40px", height: "40px", borderRadius: "10px", border: "1px solid var(--border-color, #e5e7eb)", background: "var(--bg-primary, #fff)", cursor: "pointer", fontSize: "20px", display: "flex", alignItems: "center", justifyContent: "center" }} title="Choose icon">
{newProjectIcon}
</button>
{showNewProjectIconPicker && (
<div style={{ position: "absolute", top: "100%", left: 0, marginTop: "4px", zIndex: 50, background: "var(--bg-primary, #fff)", border: "1px solid var(--border-color, #e5e7eb)", borderRadius: "12px", padding: "10px", boxShadow: "0 8px 24px rgba(0,0,0,0.12)", width: "240px" }}>
<div style={{ display: "grid", gridTemplateColumns: "repeat(8, 1fr)", gap: "4px" }}>
{projectEmojis.map((emoji) => (
<button key={emoji} onClick={() => { setNewProjectIcon(emoji); setShowNewProjectIconPicker(false); }} style={{ width: "28px", height: "28px", border: "none", background: newProjectIcon === emoji ? "var(--bg-secondary, #f3f4f6)" : "transparent", borderRadius: "6px", cursor: "pointer", fontSize: "16px", display: "flex", alignItems: "center", justifyContent: "center" }}>
{emoji}
</button>
))}
</div>
</div>
)}
</div>
<input type="color" value={newProjectColor} onChange={(e) => setNewProjectColor(e.target.value)} style={{ width: "32px", height: "32px", border: "none", cursor: "pointer", padding: 0, borderRadius: "8px" }} />
<input type="text" value={newProjectName} onChange={(e) => setNewProjectName(e.target.value)} placeholder={t.projectName} 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("📁"); }); } }} />
<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("📁"); }); }} className="weekly-btn-primary" style={{ padding: "8px 16px", fontSize: "0.85rem", whiteSpace: "nowrap" }}>
<Plus size={14} /> {t.addProject}
</button>
</div>
</div>
</div>
) : activeTab === "calendar" ? ( ) : activeTab === "calendar" ? (
isLoading ? ( isLoading ? (
<p>Loading connections...</p> <p>Loading connections...</p>