Skip to content

Commit 0df36e7

Browse files
committed
feat: parse subdomain
1 parent 3137465 commit 0df36e7

11 files changed

Lines changed: 476 additions & 144 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,4 @@ tests/screenshots/
3434

3535
supabase/.temp
3636
supabase/.branches
37+
.vercel

src/App.tsx

Lines changed: 31 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -1,118 +1,39 @@
11
import { Toaster } from "@/components/ui/toaster";
22
import { Toaster as Sonner } from "@/components/ui/sonner";
33
import { TooltipProvider } from "@/components/ui/tooltip";
4-
import { BrowserRouter, Routes, Route } from "react-router-dom";
4+
import { BrowserRouter } from "react-router-dom";
55
import { CookieConsentBanner } from "@/components/legal/CookieConsentBanner";
66
import { AppFooter } from "@/components/legal/AppFooter";
7-
import { FestivalEditionProvider } from "@/contexts/FestivalEditionContext";
8-
9-
// Public pages
10-
import FestivalSelection from "./pages/FestivalSelection";
11-
import EditionSelection from "./pages/EditionSelection";
12-
import EditionView from "./pages/EditionView";
13-
import Groups from "./pages/Groups";
14-
import GroupDetail from "./pages/GroupDetail";
15-
import Schedule from "./pages/Schedule";
16-
import { SetDetails } from "./pages/SetDetails";
17-
18-
// Admin pages
19-
import AdminLayout from "./pages/AdminLayout";
20-
import AdminDashboard from "./pages/admin/AdminDashboard";
21-
import AdminAnalytics from "./pages/admin/AdminAnalytics";
22-
import AdminRoles from "./pages/admin/AdminRoles";
23-
import AdminFestivals from "./pages/admin/AdminFestivals";
24-
import FestivalDetail from "./pages/admin/FestivalDetail";
25-
import FestivalEdition from "./pages/admin/FestivalEdition";
26-
import FestivalStages from "./pages/admin/FestivalStages";
27-
import FestivalSets from "./pages/admin/FestivalSets";
28-
29-
// Legal pages
30-
import PrivacyPolicy from "./pages/PrivacyPolicy";
31-
import TermsOfService from "./pages/TermsOfService";
32-
import CookiePolicy from "./pages/CookiePolicy";
33-
import NotFound from "./pages/NotFound";
34-
35-
const App = () => (
36-
<TooltipProvider>
37-
<Toaster />
38-
<Sonner />
39-
<CookieConsentBanner />
40-
<BrowserRouter>
41-
<div className="min-h-screen flex flex-col">
42-
<div className="flex-1">
43-
<Routes>
44-
{/* Festival/Edition Selection Routes */}
45-
<Route path="/" element={<FestivalSelection />} />
46-
<Route
47-
path="/festivals/:festivalSlug"
48-
element={
49-
<FestivalEditionProvider>
50-
<EditionSelection />
51-
</FestivalEditionProvider>
52-
}
53-
/>
54-
<Route
55-
path="/festivals/:festivalSlug/editions/:editionSlug"
56-
element={
57-
<FestivalEditionProvider>
58-
<EditionView />
59-
</FestivalEditionProvider>
60-
}
61-
/>
62-
<Route
63-
path="/festivals/:festivalSlug/editions/:editionSlug/sets/:setId"
64-
element={
65-
<FestivalEditionProvider>
66-
<SetDetails />
67-
</FestivalEditionProvider>
68-
}
69-
/>
70-
<Route
71-
path="/festivals/:festivalSlug/editions/:editionSlug/schedule"
72-
element={
73-
<FestivalEditionProvider>
74-
<Schedule />
75-
</FestivalEditionProvider>
76-
}
77-
/>
78-
79-
{/* Global routes (not scoped to festival/edition) */}
80-
<Route path="/groups" element={<Groups />} />
81-
<Route path="/groups/:groupId" element={<GroupDetail />} />
82-
83-
{/* Admin routes (unchanged) */}
84-
<Route path="/admin" element={<AdminLayout />}>
85-
<Route index element={<AdminDashboard />} />
86-
<Route path="artists" element={<AdminDashboard />} />
87-
<Route path="analytics" element={<AdminAnalytics />} />
88-
<Route path="admins" element={<AdminRoles />} />
89-
<Route path="festivals" element={<AdminFestivals />}>
90-
<Route path=":festivalId" element={<FestivalDetail />}>
91-
<Route
92-
path="editions/:editionId"
93-
element={<FestivalEdition />}
94-
>
95-
<Route index element={<FestivalStages />} />
96-
<Route path="stages" element={<FestivalStages />} />
97-
<Route path="sets" element={<FestivalSets />} />
98-
</Route>
99-
</Route>
100-
</Route>
101-
</Route>
102-
103-
{/* Legal pages */}
104-
<Route path="/privacy" element={<PrivacyPolicy />} />
105-
<Route path="/terms" element={<TermsOfService />} />
106-
<Route path="/cookies" element={<CookiePolicy />} />
107-
108-
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
109-
<Route path="*" element={<NotFound />} />
110-
</Routes>
7+
import { getSubdomainInfo } from "@/lib/subdomain";
8+
9+
import { MainDomainRoutes } from "./components/MainDomainRoutes";
10+
import { SubdomainRoutes } from "./components/SubdomainRoutes";
11+
import { useState } from "react";
12+
13+
const App = () => {
14+
const [subdomainInfo] = useState(() => getSubdomainInfo());
15+
16+
return (
17+
<TooltipProvider>
18+
<Toaster />
19+
<Sonner />
20+
<CookieConsentBanner />
21+
<BrowserRouter>
22+
<div className="min-h-screen flex flex-col">
23+
<div className="flex-1">
24+
{subdomainInfo.isSubdomain && subdomainInfo.festivalSlug ? (
25+
// Subdomain routing: boom-festival.getupline.com
26+
<SubdomainRoutes festivalSlug={subdomainInfo.festivalSlug} />
27+
) : (
28+
// Main domain routing: getupline.com
29+
<MainDomainRoutes />
30+
)}
31+
</div>
32+
<AppFooter />
11133
</div>
112-
<AppFooter />
113-
</div>
114-
</BrowserRouter>
115-
</TooltipProvider>
116-
);
34+
</BrowserRouter>
35+
</TooltipProvider>
36+
);
37+
};
11738

