diff --git a/package.json b/package.json index 64ac9d5..7e1f9b7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "my-weekly-todo-list", - "version": "1.3.1", + "version": "1.4.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/calendar/background-sync/route.ts b/src/app/api/calendar/background-sync/route.ts index 7688abd..ee3e5e2 100644 --- a/src/app/api/calendar/background-sync/route.ts +++ b/src/app/api/calendar/background-sync/route.ts @@ -12,7 +12,7 @@ export async function POST(request: NextRequest) { } const body = await request.json().catch(() => ({})); - const { timeMin, timeMax } = body; + const { timeMin, timeMax, forceRefresh } = body; const user = await prisma.user.findUnique({ where: { email: session.user.email }, @@ -30,7 +30,7 @@ export async function POST(request: NextRequest) { const staleChecks = await Promise.all( user.calendarConnections.map(async conn => ({ conn, - stale: await isCacheStale(conn.id, tMin), + stale: forceRefresh || await isCacheStale(conn.id, tMin), })) ); diff --git a/src/app/api/calendar/events/route.ts b/src/app/api/calendar/events/route.ts index bad2592..67c2c7f 100644 --- a/src/app/api/calendar/events/route.ts +++ b/src/app/api/calendar/events/route.ts @@ -39,7 +39,7 @@ export async function POST(request: NextRequest) { if (!session?.user?.email) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); const body = await request.json(); - const { calendarId, title, description, start, end, location } = body; + const { calendarId, title, description, start, end, location, allDay } = body; console.log('[API] Creating event:', { calendarId, title, start, end }); @@ -59,7 +59,8 @@ export async function POST(request: NextRequest) { description, start, end, - location + location, + allDay: !!allDay }); // Update cache @@ -80,7 +81,7 @@ export async function PATCH(request: NextRequest) { if (!session?.user?.email) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); const body = await request.json(); - const { calendarId, eventId, title, description, start, end, location } = body; + const { calendarId, eventId, title, description, start, end, location, allDay } = body; console.log('[API] Updating event:', { calendarId, eventId, title }); @@ -100,7 +101,8 @@ export async function PATCH(request: NextRequest) { description, start, end, - location + location, + allDay: allDay !== undefined ? !!allDay : undefined }); // Update cache diff --git a/src/app/api/tasks/sync/route.ts b/src/app/api/tasks/sync/route.ts index 8d49e11..807f535 100644 --- a/src/app/api/tasks/sync/route.ts +++ b/src/app/api/tasks/sync/route.ts @@ -1,13 +1,11 @@ import { NextRequest, NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; import { authOptions } from "@/lib/auth"; -import { PrismaClient } from '@prisma/client'; +import { prisma } from '@/lib/prisma'; import { createGoogleClient, updateGoogleTaskStatus, updateGoogleTask, deleteGoogleTask, fetchGoogleTasksForSync, GoogleTask } from '@/lib/google-tasks'; import { fetchMsTodoTasksForSync, updateMsTodoTask, deleteMsTodoTask, createMsTodoTask, isMsTodoTaskCompleted } from '@/lib/microsoft-todo'; import { getOutlookAccessToken } from '@/lib/outlook-token'; -const prisma = new PrismaClient(); - // GET - Pull changes from Google Tasks into local DB export async function GET(req: NextRequest) { try { @@ -478,6 +476,7 @@ export async function POST(req: NextRequest) { const created = await createMsTodoTask(outlookToken, listExternalId, { title: task.title, body: task.description || undefined, + dueDateTime: task.scheduledDate ? task.scheduledDate.toISOString() : undefined, }); const updatedTask = await prisma.task.update({ diff --git a/src/app/globals.css b/src/app/globals.css index 4845170..3d2f782 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -800,6 +800,80 @@ h3 { outline: none; } +/* Floating Notes Popup */ +.weekly-notes-popup { + position: absolute; + left: 0; + right: 0; + top: 100%; + min-width: 250px; + background: var(--weekly-bg); + border: 1px solid var(--weekly-border); + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + z-index: 1000; + padding: 8px; + cursor: default; +} + +/* Arrow default (pointing up, when popup is below) */ +.weekly-notes-popup::before { + content: ""; + position: absolute; + left: 20px; + top: -8px; + border-left: 8px solid transparent; + border-right: 8px solid transparent; + border-bottom: 8px solid var(--weekly-border); +} + +.weekly-notes-popup::after { + content: ""; + position: absolute; + left: 20px; + top: -7px; + border-left: 8px solid transparent; + border-right: 8px solid transparent; + border-bottom: 8px solid var(--weekly-bg); +} + +/* Arrow when popup is above (pointing down) */ +.weekly-notes-popup.on-top::before { + top: auto !important; + bottom: -8px !important; + border-bottom: none !important; + border-top: 8px solid var(--weekly-border) !important; +} + +.weekly-notes-popup.on-top::after { + top: auto !important; + bottom: -7px !important; + border-bottom: none !important; + border-top: 8px solid var(--weekly-bg) !important; +} + +.weekly-container.dark-mode .weekly-notes-popup { + background: #2a2a2a; + border-color: #444; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); +} + +.weekly-container.dark-mode .weekly-notes-popup::before { + border-bottom-color: #444; +} + +.weekly-container.dark-mode .weekly-notes-popup::after { + border-bottom-color: #2a2a2a; +} + +.weekly-container.dark-mode .weekly-notes-popup.on-top::before { + border-top-color: #444 !important; +} + +.weekly-container.dark-mode .weekly-notes-popup.on-top::after { + border-top-color: #2a2a2a !important; +} + .weekly-notes-editor-inline:focus { border-color: var(--weekly-teal); background: #fff; @@ -1753,6 +1827,7 @@ h3 { .time-slots-container { flex: 1; overflow-y: auto; + overflow-x: visible; } .time-slot { @@ -2709,18 +2784,129 @@ h3 { .weekly-settings-sidebar .weekly-settings-header { padding: 24px; - border-bottom: 1px solid #eee; display: flex; - align-items: center; justify-content: space-between; + align-items: center; + border-bottom: 1px solid #eee; } +/* Notes Sidebar */ +.weekly-notes-sidebar { + position: fixed; + top: 0; + right: 0; + width: 500px; + max-width: 95vw; + height: 100vh; + background: white; + box-shadow: -10px 0 30px rgba(0, 0, 0, 0.1); + display: flex; + flex-direction: column; + transform: translateX(100%); + transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1); + z-index: 2005; + overflow: hidden; + border-left: 1px solid #eee; +} + +.weekly-notes-sidebar.open { + transform: translateX(0); +} + +.dark-mode .weekly-notes-sidebar { + background: #111; + border-left: 1px solid #333; + color: white; +} + +.weekly-notes-sidebar-header { + padding: 20px 24px; + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid #eee; +} + +.dark-mode .weekly-notes-sidebar-header { + border-bottom-color: #333; +} + +.weekly-notes-sidebar-title { + margin: 0; + font-size: 1.25rem; + font-weight: 800; + text-transform: uppercase; + letter-spacing: -0.02em; +} + +.weekly-notes-sidebar-close { + background: none; + border: none; + font-size: 1.5rem; + cursor: pointer; + color: #888; + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: 50%; + transition: all 0.2s; +} + +.weekly-notes-sidebar-close:hover { + background: #f5f5f5; + color: #333; +} + +.dark-mode .weekly-notes-sidebar-close:hover { + background: #222; + color: white; +} + +.weekly-notes-sidebar-content { + flex: 1; + padding: 24px; + overflow-y: auto; + display: flex; + flex-direction: column; +} + +.weekly-notes-sidebar .weekly-notes-editor { + flex: 1; + min-height: 300px; + border: 1px solid #eee; + padding: 16px; + font-size: 1rem; + line-height: 1.6; + border-radius: 8px; + background: #fafafa; + margin-top: 16px; +} + +.dark-mode .weekly-notes-sidebar .weekly-notes-editor { + background: #1a1a1a; + border-color: #333; + color: #eee; +} + +.weekly-notes-sidebar .notes-toolbar { + display: flex; + gap: 8px; + padding: 8px; + background: #f5f5f5; + border-radius: 6px; + margin-bottom: 4px; +} + +.dark-mode .weekly-notes-sidebar .notes-toolbar { + background: #222; +} .settings-tab-btn:hover { opacity: 0.8 !important; background: rgba(0, 0, 0, 0.04) !important; border-radius: 6px 6px 0 0; } - .dark-mode .settings-tab-btn:hover { background: rgba(255, 255, 255, 0.08) !important; } diff --git a/src/components/CalendarEventModal.tsx b/src/components/CalendarEventModal.tsx index eabfeff..00e31f2 100644 --- a/src/components/CalendarEventModal.tsx +++ b/src/components/CalendarEventModal.tsx @@ -21,7 +21,12 @@ export default function CalendarEventModal({ }: CalendarEventModalProps) { // Flatten calendars from connections to get selectable options const availableCalendars = connections - .flatMap(conn => conn.calendars || []) + .flatMap(conn => (conn.calendars || []).map((cal: any) => ({ + ...cal, + providerName: conn.provider === 'google' ? 'Google Calendar' : + conn.provider === 'apple' ? 'Apple Calendar' : + 'Outlook Calendar' + }))) .filter((cal: any) => cal.editable); // Only editable calendars const [title, setTitle] = useState(event?.title || ''); @@ -59,6 +64,7 @@ export default function CalendarEventModal({ const [startDate, setStartDate] = useState(getInitialStart()); const [endDate, setEndDate] = useState(getInitialEnd()); + const [allDay, setAllDay] = useState(!!event?.allDay); const [isSaving, setIsSaving] = useState(false); const [error, setError] = useState(''); @@ -85,6 +91,7 @@ export default function CalendarEventModal({ description, location, calendarId, + allDay, start: { dateTime: startDate.toISOString() }, end: { dateTime: endDate.toISOString() } }); @@ -170,18 +177,32 @@ export default function CalendarEventModal({ > {availableCalendars.length === 0 && } {availableCalendars.map((cal: any) => ( - + ))} + {/* All Day Toggle */} +