Skip to content

Commit 2ce6c13

Browse files
committed
feat(app): add page title
1 parent a653732 commit 2ce6c13

File tree

11 files changed

+272
-129
lines changed

11 files changed

+272
-129
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,6 @@ tests/screenshots/
3535
supabase/.temp
3636
supabase/.branches
3737
.vercel
38-
dev-dist
38+
dev-dist
39+
40+
dump.sql

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
"@tanstack/react-query-devtools": "^5.81.2",
6464
"@tanstack/react-query-persist-client": "^5.85.9",
6565
"@types/marked": "^5.0.2",
66+
"@types/react-helmet": "^6.1.11",
6667
"class-variance-authority": "^0.7.1",
6768
"clsx": "^2.1.1",
6869
"cmdk": "^1.0.0",
@@ -78,6 +79,7 @@
7879
"react": "^18.3.1",
7980
"react-day-picker": "^9.8.0",
8081
"react-dom": "^18.3.1",
82+
"react-helmet-async": "^2.0.5",
8183
"react-hook-form": "^7.53.0",
8284
"react-resizable-panels": "^2.1.3",
8385
"react-router-dom": "^6.26.2",

pnpm-lock.yaml

Lines changed: 42 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/App.tsx

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Toaster } from "@/components/ui/toaster";
22
import { Toaster as Sonner } from "@/components/ui/sonner";
33
import { TooltipProvider } from "@/components/ui/tooltip";
44
import { BrowserRouter } from "react-router-dom";
5+
import { HelmetProvider } from "react-helmet-async";
56
import { CookieConsentBanner } from "@/components/layout/legal/CookieConsentBanner";
67
import { OfflineIndicator } from "@/components/ui/OfflineIndicator";
78
import {
@@ -25,24 +26,26 @@ function App() {
2526
}, []);
2627

2728
return (
28-
<TooltipProvider>
29-
<Toaster />
30-
<Sonner />
31-
<CookieConsentBanner />
32-
<BrowserRouter
33-
future={{
34-
v7_startTransition: true,
35-
v7_relativeSplatPath: true,
36-
}}
37-
>
38-
<AuthProvider>
39-
<FestivalEditionProvider>
40-
<AppRoutes subdomainInfo={subdomainInfo} />
41-
</FestivalEditionProvider>
42-
</AuthProvider>
43-
</BrowserRouter>
44-
<OfflineIndicator />
45-
</TooltipProvider>
29+
<HelmetProvider>
30+
<TooltipProvider>
31+
<Toaster />
32+
<Sonner />
33+
<CookieConsentBanner />
34+
<BrowserRouter
35+
future={{
36+
v7_startTransition: true,
37+
v7_relativeSplatPath: true,
38+
}}
39+
>
40+
<AuthProvider>
41+
<FestivalEditionProvider>
42+
<AppRoutes subdomainInfo={subdomainInfo} />
43+
</FestivalEditionProvider>
44+
</AuthProvider>
45+
</BrowserRouter>
46+
<OfflineIndicator />
47+
</TooltipProvider>
48+
</HelmetProvider>
4649
);
4750
}
4851

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { Helmet } from "react-helmet-async";
2+
3+
const DEFAULT_TITLE = "UpLine";
4+
const TITLE_SEPARATOR = " - ";
5+
6+
/**
7+
* Utility function to build title from parts
8+
*/
9+
export function buildTitle(title?: string, prefix?: string): string {
10+
const parts = [DEFAULT_TITLE];
11+
12+
if (title) {
13+
parts.unshift(title);
14+
}
15+
16+
if (prefix) {
17+
parts.unshift(prefix);
18+
}
19+
20+
return parts.join(TITLE_SEPARATOR);
21+
}
22+
23+
/**
24+
* Component to set the page title and meta tags using Helmet
25+
*/
26+
interface PageTitleProps {
27+
title?: string;
28+
prefix?: string;
29+
description?: string;
30+
}
31+
32+
export function PageTitle({ title, prefix, description }: PageTitleProps) {
33+
const fullTitle = buildTitle(title, prefix);
34+
35+
return (
36+
<Helmet>
37+
<title>{fullTitle}</title>
38+
{description && <meta name="description" content={description} />}
39+
<meta property="og:title" content={fullTitle} />
40+
{description && <meta property="og:description" content={description} />}
41+
<meta name="twitter:title" content={fullTitle} />
42+
{description && <meta name="twitter:description" content={description} />}
43+
</Helmet>
44+
);
45+
}

src/pages/EditionView/tabs/ArtistsTab/ArtistsTab.tsx

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ import { useUrlState } from "@/hooks/useUrlState";
44
import { SetsPanel } from "./SetsPanel";
55
import { useSetsByEditionQuery } from "@/hooks/queries/sets/useSetsByEdition";
66
import { useFestivalEdition } from "@/contexts/FestivalEditionContext";
7+
import { PageTitle } from "@/components/PageTitle/PageTitle";
78

