feat: implement settings sidebar and user preferences

- Refactored SettingsModal to a slide-in SettingsSidebar
- Added comprehensive preference toggles (Someday, All-Day, Schedule)
- Integrated Google Fonts selection (Headline/Body/Weight)
- Synchronized all preferences with DB and WeeklyView state
- Fixed sidebar positioning and overlay CSS
This commit is contained in:
mARTin 2026-02-13 16:53:44 +01:00
parent f603168494
commit 0e854b6e9b
5 changed files with 258 additions and 157 deletions

View File

@ -32,13 +32,17 @@ model User {
showNextTask Boolean @default(false) showNextTask Boolean @default(false)
calendarEditMode Boolean @default(false) calendarEditMode Boolean @default(false)
focusTimerDuration Int @default(25) focusTimerDuration Int @default(25)
focusBreakDuration Int @default(5)
showTimeGrid Boolean @default(true) showTimeGrid Boolean @default(true)
showSomeday Boolean @default(true)
showAllDayEvents Boolean @default(true)
showSchedule Boolean @default(true)
cellDuration Int @default(30) cellDuration Int @default(30)
viewStyle String @default("grid") viewStyle String @default("grid")
fontSize String @default("M") // "S", "M", "L" fontSize String @default("M") // "S", "M", "L"
headlineFont String @default("Inter") headlineFont String @default("Inter")
bodyFont String @default("Inter") bodyFont String @default("Inter")
fontWeight String @default("normal") // "light", "normal", "bold" fontWeight String @default("400") // "300", "400", "500", "700"
accounts Account[] accounts Account[]
sessions Session[] sessions Session[]

View File

@ -27,7 +27,11 @@ export async function GET(request: NextRequest) {
showNextTask: true, showNextTask: true,
calendarEditMode: true, calendarEditMode: true,
focusTimerDuration: true, focusTimerDuration: true,
focusBreakDuration: true,
showTimeGrid: true, showTimeGrid: true,
showSomeday: true,
showAllDayEvents: true,
showSchedule: true,
cellDuration: true, cellDuration: true,
viewStyle: true, viewStyle: true,
fontSize: true, fontSize: true,
@ -58,8 +62,9 @@ export async function PATCH(request: NextRequest) {
const { const {
name, timezone, password, autoRolling, protectEventTimes, name, timezone, password, autoRolling, protectEventTimes,
language, dateFormat, timeFormat, startHour, endHour, language, dateFormat, timeFormat, startHour, endHour,
showNextTask, calendarEditMode, focusTimerDuration, showNextTask, calendarEditMode, focusTimerDuration, focusBreakDuration,
showTimeGrid, cellDuration, viewStyle, fontSize, showTimeGrid, showSomeday, showAllDayEvents, showSchedule,
cellDuration, viewStyle, fontSize,
headlineFont, bodyFont, fontWeight headlineFont, bodyFont, fontWeight
} = body; } = body;
@ -76,7 +81,11 @@ export async function PATCH(request: NextRequest) {
...(showNextTask !== undefined && { showNextTask }), ...(showNextTask !== undefined && { showNextTask }),
...(calendarEditMode !== undefined && { calendarEditMode }), ...(calendarEditMode !== undefined && { calendarEditMode }),
...(focusTimerDuration !== undefined && !isNaN(focusTimerDuration) && { focusTimerDuration }), ...(focusTimerDuration !== undefined && !isNaN(focusTimerDuration) && { focusTimerDuration }),
...(focusBreakDuration !== undefined && !isNaN(focusBreakDuration) && { focusBreakDuration }),
...(showTimeGrid !== undefined && { showTimeGrid }), ...(showTimeGrid !== undefined && { showTimeGrid }),
...(showSomeday !== undefined && { showSomeday }),
...(showAllDayEvents !== undefined && { showAllDayEvents }),
...(showSchedule !== undefined && { showSchedule }),
...(cellDuration !== undefined && !isNaN(cellDuration) && { cellDuration }), ...(cellDuration !== undefined && !isNaN(cellDuration) && { cellDuration }),
...(viewStyle !== undefined && { viewStyle }), ...(viewStyle !== undefined && { viewStyle }),
...(fontSize !== undefined && { fontSize }), ...(fontSize !== undefined && { fontSize }),
@ -106,7 +115,11 @@ export async function PATCH(request: NextRequest) {
showNextTask: true, showNextTask: true,
calendarEditMode: true, calendarEditMode: true,
focusTimerDuration: true, focusTimerDuration: true,
focusBreakDuration: true,
showTimeGrid: true, showTimeGrid: true,
showSomeday: true,
showAllDayEvents: true,
showSchedule: true,
cellDuration: true, cellDuration: true,
viewStyle: true, viewStyle: true,
fontSize: true, fontSize: true,

View File

@ -1400,8 +1400,8 @@ h3 {
.time-slot-label { .time-slot-label {
display: flex; display: flex;
align-items: flex-end; align-items: flex-end;
justify-content: flex-end; justify-content: flex-start;
padding: 0 0.25rem; padding: 0 0.5rem;
font-size: 0.65rem; font-size: 0.65rem;
color: var(--weekly-text-light); color: var(--weekly-text-light);
box-sizing: border-box; box-sizing: border-box;
@ -2293,3 +2293,104 @@ h3 {
/* Ensure they mix/cross-fade or slide as expected */ /* Ensure they mix/cross-fade or slide as expected */
/* Default is usually fine, but duration control is key */ /* Default is usually fine, but duration control is key */
} }
/* ============================================
SETTINGS SIDEBAR STYLES
============================================ */
.weekly-settings-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.3);
z-index: 2000;
backdrop-filter: blur(2px);
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease;
display: block; /* Overriding possible flex from parent if any */
}
.weekly-settings-overlay.show {
opacity: 1;
visibility: visible;
}
.weekly-settings-sidebar {
position: fixed;
top: 0;
right: 0;
width: 500px;
max-width: 90vw;
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: 2001;
overflow: hidden;
border-left: 1px solid #eee;
}
.weekly-settings-sidebar.open {
transform: translateX(0);
}
.dark-mode .weekly-settings-sidebar {
background: #111;
border-left: 1px solid #333;
color: white;
}
.weekly-settings-sidebar .weekly-settings-header {
padding: 24px;
border-bottom: 1px solid #eee;
display: flex;
align-items: center;
justify-content: space-between;
}
.dark-mode .weekly-settings-sidebar .weekly-settings-header {
border-bottom-color: #333;
}
.weekly-settings-sidebar .weekly-settings-content {
flex: 1;
overflow-y: auto;
padding: 24px;
display: flex;
flex-direction: column;
gap: 24px;
}
.weekly-settings-sidebar .weekly-settings-footer {
padding: 20px 24px;
border-top: 1px solid #eee;
background: #f9f9f9;
display: flex;
justify-content: flex-end;
gap: 12px;
}
.dark-mode .weekly-settings-sidebar .weekly-settings-footer {
background: #1a1a1a;
border-top-color: #333;
}
.toggle-checkbox {
width: 18px;
height: 18px;
cursor: pointer;
}
.settings-section-title {
font-size: 0.75rem;
font-weight: 700;
color: #999;
text-transform: uppercase;
letter-spacing: 0.1em;
margin-bottom: 12px;
}

View File

@ -3,12 +3,11 @@ import { signOut } from 'next-auth/react';
interface UserMenuProps { interface UserMenuProps {
userEmail?: string | null; userEmail?: string | null;
onOpenRecurring: () => void;
onOpenSettings: () => void; onOpenSettings: () => void;
trigger?: React.ReactNode; trigger?: React.ReactNode;
} }
export default function UserMenu({ userEmail, onOpenRecurring, onOpenSettings, trigger }: UserMenuProps) { export default function UserMenu({ userEmail, onOpenSettings, trigger }: UserMenuProps) {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const menuRef = useRef<HTMLDivElement>(null); const menuRef = useRef<HTMLDivElement>(null);
@ -57,16 +56,6 @@ export default function UserMenu({ userEmail, onOpenRecurring, onOpenSettings, t
Settings Settings
</button> </button>
<button
onClick={() => { onOpenRecurring(); setIsOpen(false); }}
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 flex items-center gap-2"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-gray-400">
<polyline points="23 4 23 10 17 10"></polyline>
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
</svg>
Recurring To-Dos
</button>
<div className="border-t border-gray-100 my-1"></div> <div className="border-t border-gray-100 my-1"></div>

View File

@ -353,6 +353,8 @@ export default function WeeklyView() {
const [calendarEditMode, setCalendarEditMode] = useState(false); const [calendarEditMode, setCalendarEditMode] = useState(false);
const [selectedTaskForRecurrence, setSelectedTaskForRecurrence] = useState<Task | null>(null); const [selectedTaskForRecurrence, setSelectedTaskForRecurrence] = useState<Task | null>(null);
const [showFocusMode, setShowFocusMode] = useState(false); const [showFocusMode, setShowFocusMode] = useState(false);
const [showSchedule, setShowSchedule] = useState(true);
const [focusBreakDuration, setFocusBreakDuration] = useState(5);
// New UI State // New UI State
const [isSearchOpen, setIsSearchOpen] = useState(false); const [isSearchOpen, setIsSearchOpen] = useState(false);
@ -631,6 +633,12 @@ export default function WeeklyView() {
endHour: number; endHour: number;
fontSize: 'S' | 'M' | 'L'; fontSize: 'S' | 'M' | 'L';
showNextTask: boolean; showNextTask: boolean;
showSomeday: boolean;
showAllDayEvents: boolean;
showSchedule: boolean;
headlineFont: string;
bodyFont: string;
fontWeight: string;
}) => { }) => {
setShowTimeGrid(newSettings.showTimeGrid); setShowTimeGrid(newSettings.showTimeGrid);
setCellDuration(newSettings.cellDuration); setCellDuration(newSettings.cellDuration);
@ -642,9 +650,14 @@ export default function WeeklyView() {
setEndHour(newSettings.endHour); setEndHour(newSettings.endHour);
setFontSize(newSettings.fontSize); setFontSize(newSettings.fontSize);
setShowNextTask(newSettings.showNextTask); setShowNextTask(newSettings.showNextTask);
setShowSomeday(newSettings.showSomeday);
setShowAllDay(newSettings.showAllDayEvents);
setShowSchedule(newSettings.showSchedule);
setHeadlineFont(newSettings.headlineFont);
setBodyFont(newSettings.bodyFont);
setFontWeight(newSettings.fontWeight);
// Custom start/end hours might affect task placement if we filter strictly // Custom start/end hours might affect task placement if we filter strictly
// But mainly we just re-render. Fetching tasks again isn't strictly necessary unless filtering changed on backend.
// But let's do it to be safe if backend filtering relies on these.
fetchTasks(); fetchTasks();
}; };
@ -663,6 +676,18 @@ export default function WeeklyView() {
setShowNextTask(data.user.showNextTask || false); setShowNextTask(data.user.showNextTask || false);
setCalendarEditMode(data.user.calendarEditMode || false); setCalendarEditMode(data.user.calendarEditMode || false);
if (data.user.fontSize) setFontSize(data.user.fontSize as 'S' | 'M' | 'L'); if (data.user.fontSize) setFontSize(data.user.fontSize as 'S' | 'M' | 'L');
if (data.user.showSomeday !== undefined) setShowSomeday(data.user.showSomeday);
if (data.user.showAllDayEvents !== undefined) setShowAllDay(data.user.showAllDayEvents);
if (data.user.showSchedule !== undefined) setShowSchedule(data.user.showSchedule);
if (data.user.headlineFont) setHeadlineFont(data.user.headlineFont);
if (data.user.bodyFont) setBodyFont(data.user.bodyFont);
if (data.user.fontWeight) setFontWeight(data.user.fontWeight);
if (data.user.focusTimerDuration) setFocusTimerDuration(data.user.focusTimerDuration);
if (data.user.focusBreakDuration) setFocusBreakDuration(data.user.focusBreakDuration);
if (data.user.showTimeGrid !== undefined) setShowTimeGrid(data.user.showTimeGrid);
if (data.user.cellDuration) setCellDuration(data.user.cellDuration as CellDuration);
if (data.user.viewStyle) setViewStyle(data.user.viewStyle as 'grid' | 'list');
} }
} }
} catch (e) { } catch (e) {
@ -1690,7 +1715,6 @@ export default function WeeklyView() {
{/* User Menu */} {/* User Menu */}
<UserMenu <UserMenu
userEmail={session?.user?.email} userEmail={session?.user?.email}
onOpenRecurring={() => setIsRecurringTasksOpen(true)}
onOpenSettings={() => setShowPreferences(true)} onOpenSettings={() => setShowPreferences(true)}
trigger={ trigger={
<button className="p-1.5 hover:bg-gray-100 rounded-md text-gray-500 hover:text-black transition-colors" title="User Menu"> <button className="p-1.5 hover:bg-gray-100 rounded-md text-gray-500 hover:text-black transition-colors" title="User Menu">
@ -2584,10 +2608,10 @@ export default function WeeklyView() {
) )
} }
{/* Settings Modal */} {/* Settings Sidebar */}
{ {
showSettings && ( showSettings && (
<SettingsModal <SettingsSidebar
onClose={() => setShowSettings(false)} onClose={() => setShowSettings(false)}
onSettingsChanged={handleSettingsChanged} onSettingsChanged={handleSettingsChanged}
showTimeGrid={showTimeGrid} showTimeGrid={showTimeGrid}
@ -2602,12 +2626,16 @@ export default function WeeklyView() {
setShowSomeday={setShowSomeday} setShowSomeday={setShowSomeday}
showAllDay={showAllDay} showAllDay={showAllDay}
setShowAllDay={setShowAllDay} setShowAllDay={setShowAllDay}
showSchedule={showSchedule}
setShowSchedule={setShowSchedule}
motto={motto} motto={motto}
setMotto={setMotto} setMotto={setMotto}
connections={connections} connections={connections}
onUpdateConnections={setConnections} onUpdateConnections={setConnections}
focusTimerDuration={focusTimerDuration} focusTimerDuration={focusTimerDuration}
setFocusTimerDuration={setFocusTimerDuration} setFocusTimerDuration={setFocusTimerDuration}
focusBreakDuration={focusBreakDuration}
setFocusBreakDuration={setFocusBreakDuration}
fontSize={fontSize} fontSize={fontSize}
setFontSize={setFontSize} setFontSize={setFontSize}
showNextTask={showNextTask} showNextTask={showNextTask}
@ -3237,7 +3265,7 @@ function RecurrenceModal({ task, onClose, onSave }: RecurrenceModalProps) {
} }
// Settings Modal Component // Settings Modal Component
interface SettingsModalProps { interface SettingsSidebarProps {
onClose: () => void; onClose: () => void;
onSettingsChanged?: (newSettings: { onSettingsChanged?: (newSettings: {
showTimeGrid: boolean; showTimeGrid: boolean;
@ -3250,6 +3278,9 @@ interface SettingsModalProps {
endHour: number; endHour: number;
fontSize: 'S' | 'M' | 'L'; fontSize: 'S' | 'M' | 'L';
showNextTask: boolean; showNextTask: boolean;
showSomeday: boolean;
showAllDayEvents: boolean;
showSchedule: boolean;
headlineFont: string; headlineFont: string;
bodyFont: string; bodyFont: string;
fontWeight: string; fontWeight: string;
@ -3266,12 +3297,16 @@ interface SettingsModalProps {
setShowSomeday: (show: boolean) => void; setShowSomeday: (show: boolean) => void;
showAllDay: boolean; showAllDay: boolean;
setShowAllDay: (show: boolean) => void; setShowAllDay: (show: boolean) => void;
showSchedule: boolean;
setShowSchedule: (show: boolean) => void;
motto: string; motto: string;
setMotto: (motto: string) => void; setMotto: (motto: string) => void;
connections: any[]; connections: any[];
onUpdateConnections: (connections: any[]) => void; onUpdateConnections: (connections: any[]) => void;
focusTimerDuration: number; focusTimerDuration: number;
setFocusTimerDuration: (duration: number) => void; setFocusTimerDuration: (duration: number) => void;
focusBreakDuration: number;
setFocusBreakDuration: (duration: number) => void;
fontSize: 'S' | 'M' | 'L'; fontSize: 'S' | 'M' | 'L';
setFontSize: (size: 'S' | 'M' | 'L') => void; setFontSize: (size: 'S' | 'M' | 'L') => void;
showNextTask: boolean; showNextTask: boolean;
@ -3284,7 +3319,7 @@ interface SettingsModalProps {
setFontWeight: (weight: string) => void; setFontWeight: (weight: string) => void;
} }
function SettingsModal({ function SettingsSidebar({
onClose, onClose,
onSettingsChanged, onSettingsChanged,
showTimeGrid, showTimeGrid,
@ -3299,12 +3334,16 @@ function SettingsModal({
setShowSomeday, setShowSomeday,
showAllDay, showAllDay,
setShowAllDay, setShowAllDay,
showSchedule,
setShowSchedule,
motto, motto,
setMotto, setMotto,
connections, connections,
onUpdateConnections, onUpdateConnections,
focusTimerDuration, focusTimerDuration,
setFocusTimerDuration, setFocusTimerDuration,
focusBreakDuration,
setFocusBreakDuration,
fontSize, fontSize,
setFontSize, setFontSize,
showNextTask, showNextTask,
@ -3315,14 +3354,15 @@ function SettingsModal({
setBodyFont, setBodyFont,
fontWeight, fontWeight,
setFontWeight setFontWeight
}: SettingsModalProps) { }: SettingsSidebarProps) {
const [activeTab, setActiveTab] = useState<'general' | 'calendar' | 'account'>('general'); const [activeTab, setActiveTab] = useState<'general' | 'calendar' | 'account'>('general');
// connections state removed (lifted)
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [exportStartDate, setExportStartDate] = useState(''); const [exportStartDate, setExportStartDate] = useState('');
const [exportEndDate, setExportEndDate] = useState(''); const [exportEndDate, setExportEndDate] = useState('');
const [passwords, setPasswords] = useState({ new: '', confirm: '' });
const [accountMsg, setAccountMsg] = useState('');
const [isVisible, setIsVisible] = useState(false);
// Account State
const [profile, setProfile] = useState<{ const [profile, setProfile] = useState<{
name: string; name: string;
email: string; email: string;
@ -3335,11 +3375,15 @@ function SettingsModal({
startHour?: number; startHour?: number;
endHour?: number; endHour?: number;
focusTimerDuration?: number; focusTimerDuration?: number;
focusBreakDuration?: number;
showTimeGrid?: boolean; showTimeGrid?: boolean;
cellDuration?: number; cellDuration?: number;
viewStyle?: string; viewStyle?: string;
fontSize?: 'S' | 'M' | 'L'; fontSize?: 'S' | 'M' | 'L';
showNextTask?: boolean; showNextTask?: boolean;
showSomeday?: boolean;
showAllDayEvents?: boolean;
showSchedule?: boolean;
headlineFont?: string; headlineFont?: string;
bodyFont?: string; bodyFont?: string;
fontWeight?: string; fontWeight?: string;
@ -3355,69 +3399,34 @@ function SettingsModal({
startHour: 8, startHour: 8,
endHour: 18, endHour: 18,
focusTimerDuration: 25, focusTimerDuration: 25,
focusBreakDuration: 5,
showTimeGrid: true, showTimeGrid: true,
cellDuration: 30, cellDuration: 30,
viewStyle: 'list', viewStyle: 'list',
fontSize: 'M', fontSize: 'M',
showNextTask: false, showNextTask: false,
showSomeday: true,
showAllDayEvents: true,
showSchedule: true,
headlineFont: 'Inter', headlineFont: 'Inter',
bodyFont: 'Inter', bodyFont: 'Inter',
fontWeight: '400' fontWeight: '400'
}); });
// Draggable/Resizable Modal State
const [modalPos, setModalPos] = useState({ x: 0, y: 0 });
const [modalSize, setModalSize] = useState({ width: 800, height: 800 });
const [isDragging, setIsDragging] = useState(false);
const [isResizing, setIsResizing] = useState(false);
const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
const [resizeStart, setResizeStart] = useState({ w: 0, h: 0, x: 0, y: 0 });
const t = translations[profile.language || 'en'] || translations['en']; const t = translations[profile.language || 'en'] || translations['en'];
const [passwords, setPasswords] = useState({ new: '', confirm: '' });
const [accountMsg, setAccountMsg] = useState('');
useEffect(() => { useEffect(() => {
// fetchConnections removed (lifted)
fetchProfile(); fetchProfile();
// Trigger slide-in after mount
const timer = setTimeout(() => setIsVisible(true), 10);
return () => clearTimeout(timer);
}, []); }, []);
// fetchConnections function removed const handleClose = () => {
setIsVisible(false);
setTimeout(onClose, 300);
// Add event listeners for dragging and resizing
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
if (isDragging) {
setModalPos({
x: e.clientX - dragStart.x,
y: e.clientY - dragStart.y
});
}
if (isResizing) {
const newWidth = Math.max(400, resizeStart.w + (e.clientX - resizeStart.x));
const newHeight = Math.max(300, resizeStart.h + (e.clientY - resizeStart.y));
setModalSize({ width: newWidth, height: newHeight });
}
}; };
const handleMouseUp = () => {
setIsDragging(false);
setIsResizing(false);
};
if (isDragging || isResizing) {
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseup', handleMouseUp);
}
return () => {
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleMouseUp);
};
}, [isDragging, isResizing, dragStart, resizeStart]);
async function fetchProfile() { async function fetchProfile() {
try { try {
const res = await fetch('/api/user/profile'); const res = await fetch('/api/user/profile');
@ -3443,6 +3452,8 @@ function SettingsModal({
headlineFont: data.user.headlineFont || 'Inter', headlineFont: data.user.headlineFont || 'Inter',
bodyFont: data.user.bodyFont || 'Inter', bodyFont: data.user.bodyFont || 'Inter',
fontWeight: data.user.fontWeight || '400', fontWeight: data.user.fontWeight || '400',
showSchedule: data.user.showSchedule !== undefined ? data.user.showSchedule : true,
focusBreakDuration: data.user.focusBreakDuration || 5,
}); });
if (data.user.showTimeGrid !== undefined) setShowTimeGrid(data.user.showTimeGrid); if (data.user.showTimeGrid !== undefined) setShowTimeGrid(data.user.showTimeGrid);
@ -3453,6 +3464,8 @@ function SettingsModal({
if (data.user.bodyFont) setBodyFont(data.user.bodyFont); if (data.user.bodyFont) setBodyFont(data.user.bodyFont);
if (data.user.fontWeight) setFontWeight(data.user.fontWeight); if (data.user.fontWeight) setFontWeight(data.user.fontWeight);
if (data.user.focusTimerDuration) setFocusTimerDuration(data.user.focusTimerDuration); if (data.user.focusTimerDuration) setFocusTimerDuration(data.user.focusTimerDuration);
if (data.user.showSchedule !== undefined) setShowSchedule(data.user.showSchedule);
if (data.user.focusBreakDuration) setFocusBreakDuration(data.user.focusBreakDuration);
} }
} }
} catch (e) { } catch (e) {
@ -3530,9 +3543,13 @@ function SettingsModal({
cellDuration: cellDuration, cellDuration: cellDuration,
viewStyle: viewStyle, viewStyle: viewStyle,
showNextTask: showNextTask, showNextTask: showNextTask,
showSomeday: showSomeday,
showAllDayEvents: showAllDay,
showSchedule: showSchedule,
headlineFont: headlineFont, headlineFont: headlineFont,
bodyFont: bodyFont, bodyFont: bodyFont,
fontWeight: fontWeight, fontWeight: fontWeight,
focusBreakDuration: focusBreakDuration,
password: (passwords.new && passwords.new.trim() !== "") ? passwords.new : undefined password: (passwords.new && passwords.new.trim() !== "") ? passwords.new : undefined
}) })
}); });
@ -3545,25 +3562,31 @@ function SettingsModal({
// Update local app state // Update local app state
if (onSettingsChanged) { if (onSettingsChanged) {
onSettingsChanged({ onSettingsChanged({
showTimeGrid: profile.showTimeGrid !== undefined ? profile.showTimeGrid : true, showTimeGrid: showTimeGrid,
cellDuration: (profile.cellDuration || 30) as CellDuration, cellDuration: cellDuration,
viewStyle: (profile.viewStyle || 'list') as 'grid' | 'list', viewStyle: viewStyle,
language: profile.language || 'en', language: profile.language || 'en',
dateFormat: profile.dateFormat || 'MM/dd/yyyy', dateFormat: profile.dateFormat || 'MM/dd/yyyy',
timeFormat: profile.timeFormat || '12h', timeFormat: profile.timeFormat || '12h',
startHour: profile.startHour || 8, startHour: profile.startHour || 8,
endHour: profile.endHour || 18, endHour: profile.endHour || 18,
fontSize: (profile.fontSize || 'M') as 'S' | 'M' | 'L', fontSize: fontSize,
showNextTask: showNextTask, showNextTask: showNextTask,
headlineFont: profile.headlineFont || 'Inter', showSomeday: showSomeday,
bodyFont: profile.bodyFont || 'Inter', showAllDayEvents: showAllDay,
fontWeight: profile.fontWeight || '400' showSchedule: showSchedule,
headlineFont: headlineFont,
bodyFont: bodyFont,
fontWeight: fontWeight
}); });
} }
if (profile.focusTimerDuration && setFocusTimerDuration) { if (profile.focusTimerDuration && setFocusTimerDuration) {
setFocusTimerDuration(profile.focusTimerDuration); setFocusTimerDuration(profile.focusTimerDuration);
} }
if (profile.focusBreakDuration && setFocusBreakDuration) {
setFocusBreakDuration(profile.focusBreakDuration);
}
// Temporary success message // Temporary success message
setTimeout(() => setAccountMsg(''), 3000); setTimeout(() => setAccountMsg(''), 3000);
@ -3597,44 +3620,16 @@ function SettingsModal({
}; };
return ( return (
<>
<div <div
className="weekly-settings-overlay" className={`weekly-settings-overlay ${isVisible ? 'show' : ''}`}
onClick={onClose} onClick={handleClose}
style={{ style={{ zIndex: 1999 }}
display: 'flex', />
alignItems: 'center', <div className={`weekly-settings-sidebar ${isVisible ? 'open' : ''}`}>
justifyContent: 'center', <header className="weekly-settings-header">
overflow: 'hidden'
}}
>
<div
className="weekly-settings-modal"
onClick={e => e.stopPropagation()}
style={{
position: 'relative',
width: `${modalSize.width}px`,
height: `${modalSize.height}px`,
transform: `translate(${modalPos.x}px, ${modalPos.y}px)`,
display: 'flex',
flexDirection: 'column',
maxHeight: '90vh',
maxWidth: '95vw',
resize: 'none'
}}
>
<header
className="weekly-settings-header"
style={{ cursor: isDragging ? 'grabbing' : 'grab' }}
onMouseDown={(e) => {
setIsDragging(true);
setDragStart({
x: e.clientX - modalPos.x,
y: e.clientY - modalPos.y
});
}}
>
<h2 className="weekly-settings-title">{t.settings}</h2> <h2 className="weekly-settings-title">{t.settings}</h2>
<button className="weekly-settings-close" onClick={onClose}>×</button> <button className="weekly-settings-close" onClick={handleClose}>×</button>
</header> </header>
<div className="weekly-settings-tabs" style={{ display: 'flex', borderBottom: '1px solid #eee', padding: '0 24px' }}> <div className="weekly-settings-tabs" style={{ display: 'flex', borderBottom: '1px solid #eee', padding: '0 24px' }}>
@ -3658,7 +3653,7 @@ function SettingsModal({
</button> </button>
</div> </div>
<div className="weekly-settings-content"> <div className="weekly-settings-content" style={{ flex: 1, overflowY: 'auto', padding: '24px' }}>
{activeTab === 'general' ? ( {activeTab === 'general' ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
{/* Motto */} {/* Motto */}
@ -3701,6 +3696,19 @@ function SettingsModal({
</label> </label>
</div> </div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<input
type="checkbox"
id="showSchedule"
checked={showSchedule}
onChange={e => setShowSchedule(e.target.checked)}
style={{ width: '16px', height: '16px' }}
/>
<label htmlFor="showSchedule" style={{ fontSize: '0.9rem', fontWeight: 600 }}>
Show Schedule / Calendar
</label>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}> <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<input <input
type="checkbox" type="checkbox"
@ -3934,10 +3942,7 @@ function SettingsModal({
{FONT_WEIGHTS.map(weight => ( {FONT_WEIGHTS.map(weight => (
<div key={weight.value} style={{ display: 'flex', alignItems: 'center', gap: '4px' }}> <div key={weight.value} style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
<input <input
type="checkbox" // Using checkbox as requested, acting like radio here for simplicity or actual radio? User asked for checkboxes instead of radio. type="checkbox"
// But weight is mutually exclusive. Using check behavior where clicking one checks it and likely unchecks others if we want strict single selection, or just let standard radio behavior handle it but style it?
// User said "instead of radio buttons use checkboxes".
// I'll use input type="checkbox" but manage state to ensure single selection if needed, or just let opacity/style indicate selection.
checked={profile.fontWeight === weight.value} checked={profile.fontWeight === weight.value}
onChange={() => setProfile({ ...profile, fontWeight: weight.value })} onChange={() => setProfile({ ...profile, fontWeight: weight.value })}
style={{ width: '16px', height: '16px', cursor: 'pointer' }} style={{ width: '16px', height: '16px', cursor: 'pointer' }}
@ -3992,8 +3997,9 @@ function SettingsModal({
</label> </label>
</div> </div>
<div style={{ marginBottom: '1rem' }}> <div style={{ marginBottom: '1rem', display: 'flex', gap: '16px' }}>
<label style={{ display: 'block', marginBottom: '0.5rem', fontWeight: 500 }}>Focus Timer Duration (minutes)</label> <div style={{ flex: 1 }}>
<label style={{ display: 'block', marginBottom: '0.5rem', fontWeight: 500 }}>Focus Timer (min)</label>
<input <input
type="number" type="number"
min="1" min="1"
@ -4003,9 +4009,21 @@ function SettingsModal({
style={{ width: '100%', padding: '8px', border: '1px solid #ddd', borderRadius: '4px' }} style={{ width: '100%', padding: '8px', border: '1px solid #ddd', borderRadius: '4px' }}
/> />
</div> </div>
<div style={{ flex: 1 }}>
<label style={{ display: 'block', marginBottom: '0.5rem', fontWeight: 500 }}>Focus Break (min)</label>
<input
type="number"
min="1"
max="60"
value={profile.focusBreakDuration || 5}
onChange={(e) => setProfile({ ...profile, focusBreakDuration: parseInt(e.target.value) || 5 })}
style={{ width: '100%', padding: '8px', border: '1px solid #ddd', borderRadius: '4px' }}
/>
</div>
</div>
<button <button
onClick={handleUpdateProfile} // Reuse handleUpdateProfile to save onClick={handleUpdateProfile}
className="weekly-btn-primary" className="weekly-btn-primary"
style={{ marginTop: '8px', padding: '10px', alignSelf: 'flex-start' }} style={{ marginTop: '8px', padding: '10px', alignSelf: 'flex-start' }}
> >
@ -4090,8 +4108,6 @@ function SettingsModal({
</> </>
) )
) : ( ) : (
/* Account Tab */
/* Account Tab */
/* Account Tab */ /* Account Tab */
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
<form onSubmit={handleUpdateProfile} style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}> <form onSubmit={handleUpdateProfile} style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
@ -4216,30 +4232,8 @@ function SettingsModal({
</div> </div>
)} )}
</div> </div>
{/* Resize Handle */}
<div
style={{
position: 'absolute',
bottom: 0,
right: 0,
width: '20px',
height: '20px',
cursor: 'nwse-resize',
zIndex: 100
}}
onMouseDown={(e) => {
e.stopPropagation();
setIsResizing(true);
setResizeStart({
w: modalSize.width,
h: modalSize.height,
x: e.clientX,
y: e.clientY
});
}}
/>
</div>
</div> </div>
</>
); );
} }