merge: dev into main for v1.102.0 release
This commit is contained in:
commit
6847e59701
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "my-weekly-todo-list",
|
||||
"version": "1.101.1",
|
||||
"version": "1.102.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": {
|
||||
|
||||
@ -1250,6 +1250,8 @@ export default function WeeklyView() {
|
||||
const datePickerBtnRef = useRef<HTMLDivElement>(null);
|
||||
const [showQuickSettings, setShowQuickSettings] = useState(false);
|
||||
const [showProjectsSidebar, setShowProjectsSidebar] = useState(false);
|
||||
const [activeProjectFilter, setActiveProjectFilter] = useState<string | null>(null);
|
||||
const [showIntroHints, setShowIntroHints] = useState(true);
|
||||
|
||||
const [focusTimerDuration, setFocusTimerDuration] = useState(25);
|
||||
const [fontSize, setFontSize] = useState<"S" | "M" | "L">(() => {
|
||||
@ -2192,6 +2194,29 @@ export default function WeeklyView() {
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
// Intro hint: briefly reveal hidden header controls and bounce-scroll the main grid on first load
|
||||
useEffect(() => {
|
||||
const headerTimer = setTimeout(() => setShowIntroHints(false), 2200);
|
||||
|
||||
const scrollTimer = setTimeout(() => {
|
||||
const grid = gridRef.current;
|
||||
if (!grid || grid.scrollHeight <= grid.clientHeight + 20) return;
|
||||
const baseTop = grid.scrollTop;
|
||||
const peek = Math.min(80, grid.scrollHeight - grid.clientHeight - baseTop);
|
||||
if (peek <= 10) return;
|
||||
const prevBehavior = grid.style.scrollBehavior;
|
||||
grid.style.scrollBehavior = "smooth";
|
||||
grid.scrollTo({ top: baseTop + peek });
|
||||
const back = setTimeout(() => {
|
||||
grid.scrollTo({ top: baseTop });
|
||||
setTimeout(() => { grid.style.scrollBehavior = prevBehavior; }, 600);
|
||||
}, 700);
|
||||
return () => { clearTimeout(back); grid.style.scrollBehavior = prevBehavior; };
|
||||
}, 900);
|
||||
|
||||
return () => { clearTimeout(headerTimer); clearTimeout(scrollTimer); };
|
||||
}, []);
|
||||
|
||||
// Mobile: show a sticky day bar by reading scroll position on the actual grid scroll container
|
||||
useEffect(() => {
|
||||
if (!isMobile || !profile.showTimeGrid) return;
|
||||
@ -2775,6 +2800,7 @@ export default function WeeklyView() {
|
||||
if (task.parentTaskId) return false;
|
||||
// Hide completed tasks if setting is off
|
||||
if (!effectiveShowCompletedTasks && task.completed) return false;
|
||||
if (activeProjectFilter && task.projectId !== activeProjectFilter) return false;
|
||||
// Use string comparison to avoid timezone shifts
|
||||
const taskDateStr =
|
||||
typeof task.scheduledDate === "string"
|
||||
@ -2792,7 +2818,7 @@ export default function WeeklyView() {
|
||||
return a.order - b.order;
|
||||
});
|
||||
},
|
||||
[tasks, effectiveShowCompletedTasks],
|
||||
[tasks, effectiveShowCompletedTasks, activeProjectFilter],
|
||||
);
|
||||
|
||||
// Get tasks for a specific time slot
|
||||
@ -2805,6 +2831,7 @@ export default function WeeklyView() {
|
||||
if (task.parentTaskId) return false;
|
||||
// Hide completed tasks if setting is off
|
||||
if (!effectiveShowCompletedTasks && task.completed) return false;
|
||||
if (activeProjectFilter && task.projectId !== activeProjectFilter) return false;
|
||||
// Use string comparison to avoid timezone shifts
|
||||
const taskDateStr =
|
||||
typeof task.scheduledDate === "string"
|
||||
@ -2823,7 +2850,7 @@ export default function WeeklyView() {
|
||||
return taskStart >= slotStart && taskStart < slotEnd;
|
||||
});
|
||||
},
|
||||
[tasks, effectiveCellDuration, effectiveShowCompletedTasks],
|
||||
[tasks, effectiveCellDuration, effectiveShowCompletedTasks, activeProjectFilter],
|
||||
);
|
||||
|
||||
// Get calendar events for a specific date
|
||||
@ -5767,6 +5794,12 @@ export default function WeeklyView() {
|
||||
{effectiveShowProjectIcons ? <Eye size={16} /> : <EyeOff size={16} />}
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<span style={{ fontSize: "0.75rem", fontWeight: 600, color: darkMode ? "#9ca3af" : "#6b7280" }}>{profile.language === "de" ? "Prioritäts-Icons" : "Priority Icons"}</span>
|
||||
<button onClick={() => saveViewSetting("showPriorityIcons", !effectiveShowPriorityIcons, true)} style={{ background: "none", border: "none", cursor: "pointer", padding: "2px", color: darkMode ? "#9ca3af" : "#6b7280" }}>
|
||||
{effectiveShowPriorityIcons ? <Eye size={16} /> : <EyeOff size={16} />}
|
||||
</button>
|
||||
</div>
|
||||
{(profile.weatherEnabled || profile.weatherLat || profile.weatherLon) && (
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<span style={{ fontSize: "0.75rem", fontWeight: 600, color: darkMode ? "#9ca3af" : "#6b7280" }}>{profile.language === "de" ? "Wetter" : "Weather"}</span>
|
||||
@ -5839,6 +5872,9 @@ export default function WeeklyView() {
|
||||
projects={projects}
|
||||
onProjectsChanged={fetchProjects}
|
||||
onClose={() => setShowProjectsSidebar(false)}
|
||||
tasks={[...tasks, ...somedayLists.flatMap(l => l.tasks)]}
|
||||
activeProjectFilter={activeProjectFilter}
|
||||
onSetProjectFilter={setActiveProjectFilter}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -5930,7 +5966,7 @@ export default function WeeklyView() {
|
||||
{/* Desktop Header: Left, Center, Right */}
|
||||
<header className="group relative flex items-center justify-between w-full px-4 py-2 border-b border-gray-200 bg-white dark:bg-gray-900 dark:border-gray-700 dark:text-white transition-colors duration-200" style={isMobile ? { display: "none" } : {}}>
|
||||
{/* LEFT SECTION: View Switcher, Days, Hours, Slot Duration */}
|
||||
<div className="weekly-header-controls flex items-center gap-4 transition-opacity duration-300 opacity-0 group-hover:opacity-100" style={{ zIndex: 1 }}>
|
||||
<div className="weekly-header-controls flex items-center gap-4 transition-opacity duration-700 opacity-0 group-hover:opacity-100" style={{ zIndex: 1, ...(showIntroHints ? { opacity: 1 } : {}) }}>
|
||||
{/* Quick Settings toggle */}
|
||||
{!showQuickSettings && (
|
||||
<button
|
||||
@ -6188,7 +6224,7 @@ export default function WeeklyView() {
|
||||
</div>
|
||||
|
||||
{/* RIGHT SECTION: Navigation & Tools */}
|
||||
<div className="weekly-header-controls flex-shrink-0 flex items-center gap-1.5 sm:gap-2 transition-opacity duration-300 opacity-0 group-hover:opacity-100" style={{ zIndex: 10 }}>
|
||||
<div className="weekly-header-controls flex-shrink-0 flex items-center gap-1.5 sm:gap-2 transition-opacity duration-700 opacity-0 group-hover:opacity-100" style={{ zIndex: 10, ...(showIntroHints ? { opacity: 1 } : {}) }}>
|
||||
{/* Secondary actions — hidden on tablet, visible on desktop */}
|
||||
<div className="header-desktop-only flex items-center gap-1.5">
|
||||
<button onClick={handleUndo} disabled={undoCount === 0} className="weekly-btn-icon disabled:opacity-30 disabled:cursor-default" title="Undo (Ctrl+Z)">
|
||||
@ -6299,7 +6335,7 @@ export default function WeeklyView() {
|
||||
style={{ height: `${priorityViewHeight}px`, overflowY: "auto", flexShrink: 0 }}
|
||||
>
|
||||
<PriorityView
|
||||
tasks={[...tasks, ...somedayLists.flatMap(l => l.tasks)]}
|
||||
tasks={[...tasks, ...somedayLists.flatMap(l => l.tasks)].filter(t => !activeProjectFilter || t.projectId === activeProjectFilter)}
|
||||
somedayLists={somedayLists.map(l => ({ id: l.id, title: l.title }))}
|
||||
projects={projects}
|
||||
darkMode={darkMode}
|
||||
@ -6351,6 +6387,7 @@ export default function WeeklyView() {
|
||||
if (!effectiveShowCompletedTasks && t.completed) return false;
|
||||
if (kanbanSearch && !t.title.toLowerCase().includes(kanbanSearch.toLowerCase())) return false;
|
||||
if (kanbanFilterProject && t.projectId !== kanbanFilterProject) return false;
|
||||
if (activeProjectFilter && t.projectId !== activeProjectFilter) return false;
|
||||
if (kanbanFilterList) {
|
||||
if (kanbanFilterList === "__none__") {
|
||||
if (t.somedayListId) return false;
|
||||
@ -10911,12 +10948,15 @@ function ProjectIcon({ icon, size = 18, color }: { icon?: string | null; size?:
|
||||
return <span style={{ fontSize: size, lineHeight: 1 }}>{icon}</span>;
|
||||
}
|
||||
|
||||
function ProjectsSidebar({ darkMode, language, projects, onProjectsChanged, onClose }: {
|
||||
function ProjectsSidebar({ darkMode, language, projects, onProjectsChanged, onClose, tasks, activeProjectFilter, onSetProjectFilter }: {
|
||||
darkMode: boolean;
|
||||
language: string;
|
||||
projects: { id: string; name: string; icon?: string | null; color?: string | null }[];
|
||||
onProjectsChanged: () => void;
|
||||
onClose: () => void;
|
||||
tasks: Task[];
|
||||
activeProjectFilter: string | null;
|
||||
onSetProjectFilter: (id: string | null) => void;
|
||||
}) {
|
||||
const [newProjectName, setNewProjectName] = useState("");
|
||||
const [newProjectColor, setNewProjectColor] = useState("#3b82f6");
|
||||
@ -10927,6 +10967,7 @@ function ProjectsSidebar({ darkMode, language, projects, onProjectsChanged, onCl
|
||||
const [editColor, setEditColor] = useState("");
|
||||
const [editIcon, setEditIcon] = useState("folder");
|
||||
const [showEditIconPicker, setShowEditIconPicker] = useState(false);
|
||||
const [expandedProjects, setExpandedProjects] = useState<Set<string>>(new Set());
|
||||
|
||||
const t = {
|
||||
projects: language === "de" ? "Projekte" : "Projects",
|
||||
@ -10938,6 +10979,32 @@ function ProjectsSidebar({ darkMode, language, projects, onProjectsChanged, onCl
|
||||
save: language === "de" ? "Speichern" : "Save",
|
||||
color: language === "de" ? "Farbe" : "Color",
|
||||
deleteConfirm: (name: string) => language === "de" ? `Projekt "${name}" löschen?` : `Delete project "${name}"?`,
|
||||
filterOnly: language === "de" ? "Nur dieses Projekt anzeigen" : "Show only this project",
|
||||
clearFilter: language === "de" ? "Filter aufheben" : "Clear filter",
|
||||
showTasks: language === "de" ? "Aufgaben einblenden" : "Show tasks",
|
||||
hideTasks: language === "de" ? "Aufgaben ausblenden" : "Hide tasks",
|
||||
noTasks: language === "de" ? "Keine Aufgaben" : "No tasks",
|
||||
filteringBy: language === "de" ? "Filter aktiv:" : "Filtering by:",
|
||||
};
|
||||
|
||||
const tasksByProject = useMemo(() => {
|
||||
const map = new Map<string, Task[]>();
|
||||
tasks.forEach((task) => {
|
||||
if (!task.projectId || task.parentTaskId) return;
|
||||
const arr = map.get(task.projectId) || [];
|
||||
arr.push(task);
|
||||
map.set(task.projectId, arr);
|
||||
});
|
||||
return map;
|
||||
}, [tasks]);
|
||||
|
||||
const toggleExpanded = (id: string) => {
|
||||
setExpandedProjects((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(id)) next.delete(id);
|
||||
else next.add(id);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const createProject = () => {
|
||||
@ -10977,6 +11044,41 @@ function ProjectsSidebar({ darkMode, language, projects, onProjectsChanged, onCl
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Add project form — at top */}
|
||||
<div style={{ padding: "16px 20px", borderBottom: `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", display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||||
<ProjectIcon icon={newProjectIcon} size={18} color={darkMode ? "#d1d5db" : "#555"} />
|
||||
</button>
|
||||
{showNewIconPicker && (
|
||||
<div style={{ position: "absolute", top: "100%", left: 0, marginTop: "4px", zIndex: 50 }}>
|
||||
<IconPicker selectedIcon={newProjectIcon} onSelect={(name) => { setNewProjectIcon(name); setShowNewIconPicker(false); }} darkMode={darkMode} />
|
||||
</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>
|
||||
|
||||
{/* Active filter banner */}
|
||||
{activeProjectFilter && (
|
||||
<div style={{ padding: "8px 20px", background: darkMode ? "#1e3a5f" : "#dbeafe", display: "flex", alignItems: "center", gap: "8px", fontSize: "0.8rem", color: darkMode ? "#bfdbfe" : "#1e40af" }}>
|
||||
<Filter size={14} />
|
||||
<span style={{ flex: 1 }}>
|
||||
{t.filteringBy} {projects.find((p) => p.id === activeProjectFilter)?.name || ""}
|
||||
</span>
|
||||
<button onClick={() => onSetProjectFilter(null)} style={{ background: "none", border: "none", cursor: "pointer", color: darkMode ? "#bfdbfe" : "#1e40af", padding: "2px 6px", fontSize: "0.75rem", textDecoration: "underline" }}>
|
||||
{t.clearFilter}
|
||||
</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>
|
||||
@ -10987,8 +11089,13 @@ function ProjectsSidebar({ darkMode, language, projects, onProjectsChanged, onCl
|
||||
<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"}` }}>
|
||||
projects.map((p) => {
|
||||
const projectTasks = tasksByProject.get(p.id) || [];
|
||||
const isExpanded = expandedProjects.has(p.id);
|
||||
const isFiltered = activeProjectFilter === p.id;
|
||||
return (
|
||||
<div key={p.id} style={{ display: "flex", flexDirection: "column", borderRadius: "12px", background: darkMode ? "#2a2a3a" : "#f9fafb", borderLeft: `4px solid ${p.color || "#999"}`, outline: isFiltered ? `2px solid ${p.color || "#3b82f6"}` : "none" }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "10px", padding: "10px 14px" }}>
|
||||
{editingId === p.id ? (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "10px", width: "100%" }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||
@ -11014,10 +11121,17 @@ function ProjectsSidebar({ darkMode, language, projects, onProjectsChanged, onCl
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<button onClick={() => toggleExpanded(p.id)} style={{ background: "none", border: "none", cursor: "pointer", padding: "2px", color: darkMode ? "#9ca3af" : "#6b7280", display: "flex", alignItems: "center" }} title={isExpanded ? t.hideTasks : t.showTasks}>
|
||||
{isExpanded ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
|
||||
</button>
|
||||
<ProjectIcon icon={p.icon} size={20} color={p.color || "#999"} />
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<span style={{ fontSize: "0.9rem", fontWeight: 600, display: "block", color: darkMode ? "#f0f0f0" : "#333" }}>{p.name}</span>
|
||||
<span style={{ fontSize: "0.7rem", color: darkMode ? "#9ca3af" : "#888" }}>{projectTasks.length}</span>
|
||||
</div>
|
||||
<button onClick={() => onSetProjectFilter(isFiltered ? null : p.id)} style={{ padding: "6px", opacity: isFiltered ? 1 : 0.5, cursor: "pointer", background: isFiltered ? (darkMode ? "#3b3b5f" : "#e0e7ff") : "none", border: "none", borderRadius: "6px", color: isFiltered ? (p.color || "#3b82f6") : (darkMode ? "#ccc" : "#333") }} title={isFiltered ? t.clearFilter : t.filterOnly}>
|
||||
<Filter size={15} />
|
||||
</button>
|
||||
<button onClick={() => { setEditingId(p.id); setEditName(p.name); setEditColor(p.color || "#999"); setEditIcon(p.icon || "folder"); 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>
|
||||
@ -11027,30 +11141,24 @@ function ProjectsSidebar({ darkMode, language, projects, onProjectsChanged, onCl
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{isExpanded && editingId !== p.id && (
|
||||
<div style={{ padding: "0 14px 12px 38px", display: "flex", flexDirection: "column", gap: "4px" }}>
|
||||
{projectTasks.length === 0 ? (
|
||||
<span style={{ fontSize: "0.8rem", color: darkMode ? "#6b7280" : "#9ca3af", fontStyle: "italic" }}>{t.noTasks}</span>
|
||||
) : (
|
||||
projectTasks.map((task) => (
|
||||
<div key={task.id} style={{ fontSize: "0.82rem", color: darkMode ? "#d1d5db" : "#374151", padding: "3px 6px", borderRadius: "4px", textDecoration: task.completed ? "line-through" : "none", opacity: task.completed ? 0.6 : 1, display: "flex", alignItems: "center", gap: "6px" }}>
|
||||
<span style={{ width: "6px", height: "6px", borderRadius: "50%", background: p.color || "#999", flexShrink: 0 }} />
|
||||
<span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{task.title}</span>
|
||||
</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", display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||||
<ProjectIcon icon={newProjectIcon} size={18} color={darkMode ? "#d1d5db" : "#555"} />
|
||||
</button>
|
||||
{showNewIconPicker && (
|
||||
<div style={{ position: "absolute", bottom: "100%", left: 0, marginBottom: "4px", zIndex: 50 }}>
|
||||
<IconPicker selectedIcon={newProjectIcon} onSelect={(name) => { setNewProjectIcon(name); setShowNewIconPicker(false); }} darkMode={darkMode} />
|
||||
</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>
|
||||
</>
|
||||
|
||||
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user