feat: two-column priority layout + German ABCDE translations
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
This commit is contained in:
parent
0a84bf3114
commit
3ee5bf33b5
@ -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": {
|
||||
|
||||
@ -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<string, { bg: string }> = {
|
||||
E: { bg: "#1f2937" },
|
||||
};
|
||||
|
||||
const ABCDE_LABELS_DE: Record<string, { label: string; desc: string }> = {
|
||||
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<Set<string>>(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,45 +274,24 @@ export default function PriorityView({
|
||||
const accentColor = "#6366f1";
|
||||
|
||||
const methodButtons: { key: PriorityMethod; label: string; icon: React.ReactNode }[] = [
|
||||
{ key: "eisenhower", label: de ? "Eisenhower" : "Eisenhower", icon: <Grid size={14} /> },
|
||||
{ key: "eisenhower", label: "Eisenhower", icon: <Grid size={14} /> },
|
||||
{ key: "abcde", label: "ABCDE", icon: <Tag size={14} /> },
|
||||
{ key: "ivylee", label: de ? "Ivy Lee" : "Ivy Lee", icon: <ListOrdered size={14} /> },
|
||||
{ key: "pareto", label: de ? "80/20" : "80/20", icon: <BarChart2 size={14} /> },
|
||||
{ key: "ivylee", label: "Ivy Lee", icon: <ListOrdered size={14} /> },
|
||||
{ key: "pareto", label: "80/20", icon: <BarChart2 size={14} /> },
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ background: bg, minHeight: "100%", padding: "16px 12px", boxSizing: "border-box", fontFamily: "inherit" }}>
|
||||
{/* ─── Header ─── */}
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "16px", flexWrap: "wrap", gap: "8px" }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||
<Target size={20} style={{ color: accentColor }} />
|
||||
<span style={{ fontSize: "1.1rem", fontWeight: 700, color: textPrimary }}>
|
||||
{de ? "Prioritäten" : "Priority View"}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: "6px", alignItems: "center" }}>
|
||||
<button
|
||||
onClick={() => setShowFilters((v) => !v)}
|
||||
style={{ display: "flex", alignItems: "center", gap: "4px", padding: "5px 10px", borderRadius: "7px", border: `1px solid ${border}`, background: showFilters ? accentColor : cardBg, color: showFilters ? "#fff" : textSecondary, cursor: "pointer", fontSize: "0.8rem" }}
|
||||
>
|
||||
<SlidersHorizontal size={13} />
|
||||
{de ? "Filter" : "Filter"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ─── Filters Panel ─── */}
|
||||
{showFilters && (
|
||||
<div style={{ background: cardBg, border: `1px solid ${border}`, borderRadius: "10px", padding: "12px", marginBottom: "16px", display: "flex", flexWrap: "wrap", gap: "10px" }}>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "4px", minWidth: "140px" }}>
|
||||
<label style={{ fontSize: "0.7rem", fontWeight: 600, color: textSecondary, textTransform: "uppercase", letterSpacing: "0.05em" }}>
|
||||
<FolderOpen size={10} style={{ display: "inline", marginRight: "3px" }} />
|
||||
// Filter panel — rendered in Eisenhower's right column or above content in other modes
|
||||
const filterPanel = (
|
||||
<div style={{ background: cardBg, border: `1px solid ${border}`, borderRadius: "10px", padding: "10px", display: "flex", flexWrap: "wrap", gap: "8px" }}>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "3px", flex: "1 1 120px" }}>
|
||||
<label style={{ fontSize: "0.68rem", fontWeight: 600, color: textSecondary, textTransform: "uppercase", letterSpacing: "0.05em" }}>
|
||||
<FolderOpen size={9} style={{ display: "inline", marginRight: "3px" }} />
|
||||
{de ? "Projekt" : "Project"}
|
||||
</label>
|
||||
<select
|
||||
value={filterProject}
|
||||
onChange={(e) => setFilterProject(e.target.value)}
|
||||
style={{ padding: "5px 8px", borderRadius: "6px", border: `1px solid ${border}`, background: darkMode ? "#374151" : "#f3f4f6", color: textPrimary, fontSize: "0.82rem", cursor: "pointer" }}
|
||||
style={{ padding: "4px 7px", borderRadius: "6px", border: `1px solid ${border}`, background: darkMode ? "#374151" : "#f3f4f6", color: textPrimary, fontSize: "0.8rem", cursor: "pointer" }}
|
||||
>
|
||||
<option value="">{de ? "Alle Projekte" : "All projects"}</option>
|
||||
{projects.map((p) => (
|
||||
@ -304,33 +299,33 @@ export default function PriorityView({
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "4px", minWidth: "160px" }}>
|
||||
<label style={{ fontSize: "0.7rem", fontWeight: 600, color: textSecondary, textTransform: "uppercase", letterSpacing: "0.05em" }}>
|
||||
<List size={10} style={{ display: "inline", marginRight: "3px" }} />
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "3px", flex: "1 1 130px" }}>
|
||||
<label style={{ fontSize: "0.68rem", fontWeight: 600, color: textSecondary, textTransform: "uppercase", letterSpacing: "0.05em" }}>
|
||||
<List size={9} style={{ display: "inline", marginRight: "3px" }} />
|
||||
{de ? "Liste" : "List"}
|
||||
</label>
|
||||
<select
|
||||
value={filterList}
|
||||
onChange={(e) => setFilterList(e.target.value)}
|
||||
style={{ padding: "5px 8px", borderRadius: "6px", border: `1px solid ${border}`, background: darkMode ? "#374151" : "#f3f4f6", color: textPrimary, fontSize: "0.82rem", cursor: "pointer" }}
|
||||
style={{ padding: "4px 7px", borderRadius: "6px", border: `1px solid ${border}`, background: darkMode ? "#374151" : "#f3f4f6", color: textPrimary, fontSize: "0.8rem", cursor: "pointer" }}
|
||||
>
|
||||
<option value="">{de ? "Alle Listen" : "All lists"}</option>
|
||||
<option value="__scheduled__">{de ? "Geplante Aufgaben" : "Scheduled tasks"}</option>
|
||||
<option value="__scheduled__">{de ? "Geplante Aufgaben" : "Scheduled"}</option>
|
||||
<option value="__unscheduled__">{de ? "Ungeplant" : "Unscheduled"}</option>
|
||||
{somedayLists.map((l) => (
|
||||
<option key={l.id} value={l.id}>{l.title}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "4px", minWidth: "140px" }}>
|
||||
<label style={{ fontSize: "0.7rem", fontWeight: 600, color: textSecondary, textTransform: "uppercase", letterSpacing: "0.05em" }}>
|
||||
<CalendarDays size={10} style={{ display: "inline", marginRight: "3px" }} />
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "3px", flex: "1 1 120px" }}>
|
||||
<label style={{ fontSize: "0.68rem", fontWeight: 600, color: textSecondary, textTransform: "uppercase", letterSpacing: "0.05em" }}>
|
||||
<CalendarDays size={9} style={{ display: "inline", marginRight: "3px" }} />
|
||||
{de ? "Zeitraum" : "Timespan"}
|
||||
</label>
|
||||
<select
|
||||
value={filterTimespan}
|
||||
onChange={(e) => setFilterTimespan(e.target.value)}
|
||||
style={{ padding: "5px 8px", borderRadius: "6px", border: `1px solid ${border}`, background: darkMode ? "#374151" : "#f3f4f6", color: textPrimary, fontSize: "0.82rem", cursor: "pointer" }}
|
||||
style={{ padding: "4px 7px", borderRadius: "6px", border: `1px solid ${border}`, background: darkMode ? "#374151" : "#f3f4f6", color: textPrimary, fontSize: "0.8rem", cursor: "pointer" }}
|
||||
>
|
||||
<option value="all">{de ? "Alle" : "All time"}</option>
|
||||
<option value="today">{de ? "Heute" : "Today"}</option>
|
||||
@ -339,16 +334,48 @@ export default function PriorityView({
|
||||
<option value="unscheduled">{de ? "Ungeplant" : "Unscheduled"}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style={{ display: "flex", alignItems: "flex-end" }}>
|
||||
<span style={{ fontSize: "0.78rem", color: textSecondary, paddingBottom: "6px" }}>
|
||||
<div style={{ display: "flex", alignItems: "flex-end", paddingBottom: "2px" }}>
|
||||
<span style={{ fontSize: "0.75rem", color: textSecondary }}>
|
||||
{filteredTasks.length} {de ? "Aufgaben" : "tasks"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ background: bg, minHeight: "100%", padding: "12px", boxSizing: "border-box", fontFamily: "inherit" }}>
|
||||
{/* ─── Header ─── */}
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "10px", flexWrap: "wrap", gap: "8px" }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||
<Target size={18} style={{ color: accentColor }} />
|
||||
<span style={{ fontSize: "1rem", fontWeight: 700, color: textPrimary }}>
|
||||
{de ? "Prioritäten" : "Priority View"}
|
||||
</span>
|
||||
</div>
|
||||
{/* Filter toggle — only for non-Eisenhower modes (Eisenhower shows filters in its right panel) */}
|
||||
{method !== "eisenhower" && (
|
||||
<button
|
||||
onClick={() => setShowFilters((v) => !v)}
|
||||
style={{ display: "flex", alignItems: "center", gap: "4px", padding: "5px 10px", borderRadius: "7px", border: `1px solid ${border}`, background: showFilters ? accentColor : cardBg, color: showFilters ? "#fff" : textSecondary, cursor: "pointer", fontSize: "0.8rem" }}
|
||||
>
|
||||
<SlidersHorizontal size={13} />
|
||||
{de ? "Filter" : "Filter"}
|
||||
{(filterProject || filterList || filterTimespan !== "all") && (
|
||||
<span style={{ background: "#ef4444", color: "#fff", borderRadius: "99px", fontSize: "0.65rem", padding: "0 5px", fontWeight: 700 }}>!</span>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ─── Filters Panel (non-Eisenhower modes only) ─── */}
|
||||
{method !== "eisenhower" && showFilters && (
|
||||
<div style={{ marginBottom: "12px" }}>
|
||||
{filterPanel}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ─── Method Tabs ─── */}
|
||||
<div style={{ display: "flex", gap: "4px", marginBottom: "20px", background: darkMode ? "#1f2937" : "#e5e7eb", borderRadius: "9px", padding: "3px" }}>
|
||||
<div style={{ display: "flex", gap: "4px", marginBottom: "14px", background: darkMode ? "#1f2937" : "#e5e7eb", borderRadius: "9px", padding: "3px" }}>
|
||||
{methodButtons.map((m) => (
|
||||
<button
|
||||
key={m.key}
|
||||
@ -398,6 +425,8 @@ export default function PriorityView({
|
||||
border={border}
|
||||
textPrimary={textPrimary}
|
||||
textSecondary={textSecondary}
|
||||
isMobile={isMobile}
|
||||
filterPanel={filterPanel}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -417,6 +446,7 @@ export default function PriorityView({
|
||||
border={border}
|
||||
textPrimary={textPrimary}
|
||||
textSecondary={textSecondary}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -436,6 +466,7 @@ export default function PriorityView({
|
||||
border={border}
|
||||
textPrimary={textPrimary}
|
||||
textSecondary={textSecondary}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -454,6 +485,7 @@ export default function PriorityView({
|
||||
border={border}
|
||||
textPrimary={textPrimary}
|
||||
textSecondary={textSecondary}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -760,6 +792,8 @@ interface EisenhowerProps {
|
||||
border: string;
|
||||
textPrimary: string;
|
||||
textSecondary: string;
|
||||
isMobile?: boolean;
|
||||
filterPanel?: React.ReactNode;
|
||||
}
|
||||
|
||||
const QUADRANT_CONFIG = [
|
||||
@ -769,7 +803,7 @@ const QUADRANT_CONFIG = [
|
||||
{ urgency: false, importance: false, label: "Delete", labelDe: "Eliminieren", desc: "Neither", descDe: "Weder noch", color: "#6b7280", bgLight: "#f9fafb", bgDark: "#1f2937", icon: <Trash2 size={14} /> },
|
||||
];
|
||||
|
||||
function EisenhowerMatrix({ q1, q2, q3, q4, unset, allTasks, darkMode, de, onSetQuadrant, onClear, onDelegate, onToggleComplete, onUpdateTask, projects, cardBg, border, textPrimary, textSecondary }: EisenhowerProps) {
|
||||
function EisenhowerMatrix({ q1, q2, q3, q4, unset, allTasks, darkMode, de, onSetQuadrant, onClear, onDelegate, onToggleComplete, onUpdateTask, projects, cardBg, border, textPrimary, textSecondary, isMobile, filterPanel }: EisenhowerProps) {
|
||||
const quadrantTasks = [q1, q2, q3, q4];
|
||||
const [collapsed, setCollapsed] = useState<Record<number, boolean>>({});
|
||||
const [dragOver, setDragOver] = useState<number | null>(null);
|
||||
@ -782,10 +816,9 @@ function EisenhowerMatrix({ q1, q2, q3, q4, unset, allTasks, darkMode, de, onSet
|
||||
setDragOver(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Axis labels */}
|
||||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "12px", marginBottom: "12px" }}>
|
||||
// 2×2 quadrant grid
|
||||
const quadrantGrid = (
|
||||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "10px" }}>
|
||||
{QUADRANT_CONFIG.map((qc, idx) => {
|
||||
const tasks = quadrantTasks[idx];
|
||||
const isCollapsed = collapsed[idx];
|
||||
@ -845,14 +878,23 @@ function EisenhowerMatrix({ q1, q2, q3, q4, unset, allTasks, darkMode, de, onSet
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
// Right panel: filters + unassigned tasks
|
||||
const rightPanel = (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "10px" }}>
|
||||
{/* Filters always visible in Eisenhower right column */}
|
||||
{filterPanel}
|
||||
|
||||
{/* Unassigned tasks */}
|
||||
{unset.length > 0 && (
|
||||
<div style={{ background: darkMode ? "#1f2937" : "#f3f4f6", borderRadius: "10px", border: `1px solid ${border}`, padding: "12px" }}>
|
||||
<p style={{ fontSize: "0.78rem", fontWeight: 600, color: textSecondary, marginBottom: "10px" }}>
|
||||
{de ? `${unset.length} nicht eingestufte Aufgaben — drag in ein Quadrant oder klicke:` : `${unset.length} unclassified tasks — drag into a quadrant or click to assign:`}
|
||||
<p style={{ fontSize: "0.78rem", fontWeight: 600, color: textSecondary, marginBottom: unset.length > 0 ? "10px" : 0, margin: unset.length > 0 ? undefined : 0 }}>
|
||||
{unset.length > 0
|
||||
? (de ? `${unset.length} nicht eingestufte Aufgaben — in Quadrant ziehen oder klicken:` : `${unset.length} unclassified — drag to a quadrant or click to assign:`)
|
||||
: (de ? "Alle Aufgaben sind eingestuft ✓" : "All tasks classified ✓")}
|
||||
</p>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
|
||||
{unset.length > 0 && (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "8px", maxHeight: "400px", overflowY: "auto" }}>
|
||||
{unset.map((t) => (
|
||||
<div key={t.id}>
|
||||
<TaskCard task={t} darkMode={darkMode} cardBg={cardBg} border={border} textPrimary={textPrimary} textSecondary={textSecondary} onDelegate={onDelegate} onToggleComplete={onToggleComplete} onUpdateTask={onUpdateTask} projects={projects} de={de}>
|
||||
@ -871,9 +913,27 @@ function EisenhowerMatrix({ q1, q2, q3, q4, unset, allTasks, darkMode, de, onSet
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Mobile: stack vertically
|
||||
if (isMobile) {
|
||||
return (
|
||||
<div>
|
||||
{quadrantGrid}
|
||||
<div style={{ marginTop: "12px" }}>{rightPanel}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Desktop: quadrants left (60%), filter+unassigned right (40%)
|
||||
return (
|
||||
<div style={{ display: "flex", gap: "14px", alignItems: "flex-start" }}>
|
||||
<div style={{ flex: 3, minWidth: 0 }}>{quadrantGrid}</div>
|
||||
<div style={{ flex: 2, minWidth: "220px", maxWidth: "400px" }}>{rightPanel}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -894,9 +954,10 @@ interface AbcdeViewProps {
|
||||
border: string;
|
||||
textPrimary: string;
|
||||
textSecondary: string;
|
||||
isMobile?: boolean;
|
||||
}
|
||||
|
||||
function AbcdeView({ groups, allTasks, darkMode, de, onSetGrade, onDelegate, onToggleComplete, onUpdateTask, projects, cardBg, border, textPrimary, textSecondary }: AbcdeViewProps) {
|
||||
function AbcdeView({ groups, allTasks, darkMode, de, onSetGrade, onDelegate, onToggleComplete, onUpdateTask, projects, cardBg, border, textPrimary, textSecondary, isMobile }: AbcdeViewProps) {
|
||||
const [collapsed, setCollapsed] = useState<Record<string, boolean>>({ none: true });
|
||||
const [dragOver, setDragOver] = useState<string | null>(null);
|
||||
|
||||
@ -910,10 +971,12 @@ function AbcdeView({ groups, allTasks, darkMode, de, onSetGrade, onDelegate, onT
|
||||
setDragOver(null);
|
||||
};
|
||||
|
||||
return (
|
||||
// A–E grade buckets
|
||||
const gradeBuckets = (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "10px" }}>
|
||||
{gradeList.map((grade) => {
|
||||
const cfg = ABCDE_LABELS[grade];
|
||||
const cfgDe = ABCDE_LABELS_DE[grade];
|
||||
const cfgDark = ABCDE_LABELS_DARK[grade];
|
||||
const tasks = groups[grade] || [];
|
||||
const isCollapsed = collapsed[grade];
|
||||
@ -940,8 +1003,8 @@ function AbcdeView({ groups, allTasks, darkMode, de, onSetGrade, onDelegate, onT
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||
<span style={{ fontWeight: 800, fontSize: "1.1rem", color: cfg.color, lineHeight: 1 }}>{grade}</span>
|
||||
<div>
|
||||
<div style={{ fontSize: "0.82rem", fontWeight: 700, color: cfg.color }}>{cfg.label}</div>
|
||||
<div style={{ fontSize: "0.68rem", color: textSecondary }}>{cfg.desc}</div>
|
||||
<div style={{ fontSize: "0.82rem", fontWeight: 700, color: cfg.color }}>{de ? cfgDe.label : cfg.label}</div>
|
||||
<div style={{ fontSize: "0.68rem", color: textSecondary }}>{de ? cfgDe.desc : cfg.desc}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "6px" }}>
|
||||
@ -969,21 +1032,26 @@ function AbcdeView({ groups, allTasks, darkMode, de, onSetGrade, onDelegate, onT
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
{/* Ungraded */}
|
||||
{(groups.none || []).length > 0 && (
|
||||
// Ungraded tasks panel (right column)
|
||||
const hasUngraded = (groups.none || []).length > 0;
|
||||
const ungradedPanel = (
|
||||
<div style={{ background: darkMode ? "#1f2937" : "#f3f4f6", borderRadius: "10px", border: `1px solid ${border}`, overflow: "hidden" }}>
|
||||
<div
|
||||
style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "9px 14px", cursor: "pointer", borderBottom: collapsed.none ? "none" : `1px solid ${border}` }}
|
||||
onClick={() => setCollapsed((p) => ({ ...p, none: !p.none }))}
|
||||
>
|
||||
<span style={{ fontSize: "0.82rem", fontWeight: 600, color: textSecondary }}>
|
||||
{de ? "Nicht eingestuft" : "Ungraded"} ({(groups.none || []).length})
|
||||
{hasUngraded
|
||||
? `${de ? "Nicht eingestuft" : "Ungraded"} (${(groups.none || []).length})`
|
||||
: (de ? "Alle Aufgaben sind eingestuft ✓" : "All tasks graded ✓")}
|
||||
</span>
|
||||
{collapsed.none ? <ChevronDown size={14} style={{ color: textSecondary }} /> : <ChevronUp size={14} style={{ color: textSecondary }} />}
|
||||
{hasUngraded && (collapsed.none ? <ChevronDown size={14} style={{ color: textSecondary }} /> : <ChevronUp size={14} style={{ color: textSecondary }} />)}
|
||||
</div>
|
||||
{!collapsed.none && (
|
||||
<div style={{ padding: "10px", display: "flex", flexDirection: "column", gap: "6px" }}>
|
||||
{!collapsed.none && hasUngraded && (
|
||||
<div style={{ padding: "10px", display: "flex", flexDirection: "column", gap: "6px", maxHeight: "500px", overflowY: "auto" }}>
|
||||
{(groups.none || []).map((t) => (
|
||||
<TaskCard key={t.id} task={t} darkMode={darkMode} cardBg={cardBg} border={border} textPrimary={textPrimary} textSecondary={textSecondary} onDelegate={onDelegate} onToggleComplete={onToggleComplete} onUpdateTask={onUpdateTask} projects={projects} de={de}>
|
||||
<div style={{ display: "flex", gap: "3px", marginTop: "5px", flexWrap: "wrap" }}>
|
||||
@ -1002,7 +1070,21 @@ function AbcdeView({ groups, allTasks, darkMode, de, onSetGrade, onDelegate, onT
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
);
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "10px" }}>
|
||||
{gradeBuckets}
|
||||
{ungradedPanel}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", gap: "14px", alignItems: "flex-start" }}>
|
||||
<div style={{ flex: 3, minWidth: 0 }}>{gradeBuckets}</div>
|
||||
<div style={{ flex: 2, minWidth: "200px", maxWidth: "380px" }}>{ungradedPanel}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -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,10 +1124,9 @@ function IvyLeeView({ tasks, selected, darkMode, de, onToggle, onDelegate, onTog
|
||||
setDropOverTop6(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Header info */}
|
||||
<div style={{ background: darkMode ? "#1e3a5f" : "#eff6ff", borderRadius: "10px", padding: "12px 14px", marginBottom: "16px", display: "flex", alignItems: "flex-start", gap: "10px" }}>
|
||||
// Info banner
|
||||
const infoBanner = (
|
||||
<div style={{ background: darkMode ? "#1e3a5f" : "#eff6ff", borderRadius: "10px", padding: "12px 14px", marginBottom: "14px", display: "flex", alignItems: "flex-start", gap: "10px" }}>
|
||||
<Star size={16} style={{ color: "#3b82f6", flexShrink: 0, marginTop: "1px" }} />
|
||||
<div>
|
||||
<p style={{ fontSize: "0.82rem", color: darkMode ? "#93c5fd" : "#1d4ed8", fontWeight: 600, margin: "0 0 3px" }}>
|
||||
@ -1057,13 +1139,15 @@ function IvyLeeView({ tasks, selected, darkMode, de, onToggle, onDelegate, onTog
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
{/* Selected tasks */}
|
||||
// Top-6 selection panel (left column)
|
||||
const top6Panel = (
|
||||
<div
|
||||
onDragOver={(e) => { 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" }}
|
||||
style={{ borderRadius: "10px", border: dropOverTop6 ? `2px dashed ${accentColor}` : "2px solid transparent", padding: "4px", transition: "border 0.15s" }}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "8px" }}>
|
||||
<span style={{ fontSize: "0.78rem", fontWeight: 700, color: dropOverTop6 ? accentColor : textSecondary, textTransform: "uppercase", letterSpacing: "0.05em" }}>
|
||||
@ -1090,19 +1174,20 @@ function IvyLeeView({ tasks, selected, darkMode, de, onToggle, onDelegate, onTog
|
||||
<div key={`empty-${i}`} style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||
<span style={{ fontSize: "0.78rem", fontWeight: 700, color: textSecondary, width: "16px", textAlign: "center", flexShrink: 0 }}>{i + 1}</span>
|
||||
<div style={{ flex: 1, background: darkMode ? "#1f2937" : "#f9fafb", border: `1px dashed ${border}`, borderRadius: "8px", padding: "10px 12px" }}>
|
||||
<span style={{ fontSize: "0.78rem", color: textSecondary }}>{de ? "Leer — wähle eine Aufgabe unten" : "Empty — pick a task below"}</span>
|
||||
<span style={{ fontSize: "0.78rem", color: textSecondary }}>{de ? "Leer — wähle eine Aufgabe" : "Empty — pick a task"}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
{/* All other tasks to pick from */}
|
||||
{unselected.length > 0 && (
|
||||
// Available tasks pool (right column)
|
||||
const availablePool = (
|
||||
<div>
|
||||
<p style={{ fontSize: "0.78rem", fontWeight: 600, color: textSecondary, marginBottom: "8px" }}>
|
||||
{de ? "Verfügbare Aufgaben:" : "Available tasks:"}
|
||||
{unselected.length > 0 ? (de ? "Verfügbare Aufgaben:" : "Available tasks:") : (de ? "Alle Aufgaben ausgewählt ✓" : "All tasks selected ✓")}
|
||||
</p>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "5px" }}>
|
||||
{unselected.map((t) => (
|
||||
@ -1130,7 +1215,25 @@ function IvyLeeView({ tasks, selected, darkMode, de, onToggle, onDelegate, onTog
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
);
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<div>
|
||||
{infoBanner}
|
||||
{top6Panel}
|
||||
{unselected.length > 0 && <div style={{ marginTop: "16px" }}>{availablePool}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{infoBanner}
|
||||
<div style={{ display: "flex", gap: "14px", alignItems: "flex-start" }}>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>{top6Panel}</div>
|
||||
<div style={{ flex: 1, minWidth: "200px" }}>{availablePool}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -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 (
|
||||
<div>
|
||||
{/* Info banner */}
|
||||
<div style={{ background: darkMode ? "#422006" : "#fff7ed", borderRadius: "10px", padding: "12px 14px", marginBottom: "16px", display: "flex", gap: "10px", alignItems: "flex-start" }}>
|
||||
// Info banner + progress bar (full-width header)
|
||||
const header = (
|
||||
<>
|
||||
<div style={{ background: darkMode ? "#422006" : "#fff7ed", borderRadius: "10px", padding: "12px 14px", marginBottom: "12px", display: "flex", gap: "10px", alignItems: "flex-start" }}>
|
||||
<BarChart2 size={16} style={{ color: "#f97316", flexShrink: 0, marginTop: "1px" }} />
|
||||
<div>
|
||||
<p style={{ fontSize: "0.82rem", fontWeight: 700, color: "#f97316", margin: "0 0 3px" }}>
|
||||
@ -1169,19 +1272,20 @@ function ParetoView({ important, rest, darkMode, de, onDelegate, onToggleComplet
|
||||
</p>
|
||||
<p style={{ fontSize: "0.78rem", color: darkMode ? "#fb923c" : "#c2410c", margin: 0, lineHeight: 1.5 }}>
|
||||
{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.`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress bar */}
|
||||
<div style={{ background: darkMode ? "#374151" : "#e5e7eb", borderRadius: "99px", height: "8px", marginBottom: "20px", overflow: "hidden" }}>
|
||||
<div style={{ background: darkMode ? "#374151" : "#e5e7eb", borderRadius: "99px", height: "8px", marginBottom: "16px", overflow: "hidden" }}>
|
||||
<div style={{ width: `${pct}%`, height: "100%", background: "linear-gradient(90deg, #f97316, #ef4444)", borderRadius: "99px", transition: "width 0.4s" }} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
{/* Important tasks */}
|
||||
<div style={{ marginBottom: "16px" }}>
|
||||
// High-impact tasks (left column)
|
||||
const importantPanel = (
|
||||
<div>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "8px", marginBottom: "10px" }}>
|
||||
<span style={{ fontSize: "0.78rem", fontWeight: 700, color: "#f97316", textTransform: "uppercase", letterSpacing: "0.05em" }}>
|
||||
{de ? "Wichtige Aufgaben (Fokus)" : "High-Impact Tasks (Focus)"}
|
||||
@ -1203,19 +1307,20 @@ function ParetoView({ important, rest, darkMode, de, onDelegate, onToggleComplet
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
{/* The rest (80%) */}
|
||||
// Remaining tasks (right column — always visible, no toggle)
|
||||
const remainingPanel = (
|
||||
<div>
|
||||
<button
|
||||
onClick={() => setShowRest((v) => !v)}
|
||||
style={{ display: "flex", alignItems: "center", gap: "6px", background: "none", border: "none", cursor: "pointer", padding: "6px 0", marginBottom: "8px" }}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "8px", marginBottom: "10px" }}>
|
||||
<span style={{ fontSize: "0.78rem", fontWeight: 600, color: textSecondary, textTransform: "uppercase", letterSpacing: "0.05em" }}>
|
||||
{de ? "Restliche Aufgaben" : "Remaining tasks"} ({rest.length})
|
||||
{de ? "Restliche Aufgaben" : "Remaining tasks"}
|
||||
</span>
|
||||
{showRest ? <ChevronUp size={13} style={{ color: textSecondary }} /> : <ChevronDown size={13} style={{ color: textSecondary }} />}
|
||||
</button>
|
||||
{showRest && (
|
||||
<span style={{ fontSize: "0.75rem", color: textSecondary, background: darkMode ? "#374151" : "#e5e7eb", borderRadius: "9px", padding: "1px 7px" }}>{rest.length}</span>
|
||||
</div>
|
||||
{rest.length === 0 ? (
|
||||
<p style={{ fontSize: "0.78rem", color: textSecondary }}>{de ? "Keine weiteren Aufgaben." : "No remaining tasks."}</p>
|
||||
) : (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "6px" }}>
|
||||
{rest.map((t) => (
|
||||
<TaskCard key={t.id} task={t} darkMode={darkMode} cardBg={cardBg} border={border} textPrimary={textPrimary} textSecondary={textSecondary} onDelegate={onDelegate} onToggleComplete={onToggleComplete} onUpdateTask={onUpdateTask} projects={projects} de={de}>
|
||||
@ -1230,6 +1335,25 @@ function ParetoView({ important, rest, darkMode, de, onDelegate, onToggleComplet
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<div>
|
||||
{header}
|
||||
{importantPanel}
|
||||
<div style={{ marginTop: "16px" }}>{remainingPanel}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{header}
|
||||
<div style={{ display: "flex", gap: "14px", alignItems: "flex-start" }}>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>{importantPanel}</div>
|
||||
<div style={{ flex: 1, minWidth: "200px" }}>{remainingPanel}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user