From f0672e5c68158dc9fd735a0af86dfff66589e695 Mon Sep 17 00:00:00 2001 From: mARTin Date: Tue, 24 Feb 2026 23:18:13 +0100 Subject: [PATCH] feat: fix Docker deployment, OAuth redirects, event editing, and UI improvements - Fix Docker build: resolve hooks-rules-of-hooks in GridTaskBlock, add Suspense boundary for useSearchParams, fix non-root user home dir and prisma binary path - Add comprehensive DB migrations for missing columns/tables (WeeklyGoal, CachedCalendarEvent, User settings, SomedayList sync fields, Task subtasks) - Fix Outlook OAuth redirect using NEXTAUTH_URL instead of request.url - Fix CalendarEventModal preserving existing event dates (support flat format) - Fix header z-index layering so goal text doesn't block hover controls - Convert font size selects to free-form text inputs, add dayHeaderGap setting - Add goal 2-line clamping with 500px max width - Fix weekend colors not applying to date elements v1.6.0 Co-Authored-By: Claude Opus 4.6 --- Dockerfile | 36 ++-- docker-compose.yml | 3 +- next.config.js | 1 + package.json | 2 +- .../migration.sql | 117 +++++++++++++ .../20260224_add_missing_tables/migration.sql | 100 +++++++++++ scripts/docker-entrypoint.sh | 10 ++ .../api/calendar/outlook/callback/route.ts | 14 +- src/app/api/calendar/outlook/start/route.ts | 3 +- src/app/auth/oauth-complete/page.tsx | 22 ++- src/app/globals.css | 6 +- src/components/CalendarEventModal.tsx | 4 + src/components/GridTaskBlock.tsx | 48 +++--- src/components/WeeklyView.tsx | 155 +++++++++++------- tsconfig.tsbuildinfo | 2 +- 15 files changed, 410 insertions(+), 113 deletions(-) create mode 100644 prisma/migrations/20260224_add_missing_columns/migration.sql create mode 100644 prisma/migrations/20260224_add_missing_tables/migration.sql create mode 100755 scripts/docker-entrypoint.sh diff --git a/Dockerfile b/Dockerfile index ffe5836..f0986d6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,30 +23,40 @@ FROM node:18-slim AS runner WORKDIR /app ENV NODE_ENV=production +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" # Install OpenSSL for Prisma and curl for healthcheck RUN apt-get update && apt-get install -y openssl curl && rm -rf /var/lib/apt/lists/* -# Create non-root user +# Create non-root user with a writable home directory RUN addgroup --system --gid 1001 nodejs -RUN adduser --system --uid 1001 nextjs +RUN adduser --system --uid 1001 --home /home/nextjs nextjs -# Copy built output and dependencies -COPY --from=builder /app/package*.json ./ -COPY --from=builder /app/node_modules ./node_modules -COPY --from=builder /app/.next ./.next -COPY --from=builder /app/public ./public -COPY --from=builder /app/prisma ./prisma -COPY --from=builder /app/next.config.js ./ - -# Create logs directory +# Set correct permissions RUN mkdir -p /app/logs && chown -R nextjs:nodejs /app +# Copy built output +COPY --from=builder /app/public ./public +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static +COPY --from=builder /app/prisma ./prisma + +# Copy Prisma CLI and generated client from builder so npx doesn't re-download +COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma +COPY --from=builder /app/node_modules/prisma ./node_modules/prisma +COPY --from=builder /app/node_modules/@prisma ./node_modules/@prisma + +# Copy entrypoint script +COPY scripts/docker-entrypoint.sh /usr/local/bin/ +RUN chmod +x /usr/local/bin/docker-entrypoint.sh + USER nextjs EXPOSE 3000 HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ - CMD curl -f http://localhost:3001/api/auth/session || exit 1 + CMD curl -f http://localhost:3000/api/auth/session || exit 1 -CMD ["npm", "run", "start"] +ENTRYPOINT ["docker-entrypoint.sh"] +CMD ["node", "server.js"] diff --git a/docker-compose.yml b/docker-compose.yml index 6df5a6c..6b0b3d4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,6 +2,7 @@ version: '3.8' services: app: + build: . image: my-weekly-todo:latest ports: - "13000:3000" @@ -10,7 +11,7 @@ services: depends_on: - db volumes: - - app_logs:/app/logs + - ./logs:/app/logs restart: unless-stopped db: diff --git a/next.config.js b/next.config.js index a96ff51..85d21a7 100644 --- a/next.config.js +++ b/next.config.js @@ -2,6 +2,7 @@ const { version } = require('./package.json'); const nextConfig = { + output: 'standalone', env: { NEXT_PUBLIC_APP_VERSION: version, }, diff --git a/package.json b/package.json index a2b2b61..f7efb65 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "my-weekly-todo-list", - "version": "1.5.0", + "version": "1.6.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/20260224_add_missing_columns/migration.sql b/prisma/migrations/20260224_add_missing_columns/migration.sql new file mode 100644 index 0000000..a4a76d3 --- /dev/null +++ b/prisma/migrations/20260224_add_missing_columns/migration.sql @@ -0,0 +1,117 @@ +-- AlterTable: Add missing User settings columns +ALTER TABLE "User" ADD COLUMN IF NOT EXISTS "goalFallbackType" TEXT NOT NULL DEFAULT 'quote'; +ALTER TABLE "User" ADD COLUMN IF NOT EXISTS "goalDefaultSentence" TEXT NOT NULL DEFAULT 'goal of the week'; +ALTER TABLE "User" ADD COLUMN IF NOT EXISTS "dateLayout" TEXT NOT NULL DEFAULT 'right'; +ALTER TABLE "User" ADD COLUMN IF NOT EXISTS "dateAlignment" TEXT NOT NULL DEFAULT 'center'; +ALTER TABLE "User" ADD COLUMN IF NOT EXISTS "hourLabelFormat" TEXT NOT NULL DEFAULT 'short'; +ALTER TABLE "User" ADD COLUMN IF NOT EXISTS "showSubHourSlots" BOOLEAN NOT NULL DEFAULT true; +ALTER TABLE "User" ADD COLUMN IF NOT EXISTS "allDayPosition" TEXT NOT NULL DEFAULT 'below'; +ALTER TABLE "User" ADD COLUMN IF NOT EXISTS "cwColor" TEXT DEFAULT '#333333'; +ALTER TABLE "User" ADD COLUMN IF NOT EXISTS "cwFontFamily" TEXT DEFAULT 'Inter'; +ALTER TABLE "User" ADD COLUMN IF NOT EXISTS "cwFontSize" TEXT DEFAULT '1.125rem'; +ALTER TABLE "User" ADD COLUMN IF NOT EXISTS "cwFontWeight" TEXT DEFAULT '700'; +ALTER TABLE "User" ADD COLUMN IF NOT EXISTS "yearColor" TEXT DEFAULT '#333333'; +ALTER TABLE "User" ADD COLUMN IF NOT EXISTS "yearFontFamily" TEXT DEFAULT 'Inter'; +ALTER TABLE "User" ADD COLUMN IF NOT EXISTS "yearFontSize" TEXT DEFAULT '1.125rem'; +ALTER TABLE "User" ADD COLUMN IF NOT EXISTS "yearFontWeight" TEXT DEFAULT '700'; + +-- AlterTable: Add missing SomedayList columns +ALTER TABLE "SomedayList" ADD COLUMN IF NOT EXISTS "externalId" TEXT; +ALTER TABLE "SomedayList" ADD COLUMN IF NOT EXISTS "externalProvider" TEXT; +ALTER TABLE "SomedayList" ADD COLUMN IF NOT EXISTS "lastSyncedAt" TIMESTAMP(3); + +-- AlterTable: Add missing Task columns +ALTER TABLE "Task" ADD COLUMN IF NOT EXISTS "parentTaskId" TEXT; +ALTER TABLE "Task" ADD COLUMN IF NOT EXISTS "deletedAt" TIMESTAMP(3); + +-- CreateTable: WeeklyGoal +CREATE TABLE IF NOT EXISTS "WeeklyGoal" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "weekStart" TIMESTAMP(3) NOT NULL, + "text" TEXT NOT NULL DEFAULT 'your goal of this week', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "WeeklyGoal_pkey" PRIMARY KEY ("id") +); + +-- CreateTable: CachedCalendarEvent +CREATE TABLE IF NOT EXISTS "CachedCalendarEvent" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "externalId" TEXT NOT NULL, + "connectionId" TEXT NOT NULL, + "provider" TEXT NOT NULL, + "calendarId" TEXT NOT NULL, + "calendarTitle" TEXT NOT NULL, + "calendarColor" TEXT, + "title" TEXT NOT NULL, + "description" TEXT, + "location" TEXT, + "startDateTime" TIMESTAMP(3), + "startDate" TEXT, + "endDateTime" TIMESTAMP(3), + "endDate" TEXT, + "weekStart" TIMESTAMP(3) NOT NULL, + "syncedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "CachedCalendarEvent_pkey" PRIMARY KEY ("id") +); + +-- CreateIndexes for WeeklyGoal +CREATE UNIQUE INDEX IF NOT EXISTS "WeeklyGoal_userId_weekStart_key" ON "WeeklyGoal"("userId", "weekStart"); +CREATE INDEX IF NOT EXISTS "WeeklyGoal_userId_idx" ON "WeeklyGoal"("userId"); + +-- CreateIndexes for CachedCalendarEvent +CREATE UNIQUE INDEX IF NOT EXISTS "CachedCalendarEvent_userId_externalId_provider_key" ON "CachedCalendarEvent"("userId", "externalId", "provider"); +CREATE INDEX IF NOT EXISTS "CachedCalendarEvent_userId_startDateTime_idx" ON "CachedCalendarEvent"("userId", "startDateTime"); +CREATE INDEX IF NOT EXISTS "CachedCalendarEvent_userId_startDate_idx" ON "CachedCalendarEvent"("userId", "startDate"); +CREATE INDEX IF NOT EXISTS "CachedCalendarEvent_connectionId_weekStart_idx" ON "CachedCalendarEvent"("connectionId", "weekStart"); + +-- CreateIndex for Task +CREATE INDEX IF NOT EXISTS "Task_parentTaskId_idx" ON "Task"("parentTaskId"); +CREATE INDEX IF NOT EXISTS "Task_userId_deletedAt_idx" ON "Task"("userId", "deletedAt"); + +-- AddForeignKeys (idempotent) +DO $$ BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.table_constraints + WHERE constraint_name = 'Task_parentTaskId_fkey' + ) THEN + ALTER TABLE "Task" ADD CONSTRAINT "Task_parentTaskId_fkey" FOREIGN KEY ("parentTaskId") REFERENCES "Task"("id") ON DELETE CASCADE ON UPDATE CASCADE; + END IF; +END $$; + +DO $$ BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.table_constraints + WHERE constraint_name = 'WeeklyGoal_userId_fkey' + ) THEN + ALTER TABLE "WeeklyGoal" ADD CONSTRAINT "WeeklyGoal_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + END IF; +END $$; + +DO $$ BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.table_constraints + WHERE constraint_name = 'CachedCalendarEvent_connectionId_fkey' + ) THEN + ALTER TABLE "CachedCalendarEvent" ADD CONSTRAINT "CachedCalendarEvent_connectionId_fkey" FOREIGN KEY ("connectionId") REFERENCES "CalendarConnection"("id") ON DELETE CASCADE ON UPDATE CASCADE; + END IF; +END $$; + +DO $$ BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.table_constraints + WHERE constraint_name = 'CachedCalendarEvent_userId_fkey' + ) THEN + ALTER TABLE "CachedCalendarEvent" ADD CONSTRAINT "CachedCalendarEvent_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + END IF; +END $$; + +-- Fix Task_somedayListId_fkey to use SET NULL on delete (idempotent) +ALTER TABLE "Task" DROP CONSTRAINT IF EXISTS "Task_somedayListId_fkey"; +ALTER TABLE "Task" ADD CONSTRAINT "Task_somedayListId_fkey" FOREIGN KEY ("somedayListId") REFERENCES "SomedayList"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/migrations/20260224_add_missing_tables/migration.sql b/prisma/migrations/20260224_add_missing_tables/migration.sql new file mode 100644 index 0000000..2d367e2 --- /dev/null +++ b/prisma/migrations/20260224_add_missing_tables/migration.sql @@ -0,0 +1,100 @@ +-- AlterTable: Add missing SomedayList columns +ALTER TABLE "SomedayList" ADD COLUMN IF NOT EXISTS "externalId" TEXT; +ALTER TABLE "SomedayList" ADD COLUMN IF NOT EXISTS "externalProvider" TEXT; +ALTER TABLE "SomedayList" ADD COLUMN IF NOT EXISTS "lastSyncedAt" TIMESTAMP(3); + +-- AlterTable: Add missing Task columns +ALTER TABLE "Task" ADD COLUMN IF NOT EXISTS "parentTaskId" TEXT; +ALTER TABLE "Task" ADD COLUMN IF NOT EXISTS "deletedAt" TIMESTAMP(3); + +-- CreateTable: WeeklyGoal +CREATE TABLE IF NOT EXISTS "WeeklyGoal" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "weekStart" TIMESTAMP(3) NOT NULL, + "text" TEXT NOT NULL DEFAULT 'your goal of this week', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "WeeklyGoal_pkey" PRIMARY KEY ("id") +); + +-- CreateTable: CachedCalendarEvent +CREATE TABLE IF NOT EXISTS "CachedCalendarEvent" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "externalId" TEXT NOT NULL, + "connectionId" TEXT NOT NULL, + "provider" TEXT NOT NULL, + "calendarId" TEXT NOT NULL, + "calendarTitle" TEXT NOT NULL, + "calendarColor" TEXT, + "title" TEXT NOT NULL, + "description" TEXT, + "location" TEXT, + "startDateTime" TIMESTAMP(3), + "startDate" TEXT, + "endDateTime" TIMESTAMP(3), + "endDate" TEXT, + "weekStart" TIMESTAMP(3) NOT NULL, + "syncedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "CachedCalendarEvent_pkey" PRIMARY KEY ("id") +); + +-- CreateIndexes for WeeklyGoal +CREATE UNIQUE INDEX IF NOT EXISTS "WeeklyGoal_userId_weekStart_key" ON "WeeklyGoal"("userId", "weekStart"); +CREATE INDEX IF NOT EXISTS "WeeklyGoal_userId_idx" ON "WeeklyGoal"("userId"); + +-- CreateIndexes for CachedCalendarEvent +CREATE UNIQUE INDEX IF NOT EXISTS "CachedCalendarEvent_userId_externalId_provider_key" ON "CachedCalendarEvent"("userId", "externalId", "provider"); +CREATE INDEX IF NOT EXISTS "CachedCalendarEvent_userId_startDateTime_idx" ON "CachedCalendarEvent"("userId", "startDateTime"); +CREATE INDEX IF NOT EXISTS "CachedCalendarEvent_userId_startDate_idx" ON "CachedCalendarEvent"("userId", "startDate"); +CREATE INDEX IF NOT EXISTS "CachedCalendarEvent_connectionId_weekStart_idx" ON "CachedCalendarEvent"("connectionId", "weekStart"); + +-- CreateIndexes for Task +CREATE INDEX IF NOT EXISTS "Task_parentTaskId_idx" ON "Task"("parentTaskId"); +CREATE INDEX IF NOT EXISTS "Task_userId_deletedAt_idx" ON "Task"("userId", "deletedAt"); + +-- AddForeignKeys (idempotent) +DO $$ BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.table_constraints + WHERE constraint_name = 'Task_parentTaskId_fkey' + ) THEN + ALTER TABLE "Task" ADD CONSTRAINT "Task_parentTaskId_fkey" FOREIGN KEY ("parentTaskId") REFERENCES "Task"("id") ON DELETE CASCADE ON UPDATE CASCADE; + END IF; +END $$; + +DO $$ BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.table_constraints + WHERE constraint_name = 'WeeklyGoal_userId_fkey' + ) THEN + ALTER TABLE "WeeklyGoal" ADD CONSTRAINT "WeeklyGoal_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + END IF; +END $$; + +DO $$ BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.table_constraints + WHERE constraint_name = 'CachedCalendarEvent_connectionId_fkey' + ) THEN + ALTER TABLE "CachedCalendarEvent" ADD CONSTRAINT "CachedCalendarEvent_connectionId_fkey" FOREIGN KEY ("connectionId") REFERENCES "CalendarConnection"("id") ON DELETE CASCADE ON UPDATE CASCADE; + END IF; +END $$; + +DO $$ BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.table_constraints + WHERE constraint_name = 'CachedCalendarEvent_userId_fkey' + ) THEN + ALTER TABLE "CachedCalendarEvent" ADD CONSTRAINT "CachedCalendarEvent_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + END IF; +END $$; + +-- Fix Task_somedayListId_fkey to use SET NULL on delete +ALTER TABLE "Task" DROP CONSTRAINT IF EXISTS "Task_somedayListId_fkey"; +ALTER TABLE "Task" ADD CONSTRAINT "Task_somedayListId_fkey" FOREIGN KEY ("somedayListId") REFERENCES "SomedayList"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/scripts/docker-entrypoint.sh b/scripts/docker-entrypoint.sh new file mode 100755 index 0000000..4861b19 --- /dev/null +++ b/scripts/docker-entrypoint.sh @@ -0,0 +1,10 @@ +#!/bin/sh +set -e + +# Run migrations using the local prisma binary +echo "Running database migrations..." +node node_modules/prisma/build/index.js migrate deploy + +# Start the application +echo "Starting application..." +exec "$@" diff --git a/src/app/api/calendar/outlook/callback/route.ts b/src/app/api/calendar/outlook/callback/route.ts index 69d2ede..d2df28d 100644 --- a/src/app/api/calendar/outlook/callback/route.ts +++ b/src/app/api/calendar/outlook/callback/route.ts @@ -5,11 +5,13 @@ import { prisma } from '@/lib/prisma'; import { getTokens, getUserCalendars } from '@/lib/outlook-calendar'; export async function GET(request: NextRequest) { + const baseUrl = process.env.NEXTAUTH_URL || request.url; + try { const session = await getServerSession(authOptions); if (!session?.user?.email) { - return NextResponse.redirect(new URL('/auth/login', request.url)); + return NextResponse.redirect(new URL('/auth/login', baseUrl)); } const { searchParams } = new URL(request.url); @@ -18,11 +20,11 @@ export async function GET(request: NextRequest) { if (error) { console.error('Outlook OAuth error:', error); - return NextResponse.redirect(new URL('/?error=outlook_auth_failed', request.url)); + return NextResponse.redirect(new URL('/?error=outlook_auth_failed', baseUrl)); } if (!code) { - return NextResponse.redirect(new URL('/?error=no_code', request.url)); + return NextResponse.redirect(new URL('/?error=no_code', baseUrl)); } // Exchange code for tokens @@ -39,7 +41,7 @@ export async function GET(request: NextRequest) { }); if (!user) { - return NextResponse.redirect(new URL('/auth/login', request.url)); + return NextResponse.redirect(new URL('/auth/login', baseUrl)); } // Calculate expiry date @@ -89,13 +91,13 @@ export async function GET(request: NextRequest) { // Redirect to a client-side page that re-establishes the session // Direct redirects from Microsoft OAuth may lose the session cookie (SameSite policy) - const redirectUrl = new URL('/auth/oauth-complete', request.url); + const redirectUrl = new URL('/auth/oauth-complete', baseUrl); redirectUrl.searchParams.set('provider', 'outlook'); redirectUrl.searchParams.set('status', 'connected'); return NextResponse.redirect(redirectUrl); } catch (error) { console.error('Error in Outlook callback:', error); - const redirectUrl = new URL('/auth/oauth-complete', request.url); + const redirectUrl = new URL('/auth/oauth-complete', baseUrl); redirectUrl.searchParams.set('provider', 'outlook'); redirectUrl.searchParams.set('status', 'error'); redirectUrl.searchParams.set('message', 'outlook_callback_failed'); diff --git a/src/app/api/calendar/outlook/start/route.ts b/src/app/api/calendar/outlook/start/route.ts index 1a7257d..b90e54c 100644 --- a/src/app/api/calendar/outlook/start/route.ts +++ b/src/app/api/calendar/outlook/start/route.ts @@ -8,7 +8,8 @@ export async function GET(request: NextRequest) { const session = await getServerSession(authOptions); if (!session?.user?.email) { - return NextResponse.redirect(new URL('/auth/login', request.url)); + const baseUrl = process.env.NEXTAUTH_URL || request.url; + return NextResponse.redirect(new URL('/auth/login', baseUrl)); } const authUrl = getAuthUrl(); diff --git a/src/app/auth/oauth-complete/page.tsx b/src/app/auth/oauth-complete/page.tsx index 2b11c29..02df654 100644 --- a/src/app/auth/oauth-complete/page.tsx +++ b/src/app/auth/oauth-complete/page.tsx @@ -1,10 +1,10 @@ "use client"; -import { useEffect } from "react"; +import { Suspense, useEffect } from "react"; import { useRouter, useSearchParams } from "next/navigation"; import { useSession } from "next-auth/react"; -export default function OAuthCompletePage() { +function OAuthCompleteContent() { const router = useRouter(); const searchParams = useSearchParams(); const { update } = useSession(); @@ -52,3 +52,21 @@ export default function OAuthCompletePage() { ); } + +export default function OAuthCompletePage() { + return ( + +

