My-Weekly-ToDo-List/src/components/QuickSettingsSidebar.tsx
mARTin 163e9c1a07 feat: major update — mobile fix, auth decoupling, CalDAV perf, projects, fonts, quotes
Mobile fixes:
- Add viewport meta tag (was missing, causing broken mobile rendering)
- Make header nav controls visible on mobile (opacity-0 group-hover was invisible on touch)
- Add touch swipe gestures for day navigation on mobile
- Fix viewDays responsive override after profile load

Bug fixes:
- Fix goal save (WeeklyView used POST but route only had GET/PUT; fix to use PUT, add POST alias)
- Fix body key mismatch (goal → text) in goal save request

Auth & identity (Task 1):
- Add accountNumber (auto-increment) to User model for stable identity
- Fix OAuth flows: pass user CUID in state instead of email
- Google/Outlook callbacks now look up users by ID, not email
- Non-Gmail users can now connect Google Calendar/Tasks

CalDAV performance (Task 5):
- Embed CalDAV object URL in Apple event IDs (caldav::<url>::<uid> format)
- Delete/update now use O(1) direct URL access instead of O(n) full calendar scan
- Optimistic UI removal on delete (no blocking force-refresh)
- Legacy fallback for old-format IDs during transition

Apple Calendar data (Task 2):
- Pass URL, location, recurringEventId, isRecurring through full pipeline
- Add url, recurringEventId, isRecurring to CachedCalendarEvent schema
- Calendar cache now reads/writes all new fields

Projects (Task 6):
- New Project model (name, icon, color, description, order)
- CRUD API at /api/tasks/projects
- Tasks now support optional projectId with cascading nullify

Quotes (Task 4):
- Curated local quote database (50 quotes, DE+EN) with tag filtering
- Preset quote source APIs (ZenQuotes, Quotable, Forismatic, Type.fit)

Fonts (Task 7):
- FontPicker component with searchable dropdown and live preview
- /api/fonts endpoint (Google Fonts API proxy with 24h cache, fallback to 40 popular fonts)

Quick Settings (Task 8):
- QuickSettingsSidebar component (font size, spacing, show completed, start day, show lines)
- New user preferences: showCompletedTasks, showLines, startDayOffset, quoteSourceUrls

v1.9.0

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 17:55:49 +01:00

206 lines
8.0 KiB
TypeScript

