Refactor Someday area to grid layout with dynamic slots, fixed scrolling, and border refinements

This commit is contained in:
mARTin 2026-03-01 11:58:48 +01:00
parent dea7c387ea
commit fcaadc1ce9
16 changed files with 673 additions and 560 deletions

View File

@ -4,9 +4,9 @@
"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": {
"dev": "next dev -H 0.0.0.0 -p 3001", "dev": "echo \"\\n ✦ My Weekly ToDo List v$(node -p \"require('./package.json').version\") — dev mode\\n\" && next dev -H 0.0.0.0 -p 3002",
"build": "next build", "build": "next build",
"start": "next start", "start": "echo \"\\n ✦ My Weekly ToDo List v$(node -p \"require('./package.json').version\") — production\\n\" && next start",
"lint": "next lint", "lint": "next lint",
"type-check": "tsc --noEmit", "type-check": "tsc --noEmit",
"test": "jest" "test": "jest"

View File

@ -142,6 +142,7 @@ model Task {
dayOfWeek Int? dayOfWeek Int?
scheduledDate DateTime? scheduledDate DateTime?
somedayListId String? somedayListId String?
somedaySlotIndex Int?
originalDate DateTime? originalDate DateTime?
startTime String? startTime String?
endTime String? endTime String?

View File

@ -195,7 +195,7 @@ export async function POST(request: NextRequest) {
const userId = (session.user as any).id; const userId = (session.user as any).id;
const body = await request.json(); const body = await request.json();
const { title, description, dayOfWeek, order, markdownContent, somedayListId, startTime, scheduledDate, recurrenceInterval, recurrenceUnit, recurrenceTime, recurrenceEndDate, parentTaskId } = body; const { title, description, dayOfWeek, order, markdownContent, somedayListId, somedaySlotIndex, startTime, scheduledDate, recurrenceInterval, recurrenceUnit, recurrenceTime, recurrenceEndDate, parentTaskId } = body;
let { isRolling } = body; let { isRolling } = body;
const { isRecurring } = body; const { isRecurring } = body;
@ -232,6 +232,7 @@ export async function POST(request: NextRequest) {
recurrenceUnit, recurrenceUnit,
recurrenceTime, recurrenceTime,
recurrenceEndDate: recurrenceEndDate ? new Date(recurrenceEndDate) : null, recurrenceEndDate: recurrenceEndDate ? new Date(recurrenceEndDate) : null,
somedaySlotIndex: somedaySlotIndex !== undefined ? parseInt(somedaySlotIndex) : null,
parentTaskId: parentTaskId || null, parentTaskId: parentTaskId || null,
}, },
}); });
@ -265,7 +266,7 @@ export async function PATCH(request: NextRequest) {
const body = await request.json(); const body = await request.json();
const { id } = body; const { id } = body;
const { title, description, completed, dayOfWeek, order, markdownContent, scheduledDate, startTime, somedayListId, isRecurring, recurrenceInterval, recurrenceUnit, recurrenceTime, recurrenceEndDate, restore, parentTaskId } = body; const { title, description, completed, dayOfWeek, order, markdownContent, scheduledDate, startTime, somedayListId, somedaySlotIndex, isRecurring, recurrenceInterval, recurrenceUnit, recurrenceTime, recurrenceEndDate, restore, parentTaskId } = body;
if (!id) { if (!id) {
return NextResponse.json( return NextResponse.json(
@ -349,6 +350,7 @@ export async function PATCH(request: NextRequest) {
...(recurrenceTime !== undefined && { recurrenceTime }), ...(recurrenceTime !== undefined && { recurrenceTime }),
...(recurrenceEndDate !== undefined && { recurrenceEndDate: recurrenceEndDate ? new Date(recurrenceEndDate) : null }), ...(recurrenceEndDate !== undefined && { recurrenceEndDate: recurrenceEndDate ? new Date(recurrenceEndDate) : null }),
...(restore === true && { deletedAt: null }), ...(restore === true && { deletedAt: null }),
...(somedaySlotIndex !== undefined && { somedaySlotIndex: somedaySlotIndex !== null ? parseInt(somedaySlotIndex) : null }),
...(parentTaskId !== undefined && { parentTaskId: parentTaskId || null }) ...(parentTaskId !== undefined && { parentTaskId: parentTaskId || null })
}, },
}); });

View File