89
export function ArtistsTab() {
910
const { state: urlState, updateUrlState, clearFilters } = useUrlState();
10-
const { edition } = useFestivalEdition();
11+
const { edition, festival } = useFestivalEdition();
1112

1213
// Fetch sets for the current edition
1314
const { data: sets = [], isLoading: setsLoading } = useSetsByEditionQuery(
@@ -20,28 +21,34 @@ export function ArtistsTab() {
2021

2122
if (setsLoading) {
2223
return (
23-
<div className="flex items-center justify-center py-12">
24-
<div className="text-white text-xl">Loading artists...</div>
25-
</div>
24+
<>
25+
<PageTitle title="Vote" prefix={festival?.name} />
26+
<div className="flex items-center justify-center py-12">
27+
<div className="text-white text-xl">Loading artists...</div>
28+
</div>
29+
</>
2630
);
2731
}
2832

2933
return (
30-
<div>
31-
<FilterSortControls
32-
state={urlState}
33-
onStateChange={updateUrlState}
34-
onClear={clearFilters}
35-
editionId={edition?.id || ""}
36-
/>
37-
38-
<div className="mt-8">
39-
<SetsPanel
40-
sets={filteredAndSortedSets}
41-
use24Hour={urlState.use24Hour}
42-
onLockSort={() => lockCurrentOrder(updateUrlState)}
34+
<>
35+
<PageTitle title="Vote" prefix={festival?.name} />
36+
<div>
37+
<FilterSortControls
38+
state={urlState}
39+
onStateChange={updateUrlState}
40+
onClear={clearFilters}
41+
editionId={edition?.id || ""}
4342
/>
43+
44+
<div className="mt-8">
45+
<SetsPanel
46+
sets={filteredAndSortedSets}
47+
use24Hour={urlState.use24Hour}
48+
onLockSort={() => lockCurrentOrder(updateUrlState)}
49+
/>
50+
</div>
4451
</div>
45-
</div>
52+
</>
4653
);
4754
}
Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useFestivalEdition } from "@/contexts/FestivalEditionContext";
22
import { useFestivalInfoQuery } from "@/hooks/queries/festival-info/useFestivalInfo";
3+
import { PageTitle } from "@/components/PageTitle/PageTitle";
34
import { EditionTitle } from "./InfoTab/EditionTitle";
45
import { InfoText } from "./InfoTab/InfoText";
56
import { CustomLinks } from "./InfoTab/CustomLinks";
@@ -11,6 +12,7 @@ import { useCustomLinksQuery } from "@/hooks/queries/custom-links/useCustomLinks
1112
export function InfoTab() {
1213
const { edition, festival } = useFestivalEdition();
1314
const { data: festivalInfo, isLoading } = useFestivalInfoQuery(festival?.id);
15+
1416
const customLinksQuery = useCustomLinksQuery(festival?.id || "");
1517
if (isLoading) {
1618
return <LoadingInfo />;
@@ -24,28 +26,31 @@ export function InfoTab() {
2426
customLinks.length === 0;
2527

2628
return (
27-
<div className="space-y-8">
28-
<EditionTitle name={edition?.name} />
29-
30-
{festivalInfo?.info_text && (
31-
<InfoText infoText={festivalInfo.info_text} />
32-
)}
33-
34-
{customLinks.length > 0 && <CustomLinks links={customLinks} />}
35-
36-
{festivalInfo?.facebook_url ? (
37-
<SocialLinkItem
38-
link={{ title: "Facebook", url: festivalInfo.facebook_url }}
39-
/>
40-
) : null}
41-
42-
{festivalInfo?.instagram_url ? (
43-
<SocialLinkItem
44-
link={{ title: "Instagram", url: festivalInfo.instagram_url }}
45-
/>
46-
) : null}
47-
48-
{noInfoAvailable && <NoInfo />}
49-
</div>
29+
<>
30+
<PageTitle title="Info" prefix={festival?.name} />
31+
<div className="space-y-8">
32+
<EditionTitle name={edition?.name} />
33+
34+
{festivalInfo?.info_text && (
35+
<InfoText infoText={festivalInfo.info_text} />
36+
)}
37+
38+
{customLinks.length > 0 && <CustomLinks links={customLinks} />}
39+
40+
{festivalInfo?.facebook_url ? (
41+
<SocialLinkItem
42+
link={{ title: "Facebook", url: festivalInfo.facebook_url }}
43+
/>
44+
) : null}
45+
46+
{festivalInfo?.instagram_url ? (
47+
<SocialLinkItem
48+
link={{ title: "Instagram", url: festivalInfo.instagram_url }}
49+
/>
50+
) : null}
51+
52+
{noInfoAvailable && <NoInfo />}
53+
</div>
54+
</>
5055
);
5156
}

0 commit comments

Comments
 (0)