feat: visual updates and settings

This commit is contained in:
mARTin 2026-02-15 15:33:15 +01:00
parent dcd2e75537
commit 779181ddde
4 changed files with 700 additions and 149 deletions

View File

@ -41,8 +41,24 @@ model User {
viewStyle String @default("grid")
fontSize String @default("M") // "S", "M", "L"
headlineFont String @default("Inter")
headlineFontSize String? @default("1.25rem")
headlineFontWeight String? @default("900")
dateFontFamily String? @default("Inter")
dateFontSize String? @default("0.65rem")
dateFontWeight String? @default("400")
timeTaskFontFamily String? @default("Inter")
timeTaskFontSize String? @default("0.75rem")
timeTaskFontWeight String? @default("500")
bodyFont String @default("Inter")
fontWeight String @default("400") // "300", "400", "500", "700"
taskFontFamily String? @default("Inter")
taskFontSize String? @default("0.9rem")
taskFontWeight String? @default("400")
eventFontFamily String? @default("Inter")
eventFontSize String? @default("0.85rem")
eventFontWeight String? @default("400")
fontWeight String @default("400") // Legacy/Generic
weekendColorSat String? @default("#666666")
weekendColorSun String? @default("#dc2626")
accounts Account[]
sessions Session[]

View File

@ -5,6 +5,7 @@ import { PrismaClient } from '@prisma/client';
import bcrypt from 'bcryptjs';
const prisma = new PrismaClient();
// Prisma client re-initialized to pick up schema changes
// Get user profile
export async function GET(request: NextRequest) {
@ -36,8 +37,24 @@ export async function GET(request: NextRequest) {
viewStyle: true,
fontSize: true,
headlineFont: true,
headlineFontSize: true,
headlineFontWeight: true,
dateFontFamily: true,
dateFontSize: true,
dateFontWeight: true,
timeTaskFontFamily: true,
timeTaskFontSize: true,
timeTaskFontWeight: true,
bodyFont: true,
taskFontFamily: true,
taskFontSize: true,
taskFontWeight: true,
eventFontFamily: true,
eventFontSize: true,
eventFontWeight: true,
fontWeight: true,
weekendColorSat: true,
weekendColorSun: true,
createdAt: true
}
});
@ -65,7 +82,12 @@ export async function PATCH(request: NextRequest) {
showNextTask, calendarEditMode, focusTimerDuration, focusBreakDuration,
showTimeGrid, showSomeday, showAllDayEvents, showSchedule,
cellDuration, viewStyle, fontSize,
headlineFont, bodyFont, fontWeight
headlineFont, headlineFontSize, headlineFontWeight,
dateFontFamily, dateFontSize, dateFontWeight,
timeTaskFontFamily, timeTaskFontSize, timeTaskFontWeight,
bodyFont, taskFontFamily, taskFontSize, taskFontWeight,
eventFontFamily, eventFontSize, eventFontWeight,
fontWeight, weekendColorSat, weekendColorSun
} = body;
const updateData: any = {
@ -90,8 +112,24 @@ export async function PATCH(request: NextRequest) {
...(viewStyle !== undefined && { viewStyle }),
...(fontSize !== undefined && { fontSize }),
...(headlineFont !== undefined && { headlineFont }),
...(headlineFontSize !== undefined && { headlineFontSize }),
...(headlineFontWeight !== undefined && { headlineFontWeight }),
...(dateFontFamily !== undefined && { dateFontFamily }),
...(dateFontSize !== undefined && { dateFontSize }),
...(dateFontWeight !== undefined && { dateFontWeight }),
...(timeTaskFontFamily !== undefined && { timeTaskFontFamily }),
...(timeTaskFontSize !== undefined && { timeTaskFontSize }),
...(timeTaskFontWeight !== undefined && { timeTaskFontWeight }),
...(bodyFont !== undefined && { bodyFont }),
...(taskFontFamily !== undefined && { taskFontFamily }),
...(taskFontSize !== undefined && { taskFontSize }),
...(taskFontWeight !== undefined && { taskFontWeight }),
...(eventFontFamily !== undefined && { eventFontFamily }),
...(eventFontSize !== undefined && { eventFontSize }),
...(eventFontWeight !== undefined && { eventFontWeight }),
...(fontWeight !== undefined && { fontWeight }),
...(weekendColorSat !== undefined && { weekendColorSat }),
...(weekendColorSun !== undefined && { weekendColorSun }),
};
if (password && password.trim() !== "") {
updateData.passwordHash = await bcrypt.hash(password, 10);
@ -124,8 +162,24 @@ export async function PATCH(request: NextRequest) {
viewStyle: true,
fontSize: true,
headlineFont: true,
headlineFontSize: true,
headlineFontWeight: true,
dateFontFamily: true,
dateFontSize: true,
dateFontWeight: true,
timeTaskFontFamily: true,
timeTaskFontSize: true,
timeTaskFontWeight: true,
bodyFont: true,
taskFontFamily: true,
taskFontSize: true,
taskFontWeight: true,
eventFontFamily: true,
eventFontSize: true,
eventFontWeight: true,
fontWeight: true,
weekendColorSat: true,
weekendColorSun: true,
}
});

View File

