From 7e343b29e48f6651d405ba5bca5a371d5901b5ef Mon Sep 17 00:00:00 2001 From: mARTin Date: Tue, 17 Mar 2026 11:14:39 +0100 Subject: [PATCH] feat: improve project management with emoji icons and better UI - Add emoji icon picker for projects (40 icons to choose from) - Redesign project list in settings with color left-border, icon display - Show project icons on task cards, project picker, and kanban cards - Add New Project button to desktop header, tablet overflow menu, and mobile FAB - Remove duplicate unused /api/tasks/projects route v1.43.0 --- package.json | 2 +- src/app/api/tasks/projects/route.ts | 204 ---------------------- src/components/WeeklyView.tsx | 258 ++++++++++++++++++---------- 3 files changed, 173 insertions(+), 291 deletions(-) delete mode 100644 src/app/api/tasks/projects/route.ts diff --git a/package.json b/package.json index 5adcb2a..05b1cfa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "my-weekly-todo-list", - "version": "1.42.0", + "version": "1.43.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/app/api/tasks/projects/route.ts b/src/app/api/tasks/projects/route.ts deleted file mode 100644 index ea62be9..0000000 --- a/src/app/api/tasks/projects/route.ts +++ /dev/null @@ -1,204 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { getServerSession } from 'next-auth'; -import { authOptions } from '@/lib/auth'; -import { prisma } from '@/lib/prisma'; - -// GET - List all projects for authenticated user -export async function GET(request: NextRequest) { - try { - const session = await getServerSession(authOptions); - - if (!session?.user) { - return NextResponse.json( - { error: 'Unauthorized' }, - { status: 401 } - ); - } - - const userId = (session.user as any).id; - - const projects = await prisma.project.findMany({ - where: { userId }, - orderBy: { order: 'asc' }, - include: { - _count: { - select: { tasks: true }, - }, - }, - }); - - return NextResponse.json({ projects }); - } catch (error) { - console.error('Error fetching projects:', error); - return NextResponse.json( - { error: 'Failed to fetch projects' }, - { status: 500 } - ); - } -} - -// POST - Create a new project -export async function POST(request: NextRequest) { - try { - const session = await getServerSession(authOptions); - - if (!session?.user) { - return NextResponse.json( - { error: 'Unauthorized' }, - { status: 401 } - ); - } - - const userId = (session.user as any).id; - const body = await request.json(); - - const { name, icon, color, description } = body; - - if (!name) { - return NextResponse.json( - { error: 'Project name is required' }, - { status: 400 } - ); - } - - // Set order to be after the last project - const lastProject = await prisma.project.findFirst({ - where: { userId }, - orderBy: { order: 'desc' }, - select: { order: true }, - }); - - const project = await prisma.project.create({ - data: { - name, - icon: icon || null, - color: color || null, - description: description || null, - order: (lastProject?.order ?? -1) + 1, - userId, - }, - }); - - return NextResponse.json({ project }); - } catch (error) { - console.error('Error creating project:', error); - return NextResponse.json( - { error: 'Failed to create project' }, - { status: 500 } - ); - } -} - -// PATCH - Update a project -export async function PATCH(request: NextRequest) { - try { - const session = await getServerSession(authOptions); - - if (!session?.user) { - return NextResponse.json( - { error: 'Unauthorized' }, - { status: 401 } - ); - } - - const userId = (session.user as any).id; - const { searchParams } = new URL(request.url); - const id = searchParams.get('id'); - - if (!id) { - return NextResponse.json( - { error: 'Project ID is required' }, - { status: 400 } - ); - } - - // Validate ownership - const existingProject = await prisma.project.findFirst({ - where: { id, userId }, - }); - - if (!existingProject) { - return NextResponse.json( - { error: 'Project not found' }, - { status: 404 } - ); - } - - const body = await request.json(); - const { name, icon, color, description, order } = body; - - const project = await prisma.project.update({ - where: { id }, - data: { - ...(name !== undefined && { name }), - ...(icon !== undefined && { icon: icon || null }), - ...(color !== undefined && { color: color || null }), - ...(description !== undefined && { description: description || null }), - ...(order !== undefined && { order: parseInt(order) }), - }, - }); - - return NextResponse.json({ project }); - } catch (error) { - console.error('Error updating project:', error); - return NextResponse.json( - { error: 'Failed to update project' }, - { status: 500 } - ); - } -} - -// DELETE - Delete a project (tasks remain, their projectId becomes null) -export async function DELETE(request: NextRequest) { - try { - const session = await getServerSession(authOptions); - - if (!session?.user) { - return NextResponse.json( - { error: 'Unauthorized' }, - { status: 401 } - ); - } - - const userId = (session.user as any).id; - const { searchParams } = new URL(request.url); - const id = searchParams.get('id'); - - if (!id) { - return NextResponse.json( - { error: 'Project ID is required' }, - { status: 400 } - ); - } - - // Validate ownership - const existingProject = await prisma.project.findFirst({ - where: { id, userId }, - }); - - if (!existingProject) { - return NextResponse.json( - { error: 'Project not found' }, - { status: 404 } - ); - } - - // Nullify projectId on all tasks belonging to this project - await prisma.task.updateMany({ - where: { projectId: id }, - data: { projectId: null }, - }); - - await prisma.project.delete({ - where: { id }, - }); - - return NextResponse.json({ message: 'Project deleted' }); - } catch (error) { - console.error('Error deleting project:', error); - return NextResponse.json( - { error: 'Failed to delete project' }, - { status: 500 } - ); - } -} diff --git a/src/components/WeeklyView.tsx b/src/components/WeeklyView.tsx index 7bf76f5..7e60be3 100644 --- a/src/components/WeeklyView.tsx +++ b/src/components/WeeklyView.tsx @@ -67,6 +67,7 @@ import { CalendarDays, ListTodo, Filter, + Pencil, } from "lucide-react"; // Types @@ -6074,13 +6075,15 @@ export default function WeeklyView() { + )} - {/* Recurring Tasks โ€” desktop only */} + {/* Desktop only: Recurring Tasks + New Project */} + {/* User Menu */} {language === "de" ? "Suche" : "Search"} + )} {showProjectPicker && (
@@ -9144,7 +9155,8 @@ function TaskItem({ setShowProjectPicker(false); }} > - + {p.icon || "๐Ÿ“"} + {p.name} ))} @@ -9822,9 +9834,20 @@ function SettingsSidebar({ ); const [newProjectName, setNewProjectName] = useState(""); const [newProjectColor, setNewProjectColor] = useState("#3b82f6"); + const [newProjectIcon, setNewProjectIcon] = useState("๐Ÿ“"); + const [showNewProjectIconPicker, setShowNewProjectIconPicker] = useState(false); const [editingProjectId, setEditingProjectId] = useState(null); const [editProjectName, setEditProjectName] = useState(""); const [editProjectColor, setEditProjectColor] = useState(""); + const [editProjectIcon, setEditProjectIcon] = useState(""); + const [showEditProjectIconPicker, setShowEditProjectIconPicker] = useState(false); + + const projectEmojis = [ + "๐Ÿ“", "๐Ÿ“‚", "๐Ÿ’ผ", "๐ŸŽฏ", "๐Ÿš€", "โญ", "๐Ÿ’ก", "๐Ÿ”ฅ", "๐ŸŽจ", "๐ŸŽต", + "๐Ÿ“ฑ", "๐Ÿ’ป", "๐ŸŒ", "๐Ÿ ", "๐Ÿข", "๐Ÿ“Š", "๐Ÿ“ˆ", "๐Ÿ”ง", "โšก", "๐ŸŽฎ", + "๐Ÿ“", "๐Ÿ“–", "๐ŸŽ“", "๐Ÿงช", "๐Ÿ”ฌ", "๐Ÿ‹๏ธ", "๐Ÿฝ๏ธ", "โœˆ๏ธ", "๐ŸŒฟ", "โค๏ธ", + "๐Ÿ›’", "๐Ÿ’ฐ", "๐ŸŽ", "๐Ÿ“ธ", "๐ŸŽฌ", "๐Ÿงน", "๐Ÿพ", "๐ŸŒ", "๐Ÿ”’", "โœ…", + ]; // Fetch lists when the calendar tab is selected useEffect(() => { @@ -11061,57 +11084,93 @@ function SettingsSidebar({ )}
{projects.map((p) => ( -
+
{editingProjectId === p.id ? ( - <> - setEditProjectColor(e.target.value)} - style={{ width: "24px", height: "24px", border: "none", cursor: "pointer", padding: 0 }} - /> - setEditProjectName(e.target.value)} - className="weekly-input" - style={{ flex: 1, padding: "4px 8px", fontSize: "0.85rem" }} - onKeyDown={(e) => { - if (e.key === "Enter") { +
+
+
+ + {showEditProjectIconPicker && ( +
+
+ {projectEmojis.map((emoji) => ( + + ))} +
+
+ )} +
+ setEditProjectName(e.target.value)} + className="weekly-input" + style={{ flex: 1, padding: "6px 10px", fontSize: "0.85rem" }} + onKeyDown={(e) => { + if (e.key === "Enter") { + fetch("/api/projects", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ id: p.id, name: editProjectName, color: editProjectColor, icon: editProjectIcon }), + }).then(() => { onProjectsChanged(); setEditingProjectId(null); }); + } + if (e.key === "Escape") setEditingProjectId(null); + }} + autoFocus + /> +
+
+ setEditProjectColor(e.target.value)} + style={{ width: "28px", height: "28px", border: "none", cursor: "pointer", padding: 0, borderRadius: "6px" }} + /> + {profile.language === "de" ? "Farbe" : "Color"} +
+ + - + }} + className="weekly-btn-primary" + style={{ padding: "4px 12px", fontSize: "0.8rem" }} + > + + +
+
) : ( <> - - {p.name} + {p.icon || "๐Ÿ“"} + {p.name} )}
))}
-
- setNewProjectColor(e.target.value)} - style={{ width: "28px", height: "28px", border: "none", cursor: "pointer", padding: 0 }} - /> - setNewProjectName(e.target.value)} - placeholder={t.projectName} - className="weekly-input" - style={{ flex: 1, padding: "6px 10px", fontSize: "0.85rem" }} - onKeyDown={(e) => { - if (e.key === "Enter" && newProjectName.trim()) { + {/* Add new project */} +
+
+
+ + {showNewProjectIconPicker && ( +
+
+ {projectEmojis.map((emoji) => ( + + ))} +
+
+ )} +
+ setNewProjectColor(e.target.value)} + style={{ width: "28px", height: "28px", border: "none", cursor: "pointer", padding: 0, borderRadius: "6px" }} + /> + setNewProjectName(e.target.value)} + placeholder={t.projectName} + className="weekly-input" + style={{ flex: 1, padding: "6px 10px", fontSize: "0.85rem" }} + onKeyDown={(e) => { + if (e.key === "Enter" && newProjectName.trim()) { + fetch("/api/projects", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name: newProjectName.trim(), color: newProjectColor, icon: newProjectIcon }), + }).then(() => { onProjectsChanged(); setNewProjectName(""); setNewProjectIcon("๐Ÿ“"); }); + } + }} + /> + + body: JSON.stringify({ name: newProjectName.trim(), color: newProjectColor, icon: newProjectIcon }), + }).then(() => { onProjectsChanged(); setNewProjectName(""); setNewProjectIcon("๐Ÿ“"); }); + }} + className="weekly-btn-primary" + style={{ padding: "6px 12px", fontSize: "0.8rem", whiteSpace: "nowrap" }} + > + {t.addProject} + +