Add first day of week setting and fix calendar header alignment

- Add first_day_of_week column to settings (0=Sunday, 1=Monday)
- Add Calendar section in Settings with toggle button
- Pass firstDay to FullCalendar from settings
- Align calendar toolbar and sidebar header to h-16 (matches UMBRA header)
- Remove border/padding wrapper from calendar grid for full-width layout

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Kyle 2026-02-22 01:33:45 +08:00
parent 5701e067dd
commit c62f8bc2a2
7 changed files with 104 additions and 5 deletions

View File

@ -0,0 +1,23 @@
"""Add first_day_of_week to settings
Revision ID: 008
Revises: 007
Create Date: 2026-02-22
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers
revision: str = '008'
down_revision: Union[str, None] = '007'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.add_column('settings', sa.Column('first_day_of_week', sa.Integer(), server_default='0', nullable=False))
def downgrade() -> None:
op.drop_column('settings', 'first_day_of_week')

View File

@ -15,5 +15,6 @@ class Settings(Base):
weather_city: Mapped[str | None] = mapped_column(String(100), nullable=True, default=None) weather_city: Mapped[str | None] = mapped_column(String(100), nullable=True, default=None)
weather_lat: Mapped[float | None] = mapped_column(Float, nullable=True, default=None) weather_lat: Mapped[float | None] = mapped_column(Float, nullable=True, default=None)
weather_lon: Mapped[float | None] = mapped_column(Float, nullable=True, default=None) weather_lon: Mapped[float | None] = mapped_column(Float, nullable=True, default=None)
first_day_of_week: Mapped[int] = mapped_column(Integer, default=0) # 0=Sunday, 1=Monday
created_at: Mapped[datetime] = mapped_column(default=func.now()) created_at: Mapped[datetime] = mapped_column(default=func.now())
updated_at: Mapped[datetime] = mapped_column(default=func.now(), onupdate=func.now()) updated_at: Mapped[datetime] = mapped_column(default=func.now(), onupdate=func.now())

View File

@ -29,6 +29,14 @@ class SettingsUpdate(BaseModel):
weather_city: str | None = None weather_city: str | None = None
weather_lat: float | None = None weather_lat: float | None = None
weather_lon: float | None = None weather_lon: float | None = None
first_day_of_week: int | None = None
@field_validator('first_day_of_week')
@classmethod
def validate_first_day(cls, v: int | None) -> int | None:
if v is not None and v not in (0, 1):
raise ValueError('first_day_of_week must be 0 (Sunday) or 1 (Monday)')
return v
@field_validator('weather_lat') @field_validator('weather_lat')
@classmethod @classmethod
@ -53,6 +61,7 @@ class SettingsResponse(BaseModel):
weather_city: str | None = None weather_city: str | None = None
weather_lat: float | None = None weather_lat: float | None = None
weather_lon: float | None = None weather_lon: float | None = None
first_day_of_week: int = 0
created_at: datetime created_at: datetime
updated_at: datetime updated_at: datetime

View File

@ -10,6 +10,7 @@ import { ChevronLeft, ChevronRight } from 'lucide-react';
import api, { getErrorMessage } from '@/lib/api'; import api, { getErrorMessage } from '@/lib/api';
import type { CalendarEvent } from '@/types'; import type { CalendarEvent } from '@/types';
import { useCalendars } from '@/hooks/useCalendars'; import { useCalendars } from '@/hooks/useCalendars';
import { useSettings } from '@/hooks/useSettings';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { import {
Dialog, Dialog,
@ -48,6 +49,7 @@ export default function CalendarPage() {
const [scopeEvent, setScopeEvent] = useState<CalendarEvent | null>(null); const [scopeEvent, setScopeEvent] = useState<CalendarEvent | null>(null);
const [activeEditScope, setActiveEditScope] = useState<'this' | 'this_and_future' | null>(null); const [activeEditScope, setActiveEditScope] = useState<'this' | 'this_and_future' | null>(null);
const { settings } = useSettings();
const { data: calendars = [] } = useCalendars(); const { data: calendars = [] } = useCalendars();
const { data: events = [] } = useQuery({ const { data: events = [] } = useQuery({
@ -269,8 +271,8 @@ export default function CalendarPage() {
<CalendarSidebar /> <CalendarSidebar />
<div className="flex-1 flex flex-col overflow-hidden"> <div className="flex-1 flex flex-col overflow-hidden">
{/* Custom toolbar */} {/* Custom toolbar — h-16 matches sidebar header */}
<div className="border-b bg-card px-6 py-3 flex items-center gap-4"> <div className="border-b bg-card px-6 h-16 flex items-center gap-4 shrink-0">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={navigatePrev}> <Button variant="ghost" size="icon" className="h-8 w-8" onClick={navigatePrev}>
<ChevronLeft className="h-4 w-4" /> <ChevronLeft className="h-4 w-4" />
@ -305,13 +307,14 @@ export default function CalendarPage() {
</div> </div>
{/* Calendar grid */} {/* Calendar grid */}
<div className="flex-1 overflow-y-auto p-4"> <div className="flex-1 overflow-y-auto">
<div className="bg-card rounded-lg border p-3 h-full"> <div className="h-full">
<FullCalendar <FullCalendar
ref={calendarRef} ref={calendarRef}
plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]} plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]}
initialView="dayGridMonth" initialView="dayGridMonth"
headerToolbar={false} headerToolbar={false}
firstDay={settings?.first_day_of_week ?? 0}
events={calendarEvents} events={calendarEvents}
editable={true} editable={true}
selectable={true} selectable={true}

View File

@ -44,7 +44,7 @@ export default function CalendarSidebar() {
return ( return (
<div className="w-56 shrink-0 border-r bg-card flex flex-col"> <div className="w-56 shrink-0 border-r bg-card flex flex-col">
<div className="p-4 border-b flex items-center justify-between"> <div className="h-16 px-4 border-b flex items-center justify-between shrink-0">
<span className="text-sm font-semibold font-heading text-foreground">Calendars</span> <span className="text-sm font-semibold font-heading text-foreground">Calendars</span>
<Button <Button
variant="ghost" variant="ghost"

View File

@ -32,6 +32,8 @@ export default function SettingsPage() {
const [showDropdown, setShowDropdown] = useState(false); const [showDropdown, setShowDropdown] = useState(false);
const searchRef = useRef<HTMLDivElement>(null); const searchRef = useRef<HTMLDivElement>(null);
const debounceRef = useRef<ReturnType<typeof setTimeout>>(); const debounceRef = useRef<ReturnType<typeof setTimeout>>();
const [firstDayOfWeek, setFirstDayOfWeek] = useState(settings?.first_day_of_week ?? 0);
const [pinForm, setPinForm] = useState({ const [pinForm, setPinForm] = useState({
oldPin: '', oldPin: '',
newPin: '', newPin: '',
@ -136,6 +138,17 @@ export default function SettingsPage() {
} }
}; };
const handleFirstDayChange = async (value: number) => {
setFirstDayOfWeek(value);
try {
await updateSettings({ first_day_of_week: value });
queryClient.invalidateQueries({ queryKey: ['calendar-events'] });
toast.success(value === 0 ? 'Week starts on Sunday' : 'Week starts on Monday');
} catch {
toast.error('Failed to update first day of week');
}
};
const handleUpcomingDaysSubmit = async (e: FormEvent) => { const handleUpcomingDaysSubmit = async (e: FormEvent) => {
e.preventDefault(); e.preventDefault();
try { try {
@ -235,6 +248,55 @@ export default function SettingsPage() {
</CardContent> </CardContent>
</Card> </Card>
<Card>
<CardHeader>
<CardTitle>Calendar</CardTitle>
<CardDescription>Configure your calendar preferences</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-2">
<Label>First Day of Week</Label>
<div className="flex items-center rounded-md border border-border overflow-hidden w-fit">
<button
type="button"
onClick={() => handleFirstDayChange(0)}
className={cn(
'px-4 py-2 text-sm font-medium transition-colors duration-150',
firstDayOfWeek === 0
? 'text-accent'
: 'text-muted-foreground hover:text-foreground hover:bg-card-elevated'
)}
style={{
backgroundColor: firstDayOfWeek === 0 ? 'hsl(var(--accent-color) / 0.15)' : undefined,
color: firstDayOfWeek === 0 ? 'hsl(var(--accent-color))' : undefined,
}}
>
Sunday
</button>
<button
type="button"
onClick={() => handleFirstDayChange(1)}
className={cn(
'px-4 py-2 text-sm font-medium transition-colors duration-150',
firstDayOfWeek === 1
? 'text-accent'
: 'text-muted-foreground hover:text-foreground hover:bg-card-elevated'
)}
style={{
backgroundColor: firstDayOfWeek === 1 ? 'hsl(var(--accent-color) / 0.15)' : undefined,
color: firstDayOfWeek === 1 ? 'hsl(var(--accent-color))' : undefined,
}}
>
Monday
</button>
</div>
<p className="text-sm text-muted-foreground">
Sets which day the calendar week starts on
</p>
</div>
</CardContent>
</Card>
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Dashboard</CardTitle> <CardTitle>Dashboard</CardTitle>

View File

@ -6,6 +6,7 @@ export interface Settings {
weather_city?: string | null; weather_city?: string | null;
weather_lat?: number | null; weather_lat?: number | null;
weather_lon?: number | null; weather_lon?: number | null;
first_day_of_week: number;
created_at: string; created_at: string;
updated_at: string; updated_at: string;
} }