Loading...

+ + }> + +
+ ); +} diff --git a/src/app/globals.css b/src/app/globals.css index 146f1b9..12007bf 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -645,13 +645,11 @@ h3 { } /* Weekend Styling */ -.weekly-day-column.is-sat .weekly-day-name, -.weekly-day-column.is-sat .weekly-day-date { +.weekly-day-column.is-sat .weekly-day-name { color: var(--weekly-weekend-sat, #666); } -.weekly-day-column.is-sun .weekly-day-name, -.weekly-day-column.is-sun .weekly-day-date { +.weekly-day-column.is-sun .weekly-day-name { color: var(--weekly-weekend-sun, #dc2626); } diff --git a/src/components/CalendarEventModal.tsx b/src/components/CalendarEventModal.tsx index 70e2b02..f9d0699 100644 --- a/src/components/CalendarEventModal.tsx +++ b/src/components/CalendarEventModal.tsx @@ -42,7 +42,9 @@ export default function CalendarEventModal({ // Default duration: 1 hour. const getInitialStart = () => { + // Support both nested (start.dateTime) and flat (startTime) event formats if (event?.start?.dateTime) return new Date(event.start.dateTime); + if (event?.startTime) return new Date(event.startTime); if (initialDate) { const d = new Date(initialDate); if (initialStartTime) { @@ -59,7 +61,9 @@ export default function CalendarEventModal({ }; const getInitialEnd = () => { + // Support both nested (end.dateTime) and flat (endTime) event formats if (event?.end?.dateTime) return new Date(event.end.dateTime); + if (event?.endTime) return new Date(event.endTime); const start = getInitialStart(); return new Date(start.getTime() + 60 * 60 * 1000); // +1 hour }; diff --git a/src/components/GridTaskBlock.tsx b/src/components/GridTaskBlock.tsx index a56dafd..639204e 100644 --- a/src/components/GridTaskBlock.tsx +++ b/src/components/GridTaskBlock.tsx @@ -105,29 +105,8 @@ export function GridTaskBlock({ }, 0); }; - if (!task.startTime) return null; - - const [startHour, startMinute] = task.startTime.split(":").map(Number); - const startMinutes = (startHour - workingHoursStart) * 60 + startMinute; - - // Calculate top offset + // Calculate dimensions (needed by resize useEffect, must be before early return) const pixelsPerMinute = getSlotHeight(cellDuration) / cellDuration; - const topOffset = startMinutes * pixelsPerMinute; - - // Calculate height - const duration = task.duration || 15; // default 15m if not set - const baseHeight = duration * pixelsPerMinute; - const currentHeight = isResizing && resizeHeight !== null ? resizeHeight : baseHeight; - - // Handlers for resizing - const onResizeStart = (e: React.MouseEvent) => { - e.stopPropagation(); - e.preventDefault(); - setIsResizing(true); - resizeStartY.current = e.clientY; - resizeStartHeight.current = baseHeight; - document.body.style.cursor = "ns-resize"; - }; useEffect(() => { const onResizeMove = (e: MouseEvent) => { @@ -139,7 +118,7 @@ export function GridTaskBlock({ setResizeHeight(newHeight); }; - const onResizeEnd = (e: MouseEvent) => { + const onResizeEnd = () => { if (!isResizing) return; setIsResizing(false); document.body.style.cursor = ""; @@ -161,6 +140,29 @@ export function GridTaskBlock({ }; }, [isResizing, resizeHeight, pixelsPerMinute, task.id, updateTaskDuration]); + if (!task.startTime) return null; + + const [startHour, startMinute] = task.startTime.split(":").map(Number); + const startMinutes = (startHour - workingHoursStart) * 60 + startMinute; + + // Calculate top offset + const topOffset = startMinutes * pixelsPerMinute; + + // Calculate height + const duration = task.duration || 15; // default 15m if not set + const baseHeight = duration * pixelsPerMinute; + const currentHeight = isResizing && resizeHeight !== null ? resizeHeight : baseHeight; + + // Handlers for resizing + const onResizeStart = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + setIsResizing(true); + resizeStartY.current = e.clientY; + resizeStartHeight.current = baseHeight; + document.body.style.cursor = "ns-resize"; + }; + return (
({ name: session?.user?.name || "", @@ -3414,7 +3415,7 @@ export default function WeeklyView() { {/* Refactored Header: Left, Center, Right */}
{/* LEFT SECTION: Slot Duration & Days to Show */} -
+
{/* Slot Duration */} {showTimeGrid && (
{/* CENTER SECTION: Week/Year, Goal, Focus Mode - Reveal on Hover */} -
+
{/* Week & Year */}
{syncError ? ( @@ -3582,7 +3583,13 @@ export default function WeeklyView() { fontSize: profile.goalFontSize || undefined, fontWeight: profile.goalFontWeight || undefined, color: adjustColorForDarkMode((profile.goalFallbackType === "quote" ? profile.taskColor : undefined) || "#333333", darkMode), - filter: "brightness(var(--weekly-goal-brightness, 1))" + filter: "brightness(var(--weekly-goal-brightness, 1))", + maxWidth: "500px", + textAlign: "center" as const, + overflow: "hidden", + display: "-webkit-box", + WebkitLineClamp: 2, + WebkitBoxOrient: "vertical" as const, }} > {showNextTask @@ -3622,7 +3629,7 @@ export default function WeeklyView() {
{/* RIGHT SECTION: Navigation & Tools */} -
+
{/* Undo/Redo */}
+ {/* Calendar Week Font */}
{font.name} ))} - + /> - + />