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:
parent
f603168494
commit
0e854b6e9b
@ -32,13 +32,17 @@ model User {
|
||||
showNextTask Boolean @default(false)
|
||||
calendarEditMode Boolean @default(false)
|
||||
focusTimerDuration Int @default(25)
|
||||
focusBreakDuration Int @default(5)
|
||||
showTimeGrid Boolean @default(true)
|
||||
showSomeday Boolean @default(true)
|
||||
showAllDayEvents Boolean @default(true)
|
||||
showSchedule Boolean @default(true)
|
||||
cellDuration Int @default(30)
|
||||
viewStyle String @default("grid")
|
||||
fontSize String @default("M") // "S", "M", "L"
|
||||
headlineFont String @default("Inter")
|
||||
bodyFont String @default("Inter")
|
||||
fontWeight String @default("normal") // "light", "normal", "bold"
|
||||
fontWeight String @default("400") // "300", "400", "500", "700"
|
||||
|
||||
accounts Account[]
|
||||
sessions Session[]
|
||||
|
||||
@ -27,7 +27,11 @@ export async function GET(request: NextRequest) {
|
||||
showNextTask: true,
|
||||
calendarEditMode: true,
|
||||
focusTimerDuration: true,
|
||||
focusBreakDuration: true,
|
||||
showTimeGrid: true,
|
||||
showSomeday: true,
|
||||
showAllDayEvents: true,
|
||||
showSchedule: true,
|
||||
cellDuration: true,
|
||||
viewStyle: true,
|
||||
fontSize: true,
|
||||
@ -58,8 +62,9 @@ export async function PATCH(request: NextRequest) {
|
||||
const {
|
||||
name, timezone, password, autoRolling, protectEventTimes,
|
||||
language, dateFormat, timeFormat, startHour, endHour,
|
||||
showNextTask, calendarEditMode, focusTimerDuration,
|
||||
showTimeGrid, cellDuration, viewStyle, fontSize,
|
||||
showNextTask, calendarEditMode, focusTimerDuration, focusBreakDuration,
|
||||
showTimeGrid, showSomeday, showAllDayEvents, showSchedule,
|
||||
cellDuration, viewStyle, fontSize,
|
||||
headlineFont, bodyFont, fontWeight
|
||||
} = body;
|
||||
|
||||
@ -76,7 +81,11 @@ export async function PATCH(request: NextRequest) {
|
||||
...(showNextTask !== undefined && { showNextTask }),
|
||||
...(calendarEditMode !== undefined && { calendarEditMode }),
|
||||
...(focusTimerDuration !== undefined && !isNaN(focusTimerDuration) && { focusTimerDuration }),
|
||||
...(focusBreakDuration !== undefined && !isNaN(focusBreakDuration) && { focusBreakDuration }),
|
||||
...(showTimeGrid !== undefined && { showTimeGrid }),
|
||||
...(showSomeday !== undefined && { showSomeday }),
|
||||
...(showAllDayEvents !== undefined && { showAllDayEvents }),
|
||||
...(showSchedule !== undefined && { showSchedule }),
|
||||
...(cellDuration !== undefined && !isNaN(cellDuration) && { cellDuration }),
|
||||
...(viewStyle !== undefined && { viewStyle }),
|
||||
...(fontSize !== undefined && { fontSize }),
|
||||
@ -106,7 +115,11 @@ export async function PATCH(request: NextRequest) {
|
||||
showNextTask: true,
|
||||
calendarEditMode: true,
|
||||
focusTimerDuration: true,
|
||||
focusBreakDuration: true,
|
||||
showTimeGrid: true,
|
||||
showSomeday: true,
|
||||
showAllDayEvents: true,
|
||||
showSchedule: true,
|
||||
cellDuration: true,
|
||||
viewStyle: true,
|
||||
fontSize: true,
|
||||
|
||||
@ -1400,8 +1400,8 @@ h3 {
|
||||
.time-slot-label {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: flex-end;
|
||||
padding: 0 0.25rem;
|
||||
justify-content: flex-start;
|
||||
padding: 0 0.5rem;
|
||||
font-size: 0.65rem;
|
||||
color: var(--weekly-text-light);
|
||||
box-sizing: border-box;
|
||||
@ -2293,3 +2293,104 @@ h3 {
|
||||
/* Ensure they mix/cross-fade or slide as expected */
|
||||
/* 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;
|
||||
}
|
||||
|
||||
@ -3,12 +3,11 @@ import { signOut } from 'next-auth/react';
|
||||
|
||||
interface UserMenuProps {
|
||||
userEmail?: string | null;
|
||||
onOpenRecurring: () => void;
|
||||
onOpenSettings: () => void;
|
||||
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 menuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@ -57,16 +56,6 @@ export default function UserMenu({ userEmail, onOpenRecurring, onOpenSettings, t
|
||||
Settings
|
||||
</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>
|
||||
|
||||
|
||||
@ -353,6 +353,8 @@ export default function WeeklyView() {
|
||||
const [calendarEditMode, setCalendarEditMode] = useState(false);
|
||||
const [selectedTaskForRecurrence, setSelectedTaskForRecurrence] = useState<Task | null>(null);
|
||||
const [showFocusMode, setShowFocusMode] = useState(false);
|
||||
const [showSchedule, setShowSchedule] = useState(true);
|
||||
const [focusBreakDuration, setFocusBreakDuration] = useState(5);
|
||||
|
||||
// New UI State
|
||||
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
||||
@ -631,6 +633,12 @@ export default function WeeklyView() {
|
||||
endHour: number;
|
||||
fontSize: 'S' | 'M' | 'L';
|
||||
showNextTask: boolean;
|
||||
showSomeday: boolean;
|
||||
showAllDayEvents: boolean;
|
||||
showSchedule: boolean;
|
||||
headlineFont: string;
|
||||
bodyFont: string;
|
||||
fontWeight: string;
|
||||
}) => {
|
||||
setShowTimeGrid(newSettings.showTimeGrid);
|
||||
setCellDuration(newSettings.cellDuration);
|
||||
@ -642,9 +650,14 @@ export default function WeeklyView() {
|
||||
setEndHour(newSettings.endHour);
|
||||
setFontSize(newSettings.fontSize);
|
||||
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
|
||||
// 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();
|
||||
};
|
||||
|
||||
@ -663,6 +676,18 @@ export default function WeeklyView() {
|
||||
setShowNextTask(data.user.showNextTask || false);
|
||||
setCalendarEditMode(data.user.calendarEditMode || false);
|
||||
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) {
|
||||
@ -1690,7 +1715,6 @@ export default function WeeklyView() {
|
||||
{/* User Menu */}
|
||||
<UserMenu
|
||||
userEmail={session?.user?.email}
|
||||
onOpenRecurring={() => setIsRecurringTasksOpen(true)}
|
||||
onOpenSettings={() => setShowPreferences(true)}
|
||||
trigger={
|
||||
<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 && (
|
||||
<SettingsModal
|
||||
<SettingsSidebar
|
||||
onClose={() => setShowSettings(false)}
|
||||
onSettingsChanged={handleSettingsChanged}
|
||||
showTimeGrid={showTimeGrid}
|
||||
@ -2602,12 +2626,16 @@ export default function WeeklyView() {
|
||||
setShowSomeday={setShowSomeday}
|
||||
showAllDay={showAllDay}
|
||||
setShowAllDay={setShowAllDay}
|
||||
showSchedule={showSchedule}
|
||||
setShowSchedule={setShowSchedule}
|
||||
motto={motto}
|
||||
setMotto={setMotto}
|
||||
connections={connections}
|
||||
onUpdateConnections={setConnections}
|
||||
focusTimerDuration={focusTimerDuration}
|
||||
setFocusTimerDuration={setFocusTimerDuration}
|
||||
focusBreakDuration={focusBreakDuration}
|
||||
setFocusBreakDuration={setFocusBreakDuration}
|
||||
fontSize={fontSize}
|
||||
setFontSize={setFontSize}
|
||||
showNextTask={showNextTask}
|
||||
@ -3237,7 +3265,7 @@ function RecurrenceModal({ task, onClose, onSave }: RecurrenceModalProps) {
|
||||
}
|
||||
|
||||
// Settings Modal Component
|
||||
interface SettingsModalProps {
|
||||
interface SettingsSidebarProps {
|
||||
onClose: () => void;
|
||||
onSettingsChanged?: (newSettings: {
|
||||
showTimeGrid: boolean;
|
||||
@ -3250,6 +3278,9 @@ interface SettingsModalProps {
|
||||
endHour: number;
|
||||
fontSize: 'S' | 'M' | 'L';
|
||||
showNextTask: boolean;
|
||||
showSomeday: boolean;
|
||||
showAllDayEvents: boolean;
|
||||
showSchedule: boolean;
|
||||
headlineFont: string;
|
||||
bodyFont: string;
|
||||
fontWeight: string;
|
||||
@ -3266,12 +3297,16 @@ interface SettingsModalProps {
|
||||
setShowSomeday: (show: boolean) => void;
|
||||
showAllDay: boolean;
|
||||
setShowAllDay: (show: boolean) => void;
|
||||
showSchedule: boolean;
|
||||
setShowSchedule: (show: boolean) => void;
|
||||
motto: string;
|
||||
setMotto: (motto: string) => void;
|
||||
connections: any[];
|
||||
onUpdateConnections: (connections: any[]) => void;
|
||||
focusTimerDuration: number;
|
||||
setFocusTimerDuration: (duration: number) => void;
|
||||
focusBreakDuration: number;
|
||||
setFocusBreakDuration: (duration: number) => void;
|
||||
fontSize: 'S' | 'M' | 'L';
|
||||
setFontSize: (size: 'S' | 'M' | 'L') => void;
|
||||
showNextTask: boolean;
|
||||
@ -3284,7 +3319,7 @@ interface SettingsModalProps {
|
||||
setFontWeight: (weight: string) => void;
|
||||
}
|
||||
|
||||
function SettingsModal({
|
||||
function SettingsSidebar({
|
||||
onClose,
|
||||
onSettingsChanged,
|
||||
showTimeGrid,
|
||||
@ -3299,12 +3334,16 @@ function SettingsModal({
|
||||
setShowSomeday,
|
||||
showAllDay,
|
||||
setShowAllDay,
|
||||
showSchedule,
|
||||
setShowSchedule,
|
||||
motto,
|
||||
setMotto,
|
||||
connections,
|
||||
onUpdateConnections,
|
||||
focusTimerDuration,
|
||||
setFocusTimerDuration,
|
||||
focusBreakDuration,
|
||||
setFocusBreakDuration,
|
||||
fontSize,
|
||||
setFontSize,
|
||||
showNextTask,
|
||||
@ -3315,14 +3354,15 @@ function SettingsModal({
|
||||
setBodyFont,
|
||||
fontWeight,
|
||||
setFontWeight
|
||||
}: SettingsModalProps) {
|
||||
}: SettingsSidebarProps) {
|
||||
const [activeTab, setActiveTab] = useState<'general' | 'calendar' | 'account'>('general');
|
||||
// connections state removed (lifted)
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [exportStartDate, setExportStartDate] = 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<{
|
||||
name: string;
|
||||
email: string;
|
||||
@ -3335,11 +3375,15 @@ function SettingsModal({
|
||||
startHour?: number;
|
||||
endHour?: number;
|
||||
focusTimerDuration?: number;
|
||||
focusBreakDuration?: number;
|
||||
showTimeGrid?: boolean;
|
||||
cellDuration?: number;
|
||||
viewStyle?: string;
|
||||
fontSize?: 'S' | 'M' | 'L';
|
||||
showNextTask?: boolean;
|
||||
showSomeday?: boolean;
|
||||
showAllDayEvents?: boolean;
|
||||
showSchedule?: boolean;
|
||||
headlineFont?: string;
|
||||
bodyFont?: string;
|
||||
fontWeight?: string;
|
||||
@ -3355,69 +3399,34 @@ function SettingsModal({
|
||||
startHour: 8,
|
||||
endHour: 18,
|
||||
focusTimerDuration: 25,
|
||||
focusBreakDuration: 5,
|
||||
showTimeGrid: true,
|
||||
cellDuration: 30,
|
||||
viewStyle: 'list',
|
||||
fontSize: 'M',
|
||||
showNextTask: false,
|
||||
showSomeday: true,
|
||||
showAllDayEvents: true,
|
||||
showSchedule: true,
|
||||
headlineFont: 'Inter',
|
||||
bodyFont: 'Inter',
|
||||
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 [passwords, setPasswords] = useState({ new: '', confirm: '' });
|
||||
const [accountMsg, setAccountMsg] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
// fetchConnections removed (lifted)
|
||||
fetchProfile();
|
||||
// Trigger slide-in after mount
|
||||
const timer = setTimeout(() => setIsVisible(true), 10);
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
// fetchConnections function removed
|
||||
|
||||
|
||||
// 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 handleClose = () => {
|
||||
setIsVisible(false);
|
||||
setTimeout(onClose, 300);
|
||||
};
|
||||
|
||||
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() {
|
||||
try {
|
||||
const res = await fetch('/api/user/profile');
|
||||
@ -3443,6 +3452,8 @@ function SettingsModal({
|
||||
headlineFont: data.user.headlineFont || 'Inter',
|
||||
bodyFont: data.user.bodyFont || 'Inter',
|
||||
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);
|
||||
@ -3453,6 +3464,8 @@ function SettingsModal({
|
||||
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.showSchedule !== undefined) setShowSchedule(data.user.showSchedule);
|
||||
if (data.user.focusBreakDuration) setFocusBreakDuration(data.user.focusBreakDuration);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
@ -3530,9 +3543,13 @@ function SettingsModal({
|
||||
cellDuration: cellDuration,
|
||||
viewStyle: viewStyle,
|
||||
showNextTask: showNextTask,
|
||||
showSomeday: showSomeday,
|
||||
showAllDayEvents: showAllDay,
|
||||
showSchedule: showSchedule,
|
||||
headlineFont: headlineFont,
|
||||
bodyFont: bodyFont,
|
||||
fontWeight: fontWeight,
|
||||
focusBreakDuration: focusBreakDuration,
|
||||
password: (passwords.new && passwords.new.trim() !== "") ? passwords.new : undefined
|
||||
})
|
||||
});
|
||||
@ -3545,25 +3562,31 @@ function SettingsModal({
|
||||
// Update local app state
|
||||
if (onSettingsChanged) {
|
||||
onSettingsChanged({
|
||||
showTimeGrid: profile.showTimeGrid !== undefined ? profile.showTimeGrid : true,
|
||||
cellDuration: (profile.cellDuration || 30) as CellDuration,
|
||||
viewStyle: (profile.viewStyle || 'list') as 'grid' | 'list',
|
||||
showTimeGrid: showTimeGrid,
|
||||
cellDuration: cellDuration,
|
||||
viewStyle: viewStyle,
|
||||
language: profile.language || 'en',
|
||||
dateFormat: profile.dateFormat || 'MM/dd/yyyy',
|
||||
timeFormat: profile.timeFormat || '12h',
|
||||
startHour: profile.startHour || 8,
|
||||
endHour: profile.endHour || 18,
|
||||
fontSize: (profile.fontSize || 'M') as 'S' | 'M' | 'L',
|
||||
fontSize: fontSize,
|
||||
showNextTask: showNextTask,
|
||||
headlineFont: profile.headlineFont || 'Inter',
|
||||
bodyFont: profile.bodyFont || 'Inter',
|
||||
fontWeight: profile.fontWeight || '400'
|
||||
showSomeday: showSomeday,
|
||||
showAllDayEvents: showAllDay,
|
||||
showSchedule: showSchedule,
|
||||
headlineFont: headlineFont,
|
||||
bodyFont: bodyFont,
|
||||
fontWeight: fontWeight
|
||||
});
|
||||
}
|
||||
|
||||
if (profile.focusTimerDuration && setFocusTimerDuration) {
|
||||
setFocusTimerDuration(profile.focusTimerDuration);
|
||||
}
|
||||
if (profile.focusBreakDuration && setFocusBreakDuration) {
|
||||
setFocusBreakDuration(profile.focusBreakDuration);
|
||||
}
|
||||
|
||||
// Temporary success message
|
||||
setTimeout(() => setAccountMsg(''), 3000);
|
||||
@ -3597,44 +3620,16 @@ function SettingsModal({
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="weekly-settings-overlay"
|
||||
onClick={onClose}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
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
|
||||
});
|
||||
}}
|
||||
>
|
||||
className={`weekly-settings-overlay ${isVisible ? 'show' : ''}`}
|
||||
onClick={handleClose}
|
||||
style={{ zIndex: 1999 }}
|
||||
/>
|
||||
<div className={`weekly-settings-sidebar ${isVisible ? 'open' : ''}`}>
|
||||
<header className="weekly-settings-header">
|
||||
<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>
|
||||
|
||||
<div className="weekly-settings-tabs" style={{ display: 'flex', borderBottom: '1px solid #eee', padding: '0 24px' }}>
|
||||
@ -3658,7 +3653,7 @@ function SettingsModal({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="weekly-settings-content">
|
||||
<div className="weekly-settings-content" style={{ flex: 1, overflowY: 'auto', padding: '24px' }}>
|
||||
{activeTab === 'general' ? (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
|
||||
{/* Motto */}
|
||||
@ -3701,6 +3696,19 @@ function SettingsModal({
|
||||
</label>
|
||||
</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' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
@ -3934,10 +3942,7 @@ function SettingsModal({
|
||||
{FONT_WEIGHTS.map(weight => (
|
||||
<div key={weight.value} style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||||
<input
|
||||
type="checkbox" // Using checkbox as requested, acting like radio here for simplicity or actual radio? User asked for checkboxes instead of radio.
|
||||
// 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.
|
||||
type="checkbox"
|
||||
checked={profile.fontWeight === weight.value}
|
||||
onChange={() => setProfile({ ...profile, fontWeight: weight.value })}
|
||||
style={{ width: '16px', height: '16px', cursor: 'pointer' }}
|
||||
@ -3992,8 +3997,9 @@ function SettingsModal({
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '1rem' }}>
|
||||
<label style={{ display: 'block', marginBottom: '0.5rem', fontWeight: 500 }}>Focus Timer Duration (minutes)</label>
|
||||
<div style={{ marginBottom: '1rem', display: 'flex', gap: '16px' }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<label style={{ display: 'block', marginBottom: '0.5rem', fontWeight: 500 }}>Focus Timer (min)</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
@ -4003,9 +4009,21 @@ function SettingsModal({
|
||||
style={{ width: '100%', padding: '8px', border: '1px solid #ddd', borderRadius: '4px' }}
|
||||
/>
|
||||
</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
|
||||
onClick={handleUpdateProfile} // Reuse handleUpdateProfile to save
|
||||
onClick={handleUpdateProfile}
|
||||
className="weekly-btn-primary"
|
||||
style={{ marginTop: '8px', padding: '10px', alignSelf: 'flex-start' }}
|
||||
>
|
||||
@ -4090,8 +4108,6 @@ function SettingsModal({
|
||||
</>
|
||||
)
|
||||
) : (
|
||||
/* Account Tab */
|
||||
/* Account Tab */
|
||||
/* Account Tab */
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
|
||||
<form onSubmit={handleUpdateProfile} style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||
@ -4216,30 +4232,8 @@ function SettingsModal({
|
||||
</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 >
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user