feat: improve calendar sync, event modal, and task list management
- Fix All Day checkbox positioning in CalendarEventModal (own row) - Add provider name to calendar dropdown (Google/Apple/Outlook) - Optimistic UI updates after event save/delete (no reload needed) - Force-refresh calendar cache after event mutations - Reduce background sync interval from 5min to 2min - Support forceRefresh in background-sync API - Use shared Prisma singleton in tasks sync route - Add per-provider task list fetching and sync checkboxes - Add allDay support to event creation and editing v1.4.0
This commit is contained in:
parent
210a142f61
commit
8842123caf
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "my-weekly-todo-list",
|
"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",
|
"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": {
|
||||||
|
|||||||
@ -12,7 +12,7 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const body = await request.json().catch(() => ({}));
|
const body = await request.json().catch(() => ({}));
|
||||||
const { timeMin, timeMax } = body;
|
const { timeMin, timeMax, forceRefresh } = body;
|
||||||
|
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: { email: session.user.email },
|
where: { email: session.user.email },
|
||||||
@ -30,7 +30,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const staleChecks = await Promise.all(
|
const staleChecks = await Promise.all(
|
||||||
user.calendarConnections.map(async conn => ({
|
user.calendarConnections.map(async conn => ({
|
||||||
conn,
|
conn,
|
||||||
stale: await isCacheStale(conn.id, tMin),
|
stale: forceRefresh || await isCacheStale(conn.id, tMin),
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -39,7 +39,7 @@ export async function POST(request: NextRequest) {
|
|||||||
if (!session?.user?.email) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
if (!session?.user?.email) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
|
||||||
const body = await request.json();
|
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 });
|
console.log('[API] Creating event:', { calendarId, title, start, end });
|
||||||
|
|
||||||
@ -59,7 +59,8 @@ export async function POST(request: NextRequest) {
|
|||||||
description,
|
description,
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
location
|
location,
|
||||||
|
allDay: !!allDay
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update cache
|
// Update cache
|
||||||
@ -80,7 +81,7 @@ export async function PATCH(request: NextRequest) {
|
|||||||
if (!session?.user?.email) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
if (!session?.user?.email) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
|
||||||
const body = await request.json();
|
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 });
|
console.log('[API] Updating event:', { calendarId, eventId, title });
|
||||||
|
|
||||||
@ -100,7 +101,8 @@ export async function PATCH(request: NextRequest) {
|
|||||||
description,
|
description,
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
location
|
location,
|
||||||
|
allDay: allDay !== undefined ? !!allDay : undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update cache
|
// Update cache
|
||||||
|
|||||||
@ -1,13 +1,11 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
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 { prisma } from '@/lib/prisma';
|
||||||
import { createGoogleClient, updateGoogleTaskStatus, updateGoogleTask, deleteGoogleTask, fetchGoogleTasksForSync, GoogleTask } from '@/lib/google-tasks';
|
import { createGoogleClient, updateGoogleTaskStatus, updateGoogleTask, deleteGoogleTask, fetchGoogleTasksForSync, GoogleTask } from '@/lib/google-tasks';
|
||||||
import { fetchMsTodoTasksForSync, updateMsTodoTask, deleteMsTodoTask, createMsTodoTask, isMsTodoTaskCompleted } from '@/lib/microsoft-todo';
|
import { fetchMsTodoTasksForSync, updateMsTodoTask, deleteMsTodoTask, createMsTodoTask, isMsTodoTaskCompleted } from '@/lib/microsoft-todo';
|
||||||
import { getOutlookAccessToken } from '@/lib/outlook-token';
|
import { getOutlookAccessToken } from '@/lib/outlook-token';
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
// GET - Pull changes from Google Tasks into local DB
|
// GET - Pull changes from Google Tasks into local DB
|
||||||
export async function GET(req: NextRequest) {
|
export async function GET(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
@ -478,6 +476,7 @@ export async function POST(req: NextRequest) {
|
|||||||
const created = await createMsTodoTask(outlookToken, listExternalId, {
|
const created = await createMsTodoTask(outlookToken, listExternalId, {
|
||||||
title: task.title,
|
title: task.title,
|
||||||
body: task.description || undefined,
|
body: task.description || undefined,
|
||||||
|
dueDateTime: task.scheduledDate ? task.scheduledDate.toISOString() : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const updatedTask = await prisma.task.update({
|
const updatedTask = await prisma.task.update({
|
||||||
|
|||||||
@ -800,6 +800,80 @@ h3 {
|
|||||||
outline: none;
|
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 {
|
.weekly-notes-editor-inline:focus {
|
||||||
border-color: var(--weekly-teal);
|
border-color: var(--weekly-teal);
|
||||||
background: #fff;
|
background: #fff;
|
||||||
@ -1753,6 +1827,7 @@ h3 {
|
|||||||
.time-slots-container {
|
.time-slots-container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
overflow-x: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-slot {
|
.time-slot {
|
||||||
@ -2709,18 +2784,129 @@ h3 {
|
|||||||
|
|
||||||
.weekly-settings-sidebar .weekly-settings-header {
|
.weekly-settings-sidebar .weekly-settings-header {
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
border-bottom: 1px solid #eee;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
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 {
|
.settings-tab-btn:hover {
|
||||||
opacity: 0.8 !important;
|
opacity: 0.8 !important;
|
||||||
background: rgba(0, 0, 0, 0.04) !important;
|
background: rgba(0, 0, 0, 0.04) !important;
|
||||||
border-radius: 6px 6px 0 0;
|
border-radius: 6px 6px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark-mode .settings-tab-btn:hover {
|
.dark-mode .settings-tab-btn:hover {
|
||||||
background: rgba(255, 255, 255, 0.08) !important;
|
background: rgba(255, 255, 255, 0.08) !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,7 +21,12 @@ export default function CalendarEventModal({
|
|||||||
}: CalendarEventModalProps) {
|
}: CalendarEventModalProps) {
|
||||||
// Flatten calendars from connections to get selectable options
|
// Flatten calendars from connections to get selectable options
|
||||||
const availableCalendars = connections
|
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
|
.filter((cal: any) => cal.editable); // Only editable calendars
|
||||||
|
|
||||||
const [title, setTitle] = useState(event?.title || '');
|
const [title, setTitle] = useState(event?.title || '');
|
||||||
@ -59,6 +64,7 @@ export default function CalendarEventModal({
|
|||||||
|
|
||||||
const [startDate, setStartDate] = useState(getInitialStart());
|
const [startDate, setStartDate] = useState(getInitialStart());
|
||||||
const [endDate, setEndDate] = useState(getInitialEnd());
|
const [endDate, setEndDate] = useState(getInitialEnd());
|
||||||
|
const [allDay, setAllDay] = useState(!!event?.allDay);
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
@ -85,6 +91,7 @@ export default function CalendarEventModal({
|
|||||||
description,
|
description,
|
||||||
location,
|
location,
|
||||||
calendarId,
|
calendarId,
|
||||||
|
allDay,
|
||||||
start: { dateTime: startDate.toISOString() },
|
start: { dateTime: startDate.toISOString() },
|
||||||
end: { dateTime: endDate.toISOString() }
|
end: { dateTime: endDate.toISOString() }
|
||||||
});
|
});
|
||||||
@ -170,18 +177,32 @@ export default function CalendarEventModal({
|
|||||||
>
|
>
|
||||||
{availableCalendars.length === 0 && <option value="">No editable calendars</option>}
|
{availableCalendars.length === 0 && <option value="">No editable calendars</option>}
|
||||||
{availableCalendars.map((cal: any) => (
|
{availableCalendars.map((cal: any) => (
|
||||||
<option key={cal.id} value={cal.id}>{cal.summary || cal.title}</option>
|
<option key={cal.id} value={cal.id}>
|
||||||
|
{cal.summary || cal.title} ({cal.providerName})
|
||||||
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* All Day Toggle */}
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer', fontSize: '0.9rem', color: '#666' }}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={allDay}
|
||||||
|
onChange={e => setAllDay(e.target.checked)}
|
||||||
|
/>
|
||||||
|
All Day
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Date/Time */}
|
{/* Date/Time */}
|
||||||
<div style={{ display: 'flex', gap: '15px' }}>
|
<div style={{ display: 'flex', gap: '15px' }}>
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
<label style={{ display: 'block', marginBottom: '5px', fontSize: '0.9rem', color: '#666' }}>Start</label>
|
<label style={{ display: 'block', marginBottom: '5px', fontSize: '0.9rem', color: '#666' }}>Start</label>
|
||||||
<input
|
<input
|
||||||
type="datetime-local"
|
type={allDay ? "date" : "datetime-local"}
|
||||||
value={toLocalISOString(startDate)}
|
value={allDay ? startDate.toISOString().split('T')[0] : toLocalISOString(startDate)}
|
||||||
onChange={e => handleStartDateChange(e.target.value)}
|
onChange={e => handleStartDateChange(e.target.value)}
|
||||||
style={{ width: '100%', padding: '8px', border: '1px solid #ddd', borderRadius: '4px' }}
|
style={{ width: '100%', padding: '8px', border: '1px solid #ddd', borderRadius: '4px' }}
|
||||||
/>
|
/>
|
||||||
@ -189,8 +210,8 @@ export default function CalendarEventModal({
|
|||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
<label style={{ display: 'block', marginBottom: '5px', fontSize: '0.9rem', color: '#666' }}>End</label>
|
<label style={{ display: 'block', marginBottom: '5px', fontSize: '0.9rem', color: '#666' }}>End</label>
|
||||||
<input
|
<input
|
||||||
type="datetime-local"
|
type={allDay ? "date" : "datetime-local"}
|
||||||
value={toLocalISOString(endDate)}
|
value={allDay ? endDate.toISOString().split('T')[0] : toLocalISOString(endDate)}
|
||||||
onChange={e => setEndDate(new Date(e.target.value))}
|
onChange={e => setEndDate(new Date(e.target.value))}
|
||||||
style={{ width: '100%', padding: '8px', border: '1px solid #ddd', borderRadius: '4px' }}
|
style={{ width: '100%', padding: '8px', border: '1px solid #ddd', borderRadius: '4px' }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -171,7 +171,7 @@ export function GridTaskBlock({
|
|||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
minHeight: `${Math.max(currentHeight, 20)}px`,
|
minHeight: `${Math.max(currentHeight, 20)}px`,
|
||||||
height: isNotesOpen || isSubTasksOpen ? "auto" : `${currentHeight}px`,
|
height: isSubTasksOpen ? "auto" : `${currentHeight}px`,
|
||||||
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",
|
||||||
@ -342,7 +342,19 @@ export function GridTaskBlock({
|
|||||||
{/* Inline Expanders Container */}
|
{/* Inline Expanders Container */}
|
||||||
<div style={{ paddingLeft: "4px", paddingRight: "4px", paddingBottom: "10px", marginTop: "4px" }}>
|
<div style={{ paddingLeft: "4px", paddingRight: "4px", paddingBottom: "10px", marginTop: "4px" }}>
|
||||||
{isNotesOpen && (
|
{isNotesOpen && (
|
||||||
<div className="weekly-notes-inline mt-1" onClick={(e) => e.stopPropagation()}>
|
<div
|
||||||
|
className={`weekly-notes-popup ${topOffset > 180 ? "on-top" : ""}`}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
style={{
|
||||||
|
top: topOffset > 180 ? "auto" : "100%",
|
||||||
|
bottom: topOffset > 180 ? "100%" : "auto",
|
||||||
|
marginTop: topOffset > 180 ? "0" : "10px",
|
||||||
|
marginBottom: topOffset > 180 ? "10px" : "0",
|
||||||
|
left: "-10px",
|
||||||
|
right: "-10px",
|
||||||
|
width: "auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div className="notes-toolbar">
|
<div className="notes-toolbar">
|
||||||
<button className="notes-toolbar-btn" onClick={() => insertMarkdown("**", "**")} title="Bold">B</button>
|
<button className="notes-toolbar-btn" onClick={() => insertMarkdown("**", "**")} title="Bold">B</button>
|
||||||
<button className="notes-toolbar-btn" onClick={() => insertMarkdown("*", "*")} title="Italic">i</button>
|
<button className="notes-toolbar-btn" onClick={() => insertMarkdown("*", "*")} title="Italic">i</button>
|
||||||
@ -356,7 +368,7 @@ export function GridTaskBlock({
|
|||||||
onChange={(e) => setNotesValue(e.target.value)}
|
onChange={(e) => setNotesValue(e.target.value)}
|
||||||
onBlur={handleNotesBlur}
|
onBlur={handleNotesBlur}
|
||||||
placeholder="Add notes..."
|
placeholder="Add notes..."
|
||||||
style={{ minHeight: "60px", padding: "4px" }}
|
style={{ minHeight: "120px", padding: "4px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -108,6 +108,8 @@ interface SomedayList {
|
|||||||
title: string;
|
title: string;
|
||||||
tasks: Task[];
|
tasks: Task[];
|
||||||
externalProvider?: string | null;
|
externalProvider?: string | null;
|
||||||
|
externalId?: string | null;
|
||||||
|
externalListId?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Time grid configuration options
|
// Time grid configuration options
|
||||||
@ -236,6 +238,8 @@ const translations: Record<string, any> = {
|
|||||||
simpleView: "Einfach",
|
simpleView: "Einfach",
|
||||||
calendarView: "Kalender",
|
calendarView: "Kalender",
|
||||||
listView: "Liste",
|
listView: "Liste",
|
||||||
|
notes: "Notizen",
|
||||||
|
notesSidebar: "Notizen-Seitenleiste",
|
||||||
language: "Sprache",
|
language: "Sprache",
|
||||||
dateFormat: "Datumsformat",
|
dateFormat: "Datumsformat",
|
||||||
timeFormat: "Zeitformat",
|
timeFormat: "Zeitformat",
|
||||||
@ -529,6 +533,12 @@ export default function WeeklyView() {
|
|||||||
{ id: string; title: string }[]
|
{ id: string; title: string }[]
|
||||||
>([]);
|
>([]);
|
||||||
const [isFetchingLists, setIsFetchingLists] = useState(false);
|
const [isFetchingLists, setIsFetchingLists] = useState(false);
|
||||||
|
const [availableTaskLists, setAvailableTaskLists] = useState<{
|
||||||
|
[key in "google" | "apple" | "outlook"]?: { id: string; title: string }[];
|
||||||
|
}>({});
|
||||||
|
const [isFetchingProviderLists, setIsFetchingProviderLists] = useState<
|
||||||
|
Record<string, boolean>
|
||||||
|
>({});
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
const [profile, setProfile] = useState<{
|
const [profile, setProfile] = useState<{
|
||||||
name: string;
|
name: string;
|
||||||
@ -921,8 +931,20 @@ export default function WeeklyView() {
|
|||||||
throw new Error(err.error || "Failed to save event");
|
throw new Error(err.error || "Failed to save event");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh events
|
// Optimistically add/update from API response, then force refresh cache
|
||||||
await fetchCalendarEvents();
|
const data = await res.json();
|
||||||
|
if (data.event) {
|
||||||
|
setRawCalendarEvents(prev => {
|
||||||
|
if (eventData.id) {
|
||||||
|
// Update existing
|
||||||
|
return prev.map(e => e.id === eventData.id ? data.event : e);
|
||||||
|
}
|
||||||
|
// Add new
|
||||||
|
return [...prev, data.event];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Also force-refresh from provider to ensure full sync
|
||||||
|
fetchCalendarEvents(true);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("Error saving event:", error);
|
console.error("Error saving event:", error);
|
||||||
if (error.name === "AbortError") {
|
if (error.name === "AbortError") {
|
||||||
@ -948,8 +970,9 @@ export default function WeeklyView() {
|
|||||||
throw new Error(err.error || "Failed to delete event");
|
throw new Error(err.error || "Failed to delete event");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh events
|
// Optimistically remove, then force refresh
|
||||||
await fetchCalendarEvents();
|
setRawCalendarEvents(prev => prev.filter(e => e.id !== eventId));
|
||||||
|
fetchCalendarEvents(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error deleting event:", error);
|
console.error("Error deleting event:", error);
|
||||||
throw error;
|
throw error;
|
||||||
@ -1057,7 +1080,7 @@ export default function WeeklyView() {
|
|||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [session]);
|
}, [session]);
|
||||||
|
|
||||||
// Periodic background calendar cache refresh (every 5 minutes)
|
// Periodic background calendar cache refresh (every 2 minutes)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!session) return;
|
if (!session) return;
|
||||||
const interval = setInterval(
|
const interval = setInterval(
|
||||||
@ -1074,12 +1097,13 @@ export default function WeeklyView() {
|
|||||||
timeMax: new Date(
|
timeMax: new Date(
|
||||||
now.getTime() + 14 * 24 * 60 * 60 * 1000,
|
now.getTime() + 14 * 24 * 60 * 60 * 1000,
|
||||||
).toISOString(),
|
).toISOString(),
|
||||||
|
forceRefresh: true,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.queued > 0) {
|
if (data.queued > 0 || data.refreshed > 0) {
|
||||||
// Stale connections are being refreshed; re-fetch events after delay
|
// Cache was refreshed; re-fetch events after delay
|
||||||
setTimeout(() => fetchCalendarEvents(), 8000);
|
setTimeout(() => fetchCalendarEvents(), 8000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1087,7 +1111,7 @@ export default function WeeklyView() {
|
|||||||
// Silent fail for background sync
|
// Silent fail for background sync
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
5 * 60 * 1000,
|
2 * 60 * 1000,
|
||||||
);
|
);
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [session, fetchCalendarEvents]);
|
}, [session, fetchCalendarEvents]);
|
||||||
@ -1946,6 +1970,75 @@ export default function WeeklyView() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchAvailableTaskLists = useCallback(
|
||||||
|
async (provider: "google" | "apple" | "outlook") => {
|
||||||
|
setIsFetchingProviderLists((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[provider]: true,
|
||||||
|
}));
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/tasks/lists?provider=${provider}`);
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
setAvailableTaskLists((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[provider]: data.lists || [],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to fetch lists for ${provider}`, error);
|
||||||
|
} finally {
|
||||||
|
setIsFetchingProviderLists((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[provider]: false,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
const handleToggleTaskList = async (
|
||||||
|
provider: "google" | "apple" | "outlook",
|
||||||
|
list: { id: string; title: string },
|
||||||
|
) => {
|
||||||
|
const existing = somedayLists.find(
|
||||||
|
(l) => l.externalId === list.id && l.externalProvider === provider,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
// Unsync/Remove
|
||||||
|
if (
|
||||||
|
!confirm(
|
||||||
|
`Are you sure you want to stop syncing the list "${list.title}"? This will move its tasks to the trash.`,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/someday-lists?id=${existing.id}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
|
if (res.ok) {
|
||||||
|
setSomedayLists((prev) => prev.filter((l) => l.id !== existing.id));
|
||||||
|
setImportStatusMsg({
|
||||||
|
type: "success",
|
||||||
|
text: `Stopped syncing "${list.title}".`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to delete list", error);
|
||||||
|
setImportStatusMsg({
|
||||||
|
type: "error",
|
||||||
|
text: "Failed to stop syncing list.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Sync/Import
|
||||||
|
await doImport(provider, [list]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Core import logic — accepts provider directly so it works both from modal and sidebar
|
// Core import logic — accepts provider directly so it works both from modal and sidebar
|
||||||
const doImport = async (
|
const doImport = async (
|
||||||
provider: "google" | "apple" | "outlook",
|
provider: "google" | "apple" | "outlook",
|
||||||
@ -5350,6 +5443,11 @@ export default function WeeklyView() {
|
|||||||
allDayPosition={allDayPosition}
|
allDayPosition={allDayPosition}
|
||||||
setAllDayPosition={setAllDayPosition}
|
setAllDayPosition={setAllDayPosition}
|
||||||
saveSetting={saveSetting}
|
saveSetting={saveSetting}
|
||||||
|
availableTaskLists={availableTaskLists}
|
||||||
|
isFetchingProviderLists={isFetchingProviderLists}
|
||||||
|
somedayLists={somedayLists}
|
||||||
|
handleToggleTaskList={handleToggleTaskList}
|
||||||
|
fetchAvailableTaskLists={fetchAvailableTaskLists}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -5362,166 +5460,11 @@ export default function WeeklyView() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{selectedTaskForNotes && (
|
{selectedTaskForNotes && (
|
||||||
<div
|
<NotesSidebar
|
||||||
className="weekly-modal-overlay"
|
task={selectedTaskForNotes}
|
||||||
onClick={() => setSelectedTaskForNotes(null)}
|
onClose={() => setSelectedTaskForNotes(null)}
|
||||||
>
|
updateTaskNotes={updateTaskNotes}
|
||||||
<div
|
/>
|
||||||
className="weekly-modal-content"
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<h3>Notes: {selectedTaskForNotes.title}</h3>
|
|
||||||
{/* Toolbar for Modal */}
|
|
||||||
<div className="notes-toolbar" style={{ marginTop: "1rem" }}>
|
|
||||||
<button
|
|
||||||
className="notes-toolbar-btn"
|
|
||||||
onClick={() => {
|
|
||||||
const textarea = document.querySelector(
|
|
||||||
".weekly-notes-editor",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
if (!textarea) return;
|
|
||||||
const start = textarea.selectionStart;
|
|
||||||
const end = textarea.selectionEnd;
|
|
||||||
const text = textarea.value;
|
|
||||||
const before = text.substring(0, start);
|
|
||||||
const selection = text.substring(start, end);
|
|
||||||
const after = text.substring(end);
|
|
||||||
const newText = `${before}**${selection}**${after}`;
|
|
||||||
updateTaskNotes(selectedTaskForNotes.id, newText);
|
|
||||||
// Hacky re-focus and update value visually since it's uncontrolled-ish/onBlur driven
|
|
||||||
textarea.value = newText;
|
|
||||||
textarea.focus();
|
|
||||||
textarea.setSelectionRange(
|
|
||||||
start + 2,
|
|
||||||
start + 2 + selection.length,
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
title="Bold"
|
|
||||||
>
|
|
||||||
B
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="notes-toolbar-btn"
|
|
||||||
onClick={() => {
|
|
||||||
const textarea = document.querySelector(
|
|
||||||
".weekly-notes-editor",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
if (!textarea) return;
|
|
||||||
const start = textarea.selectionStart;
|
|
||||||
const end = textarea.selectionEnd;
|
|
||||||
const text = textarea.value;
|
|
||||||
const before = text.substring(0, start);
|
|
||||||
const selection = text.substring(start, end);
|
|
||||||
const after = text.substring(end);
|
|
||||||
const newText = `${before}*${selection}*${after}`;
|
|
||||||
updateTaskNotes(selectedTaskForNotes.id, newText);
|
|
||||||
textarea.value = newText;
|
|
||||||
textarea.focus();
|
|
||||||
textarea.setSelectionRange(
|
|
||||||
start + 1,
|
|
||||||
start + 1 + selection.length,
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
title="Italic"
|
|
||||||
>
|
|
||||||
i
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="notes-toolbar-btn"
|
|
||||||
onClick={() => {
|
|
||||||
const textarea = document.querySelector(
|
|
||||||
".weekly-notes-editor",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
if (!textarea) return;
|
|
||||||
const start = textarea.selectionStart;
|
|
||||||
const end = textarea.selectionEnd;
|
|
||||||
const text = textarea.value;
|
|
||||||
const before = text.substring(0, start);
|
|
||||||
const selection = text.substring(start, end);
|
|
||||||
const after = text.substring(end);
|
|
||||||
const newText = `${before}[${selection}](url)${after}`;
|
|
||||||
updateTaskNotes(selectedTaskForNotes.id, newText);
|
|
||||||
textarea.value = newText;
|
|
||||||
textarea.focus();
|
|
||||||
textarea.setSelectionRange(
|
|
||||||
start + 1,
|
|
||||||
start + 1 + selection.length,
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
title="Link"
|
|
||||||
>
|
|
||||||
🔗
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="notes-toolbar-btn"
|
|
||||||
onClick={() => {
|
|
||||||
const textarea = document.querySelector(
|
|
||||||
".weekly-notes-editor",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
if (!textarea) return;
|
|
||||||
const start = textarea.selectionStart;
|
|
||||||
const end = textarea.selectionEnd;
|
|
||||||
const text = textarea.value;
|
|
||||||
const before = text.substring(0, start);
|
|
||||||
const selection = text.substring(start, end);
|
|
||||||
const after = text.substring(end);
|
|
||||||
const newText = `${before}- ${selection}${after}`;
|
|
||||||
updateTaskNotes(selectedTaskForNotes.id, newText);
|
|
||||||
textarea.value = newText;
|
|
||||||
textarea.focus();
|
|
||||||
textarea.setSelectionRange(
|
|
||||||
start + 2,
|
|
||||||
start + 2 + selection.length,
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
title="List"
|
|
||||||
>
|
|
||||||
☑
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="notes-toolbar-btn"
|
|
||||||
onClick={() => {
|
|
||||||
const textarea = document.querySelector(
|
|
||||||
".weekly-notes-editor",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
if (!textarea) return;
|
|
||||||
const start = textarea.selectionStart;
|
|
||||||
const end = textarea.selectionEnd;
|
|
||||||
const text = textarea.value;
|
|
||||||
const before = text.substring(0, start);
|
|
||||||
const selection = text.substring(start, end);
|
|
||||||
const after = text.substring(end);
|
|
||||||
const newText = `${before}${after}`;
|
|
||||||
updateTaskNotes(selectedTaskForNotes.id, newText);
|
|
||||||
textarea.value = newText;
|
|
||||||
textarea.focus();
|
|
||||||
textarea.setSelectionRange(start + 2, start + 10); // select "alt text"
|
|
||||||
}}
|
|
||||||
title="Image"
|
|
||||||
>
|
|
||||||
🖼️
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<textarea
|
|
||||||
className="weekly-notes-editor"
|
|
||||||
defaultValue={selectedTaskForNotes.markdownContent || ""}
|
|
||||||
autoFocus
|
|
||||||
placeholder="Add details, notes, or links..."
|
|
||||||
onBlur={(e) =>
|
|
||||||
updateTaskNotes(selectedTaskForNotes.id, e.target.value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<div className="weekly-modal-actions">
|
|
||||||
<button
|
|
||||||
className="weekly-btn weekly-btn-secondary"
|
|
||||||
onClick={() => setSelectedTaskForNotes(null)}
|
|
||||||
>
|
|
||||||
Close
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
<ImportListModal
|
<ImportListModal
|
||||||
isOpen={isImportModalOpen}
|
isOpen={isImportModalOpen}
|
||||||
@ -6372,7 +6315,117 @@ interface SettingsSidebarProps {
|
|||||||
allDayPosition: "above" | "below";
|
allDayPosition: "above" | "below";
|
||||||
setAllDayPosition: (pos: "above" | "below") => void;
|
setAllDayPosition: (pos: "above" | "below") => void;
|
||||||
saveSetting: (key: string, value: any) => void;
|
saveSetting: (key: string, value: any) => void;
|
||||||
|
availableTaskLists: {
|
||||||
|
[key in "google" | "apple" | "outlook"]?: { id: string; title: string }[];
|
||||||
|
};
|
||||||
|
isFetchingProviderLists: Record<string, boolean>;
|
||||||
|
somedayLists: SomedayList[];
|
||||||
|
handleToggleTaskList: (
|
||||||
|
provider: "google" | "apple" | "outlook",
|
||||||
|
list: { id: string; title: string },
|
||||||
|
) => Promise<void>;
|
||||||
|
fetchAvailableTaskLists: (
|
||||||
|
provider: "google" | "apple" | "outlook",
|
||||||
|
) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
// Notes Sidebar Component
|
||||||
|
interface NotesSidebarProps {
|
||||||
|
task: Task;
|
||||||
|
onClose: () => void;
|
||||||
|
updateTaskNotes: (id: string, notes: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function NotesSidebar({ task, onClose, updateTaskNotes }: NotesSidebarProps) {
|
||||||
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setTimeout(() => setIsVisible(true), 10);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setIsVisible(false);
|
||||||
|
setTimeout(onClose, 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToolbarClick = (before: string, after: string, selectOffsetStart?: number, selectOffsetEnd?: number) => {
|
||||||
|
const textarea = textareaRef.current;
|
||||||
|
if (!textarea) return;
|
||||||
|
const start = textarea.selectionStart;
|
||||||
|
const end = textarea.selectionEnd;
|
||||||
|
const text = textarea.value;
|
||||||
|
const beforeText = text.substring(0, start);
|
||||||
|
const selection = text.substring(start, end);
|
||||||
|
const afterText = text.substring(end);
|
||||||
|
|
||||||
|
let newText = `${beforeText}${before}${selection}${after}${afterText}`;
|
||||||
|
if (before === "") {
|
||||||
|
// Special case for image to match original logic precisely
|
||||||
|
newText = `${beforeText}${afterText}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTaskNotes(task.id, newText);
|
||||||
|
textarea.value = newText;
|
||||||
|
textarea.focus();
|
||||||
|
|
||||||
|
if (before === "") {
|
||||||
|
textarea.setSelectionRange(start + 2, start + 10);
|
||||||
|
} else {
|
||||||
|
textarea.setSelectionRange(
|
||||||
|
start + before.length,
|
||||||
|
start + before.length + selection.length
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={`weekly-modal-overlay ${isVisible ? "show" : ""}`}
|
||||||
|
onClick={handleClose}
|
||||||
|
style={{ zIndex: 1999 }}
|
||||||
|
/>
|
||||||
|
<div className={`weekly-notes-sidebar ${isVisible ? "open" : ""}`}>
|
||||||
|
<header className="weekly-notes-sidebar-header">
|
||||||
|
<h2 className="weekly-notes-sidebar-title">Notes: {task.title}</h2>
|
||||||
|
<button className="weekly-notes-sidebar-close" onClick={handleClose}>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
<div className="weekly-notes-sidebar-content">
|
||||||
|
<div className="notes-toolbar">
|
||||||
|
<button className="notes-toolbar-btn" onClick={() => handleToolbarClick("**", "**")} title="Bold">B</button>
|
||||||
|
<button className="notes-toolbar-btn" onClick={() => handleToolbarClick("*", "*")} title="Italic">i</button>
|
||||||
|
<button className="notes-toolbar-btn" onClick={() => handleToolbarClick("[", "](url)")} title="Link">🔗</button>
|
||||||
|
<button className="notes-toolbar-btn" onClick={() => handleToolbarClick("- ", "")} title="List">☑</button>
|
||||||
|
<button className="notes-toolbar-btn" onClick={() => handleToolbarClick("")} title="Image">🖼️</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
ref={textareaRef}
|
||||||
|
className="weekly-notes-editor"
|
||||||
|
defaultValue={task.markdownContent || ""}
|
||||||
|
autoFocus
|
||||||
|
placeholder="Add details, notes, or links..."
|
||||||
|
onBlur={(e) => updateTaskNotes(task.id, e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="weekly-modal-actions" style={{ marginTop: '24px' }}>
|
||||||
|
<button
|
||||||
|
className="weekly-btn weekly-btn-secondary"
|
||||||
|
onClick={handleClose}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function SettingsSidebar({
|
function SettingsSidebar({
|
||||||
onClose,
|
onClose,
|
||||||
@ -6437,6 +6490,11 @@ function SettingsSidebar({
|
|||||||
allDayPosition,
|
allDayPosition,
|
||||||
setAllDayPosition,
|
setAllDayPosition,
|
||||||
saveSetting,
|
saveSetting,
|
||||||
|
availableTaskLists,
|
||||||
|
isFetchingProviderLists,
|
||||||
|
somedayLists,
|
||||||
|
handleToggleTaskList,
|
||||||
|
fetchAvailableTaskLists,
|
||||||
}: SettingsSidebarProps) {
|
}: SettingsSidebarProps) {
|
||||||
const [activeTab, setActiveTab] = useState<
|
const [activeTab, setActiveTab] = useState<
|
||||||
"calendar" | "general" | "account" | "styling" | "motivation" | "about"
|
"calendar" | "general" | "account" | "styling" | "motivation" | "about"
|
||||||
@ -6460,6 +6518,17 @@ function SettingsSidebar({
|
|||||||
const [confirmDisconnectId, setConfirmDisconnectId] = useState<string | null>(
|
const [confirmDisconnectId, setConfirmDisconnectId] = useState<string | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Fetch lists when the calendar tab is selected
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeTab === "calendar") {
|
||||||
|
const providersWithAccounts = connections.map((c) => c.provider);
|
||||||
|
if (providersWithAccounts.includes("google"))
|
||||||
|
fetchAvailableTaskLists("google");
|
||||||
|
if (providersWithAccounts.includes("outlook"))
|
||||||
|
fetchAvailableTaskLists("outlook");
|
||||||
|
}
|
||||||
|
}, [activeTab, connections]);
|
||||||
const [connMsg, setConnMsg] = useState<{
|
const [connMsg, setConnMsg] = useState<{
|
||||||
type: "success" | "error";
|
type: "success" | "error";
|
||||||
text: string;
|
text: string;
|
||||||
@ -7839,6 +7908,9 @@ function SettingsSidebar({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{cleanTitle}
|
{cleanTitle}
|
||||||
|
<span style={{ fontSize: '0.85em', color: 'var(--weekly-text-light)', marginLeft: '4px' }}>
|
||||||
|
({conn.provider === 'google' ? 'Google Calendar' : conn.provider === 'apple' ? 'Apple Calendar' : 'Outlook Calendar'})
|
||||||
|
</span>
|
||||||
{isShared && (
|
{isShared && (
|
||||||
<span
|
<span
|
||||||
title="Shared calendar"
|
title="Shared calendar"
|
||||||
@ -7974,57 +8046,132 @@ function SettingsSidebar({
|
|||||||
Sync tasks with Google Tasks or Microsoft To-Do. Selected
|
Sync tasks with Google Tasks or Microsoft To-Do. Selected
|
||||||
lists will be kept in sync automatically.
|
lists will be kept in sync automatically.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
gap: "1rem",
|
gap: "1.5rem",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
{connections
|
||||||
style={{ display: "flex", gap: "1rem", flexWrap: "wrap" }}
|
.filter((c) => ["google", "outlook"].includes(c.provider))
|
||||||
>
|
.map((conn) => {
|
||||||
<button
|
const providerLists =
|
||||||
onClick={() => executeImport("google")}
|
availableTaskLists[
|
||||||
className="calendar-connect-btn"
|
conn.provider as "google" | "outlook"
|
||||||
disabled={
|
] || [];
|
||||||
!connections.some((c) => c.provider === "google") ||
|
const isFetching =
|
||||||
importingTasksState
|
isFetchingProviderLists[conn.provider];
|
||||||
}
|
|
||||||
style={{
|
return (
|
||||||
opacity:
|
<div key={conn.id}>
|
||||||
!connections.some((c) => c.provider === "google") ||
|
<div
|
||||||
importingTasksState
|
style={{
|
||||||
? 0.5
|
display: "flex",
|
||||||
: 1,
|
alignItems: "center",
|
||||||
}}
|
gap: "8px",
|
||||||
>
|
marginBottom: "0.5rem",
|
||||||
<span>📅</span>{" "}
|
fontWeight: 600,
|
||||||
{importingTasksState
|
fontSize: "0.9rem",
|
||||||
? "Syncing..."
|
}}
|
||||||
: "Sync with Google Tasks"}
|
>
|
||||||
</button>
|
<span>
|
||||||
<button
|
{conn.provider === "google" ? "📅" : "📧"}
|
||||||
onClick={() => executeImport("outlook")}
|
</span>
|
||||||
className="calendar-connect-btn"
|
{conn.provider === "google"
|
||||||
disabled={
|
? "Google Tasks"
|
||||||
!connections.some((c) => c.provider === "outlook") ||
|
: "Microsoft To-Do"}
|
||||||
importingTasksState
|
{isFetching && (
|
||||||
}
|
<span
|
||||||
style={{
|
style={{
|
||||||
opacity:
|
fontSize: "0.75rem",
|
||||||
!connections.some((c) => c.provider === "outlook") ||
|
fontWeight: 400,
|
||||||
importingTasksState
|
color: "#888",
|
||||||
? 0.5
|
}}
|
||||||
: 1,
|
>
|
||||||
}}
|
(fetching lists...)
|
||||||
>
|
</span>
|
||||||
<span>📧</span>{" "}
|
)}
|
||||||
{importingTasksState
|
</div>
|
||||||
? "Syncing..."
|
|
||||||
: "Sync with Microsoft To-Do"}
|
<ul
|
||||||
</button>
|
style={{
|
||||||
</div>
|
listStyle: "none",
|
||||||
|
padding: 0,
|
||||||
|
margin: 0,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "4px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{providerLists.map((list: { id: string; title: string }) => {
|
||||||
|
const isSynced = somedayLists.some(
|
||||||
|
(sl: SomedayList) =>
|
||||||
|
sl.externalId === list.id &&
|
||||||
|
sl.externalProvider === conn.provider,
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={list.id}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "10px",
|
||||||
|
padding: "4px 8px",
|
||||||
|
borderRadius: "4px",
|
||||||
|
background: "rgba(0,0,0,0.02)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={isSynced}
|
||||||
|
onChange={() =>
|
||||||
|
handleToggleTaskList(
|
||||||
|
conn.provider as
|
||||||
|
| "google"
|
||||||
|
| "outlook",
|
||||||
|
list,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
disabled={importingTasksState}
|
||||||
|
/>
|
||||||
|
<span style={{ fontSize: "0.9rem" }}>
|
||||||
|
{list.title}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{!isFetching && providerLists.length === 0 && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: "0.85rem",
|
||||||
|
color: "#888",
|
||||||
|
paddingLeft: "24px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
No task lists found.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{connections.filter((c) =>
|
||||||
|
["google", "outlook"].includes(c.provider),
|
||||||
|
).length === 0 && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: "0.9rem",
|
||||||
|
color: "#888",
|
||||||
|
fontStyle: "italic",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Connect a provider above to sync task lists.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{importStatusMsg && (
|
{importStatusMsg && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@ -23,6 +23,7 @@ export interface CalendarEvent {
|
|||||||
calendarId: string;
|
calendarId: string;
|
||||||
calendarTitle: string;
|
calendarTitle: string;
|
||||||
backgroundColor?: string;
|
backgroundColor?: string;
|
||||||
|
allDay?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Google Calendar event color mapping (colorId -> hex color)
|
// Google Calendar event color mapping (colorId -> hex color)
|
||||||
@ -306,23 +307,24 @@ export const getCalendarEvents = async (
|
|||||||
const isDateOnly = (s: string) => s && !s.includes('T');
|
const isDateOnly = (s: string) => s && !s.includes('T');
|
||||||
const startIsAllDay = isDateOnly(event.startDate);
|
const startIsAllDay = isDateOnly(event.startDate);
|
||||||
return {
|
return {
|
||||||
id: event.id,
|
id: event.id,
|
||||||
title: event.title,
|
title: event.title,
|
||||||
description: event.description,
|
description: event.description,
|
||||||
start: {
|
start: {
|
||||||
dateTime: startIsAllDay ? undefined : event.startDate,
|
dateTime: startIsAllDay ? undefined : event.startDate,
|
||||||
date: startIsAllDay ? event.startDate : undefined,
|
date: startIsAllDay ? event.startDate : undefined,
|
||||||
},
|
},
|
||||||
end: {
|
end: {
|
||||||
dateTime: startIsAllDay ? undefined : event.endDate,
|
dateTime: startIsAllDay ? undefined : event.endDate,
|
||||||
date: startIsAllDay ? event.endDate : undefined,
|
date: startIsAllDay ? event.endDate : undefined,
|
||||||
},
|
},
|
||||||
location: event.location,
|
location: event.location,
|
||||||
source: 'apple' as const,
|
source: 'apple' as const,
|
||||||
calendarId,
|
calendarId,
|
||||||
calendarTitle: calendars.find(c => c.id === calendarId)?.title || 'Apple Calendar',
|
calendarTitle: calendars.find(c => c.id === calendarId)?.title || 'Apple Calendar',
|
||||||
backgroundColor: calendars.find(c => c.id === calendarId)?.color || '#FF3B30'
|
backgroundColor: calendars.find(c => c.id === calendarId)?.color || '#FF3B30'
|
||||||
};}));
|
};
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
} else if (connection.provider === 'outlook') {
|
} else if (connection.provider === 'outlook') {
|
||||||
console.log('[CALENDAR] Processing Outlook connection:', connection.id);
|
console.log('[CALENDAR] Processing Outlook connection:', connection.id);
|
||||||
@ -547,7 +549,8 @@ export const createCalendarEvent = async (
|
|||||||
description: event.description,
|
description: event.description,
|
||||||
start: event.start,
|
start: event.start,
|
||||||
end: event.end,
|
end: event.end,
|
||||||
location: event.location
|
location: event.location,
|
||||||
|
allDay: event.allDay
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -557,6 +560,7 @@ export const createCalendarEvent = async (
|
|||||||
start: createdEvent.start,
|
start: createdEvent.start,
|
||||||
end: createdEvent.end,
|
end: createdEvent.end,
|
||||||
location: createdEvent.location,
|
location: createdEvent.location,
|
||||||
|
allDay: createdEvent.allDay,
|
||||||
source: 'outlook',
|
source: 'outlook',
|
||||||
calendarId,
|
calendarId,
|
||||||
calendarTitle: '',
|
calendarTitle: '',
|
||||||
@ -658,7 +662,8 @@ export const updateCalendarEvent = async (
|
|||||||
description: event.description,
|
description: event.description,
|
||||||
start: event.start,
|
start: event.start,
|
||||||
end: event.end,
|
end: event.end,
|
||||||
location: event.location
|
location: event.location,
|
||||||
|
allDay: event.allDay
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -668,6 +673,7 @@ export const updateCalendarEvent = async (
|
|||||||
start: updatedEvent.start,
|
start: updatedEvent.start,
|
||||||
end: updatedEvent.end,
|
end: updatedEvent.end,
|
||||||
location: updatedEvent.location,
|
location: updatedEvent.location,
|
||||||
|
allDay: updatedEvent.allDay,
|
||||||
source: 'outlook',
|
source: 'outlook',
|
||||||
calendarId,
|
calendarId,
|
||||||
calendarTitle: '',
|
calendarTitle: '',
|
||||||
|
|||||||
@ -101,7 +101,7 @@ export const fetchMsTodoTasks = async (
|
|||||||
});
|
});
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${GRAPH_ENDPOINT}/me/todo/lists/${listId}/tasks?${params.toString()}`,
|
`${GRAPH_ENDPOINT}/me/todo/lists/${encodeURIComponent(listId)}/tasks?${params.toString()}`,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${accessToken}`,
|
'Authorization': `Bearer ${accessToken}`,
|
||||||
@ -139,7 +139,7 @@ export const fetchMsTodoTasksForSync = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${GRAPH_ENDPOINT}/me/todo/lists/${listId}/tasks?${params.toString()}`,
|
`${GRAPH_ENDPOINT}/me/todo/lists/${encodeURIComponent(listId)}/tasks?${params.toString()}`,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${accessToken}`,
|
'Authorization': `Bearer ${accessToken}`,
|
||||||
@ -183,7 +183,7 @@ export const createMsTodoTask = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${GRAPH_ENDPOINT}/me/todo/lists/${listId}/tasks`,
|
`${GRAPH_ENDPOINT}/me/todo/lists/${encodeURIComponent(listId)}/tasks`,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@ -238,7 +238,7 @@ export const updateMsTodoTask = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${GRAPH_ENDPOINT}/me/todo/lists/${listId}/tasks/${taskId}`,
|
`${GRAPH_ENDPOINT}/me/todo/lists/${encodeURIComponent(listId)}/tasks/${encodeURIComponent(taskId)}`,
|
||||||
{
|
{
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: {
|
headers: {
|
||||||
@ -266,7 +266,7 @@ export const deleteMsTodoTask = async (
|
|||||||
taskId: string
|
taskId: string
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${GRAPH_ENDPOINT}/me/todo/lists/${listId}/tasks/${taskId}`,
|
`${GRAPH_ENDPOINT}/me/todo/lists/${encodeURIComponent(listId)}/tasks/${encodeURIComponent(taskId)}`,
|
||||||
{
|
{
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: {
|
headers: {
|
||||||
@ -297,7 +297,7 @@ export const fetchMsChecklistItems = async (
|
|||||||
taskId: string
|
taskId: string
|
||||||
): Promise<MicrosoftChecklistItem[]> => {
|
): Promise<MicrosoftChecklistItem[]> => {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${GRAPH_ENDPOINT}/me/todo/lists/${listId}/tasks/${taskId}/checklistItems`,
|
`${GRAPH_ENDPOINT}/me/todo/lists/${encodeURIComponent(listId)}/tasks/${encodeURIComponent(taskId)}/checklistItems`,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${accessToken}`,
|
'Authorization': `Bearer ${accessToken}`,
|
||||||
@ -326,7 +326,7 @@ export const createMsChecklistItem = async (
|
|||||||
displayName: string
|
displayName: string
|
||||||
): Promise<MicrosoftChecklistItem> => {
|
): Promise<MicrosoftChecklistItem> => {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${GRAPH_ENDPOINT}/me/todo/lists/${listId}/tasks/${taskId}/checklistItems`,
|
`${GRAPH_ENDPOINT}/me/todo/lists/${encodeURIComponent(listId)}/tasks/${encodeURIComponent(taskId)}/checklistItems`,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@ -356,7 +356,7 @@ export const updateMsChecklistItem = async (
|
|||||||
updates: { displayName?: string; isChecked?: boolean }
|
updates: { displayName?: string; isChecked?: boolean }
|
||||||
): Promise<MicrosoftChecklistItem> => {
|
): Promise<MicrosoftChecklistItem> => {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${GRAPH_ENDPOINT}/me/todo/lists/${listId}/tasks/${taskId}/checklistItems/${checklistItemId}`,
|
`${GRAPH_ENDPOINT}/me/todo/lists/${encodeURIComponent(listId)}/tasks/${encodeURIComponent(taskId)}/checklistItems/${encodeURIComponent(checklistItemId)}`,
|
||||||
{
|
{
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: {
|
headers: {
|
||||||
@ -385,7 +385,7 @@ export const deleteMsChecklistItem = async (
|
|||||||
checklistItemId: string
|
checklistItemId: string
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${GRAPH_ENDPOINT}/me/todo/lists/${listId}/tasks/${taskId}/checklistItems/${checklistItemId}`,
|
`${GRAPH_ENDPOINT}/me/todo/lists/${encodeURIComponent(listId)}/tasks/${encodeURIComponent(taskId)}/checklistItems/${encodeURIComponent(checklistItemId)}`,
|
||||||
{
|
{
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@ -153,7 +153,7 @@ export const getUpcomingEvents = async (
|
|||||||
});
|
});
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${GRAPH_ENDPOINT}/me/calendars/${calendarId}/calendarView?${params.toString()}`,
|
`${GRAPH_ENDPOINT}/me/calendars/${encodeURIComponent(calendarId)}/calendarView?${params.toString()}`,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${accessToken}`,
|
'Authorization': `Bearer ${accessToken}`,
|
||||||
@ -170,7 +170,7 @@ export const getUpcomingEvents = async (
|
|||||||
return data.value.map((event: any) => ({
|
return data.value.map((event: any) => ({
|
||||||
id: event.id,
|
id: event.id,
|
||||||
summary: event.subject,
|
summary: event.subject,
|
||||||
description: event.bodyPreview,
|
description: event.body?.content || event.bodyPreview,
|
||||||
start: {
|
start: {
|
||||||
dateTime: event.start.dateTime,
|
dateTime: event.start.dateTime,
|
||||||
timeZone: event.start.timeZone
|
timeZone: event.start.timeZone
|
||||||
@ -185,6 +185,14 @@ export const getUpcomingEvents = async (
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ensureTimeZone = (dateTimeObj: any) => {
|
||||||
|
if (!dateTimeObj) return dateTimeObj;
|
||||||
|
return {
|
||||||
|
dateTime: dateTimeObj.dateTime,
|
||||||
|
timeZone: dateTimeObj.timeZone || 'UTC'
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create Outlook Event
|
* Create Outlook Event
|
||||||
*/
|
*/
|
||||||
@ -193,7 +201,7 @@ export const createEvent = async (
|
|||||||
calendarId: string,
|
calendarId: string,
|
||||||
event: any
|
event: any
|
||||||
) => {
|
) => {
|
||||||
const response = await fetch(`${GRAPH_ENDPOINT}/me/calendars/${calendarId}/events`, {
|
const response = await fetch(`${GRAPH_ENDPOINT}/me/calendars/${encodeURIComponent(calendarId)}/events`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${accessToken}`,
|
'Authorization': `Bearer ${accessToken}`,
|
||||||
@ -205,8 +213,9 @@ export const createEvent = async (
|
|||||||
contentType: 'HTML',
|
contentType: 'HTML',
|
||||||
content: event.description || ''
|
content: event.description || ''
|
||||||
},
|
},
|
||||||
start: event.start,
|
start: ensureTimeZone(event.start),
|
||||||
end: event.end,
|
end: ensureTimeZone(event.end),
|
||||||
|
isAllDay: !!event.allDay,
|
||||||
location: {
|
location: {
|
||||||
displayName: event.location || ''
|
displayName: event.location || ''
|
||||||
}
|
}
|
||||||
@ -225,7 +234,8 @@ export const createEvent = async (
|
|||||||
description: created.bodyPreview,
|
description: created.bodyPreview,
|
||||||
start: created.start,
|
start: created.start,
|
||||||
end: created.end,
|
end: created.end,
|
||||||
location: created.location?.displayName
|
location: created.location?.displayName,
|
||||||
|
allDay: created.isAllDay
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -238,7 +248,7 @@ export const updateEvent = async (
|
|||||||
eventId: string,
|
eventId: string,
|
||||||
event: any
|
event: any
|
||||||
) => {
|
) => {
|
||||||
const response = await fetch(`${GRAPH_ENDPOINT}/me/events/${eventId}`, {
|
const response = await fetch(`${GRAPH_ENDPOINT}/me/events/${encodeURIComponent(eventId)}`, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${accessToken}`,
|
'Authorization': `Bearer ${accessToken}`,
|
||||||
@ -250,8 +260,9 @@ export const updateEvent = async (
|
|||||||
contentType: 'HTML',
|
contentType: 'HTML',
|
||||||
content: event.description || ''
|
content: event.description || ''
|
||||||
},
|
},
|
||||||
start: event.start,
|
start: ensureTimeZone(event.start),
|
||||||
end: event.end,
|
end: ensureTimeZone(event.end),
|
||||||
|
isAllDay: event.allDay !== undefined ? !!event.allDay : undefined,
|
||||||
location: {
|
location: {
|
||||||
displayName: event.location || ''
|
displayName: event.location || ''
|
||||||
}
|
}
|
||||||
@ -270,7 +281,8 @@ export const updateEvent = async (
|
|||||||
description: updated.bodyPreview,
|
description: updated.bodyPreview,
|
||||||
start: updated.start,
|
start: updated.start,
|
||||||
end: updated.end,
|
end: updated.end,
|
||||||
location: updated.location?.displayName
|
location: updated.location?.displayName,
|
||||||
|
allDay: updated.isAllDay
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -282,7 +294,7 @@ export const deleteEvent = async (
|
|||||||
calendarId: string,
|
calendarId: string,
|
||||||
eventId: string
|
eventId: string
|
||||||
) => {
|
) => {
|
||||||
const response = await fetch(`${GRAPH_ENDPOINT}/me/events/${eventId}`, {
|
const response = await fetch(`${GRAPH_ENDPOINT}/me/events/${encodeURIComponent(eventId)}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${accessToken}`
|
'Authorization': `Bearer ${accessToken}`
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user