@ -221,6 +221,17 @@ h3 {
gap: 0.25rem;
}
.weekly-btn-primary {
background-color: var(--weekly-teal);
color: white;
border: none;
border-radius: 4px;
padding: 8px 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.nav-button:hover {
background-color: #e2e8f0;
transform: translateY(-1px);
@ -617,6 +628,17 @@ h3 {
border-left: none;
}
/* Weekend Styling */
.weekly-day-column.is-sat .weekly-day-name,
.weekly-day-column.is-sat .weekly-day-date {
color: var(--weekly-weekend-sat, #666);
}
.weekly-day-column.is-sun .weekly-day-name,
.weekly-day-column.is-sun .weekly-day-date {
color: var(--weekly-weekend-sun, #dc2626);
}
/* Day Header */
.weekly-day-header {
padding: 1rem 1rem 0.75rem;
@ -626,20 +648,22 @@ h3 {
}
.weekly-day-date {
font-size: 0.65rem;
font-size: var(--weekly-date-size, 0.65rem);
font-weight: var(--weekly-date-weight, 400);
color: var(--weekly-text-light);
text-transform: uppercase;
letter-spacing: 0.1em;
margin-bottom: 0.25rem;
font-family: var(--weekly-date-font, var(--weekly-font-headline)) !important;
}
.weekly-day-name {
font-size: 1.25rem;
font-weight: 900;
font-size: var(--weekly-headline-size, 1.25rem);
font-weight: var(--weekly-headline-weight, 900);
text-transform: uppercase;
letter-spacing: -0.02em;
color: var(--weekly-text);
font-family: var(--weekly-font-headline);
font-family: var(--weekly-font-headline) !important;
}
.weekly-day-name.is-today {
@ -685,13 +709,14 @@ h3 {
.weekly-task-text {
flex: 1;
font-size: var(--base-font-size);
line-height: 1.5;
font-family: var(--weekly-task-font, var(--weekly-font));
font-size: var(--weekly-task-size, 0.9rem);
font-weight: var(--weekly-task-weight, 400);
line-height: var(--weekly-task-line-height, 1.5);
color: var(--weekly-text);
border: none;
outline: none;
background: transparent;
font-family: inherit;
resize: none;
min-height: 1.5em;
padding-top: 2px; /* Align with icons */
@ -912,6 +937,10 @@ h3 {
color: var(--weekly-text);
}
.weekly-day-name {
color: var(--weekly-text-light);
}
.weekly-someday-title {
font-size: 0.75rem;
font-weight: 600;
@ -1296,6 +1325,7 @@ h3 {
.weekly-calendar-event-title {
color: var(--weekly-text);
font-family: var(--weekly-font);
}
/* Drag and Drop States */
@ -1311,7 +1341,9 @@ h3 {
.weekly-settings-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.4);
background: transparent !important; /* Changed from rgba(0, 0, 0, 0.4) for real-time preview */
backdrop-filter: none !important;
-webkit-backdrop-filter: none !important;
display: flex;
align-items: center;
justify-content: center;
@ -1434,7 +1466,7 @@ h3 {
.time-slot-label {
display: flex;
align-items: flex-end;
align-items: flex-start;
justify-content: flex-start;
padding: 0 0.5rem;
font-size: 0.65rem;
@ -1516,7 +1548,9 @@ h3 {
gap: 0.25rem;
padding: 2px 0;
background: transparent;
font-size: 0.875rem;
font-family: var(--weekly-time-task-font, var(--weekly-font));
font-size: var(--weekly-time-task-size, 0.75rem);
font-weight: var(--weekly-time-task-weight, 500);
cursor: pointer;
white-space: nowrap;
overflow: hidden;
@ -1573,18 +1607,25 @@ h3 {
flex-shrink: 0;
}
.event-title {
font-weight: 600;
font-size: 0.85rem;
font-family: var(--weekly-font);
}
.time-slot-event .event-title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
font-weight: 500;
font-family: var(--weekly-font);
}
.time-slot-event .event-time-row {
font-size: 0.65rem;
opacity: 0.8;
white-space: nowrap;
font-size: 0.75rem;
opacity: 0.9;
font-family: var(--weekly-font);
}
.time-slot-event:hover {
@ -2407,3 +2448,16 @@ h3 {
letter-spacing: 0.1em;
margin-bottom: 12px;
}
.event-title {
font-family: var(--weekly-event-font);
font-weight: var(--weekly-event-weight);
font-size: var(--weekly-event-size);
line-height: 1.2;
}
.event-time-row {
font-family: var(--weekly-event-font);
font-size: 0.7rem;
opacity: 0.9;
}

View File

@ -343,11 +343,100 @@ export default function WeeklyView() {
const [dateFormat, setDateFormat] = useState('yyyy-MM-dd');
const [somedayExpanded, setSomedayExpanded] = useState(true);
const [draggingListId, setDraggingListId] = useState<string | null>(null);
const [isAllDayExpanded, setIsAllDayExpanded] = useState(true);
const [somedayLists, setSomedayLists] = useState<SomedayList[]>([]);
const [editingTaskId, setEditingTaskId] = useState<string | null>(null);
const [draggingListId, setDraggingListId] = useState<string | null>(null);
// Moved state definitions to the top
const [showSettings, setShowSettings] = useState(false);
const [activeTab, setActiveTab] = useState<'general' | 'calendar' | 'account'>('general');
const [exportStartDate, setExportStartDate] = useState('');
const [exportEndDate, setExportEndDate] = useState('');
const [passwords, setPasswords] = useState({ new: '', confirm: '' });
const [accountMsg, setAccountMsg] = useState('');
const [isVisible, setIsVisible] = useState(false);
const [profile, setProfile] = useState<{
name: string;
email: string;
timezone: string;
autoRolling?: boolean;
protectEventTimes?: boolean;
language?: string;
dateFormat?: string;
timeFormat?: string;
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;
headlineFontSize?: string;
headlineFontWeight?: string;
dateFontFamily?: string;
dateFontSize?: string;
dateFontWeight?: string;
timeTaskFontFamily?: string;
timeTaskFontSize?: string;
timeTaskFontWeight?: string;
bodyFont?: string;
taskFontFamily?: string;
taskFontSize?: string;
taskFontWeight?: string;
eventFontFamily?: string;
eventFontSize?: string;
eventFontWeight?: string;
fontWeight?: string;
weekendColorSat?: string;
weekendColorSun?: string;
}>({
name: session?.user?.name || '',
email: session?.user?.email || '',
timezone: 'UTC',
language: 'de',
dateFormat: 'yyyy-MM-dd',
timeFormat: '24h',
startHour: 8,
endHour: 22,
autoRolling: true,
protectEventTimes: true,
showTimeGrid: true,
cellDuration: 60,
viewStyle: 'list',
fontSize: 'M',
showNextTask: false,
showSomeday: true,
showAllDayEvents: true,
showSchedule: true,
headlineFont: 'Inter',
headlineFontSize: '1.25rem',
headlineFontWeight: '900',
dateFontFamily: 'Inter',
dateFontSize: '0.65rem',
dateFontWeight: '400',
timeTaskFontFamily: 'Inter',
timeTaskFontSize: '0.75rem',
timeTaskFontWeight: '500',
bodyFont: 'Inter',
taskFontFamily: 'Inter',
taskFontSize: '0.9rem',
taskFontWeight: '400',
eventFontFamily: 'Inter',
eventFontSize: '0.85rem',
eventFontWeight: '400',
fontWeight: '400',
weekendColorSat: '#666666',
weekendColorSun: '#dc2626',
focusTimerDuration: 25,
focusBreakDuration: 5
});
const [showSummary, setShowSummary] = useState(false);
const [isAddingSomedayList, setIsAddingSomedayList] = useState(false);
@ -388,11 +477,36 @@ export default function WeeklyView() {
const [focusTimerDuration, setFocusTimerDuration] = useState(25);
const [fontSize, setFontSize] = useState<'S' | 'M' | 'L'>('M');
const [headlineFont, setHeadlineFont] = useState('Inter');
const [headlineFontSize, setHeadlineFontSize] = useState('1.25rem');
const [headlineFontWeight, setHeadlineFontWeight] = useState('900');
const [dateFontFamily, setDateFontFamily] = useState('Inter');
const [dateFontSize, setDateFontSize] = useState('0.65rem');
const [dateFontWeight, setDateFontWeight] = useState('400');
const [timeTaskFontFamily, setTimeTaskFontFamily] = useState('Inter');
const [timeTaskFontSize, setTimeTaskFontSize] = useState('0.75rem');
const [timeTaskFontWeight, setTimeTaskFontWeight] = useState('500');
const [bodyFont, setBodyFont] = useState('Inter');
const [taskFontFamily, setTaskFontFamily] = useState('Inter');
const [taskFontSize, setTaskFontSize] = useState('0.9rem');
const [taskFontWeight, setTaskFontWeight] = useState('400');
const [eventFontFamily, setEventFontFamily] = useState('Inter');
const [eventFontSize, setEventFontSize] = useState('0.85rem');
const [eventFontWeight, setEventFontWeight] = useState('400');
const [fontWeight, setFontWeight] = useState('400');
const [weekendColorSat, setWeekendColorSat] = useState('#666666');
const [weekendColorSun, setWeekendColorSun] = useState('#dc2626');
// Load fonts
useGoogleFonts([headlineFont, bodyFont, 'Dancing Script', 'Pacifico']);
// Dynamic font loading is handled by the main useGoogleFonts hook call below
// Load ALL available fonts at the top level to ensure they are available
// regardless of whether the settings modal is open or closed, and for
// real-time preview usage.
useGoogleFonts([
...AVAILABLE_FONTS.map(f => f.value),
'Dancing Script',
'Pacifico'
]);
// Dynamic font loading is handled by useGoogleFonts hook call above
@ -642,8 +756,24 @@ export default function WeeklyView() {
showAllDayEvents: boolean;
showSchedule: boolean;
headlineFont: string;
headlineFontSize: string;
headlineFontWeight: string;
dateFontFamily: string;
dateFontSize: string;
dateFontWeight: string;
timeTaskFontFamily: string;
timeTaskFontSize: string;
timeTaskFontWeight: string;
bodyFont: string;
taskFontFamily: string;
taskFontSize: string;
taskFontWeight: string;
eventFontFamily: string;
eventFontSize: string;
eventFontWeight: string;
fontWeight: string;
weekendColorSat: string;
weekendColorSun: string;
}) => {
setShowTimeGrid(newSettings.showTimeGrid);
setCellDuration(newSettings.cellDuration);
@ -659,8 +789,24 @@ export default function WeeklyView() {
setShowAllDay(newSettings.showAllDayEvents);
setShowSchedule(newSettings.showSchedule);
setHeadlineFont(newSettings.headlineFont);
setHeadlineFontSize(newSettings.headlineFontSize);
setHeadlineFontWeight(newSettings.headlineFontWeight);
setDateFontFamily(newSettings.dateFontFamily);
setDateFontSize(newSettings.dateFontSize);
setDateFontWeight(newSettings.dateFontWeight);
setTimeTaskFontFamily(newSettings.timeTaskFontFamily);
setTimeTaskFontSize(newSettings.timeTaskFontSize);
setTimeTaskFontWeight(newSettings.timeTaskFontWeight);
setBodyFont(newSettings.bodyFont);
setTaskFontFamily(newSettings.taskFontFamily);
setTaskFontSize(newSettings.taskFontSize);
setTaskFontWeight(newSettings.taskFontWeight);
setEventFontFamily(newSettings.eventFontFamily);
setEventFontSize(newSettings.eventFontSize);
setEventFontWeight(newSettings.eventFontWeight);
setFontWeight(newSettings.fontWeight);
setWeekendColorSat(newSettings.weekendColorSat);
setWeekendColorSun(newSettings.weekendColorSun);
// Custom start/end hours might affect task placement if we filter strictly
fetchTasks();
@ -686,8 +832,33 @@ export default function WeeklyView() {
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.headlineFontSize) setHeadlineFontSize(data.user.headlineFontSize);
if (data.user.headlineFontWeight) setHeadlineFontWeight(data.user.headlineFontWeight);
if (data.user.dateFontFamily) setDateFontFamily(data.user.dateFontFamily);
if (data.user.dateFontSize) setDateFontSize(data.user.dateFontSize);
if (data.user.dateFontWeight) setDateFontWeight(data.user.dateFontWeight);
if (data.user.timeTaskFontFamily) setTimeTaskFontFamily(data.user.timeTaskFontFamily);
if (data.user.timeTaskFontSize) setTimeTaskFontSize(data.user.timeTaskFontSize);
if (data.user.timeTaskFontWeight) setTimeTaskFontWeight(data.user.timeTaskFontWeight);
if (data.user.bodyFont) setBodyFont(data.user.bodyFont);
if (data.user.taskFontFamily) setTaskFontFamily(data.user.taskFontFamily);
if (data.user.taskFontSize) setTaskFontSize(data.user.taskFontSize);
if (data.user.taskFontSize) setTaskFontSize(data.user.taskFontSize);
if (data.user.taskFontWeight) setTaskFontWeight(data.user.taskFontWeight);
if (data.user.eventFontFamily) setEventFontFamily(data.user.eventFontFamily);
if (data.user.eventFontSize) setEventFontSize(data.user.eventFontSize);
if (data.user.eventFontWeight) setEventFontWeight(data.user.eventFontWeight);
if (data.user.fontWeight) setFontWeight(data.user.fontWeight);
if (data.user.weekendColorSat) setWeekendColorSat(data.user.weekendColorSat);
if (data.user.weekendColorSun) setWeekendColorSun(data.user.weekendColorSun);
setProfile(prev => ({
...prev,
...data.user,
name: data.user.name || prev.name,
email: data.user.email || prev.email,
}));
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);
@ -1487,9 +1658,25 @@ export default function WeeklyView() {
const visibleSlots = getTimeSlots(cellDuration, workingHoursStart, workingHoursEnd);
const containerStyle = {
'--font-headline': `"${headlineFont}", sans-serif`,
'--font-body': `"${bodyFont}", sans-serif`,
'--font-weight-body': fontWeight || '400',
'--weekly-font-headline': (profile.headlineFont || headlineFont) ? `"${profile.headlineFont || headlineFont}", sans-serif` : 'var(--font-headline)',
'--weekly-headline-size': profile.headlineFontSize || '1.25rem',
'--weekly-headline-weight': profile.headlineFontWeight || '900',
'--weekly-date-font': (profile.dateFontFamily) ? `"${profile.dateFontFamily}", sans-serif` : 'var(--weekly-font-headline)',
'--weekly-date-size': profile.dateFontSize || '0.65rem',
'--weekly-date-weight': profile.dateFontWeight || '400',
'--weekly-time-task-font': (profile.timeTaskFontFamily) ? `"${profile.timeTaskFontFamily}", sans-serif` : 'var(--weekly-font)',
'--weekly-time-task-size': profile.timeTaskFontSize || '0.75rem',
'--weekly-time-task-weight': profile.timeTaskFontWeight || '500',
'--weekly-font': 'var(--font-body)', /* Force default body font as requested */
'--weekly-task-font': (profile.taskFontFamily) ? `"${profile.taskFontFamily}", sans-serif` : 'var(--weekly-font)',
'--weekly-task-size': profile.taskFontSize || '0.9rem',
'--weekly-task-weight': profile.taskFontWeight || '400',
'--weekly-event-font': (profile.eventFontFamily || eventFontFamily) ? `"${profile.eventFontFamily || eventFontFamily}", sans-serif` : 'var(--weekly-font)',
'--weekly-event-size': profile.eventFontSize || eventFontSize || '0.85rem',
'--weekly-event-weight': profile.eventFontWeight || eventFontWeight || '400',
'--font-weight-body': profile.fontWeight || fontWeight || '400',
'--weekly-weekend-sat': profile.weekendColorSat || '#666666',
'--weekly-weekend-sun': profile.weekendColorSun || '#dc2626',
} as React.CSSProperties;
if (isLoading) {
@ -1773,14 +1960,14 @@ export default function WeeklyView() {
{getVisibleDays().map((date, colIndex) => (
<div
key={date.toISOString()}
className="weekly-day-column"
className={`weekly-day-column ${date.getDay() === 6 ? 'is-sat' : ''} ${date.getDay() === 0 ? 'is-sun' : ''}`}
style={{ viewTransitionName: `day-${date.getFullYear()}-${date.getMonth()}-${date.getDate()}` } as any}
>
{/* Day Header */}
<header className="weekly-day-header">
<div style={{ display: 'flex', alignItems: 'baseline', gap: '4px' }}>
<h3 className={`weekly-day-name ${isSameDay(date, new Date()) ? 'is-today' : ''}`} style={{ marginBottom: 0 }}>
{getDayName(date, language)}, <span style={{ fontWeight: 400 }}>{formatDateHeader(date, language)}</span>
{getDayName(date, language)} <span className="weekly-day-date">{formatDateHeader(date, language)}</span>
</h3>
</div>
</header>
@ -1825,7 +2012,9 @@ export default function WeeklyView() {
// Calculate height based on event duration
const durationMinutes = (eventEnd.getTime() - eventStart.getTime()) / (1000 * 60);
const height = durationMinutes * pixelsPerMinute;
const calculatedHeight = durationMinutes * pixelsPerMinute;
// Ensure minimum height of 15px for visibility
const height = Math.max(calculatedHeight, 15);
const isUnlocked = unlockedEvents.has(event.id);
@ -1982,24 +2171,29 @@ export default function WeeklyView() {
</span>
)}
<div className="task-actions" style={{
display: 'grid',
gridTemplateColumns: 'repeat(2, 1fr)',
display: 'flex',
alignItems: 'center',
gap: '2px',
marginLeft: '4px',
flexShrink: 0
flexShrink: 0,
flexWrap: 'nowrap',
background: darkMode ? 'rgba(0,0,0,0.6)' : 'rgba(255,255,255,0.8)',
padding: '1px 3px',
borderRadius: '4px',
boxShadow: '0 1px 3px rgba(0,0,0,0.1)'
}}>
{/* Edit button */}
<button className="task-action-btn" onClick={(e) => { e.stopPropagation(); setEditingTaskId(task.id); }} title="Edit">
<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>
<svg viewBox="0 0 24 24" width="12" height="12" stroke="currentColor" strokeWidth="2.5" fill="none" strokeLinecap="round" strokeLinejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>
</button>
{/* Notes button */}
<button className="task-action-btn" onClick={(e) => { e.stopPropagation(); setSelectedTaskForNotes(task); }} title="Notes">
<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round"><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="18" x2="21" y2="18"></line></svg>
<svg viewBox="0 0 24 24" width="12" height="12" stroke="currentColor" strokeWidth="2.5" fill="none" strokeLinecap="round" strokeLinejoin="round"><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="18" x2="21" y2="18"></line></svg>
</button>
{/* Roll button */}
{!task.completed && (
<button className={`task-action-btn ${task.isRolling ? 'active' : ''}`} onClick={(e) => { e.stopPropagation(); toggleTaskRolling(task.id); }} title={task.isRolling ? "Disable rolling" : "Enable rolling"}>
<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round"><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>
<svg viewBox="0 0 24 24" width="12" height="12" stroke="currentColor" strokeWidth="2.5" fill="none" strokeLinecap="round" strokeLinejoin="round"><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>
</button>
)}
{/* Recurrence button */}
@ -2008,14 +2202,11 @@ export default function WeeklyView() {
onClick={(e) => { e.stopPropagation(); setSelectedTaskForRecurrence(task); }}
title={task.isRecurring ? "Edit recurrence" : "Make recurring"}
>
<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round">
<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>
<Repeat size={12} strokeWidth={2.5} />
</button>
{/* Delete button */}
<button className="task-action-btn delete" onClick={(e) => { e.stopPropagation(); deleteTask(task.id); }} title="Delete">
<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line></svg>
<svg viewBox="0 0 24 24" width="12" height="12" stroke="currentColor" strokeWidth="2.5" fill="none" strokeLinecap="round" strokeLinejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line></svg>
</button>
</div>
</div>
@ -2050,8 +2241,8 @@ export default function WeeklyView() {
className="time-slot-event"
title={`${event.calendarTitle}: ${event.title}\n${timeStr}`}
style={{
height: `${eventHeight}px`,
minHeight: `${eventHeight}px`,
height: `${Math.max(eventHeight, 15)}px`,
minHeight: `15px`,
position: 'absolute',
top: `${topOffset}px`,
left: '4px',
@ -2135,6 +2326,7 @@ export default function WeeklyView() {
onRecurrence={() => setSelectedTaskForRecurrence(task)}
onDragStart={(e, t) => handleDragStart(e, t)}
onDragEnd={handleDragEnd}
variant="minimal"
/>
))}
</div>
@ -2686,11 +2878,21 @@ export default function WeeklyView() {
showNextTask={showNextTask}
setShowNextTask={setShowNextTask}
headlineFont={headlineFont}
setHeadlineFont={setHeadlineFont}
headlineFontSize={headlineFontSize}
headlineFontWeight={headlineFontWeight}
dateFontFamily={dateFontFamily}
dateFontSize={dateFontSize}
dateFontWeight={dateFontWeight}
timeTaskFontFamily={timeTaskFontFamily}
timeTaskFontSize={timeTaskFontSize}
timeTaskFontWeight={timeTaskFontWeight}
bodyFont={bodyFont}
setBodyFont={setBodyFont}
taskFontFamily={taskFontFamily}
taskFontSize={taskFontSize}
taskFontWeight={taskFontWeight}
fontWeight={fontWeight}
setFontWeight={setFontWeight}
weekendColorSat={weekendColorSat}
weekendColorSun={weekendColorSun}
/>
)
}
@ -3222,8 +3424,21 @@ interface SettingsSidebarProps {
showAllDayEvents: boolean;
showSchedule: boolean;
headlineFont: string;
headlineFontSize: string;
headlineFontWeight: string;
dateFontFamily: string;
dateFontSize: string;
dateFontWeight: string;
timeTaskFontFamily: string;
timeTaskFontSize: string;
timeTaskFontWeight: string;
bodyFont: string;
taskFontFamily: string;
taskFontSize: string;
taskFontWeight: string;
fontWeight: string;
weekendColorSat: string;
weekendColorSun: string;
}) => void;
showTimeGrid: boolean;
setShowTimeGrid: (show: boolean) => void;
@ -3252,11 +3467,24 @@ interface SettingsSidebarProps {
showNextTask: boolean;
setShowNextTask: (show: boolean) => void;
headlineFont: string;
setHeadlineFont: (font: string) => void;
headlineFontSize: string;
headlineFontWeight: string;
dateFontFamily: string;
dateFontSize: string;
dateFontWeight: string;
timeTaskFontFamily: string;
timeTaskFontSize: string;
timeTaskFontWeight: string;
bodyFont: string;
setBodyFont: (font: string) => void;
taskFontFamily: string;
taskFontSize: string;
taskFontWeight: string;
eventFontFamily?: string;
eventFontSize?: string;
eventFontWeight?: string;
fontWeight: string;
setFontWeight: (weight: string) => void;
weekendColorSat: string;
weekendColorSun: string;
}
function SettingsSidebar({
@ -3289,11 +3517,24 @@ function SettingsSidebar({
showNextTask,
setShowNextTask,
headlineFont,
setHeadlineFont,
headlineFontSize,
headlineFontWeight,
dateFontFamily,
dateFontSize,
dateFontWeight,
timeTaskFontFamily,
timeTaskFontSize,
timeTaskFontWeight,
bodyFont,
setBodyFont,
taskFontFamily,
taskFontSize,
taskFontWeight,
eventFontFamily,
eventFontSize,
eventFontWeight,
fontWeight,
setFontWeight
weekendColorSat,
weekendColorSun
}: SettingsSidebarProps) {
const [activeTab, setActiveTab] = useState<'general' | 'calendar' | 'account'>('general');
const [isLoading, setIsLoading] = useState(true);
@ -3325,8 +3566,24 @@ function SettingsSidebar({
showAllDayEvents?: boolean;
showSchedule?: boolean;
headlineFont?: string;
headlineFontSize?: string;
headlineFontWeight?: string;
dateFontFamily?: string;
dateFontSize?: string;
dateFontWeight?: string;
timeTaskFontFamily?: string;
timeTaskFontSize?: string;
timeTaskFontWeight?: string;
bodyFont?: string;
taskFontFamily?: string;
taskFontSize?: string;
taskFontWeight?: string;
eventFontFamily?: string;
eventFontSize?: string;
eventFontWeight?: string;
fontWeight?: string;
weekendColorSat?: string;
weekendColorSun?: string;
}>({
name: '',
email: '',
@ -3349,14 +3606,27 @@ function SettingsSidebar({
showAllDayEvents: true,
showSchedule: true,
headlineFont: 'Inter',
headlineFontSize: '1.25rem',
headlineFontWeight: '900',
dateFontFamily: 'Inter',
dateFontSize: '0.65rem',
dateFontWeight: '400',
timeTaskFontFamily: 'Inter',
timeTaskFontSize: '0.75rem',
timeTaskFontWeight: '500',
bodyFont: 'Inter',
fontWeight: '400'
taskFontFamily: 'Inter',
taskFontSize: '0.9rem',
taskFontWeight: '400',
fontWeight: '400',
weekendColorSat: '#666666',
weekendColorSun: '#dc2626'
});
const t = translations[profile.language || 'en'] || translations['en'];
// Load fonts for preview
useGoogleFonts([profile.headlineFont || '', profile.bodyFont || '']);
// Font loading moved to top level WeeklyView component
useEffect(() => {
fetchProfile();
@ -3397,15 +3667,28 @@ function SettingsSidebar({
fontWeight: data.user.fontWeight || '400',
showSchedule: data.user.showSchedule !== undefined ? data.user.showSchedule : true,
focusBreakDuration: data.user.focusBreakDuration || 5,
headlineFontSize: data.user.headlineFontSize || '1.25rem',
headlineFontWeight: data.user.headlineFontWeight || '900',
dateFontFamily: data.user.dateFontFamily || 'Inter',
dateFontSize: data.user.dateFontSize || '0.65rem',
dateFontWeight: data.user.dateFontWeight || '400',
timeTaskFontFamily: data.user.timeTaskFontFamily || 'Inter',
timeTaskFontSize: data.user.timeTaskFontSize || '0.75rem',
timeTaskFontWeight: data.user.timeTaskFontWeight || '500',
taskFontFamily: data.user.taskFontFamily || 'Inter',
taskFontSize: data.user.taskFontSize || '0.9rem',
taskFontWeight: data.user.taskFontWeight || '400',
eventFontFamily: data.user.eventFontFamily || 'Inter',
eventFontSize: data.user.eventFontSize || '0.85rem',
eventFontWeight: data.user.eventFontWeight || '400',
weekendColorSat: data.user.weekendColorSat || '#666666',
weekendColorSun: data.user.weekendColorSun || '#dc2626',
});
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');
if (data.user.fontSize) setFontSize(data.user.fontSize as 'S' | 'M' | 'L');
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.showSchedule !== undefined) setShowSchedule(data.user.showSchedule);
if (data.user.focusBreakDuration) setFocusBreakDuration(data.user.focusBreakDuration);
@ -3489,10 +3772,10 @@ function SettingsSidebar({
showSomeday: showSomeday,
showAllDayEvents: showAllDay,
showSchedule: showSchedule,
headlineFont: headlineFont,
bodyFont: bodyFont,
fontWeight: fontWeight,
focusBreakDuration: focusBreakDuration,
// The following will be taken from profile if present,
// ensuring edited state is saved.
focusBreakDuration: profile.focusBreakDuration || focusBreakDuration,
focusTimerDuration: profile.focusTimerDuration || focusTimerDuration,
password: (passwords.new && passwords.new.trim() !== "") ? passwords.new : undefined
})
});
@ -3519,8 +3802,24 @@ function SettingsSidebar({
showAllDayEvents: showAllDay,
showSchedule: showSchedule,
headlineFont: headlineFont,
headlineFontSize: headlineFontSize,
headlineFontWeight: headlineFontWeight,
dateFontFamily: dateFontFamily,
dateFontSize: dateFontSize,
dateFontWeight: dateFontWeight,
timeTaskFontFamily: timeTaskFontFamily,
timeTaskFontSize: timeTaskFontSize,
timeTaskFontWeight: timeTaskFontWeight,
bodyFont: bodyFont,
fontWeight: fontWeight
taskFontFamily: taskFontFamily,
taskFontSize: taskFontSize,
taskFontWeight: profile.taskFontWeight || taskFontWeight,
eventFontFamily: profile.eventFontFamily || eventFontFamily,
eventFontSize: profile.eventFontSize || eventFontSize,
eventFontWeight: profile.eventFontWeight || eventFontWeight,
fontWeight: fontWeight,
weekendColorSat: weekendColorSat,
weekendColorSun: weekendColorSun,
});
}
@ -3597,12 +3896,6 @@ function SettingsSidebar({
</div>
<div className="weekly-settings-content" style={{ flex: 1, overflowY: 'auto', padding: '24px' }}>
{accountMsg && (
<div className={`weekly-auth-message ${accountMsg.toLowerCase().includes('success') ? 'success' : 'error'}`}
style={{ marginBottom: '20px', padding: '10px', borderRadius: '4px', textAlign: 'center', fontWeight: 600 }}>
{accountMsg}
</div>
)}
{activeTab === 'general' ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
{/* Motto */}
@ -3850,85 +4143,197 @@ function SettingsSidebar({
</div>
</div>
{/* Font Settings */}
<div style={{ marginBottom: '1.5rem' }}>
<label style={{ display: 'block', fontSize: '0.9rem', fontWeight: 600, marginBottom: '8px' }}>Typography</label>
{/* Typography Settings */}
<div style={{ marginBottom: '1.5rem', borderBottom: '1px solid #eee', paddingBottom: '1rem' }}>
<label style={{ display: 'block', fontSize: '1rem', fontWeight: 700, marginBottom: '12px', color: '#111827' }}>Font Customization</label>
{/* Headline Font */}
<div style={{ marginBottom: '12px' }}>
<label style={{ display: 'block', fontSize: '0.85rem', color: '#666', marginBottom: '4px' }}>Headline Font</label>
{/* Day Names */}
<div style={{ background: '#f8fafc', padding: '12px', borderRadius: '8px', marginBottom: '12px' }}>
<label style={{ display: 'block', fontSize: '0.85rem', fontWeight: 600, color: '#475569', marginBottom: '8px' }}>Weekday Font (e.g. MONTAG)</label>
<div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr 1fr', gap: '8px' }}>
<select
value={profile.headlineFont || 'Inter'}
onChange={(e) => setProfile({ ...profile, headlineFont: e.target.value })}
className="weekly-input"
style={{ width: '100%', padding: '8px', border: '1px solid #ddd', borderRadius: '4px', fontFamily: profile.headlineFont || 'Inter' }}
style={{ width: '100%', padding: '8px', fontSize: '0.9rem' }}
>
{AVAILABLE_FONTS.map(font => (
<option key={font.value} value={font.value} style={{ fontFamily: font.value }}>{font.name}</option>
))}
</select>
</div>
{/* Body Font */}
<div style={{ marginBottom: '12px' }}>
<label style={{ display: 'block', fontSize: '0.85rem', color: '#666', marginBottom: '4px' }}>Body Font</label>
<select
value={profile.bodyFont || 'Inter'}
onChange={(e) => setProfile({ ...profile, bodyFont: e.target.value })}
value={profile.headlineFontSize || '1.25rem'}
onChange={(e) => setProfile({ ...profile, headlineFontSize: e.target.value })}
className="weekly-input"
style={{ width: '100%', padding: '8px', border: '1px solid #ddd', borderRadius: '4px', fontFamily: profile.bodyFont || 'Inter' }}
style={{ width: '100%', padding: '8px', fontSize: '0.9rem' }}
>
<option value="1rem">Small 16px</option>
<option value="1.25rem">Normal 20px</option>
<option value="1.5rem">Large 24px</option>
<option value="1.75rem">XL 28px</option>
<option value="2rem">Huge 32px</option>
</select>
<select
value={profile.headlineFontWeight || '900'}
onChange={(e) => setProfile({ ...profile, headlineFontWeight: e.target.value })}
className="weekly-input"
style={{ width: '100%', padding: '8px', fontSize: '0.9rem' }}
>
<option value="300">Light</option>
<option value="400">Normal</option>
<option value="500">Medium</option>
<option value="600">Semi</option>
<option value="700">Bold</option>
<option value="900">Black</option>
</select>
</div>
</div>
{/* Dates */}
<div style={{ background: '#f8fafc', padding: '12px', borderRadius: '8px', marginBottom: '12px' }}>
<label style={{ display: 'block', fontSize: '0.85rem', fontWeight: 600, color: '#475569', marginBottom: '8px' }}>Date Font (e.g. 12. Feb.)</label>
<div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr 1fr', gap: '8px' }}>
<select
value={profile.dateFontFamily || 'Inter'}
onChange={(e) => setProfile({ ...profile, dateFontFamily: e.target.value })}
className="weekly-input"
style={{ width: '100%', padding: '8px', fontSize: '0.9rem' }}
>
{AVAILABLE_FONTS.map(font => (
<option key={font.value} value={font.value} style={{ fontFamily: font.value }}>{font.name}</option>
))}
</select>
<select
value={profile.dateFontSize || '0.65rem'}
onChange={(e) => setProfile({ ...profile, dateFontSize: e.target.value })}
className="weekly-input"
style={{ width: '100%', padding: '8px', fontSize: '0.9rem' }}
>
<option value="0.55rem">XS 9px</option>
<option value="0.65rem">Normal 10px</option>
<option value="0.75rem">Small 12px</option>
<option value="0.85rem">Medium 14px</option>
<option value="1rem">Large 16px</option>
</select>
<select
value={profile.dateFontWeight || '400'}
onChange={(e) => setProfile({ ...profile, dateFontWeight: e.target.value })}
className="weekly-input"
style={{ width: '100%', padding: '8px', fontSize: '0.9rem' }}
>
<option value="300">Light</option>
<option value="400">Normal</option>
<option value="500">Medium</option>
<option value="600">Semi</option>
<option value="700">Bold</option>
</select>
</div>
</div>
{/* Font Weight */}
<div>
<label style={{ display: 'block', fontSize: '0.85rem', color: '#666', marginBottom: '4px' }}>Body Font Weight</label>
<div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap' }}>
{FONT_WEIGHTS.map(weight => (
<div key={weight.value} style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
<input
type="checkbox"
checked={profile.fontWeight === weight.value}
onChange={() => setProfile({ ...profile, fontWeight: weight.value })}
style={{ width: '16px', height: '16px', cursor: 'pointer' }}
/>
<label onClick={() => setProfile({ ...profile, fontWeight: weight.value })} style={{ cursor: 'pointer', fontSize: '0.9rem' }}>
{weight.name}
</label>
</div>
{/* Tasks */}
<div style={{ background: '#f8fafc', padding: '12px', borderRadius: '8px', marginBottom: '12px' }}>
<label style={{ display: 'block', fontSize: '0.85rem', fontWeight: 600, color: '#475569', marginBottom: '8px' }}>Task Font</label>
<div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr 1fr', gap: '8px' }}>
<select
value={profile.taskFontFamily || 'Inter'}
onChange={(e) => setProfile({ ...profile, taskFontFamily: e.target.value, timeTaskFontFamily: e.target.value })}
className="weekly-input"
style={{ width: '100%', padding: '8px', fontSize: '0.9rem' }}
>
{AVAILABLE_FONTS.map(font => (
<option key={font.value} value={font.value} style={{ fontFamily: font.value }}>{font.name}</option>
))}
</select>
<select
value={profile.taskFontSize || '0.9rem'}
onChange={(e) => setProfile({ ...profile, taskFontSize: e.target.value, timeTaskFontSize: e.target.value })}
className="weekly-input"
style={{ width: '100%', padding: '8px', fontSize: '0.9rem' }}
>
<option value="0.75rem">Small 12px</option>
<option value="0.9rem">Normal 14px</option>
<option value="1rem">Medium 16px</option>
<option value="1.1rem">Large 18px</option>
<option value="1.25rem">XL 20px</option>
</select>
<select
value={profile.taskFontWeight || '400'}
onChange={(e) => setProfile({ ...profile, taskFontWeight: e.target.value, timeTaskFontWeight: e.target.value })}
className="weekly-input"
style={{ width: '100%', padding: '8px', fontSize: '0.9rem' }}
>
<option value="300">Light</option>
<option value="400">Normal</option>
<option value="500">Medium</option>
<option value="600">Semi</option>
<option value="700">Bold</option>
</select>
</div>
</div>
{/* Font Preview */}
<div style={{
marginTop: '1rem',
padding: '1rem',
border: '1px solid #e5e7eb',
borderRadius: '8px',
background: '#f9fafb'
}}>
<div style={{
fontFamily: profile.headlineFont || 'Inter',
fontSize: '1.25rem',
fontWeight: 700,
marginBottom: '0.5rem',
color: '#111827'
}}>
Headline Preview
{/* Calendar Event Font */}
<div style={{ background: '#f8fafc', padding: '12px', borderRadius: '8px', marginBottom: '12px' }}>
<label style={{ display: 'block', fontSize: '0.85rem', fontWeight: 600, color: '#475569', marginBottom: '8px' }}>Calendar Event Font</label>
<div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr 1fr', gap: '8px' }}>
<select
value={profile.eventFontFamily || 'Inter'}
onChange={(e) => setProfile({ ...profile, eventFontFamily: e.target.value })}
className="weekly-input"
style={{ width: '100%', padding: '8px', fontSize: '0.9rem' }}
>
{AVAILABLE_FONTS.map(font => (
<option key={font.value} value={font.value} style={{ fontFamily: font.value }}>{font.name}</option>
))}
</select>
<select
value={profile.eventFontSize || '0.85rem'}
onChange={(e) => setProfile({ ...profile, eventFontSize: e.target.value })}
className="weekly-input"
style={{ width: '100%', padding: '8px', fontSize: '0.9rem' }}
>
<option value="0.75rem">Small 12px</option>
<option value="0.85rem">Normal 14px</option>
<option value="1rem">Medium 16px</option>
<option value="1.1rem">Large 18px</option>
</select>
<select
value={profile.eventFontWeight || '400'}
onChange={(e) => setProfile({ ...profile, eventFontWeight: e.target.value })}
className="weekly-input"
style={{ width: '100%', padding: '8px', fontSize: '0.9rem' }}
>
<option value="300">Light</option>
<option value="400">Normal</option>
<option value="500">Medium</option>
<option value="600">Semi</option>
<option value="700">Bold</option>
</select>
</div>
</div>
{/* Weekend Colors */}
<div style={{ background: '#f8fafc', padding: '12px', borderRadius: '8px', marginBottom: '12px' }}>
<label style={{ display: 'block', fontSize: '0.85rem', fontWeight: 600, color: '#475569', marginBottom: '8px' }}>Weekend Highlight Colors</label>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px' }}>
<div>
<label style={{ display: 'block', fontSize: '0.75rem', color: '#666', marginBottom: '4px' }}>Saturday</label>
<input
type="color"
value={profile.weekendColorSat || '#666666'}
onChange={(e) => setProfile({ ...profile, weekendColorSat: e.target.value })}
style={{ width: '100%', height: '30px', cursor: 'pointer', border: 'none', background: 'transparent' }}
/>
</div>
<div>
<label style={{ display: 'block', fontSize: '0.75rem', color: '#666', marginBottom: '4px' }}>Sunday</label>
<input
type="color"
value={profile.weekendColorSun || '#dc2626'}
onChange={(e) => setProfile({ ...profile, weekendColorSun: e.target.value })}
style={{ width: '100%', height: '30px', cursor: 'pointer', border: 'none', background: 'transparent' }}
/>
</div>
<div style={{
fontFamily: profile.bodyFont || 'Inter',
fontWeight: parseInt(profile.fontWeight || '400'),
fontSize: '0.95rem',
color: '#374151',
lineHeight: 1.5
}}>
Body text preview. The quick brown fox jumps over the lazy dog. 1234567890.
</div>
</div>
</div>
@ -3971,13 +4376,24 @@ function SettingsSidebar({
</div>
</div>
<div style={{ marginTop: '16px', display: 'flex', alignItems: 'center', gap: '12px' }}>
<button
onClick={handleUpdateProfile}
className="weekly-btn-primary"
style={{ marginTop: '8px', padding: '10px', alignSelf: 'flex-start' }}
style={{ padding: '10px 20px' }}
>
{t.saveChanges}
</button>
{accountMsg && (
<span style={{
fontSize: '0.9rem',
color: accountMsg.toLowerCase().includes('success') ? '#059669' : '#dc2626',
fontWeight: 600
}}>
{accountMsg}
</span>
)}
</div>
</div>
) : activeTab === 'calendar' ? (
isLoading ? (
@ -4112,13 +4528,24 @@ function SettingsSidebar({
</div>
<div style={{ marginTop: '16px', display: 'flex', alignItems: 'center', gap: '12px' }}>
<button
type="submit"
className="weekly-auth-button primary"
style={{ marginTop: '8px' }}
className="weekly-btn-primary"
style={{ padding: '10px 20px' }}
>
{t.saveChanges}
</button>
{accountMsg && (
<span style={{
fontSize: '0.9rem',
color: accountMsg.toLowerCase().includes('success') ? '#059669' : '#dc2626',
fontWeight: 600
}}>
{accountMsg}
</span>
)}
</div>
</form>
{/* Data Export Section */}