From 3ee5bf33b5056a5fe58b0173be60ec84b08d6181 Mon Sep 17 00:00:00 2001 From: mARTin Date: Mon, 6 Apr 2026 20:48:03 +0200 Subject: [PATCH] feat: two-column priority layout + German ABCDE translations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All four priority modes (Eisenhower, ABCDE, Ivy Lee, Pareto) now use a two-column layout on desktop: main content on the left, secondary panel (unassigned tasks / pool / remaining) on the right. On mobile (<768px) the layout stacks vertically automatically. Eisenhower: filters are now always visible in the right panel instead of behind a toggle button, making project/list/timespan selection more accessible. The filter button is hidden for Eisenhower mode. ABCDE: added full German translations for grade labels and descriptions (A–E now show "Kritisch", "Sollte erledigt werden", etc. in German). Ungraded tasks appear in the right column instead of below the buckets. Pareto: remaining tasks (80%) are always visible in the right column, no longer behind a show/hide toggle. v1.90.0 --- package.json | 2 +- src/components/PriorityView.tsx | 752 +++++++++++++++++++------------- 2 files changed, 439 insertions(+), 315 deletions(-) diff --git a/package.json b/package.json index 0182eb5..468614b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "my-weekly-todo-list", - "version": "1.89.0", + "version": "1.90.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": { diff --git a/src/components/PriorityView.tsx b/src/components/PriorityView.tsx index 1ddbc5e..2098496 100644 --- a/src/components/PriorityView.tsx +++ b/src/components/PriorityView.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState, useMemo, useCallback } from "react"; +import React, { useState, useMemo, useCallback, useEffect } from "react"; import { AlertTriangle, Target, @@ -88,6 +88,14 @@ const ABCDE_LABELS_DARK: Record = { E: { bg: "#1f2937" }, }; +const ABCDE_LABELS_DE: Record = { + A: { label: "A — Kritisch", desc: "Muss erledigt werden. Ernste Konsequenzen." }, + B: { label: "B — Sollte erledigt werden", desc: "Leichte Konsequenzen wenn nicht erledigt." }, + C: { label: "C — Schön wenn erledigt", desc: "Keine Konsequenzen wenn nicht erledigt." }, + D: { label: "D — Delegieren", desc: "Kann von jemand anderem erledigt werden." }, + E: { label: "E — Eliminieren", desc: "Streichen — kein echter Mehrwert." }, +}; + function today(): string { return new Date().toISOString().split("T")[0]; } @@ -129,9 +137,17 @@ export default function PriorityView({ const [delegateType, setDelegateType] = useState<"person" | "ai">("person"); const [ivyLeeSelected, setIvyLeeSelected] = useState>(new Set()); const [showFilters, setShowFilters] = useState(false); + const [isMobile, setIsMobile] = useState(false); const de = language === "de"; + useEffect(() => { + const check = () => setIsMobile(window.innerWidth < 768); + check(); + window.addEventListener("resize", check); + return () => window.removeEventListener("resize", check); + }, []); + // --- Filter tasks --- const filteredTasks = useMemo(() => { let result = tasks.filter((t) => !t.id.startsWith("virtual-")); @@ -258,97 +274,108 @@ export default function PriorityView({ const accentColor = "#6366f1"; const methodButtons: { key: PriorityMethod; label: string; icon: React.ReactNode }[] = [ - { key: "eisenhower", label: de ? "Eisenhower" : "Eisenhower", icon: }, + { key: "eisenhower", label: "Eisenhower", icon: }, { key: "abcde", label: "ABCDE", icon: }, - { key: "ivylee", label: de ? "Ivy Lee" : "Ivy Lee", icon: }, - { key: "pareto", label: de ? "80/20" : "80/20", icon: }, + { key: "ivylee", label: "Ivy Lee", icon: }, + { key: "pareto", label: "80/20", icon: }, ]; + // Filter panel — rendered in Eisenhower's right column or above content in other modes + const filterPanel = ( +
+
+ + +
+
+ + +
+
+ + +
+
+ + {filteredTasks.length} {de ? "Aufgaben" : "tasks"} + +
+
+ ); + return ( -
+
{/* ─── Header ─── */} -
+
- - + + {de ? "Prioritäten" : "Priority View"}
-
+ {/* Filter toggle — only for non-Eisenhower modes (Eisenhower shows filters in its right panel) */} + {method !== "eisenhower" && ( -
+ )}
- {/* ─── Filters Panel ─── */} - {showFilters && ( -
-
- - -
-
- - -
-
- - -
-
- - {filteredTasks.length} {de ? "Aufgaben" : "tasks"} - -
+ {/* ─── Filters Panel (non-Eisenhower modes only) ─── */} + {method !== "eisenhower" && showFilters && ( +
+ {filterPanel}
)} {/* ─── Method Tabs ─── */} -
+
{methodButtons.map((m) => ( - ))} -
- - ))} -
- )} + // Ungraded tasks panel (right column) + const hasUngraded = (groups.none || []).length > 0; + const ungradedPanel = ( +
+
setCollapsed((p) => ({ ...p, none: !p.none }))} + > + + {hasUngraded + ? `${de ? "Nicht eingestuft" : "Ungraded"} (${(groups.none || []).length})` + : (de ? "Alle Aufgaben sind eingestuft ✓" : "All tasks graded ✓")} + + {hasUngraded && (collapsed.none ? : )} +
+ {!collapsed.none && hasUngraded && ( +
+ {(groups.none || []).map((t) => ( + +
+ {gradeList.map((g) => ( + + ))} +
+
+ ))}
)}
); + + if (isMobile) { + return ( +
+ {gradeBuckets} + {ungradedPanel} +
+ ); + } + + return ( +
+
{gradeBuckets}
+
{ungradedPanel}
+
+ ); } // ───────────────────────────────────────────── @@ -1024,9 +1106,10 @@ interface IvyLeeViewProps { border: string; textPrimary: string; textSecondary: string; + isMobile?: boolean; } -function IvyLeeView({ tasks, selected, darkMode, de, onToggle, onDelegate, onToggleComplete, onUpdateTask, projects, cardBg, border, textPrimary, textSecondary }: IvyLeeViewProps) { +function IvyLeeView({ tasks, selected, darkMode, de, onToggle, onDelegate, onToggleComplete, onUpdateTask, projects, cardBg, border, textPrimary, textSecondary, isMobile }: IvyLeeViewProps) { const accentColor = "#6366f1"; const selectedTasks = tasks.filter((t) => selected.has(t.id)); const unselected = tasks.filter((t) => !selected.has(t.id)); @@ -1041,96 +1124,116 @@ function IvyLeeView({ tasks, selected, darkMode, de, onToggle, onDelegate, onTog setDropOverTop6(false); }; - return ( -
- {/* Header info */} -
- -
-

