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 <noreply@anthropic.com>
This commit is contained in:
parent
8703239f0f
commit
329f082ecf
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "my-weekly-todo-list",
|
"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",
|
"description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@ -76,6 +76,7 @@ model User {
|
|||||||
mobileDateLayout String @default("below")
|
mobileDateLayout String @default("below")
|
||||||
dateAlignment String @default("center")
|
dateAlignment String @default("center")
|
||||||
dateVerticalAlign String? @default("middle")
|
dateVerticalAlign String? @default("middle")
|
||||||
|
headerDisplay String? @default("kw")
|
||||||
hourLabelFormat String @default("short")
|
hourLabelFormat String @default("short")
|
||||||
showSubHourSlots Boolean @default(true)
|
showSubHourSlots Boolean @default(true)
|
||||||
allDayPosition String @default("above")
|
allDayPosition String @default("above")
|
||||||
|
|||||||
@ -45,6 +45,7 @@ export async function GET(request: NextRequest) {
|
|||||||
dateLayout: true,
|
dateLayout: true,
|
||||||
mobileDateLayout: true,
|
mobileDateLayout: true,
|
||||||
dateVerticalAlign: true,
|
dateVerticalAlign: true,
|
||||||
|
headerDisplay: true,
|
||||||
headlineFont: true,
|
headlineFont: true,
|
||||||
headlineFontSize: true,
|
headlineFontSize: true,
|
||||||
headlineFontWeight: true,
|
headlineFontWeight: true,
|
||||||
@ -127,7 +128,7 @@ export async function PATCH(request: NextRequest) {
|
|||||||
weekdayColor, dateColor, taskColor, todayHighlightColor,
|
weekdayColor, dateColor, taskColor, todayHighlightColor,
|
||||||
pastDayColor, goalFallbackType, goalDefaultSentence,
|
pastDayColor, goalFallbackType, goalDefaultSentence,
|
||||||
goalFontFamily, goalFontSize, goalFontWeight, goalScope,
|
goalFontFamily, goalFontSize, goalFontWeight, goalScope,
|
||||||
dateLayout, mobileDateLayout, dateVerticalAlign,
|
dateLayout, mobileDateLayout, dateVerticalAlign, headerDisplay,
|
||||||
hourLabelFormat, showSubHourSlots, allDayPosition,
|
hourLabelFormat, showSubHourSlots, allDayPosition,
|
||||||
cwFontFamily, cwFontSize, cwFontWeight, cwColor,
|
cwFontFamily, cwFontSize, cwFontWeight, cwColor,
|
||||||
yearFontFamily, yearFontSize, yearFontWeight, yearColor,
|
yearFontFamily, yearFontSize, yearFontWeight, yearColor,
|
||||||
@ -192,6 +193,7 @@ export async function PATCH(request: NextRequest) {
|
|||||||
...(dateLayout !== undefined && { dateLayout }),
|
...(dateLayout !== undefined && { dateLayout }),
|
||||||
...(mobileDateLayout !== undefined && { mobileDateLayout }),
|
...(mobileDateLayout !== undefined && { mobileDateLayout }),
|
||||||
...(dateVerticalAlign !== undefined && { dateVerticalAlign }),
|
...(dateVerticalAlign !== undefined && { dateVerticalAlign }),
|
||||||
|
...(headerDisplay !== undefined && { headerDisplay }),
|
||||||
...(hourLabelFormat !== undefined && { hourLabelFormat }),
|
...(hourLabelFormat !== undefined && { hourLabelFormat }),
|
||||||
...(showSubHourSlots !== undefined && { showSubHourSlots }),
|
...(showSubHourSlots !== undefined && { showSubHourSlots }),
|
||||||
...(allDayPosition !== undefined && { allDayPosition }),
|
...(allDayPosition !== undefined && { allDayPosition }),
|
||||||
@ -282,6 +284,7 @@ export async function PATCH(request: NextRequest) {
|
|||||||
dateLayout: true,
|
dateLayout: true,
|
||||||
mobileDateLayout: true,
|
mobileDateLayout: true,
|
||||||
dateVerticalAlign: true,
|
dateVerticalAlign: true,
|
||||||
|
headerDisplay: true,
|
||||||
cwFontFamily: true,
|
cwFontFamily: true,
|
||||||
cwFontSize: true,
|
cwFontSize: true,
|
||||||
cwFontWeight: true,
|
cwFontWeight: true,
|
||||||
|
|||||||
@ -1855,6 +1855,11 @@ h3 {
|
|||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.kanban-card-time {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--weekly-text, #555);
|
||||||
|
}
|
||||||
|
|
||||||
.kanban-card-project {
|
.kanban-card-project {
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
@ -2233,6 +2238,16 @@ h3 {
|
|||||||
.side-nav {
|
.side-nav {
|
||||||
display: none;
|
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 === */
|
/* === Phone portrait (≤ 480px): 1-day view === */
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { Repeat, Circle, X } from "lucide-react";
|
|||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { faGoogle, faMicrosoft, faApple } from "@fortawesome/free-brands-svg-icons";
|
import { faGoogle, faMicrosoft, faApple } from "@fortawesome/free-brands-svg-icons";
|
||||||
import { faServer } from "@fortawesome/free-solid-svg-icons";
|
import { faServer } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { Task } from "./WeeklyView";
|
import { Task, KanbanStage } from "./WeeklyView";
|
||||||
|
|
||||||
interface GridTaskBlockProps {
|
interface GridTaskBlockProps {
|
||||||
task: Task;
|
task: Task;
|
||||||
@ -35,6 +35,7 @@ interface GridTaskBlockProps {
|
|||||||
showTaskCheckboxes?: boolean;
|
showTaskCheckboxes?: boolean;
|
||||||
projects?: any[];
|
projects?: any[];
|
||||||
onProjectAssign?: (taskId: string, projectId: string | null) => void;
|
onProjectAssign?: (taskId: string, projectId: string | null) => void;
|
||||||
|
kanbanStages?: KanbanStage[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GridTaskBlock({
|
export function GridTaskBlock({
|
||||||
@ -66,7 +67,8 @@ export function GridTaskBlock({
|
|||||||
workingHoursStart,
|
workingHoursStart,
|
||||||
showTaskCheckboxes,
|
showTaskCheckboxes,
|
||||||
projects,
|
projects,
|
||||||
onProjectAssign
|
onProjectAssign,
|
||||||
|
kanbanStages = []
|
||||||
}: GridTaskBlockProps) {
|
}: GridTaskBlockProps) {
|
||||||
const [isNotesOpen, setIsNotesOpen] = useState(false);
|
const [isNotesOpen, setIsNotesOpen] = useState(false);
|
||||||
const [notesValue, setNotesValue] = useState(task.markdownContent || "");
|
const [notesValue, setNotesValue] = useState(task.markdownContent || "");
|
||||||
@ -198,9 +200,14 @@ export function GridTaskBlock({
|
|||||||
zIndex: isResizing || isNotesOpen || isSubTasksOpen ? 10 : 5,
|
zIndex: isResizing || isNotesOpen || isSubTasksOpen ? 10 : 5,
|
||||||
background: (isNotesOpen || isSubTasksOpen || isResizing) ? (darkMode ? "#2a2a2a" : "#ffffff") : "transparent",
|
background: (isNotesOpen || isSubTasksOpen || isResizing) ? (darkMode ? "#2a2a2a" : "#ffffff") : "transparent",
|
||||||
border: (isNotesOpen || isSubTasksOpen || isResizing) ? `1px solid ${darkMode ? "#404040" : "#e0e0e0"}` : "none",
|
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",
|
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",
|
boxShadow: (isNotesOpen || isSubTasksOpen || isResizing) ? "0 1px 3px rgba(0,0,0,0.05)" : "none",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
|
|||||||
@ -258,6 +258,9 @@ const translations: Record<string, any> = {
|
|||||||
addStage: "Add stage",
|
addStage: "Add stage",
|
||||||
stageName: "Stage name",
|
stageName: "Stage name",
|
||||||
noStage: "No stage",
|
noStage: "No stage",
|
||||||
|
headerDisplay: "Header Display",
|
||||||
|
headerDisplayKW: "Calendar Week (KW)",
|
||||||
|
headerDisplayMonth: "Month Name",
|
||||||
language: "Language",
|
language: "Language",
|
||||||
dateFormat: "Date Format",
|
dateFormat: "Date Format",
|
||||||
timeFormat: "Time Format",
|
timeFormat: "Time Format",
|
||||||
@ -461,6 +464,9 @@ const translations: Record<string, any> = {
|
|||||||
addStage: "Phase hinzufügen",
|
addStage: "Phase hinzufügen",
|
||||||
stageName: "Phasenname",
|
stageName: "Phasenname",
|
||||||
noStage: "Keine Phase",
|
noStage: "Keine Phase",
|
||||||
|
headerDisplay: "Kopfzeile",
|
||||||
|
headerDisplayKW: "Kalenderwoche (KW)",
|
||||||
|
headerDisplayMonth: "Monatsname",
|
||||||
listView: "Liste",
|
listView: "Liste",
|
||||||
notes: "Notizen",
|
notes: "Notizen",
|
||||||
notesSidebar: "Notizen-Seitenleiste",
|
notesSidebar: "Notizen-Seitenleiste",
|
||||||
@ -667,6 +673,9 @@ const translations: Record<string, any> = {
|
|||||||
addStage: "Ajouter une étape",
|
addStage: "Ajouter une étape",
|
||||||
stageName: "Nom de l'étape",
|
stageName: "Nom de l'étape",
|
||||||
noStage: "Aucune étape",
|
noStage: "Aucune étape",
|
||||||
|
headerDisplay: "Affichage en-tête",
|
||||||
|
headerDisplayKW: "Semaine calendaire (KW)",
|
||||||
|
headerDisplayMonth: "Nom du mois",
|
||||||
language: "Langue",
|
language: "Langue",
|
||||||
dateFormat: "Format de date",
|
dateFormat: "Format de date",
|
||||||
timeFormat: "Format d'heure",
|
timeFormat: "Format d'heure",
|
||||||
@ -871,6 +880,9 @@ const translations: Record<string, any> = {
|
|||||||
addStage: "Añadir etapa",
|
addStage: "Añadir etapa",
|
||||||
stageName: "Nombre de etapa",
|
stageName: "Nombre de etapa",
|
||||||
noStage: "Sin etapa",
|
noStage: "Sin etapa",
|
||||||
|
headerDisplay: "Visualización de encabezado",
|
||||||
|
headerDisplayKW: "Semana calendario (KW)",
|
||||||
|
headerDisplayMonth: "Nombre del mes",
|
||||||
language: "Idioma",
|
language: "Idioma",
|
||||||
dateFormat: "Formato de fecha",
|
dateFormat: "Formato de fecha",
|
||||||
timeFormat: "Formato de hora",
|
timeFormat: "Formato de hora",
|
||||||
@ -1075,6 +1087,9 @@ const translations: Record<string, any> = {
|
|||||||
addStage: "Aggiungi fase",
|
addStage: "Aggiungi fase",
|
||||||
stageName: "Nome fase",
|
stageName: "Nome fase",
|
||||||
noStage: "Nessuna fase",
|
noStage: "Nessuna fase",
|
||||||
|
headerDisplay: "Visualizzazione intestazione",
|
||||||
|
headerDisplayKW: "Settimana calendario (KW)",
|
||||||
|
headerDisplayMonth: "Nome del mese",
|
||||||
language: "Lingua",
|
language: "Lingua",
|
||||||
dateFormat: "Formato data",
|
dateFormat: "Formato data",
|
||||||
timeFormat: "Formato ora",
|
timeFormat: "Formato ora",
|
||||||
@ -1756,6 +1771,7 @@ export default function WeeklyView() {
|
|||||||
weekdayCase?: "normal" | "capitalize" | "uppercase";
|
weekdayCase?: "normal" | "capitalize" | "uppercase";
|
||||||
customWeekdayNames?: string;
|
customWeekdayNames?: string;
|
||||||
dateVerticalAlign?: "top" | "middle" | "bottom";
|
dateVerticalAlign?: "top" | "middle" | "bottom";
|
||||||
|
headerDisplay?: "kw" | "month";
|
||||||
}>({
|
}>({
|
||||||
name: session?.user?.name || "",
|
name: session?.user?.name || "",
|
||||||
email: session?.user?.email || "",
|
email: session?.user?.email || "",
|
||||||
@ -1903,7 +1919,7 @@ export default function WeeklyView() {
|
|||||||
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
||||||
const [isRecurringTasksOpen, setIsRecurringTasksOpen] = useState(false);
|
const [isRecurringTasksOpen, setIsRecurringTasksOpen] = useState(false);
|
||||||
const [showDatePicker, setShowDatePicker] = useState(false);
|
const [showDatePicker, setShowDatePicker] = useState(false);
|
||||||
const datePickerBtnRef = useRef<HTMLButtonElement>(null);
|
const datePickerBtnRef = useRef<HTMLDivElement>(null);
|
||||||
const [showQuickSettings, setShowQuickSettings] = useState(false);
|
const [showQuickSettings, setShowQuickSettings] = useState(false);
|
||||||
|
|
||||||
const [focusTimerDuration, setFocusTimerDuration] = useState(25);
|
const [focusTimerDuration, setFocusTimerDuration] = useState(25);
|
||||||
@ -5454,29 +5470,39 @@ export default function WeeklyView() {
|
|||||||
|
|
||||||
{/* Desktop Header: Left, Center, Right */}
|
{/* Desktop Header: Left, Center, Right */}
|
||||||
<header className="group relative flex items-center justify-between w-full px-4 py-2 border-b border-gray-200 bg-white dark:bg-gray-900 dark:border-gray-700 dark:text-white transition-colors duration-200" style={isMobile ? { display: "none" } : {}}>
|
<header className="group relative flex items-center justify-between w-full px-4 py-2 border-b border-gray-200 bg-white dark:bg-gray-900 dark:border-gray-700 dark:text-white transition-colors duration-200" style={isMobile ? { display: "none" } : {}}>
|
||||||
{/* LEFT SECTION: Slot Duration & Days to Show */}
|
{/* LEFT SECTION: View Switcher, Days, Hours, Slot Duration */}
|
||||||
<div className="weekly-header-controls flex items-center gap-4 transition-opacity duration-300 opacity-0 group-hover:opacity-100" style={{ zIndex: 1 }}>
|
<div className="weekly-header-controls flex items-center gap-4 transition-opacity duration-300 opacity-0 group-hover:opacity-100" style={{ zIndex: 1 }}>
|
||||||
{/* Slot Duration */}
|
{/* View Switcher */}
|
||||||
{showTimeGrid && (
|
<div className="flex items-center gap-0.5 bg-gray-100 dark:bg-gray-800 rounded p-0.5" title={t.viewStyle}>
|
||||||
<div
|
|
||||||
className="flex items-center gap-1 bg-gray-100 dark:bg-gray-800 rounded p-1"
|
|
||||||
title="Slot Duration"
|
|
||||||
>
|
|
||||||
<Clock size={16} className="text-gray-500 mr-1" />
|
|
||||||
{[15, 30, 60].map((duration) => (
|
|
||||||
<button
|
<button
|
||||||
key={duration}
|
onClick={() => { setViewStyle("simple"); setShowTimeGrid(true); saveSetting("viewStyle", "simple"); saveSetting("showTimeGrid", true); }}
|
||||||
onClick={() => {
|
className={`p-1.5 rounded transition-colors ${viewStyle === "simple" ? "bg-white shadow-sm text-black dark:bg-gray-700 dark:text-white" : "text-gray-500 hover:text-gray-900 hover:bg-gray-200 dark:hover:bg-gray-700"}`}
|
||||||
setCellDuration(duration as CellDuration);
|
title={t.simpleView}
|
||||||
saveSetting("cellDuration", duration);
|
|
||||||
}}
|
|
||||||
className={`px-2 py-0.5 text-xs rounded transition-colors ${cellDuration === duration ? "bg-white shadow-sm font-bold text-black" : "text-gray-500 hover:text-gray-900 hover:bg-gray-200 dark:hover:bg-gray-700"}`}
|
|
||||||
>
|
>
|
||||||
{duration}m
|
<Calendar size={16} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => { setViewStyle("calendar"); setShowTimeGrid(true); saveSetting("viewStyle", "calendar"); saveSetting("showTimeGrid", true); }}
|
||||||
|
className={`p-1.5 rounded transition-colors ${viewStyle === "calendar" ? "bg-white shadow-sm text-black dark:bg-gray-700 dark:text-white" : "text-gray-500 hover:text-gray-900 hover:bg-gray-200 dark:hover:bg-gray-700"}`}
|
||||||
|
title={t.calendarView}
|
||||||
|
>
|
||||||
|
<CalendarDays size={16} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => { setViewStyle("list"); setShowTimeGrid(false); saveSetting("viewStyle", "list"); saveSetting("showTimeGrid", false); }}
|
||||||
|
className={`p-1.5 rounded transition-colors ${viewStyle === "list" ? "bg-white shadow-sm text-black dark:bg-gray-700 dark:text-white" : "text-gray-500 hover:text-gray-900 hover:bg-gray-200 dark:hover:bg-gray-700"}`}
|
||||||
|
title={t.listView}
|
||||||
|
>
|
||||||
|
<ListTodo size={16} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => { setViewStyle("kanban"); saveSetting("viewStyle", "kanban"); }}
|
||||||
|
className={`p-1.5 rounded transition-colors ${viewStyle === "kanban" ? "bg-white shadow-sm text-black dark:bg-gray-700 dark:text-white" : "text-gray-500 hover:text-gray-900 hover:bg-gray-200 dark:hover:bg-gray-700"}`}
|
||||||
|
title={t.kanbanView}
|
||||||
|
>
|
||||||
|
<Kanban size={16} />
|
||||||
</button>
|
</button>
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Days to Show */}
|
{/* Days to Show */}
|
||||||
<div
|
<div
|
||||||
@ -5542,30 +5568,27 @@ export default function WeeklyView() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* View Switcher */}
|
{/* Slot Duration */}
|
||||||
<div className="flex items-center gap-0.5 bg-gray-100 dark:bg-gray-800 rounded p-0.5" title={t.viewStyle}>
|
{showTimeGrid && (
|
||||||
<button
|
<div
|
||||||
onClick={() => { setViewStyle("simple"); saveSetting("viewStyle", "simple"); }}
|
className="flex items-center gap-1 bg-gray-100 dark:bg-gray-800 rounded p-1"
|
||||||
className={`p-1.5 rounded transition-colors ${viewStyle !== "kanban" && viewStyle !== "list" ? "bg-white shadow-sm text-black dark:bg-gray-700 dark:text-white" : "text-gray-500 hover:text-gray-900 hover:bg-gray-200 dark:hover:bg-gray-700"}`}
|
title="Slot Duration"
|
||||||
title={t.weekView}
|
|
||||||
>
|
>
|
||||||
<CalendarDays size={16} />
|
<Clock size={16} className="text-gray-500 mr-1" />
|
||||||
</button>
|
{[15, 30, 60].map((duration) => (
|
||||||
<button
|
<button
|
||||||
onClick={() => { setViewStyle("list"); saveSetting("viewStyle", "list"); }}
|
key={duration}
|
||||||
className={`p-1.5 rounded transition-colors ${viewStyle === "list" ? "bg-white shadow-sm text-black dark:bg-gray-700 dark:text-white" : "text-gray-500 hover:text-gray-900 hover:bg-gray-200 dark:hover:bg-gray-700"}`}
|
onClick={() => {
|
||||||
title={t.listView}
|
setCellDuration(duration as CellDuration);
|
||||||
|
saveSetting("cellDuration", duration);
|
||||||
|
}}
|
||||||
|
className={`px-2 py-0.5 text-xs rounded transition-colors ${cellDuration === duration ? "bg-white shadow-sm font-bold text-black" : "text-gray-500 hover:text-gray-900 hover:bg-gray-200 dark:hover:bg-gray-700"}`}
|
||||||
>
|
>
|
||||||
<ListTodo size={16} />
|
{duration}m
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => { setViewStyle("kanban"); saveSetting("viewStyle", "kanban"); }}
|
|
||||||
className={`p-1.5 rounded transition-colors ${viewStyle === "kanban" ? "bg-white shadow-sm text-black dark:bg-gray-700 dark:text-white" : "text-gray-500 hover:text-gray-900 hover:bg-gray-200 dark:hover:bg-gray-700"}`}
|
|
||||||
title={t.kanbanView}
|
|
||||||
>
|
|
||||||
<Kanban size={16} />
|
|
||||||
</button>
|
</button>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -5573,16 +5596,13 @@ export default function WeeklyView() {
|
|||||||
<div className="flex items-center justify-center gap-6 absolute left-1/2 transform -translate-x-1/2 group z-10 opacity-100" style={{ pointerEvents: "auto" }}>
|
<div className="flex items-center justify-center gap-6 absolute left-1/2 transform -translate-x-1/2 group z-10 opacity-100" style={{ pointerEvents: "auto" }}>
|
||||||
{/* Week & Year */}
|
{/* Week & Year */}
|
||||||
<div className="whitespace-nowrap flex items-center gap-2">
|
<div className="whitespace-nowrap flex items-center gap-2">
|
||||||
{/* Date Picker Toggle - Moved to front */}
|
{/* Clickable Week & Year */}
|
||||||
<div className="relative">
|
<div
|
||||||
<button
|
|
||||||
ref={datePickerBtnRef}
|
ref={datePickerBtnRef}
|
||||||
className={`p-1.5 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors ${showDatePicker ? "text-teal-600 bg-teal-50 opacity-100" : "text-gray-500 hover:text-black opacity-0 group-hover:opacity-100"}`}
|
className="relative flex items-center gap-2 cursor-pointer hover:opacity-80"
|
||||||
onClick={() => setShowDatePicker(!showDatePicker)}
|
onClick={() => setShowDatePicker(!showDatePicker)}
|
||||||
title="Jump to date"
|
title="Jump to date"
|
||||||
>
|
>
|
||||||
<Calendar size={18} />
|
|
||||||
</button>
|
|
||||||
{showDatePicker && (
|
{showDatePicker && (
|
||||||
<SimpleDatePicker
|
<SimpleDatePicker
|
||||||
selected={currentWeekStart}
|
selected={currentWeekStart}
|
||||||
@ -5595,14 +5615,6 @@ export default function WeeklyView() {
|
|||||||
anchorRef={datePickerBtnRef}
|
anchorRef={datePickerBtnRef}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Clickable Week & Year */}
|
|
||||||
<div
|
|
||||||
className="flex items-center gap-2 cursor-pointer hover:opacity-80"
|
|
||||||
onClick={() => setShowDatePicker(!showDatePicker)}
|
|
||||||
title="Jump to date"
|
|
||||||
>
|
|
||||||
<span style={{
|
<span style={{
|
||||||
fontFamily: profile.cwFontFamily || "Inter",
|
fontFamily: profile.cwFontFamily || "Inter",
|
||||||
fontSize: profile.cwFontSize || "1.125rem",
|
fontSize: profile.cwFontSize || "1.125rem",
|
||||||
@ -5610,7 +5622,10 @@ export default function WeeklyView() {
|
|||||||
color: adjustColorForDarkMode(profile.cwColor || "#333333", darkMode),
|
color: adjustColorForDarkMode(profile.cwColor || "#333333", darkMode),
|
||||||
filter: "brightness(var(--weekly-header-brightness, 1))"
|
filter: "brightness(var(--weekly-header-brightness, 1))"
|
||||||
}}>
|
}}>
|
||||||
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")}`
|
||||||
|
}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-gray-400">|</span>
|
<span className="text-gray-400">|</span>
|
||||||
<span
|
<span
|
||||||
@ -5962,9 +5977,10 @@ export default function WeeklyView() {
|
|||||||
{task.title}
|
{task.title}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{task.scheduledDate && (
|
{(task.scheduledDate || task.startTime) && (
|
||||||
<div className="kanban-card-date">
|
<div className="kanban-card-date">
|
||||||
{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 && <span className="kanban-card-time"> {task.startTime}</span>}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{task.project && (
|
{task.project && (
|
||||||
@ -6410,6 +6426,7 @@ export default function WeeklyView() {
|
|||||||
showTaskCheckboxes={profile.showTaskCheckboxes}
|
showTaskCheckboxes={profile.showTaskCheckboxes}
|
||||||
projects={projects}
|
projects={projects}
|
||||||
onProjectAssign={assignProject}
|
onProjectAssign={assignProject}
|
||||||
|
kanbanStages={kanbanStages}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{visibleSlots.map((slot) => {
|
{visibleSlots.map((slot) => {
|
||||||
@ -8365,36 +8382,8 @@ function TaskItem({
|
|||||||
onEdit();
|
onEdit();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onTouchStart={(e) => {
|
onTouchStart={() => {
|
||||||
const touch = e.touches[0];
|
// No task-level swipe — container handles day navigation
|
||||||
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;
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Swipe indicators */}
|
{/* Swipe indicators */}
|
||||||
@ -9565,6 +9554,7 @@ function SettingsSidebar({
|
|||||||
weekdayCase?: "normal" | "capitalize" | "uppercase";
|
weekdayCase?: "normal" | "capitalize" | "uppercase";
|
||||||
customWeekdayNames?: string;
|
customWeekdayNames?: string;
|
||||||
dateVerticalAlign?: "top" | "middle" | "bottom";
|
dateVerticalAlign?: "top" | "middle" | "bottom";
|
||||||
|
headerDisplay?: "kw" | "month";
|
||||||
}>({
|
}>({
|
||||||
name: "",
|
name: "",
|
||||||
email: "",
|
email: "",
|
||||||
@ -10680,6 +10670,27 @@ function SettingsSidebar({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Header Display */}
|
||||||
|
<div style={{ marginTop: "16px" }}>
|
||||||
|
<label style={{ fontSize: "0.85rem", fontWeight: 600, color: "var(--weekly-settings-label)" }}>
|
||||||
|
{t.headerDisplay}
|
||||||
|
</label>
|
||||||
|
<div className="flex items-center gap-1 bg-gray-100 dark:bg-gray-800 rounded p-1 mt-1" style={{ width: "fit-content" }}>
|
||||||
|
<button
|
||||||
|
onClick={() => setProfile({ ...profile, headerDisplay: "kw" })}
|
||||||
|
className={`px-4 py-2 text-sm rounded transition-colors ${(profile.headerDisplay || "kw") === "kw" ? "bg-white shadow-sm font-bold text-black" : "text-gray-500 hover:text-gray-900 hover:bg-gray-200 dark:hover:bg-gray-700"}`}
|
||||||
|
>
|
||||||
|
{t.headerDisplayKW}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setProfile({ ...profile, headerDisplay: "month" })}
|
||||||
|
className={`px-4 py-2 text-sm rounded transition-colors ${profile.headerDisplay === "month" ? "bg-white shadow-sm font-bold text-black" : "text-gray-500 hover:text-gray-900 hover:bg-gray-200 dark:hover:bg-gray-700"}`}
|
||||||
|
>
|
||||||
|
{t.headerDisplayMonth}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Kanban Stages Settings */}
|
{/* Kanban Stages Settings */}
|
||||||
<div style={{ marginTop: "24px", borderTop: "1px solid var(--border-color, #e5e7eb)", paddingTop: "16px" }}>
|
<div style={{ marginTop: "24px", borderTop: "1px solid var(--border-color, #e5e7eb)", paddingTop: "16px" }}>
|
||||||
<h4 style={{ fontSize: "0.95rem", fontWeight: 700, marginBottom: "8px", display: "flex", alignItems: "center", gap: "6px" }}>
|
<h4 style={{ fontSize: "0.95rem", fontWeight: 700, marginBottom: "8px", display: "flex", alignItems: "center", gap: "6px" }}>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user