feat: render sessions grouped by day with styled header bars
Made-with: Cursor
This commit is contained in:
@@ -1,9 +1,9 @@
|
|||||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useAuth } from '../context/AuthContext';
|
import { useAuth } from '../context/AuthContext';
|
||||||
import { useToast } from '../components/Toast';
|
import { useToast } from '../components/Toast';
|
||||||
import api from '../api/axios';
|
import api from '../api/axios';
|
||||||
import { formatLocalDate, isSunday } from '../utils/dateUtils';
|
import { formatDayHeader, formatTimeOnly, getLocalDateKey, isSunday } from '../utils/dateUtils';
|
||||||
import { prefixKey } from '../utils/adminPrefs';
|
import { prefixKey } from '../utils/adminPrefs';
|
||||||
|
|
||||||
function History() {
|
function History() {
|
||||||
@@ -150,6 +150,23 @@ function History() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const groupedSessions = useMemo(() => {
|
||||||
|
const groups = [];
|
||||||
|
let currentKey = null;
|
||||||
|
|
||||||
|
sessions.forEach(session => {
|
||||||
|
const dateKey = getLocalDateKey(session.created_at);
|
||||||
|
if (dateKey !== currentKey) {
|
||||||
|
currentKey = dateKey;
|
||||||
|
groups.push({ dateKey, sessions: [session] });
|
||||||
|
} else {
|
||||||
|
groups[groups.length - 1].sessions.push(session);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
}, [sessions]);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center items-center h-64">
|
<div className="flex justify-center items-center h-64">
|
||||||
@@ -220,109 +237,134 @@ function History() {
|
|||||||
<p className="text-gray-500 dark:text-gray-400">No sessions found</p>
|
<p className="text-gray-500 dark:text-gray-400">No sessions found</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{sessions.map(session => {
|
{groupedSessions.map((group, groupIdx) => {
|
||||||
const isActive = session.is_active === 1;
|
const isSundayGroup = isSunday(group.sessions[0].created_at);
|
||||||
const isSelected = selectedIds.has(session.id);
|
const isContinued = groupIdx === 0 && page > 1 && prevLastDate &&
|
||||||
const isSundaySession = isSunday(session.created_at);
|
getLocalDateKey(prevLastDate) === group.dateKey;
|
||||||
const isArchived = session.archived === 1;
|
|
||||||
const canSelect = selectMode && !isActive;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div key={group.dateKey}>
|
||||||
key={session.id}
|
{/* Day header bar */}
|
||||||
className={`border rounded-lg transition ${
|
<div className="bg-gray-100 dark:bg-[#1e2a3a] rounded-md px-3.5 py-2 mb-2 flex justify-between items-center border-l-[3px] border-indigo-500">
|
||||||
selectMode && isActive
|
<div className="flex items-center gap-2">
|
||||||
? 'opacity-50 cursor-not-allowed border-gray-300 dark:border-gray-600'
|
<span className="text-sm font-semibold text-indigo-700 dark:text-indigo-300">
|
||||||
: isSelected
|
{formatDayHeader(group.sessions[0].created_at)}
|
||||||
? 'border-indigo-500 bg-indigo-50 dark:bg-indigo-900/20 cursor-pointer'
|
</span>
|
||||||
: 'border-gray-300 dark:border-gray-600 hover:border-indigo-400 dark:hover:border-indigo-500 cursor-pointer'
|
{isContinued && (
|
||||||
}`}
|
<span className="text-xs text-gray-400 dark:text-gray-500 italic">(continued)</span>
|
||||||
onClick={() => {
|
|
||||||
if (longPressFired.current) {
|
|
||||||
longPressFired.current = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (selectMode) {
|
|
||||||
if (!isActive) toggleSelection(session.id);
|
|
||||||
} else {
|
|
||||||
navigate(`/history/${session.id}`);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onPointerDown={() => {
|
|
||||||
if (!isActive) handlePointerDown(session.id);
|
|
||||||
}}
|
|
||||||
onPointerUp={handlePointerUp}
|
|
||||||
onPointerLeave={handlePointerUp}
|
|
||||||
>
|
|
||||||
<div className="p-4">
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
{selectMode && (
|
|
||||||
<div className={`mt-0.5 w-5 h-5 flex-shrink-0 rounded border-2 flex items-center justify-center ${
|
|
||||||
isActive
|
|
||||||
? 'border-gray-300 dark:border-gray-600 bg-gray-100 dark:bg-gray-700'
|
|
||||||
: isSelected
|
|
||||||
? 'border-indigo-600 bg-indigo-600'
|
|
||||||
: 'border-gray-300 dark:border-gray-600'
|
|
||||||
}`}>
|
|
||||||
{isSelected && (
|
|
||||||
<span className="text-white text-xs font-bold">✓</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
<div className="flex-1 min-w-0">
|
</div>
|
||||||
<div className="flex justify-between items-center mb-1">
|
{!isContinued && (
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex items-center gap-2">
|
||||||
<span className="font-semibold text-gray-800 dark:text-gray-100">
|
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
Session #{session.id}
|
{group.sessions.length} session{group.sessions.length !== 1 ? 's' : ''}
|
||||||
</span>
|
</span>
|
||||||
{isActive && (
|
{isSundayGroup && (
|
||||||
<span className="bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 text-xs px-2 py-0.5 rounded">
|
<span className="text-xs font-semibold text-amber-700 dark:text-amber-300">🎲 Game Night</span>
|
||||||
Active
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{isSundaySession && (
|
|
||||||
<span className="bg-amber-100 dark:bg-amber-900 text-amber-800 dark:text-amber-200 text-xs px-2 py-0.5 rounded font-semibold">
|
|
||||||
🎲 Game Night
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{isArchived && (filter === 'all' || filter === 'archived') && (
|
|
||||||
<span className="bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 text-xs px-2 py-0.5 rounded">
|
|
||||||
Archived
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
{session.games_played} game{session.games_played !== 1 ? 's' : ''}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
{formatLocalDate(session.created_at)}
|
|
||||||
{isSundaySession && (
|
|
||||||
<span className="text-gray-400 dark:text-gray-500"> · Sunday</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{session.has_notes && session.notes_preview && (
|
|
||||||
<div className="mt-2 text-sm text-indigo-400 dark:text-indigo-300 bg-indigo-50 dark:bg-indigo-900/20 px-3 py-2 rounded border-l-2 border-indigo-500">
|
|
||||||
{session.notes_preview}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!selectMode && isAuthenticated && isActive && (
|
{/* Session cards under this day */}
|
||||||
<div className="px-4 pb-4 pt-0">
|
<div className="ml-3 space-y-1.5 mb-4">
|
||||||
<button
|
{group.sessions.map(session => {
|
||||||
onClick={(e) => {
|
const isActive = session.is_active === 1;
|
||||||
e.stopPropagation();
|
const isSelected = selectedIds.has(session.id);
|
||||||
setClosingSession(session.id);
|
const isArchived = session.archived === 1;
|
||||||
}}
|
|
||||||
className="w-full bg-orange-600 dark:bg-orange-700 text-white px-4 py-2 rounded text-sm hover:bg-orange-700 dark:hover:bg-orange-800 transition"
|
return (
|
||||||
>
|
<div
|
||||||
End Session
|
key={session.id}
|
||||||
</button>
|
className={`border rounded-lg transition ${
|
||||||
</div>
|
selectMode && isActive
|
||||||
)}
|
? 'opacity-50 cursor-not-allowed border-gray-300 dark:border-gray-600'
|
||||||
|
: isSelected
|
||||||
|
? 'border-indigo-500 bg-indigo-50 dark:bg-indigo-900/20 cursor-pointer'
|
||||||
|
: 'border-gray-300 dark:border-gray-600 hover:border-indigo-400 dark:hover:border-indigo-500 cursor-pointer'
|
||||||
|
}`}
|
||||||
|
onClick={() => {
|
||||||
|
if (longPressFired.current) {
|
||||||
|
longPressFired.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (selectMode) {
|
||||||
|
if (!isActive) toggleSelection(session.id);
|
||||||
|
} else {
|
||||||
|
navigate(`/history/${session.id}`);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onPointerDown={() => {
|
||||||
|
if (!isActive) handlePointerDown(session.id);
|
||||||
|
}}
|
||||||
|
onPointerUp={handlePointerUp}
|
||||||
|
onPointerLeave={handlePointerUp}
|
||||||
|
>
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
{selectMode && (
|
||||||
|
<div className={`mt-0.5 w-5 h-5 flex-shrink-0 rounded border-2 flex items-center justify-center ${
|
||||||
|
isActive
|
||||||
|
? 'border-gray-300 dark:border-gray-600 bg-gray-100 dark:bg-gray-700'
|
||||||
|
: isSelected
|
||||||
|
? 'border-indigo-600 bg-indigo-600'
|
||||||
|
: 'border-gray-300 dark:border-gray-600'
|
||||||
|
}`}>
|
||||||
|
{isSelected && (
|
||||||
|
<span className="text-white text-xs font-bold">✓</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex justify-between items-center mb-1">
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
<span className="font-semibold text-gray-800 dark:text-gray-100">
|
||||||
|
Session #{session.id}
|
||||||
|
</span>
|
||||||
|
{isActive && (
|
||||||
|
<span className="bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 text-xs px-2 py-0.5 rounded">
|
||||||
|
Active
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{isArchived && (filter === 'all' || filter === 'archived') && (
|
||||||
|
<span className="bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 text-xs px-2 py-0.5 rounded">
|
||||||
|
Archived
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{session.games_played} game{session.games_played !== 1 ? 's' : ''}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{formatTimeOnly(session.created_at)}
|
||||||
|
</div>
|
||||||
|
{session.has_notes && session.notes_preview && (
|
||||||
|
<div className="mt-2 text-sm text-indigo-400 dark:text-indigo-300 bg-indigo-50 dark:bg-indigo-900/20 px-3 py-2 rounded border-l-2 border-indigo-500">
|
||||||
|
{session.notes_preview}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!selectMode && isAuthenticated && isActive && (
|
||||||
|
<div className="px-4 pb-4 pt-0">
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setClosingSession(session.id);
|
||||||
|
}}
|
||||||
|
className="w-full bg-orange-600 dark:bg-orange-700 text-white px-4 py-2 rounded text-sm hover:bg-orange-700 dark:hover:bg-orange-800 transition"
|
||||||
|
>
|
||||||
|
End Session
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
Reference in New Issue
Block a user