From 329f082ecf55c79e448e5edcf7a93143a856be32 Mon Sep 17 00:00:00 2001 From: mARTin Date: Wed, 11 Mar 2026 20:22:32 +0100 Subject: [PATCH] feat: view switcher improvements, header month option, kanban date/time, mobile fixes - Swap simple/calendar icons in header view switcher - Add calendar view button to header view switcher - Reorder header: view switcher first, then days, hours, slot duration - Add header display setting: calendar week (KW) or month name - Show kanban stage color border on GridTaskBlock (week/simple views) - Show date, weekday, and time on kanban cards - Remove task swipe gestures on mobile (use container swipe for day nav) - Fix mobile landscape 3-day grid overflow - Add headerDisplay to schema, API, and settings UI v1.30.0 Co-Authored-By: Claude Opus 4.6 --- package.json | 2 +- prisma/schema.prisma | 1 + src/app/api/user/profile/route.ts | 5 +- src/app/globals.css | 15 +++ src/components/GridTaskBlock.tsx | 15 ++- src/components/WeeklyView.tsx | 203 ++++++++++++++++-------------- 6 files changed, 139 insertions(+), 102 deletions(-) diff --git a/package.json b/package.json index 658aa1d..7286c3e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "my-weekly-todo-list", - "version": "1.29.0", + "version": "1.30.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/prisma/schema.prisma b/prisma/schema.prisma index 271a28e..427d9fe 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -76,6 +76,7 @@ model User { mobileDateLayout String @default("below") dateAlignment String @default("center") dateVerticalAlign String? @default("middle") + headerDisplay String? @default("kw") hourLabelFormat String @default("short") showSubHourSlots Boolean @default(true) allDayPosition String @default("above") diff --git a/src/app/api/user/profile/route.ts b/src/app/api/user/profile/route.ts index 5decba2..1acc463 100644 --- a/src/app/api/user/profile/route.ts +++ b/src/app/api/user/profile/route.ts @@ -45,6 +45,7 @@ export async function GET(request: NextRequest) { dateLayout: true, mobileDateLayout: true, dateVerticalAlign: true, + headerDisplay: true, headlineFont: true, headlineFontSize: true, headlineFontWeight: true, @@ -127,7 +128,7 @@ export async function PATCH(request: NextRequest) { weekdayColor, dateColor, taskColor, todayHighlightColor, pastDayColor, goalFallbackType, goalDefaultSentence, goalFontFamily, goalFontSize, goalFontWeight, goalScope, - dateLayout, mobileDateLayout, dateVerticalAlign, + dateLayout, mobileDateLayout, dateVerticalAlign, headerDisplay, hourLabelFormat, showSubHourSlots, allDayPosition, cwFontFamily, cwFontSize, cwFontWeight, cwColor, yearFontFamily, yearFontSize, yearFontWeight, yearColor, @@ -192,6 +193,7 @@ export async function PATCH(request: NextRequest) { ...(dateLayout !== undefined && { dateLayout }), ...(mobileDateLayout !== undefined && { mobileDateLayout }), ...(dateVerticalAlign !== undefined && { dateVerticalAlign }), + ...(headerDisplay !== undefined && { headerDisplay }), ...(hourLabelFormat !== undefined && { hourLabelFormat }), ...(showSubHourSlots !== undefined && { showSubHourSlots }), ...(allDayPosition !== undefined && { allDayPosition }), @@ -282,6 +284,7 @@ export async function PATCH(request: NextRequest) { dateLayout: true, mobileDateLayout: true, dateVerticalAlign: true, + headerDisplay: true, cwFontFamily: true, cwFontSize: true, cwFontWeight: true, diff --git a/src/app/globals.css b/src/app/globals.css index c123966..327259f 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1855,6 +1855,11 @@ h3 { padding-left: 20px; } +.kanban-card-time { + font-weight: 600; + color: var(--weekly-text, #555); +} + .kanban-card-project { font-size: 0.7rem; margin-top: 2px; @@ -2233,6 +2238,16 @@ h3 { .side-nav { display: none; } + + /* Ensure grid respects viewDays columns on mobile landscape */ + .time-grid-wrapper { + overflow: visible; + min-width: 0; + } + .time-grid-wrapper .weekly-days-grid { + overflow: visible; + min-width: 0; + } } /* === Phone portrait (≤ 480px): 1-day view === */ diff --git a/src/components/GridTaskBlock.tsx b/src/components/GridTaskBlock.tsx index a022d41..6e899db 100644 --- a/src/components/GridTaskBlock.tsx +++ b/src/components/GridTaskBlock.tsx @@ -3,7 +3,7 @@ import { Repeat, Circle, X } from "lucide-react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faGoogle, faMicrosoft, faApple } from "@fortawesome/free-brands-svg-icons"; import { faServer } from "@fortawesome/free-solid-svg-icons"; -import { Task } from "./WeeklyView"; +import { Task, KanbanStage } from "./WeeklyView"; interface GridTaskBlockProps { task: Task; @@ -35,6 +35,7 @@ interface GridTaskBlockProps { showTaskCheckboxes?: boolean; projects?: any[]; onProjectAssign?: (taskId: string, projectId: string | null) => void; + kanbanStages?: KanbanStage[]; } export function GridTaskBlock({ @@ -66,7 +67,8 @@ export function GridTaskBlock({ workingHoursStart, showTaskCheckboxes, projects, - onProjectAssign + onProjectAssign, + kanbanStages = [] }: GridTaskBlockProps) { const [isNotesOpen, setIsNotesOpen] = useState(false); const [notesValue, setNotesValue] = useState(task.markdownContent || ""); @@ -198,9 +200,14 @@ export function GridTaskBlock({ zIndex: isResizing || isNotesOpen || isSubTasksOpen ? 10 : 5, background: (isNotesOpen || isSubTasksOpen || isResizing) ? (darkMode ? "#2a2a2a" : "#ffffff") : "transparent", border: (isNotesOpen || isSubTasksOpen || isResizing) ? `1px solid ${darkMode ? "#404040" : "#e0e0e0"}` : "none", - borderLeft: task.project?.color ? `4px solid ${task.project.color}` : (isNotesOpen || isSubTasksOpen || isResizing) ? `1px solid ${darkMode ? "#404040" : "#e0e0e0"}` : "none", + borderLeft: (() => { + const stageColor = task.kanbanStage ? kanbanStages.find(s => s.id === task.kanbanStage)?.color : null; + if (stageColor) return `4px solid ${stageColor}`; + if (task.project?.color) return `4px solid ${task.project.color}`; + return (isNotesOpen || isSubTasksOpen || isResizing) ? `1px solid ${darkMode ? "#404040" : "#e0e0e0"}` : "none"; + })(), borderRadius: (isNotesOpen || isSubTasksOpen || isResizing) ? "4px" : "0", - padding: task.project?.color ? "2px 4px 2px 8px" : "2px 4px", + padding: (task.kanbanStage && kanbanStages.some(s => s.id === task.kanbanStage)) || task.project?.color ? "2px 4px 2px 8px" : "2px 4px", boxShadow: (isNotesOpen || isSubTasksOpen || isResizing) ? "0 1px 3px rgba(0,0,0,0.05)" : "none", display: "flex", flexDirection: "column", diff --git a/src/components/WeeklyView.tsx b/src/components/WeeklyView.tsx index dfafde5..190f100 100644 --- a/src/components/WeeklyView.tsx +++ b/src/components/WeeklyView.tsx @@ -258,6 +258,9 @@ const translations: Record = { addStage: "Add stage", stageName: "Stage name", noStage: "No stage", + headerDisplay: "Header Display", + headerDisplayKW: "Calendar Week (KW)", + headerDisplayMonth: "Month Name", language: "Language", dateFormat: "Date Format", timeFormat: "Time Format", @@ -461,6 +464,9 @@ const translations: Record = { addStage: "Phase hinzufügen", stageName: "Phasenname", noStage: "Keine Phase", + headerDisplay: "Kopfzeile", + headerDisplayKW: "Kalenderwoche (KW)", + headerDisplayMonth: "Monatsname", listView: "Liste", notes: "Notizen", notesSidebar: "Notizen-Seitenleiste", @@ -667,6 +673,9 @@ const translations: Record = { addStage: "Ajouter une étape", stageName: "Nom de l'étape", noStage: "Aucune étape", + headerDisplay: "Affichage en-tête", + headerDisplayKW: "Semaine calendaire (KW)", + headerDisplayMonth: "Nom du mois", language: "Langue", dateFormat: "Format de date", timeFormat: "Format d'heure", @@ -871,6 +880,9 @@ const translations: Record = { addStage: "Añadir etapa", stageName: "Nombre de etapa", noStage: "Sin etapa", + headerDisplay: "Visualización de encabezado", + headerDisplayKW: "Semana calendario (KW)", + headerDisplayMonth: "Nombre del mes", language: "Idioma", dateFormat: "Formato de fecha", timeFormat: "Formato de hora", @@ -1075,6 +1087,9 @@ const translations: Record = { addStage: "Aggiungi fase", stageName: "Nome fase", noStage: "Nessuna fase", + headerDisplay: "Visualizzazione intestazione", + headerDisplayKW: "Settimana calendario (KW)", + headerDisplayMonth: "Nome del mese", language: "Lingua", dateFormat: "Formato data", timeFormat: "Formato ora", @@ -1756,6 +1771,7 @@ export default function WeeklyView() { weekdayCase?: "normal" | "capitalize" | "uppercase"; customWeekdayNames?: string; dateVerticalAlign?: "top" | "middle" | "bottom"; + headerDisplay?: "kw" | "month"; }>({ name: session?.user?.name || "", email: session?.user?.email || "", @@ -1903,7 +1919,7 @@ export default function WeeklyView() { const [isSearchOpen, setIsSearchOpen] = useState(false); const [isRecurringTasksOpen, setIsRecurringTasksOpen] = useState(false); const [showDatePicker, setShowDatePicker] = useState(false); - const datePickerBtnRef = useRef(null); + const datePickerBtnRef = useRef(null); const [showQuickSettings, setShowQuickSettings] = useState(false); const [focusTimerDuration, setFocusTimerDuration] = useState(25); @@ -5454,29 +5470,39 @@ export default function WeeklyView() { {/* Desktop Header: Left, Center, Right */}
- {/* LEFT SECTION: Slot Duration & Days to Show */} + {/* LEFT SECTION: View Switcher, Days, Hours, Slot Duration */}
- {/* Slot Duration */} - {showTimeGrid && ( -
+ - ))} -
- )} + + + + + +
{/* Days to Show */}
)} - {/* View Switcher */} -
- - - -
+ + {[15, 30, 60].map((duration) => ( + + ))} +
+ )} @@ -5573,16 +5596,13 @@ export default function WeeklyView() {
{/* Week & Year */}
- {/* Date Picker Toggle - Moved to front */} -
- + {/* Clickable Week & Year */} +
setShowDatePicker(!showDatePicker)} + title="Jump to date" + > {showDatePicker && ( )} -
- - {/* Clickable Week & Year */} -
setShowDatePicker(!showDatePicker)} - title="Jump to date" - > - KW {getWeekNumber(getCWReferenceDate(getVisibleDays())).toString().padStart(2, "0")} + {profile.headerDisplay === "month" + ? getCWReferenceDate(getVisibleDays()).toLocaleDateString(language, { month: "long" }) + : `KW ${getWeekNumber(getCWReferenceDate(getVisibleDays())).toString().padStart(2, "0")}` + } |
- {task.scheduledDate && ( + {(task.scheduledDate || task.startTime) && (
- {new Date(task.scheduledDate).toLocaleDateString(language, { month: "short", day: "numeric" })} + {task.scheduledDate && new Date(task.scheduledDate).toLocaleDateString(language, { weekday: "short", month: "short", day: "numeric" })} + {task.startTime && {task.startTime}}
)} {task.project && ( @@ -6410,6 +6426,7 @@ export default function WeeklyView() { showTaskCheckboxes={profile.showTaskCheckboxes} projects={projects} onProjectAssign={assignProject} + kanbanStages={kanbanStages} /> ))} {visibleSlots.map((slot) => { @@ -8365,36 +8382,8 @@ function TaskItem({ onEdit(); } }} - onTouchStart={(e) => { - const touch = e.touches[0]; - swipeTouchStart.current = { x: touch.clientX, y: touch.clientY, swiping: false }; - setSwipeX(0); - }} - onTouchMove={(e) => { - const touch = e.touches[0]; - const dx = touch.clientX - swipeTouchStart.current.x; - const dy = touch.clientY - swipeTouchStart.current.y; - // Only swipe if horizontal dominant and past 10px threshold - if (!swipeTouchStart.current.swiping && Math.abs(dx) > 10 && Math.abs(dx) > Math.abs(dy) * 1.5) { - swipeTouchStart.current.swiping = true; - } - if (swipeTouchStart.current.swiping) { - e.preventDefault(); - setSwipeX(dx); - } - }} - onTouchEnd={() => { - if (Math.abs(swipeX) > 80) { - if (swipeX > 0) { - // Swipe right: toggle complete - onToggle(); - } else { - // Swipe left: delete - onDelete(); - } - } - setSwipeX(0); - swipeTouchStart.current.swiping = false; + onTouchStart={() => { + // No task-level swipe — container handles day navigation }} > {/* Swipe indicators */} @@ -9565,6 +9554,7 @@ function SettingsSidebar({ weekdayCase?: "normal" | "capitalize" | "uppercase"; customWeekdayNames?: string; dateVerticalAlign?: "top" | "middle" | "bottom"; + headerDisplay?: "kw" | "month"; }>({ name: "", email: "", @@ -10680,6 +10670,27 @@ function SettingsSidebar({
+ {/* Header Display */} +
+ +
+ + +
+
+ {/* Kanban Stages Settings */}