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",
|
||||
"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",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@ -4358,10 +4358,15 @@ h3 {
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.time-grid-on .weekly-day-header,
|
||||
.time-grid-on .time-column-header {
|
||||
.time-grid-on .weekly-day-header {
|
||||
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 */
|
||||
|
||||
@ -1807,7 +1807,7 @@ export default function WeeklyView() {
|
||||
// Moved state definitions to the top
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<
|
||||
"calendar" | "general" | "account" | "styling" | "motivation" | "about" | "projects"
|
||||
"calendar" | "general" | "account" | "styling" | "motivation" | "about"
|
||||
>("general");
|
||||
const [exportStartDate, setExportStartDate] = useState("");
|
||||
const [exportEndDate, setExportEndDate] = useState("");
|
||||
@ -1991,6 +1991,7 @@ export default function WeeklyView() {
|
||||
const [showDatePicker, setShowDatePicker] = useState(false);
|
||||
const datePickerBtnRef = useRef<HTMLDivElement>(null);
|
||||
const [showQuickSettings, setShowQuickSettings] = useState(false);
|
||||
const [showProjectsSidebar, setShowProjectsSidebar] = useState(false);
|
||||
|
||||
const [focusTimerDuration, setFocusTimerDuration] = useState(25);
|
||||
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");
|
||||
|
||||
// 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)
|
||||
const allDaySection = (() => {
|
||||
if (!showAllDay) return null;
|
||||
@ -5471,166 +5484,167 @@ export default function WeeklyView() {
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: "220px",
|
||||
width: "240px",
|
||||
background: darkMode ? "#1a1a2e" : "#fafafa",
|
||||
borderRight: `1px solid ${darkMode ? "#333" : "#e5e7eb"}`,
|
||||
zIndex: 1000,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
padding: "20px 16px",
|
||||
gap: "18px",
|
||||
padding: "16px 14px",
|
||||
gap: "10px",
|
||||
overflowY: "auto",
|
||||
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"}
|
||||
</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" }}>
|
||||
<span style={{ fontSize: "0.75rem", fontWeight: 600, color: darkMode ? "#9ca3af" : "#6b7280" }}>
|
||||
{language === "de" ? "Spalten" : "Columns"}
|
||||
<span style={{ fontSize: "0.7rem", fontWeight: 600, color: darkMode ? "#9ca3af" : "#6b7280" }}>
|
||||
{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>
|
||||
<div style={{ display: "flex", gap: "4px" }}>
|
||||
{[1, 2, 3, 5, 7].map((num) => (
|
||||
<button
|
||||
key={num}
|
||||
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"),
|
||||
}}
|
||||
>
|
||||
<button key={num} onClick={() => { setViewDays(num); savedViewDaysRef.current = num; saveSetting("viewDays", 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") }}>
|
||||
{num}
|
||||
</button>
|
||||
))}
|
||||
</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 */}
|
||||
<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"}
|
||||
</span>
|
||||
<div style={{ display: "flex", gap: "4px" }}>
|
||||
{(["S", "M", "L"] as const).map((size) => (
|
||||
<button
|
||||
key={size}
|
||||
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"),
|
||||
}}
|
||||
>
|
||||
<button key={size} 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}
|
||||
</button>
|
||||
))}
|
||||
</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" }}>
|
||||
<span style={{ fontSize: "0.75rem", fontWeight: 600, color: darkMode ? "#9ca3af" : "#6b7280" }}>
|
||||
{language === "de" ? "Irgendwann" : "Someday"}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => { const v = !showSomeday; setShowSomeday(v); saveSetting("showSomeday", v); }}
|
||||
style={{ background: "none", border: "none", cursor: "pointer", padding: "2px", color: darkMode ? "#9ca3af" : "#6b7280" }}
|
||||
>
|
||||
<span style={{ fontSize: "0.75rem", fontWeight: 600, color: darkMode ? "#9ca3af" : "#6b7280" }}>{language === "de" ? "Irgendwann" : "Someday"}</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} />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Schedule / Time Grid */}
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<span style={{ fontSize: "0.75rem", fontWeight: 600, color: darkMode ? "#9ca3af" : "#6b7280" }}>
|
||||
{language === "de" ? "Zeitplan" : "Schedule"}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => { const v = !showTimeGrid; setShowTimeGrid(v); saveSetting("showTimeGrid", v); }}
|
||||
style={{ background: "none", border: "none", cursor: "pointer", padding: "2px", color: darkMode ? "#9ca3af" : "#6b7280" }}
|
||||
>
|
||||
<span style={{ fontSize: "0.75rem", fontWeight: 600, color: darkMode ? "#9ca3af" : "#6b7280" }}>{language === "de" ? "Zeitplan" : "Schedule"}</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} />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* All-day events */}
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<span style={{ fontSize: "0.75rem", fontWeight: 600, color: darkMode ? "#9ca3af" : "#6b7280" }}>
|
||||
{language === "de" ? "Ganztägig" : "All-day"}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => { const v = !showAllDay; setShowAllDay(v); saveSetting("showAllDayEvents", v); }}
|
||||
style={{ background: "none", border: "none", cursor: "pointer", padding: "2px", color: darkMode ? "#9ca3af" : "#6b7280" }}
|
||||
>
|
||||
<span style={{ fontSize: "0.75rem", fontWeight: 600, color: darkMode ? "#9ca3af" : "#6b7280" }}>{language === "de" ? "Ganztägig" : "All-day"}</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} />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Checkboxes */}
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<span style={{ fontSize: "0.75rem", fontWeight: 600, color: darkMode ? "#9ca3af" : "#6b7280" }}>
|
||||
{language === "de" ? "Checkboxen" : "Checkboxes"}
|
||||
</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" }}
|
||||
>
|
||||
<span style={{ fontSize: "0.75rem", fontWeight: 600, color: darkMode ? "#9ca3af" : "#6b7280" }}>{language === "de" ? "Checkboxen" : "Checkboxes"}</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} />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Start on */}
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "6px" }}>
|
||||
<span style={{ fontSize: "0.75rem", fontWeight: 600, color: darkMode ? "#9ca3af" : "#6b7280" }}>
|
||||
{language === "de" ? "Starten mit" : "Start on"}
|
||||
</span>
|
||||
<span style={{ fontSize: "0.7rem", fontWeight: 600, color: darkMode ? "#9ca3af" : "#6b7280" }}>{language === "de" ? "Starten mit" : "Start on"}</span>
|
||||
<div style={{ display: "flex", gap: "4px" }}>
|
||||
<button
|
||||
onClick={() => {
|
||||
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"),
|
||||
}}
|
||||
>
|
||||
<button onClick={() => { 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"}
|
||||
</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);
|
||||
}}
|
||||
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"),
|
||||
}}
|
||||
>
|
||||
<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); }}
|
||||
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"}
|
||||
</button>
|
||||
</div>
|
||||
@ -5638,49 +5652,36 @@ export default function WeeklyView() {
|
||||
|
||||
{/* Display mode */}
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "6px" }}>
|
||||
<span style={{ fontSize: "0.75rem", fontWeight: 600, color: darkMode ? "#9ca3af" : "#6b7280" }}>
|
||||
{language === "de" ? "Anzeige" : "Display"}
|
||||
</span>
|
||||
<span style={{ fontSize: "0.7rem", fontWeight: 600, color: darkMode ? "#9ca3af" : "#6b7280" }}>{language === "de" ? "Anzeige" : "Display"}</span>
|
||||
<div style={{ display: "flex", gap: "4px" }}>
|
||||
<button
|
||||
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 ? (darkMode ? "#374151" : "#333") : (darkMode ? "#1f2937" : "#e5e7eb"),
|
||||
color: !darkMode ? "#fff" : (darkMode ? "#9ca3af" : "#6b7280"),
|
||||
}}
|
||||
>
|
||||
<button 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") }}>
|
||||
<Sun size={12} /> {language === "de" ? "Hell" : "Light"}
|
||||
</button>
|
||||
<button
|
||||
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",
|
||||
}}
|
||||
>
|
||||
<button 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" }}>
|
||||
<Moon size={12} /> {language === "de" ? "Dunkel" : "Dark"}
|
||||
</button>
|
||||
</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 */}
|
||||
<div style={{ flex: 1 }} />
|
||||
|
||||
{/* Close button at bottom */}
|
||||
<button
|
||||
onClick={() => setShowQuickSettings(false)}
|
||||
style={{
|
||||
display: "flex", alignItems: "center", gap: "6px",
|
||||
background: "none", border: "none", cursor: "pointer",
|
||||
fontSize: "0.75rem", color: darkMode ? "#6b7280" : "#9ca3af",
|
||||
padding: "4px 0",
|
||||
}}
|
||||
style={{ 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} />
|
||||
{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) */}
|
||||
{!showQuickSettings && (
|
||||
<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={() => { 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={() => { 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={{ padding: "6px 12px", display: "flex", alignItems: "center", gap: "6px", flexWrap: "wrap" }}>
|
||||
<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 */}
|
||||
<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 */}
|
||||
<UserMenu
|
||||
@ -8315,7 +8327,7 @@ export default function WeeklyView() {
|
||||
const now = new Date();
|
||||
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),
|
||||
onToggleNextTask: () => { const newVal = !showNextTask; setShowNextTask(newVal); saveSetting("showNextTask", newVal); },
|
||||
onFocusMode: () => setShowFocusMode(true),
|
||||
@ -8401,7 +8413,7 @@ export default function WeeklyView() {
|
||||
<div className="mobile-fab-menu-icon" style={{ background: "#10b981" }}><Search size={18} /></div>
|
||||
<span>{language === "de" ? "Suche" : "Search"}</span>
|
||||
</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>
|
||||
<span>{language === "de" ? "Projekt" : "Project"}</span>
|
||||
</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
|
||||
interface SettingsSidebarProps {
|
||||
onClose: () => void;
|
||||
@ -9639,7 +9817,7 @@ interface SettingsSidebarProps {
|
||||
fetchAvailableTaskLists: (
|
||||
provider: "google" | "apple" | "outlook" | "synology",
|
||||
) => 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 }[];
|
||||
onProjectsChanged: () => void;
|
||||
kanbanStages: KanbanStage[];
|
||||
@ -9905,7 +10083,7 @@ function SettingsSidebar({
|
||||
mobileActions,
|
||||
}: SettingsSidebarProps) {
|
||||
const [activeTab, setActiveTab] = useState<
|
||||
"calendar" | "general" | "account" | "styling" | "motivation" | "about" | "localisation" | "projects"
|
||||
"calendar" | "general" | "account" | "styling" | "motivation" | "about" | "localisation"
|
||||
>(initialTab || "general");
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isSyncing, setIsSyncing] = useState(false);
|
||||
@ -10375,193 +10553,6 @@ function SettingsSidebar({
|
||||
</button>
|
||||
</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
|
||||
className="weekly-settings-tabs"
|
||||
style={{
|
||||
@ -10575,7 +10566,6 @@ function SettingsSidebar({
|
||||
>
|
||||
{([
|
||||
{ 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: "calendar", icon: <Link size={18} />, label: t.calendar },
|
||||
{ key: "account", icon: <User size={18} />, label: t.account },
|
||||
@ -11526,125 +11516,6 @@ function SettingsSidebar({
|
||||
|
||||
<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" ? (
|
||||
isLoading ? (
|
||||
<p>Loading connections...</p>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user