From e1a4b560211e32939fc22beea9b46bae55e939d4 Mon Sep 17 00:00:00 2001 From: Dylan De Faoite Date: Sat, 17 Oct 2026 22:20:57 +0100 Subject: [PATCH] fix --- .../src/components/video/TimeGroupedGrid.tsx | 25 ++++++++++ frontend/src/pages/MyClips.tsx | 14 ++++-- frontend/src/pages/MyVods.tsx | 14 ++++-- frontend/src/utils/utils.ts | 50 ++++++++++++++++++- 4 files changed, 92 insertions(+), 11 deletions(-) create mode 100644 frontend/src/components/video/TimeGroupedGrid.tsx diff --git a/frontend/src/components/video/TimeGroupedGrid.tsx b/frontend/src/components/video/TimeGroupedGrid.tsx new file mode 100644 index 0000000..5cc2944 --- /dev/null +++ b/frontend/src/components/video/TimeGroupedGrid.tsx @@ -0,0 +1,25 @@ +import type { ReactNode } from "react"; +import { groupByTimeRange } from "../../utils/utils"; + +type props = { + items: T[]; + getDate: (item: T) => string; + renderItem: (item: T) => ReactNode; +} + +const TimeGroupedGrid = ({ items, getDate, renderItem }: props) => { + return ( +
+ {groupByTimeRange(items, getDate).map((group) => ( +
+

{group.label}

+
+ {group.items.map(renderItem)} +
+
+ ))} +
+ ); +} + +export default TimeGroupedGrid; diff --git a/frontend/src/pages/MyClips.tsx b/frontend/src/pages/MyClips.tsx index 9abede9..4bc2de0 100644 --- a/frontend/src/pages/MyClips.tsx +++ b/frontend/src/pages/MyClips.tsx @@ -1,7 +1,9 @@ import VideoCard from "../components/video/VideoCard"; +import TimeGroupedGrid from "../components/video/TimeGroupedGrid"; import { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import { getClips, deleteClip } from "../utils/api/clips"; +import { stringToDate } from "../utils/utils"; import type { Clip } from "../utils/types"; const MyClips = () => { @@ -12,7 +14,7 @@ const MyClips = () => { useEffect(() => { getClips() - .then((data) => setClips(data.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()))) + .then((data) => setClips(data.sort((a, b) => stringToDate(b.createdAt).getTime() - stringToDate(a.createdAt).getTime()))) .catch((err) => setError(err)) .finally(() => setLoading(false)); }, []); @@ -30,8 +32,10 @@ const MyClips = () => {

My Clips

-
- {clips.map((clip) => ( + clip.createdAt} + renderItem={(clip) => ( { onEdit={() => navigate(`/clips/${clip.id}/edit`)} onDelete={() => handleDelete(clip.id)} /> - ))} -
+ )} + /> {clips.length === 0 && !error && !loading && (
diff --git a/frontend/src/pages/MyVods.tsx b/frontend/src/pages/MyVods.tsx index 5783923..f477937 100644 --- a/frontend/src/pages/MyVods.tsx +++ b/frontend/src/pages/MyVods.tsx @@ -1,7 +1,9 @@ import VideoCard from "../components/video/VideoCard"; +import TimeGroupedGrid from "../components/video/TimeGroupedGrid"; import { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import { getVods, deleteVod } from "../utils/api/vods"; +import { stringToDate } from "../utils/utils"; import type { Vod } from "../utils/types"; const MyVods = () => { @@ -12,7 +14,7 @@ const MyVods = () => { useEffect(() => { getVods() - .then((data) => setVods(data.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()))) + .then((data) => setVods(data.sort((a, b) => stringToDate(b.createdAt).getTime() - stringToDate(a.createdAt).getTime()))) .catch((err) => setError(err instanceof Error ? err.message : "Failed to load vods")) .finally(() => setLoading(false)); }, []); @@ -30,8 +32,10 @@ const MyVods = () => {

My VoDs

-
- {vods.map((vod) => ( + vod.createdAt} + renderItem={(vod) => ( { onEdit={() => navigate(`/vods/${vod.id}/edit`)} onDelete={() => handleDelete(vod.id)} /> - ))} -
+ )} + /> {vods.length === 0 && !error && !loading && (
diff --git a/frontend/src/utils/utils.ts b/frontend/src/utils/utils.ts index 7c225ea..0204cd1 100644 --- a/frontend/src/utils/utils.ts +++ b/frontend/src/utils/utils.ts @@ -46,4 +46,52 @@ function dateToTimeAgo(date: Date): string { return `${s} second${s !== 1 ? 's' : ''} ago`; } -export { formatTime, stringToDate, dateToTimeAgo, formatLocalDate, formatLocalTime }; +function startOfDay(date: Date): Date { + return new Date(date.getFullYear(), date.getMonth(), date.getDate()); +} + +// Weeks start on Monday. +function startOfWeek(date: Date): Date { + const day = startOfDay(date); + day.setDate(day.getDate() - ((day.getDay() + 6) % 7)); + return day; +} + +// Buckets a date relative to now. Anything before last month falls back to its +// month and year, so older items still group sensibly. +function timeRangeLabel(date: Date, now: Date = new Date()): string { + const today = startOfDay(now); + const yesterday = new Date(today); + yesterday.setDate(today.getDate() - 1); + const thisWeek = startOfWeek(now); + const lastWeek = new Date(thisWeek); + lastWeek.setDate(thisWeek.getDate() - 7); + const thisMonth = new Date(now.getFullYear(), now.getMonth(), 1); + const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1); + + if (date >= today) return "Today"; + if (date >= yesterday) return "Yesterday"; + if (date >= thisWeek) return "This Week"; + if (date >= lastWeek) return "Last Week"; + if (date >= thisMonth) return "This Month"; + if (date >= lastMonth) return "Last Month"; + return date.toLocaleDateString(undefined, { month: "long", year: "numeric" }); +} + +type TimeGroup = { label: string, items: T[] }; + +// Expects items already sorted newest first; groups keep that order. +function groupByTimeRange(items: T[], getDateString: (item: T) => string): TimeGroup[] { + const now = new Date(); + const groups: TimeGroup[] = []; + for (const item of items) { + const label = timeRangeLabel(stringToDate(getDateString(item)), now); + const last = groups[groups.length - 1]; + if (last?.label === label) last.items.push(item); + else groups.push({ label, items: [item] }); + } + return groups; +} + +export { formatTime, stringToDate, dateToTimeAgo, formatLocalDate, formatLocalTime, timeRangeLabel, groupByTimeRange }; +export type { TimeGroup }; -- 2.49.1