- >
- );
-}
diff --git a/src/components/weekly/NotesSidebar.tsx b/src/components/weekly/NotesSidebar.tsx
new file mode 100644
index 0000000..5148628
--- /dev/null
+++ b/src/components/weekly/NotesSidebar.tsx
@@ -0,0 +1,168 @@
+"use client";
+
+import React, { useState, useEffect, useRef } from "react";
+import dynamic from "next/dynamic";
+
+const RichTextEditor = dynamic(() => import("../RichTextEditor"), { ssr: false });
+import type { Task } from "@/lib/weekly/types";
+
+/**
+ * Seitenleiste für Notizen und Verweis einer Aufgabe. Lag bis v1.119.1 in WeeklyView.tsx.
+ */
+interface NotesSidebarProps {
+ task: Task;
+ onClose: () => void;
+ updateTaskNotes: (id: string, notes: string) => void;
+ updateTaskUrl: (id: string, url: string) => void;
+}
+
+export function NotesSidebar({ task, onClose, updateTaskNotes, updateTaskUrl }: NotesSidebarProps) {
+ const [isVisible, setIsVisible] = useState(false);
+ const [sidebarWidth, setSidebarWidth] = useState(500);
+ const [urlValue, setUrlValue] = useState(task.url || "");
+ const isResizing = useRef(false);
+
+ useEffect(() => { setUrlValue(task.url || ""); }, [task.url]);
+
+ useEffect(() => {
+ const handleMouseMove = (e: MouseEvent) => {
+ if (!isResizing.current) return;
+ const newWidth = window.innerWidth - e.clientX;
+ setSidebarWidth(Math.max(320, Math.min(newWidth, window.innerWidth * 0.9)));
+ };
+ const handleMouseUp = () => {
+ if (isResizing.current) {
+ isResizing.current = false;
+ document.body.style.cursor = '';
+ document.body.style.userSelect = '';
+ }
+ };
+ window.addEventListener('mousemove', handleMouseMove);
+ window.addEventListener('mouseup', handleMouseUp);
+ return () => {
+ window.removeEventListener('mousemove', handleMouseMove);
+ window.removeEventListener('mouseup', handleMouseUp);
+ };
+ }, []);
+
+ useEffect(() => {
+ const timer = setTimeout(() => setIsVisible(true), 10);
+ return () => clearTimeout(timer);
+ }, []);
+
+ const handleClose = () => {
+ setIsVisible(false);
+ setTimeout(onClose, 300);
+ };
+
+ const handleUrlBlur = () => {
+ const normalised = urlValue.trim()
+ ? urlValue.trim().startsWith("http") ? urlValue.trim() : `https://${urlValue.trim()}`
+ : "";
+ if (normalised !== (task.url || "")) {
+ updateTaskUrl(task.id, normalised);
+ }
+ };
+
+ return (
+ <>
+
+ {/* Resize handle */}
+
{
+ e.preventDefault();
+ isResizing.current = true;
+ document.body.style.cursor = 'col-resize';
+ document.body.style.userSelect = 'none';
+ }}
+ style={{
+ position: 'absolute',
+ left: 0,
+ top: 0,
+ bottom: 0,
+ width: '6px',
+ cursor: 'col-resize',
+ zIndex: 10,
+ }}
+ title="Drag to resize"
+ />
+
+ Notes: {task.title}
+
+
+
+ {/* URL field */}
+
+
+
+
setUrlValue(e.target.value)}
+ onBlur={handleUrlBlur}
+ onKeyDown={(e) => { if (e.key === "Enter") (e.target as HTMLInputElement).blur(); }}
+ placeholder="https://..."
+ style={{ flex: 1, fontSize: "0.82rem", border: "1px solid var(--weekly-border, #ddd)", borderRadius: "4px", padding: "5px 8px", background: "var(--weekly-bg, white)", color: "var(--weekly-text, #333)", outline: "none" }}
+ />
+ {urlValue && (
+
+ )}
+ {urlValue && (
+
+
+
+ )}
+
+
+
+ {/* Notes */}
+
+
+ updateTaskNotes(task.id, html)}
+ placeholder="Add details, notes, or links..."
+ minHeight="300px"
+ />
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/src/components/weekly/ProjectIcon.tsx b/src/components/weekly/ProjectIcon.tsx
new file mode 100644
index 0000000..d8ea27e
--- /dev/null
+++ b/src/components/weekly/ProjectIcon.tsx
@@ -0,0 +1,64 @@
+"use client";
+
+import React from "react";
+import MdiIcon from "@mdi/react";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import type { IconDefinition } from "@fortawesome/free-solid-svg-icons";
+import {
+ faBolt, faBook, faBriefcase, faBroom, faBuilding, faBullseye, faCamera, faCar, faCartShopping, faChartBar, faChartLine, faCheck, faClock, faCode, faCoins, faCube, faDumbbell, faEarthAmericas, faFilm, faFire, faFlask, faFolder, faGamepad, faGift, faGlobe, faGraduationCap, faHeart, faHouse, faLaptop, faLeaf, faLightbulb, faLock, faMicroscope, faMobileScreen, faMountain, faMusic, faPalette, faPaw, faPen, faPlane, faRocket, faStar, faTag, faUmbrella, faUsers, faUtensils, faWrench,
+} from "@fortawesome/free-solid-svg-icons";
+import { allIcons } from "../iconRegistry";
+
+// Ältere Projekte speichern Symbolnamen aus dieser Liste. Wanderte zusammen mit der
+// Komponente aus WeeklyView.tsx hierher, weil nur sie die Liste benutzt.
+const projectIconsGlobal: { name: string; icon: IconDefinition }[] = [
+ { name: "folder", icon: faFolder }, { name: "briefcase", icon: faBriefcase },
+ { name: "bullseye", icon: faBullseye }, { name: "rocket", icon: faRocket },
+ { name: "star", icon: faStar }, { name: "lightbulb", icon: faLightbulb },
+ { name: "fire", icon: faFire }, { name: "palette", icon: faPalette },
+ { name: "music", icon: faMusic }, { name: "mobile", icon: faMobileScreen },
+ { name: "laptop", icon: faLaptop }, { name: "globe", icon: faGlobe },
+ { name: "house", icon: faHouse }, { name: "building", icon: faBuilding },
+ { name: "chart-bar", icon: faChartBar }, { name: "chart-line", icon: faChartLine },
+ { name: "wrench", icon: faWrench }, { name: "bolt", icon: faBolt },
+ { name: "gamepad", icon: faGamepad }, { name: "pen", icon: faPen },
+ { name: "book", icon: faBook }, { name: "graduation-cap", icon: faGraduationCap },
+ { name: "flask", icon: faFlask }, { name: "microscope", icon: faMicroscope },
+ { name: "dumbbell", icon: faDumbbell }, { name: "utensils", icon: faUtensils },
+ { name: "plane", icon: faPlane }, { name: "leaf", icon: faLeaf },
+ { name: "heart", icon: faHeart }, { name: "cart-shopping", icon: faCartShopping },
+ { name: "coins", icon: faCoins }, { name: "gift", icon: faGift },
+ { name: "camera", icon: faCamera }, { name: "film", icon: faFilm },
+ { name: "broom", icon: faBroom }, { name: "paw", icon: faPaw },
+ { name: "earth", icon: faEarthAmericas }, { name: "lock", icon: faLock },
+ { name: "check", icon: faCheck }, { name: "code", icon: faCode },
+ { name: "cube", icon: faCube }, { name: "users", icon: faUsers },
+ { name: "car", icon: faCar }, { name: "mountain", icon: faMountain },
+ { name: "umbrella", icon: faUmbrella }, { name: "clock", icon: faClock },
+ { name: "tag", icon: faTag },
+];
+
+
+/**
+ * Zeigt das Symbol eines Projekts. Lag bis v1.119.1 in WeeklyView.tsx.
+ */
+export function ProjectIcon({ icon, size = 18, color }: { icon?: string | null; size?: number; color?: string }) {
+ if (!icon) return
;
+ // Normalise legacy "faHeart" → "heart" prefix style
+ const normalised = icon.startsWith("fa") && icon.length > 2 && icon[2] === icon[2].toUpperCase()
+ ? icon.slice(2, 3).toLowerCase() + icon.slice(3)
+ : icon;
+ // Check unified icon registry (FA + MDI)
+ const found = allIcons.find((i) => i.name === normalised || i.name === icon);
+ if (found) {
+ if (found.type === "fa") {
+ return
;
+ }
+ return
;
+ }
+ // Legacy: check old projectIconsGlobal for backwards compat
+ const legacy = projectIconsGlobal.find((i) => i.name === normalised || i.name === icon);
+ if (legacy) return
;
+ // Legacy emoji fallback
+ return
{icon};
+}
diff --git a/src/components/weekly/ProjectsSidebar.tsx b/src/components/weekly/ProjectsSidebar.tsx
new file mode 100644
index 0000000..70db22b
--- /dev/null
+++ b/src/components/weekly/ProjectsSidebar.tsx
@@ -0,0 +1,229 @@
+"use client";
+
+import React, { useState, useMemo } from "react";
+import dynamic from "next/dynamic";
+import { FolderOpen, X, Plus, Filter, Check, ChevronDown, ChevronRight, Pencil, Trash2 } from "lucide-react";
+
+const IconPicker = dynamic(() => import("../IconPicker"), { ssr: false });
+import { ProjectIcon } from "./ProjectIcon";
+import type { Task } from "@/lib/weekly/types";
+
+/**
+ * Seitenleiste zur Verwaltung der Projekte. Lag bis v1.119.1 in WeeklyView.tsx.
+ */
+export 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");
+ const [newProjectIcon, setNewProjectIcon] = useState("folder");
+ const [showNewIconPicker, setShowNewIconPicker] = useState(false);
+ const [editingId, setEditingId] = useState
(null);
+ const [editName, setEditName] = useState("");
+ const [editColor, setEditColor] = useState("");
+ const [editIcon, setEditIcon] = useState("folder");
+ const [showEditIconPicker, setShowEditIconPicker] = useState(false);
+ const [expandedProjects, setExpandedProjects] = useState>(new Set());
+
+ const t = {
+ projects: language === "de" ? "Projekte" : "Projects",
+ desc: language === "de" ? "Aufgaben mit farbcodierten Projekten organisieren" : "Organize tasks with color-coded projects",
+ noProjects: language === "de" ? "Noch keine Projekte erstellt" : "No projects yet",
+ addProject: language === "de" ? "Projekt hinzufügen" : "Add Project",
+ name: language === "de" ? "Name" : "Name",
+ cancel: language === "de" ? "Abbrechen" : "Cancel",
+ save: language === "de" ? "Speichern" : "Save",
+ color: language === "de" ? "Farbe" : "Color",
+ deleteConfirm: (name: string) => language === "de" ? `Projekt "${name}" löschen?` : `Delete project "${name}"?`,
+ 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();
+ 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 = () => {
+ if (!newProjectName.trim()) return;
+ fetch("/api/projects", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: newProjectName.trim(), color: newProjectColor, icon: newProjectIcon }) })
+ .then(() => { onProjectsChanged(); setNewProjectName(""); setNewProjectIcon("folder"); });
+ };
+
+ const updateProject = (id: string) => {
+ fetch("/api/projects", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id, name: editName, color: editColor, icon: editIcon }) })
+ .then(() => { onProjectsChanged(); setEditingId(null); });
+ };
+
+ const deleteProject = (id: string, name: string) => {
+ if (confirm(t.deleteConfirm(name))) {
+ fetch(`/api/projects?id=${id}`, { method: "DELETE" }).then(() => onProjectsChanged());
+ }
+ };
+
+ return (
+ <>
+
+
+ {/* Header */}
+
+
+ {t.projects}
+
+
+
+
+ {/* Add project form — at top */}
+
+
{t.addProject}
+
+
+
+ {showNewIconPicker && (
+
+ { setNewProjectIcon(name); setShowNewIconPicker(false); }} darkMode={darkMode} />
+
+ )}
+
+
setNewProjectColor(e.target.value)} style={{ width: "32px", height: "32px", border: "none", cursor: "pointer", padding: 0, borderRadius: "8px" }} />
+
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(); }} />
+
+
+
+
+ {/* Active filter banner */}
+ {activeProjectFilter && (
+
+
+
+ {t.filteringBy} {projects.find((p) => p.id === activeProjectFilter)?.name || ""}
+
+
+
+ )}
+
+ {/* Scrollable content */}
+
+
{t.desc}
+
+ {projects.length === 0 ? (
+
+ ) : (
+ projects.map((p) => {
+ const projectTasks = tasksByProject.get(p.id) || [];
+ const isExpanded = expandedProjects.has(p.id);
+ const isFiltered = activeProjectFilter === p.id;
+ return (
+
+
+ {editingId === p.id ? (
+
+
+
+
+ {showEditIconPicker && (
+
+ { setEditIcon(name); setShowEditIconPicker(false); }} darkMode={darkMode} />
+
+ )}
+
+
setEditName(e.target.value)} className="weekly-input" style={{ flex: 1, padding: "8px 12px", fontSize: "0.9rem" }} onKeyDown={(e) => { if (e.key === "Enter") updateProject(p.id); if (e.key === "Escape") setEditingId(null); }} autoFocus />
+
+
+
setEditColor(e.target.value)} style={{ width: "32px", height: "32px", border: "none", cursor: "pointer", padding: 0, borderRadius: "8px" }} />
+
{t.color}
+
+
+
+
+
+ ) : (
+ <>
+
+
+
+ {p.name}
+ {projectTasks.length}
+
+
+
+
+ >
+ )}
+
+ {isExpanded && editingId !== p.id && (
+
+ {projectTasks.length === 0 ? (
+
{t.noTasks}
+ ) : (
+ projectTasks.map((task) => (
+
+
+ {task.title}
+
+ ))
+ )}
+
+ )}
+
+ );
+ })
+ )}
+
+
+ >
+ );
+}