- {de ? "Ivy Lee Methode" : "Ivy Lee Method"} -

-

- {de - ? "Wähle genau 6 wichtige Aufgaben für morgen, geordnet nach Priorität." - : "Select exactly 6 important tasks for tomorrow, ordered by priority."} -

-
+ // Info banner + const infoBanner = ( +
+ +
+

+ {de ? "Ivy Lee Methode" : "Ivy Lee Method"} +

+

+ {de + ? "Wähle genau 6 wichtige Aufgaben für morgen, geordnet nach Priorität." + : "Select exactly 6 important tasks for tomorrow, ordered by priority."} +

+
+ ); - {/* Selected tasks */} -
{ e.preventDefault(); e.dataTransfer.dropEffect = selected.size < 6 ? "move" : "none"; setDropOverTop6(true); }} - onDragLeave={(e) => { if (!e.currentTarget.contains(e.relatedTarget as Node)) setDropOverTop6(false); }} - onDrop={handleTop6Drop} - style={{ marginBottom: "16px", borderRadius: "10px", border: dropOverTop6 ? `2px dashed ${accentColor}` : "2px solid transparent", padding: "4px", transition: "border 0.15s" }} - > -
- - {dropOverTop6 ? (de ? "Hier ablegen" : "Drop to add") : (de ? "Meine Top-6" : "My Top 6")} - - - {selected.size}/6 - -
-
- {Array.from({ length: 6 }).map((_, i) => { - const t = selectedTasks[i]; - if (t) { - return ( -
- {i + 1} -
- onToggle(t.id)} de={de} /> -
-
- ); - } + // Top-6 selection panel (left column) + const top6Panel = ( +
{ e.preventDefault(); e.dataTransfer.dropEffect = selected.size < 6 ? "move" : "none"; setDropOverTop6(true); }} + onDragLeave={(e) => { if (!e.currentTarget.contains(e.relatedTarget as Node)) setDropOverTop6(false); }} + onDrop={handleTop6Drop} + style={{ borderRadius: "10px", border: dropOverTop6 ? `2px dashed ${accentColor}` : "2px solid transparent", padding: "4px", transition: "border 0.15s" }} + > +
+ + {dropOverTop6 ? (de ? "Hier ablegen" : "Drop to add") : (de ? "Meine Top-6" : "My Top 6")} + + + {selected.size}/6 + +
+
+ {Array.from({ length: 6 }).map((_, i) => { + const t = selectedTasks[i]; + if (t) { return ( -
- {i + 1} -
- {de ? "Leer — wähle eine Aufgabe unten" : "Empty — pick a task below"} +
+ {i + 1} +
+ onToggle(t.id)} de={de} />
); - })} -
-
- - {/* All other tasks to pick from */} - {unselected.length > 0 && ( -
-

- {de ? "Verfügbare Aufgaben:" : "Available tasks:"} -

-
- {unselected.map((t) => ( -
onToggle(t.id)} - style={{ - background: cardBg, - border: `1px solid ${border}`, - borderRadius: "8px", - padding: "8px 12px", - cursor: selected.size >= 6 ? "not-allowed" : "pointer", - opacity: selected.size >= 6 ? 0.5 : 1, - display: "flex", - alignItems: "center", - gap: "8px", - transition: "background 0.1s", - }} - > - - {t.title} - {t.scheduledDate && {formatDate(t.scheduledDate)}} - {t.project && {t.project.name}} + } + return ( +
+ {i + 1} +
+ {de ? "Leer — wähle eine Aufgabe" : "Empty — pick a task"}
- ))} +
+ ); + })} +
+
+ ); + + // Available tasks pool (right column) + const availablePool = ( +
+

