diff --git a/package.json b/package.json index 1d985b0..b824823 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "my-weekly-todo-list", - "version": "1.24.0", + "version": "1.25.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/migrations/20260310_add_someday_tab/migration.sql b/prisma/migrations/20260310_add_someday_tab/migration.sql new file mode 100644 index 0000000..c341828 --- /dev/null +++ b/prisma/migrations/20260310_add_someday_tab/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable: Add tab field to SomedayList +ALTER TABLE "SomedayList" ADD COLUMN IF NOT EXISTS "tab" TEXT; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 6e37dd6..dd71248 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -195,6 +195,7 @@ model SomedayList { userId String title String order Int @default(0) + tab String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt externalId String? diff --git a/src/app/api/someday-lists/route.ts b/src/app/api/someday-lists/route.ts index 9fbfe60..a9632cf 100644 --- a/src/app/api/someday-lists/route.ts +++ b/src/app/api/someday-lists/route.ts @@ -185,12 +185,12 @@ export async function PATCH(request: NextRequest) { return NextResponse.json({ success: true }); } - // Handle Single Update (Title) - const { id, title } = body; + // Handle Single Update (Title and/or Tab) + const { id, title, tab } = body; - if (!id || !title) { + if (!id) { return NextResponse.json( - { error: 'ID and Title are required' }, + { error: 'ID is required' }, { status: 400 } ); } @@ -207,9 +207,13 @@ export async function PATCH(request: NextRequest) { ); } + const data: Record = {}; + if (title !== undefined) data.title = title; + if (tab !== undefined) data.tab = tab; + const list = await prisma.somedayList.update({ where: { id }, - data: { title } + data }); return NextResponse.json({ list }); diff --git a/src/app/globals.css b/src/app/globals.css index 4eed357..5b25730 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1443,6 +1443,7 @@ h3 { min-height: 56px; display: flex; align-items: center; + gap: 6px; border-bottom: 1px solid var(--weekly-border); } @@ -1492,6 +1493,72 @@ h3 { color: #666; } +/* Someday Tab Buttons (vertical in label column) */ +.someday-tab-btn { + background: none; + border: none; + cursor: pointer; + color: var(--weekly-text-light, #999); + font-size: 0.45rem; + font-weight: 500; + padding: 3px 4px; + border-radius: 3px; + text-align: center; + width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + line-height: 1.2; + transition: background 0.15s, color 0.15s; + text-transform: uppercase; + letter-spacing: 0.03em; +} + +.someday-tab-btn:hover { + background: var(--weekly-hover-bg, rgba(0, 0, 0, 0.06)); + color: var(--weekly-text, #333); +} + +.someday-tab-btn.active { + background: var(--weekly-accent, #6366f1); + color: #fff; +} + +/* Someday Tab Buttons (horizontal in non-time-grid) */ +.someday-tab-btn-h { + background: none; + border: none; + cursor: pointer; + color: var(--weekly-text-light, #999); + font-size: 0.6rem; + font-weight: 500; + padding: 2px 8px; + border-radius: 3px; + white-space: nowrap; + transition: background 0.15s, color 0.15s; +} + +.someday-tab-btn-h:hover { + background: var(--weekly-hover-bg, rgba(0, 0, 0, 0.06)); + color: var(--weekly-text, #333); +} + +.someday-tab-btn-h.active { + background: var(--weekly-accent, #6366f1); + color: #fff; +} + +/* Someday Tab Select (in list headers) */ +.someday-tab-select { + appearance: none; + -webkit-appearance: none; + outline: none; +} + +.someday-tab-select:hover { + color: var(--weekly-text, #333); +} + /* Preferences Slide-in Panel */ .preferences-panel { position: fixed; @@ -1857,6 +1924,11 @@ h3 { .time-slot { min-height: 28px; } + + /* Hide side nav on mobile (swipe navigation is used instead) */ + .side-nav { + display: none; + } } /* === Phone portrait (≤ 480px): 1-day view === */ @@ -1927,6 +1999,55 @@ h3 { display: flex; flex: 1; overflow: hidden; + position: relative; +} + +/* Side Navigation Arrows (hover overlays) */ +.side-nav { + position: absolute; + top: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 0.35rem 0.15rem; + gap: 0.1rem; + z-index: 30; +} + +.side-nav .side-nav-btn { + opacity: 0; + transition: opacity 0.2s, background 0.15s, color 0.15s; +} + +.side-nav:hover .side-nav-btn { + opacity: 1; +} + +.side-nav-left { + left: 0; +} + +.side-nav-right { + right: 0; +} + +.side-nav-btn { + display: flex; + align-items: center; + justify-content: center; + width: 26px; + height: 26px; + border: none; + background: transparent; + color: var(--weekly-text-light, #999); + cursor: pointer; + border-radius: 6px; +} + +.side-nav-btn:hover { + background: var(--weekly-hover-bg, rgba(0, 0, 0, 0.06)); + color: var(--weekly-text, #333); } /* Time Column (Hours) */ @@ -1949,6 +2070,7 @@ h3 { flex: 1; overflow-y: auto; position: relative; + padding-top: 0.4rem; } diff --git a/src/components/WeeklyView.tsx b/src/components/WeeklyView.tsx index 88a7a7d..7ec6944 100644 --- a/src/components/WeeklyView.tsx +++ b/src/components/WeeklyView.tsx @@ -59,6 +59,7 @@ import { Cable, Link, Globe, + Tag, } from "lucide-react"; // Types @@ -144,6 +145,7 @@ interface SomedayList { id: string; title: string; tasks: Task[]; + tab?: string | null; externalProvider?: string | null; externalId?: string | null; externalListId?: string | null; @@ -254,6 +256,11 @@ const translations: Record = { confirmPassword: "Confirm Password", someday: "SOMEDAY", lists: "Lists", + allTabs: "All", + newTab: "New tab", + newTabName: "New tab name:", + assignTab: "Assign to tab", + renameTab: "Double-click to rename", loading: "Loading your tasks...", sycing: "Syncing...", synced: "Synced", @@ -436,6 +443,11 @@ const translations: Record = { confirmPassword: "Passwort bestätigen", someday: "IRGENDWANN", lists: "Listen", + allTabs: "Alle", + newTab: "Neuer Tab", + newTabName: "Neuer Tab-Name:", + assignTab: "Tab zuweisen", + renameTab: "Doppelklick zum Umbenennen", loading: "Lade Aufgaben...", syncing: "Synchronisiere...", synced: "Synchronisiert", @@ -617,6 +629,11 @@ const translations: Record = { confirmPassword: "Confirmer le mot de passe", someday: "UN JOUR", lists: "Listes", + allTabs: "Tous", + newTab: "Nouvel onglet", + newTabName: "Nom du nouvel onglet :", + assignTab: "Assigner à un onglet", + renameTab: "Double-cliquez pour renommer", loading: "Chargement de vos tâches…", sycing: "Synchronisation…", synced: "Synchronisé", @@ -798,6 +815,11 @@ const translations: Record = { confirmPassword: "Confirmar contraseña", someday: "ALGÚN DÍA", lists: "Listas", + allTabs: "Todas", + newTab: "Nueva pestaña", + newTabName: "Nombre de nueva pestaña:", + assignTab: "Asignar a pestaña", + renameTab: "Doble clic para renombrar", loading: "Cargando tus tareas…", sycing: "Sincronizando…", synced: "Sincronizado", @@ -979,6 +1001,11 @@ const translations: Record = { confirmPassword: "Conferma password", someday: "UN GIORNO", lists: "Liste", + allTabs: "Tutte", + newTab: "Nuova scheda", + newTabName: "Nome nuova scheda:", + assignTab: "Assegna a scheda", + renameTab: "Doppio clic per rinominare", loading: "Caricamento delle attività…", sycing: "Sincronizzazione…", synced: "Sincronizzato", @@ -1398,6 +1425,64 @@ export default function WeeklyView() { const [projects, setProjects] = useState<{ id: string; name: string; icon?: string | null; color?: string | null }[]>([]); const [editingTaskId, setEditingTaskId] = useState(null); const [draggingListId, setDraggingListId] = useState(null); + const [listToDelete, setListToDelete] = useState(null); + const [activeSomedayTab, setActiveSomedayTab] = useState(null); + const [editingTabName, setEditingTabName] = useState(null); + const [renamingTabValue, setRenamingTabValue] = useState(""); + + useEffect(() => { + if (typeof window !== "undefined") { + const saved = localStorage.getItem("weekly_active_someday_tab"); + if (saved) setActiveSomedayTab(saved === "__all__" ? null : saved); + } + }, []); + + const somedayTabs = useMemo(() => { + const tabs = new Set(); + somedayLists.forEach(l => { if (l.tab) tabs.add(l.tab); }); + return Array.from(tabs).sort(); + }, [somedayLists]); + + const setSomedayTab = (tab: string | null) => { + setActiveSomedayTab(tab); + localStorage.setItem("weekly_active_someday_tab", tab ?? "__all__"); + }; + + const assignListToTab = async (listId: string, tab: string | null) => { + setSomedayLists(prev => prev.map(l => l.id === listId ? { ...l, tab } : l)); + try { + await fetch("/api/someday-lists", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ id: listId, tab }), + }); + } catch (e) { + console.error("Failed to update list tab:", e); + } + }; + + const renameTab = async (oldName: string, newName: string) => { + if (!newName.trim() || newName === oldName) return; + const listsToUpdate = somedayLists.filter(l => l.tab === oldName); + setSomedayLists(prev => prev.map(l => l.tab === oldName ? { ...l, tab: newName.trim() } : l)); + if (activeSomedayTab === oldName) setSomedayTab(newName.trim()); + for (const list of listsToUpdate) { + try { + await fetch("/api/someday-lists", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ id: list.id, tab: newName.trim() }), + }); + } catch (e) { + console.error("Failed to rename tab for list:", e); + } + } + }; + + const filteredSomedayLists = useMemo(() => { + if (activeSomedayTab === null) return somedayLists; + return somedayLists.filter(l => (l.tab || null) === activeSomedayTab); + }, [somedayLists, activeSomedayTab]); const [dropTargetListIndex, setDropTargetListIndex] = useState(null); const [activeAddSlot, setActiveAddSlot] = useState<{ listId: string; slotIdx: number } | null>(null); const isDragFromHandle = useRef(false); @@ -5142,7 +5227,7 @@ export default function WeeklyView() { )} {/* Desktop Header: Left, Center, Right */} -
+
{/* LEFT SECTION: Slot Duration & Days to Show */}
{/* Slot Duration */} @@ -5235,51 +5320,81 @@ export default function WeeklyView() {
{/* CENTER SECTION: Week/Year, Goal, Focus Mode - Reveal on Hover */} -
+
{/* Week & Year */}
+ {/* Date Picker Toggle - Moved to front */} +
+ + {showDatePicker && ( + { + setCurrentWeekStart(getStartOfWeek(date)); + setShowDatePicker(false); + }} + onClose={() => setShowDatePicker(false)} + language={language} + anchorRef={datePickerBtnRef} + /> + )} +
+ + {/* Clickable Week & Year */} +
setShowDatePicker(!showDatePicker)} + title="Jump to date" + > + + KW {getWeekNumber(getCWReferenceDate(getVisibleDays())).toString().padStart(2, "0")} + + | + + {currentWeekStart.getFullYear()} + +
+
+ + {/* Goal */} +
{syncError ? ( -
+
{syncError}
) : (isLoading || isSyncing || syncStatus === "syncing") ? ( -
+
) : ( )} - - KW {getWeekNumber(getCWReferenceDate(getVisibleDays())).toString().padStart(2, "0")} - - | - - {currentWeekStart.getFullYear()} - -
- - {/* Goal */} -
- - {isEditingGoal ? ( )} - -
@@ -5476,29 +5590,6 @@ export default function WeeklyView() {
- {/* Date Picker Toggle */} -
- - {showDatePicker && ( - { - setCurrentWeekStart(getStartOfWeek(date)); - setShowDatePicker(false); - }} - onClose={() => setShowDatePicker(false)} - language={language} - anchorRef={datePickerBtnRef} - /> - )} -
{/* Search */} + +
{/* Time Column */} {showTimeGrid && (
@@ -6139,6 +6239,16 @@ export default function WeeklyView() { ); })} + + {/* Right Navigation Arrows (after grid so it paints on top) */} +
+ + +
{/* All-Day Events Section (below position) */} @@ -6227,6 +6337,69 @@ export default function WeeklyView() { > + + {/* Someday Tabs */} + {somedayTabs.length > 0 && ( +
+ + {somedayTabs.map(tab => ( + editingTabName === tab ? ( + setRenamingTabValue(e.target.value)} + onBlur={() => { + renameTab(tab, renamingTabValue); + setEditingTabName(null); + }} + onKeyDown={(e) => { + if (e.key === "Enter") e.currentTarget.blur(); + if (e.key === "Escape") setEditingTabName(null); + }} + autoFocus + style={{ + width: "100%", + fontSize: "0.5rem", + textAlign: "center", + border: "1px solid var(--weekly-border)", + borderRadius: "3px", + padding: "2px", + background: "var(--weekly-bg)", + color: "var(--weekly-text)", + }} + /> + ) : ( + + ) + ))} +
+ )} )} {!showTimeGrid && ( @@ -6290,6 +6463,26 @@ export default function WeeklyView() { > + + {/* Horizontal tabs for non-time-grid */} + {somedayTabs.length > 0 && ( + <> + + {somedayTabs.map(tab => ( + + ))} + + )} )}
@@ -6305,10 +6498,10 @@ export default function WeeklyView() { }} > {(() => { - const baseLists = somedayLists.length > 0 - ? somedayLists + const baseLists = filteredSomedayLists.length > 0 + ? filteredSomedayLists : [{ id: "default", title: "LISTE", tasks: [] as Task[] }]; - return baseLists.slice(0, Math.max(somedayLists.length, viewDays)); + return baseLists.slice(0, Math.max(filteredSomedayLists.length, viewDays)); })() .flatMap((list, listIdx, arr) => { const indicator = draggingListId && dropTargetListIndex === listIdx && draggingListId !== list.id ? ( @@ -6465,110 +6658,162 @@ export default function WeeklyView() { alignItems: "center", }} > -
{ isDragFromHandle.current = true; }} - onMouseUp={() => { isDragFromHandle.current = false; }} - > - -
- { - const newTitle = e.target.value.trim(); - if (newTitle && newTitle !== list.title) { - try { - await fetch("/api/someday-lists", { - method: "PATCH", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - id: list.id, - title: newTitle, - }), - }); - setSomedayLists((prev) => - prev.map((l) => - l.id === list.id - ? { ...l, title: newTitle } - : l, - ), - ); - } catch (err) { - console.error(err); - e.target.value = list.title; - } - } - }} - onKeyDown={(e) => { - if (e.key === "Enter") e.currentTarget.blur(); - }} - /> - {list.externalProvider && ( - - {list.externalProvider === "outlook" ? ( - - ) : list.externalProvider === "google" ? ( - - ) : list.externalProvider === "apple" ? ( - - ) : list.externalProvider === "synology" ? ( - - ) : ( - - )} - + {listToDelete === list.id ? ( +
+ Delete this list? + {list.externalProvider && Note: This list is not deleted from {list.externalProvider}, just from this view.} +
+ + +
+
+ ) : ( + <> +
{ isDragFromHandle.current = true; }} + onMouseUp={() => { isDragFromHandle.current = false; }} + > + +
+ { + const newTitle = e.target.value.trim(); + if (newTitle && newTitle !== list.title) { + try { + await fetch("/api/someday-lists", { + method: "PATCH", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + id: list.id, + title: newTitle, + }), + }); + setSomedayLists((prev) => + prev.map((l) => + l.id === list.id + ? { ...l, title: newTitle } + : l, + ), + ); + } catch (err) { + console.error(err); + e.target.value = list.title; + } + } + }} + onKeyDown={(e) => { + if (e.key === "Enter") e.currentTarget.blur(); + }} + /> + {list.externalProvider && (() => { + const providerUrl = list.externalProvider === "google" ? "https://tasks.google.com/tasks/" : + list.externalProvider === "outlook" ? "https://to-do.live.com/tasks/" : + list.externalProvider === "apple" ? "https://www.icloud.com/reminders/" : null; + const iconContent = ( + + {list.externalProvider === "outlook" ? ( + + ) : list.externalProvider === "google" ? ( + + ) : list.externalProvider === "apple" ? ( + + ) : list.externalProvider === "synology" ? ( + + ) : ( + + )} + + ); + return providerUrl ? ( + e.stopPropagation()}> + {iconContent} + + ) : iconContent; + })()} +
+ + +
+ + )} -
{(() => { @@ -9165,7 +9410,7 @@ function SettingsSidebar({ setShowAppleCalendarModal(false); showConnMsg("success", "Apple Calendar connected successfully!"); - setTimeout(() => window.location.reload(), 1200); + setTimeout(() => { window.location.href = window.location.pathname + "?calendar=apple_connected&openSettings=calendars"; }, 1200); } catch (err: any) { setAppleCalError(err.message || "Connection failed"); } finally { @@ -9210,7 +9455,7 @@ function SettingsSidebar({ setShowSynologyCalendarModal(false); showConnMsg("success", "Synology Calendar connected successfully!"); - setTimeout(() => window.location.reload(), 1200); + setTimeout(() => { window.location.href = window.location.pathname + "?calendar=synology_connected&openSettings=calendars"; }, 1200); } catch (err: any) { setSynologyCalError(err.message || "Connection failed"); } finally { @@ -9498,6 +9743,7 @@ function SettingsSidebar({ style={{ display: "flex", justifyContent: "center", + flexWrap: "wrap", gap: "4px", borderBottom: "1px solid var(--weekly-border, #eee)", padding: "0 24px",