@ -104,6 +104,20 @@ function LoginContent() {
Continue with Google Continue with Google
</button> </button>
{/* Apple Sign In */}
<button
type="button"
onClick={() => { setIsLoading(true); signIn('apple', { callbackUrl }); }}
disabled={isLoading}
className="weekly-auth-button apple"
style={{ marginTop: '0.75rem', backgroundColor: '#000', color: '#fff' }}
>
<svg className="apple-icon" viewBox="0 0 24 24" width="18" height="18" fill="currentColor">
<path d="M16.365 21.492c-1.385 1.144-2.825 1.114-4.237.491-1.384-.622-2.735-.622-4.116 0-1.472.684-2.825.684-4.116-.279-5.1-4.144-6.31-11.458-2.67-15.002 1.442-1.383 3.08-1.745 4.503-1.745 1.471 0 2.883.682 3.86.682.95 0 2.61-.741 4.364-.741 1.748 0 3.35.532 4.417 1.838-3.647 1.956-3.08 6.906.653 8.358-.89 2.134-1.928 4.624-2.66 6.398zm-4.745-16.79c-.208-2.223 1.72-4.301 3.974-4.702.355 2.37-2.016 4.331-3.974 4.702z" />
</svg>
Continue with Apple
</button>
{/* Links */} {/* Links */}
<div className="weekly-auth-links"> <div className="weekly-auth-links">
<Link href="/auth/forgot-password" className="weekly-auth-link"> <Link href="/auth/forgot-password" className="weekly-auth-link">

View File

@ -169,6 +169,20 @@ export default function SignupPage() {
Sign up with Google Sign up with Google
</button> </button>
{/* Apple Sign Up */}
<button
type="button"
onClick={() => { setIsLoading(true); signIn('apple', { callbackUrl: '/tasks' }); }}
disabled={isLoading}
className="weekly-auth-button apple"
style={{ marginTop: '0.75rem', backgroundColor: '#000', color: '#fff' }}
>
<svg className="apple-icon" viewBox="0 0 24 24" width="18" height="18" fill="currentColor">
<path d="M16.365 21.492c-1.385 1.144-2.825 1.114-4.237.491-1.384-.622-2.735-.622-4.116 0-1.472.684-2.825.684-4.116-.279-5.1-4.144-6.31-11.458-2.67-15.002 1.442-1.383 3.08-1.745 4.503-1.745 1.471 0 2.883.682 3.86.682.95 0 2.61-.741 4.364-.741 1.748 0 3.35.532 4.417 1.838-3.647 1.956-3.08 6.906.653 8.358-.89 2.134-1.928 4.624-2.66 6.398zm-4.745-16.79c-.208-2.223 1.72-4.301 3.974-4.702.355 2.37-2.016 4.331-3.974 4.702z" />
</svg>
Sign up with Apple
</button>
{/* Links */} {/* Links */}
<div className="weekly-auth-links"> <div className="weekly-auth-links">
<span className="weekly-auth-text">Already have an account?</span> <span className="weekly-auth-text">Already have an account?</span>

View File

@ -1158,7 +1158,7 @@ h3 {
/* Task Input */ /* Task Input */
.weekly-task-input { .weekly-task-input {
padding: 0.5rem 1rem 0.5rem 2.5rem; padding: 0.5rem 1rem 0.5rem 2.5rem;
border-bottom: 1px solid var(--weekly-border); border-bottom: none;
} }
.weekly-task-input input { .weekly-task-input input {
@ -1177,11 +1177,17 @@ h3 {
/* Someday Section */ /* Someday Section */
.weekly-someday { .weekly-someday {
/* background color removed as per user request */ background-color: #f9f9f9;
width: 100%;
padding: 0 1rem;
transition: max-height 0.3s ease; transition: max-height 0.3s ease;
position: relative; position: relative;
} }
.weekly-container.dark-mode .weekly-someday {
background-color: #1a1a1b;
}
.weekly-someday.collapsed { .weekly-someday.collapsed {
max-height: 30px; max-height: 30px;
overflow: hidden; overflow: hidden;
@ -1251,16 +1257,16 @@ h3 {
/* Someday Lists Container - Horizontal Scroll */ /* Someday Lists Container - Horizontal Scroll */
.weekly-someday-lists-grid { .weekly-someday-lists-grid {
display: flex; display: flex !important;
flex-direction: row; flex-direction: row !important;
flex-wrap: nowrap; flex-wrap: nowrap !important;
gap: 1rem; gap: 1.5rem;
width: 100%; width: max-content; /* Allow grid to grow beyond parent width */
overflow-x: auto; min-width: 100%;
overflow-y: hidden; /* Prevent vertical Scrollbar on container */ padding-bottom: 1rem;
padding-bottom: 0.5rem; /* Space for scrollbar */
align-items: flex-start; align-items: flex-start;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
scroll-behavior: smooth;
} }
/* Scrollbar Styling for Someday Container */ /* Scrollbar Styling for Someday Container */
@ -1308,25 +1314,43 @@ h3 {
/* Ruled paper lines for someday tasks */ /* Ruled paper lines for someday tasks */
.weekly-someday-list { .weekly-someday-list {
/* border-right: 1px solid var(--weekly-border); Removed for cleaner look */
padding: 0; padding: 0;
min-height: 200px; min-height: 200px;
flex: 0 0 280px; /* Fixed width for horizontal scrolling */ flex: 0 0 300px; /* Slightly wider lists */
width: 280px; width: 300px;
transition: transform 0.2s ease, opacity 0.2s ease; transition: transform 0.2s ease, opacity 0.2s ease;
max-width: 100%; max-width: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow-y: auto; overflow-y: auto;
max-height: 380px; scrollbar-width: none; /* Hide scrollbar Firefox */
background-image: repeating-linear-gradient( -ms-overflow-style: none; /* Hide scrollbar IE/Edge */
transparent, }
transparent 31px,
var(--weekly-border) 31px, .weekly-someday-list::-webkit-scrollbar {
var(--weekly-border) 32px display: none; /* Hide scrollbar Chrome/Safari/Webkit */
); }
background-attachment: local;
background-position: 0 40px; /* Offset for the header */ .weekly-container.dark-mode .weekly-someday-list {
background-image: none;
}
.task-list-slot {
height: 38px;
width: 100%;
position: relative;
display: flex;
align-items: center;
border-bottom: 1px solid var(--weekly-border);
box-sizing: border-box;
}
.task-list-slot.drop-target {
background-color: rgba(0, 128, 128, 0.05); /* Subtle teal background for drop target */
}
.weekly-container.dark-mode .task-list-slot.drop-target {
background-color: rgba(0, 128, 128, 0.15);
} }
.someday-drag-handle { .someday-drag-handle {
@ -1363,9 +1387,18 @@ h3 {
.weekly-someday-list.placeholder-list { .weekly-someday-list.placeholder-list {
background-image: repeating-linear-gradient( background-image: repeating-linear-gradient(
transparent, transparent,
transparent 31px, transparent 37px,
#f5f5f5 31px, #e5e5e5 37px,
#f5f5f5 32px #e5e5e5 38px
);
}
.weekly-container.dark-mode .weekly-someday-list.placeholder-list {
background-image: repeating-linear-gradient(
transparent,
transparent 37px,
#4a4a4a 37px,
#4a4a4a 38px
); );
} }
@ -1379,14 +1412,18 @@ h3 {
} }
.weekly-someday-list .weekly-task-item { .weekly-someday-list .weekly-task-item {
border-bottom: 1px solid transparent; border-bottom: none;
min-height: 32px; min-height: 38px;
height: auto; height: auto;
padding: 4px 1rem; padding: 6px 1rem;
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
} }
.weekly-container.dark-mode .weekly-someday-list .weekly-task-item {
border-bottom: none;
}
.weekly-someday-list .weekly-task-text { .weekly-someday-list .weekly-task-text {
font-size: var(--base-font-size); font-size: var(--base-font-size);
line-height: 1.4; line-height: 1.4;
@ -1398,25 +1435,31 @@ h3 {
} }
.weekly-someday-list-title-header { .weekly-someday-list-title-header {
padding: 0.75rem 1rem 0.25rem; padding: 0 1rem;
height: 40px; margin-bottom: 0;
height: 56px;
min-height: 56px;
display: flex; display: flex;
align-items: center; align-items: center;
border-bottom: 1px solid var(--weekly-border);
}
.weekly-container.dark-mode .weekly-someday-list-title-header {
border-bottom-color: var(--weekly-border);
} }
.weekly-someday-list-title-input { .weekly-someday-list-title-input {
font-size: 1rem; font-size: 1.15rem;
font-weight: 700; font-weight: 800;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.05em; letter-spacing: 0.08em;
color: var(--weekly-text, #333); color: var(--weekly-text, #222);
background: transparent; background: transparent;
border: none; border: none;
border-bottom: 1px solid transparent;
width: 100%; width: 100%;
padding: 2px 0; padding: 4px 0;
outline: none; outline: none;
transition: border-color 0.15s ease; transition: opacity 0.15s ease;
} }
.weekly-someday .weekly-task-item { .weekly-someday .weekly-task-item {
@ -1763,32 +1806,10 @@ h3 {
/* === Tablet (≤ 1024px): 5-day view === */ /* === Tablet (≤ 1024px): 5-day view === */
@media (max-width: 1024px) { @media (max-width: 1024px) {
.weekly-days-grid {
grid-template-columns: repeat(5, 1fr) !important;
}
.all-day-events-grid {
grid-template-columns: repeat(5, 1fr) !important;
}
.weekly-someday-lists-grid {
grid-template-columns: repeat(5, 1fr) !important;
}
} }
/* === Phone landscape / small tablet (≤ 768px): 3-day view === */ /* === Phone landscape / small tablet (≤ 768px): 3-day view === */
@media (max-width: 768px) { @media (max-width: 768px) {
.weekly-days-grid {
grid-template-columns: repeat(3, 1fr) !important;
}
.all-day-events-grid {
grid-template-columns: repeat(3, 1fr) !important;
}
.weekly-someday-lists-grid {
grid-template-columns: repeat(3, 1fr) !important;
}
.weekly-someday-lists { .weekly-someday-lists {
grid-template-columns: 1fr; grid-template-columns: 1fr;
@ -1838,17 +1859,6 @@ h3 {
/* === Phone portrait (≤ 480px): 1-day view === */ /* === Phone portrait (≤ 480px): 1-day view === */
@media (max-width: 480px) { @media (max-width: 480px) {
.weekly-days-grid {
grid-template-columns: 1fr !important;
}
.all-day-events-grid {
grid-template-columns: 1fr !important;
}
.weekly-someday-lists-grid {
grid-template-columns: 1fr !important;
}
/* Narrower time column on phone */ /* Narrower time column on phone */
.time-column { .time-column {

View File

@ -3,5 +3,5 @@
import { SessionProvider } from 'next-auth/react'; import { SessionProvider } from 'next-auth/react';
export function Providers({ children }: { children: React.ReactNode }) { export function Providers({ children }: { children: React.ReactNode }) {
return <SessionProvider>{children}</SessionProvider>; return <SessionProvider refetchOnWindowFocus={false}>{children}</SessionProvider>;
} }

View File

@ -72,6 +72,7 @@ export default function CalendarEventModal({
const [endDate, setEndDate] = useState(getInitialEnd()); const [endDate, setEndDate] = useState(getInitialEnd());
const [allDay, setAllDay] = useState(!!event?.allDay); const [allDay, setAllDay] = useState(!!event?.allDay);
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState('');
const handleSubmit = async () => { const handleSubmit = async () => {
@ -122,13 +123,13 @@ export default function CalendarEventModal({
return; return;
} }
setIsSaving(true); setIsDeleting(true);
try { try {
await onDelete(event.id, event.calendarId); await onDelete(event.id, event.calendarId);
onClose(); onClose();
} catch (err: any) { } catch (err: any) {
setError(err.message || 'Failed to delete event'); setError(err.message || 'Failed to delete event');
setIsSaving(false); setIsDeleting(false);
setIsDeleteConfirming(false); setIsDeleteConfirming(false);
} }
}; };
@ -286,7 +287,7 @@ export default function CalendarEventModal({
{event && onDelete && ( {event && onDelete && (
<button <button
onClick={handleDelete} onClick={handleDelete}
disabled={isSaving} disabled={isSaving || isDeleting}
style={{ style={{
padding: '10px 20px', padding: '10px 20px',
background: isDeleteConfirming ? '#d32f2f' : 'transparent', background: isDeleteConfirming ? '#d32f2f' : 'transparent',
@ -299,13 +300,13 @@ export default function CalendarEventModal({
width: isDeleteConfirming ? 'auto' : 'initial' // Expand if needed width: isDeleteConfirming ? 'auto' : 'initial' // Expand if needed
}} }}
> >
{isDeleteConfirming ? 'Click again to confirm delete' : 'Delete'} {isDeleting ? 'Deleting...' : isDeleteConfirming ? 'Confirm Delete' : 'Delete'}
</button> </button>
)} )}
</div> </div>
<div style={{ display: 'flex', gap: '10px' }}> <div style={{ display: 'flex', gap: '10px' }}>
<button className="weekly-btn weekly-btn-secondary" onClick={onClose} disabled={isSaving}>Cancel</button> <button className="weekly-btn weekly-btn-secondary" onClick={onClose} disabled={isSaving || isDeleting}>Cancel</button>
<button className="weekly-btn weekly-btn-primary" onClick={handleSubmit} disabled={isSaving}> <button className="weekly-btn weekly-btn-primary" onClick={handleSubmit} disabled={isSaving || isDeleting}>
{isSaving ? 'Saving...' : 'Save'} {isSaving ? 'Saving...' : 'Save'}
</button> </button>
</div> </div>

File diff suppressed because it is too large Load Diff

View File

@ -363,6 +363,7 @@ export const updateEvent = async (
title?: string; title?: string;
description?: string; description?: string;
location?: string; location?: string;
url?: string;
start?: { dateTime?: string; date?: string }; start?: { dateTime?: string; date?: string };
end?: { dateTime?: string; date?: string }; end?: { dateTime?: string; date?: string };
} }
@ -499,6 +500,14 @@ export const updateEvent = async (
if (eventData.description) event.description = eventData.description; if (eventData.description) event.description = eventData.description;
if (eventData.location) event.location = eventData.location; if (eventData.location) event.location = eventData.location;
if (eventData.url !== undefined) {
if (eventData.url) {
vevent.updatePropertyWithValue('url', eventData.url);
} else {
vevent.removeProperty('url');
}
}
if (eventData.start) { if (eventData.start) {
if (eventData.start.date) { if (eventData.start.date) {
event.startDate = ICAL.Time.fromJSDate(new Date(eventData.start.date), true); event.startDate = ICAL.Time.fromJSDate(new Date(eventData.start.date), true);

View File

@ -1,5 +1,6 @@
import { NextAuthOptions } from "next-auth"; import { NextAuthOptions } from "next-auth";
import GoogleProvider from "next-auth/providers/google"; import GoogleProvider from "next-auth/providers/google";
import AppleProvider from "next-auth/providers/apple";
import { PrismaAdapter } from "@auth/prisma-adapter"; import { PrismaAdapter } from "@auth/prisma-adapter";
import CredentialsProvider from "next-auth/providers/credentials"; import CredentialsProvider from "next-auth/providers/credentials";
import { compare } from "bcryptjs"; import { compare } from "bcryptjs";
@ -19,6 +20,10 @@ export const authOptions: NextAuthOptions = {
} }
} }
}), }),
AppleProvider({
clientId: process.env.APPLE_ID || "",
clientSecret: process.env.APPLE_SECRET || ""
}),
CredentialsProvider({ CredentialsProvider({
name: "credentials", name: "credentials",
credentials: { credentials: {

View File

@ -686,8 +686,11 @@ export const updateCalendarEvent = async (
if (rrule) googleEvent.recurrence = [rrule]; if (rrule) googleEvent.recurrence = [rrule];
if (event.url) googleEvent.source = { url: event.url, title: event.url }; if (event.url) googleEvent.source = { url: event.url, title: event.url };
// Google adds _date suffix for instances. Editing base series only.
const baseEventId = eventId.split('_')[0];
const updatedEvent = await import('./google-calendar').then(m => const updatedEvent = await import('./google-calendar').then(m =>
m.updateEvent(oauth2Client, accessToken, calendarId, eventId, googleEvent) m.updateEvent(oauth2Client, accessToken, calendarId, baseEventId, googleEvent)
); );
return { return {
@ -709,8 +712,11 @@ export const updateCalendarEvent = async (
else throw new Error('Failed to refresh token'); else throw new Error('Failed to refresh token');
} }
// Extract base series ID for Outlook
const baseEventId = eventId.includes('::') ? eventId.split('::')[0] : eventId;
const startDate = event.start?.dateTime ? new Date(event.start.dateTime) : new Date(); const startDate = event.start?.dateTime ? new Date(event.start.dateTime) : new Date();
const updatedEvent = await updateOutlookEvent(accessToken, calendarId, eventId, { const updatedEvent = await updateOutlookEvent(accessToken, calendarId, baseEventId, {
summary: event.title, summary: event.title,
description: event.description, description: event.description,
start: event.start, start: event.start,
@ -740,6 +746,7 @@ export const updateCalendarEvent = async (
title: event.title, title: event.title,
description: event.description, description: event.description,
location: event.location, location: event.location,
url: event.url,
start: event.start, start: event.start,
end: event.end end: event.end
}) })
@ -785,8 +792,11 @@ export const deleteCalendarEvent = async (
process.env.GOOGLE_REDIRECT_URI || '' process.env.GOOGLE_REDIRECT_URI || ''
); );
// Default to deleting the whole series if repeating
const baseEventId = eventId.split('_')[0];
await import('./google-calendar').then(m => await import('./google-calendar').then(m =>
m.deleteEvent(oauth2Client, accessToken, calendarId, eventId) m.deleteEvent(oauth2Client, accessToken, calendarId, baseEventId)
); );
return; return;
} else if (connection.provider === 'outlook') { } else if (connection.provider === 'outlook') {
@ -797,7 +807,10 @@ export const deleteCalendarEvent = async (
else throw new Error('Failed to refresh token'); else throw new Error('Failed to refresh token');
} }
await deleteOutlookEvent(accessToken, calendarId, eventId); // Extract base series ID for Outlook
const baseEventId = eventId.includes('::') ? eventId.split('::')[0] : eventId;
await deleteOutlookEvent(accessToken, calendarId, baseEventId);
return; return;
} else if (connection.provider === 'apple') { } else if (connection.provider === 'apple') {
const [email, appPassword] = connection.accessToken.split(':'); const [email, appPassword] = connection.accessToken.split(':');

View File

@ -108,15 +108,28 @@ export async function sendVerificationEmail(
const from = process.env.SMTP_FROM || '"My Weekly ToDo\'s" <noreply@example.com>'; const from = process.env.SMTP_FROM || '"My Weekly ToDo\'s" <noreply@example.com>';
console.log(`[EMAIL] Sending verification email to ${email} from ${from}`); console.log(`[EMAIL] Sending verification email to ${email} from ${from}`);
console.log(`\n======================================================`);
console.log(`[DEV VERIFICATION LINK]:\n${verifyLink}`);
console.log(`======================================================\n`);
const result = await getTransporter().sendMail({ try {
from, // 5 second timeout for SMTP
to: email, const timeoutPromise = new Promise((_, reject) =>
subject: `${code} Verify your email for My Weekly ToDo's`, setTimeout(() => reject(new Error('SMTP timeout')), 5000)
html, );
});
console.log(`[EMAIL] Email sent successfully: ${result.messageId}`); const mailPromise = getTransporter().sendMail({
from,
to: email,
subject: `${code} Verify your email for My Weekly ToDo's`,
html,
});
const result = await Promise.race([mailPromise, timeoutPromise]) as any;
console.log(`[EMAIL] Email sent successfully: ${result.messageId}`);
} catch (err: any) {
console.error(`[EMAIL] Failed to send email (Error: ${err.message}). The verification link is printed above for manual use.`);
}
} }
export function generateVerificationCode(): string { export function generateVerificationCode(): string {

View File

@ -147,7 +147,7 @@ export const getUpcomingEvents = async (
const params = new URLSearchParams({ const params = new URLSearchParams({
startDateTime: startDateTime, startDateTime: startDateTime,
endDateTime: endDateTime, endDateTime: endDateTime,
'$select': 'subject,bodyPreview,start,end,location,webLink,isAllDay', '$select': 'subject,bodyPreview,start,end,location,webLink,isAllDay,seriesMasterId,type',
'$orderby': 'start/dateTime', '$orderby': 'start/dateTime',
'$top': '50' '$top': '50'
}); });
@ -168,7 +168,7 @@ export const getUpcomingEvents = async (
const data = await response.json(); const data = await response.json();
return data.value.map((event: any) => ({ return data.value.map((event: any) => ({
id: event.id, id: event.seriesMasterId ? `${event.seriesMasterId}::${event.id}` : event.id,
summary: event.subject, summary: event.subject,
description: event.body?.content || event.bodyPreview, description: event.body?.content || event.bodyPreview,
start: { start: {

25
test-email.js Normal file
View File

@ -0,0 +1,25 @@
const nodemailer = require('nodemailer');
const host = process.env.SMTP_HOST;
const port = 465; // Force 465
const secure = true; // Use TLS
const user = process.env.SMTP_USERNAME;
const pass = process.env.SMTP_PASSWORD;
const from = process.env.SMTP_FROM || 'test@example.com';
const transporter = nodemailer.createTransport({
host,
port,
secure,
auth: { user, pass },
tls: { rejectUnauthorized: false },
});
transporter.verify(function (error, success) {
if (error) {
console.log('SMTP Verification Error:', error);
} else {
console.log('SMTP Server is ready on port 465');
process.exit(0);
}
});

File diff suppressed because one or more lines are too long