feat: Goal persistence, font settings, z-index fix, task sync, and scope fix
- Save edited quotes as weekly goals on blur (header + settings panel) - Fresh quote shown when navigating between weeks (no stale cache) - Add goal font family, size, and weight settings with UI controls - Lower calendar event z-index so tasks are always draggable on top - Set line-height: initial on task items for consistent rendering - Fix Google Tasks auth scope from tasks.readonly to tasks (read/write) - Add Google Tasks update/delete functions for bidirectional sync - Sync task renames and deletes back to Google Tasks - Add goal font fields to Prisma schema and profile API Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
798bcfe44a
commit
174c7f3e0e
@ -0,0 +1,4 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "User" ADD COLUMN "goalFontFamily" TEXT DEFAULT 'Inter';
|
||||||
|
ALTER TABLE "User" ADD COLUMN "goalFontSize" TEXT DEFAULT '0.9rem';
|
||||||
|
ALTER TABLE "User" ADD COLUMN "goalFontWeight" TEXT DEFAULT '500';
|
||||||
@ -43,6 +43,9 @@ model User {
|
|||||||
fontSize String @default("M") // "S", "M", "L"
|
fontSize String @default("M") // "S", "M", "L"
|
||||||
goalFallbackType String @default("quote") // "quote" | "next_todo" | "default"
|
goalFallbackType String @default("quote") // "quote" | "next_todo" | "default"
|
||||||
goalDefaultSentence String @default("goal of the week")
|
goalDefaultSentence String @default("goal of the week")
|
||||||
|
goalFontFamily String? @default("Inter")
|
||||||
|
goalFontSize String? @default("0.9rem")
|
||||||
|
goalFontWeight String? @default("500")
|
||||||
headlineFont String @default("Inter")
|
headlineFont String @default("Inter")
|
||||||
headlineFontSize String? @default("1.25rem")
|
headlineFontSize String? @default("1.25rem")
|
||||||
headlineFontWeight String? @default("900")
|
headlineFontWeight String? @default("900")
|
||||||
|
|||||||
@ -36,7 +36,7 @@ export async function GET(request: NextRequest) {
|
|||||||
scope: [
|
scope: [
|
||||||
'https://www.googleapis.com/auth/calendar',
|
'https://www.googleapis.com/auth/calendar',
|
||||||
'https://www.googleapis.com/auth/calendar.events',
|
'https://www.googleapis.com/auth/calendar.events',
|
||||||
'https://www.googleapis.com/auth/tasks.readonly',
|
'https://www.googleapis.com/auth/tasks',
|
||||||
],
|
],
|
||||||
prompt: 'consent',
|
prompt: 'consent',
|
||||||
state: session.user.email, // Pass user email to identify in callback
|
state: session.user.email, // Pass user email to identify in callback
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||||||
import { getServerSession } from 'next-auth';
|
import { getServerSession } from 'next-auth';
|
||||||
import { authOptions } from "@/lib/auth";
|
import { authOptions } from "@/lib/auth";
|
||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
import { createGoogleClient, updateGoogleTaskStatus } from '@/lib/google-tasks';
|
import { createGoogleClient, updateGoogleTaskStatus, updateGoogleTask, deleteGoogleTask } from '@/lib/google-tasks';
|
||||||
import { updateTaskStatus } from '@/lib/apple-calendar';
|
import { updateTaskStatus } from '@/lib/apple-calendar';
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
@ -15,7 +15,7 @@ export async function PATCH(req: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const body = await req.json();
|
const body = await req.json();
|
||||||
const { taskId, completed } = body;
|
const { taskId, completed, title, action } = body;
|
||||||
|
|
||||||
if (!taskId) {
|
if (!taskId) {
|
||||||
return NextResponse.json({ error: 'Task ID required' }, { status: 400 });
|
return NextResponse.json({ error: 'Task ID required' }, { status: 400 });
|
||||||
@ -44,29 +44,47 @@ export async function PATCH(req: NextRequest) {
|
|||||||
|
|
||||||
if (account && account.access_token) {
|
if (account && account.access_token) {
|
||||||
const client = createGoogleClient(account.access_token, account.refresh_token || undefined);
|
const client = createGoogleClient(account.access_token, account.refresh_token || undefined);
|
||||||
await updateGoogleTaskStatus(
|
|
||||||
client,
|
if (action === 'delete') {
|
||||||
task.externalListId,
|
await deleteGoogleTask(client, task.externalListId, task.externalId);
|
||||||
task.externalId,
|
} else if (title !== undefined && completed !== undefined) {
|
||||||
completed ? 'completed' : 'needsAction'
|
await updateGoogleTask(client, task.externalListId, task.externalId, {
|
||||||
);
|
title,
|
||||||
|
status: completed ? 'completed' : 'needsAction',
|
||||||
|
});
|
||||||
|
} else if (title !== undefined) {
|
||||||
|
await updateGoogleTask(client, task.externalListId, task.externalId, { title });
|
||||||
|
} else if (completed !== undefined) {
|
||||||
|
await updateGoogleTaskStatus(
|
||||||
|
client,
|
||||||
|
task.externalListId,
|
||||||
|
task.externalId,
|
||||||
|
completed ? 'completed' : 'needsAction'
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (task.externalProvider === 'apple' && task.externalListId) {
|
else if (task.externalProvider === 'apple' && task.externalListId) {
|
||||||
// Look for apple-reminders connection for task sync
|
|
||||||
const connection = await prisma.calendarConnection.findFirst({
|
const connection = await prisma.calendarConnection.findFirst({
|
||||||
where: { userId: task.userId, provider: 'apple-reminders' }
|
where: { userId: task.userId, provider: 'apple-reminders' }
|
||||||
});
|
});
|
||||||
|
|
||||||
if (connection) {
|
if (connection) {
|
||||||
const [email, password] = connection.accessToken.split(':');
|
const colonIdx = connection.accessToken.indexOf(':');
|
||||||
await updateTaskStatus(
|
const email = connection.accessToken.slice(0, colonIdx);
|
||||||
email,
|
const password = connection.accessToken.slice(colonIdx + 1);
|
||||||
password,
|
|
||||||
task.externalListId, // In our logic, externalListId is the calendar URL
|
if (completed !== undefined) {
|
||||||
task.externalId,
|
await updateTaskStatus(
|
||||||
completed
|
email,
|
||||||
);
|
password,
|
||||||
|
task.externalListId,
|
||||||
|
task.externalId,
|
||||||
|
completed
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Note: Apple Reminders title update and delete via CloudKit
|
||||||
|
// is not currently supported due to API limitations
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,20 +95,20 @@ export async function PATCH(req: NextRequest) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// We also update the local task status if it wasn't already updated by the frontend calling simple toggle
|
// Update local task state
|
||||||
// But usually frontend updates local state then calls this.
|
const updateData: any = {};
|
||||||
// Let's assume this endpoint is purely for triggering the sync side-effect or ensuring consistency.
|
if (completed !== undefined) updateData.completed = completed;
|
||||||
// Actually, strictly speaking, this endpoint is 'sync'. It should probably update the local task too if not done.
|
if (title !== undefined) updateData.title = title;
|
||||||
// But the frontend usually calls `updateTask` (PUT/PATCH /api/tasks/id) for local updates.
|
|
||||||
// Let's assume the frontend calls this *in addition* or we bundle it.
|
|
||||||
// For now, let's explicitely update local state here too to be safe/sure.
|
|
||||||
|
|
||||||
const updatedTask = await prisma.task.update({
|
if (Object.keys(updateData).length > 0) {
|
||||||
where: { id: taskId },
|
const updatedTask = await prisma.task.update({
|
||||||
data: { completed } // Ensure local db matches intent
|
where: { id: taskId },
|
||||||
});
|
data: updateData
|
||||||
|
});
|
||||||
|
return NextResponse.json({ success: true, task: updatedTask });
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json({ success: true, task: updatedTask });
|
return NextResponse.json({ success: true });
|
||||||
|
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
console.error('Sync error:', error);
|
console.error('Sync error:', error);
|
||||||
|
|||||||
@ -36,6 +36,9 @@ export async function GET(request: NextRequest) {
|
|||||||
fontSize: true,
|
fontSize: true,
|
||||||
goalFallbackType: true,
|
goalFallbackType: true,
|
||||||
goalDefaultSentence: true,
|
goalDefaultSentence: true,
|
||||||
|
goalFontFamily: true,
|
||||||
|
goalFontSize: true,
|
||||||
|
goalFontWeight: true,
|
||||||
headlineFont: true,
|
headlineFont: true,
|
||||||
headlineFontSize: true,
|
headlineFontSize: true,
|
||||||
headlineFontWeight: true,
|
headlineFontWeight: true,
|
||||||
@ -94,7 +97,8 @@ export async function PATCH(request: NextRequest) {
|
|||||||
eventFontFamily, eventFontSize, eventFontWeight,
|
eventFontFamily, eventFontSize, eventFontWeight,
|
||||||
fontWeight, weekendColorSat, weekendColorSun,
|
fontWeight, weekendColorSat, weekendColorSun,
|
||||||
weekdayColor, dateColor, taskColor, todayHighlightColor,
|
weekdayColor, dateColor, taskColor, todayHighlightColor,
|
||||||
pastDayColor, goalFallbackType, goalDefaultSentence
|
pastDayColor, goalFallbackType, goalDefaultSentence,
|
||||||
|
goalFontFamily, goalFontSize, goalFontWeight
|
||||||
} = body;
|
} = body;
|
||||||
|
|
||||||
const updateData: any = {
|
const updateData: any = {
|
||||||
@ -145,6 +149,9 @@ export async function PATCH(request: NextRequest) {
|
|||||||
...(pastDayColor !== undefined && { pastDayColor }),
|
...(pastDayColor !== undefined && { pastDayColor }),
|
||||||
...(goalFallbackType !== undefined && { goalFallbackType }),
|
...(goalFallbackType !== undefined && { goalFallbackType }),
|
||||||
...(goalDefaultSentence !== undefined && { goalDefaultSentence }),
|
...(goalDefaultSentence !== undefined && { goalDefaultSentence }),
|
||||||
|
...(goalFontFamily !== undefined && { goalFontFamily }),
|
||||||
|
...(goalFontSize !== undefined && { goalFontSize }),
|
||||||
|
...(goalFontWeight !== undefined && { goalFontWeight }),
|
||||||
};
|
};
|
||||||
if (password && password.trim() !== "") {
|
if (password && password.trim() !== "") {
|
||||||
updateData.passwordHash = await bcrypt.hash(password, 10);
|
updateData.passwordHash = await bcrypt.hash(password, 10);
|
||||||
@ -203,6 +210,9 @@ export async function PATCH(request: NextRequest) {
|
|||||||
pastDayColor: true,
|
pastDayColor: true,
|
||||||
goalFallbackType: true,
|
goalFallbackType: true,
|
||||||
goalDefaultSentence: true,
|
goalDefaultSentence: true,
|
||||||
|
goalFontFamily: true,
|
||||||
|
goalFontSize: true,
|
||||||
|
goalFontWeight: true,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -709,6 +709,7 @@ h3 {
|
|||||||
position: relative;
|
position: relative;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
border-bottom: 1px solid var(--weekly-border); /* Restore lines */
|
border-bottom: 1px solid var(--weekly-border); /* Restore lines */
|
||||||
|
line-height: initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Remove border from last item to look cleaner, or keep for paper look */
|
/* Remove border from last item to look cleaner, or keep for paper look */
|
||||||
@ -1630,8 +1631,10 @@ h3 {
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
transition: color 0.15s ease;
|
transition: color 0.15s ease;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
z-index: 5;
|
||||||
/* Weekly-style: clean text, no boxes */
|
/* Weekly-style: clean text, no boxes */
|
||||||
color: var(--weekly-text);
|
color: var(--weekly-text);
|
||||||
|
line-height: initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-slot-task:hover {
|
.time-slot-task:hover {
|
||||||
|
|||||||
@ -455,6 +455,9 @@ export default function WeeklyView() {
|
|||||||
pastDayColor?: string;
|
pastDayColor?: string;
|
||||||
goalFallbackType?: 'quote' | 'next_todo' | 'default';
|
goalFallbackType?: 'quote' | 'next_todo' | 'default';
|
||||||
goalDefaultSentence?: string;
|
goalDefaultSentence?: string;
|
||||||
|
goalFontFamily?: string;
|
||||||
|
goalFontSize?: string;
|
||||||
|
goalFontWeight?: string;
|
||||||
}>({
|
}>({
|
||||||
name: session?.user?.name || '',
|
name: session?.user?.name || '',
|
||||||
email: session?.user?.email || '',
|
email: session?.user?.email || '',
|
||||||
@ -491,6 +494,9 @@ export default function WeeklyView() {
|
|||||||
eventFontSize: '0.85rem',
|
eventFontSize: '0.85rem',
|
||||||
eventFontWeight: '400',
|
eventFontWeight: '400',
|
||||||
fontWeight: '400',
|
fontWeight: '400',
|
||||||
|
goalFontFamily: 'Inter',
|
||||||
|
goalFontSize: '0.9rem',
|
||||||
|
goalFontWeight: '500',
|
||||||
weekendColorSat: '#666666',
|
weekendColorSat: '#666666',
|
||||||
weekendColorSun: '#dc2626',
|
weekendColorSun: '#dc2626',
|
||||||
focusTimerDuration: 25,
|
focusTimerDuration: 25,
|
||||||
@ -1572,6 +1578,16 @@ export default function WeeklyView() {
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ id: taskId, title: newTitle.trim() }),
|
body: JSON.stringify({ id: taskId, title: newTitle.trim() }),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Sync title change to external provider if applicable
|
||||||
|
const task = tasks.find(t => t.id === taskId);
|
||||||
|
if (task?.externalId && task?.externalProvider) {
|
||||||
|
fetch('/api/tasks/sync', {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ taskId, title: newTitle.trim() }),
|
||||||
|
}).catch(e => console.error('Sync error:', e));
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating task:', error);
|
console.error('Error updating task:', error);
|
||||||
}
|
}
|
||||||
@ -1727,11 +1743,20 @@ export default function WeeklyView() {
|
|||||||
setEditingTaskId(null);
|
setEditingTaskId(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Sync delete to external provider if applicable
|
||||||
|
const origTask = tasks.find(t => t.id === originalId);
|
||||||
|
if (origTask?.externalId && origTask?.externalProvider) {
|
||||||
|
fetch('/api/tasks/sync', {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ taskId: originalId, action: 'delete' }),
|
||||||
|
}).catch(e => console.error('Sync delete error:', e));
|
||||||
|
}
|
||||||
|
|
||||||
// Deleting the original ID stops the series
|
// Deleting the original ID stops the series
|
||||||
await fetch(`/api/tasks?id=${originalId}`, { method: 'DELETE' });
|
await fetch(`/api/tasks?id=${originalId}`, { method: 'DELETE' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting series:', error);
|
console.error('Error deleting series:', error);
|
||||||
// Optionally revert UI state here if needed, but for now assuming success
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1742,6 +1767,15 @@ export default function WeeklyView() {
|
|||||||
setEditingTaskId(null);
|
setEditingTaskId(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Sync delete to external provider if applicable
|
||||||
|
if (taskToDelete?.externalId && taskToDelete?.externalProvider) {
|
||||||
|
fetch('/api/tasks/sync', {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ taskId, action: 'delete' }),
|
||||||
|
}).catch(e => console.error('Sync delete error:', e));
|
||||||
|
}
|
||||||
|
|
||||||
await fetch(`/api/tasks?id=${taskId}`, { method: 'DELETE' });
|
await fetch(`/api/tasks?id=${taskId}`, { method: 'DELETE' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting task:', error);
|
console.error('Error deleting task:', error);
|
||||||
@ -2184,17 +2218,27 @@ export default function WeeklyView() {
|
|||||||
type="text"
|
type="text"
|
||||||
value={goal}
|
value={goal}
|
||||||
onChange={(e) => setGoal(e.target.value)}
|
onChange={(e) => setGoal(e.target.value)}
|
||||||
onBlur={() => setIsEditingGoal(false)}
|
onBlur={() => { saveGoal(goal); setIsEditingGoal(false); }}
|
||||||
onKeyDown={(e) => e.key === 'Enter' && e.currentTarget.blur()}
|
onKeyDown={(e) => e.key === 'Enter' && e.currentTarget.blur()}
|
||||||
autoFocus
|
autoFocus
|
||||||
className="border-b border-gray-300 focus:outline-none focus:border-black px-1 text-center font-medium italic"
|
className="border-b border-gray-300 focus:outline-none focus:border-black px-1 text-center font-medium italic"
|
||||||
style={{ width: `${Math.max(10, goal.length)}ch` }}
|
style={{
|
||||||
|
width: `${Math.max(10, goal.length)}ch`,
|
||||||
|
fontFamily: profile.goalFontFamily ? `"${profile.goalFontFamily}", sans-serif` : undefined,
|
||||||
|
fontSize: profile.goalFontSize || undefined,
|
||||||
|
fontWeight: profile.goalFontWeight || undefined,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<span
|
<span
|
||||||
onClick={() => !showNextTask && setIsEditingGoal(true)}
|
onClick={() => !showNextTask && setIsEditingGoal(true)}
|
||||||
className={`cursor-pointer font-medium italic text-gray-600 hover:text-black transition-colors ${showNextTask ? 'cursor-default' : ''}`}
|
className={`cursor-pointer font-medium italic text-gray-600 hover:text-black transition-colors ${showNextTask ? 'cursor-default' : ''}`}
|
||||||
title={showNextTask ? "Next task" : "Edit goal"}
|
title={showNextTask ? "Next task" : "Edit goal"}
|
||||||
|
style={{
|
||||||
|
fontFamily: profile.goalFontFamily ? `"${profile.goalFontFamily}", sans-serif` : undefined,
|
||||||
|
fontSize: profile.goalFontSize || undefined,
|
||||||
|
fontWeight: profile.goalFontWeight || undefined,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{showNextTask ? (() => {
|
{showNextTask ? (() => {
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
@ -2442,7 +2486,7 @@ export default function WeeklyView() {
|
|||||||
right: 0,
|
right: 0,
|
||||||
height: `${height}px`,
|
height: `${height}px`,
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
pointerEvents: 'none'
|
pointerEvents: 'none',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
@ -2660,7 +2704,7 @@ export default function WeeklyView() {
|
|||||||
top: `${topOffset}px`,
|
top: `${topOffset}px`,
|
||||||
left: '-10px',
|
left: '-10px',
|
||||||
right: '-15px',
|
right: '-15px',
|
||||||
zIndex: 5,
|
zIndex: 1,
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
alignItems: 'flex-start',
|
alignItems: 'flex-start',
|
||||||
backgroundColor: bgColor,
|
backgroundColor: bgColor,
|
||||||
@ -4205,6 +4249,9 @@ function SettingsSidebar({
|
|||||||
pastDayColor?: string;
|
pastDayColor?: string;
|
||||||
goalFallbackType?: 'quote' | 'next_todo' | 'default';
|
goalFallbackType?: 'quote' | 'next_todo' | 'default';
|
||||||
goalDefaultSentence?: string;
|
goalDefaultSentence?: string;
|
||||||
|
goalFontFamily?: string;
|
||||||
|
goalFontSize?: string;
|
||||||
|
goalFontWeight?: string;
|
||||||
}>({
|
}>({
|
||||||
name: '',
|
name: '',
|
||||||
email: '',
|
email: '',
|
||||||
@ -4240,6 +4287,9 @@ function SettingsSidebar({
|
|||||||
taskFontSize: '0.9rem',
|
taskFontSize: '0.9rem',
|
||||||
taskFontWeight: '400',
|
taskFontWeight: '400',
|
||||||
fontWeight: '400',
|
fontWeight: '400',
|
||||||
|
goalFontFamily: 'Inter',
|
||||||
|
goalFontSize: '0.9rem',
|
||||||
|
goalFontWeight: '500',
|
||||||
weekendColorSat: '#666666',
|
weekendColorSat: '#666666',
|
||||||
weekendColorSun: '#dc2626',
|
weekendColorSun: '#dc2626',
|
||||||
weekdayColor: '#888888',
|
weekdayColor: '#888888',
|
||||||
@ -4310,6 +4360,9 @@ function SettingsSidebar({
|
|||||||
eventFontFamily: data.user.eventFontFamily || 'Inter',
|
eventFontFamily: data.user.eventFontFamily || 'Inter',
|
||||||
eventFontSize: data.user.eventFontSize || '0.85rem',
|
eventFontSize: data.user.eventFontSize || '0.85rem',
|
||||||
eventFontWeight: data.user.eventFontWeight || '400',
|
eventFontWeight: data.user.eventFontWeight || '400',
|
||||||
|
goalFontFamily: data.user.goalFontFamily || 'Inter',
|
||||||
|
goalFontSize: data.user.goalFontSize || '0.9rem',
|
||||||
|
goalFontWeight: data.user.goalFontWeight || '500',
|
||||||
weekendColorSat: data.user.weekendColorSat || '#666666',
|
weekendColorSat: data.user.weekendColorSat || '#666666',
|
||||||
weekendColorSun: data.user.weekendColorSun || '#dc2626',
|
weekendColorSun: data.user.weekendColorSun || '#dc2626',
|
||||||
weekdayColor: data.user.weekdayColor || '#888888',
|
weekdayColor: data.user.weekdayColor || '#888888',
|
||||||
@ -4672,6 +4725,8 @@ function SettingsSidebar({
|
|||||||
type="text"
|
type="text"
|
||||||
value={goal}
|
value={goal}
|
||||||
onChange={(e) => setGoal(e.target.value)}
|
onChange={(e) => setGoal(e.target.value)}
|
||||||
|
onBlur={() => saveGoal(goal)}
|
||||||
|
onKeyDown={(e) => e.key === 'Enter' && (e.currentTarget.blur())}
|
||||||
className="weekly-input"
|
className="weekly-input"
|
||||||
placeholder={t.goalOfWeek}
|
placeholder={t.goalOfWeek}
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
@ -5079,6 +5134,47 @@ function SettingsSidebar({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Goal Font */}
|
||||||
|
<div style={{ background: 'var(--weekly-settings-item-bg)', padding: '12px', borderRadius: '8px', marginBottom: '12px' }}>
|
||||||
|
<label style={{ display: 'block', fontSize: '0.85rem', fontWeight: 600, color: 'var(--weekly-settings-label)', marginBottom: '8px' }}>Goal Font</label>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr 1fr', gap: '8px' }}>
|
||||||
|
<select
|
||||||
|
value={profile.goalFontFamily || 'Inter'}
|
||||||
|
onChange={(e) => setProfile({ ...profile, goalFontFamily: e.target.value })}
|
||||||
|
className="weekly-input"
|
||||||
|
style={{ width: '100%', padding: '8px', fontSize: '0.9rem' }}
|
||||||
|
>
|
||||||
|
{AVAILABLE_FONTS.map(font => (
|
||||||
|
<option key={font.value} value={font.value} style={{ fontFamily: font.value }}>{font.name}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<select
|
||||||
|
value={profile.goalFontSize || '0.9rem'}
|
||||||
|
onChange={(e) => setProfile({ ...profile, goalFontSize: e.target.value })}
|
||||||
|
className="weekly-input"
|
||||||
|
style={{ width: '100%', padding: '8px', fontSize: '0.9rem' }}
|
||||||
|
>
|
||||||
|
<option value="0.75rem">Small 12px</option>
|
||||||
|
<option value="0.85rem">Normal 14px</option>
|
||||||
|
<option value="0.9rem">Default 14px</option>
|
||||||
|
<option value="1rem">Medium 16px</option>
|
||||||
|
<option value="1.1rem">Large 18px</option>
|
||||||
|
<option value="1.25rem">XL 20px</option>
|
||||||
|
</select>
|
||||||
|
<select
|
||||||
|
value={profile.goalFontWeight || '500'}
|
||||||
|
onChange={(e) => setProfile({ ...profile, goalFontWeight: e.target.value })}
|
||||||
|
className="weekly-input"
|
||||||
|
style={{ width: '100%', padding: '8px', fontSize: '0.9rem', border: '1px solid var(--weekly-settings-input-border)', borderRadius: '4px', background: 'var(--weekly-settings-input-bg)', color: 'var(--weekly-settings-text)' }}
|
||||||
|
>
|
||||||
|
<option value="300">Light</option>
|
||||||
|
<option value="400">Normal</option>
|
||||||
|
<option value="500">Medium</option>
|
||||||
|
<option value="600">Semi</option>
|
||||||
|
<option value="700">Bold</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Element Colors */}
|
{/* Element Colors */}
|
||||||
<div style={{ background: 'var(--weekly-settings-item-bg)', padding: '12px', borderRadius: '8px', marginBottom: '12px' }}>
|
<div style={{ background: 'var(--weekly-settings-item-bg)', padding: '12px', borderRadius: '8px', marginBottom: '12px' }}>
|
||||||
|
|||||||
@ -79,6 +79,49 @@ export const fetchGoogleTasks = async (client: OAuth2Client, taskListId: string)
|
|||||||
/**
|
/**
|
||||||
* Update a Google Task status
|
* Update a Google Task status
|
||||||
*/
|
*/
|
||||||
|
export const updateGoogleTask = async (client: OAuth2Client, taskListId: string, taskId: string, updates: { title?: string; notes?: string; status?: 'needsAction' | 'completed' }): Promise<GoogleTask> => {
|
||||||
|
const service = google.tasks({ version: 'v1', auth: client });
|
||||||
|
try {
|
||||||
|
const requestBody: any = {};
|
||||||
|
if (updates.title !== undefined) requestBody.title = updates.title;
|
||||||
|
if (updates.notes !== undefined) requestBody.notes = updates.notes;
|
||||||
|
if (updates.status !== undefined) {
|
||||||
|
requestBody.status = updates.status;
|
||||||
|
requestBody.completed = updates.status === 'completed' ? new Date().toISOString() : null;
|
||||||
|
}
|
||||||
|
const response = await service.tasks.patch({
|
||||||
|
tasklist: taskListId,
|
||||||
|
task: taskId,
|
||||||
|
requestBody,
|
||||||
|
});
|
||||||
|
const item = response.data;
|
||||||
|
return {
|
||||||
|
id: item.id!,
|
||||||
|
title: item.title!,
|
||||||
|
notes: item.notes || undefined,
|
||||||
|
status: item.status!,
|
||||||
|
due: item.due || undefined,
|
||||||
|
updated: item.updated!
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error updating Google Task ${taskId}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteGoogleTask = async (client: OAuth2Client, taskListId: string, taskId: string): Promise<void> => {
|
||||||
|
const service = google.tasks({ version: 'v1', auth: client });
|
||||||
|
try {
|
||||||
|
await service.tasks.delete({
|
||||||
|
tasklist: taskListId,
|
||||||
|
task: taskId,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error deleting Google Task ${taskId}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const updateGoogleTaskStatus = async (client: OAuth2Client, taskListId: string, taskId: string, status: 'needsAction' | 'completed'): Promise<GoogleTask> => {
|
export const updateGoogleTaskStatus = async (client: OAuth2Client, taskListId: string, taskId: string, status: 'needsAction' | 'completed'): Promise<GoogleTask> => {
|
||||||
const service = google.tasks({ version: 'v1', auth: client });
|
const service = google.tasks({ version: 'v1', auth: client });
|
||||||
try {
|
try {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user