11839
export default App;
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { Routes, Route } from "react-router-dom";
2+
import { SubdomainRedirect } from "./SubdomainRedirect";
3+
4+
// Public pages
5+
import FestivalSelection from "@/pages/FestivalSelection";
6+
import Groups from "@/pages/Groups";
7+
import GroupDetail from "@/pages/GroupDetail";
8+
9+
// Admin pages
10+
import AdminLayout from "@/pages/AdminLayout";
11+
import AdminDashboard from "@/pages/admin/AdminDashboard";
12+
import AdminAnalytics from "@/pages/admin/AdminAnalytics";
13+
import AdminRoles from "@/pages/admin/AdminRoles";
14+
import AdminFestivals from "@/pages/admin/AdminFestivals";
15+
import FestivalDetail from "@/pages/admin/FestivalDetail";
16+
import FestivalEdition from "@/pages/admin/FestivalEdition";
17+
import FestivalStages from "@/pages/admin/FestivalStages";
18+
import FestivalSets from "@/pages/admin/FestivalSets";
19+
20+
// Legal pages
21+
import PrivacyPolicy from "@/pages/PrivacyPolicy";
22+
import TermsOfService from "@/pages/TermsOfService";
23+
import CookiePolicy from "@/pages/CookiePolicy";
24+
import NotFound from "@/pages/NotFound";
25+
26+
/**
27+
* Routes for main domain access (getupline.com)
28+
* Includes festival selection and full admin interface
29+
*/
30+
export function MainDomainRoutes() {
31+
return (
32+
<Routes>
33+
{/* Festival/Edition Selection Routes */}
34+
<Route path="/" element={<FestivalSelection />} />
35+
36+
{/* Festival routes redirect to subdomains */}
37+
<Route path="/festivals/:festivalSlug" element={<SubdomainRedirect />} />
38+
<Route
39+
path="/festivals/:festivalSlug/editions/:editionSlug"
40+
element={<SubdomainRedirect />}
41+
/>
42+
<Route
43+
path="/festivals/:festivalSlug/editions/:editionSlug/sets/:setId"
44+
element={<SubdomainRedirect />}
45+
/>
46+
<Route
47+
path="/festivals/:festivalSlug/editions/:editionSlug/schedule"
48+
element={<SubdomainRedirect />}
49+
/>
50+
51+
{/* Global routes (not scoped to festival/edition) */}
52+
<Route path="/groups" element={<Groups />} />
53+
<Route path="/groups/:groupId" element={<GroupDetail />} />
54+
55+
{/* Admin routes */}
56+
<Route path="/admin" element={<AdminLayout />}>
57+
<Route index element={<AdminDashboard />} />
58+
<Route path="artists" element={<AdminDashboard />} />
59+
<Route path="analytics" element={<AdminAnalytics />} />
60+
<Route path="admins" element={<AdminRoles />} />
61+
<Route path="festivals" element={<AdminFestivals />}>
62+
<Route path=":festivalId" element={<FestivalDetail />}>
63+
<Route path="editions/:editionId" element={<FestivalEdition />}>
64+
<Route index element={<FestivalStages />} />
65+
<Route path="stages" element={<FestivalStages />} />
66+
<Route path="sets" element={<FestivalSets />} />
67+
</Route>
68+
</Route>
69+
</Route>
70+
</Route>
71+
72+
{/* Legal pages */}
73+
<Route path="/privacy" element={<PrivacyPolicy />} />
74+
<Route path="/terms" element={<TermsOfService />} />
75+
<Route path="/cookies" element={<CookiePolicy />} />
76+
77+
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
78+
<Route path="*" element={<NotFound />} />
79+
</Routes>
80+
);
81+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { useEffect } from "react";
2+
import { useParams } from "react-router-dom";
3+
import { createFestivalSubdomainUrl } from "@/lib/subdomain";
4+
import { FestivalEditionProvider } from "@/contexts/FestivalEditionContext";
5+
import EditionSelection from "@/pages/EditionSelection";
6+
import EditionView from "@/pages/EditionView";
7+
import Schedule from "@/pages/Schedule";
8+
import { SetDetails } from "@/pages/SetDetails";
9+
10+
/**
11+
* Component that redirects main domain festival URLs to subdomains
12+
* Used for: /festivals/boom-festival -> boom-festival.getupline.com
13+
* For localhost development, renders the appropriate component directly
14+
*/
15+
export function SubdomainRedirect() {
16+
const { festivalSlug, editionSlug, setId } = useParams<{
17+
festivalSlug?: string;
18+
editionSlug?: string;
19+
setId?: string;
20+
}>();
21+
22+
const isLocalhost =
23+
window.location.hostname.includes("localhost") ||
24+
window.location.hostname.includes("127.0.0.1");
25+
26+
useEffect(() => {
27+
if (!festivalSlug || isLocalhost) return;
28+
29+
// Build the target path based on current route
30+
let targetPath = "/";
31+
32+
if (editionSlug && setId) {
33+
targetPath = `/editions/${editionSlug}/sets/${setId}`;
34+
} else if (editionSlug && window.location.pathname.includes("schedule")) {
35+
targetPath = `/editions/${editionSlug}/schedule`;
36+
} else if (editionSlug) {
37+
targetPath = `/editions/${editionSlug}`;
38+
}
39+
40+
// Redirect to subdomain
41+
const subdomainUrl = createFestivalSubdomainUrl(festivalSlug, targetPath);
42+
window.location.href = subdomainUrl;
43+
}, [festivalSlug, editionSlug, setId, isLocalhost]);
44+
45+
// For localhost development, render the appropriate component directly
46+
if (isLocalhost && festivalSlug) {
47+
return (
48+
<FestivalEditionProvider festivalSlug={festivalSlug}>
49+
{setId ? (
50+
<SetDetails />
51+
) : editionSlug && window.location.pathname.includes("schedule") ? (
52+
<Schedule />
53+
) : editionSlug ? (
54+
<EditionView />
55+
) : (
56+
<EditionSelection />
57+
)}
58+
</FestivalEditionProvider>
59+
);
60+
}
61+
62+
// Show loading message while redirecting (production only)
63+
return (
64+
<div className="min-h-screen bg-app-gradient flex items-center justify-center">
65+
<div className="text-center">
66+
<div className="text-white text-xl mb-4">Redirecting...</div>
67+
<div className="text-purple-200">
68+
Taking you to{" "}
69+
{festivalSlug ? `${festivalSlug}.getupline.com` : "the festival site"}
70+
</div>
71+
</div>
72+
</div>
73+
);
74+
}

src/components/SubdomainRoutes.tsx

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { Routes, Route } from "react-router-dom";
2+
import { FestivalEditionProvider } from "@/contexts/FestivalEditionContext";
3+
import EditionView from "@/pages/EditionView";
4+
import Schedule from "@/pages/Schedule";
5+
import { SetDetails } from "@/pages/SetDetails";
6+
import Groups from "@/pages/Groups";
7+
import GroupDetail from "@/pages/GroupDetail";
8+
9+
// Legal pages
10+
import PrivacyPolicy from "@/pages/PrivacyPolicy";
11+
import TermsOfService from "@/pages/TermsOfService";
12+
import CookiePolicy from "@/pages/CookiePolicy";
13+
import NotFound from "@/pages/NotFound";
14+
import EditionSelection from "@/pages/EditionSelection";
15+
16+
/**
17+
* Routes for subdomain access (boom-festival.getupline.com)
18+
* Root path shows edition selection for the festival
19+
*/
20+
export function SubdomainRoutes({ festivalSlug }: { festivalSlug?: string }) {
21+
return (
22+
<FestivalEditionProvider festivalSlug={festivalSlug} isSubDomain>
23+
<Routes>
24+
<Route path="/" element={<EditionSelection />} />
25+
{/* Edition-specific routes */}
26+
<Route path="/editions/:editionSlug" element={<EditionView />} />
27+
<Route
28+
path="/editions/:editionSlug/sets/:setId"
29+
element={<SetDetails />}
30+
/>
31+
<Route path="/editions/:editionSlug/schedule" element={<Schedule />} />
32+
33+
{/* Global routes still available */}
34+
<Route path="/groups" element={<Groups />} />
35+
<Route path="/groups/:groupId" element={<GroupDetail />} />
36+
37+
{/* Legal pages */}
38+
<Route path="/privacy" element={<PrivacyPolicy />} />
39+
<Route path="/terms" element={<TermsOfService />} />
40+
<Route path="/cookies" element={<CookiePolicy />} />
41+
42+
{/* Catch-all */}
43+
<Route path="*" element={<NotFound />} />
44+
</Routes>
45+
</FestivalEditionProvider>
46+
);
47+
}

0 commit comments

Comments
 (0)