Pagination (but according to agent, not done)

This commit is contained in:
2026-05-31 17:40:03 +02:00
parent 5534a25ce5
commit cdc57a441b
18 changed files with 7697 additions and 117 deletions
@@ -0,0 +1,104 @@
import React from 'react';
import { ChevronLeft, ChevronRight, MoreHorizontal } from 'lucide-react';
interface PaginationProps {
currentPage: number;
totalPages: number;
onPageChange: (page: number) => void;
}
export function Pagination({ currentPage, totalPages, onPageChange }: PaginationProps) {
if (totalPages <= 1) return null;
const getPageNumbers = () => {
const pages: (number | string)[] = [];
const maxVisiblePages = 5;
if (totalPages <= maxVisiblePages) {
for (let i = 1; i <= totalPages; i++) {
pages.push(i);
}
} else {
// Always include first page
pages.push(1);
if (currentPage > 3) {
pages.push('...');
}
// Calculate middle pages
const start = Math.max(2, currentPage - 1);
const end = Math.min(totalPages - 1, currentPage + 1);
for (let i = start; i <= end; i++) {
if (!pages.includes(i)) {
pages.push(i);
}
}
if (currentPage < totalPages - 2) {
pages.push('...');
}
// Always include last page
if (!pages.includes(totalPages)) {
pages.push(totalPages);
}
}
return pages;
};
const pages = getPageNumbers();
return (
<div data-testid="pagination" className="flex items-center justify-center gap-1 mt-6 py-4">
<button
onClick={() => onPageChange(Math.max(1, currentPage - 1))}
disabled={currentPage === 1}
className="p-2 rounded-lg hover:bg-gray-100 disabled:opacity-30 disabled:cursor-not-allowed transition-colors text-gray-600"
aria-label="Previous page"
>
<ChevronLeft size={20} />
</button>
<div className="flex items-center gap-1">
{pages.map((page, idx) => {
if (page === '...') {
return (
<div key={`ellipsis-${idx}`} className="px-2 text-gray-400">
<MoreHorizontal size={16} />
</div>
);
}
const pageNum = page as number;
const isActive = currentPage === pageNum;
return (
<button
key={pageNum}
onClick={() => onPageChange(pageNum)}
className={`min-w-[36px] h-9 px-2 rounded-lg text-sm font-medium transition-all ${
isActive
? 'bg-blue-600 text-white shadow-sm'
: 'text-gray-600 hover:bg-gray-100'
}`}
>
{pageNum}
</button>
);
})}
</div>
<button
onClick={() => onPageChange(Math.min(totalPages, currentPage + 1))}
disabled={currentPage === totalPages}
className="p-2 rounded-lg hover:bg-gray-100 disabled:opacity-30 disabled:cursor-not-allowed transition-colors text-gray-600"
aria-label="Next page"
>
<ChevronRight size={20} />
</button>
</div>
);
}
+23 -2
View File
@@ -1,15 +1,29 @@
import React, { useState } from 'react';
import { useSessions } from '../../hooks/useSleepSession';
import { useSettings } from '../../hooks/useSettings';
import { Pagination } from './Pagination';
import { Edit2, Check, X, Trash2 } from 'lucide-react';
import type { SleepSessionRead } from '../../types';
import { formatDateTime, toLocalDatetimeInputValue, getDurationMinutes, formatMinutes } from '../../utils/time';
export function SessionList() {
const { sessions, isLoading, updateSession, deleteSession, isDeleting } = useSessions();
const { settings } = useSettings();
const sessionsPerPage = settings.sessionsPerPage;
const [currentPage, setCurrentPage] = useState(1);
const [editingId, setEditingId] = useState<number | null>(null);
const [editStart, setEditStart] = useState<string>('');
const [editEnd, setEditEnd] = useState<string>('');
const totalPages = Math.ceil(sessions.length / sessionsPerPage);
const clampedPage = Math.max(1, Math.min(currentPage, totalPages || 1));
const paginatedSessions = sessions.slice(
(clampedPage - 1) * sessionsPerPage,
clampedPage * sessionsPerPage
);
if (isLoading) return <div className="text-center p-4">Loading sessions...</div>;
if (!sessions.length) return <div className="text-center p-4 text-gray-500">No sessions recorded.</div>;
@@ -52,10 +66,10 @@ export function SessionList() {
};
return (
<div className="bg-white p-4 rounded-xl shadow-sm border border-gray-200">
<div className="session-list bg-white p-4 rounded-xl shadow-sm border border-gray-200">
<h3 className="text-lg font-semibold mb-4 text-gray-800">Sleep Sessions</h3>
<div className="space-y-2">
{sessions.map((s) => (
{paginatedSessions.map((s) => (
<div key={s.id} className="border-b border-gray-100 last:border-0 py-3 sm:py-2">
{editingId === s.id ? (
<div className="flex flex-col space-y-3 py-1">
@@ -140,6 +154,13 @@ export function SessionList() {
</div>
))}
</div>
{sessions.length > sessionsPerPage && (
<Pagination
currentPage={clampedPage}
totalPages={totalPages}
onPageChange={setCurrentPage}
/>
)}
</div>
);
}
@@ -6,15 +6,17 @@ interface SettingsPopoverProps {
dayEnd: string;
wakeupRef: string;
bedtimeRef: string;
onSave: (settings: { dayStart: string; dayEnd: string; wakeupRef: string; bedtimeRef: string }) => void;
sessionsPerPage: number;
onSave: (settings: { dayStart: string; dayEnd: string; wakeupRef: string; bedtimeRef: string; sessionsPerPage: number }) => void;
}
export function SettingsPopover({ dayStart, dayEnd, wakeupRef, bedtimeRef, onSave }: SettingsPopoverProps) {
export function SettingsPopover({ dayStart, dayEnd, wakeupRef, bedtimeRef, sessionsPerPage, onSave }: SettingsPopoverProps) {
const [isOpen, setIsOpen] = useState(false);
const [localDayStart, setLocalDayStart] = useState(dayStart);
const [localDayEnd, setLocalDayEnd] = useState(dayEnd);
const [localWakeupRef, setLocalWakeupRef] = useState(wakeupRef);
const [localBedtimeRef, setLocalBedtimeRef] = useState(bedtimeRef);
const [localSessionsPerPage, setLocalSessionsPerPage] = useState(sessionsPerPage);
const popoverRef = useRef<HTMLDivElement>(null);
useEffect(() => {
@@ -22,7 +24,8 @@ export function SettingsPopover({ dayStart, dayEnd, wakeupRef, bedtimeRef, onSav
setLocalDayEnd(dayEnd);
setLocalWakeupRef(wakeupRef);
setLocalBedtimeRef(bedtimeRef);
}, [dayStart, dayEnd, wakeupRef, bedtimeRef]);
setLocalSessionsPerPage(sessionsPerPage);
}, [dayStart, dayEnd, wakeupRef, bedtimeRef, sessionsPerPage]);
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
@@ -42,6 +45,7 @@ export function SettingsPopover({ dayStart, dayEnd, wakeupRef, bedtimeRef, onSav
dayEnd: localDayEnd,
wakeupRef: localWakeupRef,
bedtimeRef: localBedtimeRef,
sessionsPerPage: localSessionsPerPage,
});
setIsOpen(false);
};
@@ -65,44 +69,58 @@ export function SettingsPopover({ dayStart, dayEnd, wakeupRef, bedtimeRef, onSav
<div className="space-y-3">
<div>
<label htmlFor="day-start" className="block text-xs text-gray-500 mb-1">Day starts (HH:MM)</label>
<input
id="day-start"
type="time"
value={localDayStart}
onChange={(e) => setLocalDayStart(e.target.value)}
className="w-full p-2 border rounded-md text-sm"
/>
<label htmlFor="day-start" className="block text-xs text-gray-500 mb-1">Day starts (HH:MM)</label>
<input
id="day-start"
type="time"
value={localDayStart}
onChange={(e) => setLocalDayStart(e.target.value)}
className="w-full p-2 border rounded-md text-sm"
/>
</div>
<div>
<label htmlFor="day-end" className="block text-xs text-gray-500 mb-1">Day ends (HH:MM)</label>
<input
id="day-end"
type="time"
value={localDayEnd}
onChange={(e) => setLocalDayEnd(e.target.value)}
className="w-full p-2 border rounded-md text-sm"
/>
<label htmlFor="day-end" className="block text-xs text-gray-500 mb-1">Day ends (HH:MM)</label>
<input
id="day-end"
type="time"
value={localDayEnd}
onChange={(e) => setLocalDayEnd(e.target.value)}
className="w-full p-2 border rounded-md text-sm"
/>
</div>
<div>
<label htmlFor="wakeup-ref" className="block text-xs text-gray-500 mb-1">Wakeup ref (HH:MM)</label>
<input
id="wakeup-ref"
type="time"
value={localWakeupRef}
onChange={(e) => setLocalWakeupRef(e.target.value)}
className="w-full p-2 border rounded-md text-sm"
/>
<label htmlFor="wakeup-ref" className="block text-xs text-gray-500 mb-1">Wakeup ref (HH:MM)</label>
<input
id="wakeup-ref"
type="time"
value={localWakeupRef}
onChange={(e) => setLocalWakeupRef(e.target.value)}
className="w-full p-2 border rounded-md text-sm"
/>
</div>
<div>
<label htmlFor="bedtime-ref" className="block text-xs text-gray-500 mb-1">Bedtime ref (HH:MM)</label>
<input
id="bedtime-ref"
type="time"
value={localBedtimeRef}
onChange={(e) => setLocalBedtimeRef(e.target.value)}
className="w-full p-2 border rounded-md text-sm"
/>
<label htmlFor="bedtime-ref" className="block text-xs text-gray-500 mb-1">Bedtime ref (HH:MM)</label>
<input
id="bedtime-ref"
type="time"
value={localBedtimeRef}
onChange={(e) => setLocalBedtimeRef(e.target.value)}
className="w-full p-2 border rounded-md text-sm"
/>
</div>
<div>
<label htmlFor="sessions-per-page" className="block text-xs text-gray-500 mb-1">Results per page</label>
<select
id="sessions-per-page"
value={localSessionsPerPage}
onChange={(e) => setLocalSessionsPerPage(Number(e.target.value))}
className="w-full p-2 border rounded-md text-sm"
>
<option value={5}>5</option>
<option value={10}>10</option>
<option value={25}>25</option>
<option value={50}>50</option>
</select>
</div>
</div>
+14 -12
View File
@@ -27,18 +27,20 @@ const StatsPage: React.FC = () => {
<h2 className="text-3xl font-extrabold text-slate-900 tracking-tight">Statistics</h2>
<p className="text-slate-500 text-sm mt-1">Overview of sleep patterns and trends</p>
</div>
<SettingsPopover
dayStart={settings.dayStart}
dayEnd={settings.dayEnd}
wakeupRef={settings.wakeupRef}
bedtimeRef={settings.bedtimeRef}
onSave={(s) => {
update('dayStart', s.dayStart);
update('dayEnd', s.dayEnd);
update('wakeupRef', s.wakeupRef);
update('bedtimeRef', s.bedtimeRef);
}}
/>
<SettingsPopover
dayStart={settings.dayStart}
dayEnd={settings.dayEnd}
wakeupRef={settings.wakeupRef}
bedtimeRef={settings.bedtimeRef}
sessionsPerPage={settings.sessionsPerPage}
onSave={(s) => {
update('dayStart', s.dayStart);
update('dayEnd', s.dayEnd);
update('wakeupRef', s.wakeupRef);
update('bedtimeRef', s.bedtimeRef);
update('sessionsPerPage', s.sessionsPerPage);
}}
/>
</div>
<div className="bg-white p-6 rounded-[2rem] shadow-md border border-gray-100">
+57
View File
@@ -0,0 +1,57 @@
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
const STORAGE_KEY = 'baby-sleep-settings';
export interface Settings {
dayStart: string;
dayEnd: string;
wakeupRef: string;
bedtimeRef: string;
statsInterval: number;
sessionsPerPage: number;
}
interface SettingsContextType {
settings: Settings;
update: (key: keyof Settings, value: string | number) => void;
}
export const SettingsContext = createContext<SettingsContextType | undefined>(undefined);
const DEFAULT_SETTINGS: Settings = {
dayStart: '07:00',
dayEnd: '20:00',
wakeupRef: '04:00',
bedtimeRef: '19:00',
statsInterval: 7,
sessionsPerPage: 10,
};
export function SettingsProvider({ children }: { children: ReactNode }) {
const [settings, setSettings] = useState<Settings>(() => {
if (typeof window === 'undefined') return DEFAULT_SETTINGS;
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
try {
return { ...DEFAULT_SETTINGS, ...JSON.parse(stored) };
} catch {
return DEFAULT_SETTINGS;
}
}
return DEFAULT_SETTINGS;
});
useEffect(() => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
}, [settings]);
const update = (key: keyof Settings, value: string | number) => {
setSettings((prev) => ({ ...prev, [key]: value }));
};
return (
<SettingsContext.Provider value={{ settings, update }}>
{children}
</SettingsContext.Provider>
);
}
+8 -42
View File
@@ -1,44 +1,10 @@
import { useState, useEffect } from 'react';
const STORAGE_KEY = 'baby-sleep-settings';
interface Settings {
dayStart: string;
dayEnd: string;
wakeupRef: string;
bedtimeRef: string;
statsInterval: number;
}
const DEFAULT_SETTINGS: Settings = {
dayStart: '07:00',
dayEnd: '20:00',
wakeupRef: '04:00',
bedtimeRef: '19:00',
statsInterval: 7,
};
import { useContext } from 'react';
import { SettingsContext, Settings } from './SettingsProvider';
export function useSettings() {
const [settings, setSettings] = useState<Settings>(() => {
if (typeof window === 'undefined') return DEFAULT_SETTINGS;
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
try {
return { ...DEFAULT_SETTINGS, ...JSON.parse(stored) };
} catch {
return DEFAULT_SETTINGS;
}
}
return DEFAULT_SETTINGS;
});
useEffect(() => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
}, [settings]);
const update = (key: keyof Settings, value: string | number) => {
setSettings((prev) => ({ ...prev, [key]: value }));
};
return { settings, update };
}
const context = useContext(SettingsContext);
if (context === undefined) {
throw new Error('useSettings must be used within a SettingsProvider');
}
return context;
}
+5 -2
View File
@@ -2,14 +2,17 @@ import React from 'react'
import ReactDOM from 'react-dom/client'
import './index.css'
import App from './App'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { SettingsProvider } from './hooks/SettingsProvider';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient()
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<App />
<SettingsProvider>
<App />
</SettingsProvider>
</QueryClientProvider>
</React.StrictMode>,
)