Skip to content

Commit 617a028

Browse files
authored
Merge pull request #15 from junctor/refactor/event-to-content
Move schedule details from legacy events to content routes
2 parents 843b04e + 5896e8e commit 617a028

38 files changed

Lines changed: 1266 additions & 808 deletions

src/app/router.tsx

Lines changed: 86 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
import { Suspense, lazy } from "react";
2-
import { Routes, Route } from "react-router";
2+
import { Navigate, Routes, Route, useLocation, useParams } from "react-router";
33

44
import LoadingPage from "@/components/LoadingPage";
55
import NotFound from "@/components/NotFound";
6+
import {
7+
bookmarksPath,
8+
contentPath,
9+
peoplePath,
10+
personPath,
11+
schedulePath,
12+
} from "@/lib/utils/routes";
613

714
const SplashRoute = lazy(() =>
815
import("../features/splash/Splash").then((module) => ({
@@ -40,9 +47,9 @@ const BookmarksRoute = lazy(() =>
4047
})),
4148
);
4249

43-
const EventRoute = lazy(() =>
44-
import("../features/event/Event").then((module) => ({
45-
default: module.Event,
50+
const ContentRoute = lazy(() =>
51+
import("../features/content/Content").then((module) => ({
52+
default: module.ContentDetailPage,
4653
})),
4754
);
4855

@@ -58,6 +65,57 @@ const PersonRoute = lazy(() =>
5865
})),
5966
);
6067

68+
function parseLegacyId(value: string | null): number | null {
69+
if (!value || !/^\d+$/.test(value)) return null;
70+
return Number(value);
71+
}
72+
73+
function LegacyRedirect() {
74+
const { pathname, search } = useLocation();
75+
const params = new URLSearchParams(search);
76+
const confCode = params.get("conf") ?? params.get("conference");
77+
const contentId = parseLegacyId(params.get("event") ?? params.get("content") ?? params.get("id"));
78+
const personId = parseLegacyId(params.get("person") ?? params.get("id"));
79+
80+
if (!confCode) return <NotFound />;
81+
82+
if (
83+
pathname === "/event" ||
84+
pathname === "/event/" ||
85+
pathname === "/content" ||
86+
pathname === "/content/"
87+
) {
88+
return contentId ? <Navigate replace to={contentPath(confCode, contentId)} /> : <NotFound />;
89+
}
90+
91+
if (pathname === "/schedule") {
92+
return <Navigate replace to={schedulePath(confCode)} />;
93+
}
94+
95+
if (pathname === "/bookmarks") {
96+
return <Navigate replace to={bookmarksPath(confCode)} />;
97+
}
98+
99+
if (pathname === "/people" || pathname === "/people/") {
100+
return (
101+
<Navigate replace to={personId ? personPath(confCode, personId) : peoplePath(confCode)} />
102+
);
103+
}
104+
105+
if (pathname === "/person") {
106+
return personId ? <Navigate replace to={personPath(confCode, personId)} /> : <NotFound />;
107+
}
108+
109+
return <NotFound />;
110+
}
111+
112+
function LegacyContentRedirect() {
113+
const { confCode, contentId } = useParams();
114+
const parsedContentId = contentId && /^\d+$/.test(contentId) ? Number(contentId) : null;
115+
if (!confCode || !parsedContentId) return <NotFound />;
116+
return <Navigate replace to={contentPath(confCode, parsedContentId)} />;
117+
}
118+
61119
export default function AppRouter() {
62120
return (
63121
<Routes>
@@ -77,46 +135,63 @@ export default function AppRouter() {
77135
</Suspense>
78136
}
79137
/>
138+
<Route path="/schedule" element={<LegacyRedirect />} />
139+
<Route path="/bookmarks" element={<LegacyRedirect />} />
140+
<Route path="/people" element={<LegacyRedirect />} />
141+
<Route path="/people/" element={<LegacyRedirect />} />
142+
<Route path="/person" element={<LegacyRedirect />} />
143+
<Route path="/event" element={<LegacyRedirect />} />
144+
<Route path="/content" element={<LegacyRedirect />} />
145+
<Route path="/content/" element={<LegacyRedirect />} />
146+
<Route
147+
path="/:confCode"
148+
element={
149+
<Suspense fallback={<LoadingPage message="Loading schedule..." />}>
150+
<ScheduleRoute />
151+
</Suspense>
152+
}
153+
/>
80154
<Route
81-
path="/schedule"
155+
path="/:confCode/schedule"
82156
element={
83157
<Suspense fallback={<LoadingPage message="Loading schedule..." />}>
84158
<ScheduleRoute />
85159
</Suspense>
86160
}
87161
/>
88162
<Route
89-
path="/bookmarks"
163+
path="/:confCode/bookmarks"
90164
element={
91165
<Suspense fallback={<LoadingPage message="Loading bookmarks..." />}>
92166
<BookmarksRoute />
93167
</Suspense>
94168
}
95169
/>
96170
<Route
97-
path="/people"
171+
path="/:confCode/people"
98172
element={
99173
<Suspense fallback={<LoadingPage message="Loading people..." />}>
100174
<PeopleRoute />
101175
</Suspense>
102176
}
103177
/>
104178
<Route
105-
path="/person"
179+
path="/:confCode/people/:personId"
106180
element={
107181
<Suspense fallback={<LoadingPage message="Loading person..." />}>
108182
<PersonRoute />
109183
</Suspense>
110184
}
111185
/>
112186
<Route
113-
path="/event"
187+
path="/:confCode/content/:contentId"
114188
element={
115-
<Suspense fallback={<LoadingPage message="Loading event..." />}>
116-
<EventRoute />
189+
<Suspense fallback={<LoadingPage message="Loading content..." />}>
190+
<ContentRoute />
117191
</Suspense>
118192
}
119193
/>
194+
<Route path="/:confCode/event/:contentId" element={<LegacyContentRedirect />} />
120195
<Route
121196
path="/about"
122197
element={

src/components/ConferenceHeader.tsx

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ import {
99
UserGroupIcon,
1010
} from "@heroicons/react/24/outline";
1111
import { Fragment, useEffect, useMemo, useState } from "react";
12-
import { Link, useLocation, useSearchParams } from "react-router";
12+
import { Link, useLocation } from "react-router";
1313

1414
import type { HTConference } from "@/types/db";
1515

16+
import { bookmarksPath, conferencePath, peoplePath, schedulePath } from "@/lib/utils/routes";
17+
1618
type NavItem = {
1719
key: string;
1820
label: string;
@@ -24,7 +26,6 @@ type NavItem = {
2426

2527
export function ConferenceHeader({ conference }: { conference: HTConference }) {
2628
const { pathname } = useLocation();
27-
const [params] = useSearchParams();
2829
const [scrolled, setScrolled] = useState(false);
2930

3031
useEffect(() => {
@@ -36,16 +37,16 @@ export function ConferenceHeader({ conference }: { conference: HTConference }) {
3637

3738
// Build first-class, conference-aware items
3839
const confCode = conference.code;
39-
const scheduleHref = `/schedule?conf=${encodeURIComponent(confCode)}`;
40-
const bookmarksHref = `/bookmarks?conf=${encodeURIComponent(confCode)}`;
41-
const peopleHref = `/people?conf=${encodeURIComponent(confCode)}`;
40+
const confHref = conferencePath(confCode);
41+
const scheduleHref = schedulePath(confCode);
42+
const bookmarksHref = bookmarksPath(confCode);
43+
const peopleHref = peoplePath(confCode);
4244

43-
// Active states (path + query awareness)
44-
const isSchedule =
45-
pathname.startsWith("/schedule") && (params.get("conf") ?? confCode) === confCode;
46-
const isBookmarks =
47-
pathname.startsWith("/bookmarks") && (params.get("conf") ?? confCode) === confCode;
48-
const isPeople = pathname.startsWith("/people") && (params.get("conf") ?? confCode) === confCode;
45+
const normalizedPathname = pathname.toLowerCase().replace(/\/+$/, "") || "/";
46+
const isSchedule = normalizedPathname === confHref || normalizedPathname === scheduleHref;
47+
const isBookmarks = normalizedPathname === bookmarksHref;
48+
const isPeople =
49+
normalizedPathname === peopleHref || normalizedPathname.startsWith(`${peopleHref}/`);
4950

5051
const items: NavItem[] = useMemo(() => {
5152
const base: NavItem[] = [
@@ -91,8 +92,8 @@ export function ConferenceHeader({ conference }: { conference: HTConference }) {
9192
}, [scheduleHref, isSchedule, bookmarksHref, isBookmarks, peopleHref, isPeople, conference.link]);
9293

9394
const baseHeader =
94-
"sticky top-0 z-50 min-h-16 border-b border-white/10 text-white backdrop-blur-md transition-[background-color,border-color,box-shadow] duration-200 supports-[backdrop-filter]:backdrop-blur-md";
95-
const bg = scrolled ? "bg-slate-950/92 shadow-[0_12px_32px_rgba(2,6,23,0.3)]" : "bg-slate-950/82";
95+
"sticky top-0 z-50 min-h-16 border-b border-white/10 bg-[var(--color-page-bg)] text-white transition-[border-color,box-shadow] duration-200";
96+
const bg = scrolled ? "shadow-[0_12px_32px_rgba(2,6,23,0.3)]" : "";
9697

9798
return (
9899
<header className={`${baseHeader} ${bg}`}>

src/components/ErrorPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { HTHeader } from "./HTHeader";
55

66
export default function ErrorPage({ msg }: { msg?: string }) {
77
return (
8-
<div className="flex min-h-dvh flex-col">
8+
<div className="ui-page flex flex-col">
99
<HTHeader />
1010

1111
<main id="main" className="flex flex-1 items-center justify-center px-6">

src/components/HTFooter.tsx

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
import { CodeBracketSquareIcon } from "@heroicons/react/16/solid";
21
import { Link } from "react-router";
32

43
export function HTFooter() {
54
return (
6-
<footer className="mt-auto border-t border-white/10 bg-[linear-gradient(180deg,rgba(2,6,23,0.92),rgba(2,6,23,0.98))] text-slate-300">
7-
<div className="mx-auto w-[min(72rem,calc(100%_-_2rem))] py-5 sm:w-[min(72rem,calc(100%_-_3rem))] sm:py-6">
5+
<footer className="mt-auto border-t border-white/10 bg-(--color-page-bg) text-slate-300">
6+
<div className="ui-container py-5 sm:py-6">
87
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
98
<div className="min-w-0 space-y-1.5">
109
<p className="text-[11px] font-semibold tracking-[0.16em] text-slate-500 uppercase">
@@ -46,17 +45,6 @@ export function HTFooter() {
4645
GitHub
4746
</a>
4847
</nav>
49-
50-
<a
51-
href="https://github.com/junctor/hackertracker-web"
52-
target="_blank"
53-
rel="noopener noreferrer"
54-
aria-label="View source on GitHub"
55-
className="ui-btn-base ui-btn-secondary ui-focus-ring w-fit gap-2 rounded-xl px-3.5 text-sm text-slate-200 shadow-[0_10px_24px_rgba(2,6,23,0.18)] focus-visible:outline-none"
56-
>
57-
<CodeBracketSquareIcon className="h-4 w-4 text-[#6CCDBB]" aria-hidden="true" />
58-
<span>View Source</span>
59-
</a>
6048
</div>
6149
</div>
6250
</footer>

src/components/HTHeader.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,15 @@ export function HTHeader({ variant = "default" }: { variant?: "default" | "splas
4848
}, []);
4949

5050
const base =
51-
"sticky top-0 z-50 min-h-16 border-b border-white/10 text-white backdrop-blur-md transition-[background-color,border-color,box-shadow] duration-200 supports-[backdrop-filter]:backdrop-blur-md";
51+
"sticky top-0 z-50 min-h-16 border-b border-white/10 bg-[var(--color-page-bg)] text-white transition-[border-color,box-shadow] duration-200";
5252
const bg =
5353
variant === "splash"
5454
? scrolled
55-
? "bg-slate-950/90 shadow-[0_12px_32px_rgba(2,6,23,0.28)]"
56-
: "bg-slate-950/72 shadow-[0_10px_28px_rgba(2,6,23,0.18)]"
55+
? "shadow-[0_12px_32px_rgba(2,6,23,0.28)]"
56+
: "shadow-[0_10px_28px_rgba(2,6,23,0.12)]"
5757
: scrolled
58-
? "bg-slate-950/92 shadow-[0_12px_32px_rgba(2,6,23,0.3)]"
59-
: "bg-slate-950/82";
58+
? "shadow-[0_12px_32px_rgba(2,6,23,0.3)]"
59+
: "";
6060

6161
return (
6262
<header className={`${base} ${bg}`}>

src/components/NotFound.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export default function NotFound() {
1919
const message = useMemo(() => messages[Math.floor(Math.random() * messages.length)], []);
2020

2121
return (
22-
<div className="flex min-h-dvh flex-col">
22+
<div className="ui-page flex flex-col">
2323
<HTHeader />
2424

2525
<main id="main" className="flex flex-1 items-center justify-center px-6">

src/features/bookmarks/Bookmarks.tsx

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useEffect, useState, lazy, Suspense, startTransition } from "react";
2-
import { Link, useSearchParams } from "react-router";
2+
import { Link } from "react-router";
33

44
import type { HTConference } from "@/types/db";
55
import type { GroupedSchedule } from "@/types/ht";
@@ -9,24 +9,25 @@ import ErrorPage from "@/components/ErrorPage";
99
import { HTFooter } from "@/components/HTFooter";
1010
import LoadingPage from "@/components/LoadingPage";
1111
import {
12-
filterScheduleByEventIds,
12+
filterScheduleByContentIds,
1313
getCachedConferenceSchedule,
1414
getConferenceSchedule,
1515
} from "@/lib/db";
16+
import { useNormalizedParams } from "@/lib/utils/params";
17+
import { schedulePath } from "@/lib/utils/routes";
1618
import { loadConfBookmarks } from "@/lib/utils/storage";
1719

18-
const EventsList = lazy(() => import("../schedule/EventsList"));
20+
const ScheduleContentList = lazy(() => import("../schedule/ScheduleContentList"));
1921

2022
export function Bookmarks() {
21-
const [searchParams] = useSearchParams();
22-
const confCode = searchParams.get("conf")?.trim().toUpperCase() ?? null;
23+
const { confCode } = useNormalizedParams();
2324
const [initial] = useState(() => {
2425
if (!confCode) return null;
2526
const schedule = getCachedConferenceSchedule(confCode);
2627
if (!schedule) return null;
2728
return {
2829
conference: schedule.conference,
29-
grouped: filterScheduleByEventIds(schedule.grouped, loadConfBookmarks(confCode)),
30+
grouped: filterScheduleByContentIds(schedule.grouped, loadConfBookmarks(confCode)),
3031
};
3132
});
3233

@@ -62,7 +63,7 @@ export function Bookmarks() {
6263
const bookmarks = loadConfBookmarks(confCode);
6364
if (cachedSchedule) {
6465
setConference(cachedSchedule.conference);
65-
setGrouped(filterScheduleByEventIds(cachedSchedule.grouped, bookmarks));
66+
setGrouped(filterScheduleByContentIds(cachedSchedule.grouped, bookmarks));
6667
setLoading(false);
6768
} else {
6869
setLoading(true);
@@ -84,7 +85,7 @@ export function Bookmarks() {
8485
return;
8586
}
8687

87-
const groupedSchedule = filterScheduleByEventIds(schedule.grouped, bookmarks);
88+
const groupedSchedule = filterScheduleByContentIds(schedule.grouped, bookmarks);
8889

8990
startTransition(() => {
9091
setConference(schedule.conference);
@@ -109,19 +110,19 @@ export function Bookmarks() {
109110
if (error) return <ErrorPage msg={error} />;
110111

111112
return (
112-
<div className="flex min-h-dvh flex-col">
113+
<div className="ui-page flex flex-col">
113114
{conference && <ConferenceHeader conference={conference} />}
114115
<main className="flex-1">
115116
{conference && grouped && Object.keys(grouped).length > 0 ? (
116117
<Suspense fallback={<LoadingPage message="Loading events..." />}>
117-
<EventsList dateGroup={grouped} conf={conference} pageTitle="Bookmarks" />
118+
<ScheduleContentList dateGroup={grouped} conf={conference} pageTitle="Bookmarks" />
118119
</Suspense>
119120
) : (
120121
<div className="ui-empty-state mx-auto mt-20 max-w-md">
121122
<p className="text-gray-200">No bookmarks found.</p>
122123
{confCode && (
123124
<Link
124-
to={`/schedule?conf=${confCode}`}
125+
to={schedulePath(confCode)}
125126
className="ui-btn-base ui-btn-secondary ui-focus-ring ui-empty-state-action focus-visible:outline-none"
126127
>
127128
Browse Schedule

src/features/conferences/ConferenceCard.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Link } from "react-router";
44
import type { HTConference } from "@/types/db";
55

66
import { formatDateRange, toDate, tzAbbrev } from "@/lib/utils/dates";
7+
import { conferencePath } from "@/lib/utils/routes";
78

89
export const ConferenceCard = React.memo(function ConferenceCard({
910
conference,
@@ -26,12 +27,12 @@ export const ConferenceCard = React.memo(function ConferenceCard({
2627

2728
return (
2829
<Link
29-
to={`/schedule?conf=${conference.code}`}
30+
to={conferencePath(conference.code)}
3031
className="ui-focus-ring group block h-full rounded-[0.875rem] focus-visible:outline-none"
3132
>
32-
<article className="ui-card ui-card-interactive flex h-full min-h-[7.25rem] flex-col p-5 transition group-hover:shadow-md">
33+
<article className="ui-card ui-card-interactive flex h-full min-h-29 flex-col p-5 transition group-hover:shadow-md">
3334
<h3
34-
className="line-clamp-2 min-h-[2.75rem] text-lg leading-snug font-semibold break-words text-neutral-100 transition-colors group-hover:text-white"
35+
className="line-clamp-2 min-h-11 text-lg leading-snug font-semibold wrap-break-word text-neutral-100 transition-colors group-hover:text-white"
3536
title={conference.name}
3637
>
3738
{conference.name}

0 commit comments

Comments
 (0)