Skip to content

Commit 6337f08

Browse files
committed
refactor: add updatedAt prop to ConferenceCard and enhance date handling in utils
1 parent a5efd06 commit 6337f08

3 files changed

Lines changed: 89 additions & 25 deletions

File tree

src/features/conferences/ConferenceCard.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,22 @@ import { formatDateRange, toDate, tzAbbrev } from "@/lib/utils/dates";
77

88
export const ConferenceCard = React.memo(function ConferenceCard({
99
conference,
10+
updatedAt,
1011
}: {
1112
conference: HTConference;
13+
updatedAt?: Date;
1214
}) {
1315
const start = toDate(conference.start_timestamp) ?? toDate(conference.start_date);
1416
const end = toDate(conference.end_timestamp) ?? toDate(conference.end_date);
1517
const range = formatDateRange(start, end, conference.timezone);
1618
const tz = tzAbbrev(conference.timezone);
19+
const updatedLabel = updatedAt
20+
? new Intl.DateTimeFormat(undefined, {
21+
month: "short",
22+
day: "numeric",
23+
year: "numeric",
24+
}).format(updatedAt)
25+
: undefined;
1726

1827
return (
1928
<Link
@@ -35,6 +44,12 @@ export const ConferenceCard = React.memo(function ConferenceCard({
3544
{tz && <span className="uppercase">{tz}</span>}
3645
</p>
3746
)}
47+
48+
{updatedAt && updatedLabel && (
49+
<p className="mt-2 text-xs font-medium text-neutral-500">
50+
Updated <time dateTime={updatedAt.toISOString()}>{updatedLabel}</time>
51+
</p>
52+
)}
3853
</article>
3954
</Link>
4055
);

src/features/conferences/DisplayConferences.tsx

Lines changed: 45 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,50 @@ import { useMemo } from "react";
22

33
import type { HTConference } from "@/types/db";
44

5-
import { ConferenceCard } from "./ConferenceCard";
5+
import { type DateLike, toDate } from "@/lib/utils/dates";
66

7-
/** Firestore / date helpers */
8-
type FirestoreTimestampLike = { toDate: () => Date };
9-
type DateLike = Date | string | number | FirestoreTimestampLike | null | undefined;
7+
import { ConferenceCard } from "./ConferenceCard";
108

11-
function isFirestoreTimestamp(v: unknown): v is FirestoreTimestampLike {
12-
return typeof (v as { toDate?: unknown })?.toDate === "function";
13-
}
149
function toMillis(value: DateLike): number {
15-
if (!value) return 0;
16-
if (value instanceof Date) return value.getTime();
17-
if (typeof value === "number") return value;
18-
if (typeof value === "string") return new Date(value).getTime();
19-
if (isFirestoreTimestamp(value)) return value.toDate().getTime();
20-
return 0;
10+
return toDate(value)?.getTime() ?? 0;
2111
}
2212

2313
type ConferenceWithDates<T> = T & {
2414
start_timestamp?: DateLike;
15+
end_timestamp?: DateLike;
2516
start_date?: DateLike;
17+
end_date?: DateLike;
18+
start_timestamp_str?: DateLike;
19+
end_timestamp_str?: DateLike;
20+
begin_tsz?: DateLike;
21+
end_tsz?: DateLike;
2622
updated_at?: DateLike;
23+
updated_timestamp?: DateLike;
24+
updated_tsz?: DateLike;
25+
updated?: DateLike;
2726
modified?: DateLike;
2827
};
2928

3029
const startMs = (c: ConferenceWithDates<HTConference>) =>
31-
toMillis(c.start_timestamp ?? c.start_date);
30+
toMillis(c.start_timestamp ?? c.start_timestamp_str ?? c.start_date ?? c.begin_tsz);
31+
32+
const endMs = (c: ConferenceWithDates<HTConference>) =>
33+
toMillis(c.end_timestamp ?? c.end_timestamp_str ?? c.end_date ?? c.end_tsz);
3234

3335
const updatedMs = (c: ConferenceWithDates<HTConference>) =>
34-
toMillis(c.updated_at ?? c.modified ?? c.start_timestamp ?? c.start_date);
36+
toMillis(
37+
c.updated_at ??
38+
c.updated_timestamp ??
39+
c.updated_tsz ??
40+
c.updated ??
41+
c.modified ??
42+
c.start_timestamp ??
43+
c.start_timestamp_str ??
44+
c.start_date,
45+
);
46+
47+
const updatedDate = (c: ConferenceWithDates<HTConference>) =>
48+
toDate(c.updated_at ?? c.updated_timestamp ?? c.updated_tsz ?? c.updated ?? c.modified);
3549

3650
const byStartAsc = (a: HTConference, b: HTConference) =>
3751
startMs(a as ConferenceWithDates<HTConference>) - startMs(b as ConferenceWithDates<HTConference>);
@@ -46,12 +60,18 @@ const byUpdatedDesc = (a: HTConference, b: HTConference) =>
4660
export function DisplayConferences({ conferences }: { conferences: HTConference[] }) {
4761
const { upcoming, updated, past } = useMemo(() => {
4862
const now = Date.now();
49-
const future = conferences.filter(
50-
(c) => startMs(c as ConferenceWithDates<HTConference>) >= now,
51-
);
52-
const history = conferences.filter(
53-
(c) => startMs(c as ConferenceWithDates<HTConference>) < now,
54-
);
63+
const future = conferences.filter((c) => {
64+
const conf = c as ConferenceWithDates<HTConference>;
65+
const endsAt = endMs(conf);
66+
const startsAt = startMs(conf);
67+
return (endsAt || startsAt) >= now;
68+
});
69+
const history = conferences.filter((c) => {
70+
const conf = c as ConferenceWithDates<HTConference>;
71+
const endsAt = endMs(conf);
72+
const startsAt = startMs(conf);
73+
return (endsAt || startsAt) < now;
74+
});
5575

5676
const THIRTY_DAYS = 1000 * 60 * 60 * 24 * 30;
5777

@@ -106,7 +126,10 @@ export function DisplayConferences({ conferences }: { conferences: HTConference[
106126
<CardsGrid>
107127
{updated.map((c) => (
108128
<CardWrap key={c.id}>
109-
<ConferenceCard conference={c} />
129+
<ConferenceCard
130+
conference={c}
131+
updatedAt={updatedDate(c as ConferenceWithDates<HTConference>)}
132+
/>
110133
</CardWrap>
111134
))}
112135
</CardsGrid>

src/lib/utils/dates.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,37 @@
1-
export function toDate(v?: string | Date | { seconds: number; nanoseconds?: number } | null) {
1+
type TimestampParts = { seconds: number; nanoseconds?: number };
2+
type FirestoreTimestampLike = { toDate: () => Date };
3+
4+
export type DateLike = string | number | Date | TimestampParts | FirestoreTimestampLike | null;
5+
6+
function isFirestoreTimestampLike(value: unknown): value is FirestoreTimestampLike {
7+
return typeof (value as { toDate?: unknown })?.toDate === "function";
8+
}
9+
10+
function isTimestampParts(value: unknown): value is TimestampParts {
11+
return (
12+
value !== null &&
13+
typeof value === "object" &&
14+
typeof (value as { seconds?: unknown }).seconds === "number"
15+
);
16+
}
17+
18+
function validDate(date: Date): Date | undefined {
19+
return Number.isNaN(date.getTime()) ? undefined : date;
20+
}
21+
22+
export function toDate(v?: DateLike) {
223
if (!v) return undefined;
324
if (v instanceof Date) return v;
25+
if (typeof v === "number") return validDate(new Date(v));
426
if (typeof v === "string") {
527
const d = new Date(v);
6-
return Number.isNaN(d.getTime()) ? undefined : d;
28+
return validDate(d);
29+
}
30+
if (isFirestoreTimestampLike(v)) return validDate(v.toDate());
31+
if (isTimestampParts(v)) {
32+
const nanoseconds = v.nanoseconds ?? 0;
33+
return validDate(new Date(v.seconds * 1000 + Math.floor(nanoseconds / 1_000_000)));
734
}
8-
if ("seconds" in v) return new Date(v.seconds * 1000);
935
return undefined;
1036
}
1137

0 commit comments

Comments
 (0)