AI removed features....

This commit is contained in:
2026-05-29 09:55:59 +02:00
parent 18a767f697
commit 929b75dfe3
6 changed files with 257 additions and 108 deletions
+57 -30
View File
@@ -1,11 +1,11 @@
import React, { useState } from 'react';
import { useSessions } from '../../hooks/useSleepSession';
import { Edit2, Check, X } from 'lucide-react';
import { Edit2, Check, X, Trash2 } from 'lucide-react';
import type { SleepSessionRead } from '../../types';
import { formatDateTime, toLocalDatetimeInputValue, getDurationMinutes } from '../../utils/time';
import { formatDateTime, toLocalDatetimeInputValue, getDurationMinutes, formatMinutes } from '../../utils/time';
export function SessionList() {
const { sessions, isLoading, updateSession } = useSessions();
const { sessions, isLoading, updateSession, deleteSession, isDeleting } = useSessions();
const [editingId, setEditingId] = useState<number | null>(null);
const [editStart, setEditStart] = useState('');
const [editEnd, setEditEnd] = useState('');
@@ -29,6 +29,18 @@ export function SessionList() {
}
};
const handleDelete = async (e: React.MouseEvent, id: number) => {
e.stopPropagation();
if (window.confirm('Are you sure you want to delete this sleep session?')) {
try {
await deleteSession(id);
} catch (err) {
console.error(err);
alert('Failed to delete session');
}
}
};
const startEditing = (s: SleepSessionRead) => {
setEditingId(s.id);
setEditStart(toLocalDatetimeInputValue(new Date(s.start_time)));
@@ -44,69 +56,84 @@ export function SessionList() {
<h3 className="text-lg font-semibold mb-4 text-gray-800">Sleep Sessions</h3>
<div className="space-y-2">
{sessions.map((s) => (
<div key={s.id} className="border-b border-gray-100 last:border-0 pb-2">
<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-2 py-1">
<div className="grid grid-cols-1 gap-2 text-xs text-gray-500">
<div className="flex flex-col space-y-3 py-1">
<div className="grid grid-cols-1 gap-3 text-xs text-gray-500">
<div className="flex flex-col gap-1">
<span>Start Time</span>
<span className="font-medium text-gray-700">Start Time</span>
<input
type="datetime-local"
className="border rounded px-1 py-0.5 text-sm text-black w-full"
className="border rounded px-2 py-1.5 text-sm text-black w-full focus:ring-2 focus:ring-blue-500 outline-none"
value={editStart}
onChange={(e) => setEditStart(e.target.value)}
/>
</div>
<div className="flex flex-col gap-1">
<span>End Time</span>
<span className="font-medium text-gray-700">End Time</span>
<input
type="datetime-local"
className="border rounded px-1 py-0.5 text-sm text-black w-full"
className="border rounded px-2 py-1.5 text-sm text-black w-full focus:ring-2 focus:ring-blue-500 outline-none"
value={editEnd}
onChange={(e) => setEditEnd(e.target.value)}
/>
</div>
</div>
<div className="flex justify-end space-x-2">
<div className="flex justify-end space-x-4">
<button
onClick={handleCancel}
className="p-1 text-gray-500 hover:text-gray-700 transition-colors"
className="flex items-center gap-1 px-3 py-1.5 text-sm text-gray-500 hover:text-gray-700 bg-gray-100 rounded-lg transition-colors"
title="Cancel"
>
<X size={18} />
<X size={16} />
<span>Cancel</span>
</button>
<button
onClick={() => handleSave(s.id)}
className="p-1 text-green-600 hover:text-green-700 transition-colors"
className="flex items-center gap-1 px-3 py-1.5 text-sm text-white bg-green-600 hover:text-green-700 rounded-lg transition-colors"
title="Save"
>
<Check size={18} />
<Check size={16} />
<span>Save</span>
</button>
</div>
</div>
) : (
<div
className="flex justify-between items-center text-sm cursor-pointer hover:bg-gray-50 p-1 rounded transition-colors"
className="flex flex-col sm:flex-row sm:justify-between sm:items-center cursor-pointer hover:bg-gray-50 transition-colors rounded-lg px-2"
onClick={() => startEditing(s)}
>
<div className="flex flex-col">
<span className="text-gray-900 font-medium">
{formatDateTime(s.start_time)}
</span>
<div className="flex flex-col sm:flex-row sm:items-baseline sm:gap-3">
<div className="flex flex-col sm:flex-row sm:items-baseline sm:gap-2">
<span className="text-gray-900 font-bold text-base sm:text-sm">
{formatDateTime(s.start_time)}
</span>
<span className="text-blue-600 font-semibold text-xs sm:text-sm">
{s.end_time && s.start_time ? formatMinutes(getDurationMinutes(s.start_time, s.end_time)) : '—'}
</span>
</div>
<span className="text-gray-500 text-xs">
{s.end_time
? `${formatDateTime(s.start_time)} → ${formatDateTime(s.end_time)}`
: 'In progress'}
{s.end_time ? `${formatDateTime(s.start_time)} → ${formatDateTime(s.end_time)}` : 'In progress'}
</span>
</div>
<div className="flex items-center space-x-2">
<span className="text-blue-600 font-mono text-sm">
{s.end_time && s.start_time
? `${getDurationMinutes(s.start_time, s.end_time)} min`
: '—'}
</span>
<Edit2 size={14} className="text-gray-400" />
<div className="flex items-center justify-end space-x-2 sm:space-x-4 pt-3 sm:pt-0">
<div className="flex items-center gap-2 sm:hidden text-blue-600 font-semibold text-sm">
<span>{s.end_time && s.start_time ? formatMinutes(getDurationMinutes(s.start_time, s.end_time)) : '—'}</span>
</div>
<div className="flex items-center gap-1 sm:gap-2">
<button
onClick={(e) => handleDelete(e, s.id)}
disabled={isDeleting}
className="p-2 text-red-400 hover:text-red-600 disabled:opacity-50 transition-colors"
title="Delete session"
>
<Trash2 size={18} />
</button>
<div className="p-2 text-gray-400">
<Edit2 size={18} />
</div>
</div>
</div>
</div>
)}
+129 -41
View File
@@ -1,4 +1,4 @@
import React, { useMemo } from 'react';
import { useMemo, useState, useEffect, Fragment } from 'react';
import { useStats } from '../../hooks/useStats';
import { timeToMinutes } from '../../utils/time';
@@ -9,8 +9,26 @@ interface TimetableProps {
day_end: string;
}
const Timetable: React.FC<TimetableProps> = ({ from_date, to_date, day_start, day_end }) => {
function getDayName(dateStr: string) {
return new Date(dateStr + 'T00:00:00').toLocaleDateString('en-US', { weekday: 'short' });
}
function formatTimeLabel(mins: number) {
const hours = Math.floor(mins / 60);
const minutes = mins % 60;
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`;
}
const Timetable = ({ from_date, to_date, day_start, day_end }: TimetableProps) => {
const { timetable, isLoading } = useStats(from_date, to_date, day_start, day_end);
const [selectedDayIndex, setSelectedDayIndex] = useState(0);
const [windowWidth, setWindowWidth] = useState(typeof window !== 'undefined' ? window.innerWidth : 1024);
useEffect(() => {
const handleResize = () => setWindowWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
const days = useMemo(() => {
const start = new Date(from_date + 'T00:00:00');
@@ -21,29 +39,57 @@ const Timetable: React.FC<TimetableProps> = ({ from_date, to_date, day_start, da
result.push(current.toLocaleDateString('en-CA'));
current.setDate(current.getDate() + 1);
}
return result;
return result; // Chronological (oldest to newest)
}, [from_date, to_date]);
const reversedDays = useMemo(() => [...days].reverse(), [days]);
const timeSteps = useMemo(() => {
return Array.from({ length: 24 }, (_, i) => i * 60);
return Array.from({ length: 96 }, (_, i) => i * 15);
}, []);
if (isLoading) return <div className="text-center p-4">Loading timetable...</div>;
if (!timetable?.slots || timetable.slots.length === 0) {
return <div className="text-center p-4 text-gray-500">No data available for this period.</div>;
}
const tier = useMemo(() => {
if (windowWidth >= 768) return 'desktop';
if (windowWidth >= 400) return 'medium';
return 'small';
}, [windowWidth]);
const getDayName = (dateStr: string) => {
return new Date(dateStr).toLocaleDateString('en-US', { weekday: 'short' });
};
const { visibleDays, tabGroups } = useMemo(() => {
if (tier === 'desktop') {
return { visibleDays: days, tabGroups: [] };
}
const formatTimeLabel = (mins: number) => {
const hours = Math.floor(mins / 60);
const minutes = mins % 60;
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`;
};
const activeDays = reversedDays;
const selectedDay = days[selectedDayIndex];
const indexInActiveDays = activeDays.indexOf(selectedDay);
if (tier === 'medium') {
const groups = [];
for (let i = 0; i < activeDays.length; i += 3) {
groups.push({
label: `${getDayName(activeDays[i])} - ${getDayName(activeDays[Math.min(i + 2, activeDays.length - 1)])}`,
startIndex: i,
});
}
return {
visibleDays: activeDays.slice(indexInActiveDays, indexInActiveDays + 3),
tabGroups: groups,
};
}
// Small tier (single day)
const groups = activeDays.map((day, idx) => ({
label: getDayName(day),
startIndex: idx,
}));
return {
visibleDays: activeDays.slice(indexInActiveDays, indexInActiveDays + 1),
tabGroups: groups,
};
}, [tier, days, reversedDays, selectedDayIndex]);
const isSleepingInInterval = (date: string, intervalStart: number, intervalEnd: number) => {
if (!timetable?.slots) return false;
return timetable.slots.some((slot) => {
if (slot.date !== date || !slot.sleeping) return false;
@@ -51,7 +97,6 @@ const Timetable: React.FC<TimetableProps> = ({ from_date, to_date, day_start, da
const sEnd = timeToMinutes(slot.slot_end);
if (sEnd <= sStart) {
// Handle wrap-around within a single slot if it happens (though unlikely for 15m segments)
const overlap1 = Math.max(sStart, intervalStart) < Math.min(1440, intervalEnd);
const overlap2 = Math.max(0, intervalStart) < Math.min(sEnd, intervalEnd);
return overlap1 || overlap2;
@@ -61,71 +106,114 @@ const Timetable: React.FC<TimetableProps> = ({ from_date, to_date, day_start, da
});
};
const handleTabClick = (startIndexInReversed: number) => {
const day = reversedDays[startIndexInReversed];
const indexInDays = days.indexOf(day);
setSelectedDayIndex(indexInDays);
};
const currentMins = new Date().getHours() * 60 + new Date().getMinutes();
const rowHeightPx = 32; // h-8 is 2rem, which is 32px in standard Tailwind config
const rowHeightPx = 8;
if (isLoading) return <div className="text-center p-4">Loading timetable...</div>;
if (!timetable?.slots || timetable.slots.length === 0) {
return <div className="text-center p-4 text-gray-500">No data available for this period.</div>;
}
return (
<div className="bg-white p-4 rounded-xl shadow-sm border border-gray-200 overflow-x-auto">
<div className="bg-white p-4 rounded-xl shadow-sm border border-gray-200">
<h3 className="text-lg font-semibold mb-4 text-gray-800">Sleep Timetable</h3>
<div className="min-w-[600px] flex flex-col gap-px">
{/* Tab Selector (Mobile & Medium) */}
{(tier === 'medium' || tier === 'small') && (
<div className="flex overflow-x-auto gap-2 mb-4 pb-2 no-scrollbar">
{tabGroups.map((group, idx) => {
const isActive = selectedDayIndex === days.indexOf(reversedDays[group.startIndex]);
return (
<button
key={idx}
onClick={() => handleTabClick(group.startIndex)}
className={`px-4 py-1.5 rounded-full text-xs font-medium whitespace-nowrap transition-colors ${
isActive
? 'bg-blue-600 text-white'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
}`}
>
{group.label}
</button>
);
})}
</div >
)}
{/* Grid Container */}
<div className="flex flex-col gap-px">
{/* Header Row */}
<div
className="grid"
style={{
gridTemplateColumns: `4rem repeat(${days.length}, minmax(40px, 1fr))`
gridTemplateColumns: tier === 'desktop'
? `4rem repeat(${days.length}, minmax(40px, 1fr))`
: tier === 'medium'
? `4rem repeat(3, 1fr)`
: `4rem 1fr`
}}
>
<div className="h-8" /> {/* Empty corner cell */}
{days.map((day) => (
<div key={day} className="text-center text-[10px] font-medium text-gray-500 uppercase flex items-center justify-center h-8">
<div className="h-2" /> {/* Empty corner cell */}
{visibleDays.map((day) => (
<div key={day} className="text-center text-[10px] font-medium text-gray-500 uppercase flex items-center justify-center h-2">
{getDayName(day)}
</div>
</div >
))}
</div>
</div >
{/* Time Grid Container */}
<div className="relative">
<div
className="grid"
style={{
gridTemplateColumns: `4rem repeat(${days.length}, minmax(40px, 1fr))`
gridTemplateColumns: tier === 'desktop'
? `4rem repeat(${days.length}, minmax(40px, 1fr))`
: tier === 'medium'
? `4rem repeat(3, 1fr)`
: `4rem 1fr`
}}
>
{/* Time Rows */}
{timeSteps.map((step) => (
<React.Fragment key={step}>
<Fragment key={step}>
{/* Time Label */}
<div className="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8">
<div className="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-2">
{step % 60 === 0 ? formatTimeLabel(step) : ''}
</div>
</div >
{/* Grid Cells */}
{days.map((day) => {
const sleeping = isSleepingInInterval(day, step, step + 60);
{visibleDays.map((day) => {
const sleeping = isSleepingInInterval(day, step, step + 15);
return (
<div
key={`${day}-${step}`}
className={`h-8 w-full ${sleeping ? 'bg-green-500' : 'bg-gray-50'}`}
className={`h-2 w-full border border-gray-100 ${sleeping ? 'bg-green-500' : 'bg-gray-50'}`}
/>
);
})}
</React.Fragment>
</Fragment>
))}
{/* Red Marker Line */}
<div
className="absolute left-[4rem] right-0 h-0.5 bg-red-500 pointer-events-none z-10"
style={{
top: `${(currentMins / 1440) * (24 * rowHeightPx)}px`,
top: `${(currentMins / 1440) * (timeSteps.length * rowHeightPx)}px`,
transform: 'translateY(-50%)'
}}
/>
</div>
</div>
</div>
</div>
</div >
</div >
</div >
</div >
);
};
export default Timetable;
export default Timetable;
+1
View File
@@ -0,0 +1 @@
"use strict";