"use client";
import { X, Type, Space, CheckSquare, Calendar, Minus } from "lucide-react";
interface QuickSettingsProps {
fontSize: string;
onFontSizeChange: (size: string) => void;
spacing: string;
onSpacingChange: (spacing: string) => void;
showCompleted: boolean;
onShowCompletedChange: (show: boolean) => void;
startDayOffset: number;
onStartDayOffsetChange: (offset: number) => void;
showLines: boolean;
onShowLinesChange: (show: boolean) => void;
isOpen: boolean;
onClose: () => void;
darkMode?: boolean;
}
export default function QuickSettingsSidebar({
fontSize,
onFontSizeChange,
spacing,
onSpacingChange,
showCompleted,
onShowCompletedChange,
startDayOffset,
onStartDayOffsetChange,
showLines,
onShowLinesChange,
isOpen,
onClose,
darkMode,
}: QuickSettingsProps) {
const bg = darkMode ? "#1f2937" : "#ffffff";
const text = darkMode ? "#e5e7eb" : "#333333";
const border = darkMode ? "#374151" : "#e5e7eb";
const accent = "#0ea5e9";
const btnBg = darkMode ? "#374151" : "#f3f4f6";
const btnActiveBg = accent;
const btnActiveText = "#ffffff";
const labelColor = darkMode ? "#9ca3af" : "#6b7280";
const sizes = ["S", "M", "L"];
const SegmentedButton = ({
options,
value,
onChange,
}: {
options: string[];
value: string;
onChange: (v: string) => void;
}) => (
<div style={{ display: "flex", gap: "2px", background: btnBg, borderRadius: "8px", padding: "2px" }}>
{options.map((opt) => (
<button
key={opt}
onClick={() => onChange(opt)}
style={{
flex: 1,
padding: "6px 10px",
border: "none",
borderRadius: "6px",
cursor: "pointer",
fontSize: "0.75rem",
fontWeight: value === opt ? 600 : 400,
background: value === opt ? btnActiveBg : "transparent",
color: value === opt ? btnActiveText : text,
transition: "all 0.15s ease",
}}
>
{opt}
</button>
))}
</div>
);
const Toggle = ({ checked, onChange }: { checked: boolean; onChange: (v: boolean) => void }) => (
<button
onClick={() => onChange(!checked)}
style={{
width: "36px",
height: "20px",
borderRadius: "10px",
border: "none",
background: checked ? accent : (darkMode ? "#4b5563" : "#d1d5db"),
cursor: "pointer",
position: "relative",
transition: "background 0.2s ease",
flexShrink: 0,
}}
>
<div
style={{
width: "16px",
height: "16px",
borderRadius: "50%",
background: "#fff",
position: "absolute",
top: "2px",
left: checked ? "18px" : "2px",
transition: "left 0.2s ease",
boxShadow: "0 1px 3px rgba(0,0,0,0.2)",
}}
/>
</button>
);
return (
<>
{/* Backdrop */}
{isOpen && (
<div
onClick={onClose}
style={{
position: "fixed",
inset: 0,
background: "rgba(0,0,0,0.1)",
zIndex: 998,
}}
/>
)}
{/* Panel */}
<div
style={{
position: "fixed",
right: 0,
top: 0,
bottom: 0,
width: isOpen ? "220px" : "0",
overflow: "hidden",
background: bg,
borderLeft: isOpen ? `1px solid ${border}` : "none",
transition: "width 0.2s ease",
zIndex: 999,
display: "flex",
flexDirection: "column",
}}
>
<div style={{ padding: "16px", display: "flex", flexDirection: "column", gap: "20px", overflowY: "auto", flex: 1 }}>
{/* Header */}
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<span style={{ fontWeight: 600, fontSize: "0.85rem", color: text }}>Quick Settings</span>
<button
onClick={onClose}
style={{ background: "none", border: "none", cursor: "pointer", color: labelColor, padding: "2px" }}
>
<X size={16} />
</button>
</div>
{/* Font Size */}
<div>
<div style={{ display: "flex", alignItems: "center", gap: "6px", marginBottom: "6px" }}>
<Type size={14} style={{ color: labelColor }} />
<span style={{ fontSize: "0.75rem", color: labelColor }}>Font Size</span>
</div>
<SegmentedButton options={sizes} value={fontSize} onChange={onFontSizeChange} />
</div>
{/* Spacing */}
<div>
<div style={{ display: "flex", alignItems: "center", gap: "6px", marginBottom: "6px" }}>
<Space size={14} style={{ color: labelColor }} />
<span style={{ fontSize: "0.75rem", color: labelColor }}>Spacing</span>
</div>
<SegmentedButton options={sizes} value={spacing} onChange={onSpacingChange} />
</div>
{/* Show Completed */}
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<div style={{ display: "flex", alignItems: "center", gap: "6px" }}>
<CheckSquare size={14} style={{ color: labelColor }} />
<span style={{ fontSize: "0.75rem", color: labelColor }}>Show Completed</span>
</div>
<Toggle checked={showCompleted} onChange={onShowCompletedChange} />
</div>
{/* Start Day */}
<div>
<div style={{ display: "flex", alignItems: "center", gap: "6px", marginBottom: "6px" }}>
<Calendar size={14} style={{ color: labelColor }} />
<span style={{ fontSize: "0.75rem", color: labelColor }}>Start View</span>
</div>
<SegmentedButton
options={["Yesterday", "Today"]}
value={startDayOffset === -1 ? "Yesterday" : "Today"}
onChange={(v) => onStartDayOffsetChange(v === "Yesterday" ? -1 : 0)}
/>
</div>
{/* Show Lines */}
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<div style={{ display: "flex", alignItems: "center", gap: "6px" }}>
<Minus size={14} style={{ color: labelColor }} />
<span style={{ fontSize: "0.75rem", color: labelColor }}>Show Lines</span>
</div>
<Toggle checked={showLines} onChange={onShowLinesChange} />
</div>
</div>
</div>
</>
);
}