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)
|
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[]
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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 >
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user