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:
parent
62257ff089
commit
183fd6ea48
@ -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": {
|
||||||
|
|||||||
@ -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 */
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user