Refactor Someday area to grid layout with dynamic slots, fixed scrolling, and border refinements
This commit is contained in:
parent
dea7c387ea
commit
fcaadc1ce9
@ -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",
|
||||
"main": "index.js",
|
||||
"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",
|
||||
"start": "next start",
|
||||
"start": "echo \"\\n ✦ My Weekly ToDo List v$(node -p \"require('./package.json').version\") — production\\n\" && next start",
|
||||
"lint": "next lint",
|
||||
"type-check": "tsc --noEmit",
|
||||
"test": "jest"
|
||||
|
||||
@ -142,6 +142,7 @@ model Task {
|
||||
dayOfWeek Int?
|
||||
scheduledDate DateTime?
|
||||
somedayListId String?
|
||||
somedaySlotIndex Int?
|
||||
originalDate DateTime?
|
||||
startTime String?
|
||||
endTime String?
|
||||
|
||||
@ -195,7 +195,7 @@ export async function POST(request: NextRequest) {
|
||||
const userId = (session.user as any).id;
|
||||
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;
|
||||
const { isRecurring } = body;
|
||||
|
||||
@ -232,6 +232,7 @@ export async function POST(request: NextRequest) {
|
||||
recurrenceUnit,
|
||||
recurrenceTime,
|
||||
recurrenceEndDate: recurrenceEndDate ? new Date(recurrenceEndDate) : null,
|
||||
somedaySlotIndex: somedaySlotIndex !== undefined ? parseInt(somedaySlotIndex) : null,
|
||||
parentTaskId: parentTaskId || null,
|
||||
},
|
||||
});
|
||||
@ -265,7 +266,7 @@ export async function PATCH(request: NextRequest) {
|
||||
const body = await request.json();
|
||||
|
||||
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) {
|
||||
return NextResponse.json(
|
||||
@ -349,6 +350,7 @@ export async function PATCH(request: NextRequest) {
|
||||
...(recurrenceTime !== undefined && { recurrenceTime }),
|
||||
...(recurrenceEndDate !== undefined && { recurrenceEndDate: recurrenceEndDate ? new Date(recurrenceEndDate) : null }),
|
||||
...(restore === true && { deletedAt: null }),
|
||||
...(somedaySlotIndex !== undefined && { somedaySlotIndex: somedaySlotIndex !== null ? parseInt(somedaySlotIndex) : null }),
|
||||
...(parentTaskId !== undefined && { parentTaskId: parentTaskId || null })
|
||||
},
|
||||
});
|
||||
|
||||
@ -104,6 +104,20 @@ function LoginContent() {
|
||||
Continue with Google
|
||||
</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 */}
|
||||
<div className="weekly-auth-links">
|
||||
<Link href="/auth/forgot-password" className="weekly-auth-link">
|
||||
|
||||
@ -169,6 +169,20 @@ export default function SignupPage() {
|
||||
Sign up with Google
|
||||
</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 */}
|
||||
<div className="weekly-auth-links">
|
||||
<span className="weekly-auth-text">Already have an account?</span>
|
||||
|
||||
@ -1158,7 +1158,7 @@ h3 {
|
||||
/* Task Input */
|
||||
.weekly-task-input {
|
||||
padding: 0.5rem 1rem 0.5rem 2.5rem;
|
||||
border-bottom: 1px solid var(--weekly-border);
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.weekly-task-input input {
|
||||
@ -1177,11 +1177,17 @@ h3 {
|
||||
|
||||
/* Someday Section */
|
||||
.weekly-someday {
|
||||
/* background color removed as per user request */
|
||||
background-color: #f9f9f9;
|
||||
width: 100%;
|
||||
padding: 0 1rem;
|
||||
transition: max-height 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.weekly-container.dark-mode .weekly-someday {
|
||||
background-color: #1a1a1b;
|
||||
}
|
||||
|
||||
.weekly-someday.collapsed {
|
||||
max-height: 30px;
|
||||
overflow: hidden;
|
||||
@ -1251,16 +1257,16 @@ h3 {
|
||||
|
||||
/* Someday Lists Container - Horizontal Scroll */
|
||||
.weekly-someday-lists-grid {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden; /* Prevent vertical Scrollbar on container */
|
||||
padding-bottom: 0.5rem; /* Space for scrollbar */
|
||||
display: flex !important;
|
||||
flex-direction: row !important;
|
||||
flex-wrap: nowrap !important;
|
||||
gap: 1.5rem;
|
||||
width: max-content; /* Allow grid to grow beyond parent width */
|
||||
min-width: 100%;
|
||||
padding-bottom: 1rem;
|
||||
align-items: flex-start;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* Scrollbar Styling for Someday Container */
|
||||
@ -1308,25 +1314,43 @@ h3 {
|
||||
|
||||
/* Ruled paper lines for someday tasks */
|
||||
.weekly-someday-list {
|
||||
/* border-right: 1px solid var(--weekly-border); Removed for cleaner look */
|
||||
padding: 0;
|
||||
min-height: 200px;
|
||||
flex: 0 0 280px; /* Fixed width for horizontal scrolling */
|
||||
width: 280px;
|
||||
flex: 0 0 300px; /* Slightly wider lists */
|
||||
width: 300px;
|
||||
transition: transform 0.2s ease, opacity 0.2s ease;
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
max-height: 380px;
|
||||
background-image: repeating-linear-gradient(
|
||||
transparent,
|
||||
transparent 31px,
|
||||
var(--weekly-border) 31px,
|
||||
var(--weekly-border) 32px
|
||||
);
|
||||
background-attachment: local;
|
||||
background-position: 0 40px; /* Offset for the header */
|
||||
scrollbar-width: none; /* Hide scrollbar Firefox */
|
||||
-ms-overflow-style: none; /* Hide scrollbar IE/Edge */
|
||||
}
|
||||
|
||||
.weekly-someday-list::-webkit-scrollbar {
|
||||
display: none; /* Hide scrollbar Chrome/Safari/Webkit */
|
||||
}
|
||||
|
||||
.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 {
|
||||
@ -1363,9 +1387,18 @@ h3 {
|
||||
.weekly-someday-list.placeholder-list {
|
||||
background-image: repeating-linear-gradient(
|
||||
transparent,
|
||||
transparent 31px,
|
||||
#f5f5f5 31px,
|
||||
#f5f5f5 32px
|
||||
transparent 37px,
|
||||
#e5e5e5 37px,
|
||||
#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 {
|
||||
border-bottom: 1px solid transparent;
|
||||
min-height: 32px;
|
||||
border-bottom: none;
|
||||
min-height: 38px;
|
||||
height: auto;
|
||||
padding: 4px 1rem;
|
||||
padding: 6px 1rem;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.weekly-container.dark-mode .weekly-someday-list .weekly-task-item {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.weekly-someday-list .weekly-task-text {
|
||||
font-size: var(--base-font-size);
|
||||
line-height: 1.4;
|
||||
@ -1398,25 +1435,31 @@ h3 {
|
||||
}
|
||||
|
||||
.weekly-someday-list-title-header {
|
||||
padding: 0.75rem 1rem 0.25rem;
|
||||
height: 40px;
|
||||
padding: 0 1rem;
|
||||
margin-bottom: 0;
|
||||
height: 56px;
|
||||
min-height: 56px;
|
||||
display: flex;
|
||||
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 {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
font-size: 1.15rem;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--weekly-text, #333);
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--weekly-text, #222);
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-bottom: 1px solid transparent;
|
||||
width: 100%;
|
||||
padding: 2px 0;
|
||||
padding: 4px 0;
|
||||
outline: none;
|
||||
transition: border-color 0.15s ease;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.weekly-someday .weekly-task-item {
|
||||
@ -1763,32 +1806,10 @@ h3 {
|
||||
|
||||
/* === Tablet (≤ 1024px): 5-day view === */
|
||||
@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 === */
|
||||
@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 {
|
||||
grid-template-columns: 1fr;
|
||||
@ -1838,17 +1859,6 @@ h3 {
|
||||
|
||||
/* === Phone portrait (≤ 480px): 1-day view === */
|
||||
@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 */
|
||||
.time-column {
|
||||
|
||||
@ -3,5 +3,5 @@
|
||||
import { SessionProvider } from 'next-auth/react';
|
||||
|
||||
export function Providers({ children }: { children: React.ReactNode }) {
|
||||
return <SessionProvider>{children}</SessionProvider>;
|
||||
return <SessionProvider refetchOnWindowFocus={false}>{children}</SessionProvider>;
|
||||
}
|
||||
|
||||
@ -72,6 +72,7 @@ export default function CalendarEventModal({
|
||||
const [endDate, setEndDate] = useState(getInitialEnd());
|
||||
const [allDay, setAllDay] = useState(!!event?.allDay);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const handleSubmit = async () => {
|
||||
@ -122,13 +123,13 @@ export default function CalendarEventModal({
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSaving(true);
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
await onDelete(event.id, event.calendarId);
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to delete event');
|
||||
setIsSaving(false);
|
||||
setIsDeleting(false);
|
||||
setIsDeleteConfirming(false);
|
||||
}
|
||||
};
|
||||
@ -286,7 +287,7 @@ export default function CalendarEventModal({
|
||||
{event && onDelete && (
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
disabled={isSaving}
|
||||
disabled={isSaving || isDeleting}
|
||||
style={{
|
||||
padding: '10px 20px',
|
||||
background: isDeleteConfirming ? '#d32f2f' : 'transparent',
|
||||
@ -299,13 +300,13 @@ export default function CalendarEventModal({
|
||||
width: isDeleteConfirming ? 'auto' : 'initial' // Expand if needed
|
||||
}}
|
||||
>
|
||||
{isDeleteConfirming ? 'Click again to confirm delete' : 'Delete'}
|
||||
{isDeleting ? 'Deleting...' : isDeleteConfirming ? 'Confirm Delete' : 'Delete'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<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-primary" onClick={handleSubmit} disabled={isSaving}>
|
||||
<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 || isDeleting}>
|
||||
{isSaving ? 'Saving...' : 'Save'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -363,6 +363,7 @@ export const updateEvent = async (
|
||||
title?: string;
|
||||
description?: string;
|
||||
location?: string;
|
||||
url?: string;
|
||||
start?: { 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.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.date) {
|
||||
event.startDate = ICAL.Time.fromJSDate(new Date(eventData.start.date), true);
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { NextAuthOptions } from "next-auth";
|
||||
import GoogleProvider from "next-auth/providers/google";
|
||||
import AppleProvider from "next-auth/providers/apple";
|
||||
import { PrismaAdapter } from "@auth/prisma-adapter";
|
||||
import CredentialsProvider from "next-auth/providers/credentials";
|
||||
import { compare } from "bcryptjs";
|
||||
@ -19,6 +20,10 @@ export const authOptions: NextAuthOptions = {
|
||||
}
|
||||
}
|
||||
}),
|
||||
AppleProvider({
|
||||
clientId: process.env.APPLE_ID || "",
|
||||
clientSecret: process.env.APPLE_SECRET || ""
|
||||
}),
|
||||
CredentialsProvider({
|
||||
name: "credentials",
|
||||
credentials: {
|
||||
|
||||
@ -686,8 +686,11 @@ export const updateCalendarEvent = async (
|
||||
if (rrule) googleEvent.recurrence = [rrule];
|
||||
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 =>
|
||||
m.updateEvent(oauth2Client, accessToken, calendarId, eventId, googleEvent)
|
||||
m.updateEvent(oauth2Client, accessToken, calendarId, baseEventId, googleEvent)
|
||||
);
|
||||
|
||||
return {
|
||||
@ -709,8 +712,11 @@ export const updateCalendarEvent = async (
|
||||
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 updatedEvent = await updateOutlookEvent(accessToken, calendarId, eventId, {
|
||||
const updatedEvent = await updateOutlookEvent(accessToken, calendarId, baseEventId, {
|
||||
summary: event.title,
|
||||
description: event.description,
|
||||
start: event.start,
|
||||
@ -740,6 +746,7 @@ export const updateCalendarEvent = async (
|
||||
title: event.title,
|
||||
description: event.description,
|
||||
location: event.location,
|
||||
url: event.url,
|
||||
start: event.start,
|
||||
end: event.end
|
||||
})
|
||||
@ -785,8 +792,11 @@ export const deleteCalendarEvent = async (
|
||||
process.env.GOOGLE_REDIRECT_URI || ''
|
||||
);
|
||||
|
||||
// Default to deleting the whole series if repeating
|
||||
const baseEventId = eventId.split('_')[0];
|
||||
|
||||
await import('./google-calendar').then(m =>
|
||||
m.deleteEvent(oauth2Client, accessToken, calendarId, eventId)
|
||||
m.deleteEvent(oauth2Client, accessToken, calendarId, baseEventId)
|
||||
);
|
||||
return;
|
||||
} else if (connection.provider === 'outlook') {
|
||||
@ -797,7 +807,10 @@ export const deleteCalendarEvent = async (
|
||||
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;
|
||||
} else if (connection.provider === 'apple') {
|
||||
const [email, appPassword] = connection.accessToken.split(':');
|
||||
|
||||
@ -108,15 +108,28 @@ export async function sendVerificationEmail(
|
||||
|
||||
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(`\n======================================================`);
|
||||
console.log(`[DEV VERIFICATION LINK]:\n${verifyLink}`);
|
||||
console.log(`======================================================\n`);
|
||||
|
||||
const result = await getTransporter().sendMail({
|
||||
from,
|
||||
to: email,
|
||||
subject: `${code} – Verify your email for My Weekly ToDo's`,
|
||||
html,
|
||||
});
|
||||
try {
|
||||
// 5 second timeout for SMTP
|
||||
const timeoutPromise = new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('SMTP timeout')), 5000)
|
||||
);
|
||||
|
||||
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 {
|
||||
|
||||
@ -147,7 +147,7 @@ export const getUpcomingEvents = async (
|
||||
const params = new URLSearchParams({
|
||||
startDateTime: startDateTime,
|
||||
endDateTime: endDateTime,
|
||||
'$select': 'subject,bodyPreview,start,end,location,webLink,isAllDay',
|
||||
'$select': 'subject,bodyPreview,start,end,location,webLink,isAllDay,seriesMasterId,type',
|
||||
'$orderby': 'start/dateTime',
|
||||
'$top': '50'
|
||||
});
|
||||
@ -168,7 +168,7 @@ export const getUpcomingEvents = async (
|
||||
|
||||
const data = await response.json();
|
||||
return data.value.map((event: any) => ({
|
||||
id: event.id,
|
||||
id: event.seriesMasterId ? `${event.seriesMasterId}::${event.id}` : event.id,
|
||||
summary: event.subject,
|
||||
description: event.body?.content || event.bodyPreview,
|
||||
start: {
|
||||
|
||||
25
test-email.js
Normal file
25
test-email.js
Normal 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
Loading…
Reference in New Issue
Block a user