Fixed live line and auto scrolling on courts page
This commit is contained in:
@@ -59,22 +59,24 @@ def get_court_schedule(court_id: int, db: Session = Depends(get_db)):
|
|||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
|
|
||||||
schedule = []
|
return {
|
||||||
for m in matches:
|
"court": court.name,
|
||||||
schedule.append(
|
"matches": [
|
||||||
{
|
{
|
||||||
"id": m.id,
|
"id": m.id,
|
||||||
"tournament_id": m.tournament_id,
|
"tournament_id": m.tournament.id,
|
||||||
"tournament_name": m.tournament.name,
|
"tournament_name": m.tournament.name,
|
||||||
"time": m.start_time.strftime("%H:%M") if m.start_time else "TBD",
|
"duration": m.tournament.duration,
|
||||||
"status": m.status,
|
"time": m.start_time.strftime("%H:%M") if m.start_time else None,
|
||||||
|
"status": m.status.value,
|
||||||
"match_number": m.match_number,
|
"match_number": m.match_number,
|
||||||
"p1": m.p1_team.name if m.p1_team else "TBD",
|
"p1": m.p1_team.name if m.p1_team else "TBD",
|
||||||
"p2": m.p2_team.name if m.p2_team else "TBD",
|
"p2": m.p2_team.name if m.p2_team else "TBD",
|
||||||
"p1_sets": sum(1 for s in m.sets if s["p1"] > s["p2"]) if m.sets else 0,
|
"p1_sets": len([s for s in m.sets if s.get("p1", 0) > s.get("p2", 0)]),
|
||||||
"p2_sets": sum(1 for s in m.sets if s["p2"] > s["p1"]) if m.sets else 0,
|
"p2_sets": len([s for s in m.sets if s.get("p2", 0) > s.get("p1", 0)]),
|
||||||
"ref_name": m.ref_team.name if m.ref_team else m.ref_label,
|
"ref_name": m.ref_team.name if m.ref_team else m.ref_label,
|
||||||
}
|
}
|
||||||
)
|
for m in matches
|
||||||
|
if m.start_time
|
||||||
return {"court": court.name, "matches": schedule}
|
],
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ interface CourtMatch {
|
|||||||
tournament_id: string;
|
tournament_id: string;
|
||||||
tournament_name: string;
|
tournament_name: string;
|
||||||
time: string;
|
time: string;
|
||||||
|
duration: number;
|
||||||
status: string;
|
status: string;
|
||||||
match_number: number;
|
match_number: number;
|
||||||
p1: string;
|
p1: string;
|
||||||
@@ -72,32 +73,65 @@ const CourtColumn = ({ courtId, onDelete, role }: { courtId: number, onDelete: (
|
|||||||
void fetchSchedule();
|
void fetchSchedule();
|
||||||
}, [courtId]);
|
}, [courtId]);
|
||||||
|
|
||||||
|
let isDayFinished = false;
|
||||||
|
let activeIndex = -1;
|
||||||
|
|
||||||
|
if (data && data.matches.length > 0 && currentTime) {
|
||||||
|
const [currH, currM] = currentTime.split(':').map(Number);
|
||||||
|
const currTotalMins = currH * 60 + currM;
|
||||||
|
|
||||||
|
for (let i = 0; i < data.matches.length; i++) {
|
||||||
|
const m = data.matches[i];
|
||||||
|
if (!m.time) continue;
|
||||||
|
|
||||||
|
const [mH, mM] = m.time.split(':').map(Number);
|
||||||
|
const mStartTotalMins = mH * 60 + mM;
|
||||||
|
const duration = m.duration || 30;
|
||||||
|
const mEndTotalMins = mStartTotalMins + duration;
|
||||||
|
|
||||||
|
if (currTotalMins >= mStartTotalMins && currTotalMins < mEndTotalMins) {
|
||||||
|
activeIndex = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const lastMatch = data.matches[data.matches.length - 1];
|
||||||
|
if (lastMatch.time) {
|
||||||
|
const [lastH, lastM] = lastMatch.time.split(':').map(Number);
|
||||||
|
const lastStartTotal = lastH * 60 + lastM;
|
||||||
|
const lastDuration = lastMatch.duration || 30;
|
||||||
|
const lastEndTotal = lastStartTotal + lastDuration;
|
||||||
|
|
||||||
|
if (currTotalMins >= lastEndTotal) {
|
||||||
|
isDayFinished = true;
|
||||||
|
activeIndex = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// AUTO-SCROLL LOGIC
|
// AUTO-SCROLL LOGIC
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data && currentTime && !hasScrolled && scrollContainerRef.current) {
|
if (data && currentTime && !hasScrolled && scrollContainerRef.current) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const line = scrollContainerRef.current?.querySelector('.now-line');
|
const container = scrollContainerRef.current;
|
||||||
if (line) {
|
if (!container) return;
|
||||||
line.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
||||||
setHasScrolled(true);
|
if (isDayFinished) {
|
||||||
|
container.scrollTo({ top: container.scrollHeight, behavior: 'smooth' });
|
||||||
|
} else {
|
||||||
|
const line = container.querySelector('.now-line') as HTMLElement;
|
||||||
|
if (line) {
|
||||||
|
container.scrollTo({
|
||||||
|
top: line.offsetTop - 20,
|
||||||
|
behavior: 'smooth'
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
setHasScrolled(true);
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
}, [data, currentTime, hasScrolled]);
|
}, [data, currentTime, hasScrolled, isDayFinished]);
|
||||||
|
|
||||||
if (!data) return <div className="w-80 shrink-0 bg-zinc-50 dark:bg-zinc-900/50 rounded-2xl flex items-center justify-center border border-zinc-200 dark:border-zinc-800"><Loader2 className="animate-spin text-orange-500" /></div>;
|
if (!data) return <div className="w-80 shrink-0 bg-zinc-50 dark:bg-zinc-900/50 rounded-2xl flex items-center justify-center border border-zinc-200 dark:border-zinc-800"><Loader2 className="animate-spin text-orange-500" /></div>;
|
||||||
|
|
||||||
// --- ACTIVE INDEX LOGIC ---
|
|
||||||
let activeIndex = -1;
|
|
||||||
if (currentTime) {
|
|
||||||
// Find the index of the most recently started match
|
|
||||||
for (let i = 0; i < data.matches.length; i++) {
|
|
||||||
if (data.matches[i].time <= currentTime) {
|
|
||||||
activeIndex = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-72 md:w-80 shrink-0 flex flex-col h-full bg-zinc-100/50 dark:bg-zinc-900/20 rounded-2xl border border-zinc-200 dark:border-zinc-800 overflow-hidden">
|
<div className="w-72 md:w-80 shrink-0 flex flex-col h-full bg-zinc-100/50 dark:bg-zinc-900/20 rounded-2xl border border-zinc-200 dark:border-zinc-800 overflow-hidden">
|
||||||
<div className="p-3 md:p-4 bg-zinc-100 dark:bg-zinc-900 border-b border-zinc-200 dark:border-zinc-800 flex justify-between items-center gap-3 shrink-0">
|
<div className="p-3 md:p-4 bg-zinc-100 dark:bg-zinc-900 border-b border-zinc-200 dark:border-zinc-800 flex justify-between items-center gap-3 shrink-0">
|
||||||
@@ -112,28 +146,22 @@ const CourtColumn = ({ courtId, onDelete, role }: { courtId: number, onDelete: (
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ref={scrollContainerRef} className="flex-1 overflow-y-auto p-3 space-y-2.5 pb-32">
|
<div ref={scrollContainerRef} className="flex-1 overflow-y-auto p-3 space-y-2.5 pb-32 relative">
|
||||||
{data.matches.length === 0 && (
|
{data.matches.length === 0 && (
|
||||||
<div className="text-center text-sm font-bold uppercase text-zinc-400 dark:text-zinc-600 mt-10">No matches</div>
|
<div className="text-center text-sm font-bold uppercase text-zinc-400 dark:text-zinc-600 mt-10">No matches</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{data.matches.map((m, index) => {
|
{data.matches.map((m, index) => {
|
||||||
const isFinished = m.status === 'Finished';
|
const isFinished = m.status === 'Finished';
|
||||||
const isPastSlot = index < activeIndex; // Strictly before the active match
|
const isPastSlot = isDayFinished || (activeIndex !== -1 && index < activeIndex);
|
||||||
const isLive = index === activeIndex && !isFinished; // Currently active and incomplete
|
const isLive = !isDayFinished && index === activeIndex;
|
||||||
|
const showNowLine = !isDayFinished && ((activeIndex !== -1 && index === activeIndex) || (activeIndex === -1 && index === 0));
|
||||||
|
|
||||||
// The line is drawn right above the active index (or index 0 if the day hasn't started)
|
|
||||||
const showNowLine = (activeIndex !== -1 && index === activeIndex) || (activeIndex === -1 && index === 0);
|
|
||||||
|
|
||||||
// Styling configurations based on match state
|
|
||||||
let borderStyle = 'border-zinc-200 dark:border-zinc-800 hover:-translate-y-1 hover:shadow-md';
|
let borderStyle = 'border-zinc-200 dark:border-zinc-800 hover:-translate-y-1 hover:shadow-md';
|
||||||
let textOpacity = 'text-zinc-900 dark:text-white';
|
let textOpacity = 'text-zinc-900 dark:text-white';
|
||||||
let pOpacity = 'text-zinc-800 dark:text-zinc-200';
|
let pOpacity = 'text-zinc-800 dark:text-zinc-200';
|
||||||
|
|
||||||
if (isFinished) {
|
if (isFinished || isPastSlot || isDayFinished) {
|
||||||
borderStyle = 'border-orange-500/30 opacity-60 grayscale hover:opacity-100 hover:grayscale-0';
|
|
||||||
textOpacity = 'text-zinc-500';
|
|
||||||
} else if (isPastSlot) {
|
|
||||||
borderStyle = 'border-zinc-300 dark:border-zinc-700 opacity-50 grayscale hover:opacity-100 hover:grayscale-0';
|
borderStyle = 'border-zinc-300 dark:border-zinc-700 opacity-50 grayscale hover:opacity-100 hover:grayscale-0';
|
||||||
textOpacity = 'text-zinc-500';
|
textOpacity = 'text-zinc-500';
|
||||||
pOpacity = 'text-zinc-500';
|
pOpacity = 'text-zinc-500';
|
||||||
@@ -147,7 +175,7 @@ const CourtColumn = ({ courtId, onDelete, role }: { courtId: number, onDelete: (
|
|||||||
<React.Fragment key={m.id}>
|
<React.Fragment key={m.id}>
|
||||||
{/* --- THE --NOW-- LINE --- */}
|
{/* --- THE --NOW-- LINE --- */}
|
||||||
{showNowLine && (
|
{showNowLine && (
|
||||||
<div className="now-line relative flex items-center py-3 animate-in fade-in">
|
<div className="now-line relative flex items-center py-3 animate-in fade-in" style={{ scrollMarginTop: '20px' }}>
|
||||||
<div className="flex-1 border-t-2 border-red-500 rounded-full"></div>
|
<div className="flex-1 border-t-2 border-red-500 rounded-full"></div>
|
||||||
<div className="mx-2 text-[10px] font-black text-red-600 uppercase tracking-widest bg-red-100 dark:bg-red-900/30 px-3 py-1 rounded-full border border-red-200 dark:border-red-900/50 shadow-sm">Now</div>
|
<div className="mx-2 text-[10px] font-black text-red-600 uppercase tracking-widest bg-red-100 dark:bg-red-900/30 px-3 py-1 rounded-full border border-red-200 dark:border-red-900/50 shadow-sm">Now</div>
|
||||||
<div className="flex-1 border-t-2 border-red-500 rounded-full"></div>
|
<div className="flex-1 border-t-2 border-red-500 rounded-full"></div>
|
||||||
@@ -194,6 +222,17 @@ const CourtColumn = ({ courtId, onDelete, role }: { courtId: number, onDelete: (
|
|||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
{/* --- THE LINE AT THE VERY END --- */}
|
||||||
|
{isDayFinished && (
|
||||||
|
<div className="now-line relative flex items-center py-6 animate-in fade-in">
|
||||||
|
<div className="flex-1 border-t border-zinc-300 dark:border-zinc-700"></div>
|
||||||
|
<div className="mx-3 text-[10px] font-black text-zinc-500 uppercase tracking-widest bg-zinc-100 dark:bg-zinc-800 px-3 py-1 rounded-full border border-zinc-200 dark:border-zinc-700 shadow-sm">
|
||||||
|
All Matched played for today
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 border-t border-zinc-300 dark:border-zinc-700"></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user