feat: priority view resize handle + dense task cards with actions
Resize handle: - Drag the divider between priority panel and calendar to resize it - Height persists in cookie (priorityViewHeight) Dense task cards: - Single-row compact layout: [✓] date · title · action icons - Date shown before the title - Action icon bar: rename (✏), note (📝), link (🔗), project (📁), delegate (👤), remove (×) - Inline title editing: double-click or click ✏ icon - Inline note textarea: toggles on 📝 click, saves on blur - Inline URL input with open-link button: toggles on 🔗 click - Inline project selector: toggles on 📁 click - Icons colored when field has a value (note, url, project, delegate) - Export/import now round-trips url, kanbanStage, recurrenceDays fields v1.88.0
This commit is contained in:
parent
8ffc127671
commit
149f0cf021
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "my-weekly-todo-list",
|
||||
"version": "1.87.2",
|
||||
"version": "1.88.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": {
|
||||
|
||||
@ -24,6 +24,10 @@ import {
|
||||
CalendarDays,
|
||||
FolderOpen,
|
||||
List,
|
||||
Pencil,
|
||||
FileText,
|
||||
Link2,
|
||||
ExternalLink,
|
||||
} from "lucide-react";
|
||||
|
||||
export interface PriorityTask {
|
||||
@ -43,6 +47,7 @@ export interface PriorityTask {
|
||||
dayOfWeek?: number | null;
|
||||
markdownContent?: string | null;
|
||||
startTime?: string | null;
|
||||
url?: string | null;
|
||||
}
|
||||
|
||||
export interface PrioritySomedayList {
|
||||
@ -385,6 +390,8 @@ export default function PriorityView({
|
||||
onClear={clearEisenhower}
|
||||
onDelegate={openDelegate}
|
||||
onToggleComplete={toggleComplete}
|
||||
onUpdateTask={onUpdateTask}
|
||||
projects={projects}
|
||||
cardBg={cardBg}
|
||||
border={border}
|
||||
textPrimary={textPrimary}
|
||||
@ -402,6 +409,8 @@ export default function PriorityView({
|
||||
onSetGrade={setAbcde}
|
||||
onDelegate={openDelegate}
|
||||
onToggleComplete={toggleComplete}
|
||||
onUpdateTask={onUpdateTask}
|
||||
projects={projects}
|
||||
cardBg={cardBg}
|
||||
border={border}
|
||||
textPrimary={textPrimary}
|
||||
@ -419,6 +428,8 @@ export default function PriorityView({
|
||||
onToggle={toggleIvyLee}
|
||||
onDelegate={openDelegate}
|
||||
onToggleComplete={toggleComplete}
|
||||
onUpdateTask={onUpdateTask}
|
||||
projects={projects}
|
||||
cardBg={cardBg}
|
||||
border={border}
|
||||
textPrimary={textPrimary}
|
||||
@ -436,6 +447,7 @@ export default function PriorityView({
|
||||
onDelegate={openDelegate}
|
||||
onToggleComplete={toggleComplete}
|
||||
onUpdateTask={onUpdateTask}
|
||||
projects={projects}
|
||||
cardBg={cardBg}
|
||||
border={border}
|
||||
textPrimary={textPrimary}
|
||||
@ -565,76 +577,161 @@ interface TaskCardProps {
|
||||
textSecondary: string;
|
||||
onDelegate: (t: PriorityTask) => void;
|
||||
onToggleComplete: (t: PriorityTask) => void;
|
||||
onUpdateTask?: (id: string, fields: Partial<PriorityTask>) => Promise<void>;
|
||||
projects?: PriorityProject[];
|
||||
onRemove?: () => void;
|
||||
children?: React.ReactNode;
|
||||
compact?: boolean;
|
||||
de?: boolean;
|
||||
}
|
||||
|
||||
function TaskCard({ task, darkMode, cardBg, border, textPrimary, textSecondary, onDelegate, onToggleComplete, children, compact }: TaskCardProps) {
|
||||
const hasDelegate = !!task.delegatedTo;
|
||||
function TaskCard({ task, darkMode, cardBg, border, textPrimary, textSecondary, onDelegate, onToggleComplete, onUpdateTask, projects, onRemove, children, de }: TaskCardProps) {
|
||||
const [editingTitle, setEditingTitle] = useState(false);
|
||||
const [titleVal, setTitleVal] = useState(task.title);
|
||||
const [showNote, setShowNote] = useState(false);
|
||||
const [noteVal, setNoteVal] = useState(task.markdownContent || "");
|
||||
const [showUrl, setShowUrl] = useState(false);
|
||||
const [urlVal, setUrlVal] = useState(task.url || "");
|
||||
const [showProject, setShowProject] = useState(false);
|
||||
|
||||
const saveTitle = () => {
|
||||
if (onUpdateTask && titleVal.trim() && titleVal.trim() !== task.title) {
|
||||
onUpdateTask(task.id, { title: titleVal.trim() });
|
||||
}
|
||||
setEditingTitle(false);
|
||||
};
|
||||
|
||||
const iconBtn = (onClick: (e: React.MouseEvent) => void, title: string, active: boolean, activeColor: string, icon: React.ReactNode) => (
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onClick(e); }}
|
||||
title={title}
|
||||
style={{ background: "none", border: "none", cursor: "pointer", padding: "2px 3px", borderRadius: "3px", color: active ? activeColor : "#9ca3af", flexShrink: 0, display: "flex", alignItems: "center" }}
|
||||
>
|
||||
{icon}
|
||||
</button>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
draggable
|
||||
onDragStart={(e) => {
|
||||
e.dataTransfer.setData("text/plain", task.id);
|
||||
e.dataTransfer.effectAllowed = "move";
|
||||
}}
|
||||
onDragStart={(e) => { e.dataTransfer.setData("text/plain", task.id); e.dataTransfer.effectAllowed = "move"; }}
|
||||
style={{
|
||||
background: task.completed ? (darkMode ? "#111827" : "#f9fafb") : cardBg,
|
||||
border: `1px solid ${border}`,
|
||||
borderRadius: "8px",
|
||||
padding: compact ? "7px 10px" : "10px 12px",
|
||||
display: "flex",
|
||||
alignItems: "flex-start",
|
||||
gap: "8px",
|
||||
borderRadius: "7px",
|
||||
padding: "4px 6px",
|
||||
opacity: task.completed ? 0.6 : 1,
|
||||
transition: "opacity 0.15s",
|
||||
cursor: "grab",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "2px",
|
||||
}}
|
||||
>
|
||||
{/* Checkbox */}
|
||||
<button
|
||||
onClick={() => onToggleComplete(task)}
|
||||
style={{ background: "none", border: "none", cursor: "pointer", padding: 0, marginTop: "1px", flexShrink: 0 }}
|
||||
title="Toggle complete"
|
||||
>
|
||||
{task.completed
|
||||
? <Check size={16} style={{ color: "#059669" }} />
|
||||
: <Circle size={16} style={{ color: "#9ca3af" }} />
|
||||
}
|
||||
</button>
|
||||
{/* Body */}
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<span style={{ fontSize: "0.88rem", color: textPrimary, fontWeight: 500, textDecoration: task.completed ? "line-through" : "none", wordBreak: "break-word" }}>
|
||||
{task.title}
|
||||
</span>
|
||||
<div style={{ display: "flex", flexWrap: "wrap", gap: "5px", marginTop: "4px" }}>
|
||||
{task.scheduledDate && (
|
||||
<span style={{ fontSize: "0.7rem", color: "#6b7280", background: darkMode ? "#374151" : "#f3f4f6", borderRadius: "4px", padding: "1px 5px" }}>
|
||||
{formatDate(task.scheduledDate)}
|
||||
</span>
|
||||
)}
|
||||
{/* Main row */}
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "4px" }}>
|
||||
{/* Complete toggle */}
|
||||
<button
|
||||
onClick={() => onToggleComplete(task)}
|
||||
style={{ background: "none", border: "none", cursor: "pointer", padding: 0, flexShrink: 0, display: "flex", alignItems: "center" }}
|
||||
>
|
||||
{task.completed ? <Check size={13} style={{ color: "#059669" }} /> : <Circle size={13} style={{ color: "#9ca3af" }} />}
|
||||
</button>
|
||||
{/* Date */}
|
||||
{task.scheduledDate && (
|
||||
<span style={{ fontSize: "0.63rem", color: "#6b7280", flexShrink: 0, fontVariantNumeric: "tabular-nums" }}>
|
||||
{formatDate(task.scheduledDate)}
|
||||
</span>
|
||||
)}
|
||||
{/* Title */}
|
||||
{editingTitle ? (
|
||||
<input
|
||||
value={titleVal}
|
||||
onChange={(e) => setTitleVal(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") saveTitle(); if (e.key === "Escape") { setTitleVal(task.title); setEditingTitle(false); } }}
|
||||
onBlur={saveTitle}
|
||||
autoFocus
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{ flex: 1, fontSize: "0.82rem", fontWeight: 500, border: `1px solid ${border}`, borderRadius: "4px", padding: "1px 4px", background: darkMode ? "#374151" : "#fff", color: textPrimary, outline: "none", minWidth: 0 }}
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
style={{ flex: 1, fontSize: "0.82rem", fontWeight: 500, color: textPrimary, textDecoration: task.completed ? "line-through" : "none", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", minWidth: 0 }}
|
||||
title={task.title}
|
||||
onDoubleClick={() => setEditingTitle(true)}
|
||||
>
|
||||
{task.title}
|
||||
</span>
|
||||
)}
|
||||
{/* Action icons */}
|
||||
{iconBtn(() => setEditingTitle(true), de ? "Umbenennen" : "Rename", false, "#6366f1", <Pencil size={11} />)}
|
||||
{iconBtn(() => { setShowNote((v) => !v); if (!showNote) setNoteVal(task.markdownContent || ""); }, de ? "Notiz" : "Note", !!task.markdownContent || showNote, "#6366f1", <FileText size={11} />)}
|
||||
{iconBtn(() => { setShowUrl((v) => !v); if (!showUrl) setUrlVal(task.url || ""); }, "Link", !!task.url || showUrl, "#3b82f6", <Link2 size={11} />)}
|
||||
{projects && projects.length > 0 && iconBtn(() => setShowProject((v) => !v), de ? "Projekt" : "Project", !!task.projectId || showProject, "#059669", <FolderOpen size={11} />)}
|
||||
{iconBtn(() => onDelegate(task), de ? "Delegieren" : "Delegate", !!task.delegatedTo, "#8b5cf6", <UserCheck size={11} />)}
|
||||
{onRemove && iconBtn(() => onRemove(), de ? "Entfernen" : "Remove", false, "#ef4444", <X size={11} />)}
|
||||
</div>
|
||||
|
||||
{/* Badges */}
|
||||
{(task.project || task.delegatedTo) && (
|
||||
<div style={{ display: "flex", gap: "3px", paddingLeft: "17px", flexWrap: "wrap" }}>
|
||||
{task.project && (
|
||||
<span style={{ fontSize: "0.7rem", color: task.project.color || "#6366f1", background: `${task.project.color || "#6366f1"}20`, borderRadius: "4px", padding: "1px 5px" }}>
|
||||
<span style={{ fontSize: "0.62rem", color: task.project.color || "#6366f1", background: `${task.project.color || "#6366f1"}20`, borderRadius: "3px", padding: "0 4px" }}>
|
||||
{task.project.name}
|
||||
</span>
|
||||
)}
|
||||
{hasDelegate && (
|
||||
<span style={{ fontSize: "0.7rem", color: "#8b5cf6", background: "#8b5cf620", borderRadius: "4px", padding: "1px 5px", display: "flex", alignItems: "center", gap: "3px" }}>
|
||||
{task.delegatedTo === "AI" ? <Sparkles size={9} /> : <User size={9} />}
|
||||
{task.delegatedTo && (
|
||||
<span style={{ fontSize: "0.62rem", color: "#8b5cf6", background: "#8b5cf620", borderRadius: "3px", padding: "0 4px", display: "flex", alignItems: "center", gap: "2px" }}>
|
||||
{task.delegatedTo === "AI" ? <Sparkles size={8} /> : <User size={8} />}
|
||||
{task.delegatedTo}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
{/* Delegate button */}
|
||||
<button
|
||||
onClick={() => onDelegate(task)}
|
||||
style={{ background: "none", border: "none", cursor: "pointer", padding: "2px 4px", borderRadius: "5px", color: hasDelegate ? "#8b5cf6" : "#9ca3af", flexShrink: 0 }}
|
||||
title="Delegate"
|
||||
>
|
||||
<UserCheck size={14} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Note editor */}
|
||||
{showNote && (
|
||||
<textarea
|
||||
value={noteVal}
|
||||
onChange={(e) => setNoteVal(e.target.value)}
|
||||
onBlur={() => { if (onUpdateTask) onUpdateTask(task.id, { markdownContent: noteVal || null }); }}
|
||||
placeholder={de ? "Notiz..." : "Note..."}
|
||||
rows={2}
|
||||
style={{ fontSize: "0.78rem", border: `1px solid ${border}`, borderRadius: "5px", padding: "4px 6px", background: darkMode ? "#374151" : "#f9fafb", color: textPrimary, resize: "vertical", fontFamily: "inherit", outline: "none", width: "100%", boxSizing: "border-box", marginTop: "2px" }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* URL input */}
|
||||
{showUrl && (
|
||||
<div style={{ display: "flex", gap: "4px", alignItems: "center", marginTop: "2px" }}>
|
||||
<input
|
||||
value={urlVal}
|
||||
onChange={(e) => setUrlVal(e.target.value)}
|
||||
onBlur={() => { if (onUpdateTask) onUpdateTask(task.id, { url: urlVal || null }); }}
|
||||
onKeyDown={(e) => { if (e.key === "Enter" && onUpdateTask) { onUpdateTask(task.id, { url: urlVal || null }); setShowUrl(false); } }}
|
||||
placeholder="https://..."
|
||||
style={{ flex: 1, fontSize: "0.78rem", border: `1px solid ${border}`, borderRadius: "5px", padding: "3px 6px", background: darkMode ? "#374151" : "#f9fafb", color: textPrimary, outline: "none" }}
|
||||
/>
|
||||
{task.url && (
|
||||
<a href={task.url} target="_blank" rel="noopener noreferrer" style={{ color: "#3b82f6", flexShrink: 0, display: "flex" }}>
|
||||
<ExternalLink size={12} />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Project selector */}
|
||||
{showProject && projects && (
|
||||
<select
|
||||
value={task.projectId || ""}
|
||||
onChange={(e) => { if (onUpdateTask) { onUpdateTask(task.id, { projectId: e.target.value || null }); setShowProject(false); } }}
|
||||
style={{ fontSize: "0.78rem", border: `1px solid ${border}`, borderRadius: "5px", padding: "3px 6px", background: darkMode ? "#374151" : "#f9fafb", color: textPrimary, outline: "none", width: "100%", cursor: "pointer", marginTop: "2px" }}
|
||||
>
|
||||
<option value="">{de ? "Kein Projekt" : "No project"}</option>
|
||||
{projects.map((p) => <option key={p.id} value={p.id}>{p.name}</option>)}
|
||||
</select>
|
||||
)}
|
||||
|
||||
{/* Children (quick-assign buttons for unset/ungraded sections) */}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -655,6 +752,8 @@ interface EisenhowerProps {
|
||||
onClear: (t: PriorityTask) => void;
|
||||
onDelegate: (t: PriorityTask) => void;
|
||||
onToggleComplete: (t: PriorityTask) => void;
|
||||
onUpdateTask: (id: string, fields: Partial<PriorityTask>) => Promise<void>;
|
||||
projects: PriorityProject[];
|
||||
cardBg: string;
|
||||
border: string;
|
||||
textPrimary: string;
|
||||
@ -668,7 +767,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, 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 }: EisenhowerProps) {
|
||||
const quadrantTasks = [q1, q2, q3, q4];
|
||||
const [collapsed, setCollapsed] = useState<Record<number, boolean>>({});
|
||||
const [dragOver, setDragOver] = useState<number | null>(null);
|
||||
@ -730,11 +829,7 @@ function EisenhowerMatrix({ q1, q2, q3, q4, unset, allTasks, darkMode, de, onSet
|
||||
</p>
|
||||
)}
|
||||
{tasks.map((t) => (
|
||||
<TaskCard key={t.id} task={t} darkMode={darkMode} cardBg={cardBg} border={border} textPrimary={textPrimary} textSecondary={textSecondary} onDelegate={onDelegate} onToggleComplete={onToggleComplete} compact>
|
||||
<button onClick={() => onClear(t)} style={{ fontSize: "0.65rem", marginTop: "3px", background: "none", border: "none", color: textSecondary, cursor: "pointer", padding: 0, display: "flex", alignItems: "center", gap: "2px" }}>
|
||||
<X size={9} /> {de ? "Entfernen" : "Remove"}
|
||||
</button>
|
||||
</TaskCard>
|
||||
<TaskCard key={t.id} task={t} darkMode={darkMode} cardBg={cardBg} border={border} textPrimary={textPrimary} textSecondary={textSecondary} onDelegate={onDelegate} onToggleComplete={onToggleComplete} onUpdateTask={onUpdateTask} projects={projects} onRemove={() => onClear(t)} de={de} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@ -758,13 +853,13 @@ function EisenhowerMatrix({ q1, q2, q3, q4, unset, allTasks, darkMode, de, onSet
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
|
||||
{unset.map((t) => (
|
||||
<div key={t.id}>
|
||||
<TaskCard task={t} darkMode={darkMode} cardBg={cardBg} border={border} textPrimary={textPrimary} textSecondary={textSecondary} onDelegate={onDelegate} onToggleComplete={onToggleComplete}>
|
||||
<div style={{ display: "flex", gap: "4px", marginTop: "6px", flexWrap: "wrap" }}>
|
||||
<TaskCard 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: "4px", marginTop: "5px", flexWrap: "wrap" }}>
|
||||
{QUADRANT_CONFIG.map((qc, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => onSetQuadrant(t, qc.urgency, qc.importance)}
|
||||
style={{ fontSize: "0.68rem", padding: "2px 7px", borderRadius: "5px", border: `1px solid ${qc.color}`, background: `${qc.color}18`, color: qc.color, cursor: "pointer", display: "flex", alignItems: "center", gap: "3px", fontWeight: 600 }}
|
||||
style={{ fontSize: "0.65rem", padding: "2px 6px", borderRadius: "4px", border: `1px solid ${qc.color}`, background: `${qc.color}18`, color: qc.color, cursor: "pointer", display: "flex", alignItems: "center", gap: "3px", fontWeight: 600 }}
|
||||
>
|
||||
{qc.icon} {de ? qc.labelDe : qc.label}
|
||||
</button>
|
||||
@ -791,13 +886,15 @@ interface AbcdeViewProps {
|
||||
onSetGrade: (t: PriorityTask, grade: string | null) => void;
|
||||
onDelegate: (t: PriorityTask) => void;
|
||||
onToggleComplete: (t: PriorityTask) => void;
|
||||
onUpdateTask: (id: string, fields: Partial<PriorityTask>) => Promise<void>;
|
||||
projects: PriorityProject[];
|
||||
cardBg: string;
|
||||
border: string;
|
||||
textPrimary: string;
|
||||
textSecondary: string;
|
||||
}
|
||||
|
||||
function AbcdeView({ groups, allTasks, darkMode, de, onSetGrade, onDelegate, onToggleComplete, cardBg, border, textPrimary, textSecondary }: AbcdeViewProps) {
|
||||
function AbcdeView({ groups, allTasks, darkMode, de, onSetGrade, onDelegate, onToggleComplete, onUpdateTask, projects, cardBg, border, textPrimary, textSecondary }: AbcdeViewProps) {
|
||||
const [collapsed, setCollapsed] = useState<Record<string, boolean>>({ none: true });
|
||||
const [dragOver, setDragOver] = useState<string | null>(null);
|
||||
|
||||
@ -858,25 +955,7 @@ function AbcdeView({ groups, allTasks, darkMode, de, onSetGrade, onDelegate, onT
|
||||
</p>
|
||||
)}
|
||||
{tasks.map((t) => (
|
||||
<TaskCard key={t.id} task={t} darkMode={darkMode} cardBg={cardBg} border={border} textPrimary={textPrimary} textSecondary={textSecondary} onDelegate={onDelegate} onToggleComplete={onToggleComplete}>
|
||||
<div style={{ display: "flex", gap: "3px", marginTop: "6px", flexWrap: "wrap" }}>
|
||||
{gradeList.filter((g) => g !== grade).map((g) => (
|
||||
<button
|
||||
key={g}
|
||||
onClick={() => onSetGrade(t, g)}
|
||||
style={{ fontSize: "0.68rem", padding: "1px 7px", borderRadius: "5px", border: `1px solid ${ABCDE_LABELS[g].color}`, background: `${ABCDE_LABELS[g].color}18`, color: ABCDE_LABELS[g].color, cursor: "pointer", fontWeight: 700 }}
|
||||
>
|
||||
{g}
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
onClick={() => onSetGrade(t, null)}
|
||||
style={{ fontSize: "0.68rem", padding: "1px 7px", borderRadius: "5px", border: `1px solid ${textSecondary}`, background: "transparent", color: textSecondary, cursor: "pointer" }}
|
||||
>
|
||||
<X size={9} />
|
||||
</button>
|
||||
</div>
|
||||
</TaskCard>
|
||||
<TaskCard key={t.id} task={t} darkMode={darkMode} cardBg={cardBg} border={border} textPrimary={textPrimary} textSecondary={textSecondary} onDelegate={onDelegate} onToggleComplete={onToggleComplete} onUpdateTask={onUpdateTask} projects={projects} onRemove={() => onSetGrade(t, null)} de={de} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@ -904,13 +983,13 @@ function AbcdeView({ groups, allTasks, darkMode, de, onSetGrade, onDelegate, onT
|
||||
{!collapsed.none && (
|
||||
<div style={{ padding: "10px", display: "flex", flexDirection: "column", gap: "6px" }}>
|
||||
{(groups.none || []).map((t) => (
|
||||
<TaskCard key={t.id} task={t} darkMode={darkMode} cardBg={cardBg} border={border} textPrimary={textPrimary} textSecondary={textSecondary} onDelegate={onDelegate} onToggleComplete={onToggleComplete}>
|
||||
<div style={{ display: "flex", gap: "3px", marginTop: "6px", flexWrap: "wrap" }}>
|
||||
<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" }}>
|
||||
{gradeList.map((g) => (
|
||||
<button
|
||||
key={g}
|
||||
onClick={() => onSetGrade(t, g)}
|
||||
style={{ fontSize: "0.68rem", padding: "1px 7px", borderRadius: "5px", border: `1px solid ${ABCDE_LABELS[g].color}`, background: `${ABCDE_LABELS[g].color}18`, color: ABCDE_LABELS[g].color, cursor: "pointer", fontWeight: 700 }}
|
||||
style={{ fontSize: "0.65rem", padding: "1px 6px", borderRadius: "4px", border: `1px solid ${ABCDE_LABELS[g].color}`, background: `${ABCDE_LABELS[g].color}18`, color: ABCDE_LABELS[g].color, cursor: "pointer", fontWeight: 700 }}
|
||||
>
|
||||
{g}
|
||||
</button>
|
||||
@ -937,13 +1016,15 @@ interface IvyLeeViewProps {
|
||||
onToggle: (id: string) => void;
|
||||
onDelegate: (t: PriorityTask) => void;
|
||||
onToggleComplete: (t: PriorityTask) => void;
|
||||
onUpdateTask: (id: string, fields: Partial<PriorityTask>) => Promise<void>;
|
||||
projects: PriorityProject[];
|
||||
cardBg: string;
|
||||
border: string;
|
||||
textPrimary: string;
|
||||
textSecondary: string;
|
||||
}
|
||||
|
||||
function IvyLeeView({ tasks, selected, darkMode, de, onToggle, onDelegate, onToggleComplete, cardBg, border, textPrimary, textSecondary }: IvyLeeViewProps) {
|
||||
function IvyLeeView({ tasks, selected, darkMode, de, onToggle, onDelegate, onToggleComplete, onUpdateTask, projects, cardBg, border, textPrimary, textSecondary }: IvyLeeViewProps) {
|
||||
const accentColor = "#6366f1";
|
||||
const selectedTasks = tasks.filter((t) => selected.has(t.id));
|
||||
const unselected = tasks.filter((t) => !selected.has(t.id));
|
||||
@ -998,11 +1079,7 @@ function IvyLeeView({ tasks, selected, darkMode, de, onToggle, onDelegate, onTog
|
||||
<div key={t.id} style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||
<span style={{ fontSize: "0.78rem", fontWeight: 700, color: accentColor, width: "16px", textAlign: "center", flexShrink: 0 }}>{i + 1}</span>
|
||||
<div style={{ flex: 1 }}>
|
||||
<TaskCard task={t} darkMode={darkMode} cardBg={cardBg} border={border} textPrimary={textPrimary} textSecondary={textSecondary} onDelegate={onDelegate} onToggleComplete={onToggleComplete} compact>
|
||||
<button onClick={() => onToggle(t.id)} style={{ fontSize: "0.65rem", background: "none", border: "none", color: "#ef4444", cursor: "pointer", padding: 0, marginTop: "2px", display: "flex", alignItems: "center", gap: "2px" }}>
|
||||
<X size={9} /> {de ? "Entfernen" : "Remove"}
|
||||
</button>
|
||||
</TaskCard>
|
||||
<TaskCard task={t} darkMode={darkMode} cardBg={cardBg} border={border} textPrimary={textPrimary} textSecondary={textSecondary} onDelegate={onDelegate} onToggleComplete={onToggleComplete} onUpdateTask={onUpdateTask} projects={projects} onRemove={() => onToggle(t.id)} de={de} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -1067,13 +1144,14 @@ interface ParetoViewProps {
|
||||
onDelegate: (t: PriorityTask) => void;
|
||||
onToggleComplete: (t: PriorityTask) => void;
|
||||
onUpdateTask: (id: string, fields: Partial<PriorityTask>) => Promise<void>;
|
||||
projects: PriorityProject[];
|
||||
cardBg: string;
|
||||
border: string;
|
||||
textPrimary: string;
|
||||
textSecondary: string;
|
||||
}
|
||||
|
||||
function ParetoView({ important, rest, darkMode, de, onDelegate, onToggleComplete, onUpdateTask, cardBg, border, textPrimary, textSecondary }: ParetoViewProps) {
|
||||
function ParetoView({ important, rest, darkMode, de, onDelegate, onToggleComplete, onUpdateTask, projects, cardBg, border, textPrimary, textSecondary }: ParetoViewProps) {
|
||||
const [showRest, setShowRest] = useState(false);
|
||||
const total = important.length + rest.length;
|
||||
const pct = total > 0 ? Math.round((important.length / total) * 100) : 0;
|
||||
@ -1118,16 +1196,7 @@ function ParetoView({ important, rest, darkMode, de, onDelegate, onToggleComplet
|
||||
)}
|
||||
{important.map((t) => (
|
||||
<div key={t.id} style={{ borderLeft: "3px solid #f97316", borderRadius: "0 8px 8px 0" }}>
|
||||
<TaskCard task={t} darkMode={darkMode} cardBg={cardBg} border={border} textPrimary={textPrimary} textSecondary={textSecondary} onDelegate={onDelegate} onToggleComplete={onToggleComplete}>
|
||||
<div style={{ display: "flex", gap: "4px", marginTop: "5px" }}>
|
||||
<button
|
||||
onClick={() => onUpdateTask(t.id, { importance: false })}
|
||||
style={{ fontSize: "0.65rem", background: "none", border: "none", color: textSecondary, cursor: "pointer", padding: 0, display: "flex", alignItems: "center", gap: "2px" }}
|
||||
>
|
||||
<X size={9} /> {de ? "Aus Fokus" : "Remove focus"}
|
||||
</button>
|
||||
</div>
|
||||
</TaskCard>
|
||||
<TaskCard task={t} darkMode={darkMode} cardBg={cardBg} border={border} textPrimary={textPrimary} textSecondary={textSecondary} onDelegate={onDelegate} onToggleComplete={onToggleComplete} onUpdateTask={onUpdateTask} projects={projects} onRemove={() => onUpdateTask(t.id, { importance: false })} de={de} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -1147,7 +1216,7 @@ function ParetoView({ important, rest, darkMode, de, onDelegate, onToggleComplet
|
||||
{showRest && (
|
||||
<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}>
|
||||
<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}>
|
||||
<button
|
||||
onClick={() => onUpdateTask(t.id, { importance: true })}
|
||||
style={{ fontSize: "0.65rem", marginTop: "3px", background: "none", border: "none", color: "#f97316", cursor: "pointer", padding: 0, display: "flex", alignItems: "center", gap: "2px" }}
|
||||
|
||||
@ -626,13 +626,22 @@ export default function WeeklyView() {
|
||||
}
|
||||
return null;
|
||||
});
|
||||
const resizingRef = useRef<{ target: 'someday' | 'allday'; startY: number; startHeight: number; handleOnTop: boolean } | null>(null);
|
||||
const [priorityViewHeight, setPriorityViewHeight] = useState<number>(() => {
|
||||
if (typeof document !== 'undefined') {
|
||||
const c = document.cookie.match(/priorityViewHeight=(\d+)/);
|
||||
return c ? parseInt(c[1]) : 340;
|
||||
}
|
||||
return 340;
|
||||
});
|
||||
const priorityViewRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const startResize = useCallback((e: React.MouseEvent | React.TouchEvent, target: 'someday' | 'allday', handleOnTop = false) => {
|
||||
const resizingRef = useRef<{ target: 'someday' | 'allday' | 'priority'; startY: number; startHeight: number; handleOnTop: boolean } | null>(null);
|
||||
|
||||
const startResize = useCallback((e: React.MouseEvent | React.TouchEvent, target: 'someday' | 'allday' | 'priority', handleOnTop = false) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY;
|
||||
const section = target === 'someday' ? somedaySectionRef.current : (document.querySelector('.all-day-events-section') as HTMLElement);
|
||||
const section = target === 'someday' ? somedaySectionRef.current : target === 'priority' ? priorityViewRef.current : (document.querySelector('.all-day-events-section') as HTMLElement);
|
||||
if (!section) return;
|
||||
resizingRef.current = { target, startY: clientY, startHeight: section.getBoundingClientRect().height, handleOnTop };
|
||||
|
||||
@ -642,16 +651,21 @@ export default function WeeklyView() {
|
||||
const rawDelta = y - resizingRef.current.startY;
|
||||
// Top handle: dragging up = increase height (invert delta); bottom handle: normal
|
||||
const delta = resizingRef.current.handleOnTop ? -rawDelta : rawDelta;
|
||||
const newHeight = Math.max(40, Math.min(600, resizingRef.current.startHeight + delta));
|
||||
if (resizingRef.current.target === 'someday') setSomedayHeight(newHeight);
|
||||
else setAllDayHeight(newHeight);
|
||||
if (resizingRef.current.target === 'priority') {
|
||||
setPriorityViewHeight(Math.max(80, Math.min(800, resizingRef.current.startHeight + delta)));
|
||||
} else {
|
||||
const newHeight = Math.max(40, Math.min(600, resizingRef.current.startHeight + delta));
|
||||
if (resizingRef.current.target === 'someday') setSomedayHeight(newHeight);
|
||||
else setAllDayHeight(newHeight);
|
||||
}
|
||||
};
|
||||
const onEnd = () => {
|
||||
if (resizingRef.current) {
|
||||
const section2 = resizingRef.current.target === 'someday' ? somedaySectionRef.current : (document.querySelector('.all-day-events-section') as HTMLElement);
|
||||
const cookieKey = resizingRef.current.target === 'someday' ? 'somedayHeight' : resizingRef.current.target === 'priority' ? 'priorityViewHeight' : 'allDayHeight';
|
||||
const section2 = resizingRef.current.target === 'someday' ? somedaySectionRef.current : resizingRef.current.target === 'priority' ? priorityViewRef.current : (document.querySelector('.all-day-events-section') as HTMLElement);
|
||||
if (section2) {
|
||||
const h = Math.round(section2.getBoundingClientRect().height);
|
||||
document.cookie = `${resizingRef.current.target === 'someday' ? 'somedayHeight' : 'allDayHeight'}=${h};path=/;max-age=31536000`;
|
||||
document.cookie = `${cookieKey}=${h};path=/;max-age=31536000`;
|
||||
}
|
||||
}
|
||||
resizingRef.current = null;
|
||||
@ -6122,16 +6136,28 @@ export default function WeeklyView() {
|
||||
|
||||
{/* Priority View */}
|
||||
{profile.viewStyle === "priority" && (
|
||||
<div style={{ flex: 1, overflowY: "auto" }}>
|
||||
<PriorityView
|
||||
tasks={[...tasks, ...somedayLists.flatMap(l => l.tasks)]}
|
||||
somedayLists={somedayLists.map(l => ({ id: l.id, title: l.title }))}
|
||||
projects={projects}
|
||||
darkMode={darkMode}
|
||||
language={profile.language}
|
||||
onUpdateTask={async (id, fields) => { await updateTaskFields(id, fields as any); }}
|
||||
/>
|
||||
</div>
|
||||
<>
|
||||
<div
|
||||
ref={priorityViewRef}
|
||||
style={{ height: `${priorityViewHeight}px`, overflowY: "auto", flexShrink: 0 }}
|
||||
>
|
||||
<PriorityView
|
||||
tasks={[...tasks, ...somedayLists.flatMap(l => l.tasks)]}
|
||||
somedayLists={somedayLists.map(l => ({ id: l.id, title: l.title }))}
|
||||
projects={projects}
|
||||
darkMode={darkMode}
|
||||
language={profile.language}
|
||||
onUpdateTask={async (id, fields) => { await updateTaskFields(id, fields as any); }}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="resize-handle"
|
||||
onMouseDown={(e) => startResize(e, 'priority')}
|
||||
onTouchStart={(e) => startResize(e, 'priority')}
|
||||
>
|
||||
<div className="resize-handle-bar" />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Kanban Board View */}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user