+ {unselected.length > 0 ? (de ? "Verfügbare Aufgaben:" : "Available tasks:") : (de ? "Alle Aufgaben ausgewählt ✓" : "All tasks selected ✓")} +

+
+ {unselected.map((t) => ( +
onToggle(t.id)} + style={{ + background: cardBg, + border: `1px solid ${border}`, + borderRadius: "8px", + padding: "8px 12px", + cursor: selected.size >= 6 ? "not-allowed" : "pointer", + opacity: selected.size >= 6 ? 0.5 : 1, + display: "flex", + alignItems: "center", + gap: "8px", + transition: "background 0.1s", + }} + > + + {t.title} + {t.scheduledDate && {formatDate(t.scheduledDate)}} + {t.project && {t.project.name}}
-
- )} + ))} +
+
+ ); + + if (isMobile) { + return ( +
+ {infoBanner} + {top6Panel} + {unselected.length > 0 &&
{availablePool}
} +
+ ); + } + + return ( +
+ {infoBanner} +
+
{top6Panel}
+
{availablePool}
+
); } @@ -1151,17 +1254,17 @@ interface ParetoViewProps { border: string; textPrimary: string; textSecondary: string; + isMobile?: boolean; } -function ParetoView({ important, rest, darkMode, de, onDelegate, onToggleComplete, onUpdateTask, projects, cardBg, border, textPrimary, textSecondary }: ParetoViewProps) { - const [showRest, setShowRest] = useState(false); +function ParetoView({ important, rest, darkMode, de, onDelegate, onToggleComplete, onUpdateTask, projects, cardBg, border, textPrimary, textSecondary, isMobile }: ParetoViewProps) { const total = important.length + rest.length; const pct = total > 0 ? Math.round((important.length / total) * 100) : 0; - return ( -
- {/* Info banner */} -
+ // Info banner + progress bar (full-width header) + const header = ( + <> +

@@ -1169,66 +1272,87 @@ function ParetoView({ important, rest, darkMode, de, onDelegate, onToggleComplet

{de - ? `${important.length} von ${total} Aufgaben (${pct}%) sind als wichtig markiert. Fokussiere dich auf diese, um 80% der Ergebnisse zu erzielen.` - : `${important.length} of ${total} tasks (${pct}%) are marked as important. Focus on these to achieve 80% of your outcomes.`} + ? `${important.length} von ${total} Aufgaben (${pct}%) sind wichtig. Fokussiere dich auf diese für 80% der Ergebnisse.` + : `${important.length} of ${total} tasks (${pct}%) are marked as important. Focus on these to achieve 80% of outcomes.`}

- - {/* Progress bar */} -
+
+ + ); - {/* Important tasks */} -
-
- - {de ? "Wichtige Aufgaben (Fokus)" : "High-Impact Tasks (Focus)"} - - {important.length} -
+ // High-impact tasks (left column) + const importantPanel = ( +
+
+ + {de ? "Wichtige Aufgaben (Fokus)" : "High-Impact Tasks (Focus)"} + + {important.length} +
+
+ {important.length === 0 && ( +

+ {de + ? "Keine wichtigen Aufgaben. Markiere Aufgaben als wichtig im Eisenhower- oder ABCDE-Modus." + : "No important tasks. Mark tasks as important via Eisenhower or ABCDE mode."} +

+ )} + {important.map((t) => ( +
+ onUpdateTask(t.id, { importance: false })} de={de} /> +
+ ))} +
+
+ ); + + // Remaining tasks (right column — always visible, no toggle) + const remainingPanel = ( +
+
+ + {de ? "Restliche Aufgaben" : "Remaining tasks"} + + {rest.length} +
+ {rest.length === 0 ? ( +

{de ? "Keine weiteren Aufgaben." : "No remaining tasks."}

+ ) : (
- {important.length === 0 && ( -

- {de - ? "Keine wichtigen Aufgaben. Markiere Aufgaben als wichtig im Eisenhower- oder ABCDE-Modus." - : "No important tasks. Mark tasks as important via Eisenhower or ABCDE mode."} -

- )} - {important.map((t) => ( -
- onUpdateTask(t.id, { importance: false })} de={de} /> -
+ {rest.map((t) => ( + + + ))}
-
+ )} +
+ ); - {/* The rest (80%) */} + if (isMobile) { + return (
- - {showRest && ( -
- {rest.map((t) => ( - - - - ))} -
- )} + {header} + {importantPanel} +
{remainingPanel}
+
+ ); + } + + return ( +
+ {header} +
+
{importantPanel}
+
{remainingPanel}
);