Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added public/timeline-screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 3 additions & 3 deletions src/components/router/GlobalRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export function GlobalRoutes() {
<Routes>
{/* Global routes (not scoped to festival/edition) */}
<Route path="/groups" element={<Groups />} />
<Route path="/groups/:groupId" element={<GroupDetail />} />
<Route path="/groups/:groupSlug" element={<GroupDetail />} />

{/* Admin routes */}
<Route path="/admin" element={<AdminLayout />}>
Expand All @@ -30,8 +30,8 @@ export function GlobalRoutes() {
<Route path="analytics" element={<AdminAnalytics />} />
<Route path="admins" element={<AdminRolesTable />} />
<Route path="festivals" element={<AdminFestivals />}>
<Route path=":festivalId" element={<FestivalDetail />}>
<Route path="editions/:editionId" element={<FestivalEdition />}>
<Route path=":festivalSlug" element={<FestivalDetail />}>
<Route path="editions/:editionSlug" element={<FestivalEdition />}>
<Route index element={<FestivalStages />} />
<Route path="stages" element={<FestivalStages />} />
<Route path="sets" element={<FestivalSets />} />
Expand Down
10 changes: 5 additions & 5 deletions src/components/router/SubdomainRedirect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ interface SubdomainRedirectProps {
export function SubdomainRedirect({
component: Component,
}: SubdomainRedirectProps) {
const { festivalSlug, editionSlug, setId } = useParams<{
const { festivalSlug, editionSlug, setSlug } = useParams<{
festivalSlug?: string;
editionSlug?: string;
setId?: string;
setSlug?: string;
}>();

const shouldNotRedirect = !isMainGetuplineDomain();
Expand All @@ -32,8 +32,8 @@ export function SubdomainRedirect({
// Build the target path based on current route
let targetPath = "/";

if (editionSlug && setId) {
targetPath = `/editions/${editionSlug}/sets/${setId}`;
if (editionSlug && setSlug) {
targetPath = `/editions/${editionSlug}/sets/${setSlug}`;
} else if (editionSlug && window.location.pathname.includes("schedule")) {
targetPath = `/editions/${editionSlug}/schedule`;
} else if (editionSlug) {
Expand All @@ -43,7 +43,7 @@ export function SubdomainRedirect({
// Redirect to subdomain
const subdomainUrl = createFestivalSubdomainUrl(festivalSlug, targetPath);
window.location.href = subdomainUrl;
}, [festivalSlug, editionSlug, setId, shouldNotRedirect]);
}, [festivalSlug, editionSlug, setSlug, shouldNotRedirect]);

if (shouldNotRedirect) {
return <Component />;
Expand Down
2 changes: 2 additions & 0 deletions src/hooks/queries/groups/useCreateGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useToast } from "@/hooks/use-toast";
import { supabase } from "@/integrations/supabase/client";
import { userGroupsKeys } from "./useUserGroups";
import { generateSlug } from "@/lib/slug";

// Mutation function
async function createGroup(variables: {
Expand All @@ -15,6 +16,7 @@ async function createGroup(variables: {
.from("groups")
.insert({
name,
slug: generateSlug(name),
description,
created_by: userId,
})
Expand Down
68 changes: 68 additions & 0 deletions src/hooks/queries/groups/useGroupBySlug.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { useQuery } from "@tanstack/react-query";
import { supabase } from "@/integrations/supabase/client";
import type { Group } from "@/types/groups";

// Query key factory
export const groupBySlugKeys = {
all: ["groups"] as const,
bySlug: () => [...groupBySlugKeys.all, "by-slug"] as const,
detail: (slug: string, userId: string) =>
[...groupBySlugKeys.bySlug(), slug, userId] as const,
};

interface UseGroupBySlugParams {
slug?: string;
userId?: string;
}

async function fetchGroupBySlug(slug: string, userId: string): Promise<Group> {
// First, try to find the group where user is a member
const { data: membership, error: membershipError } = await supabase
.from("group_members")
.select(
`
group_id,
groups!inner (
id,
name,
slug,
description,
created_by,
archived,
created_at,
updated_at
)
`,
)
.eq("user_id", userId)
.eq("groups.slug", slug)
.eq("groups.archived", false)
.single();

if (!membershipError && membership) {
return membership.groups as Group;
}

// If not found as a member, check if user is the creator
const { data, error } = await supabase
.from("groups")
.select("*")
.eq("slug", slug)
.eq("created_by", userId)
.eq("archived", false)
.single();

if (error) {
throw new Error("Group not found or you don't have access");
}

return data;
}

export function useGroupBySlugQuery({ slug, userId }: UseGroupBySlugParams) {
return useQuery({
queryKey: groupBySlugKeys.detail(slug!, userId!),
queryFn: () => fetchGroupBySlug(slug!, userId!),
enabled: !!slug && !!userId,
});
}
6 changes: 5 additions & 1 deletion src/hooks/queries/stages/useCreateStage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useToast } from "@/hooks/use-toast";
import { supabase } from "@/integrations/supabase/client";
import { stagesKeys } from "./types";
import { generateSlug } from "@/lib/slug";

async function createStage(stageData: {
name: string;
festival_edition_id: string;
}) {
const { data, error } = await supabase
.from("stages")
.insert(stageData)
.insert({
...stageData,
slug: generateSlug(stageData.name),
})
.select()
.single();

Expand Down
6 changes: 6 additions & 0 deletions src/integrations/supabase/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,7 @@ export type Database = {
description: string | null;
id: string;
name: string;
slug: string;
updated_at: string;
};
Insert: {
Expand All @@ -401,6 +402,7 @@ export type Database = {
description?: string | null;
id?: string;
name: string;
slug: string;
updated_at?: string;
};
Update: {
Expand All @@ -410,6 +412,7 @@ export type Database = {
description?: string | null;
id?: string;
name?: string;
slug?: string;
updated_at?: string;
};
Relationships: [];
Expand Down Expand Up @@ -565,6 +568,7 @@ export type Database = {
festival_edition_id: string;
id: string;
name: string;
slug: string;
updated_at: string;
};
Insert: {
Expand All @@ -573,6 +577,7 @@ export type Database = {
festival_edition_id: string;
id?: string;
name: string;
slug: string;
updated_at?: string;
};
Update: {
Expand All @@ -581,6 +586,7 @@ export type Database = {
festival_edition_id?: string;
id?: string;
name?: string;
slug?: string;
updated_at?: string;
};
Relationships: [];
Expand Down
14 changes: 7 additions & 7 deletions src/pages/admin/festivals/AdminFestivals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ export default function AdminFestivals() {
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);

const navigate = useNavigate();
const { festivalId = "" } = useParams<{
festivalId?: string;
const { festivalSlug = "" } = useParams<{
festivalSlug?: string;
}>();

function handleFestivalChange(festivalId: string) {
if (festivalId === "none") {
function handleFestivalChange(festivalSlug: string) {
if (festivalSlug === "none") {
navigate("/admin/festivals");
} else {
navigate(`/admin/festivals/${festivalId}`);
navigate(`/admin/festivals/${festivalSlug}`);
}
}

Expand Down Expand Up @@ -50,9 +50,9 @@ export default function AdminFestivals() {
setIsEditDialogOpen(true);
}}
onSelect={(festival) => {
handleFestivalChange(festival.id);
handleFestivalChange(festival.slug);
}}
selected={festivalId}
selected={festivalSlug}
/>
</CardContent>
</Card>
Expand Down
26 changes: 13 additions & 13 deletions src/pages/admin/festivals/FestivalDetail.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import { useParams, useNavigate, Outlet } from "react-router-dom";
import { FestivalEditionManagement } from "./FestivalEditionManagement";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { useFestivalQuery } from "@/hooks/queries/festivals/useFestival";
import { useFestivalBySlugQuery } from "@/hooks/queries/festivals/useFestivalBySlug";
import { Loader2 } from "lucide-react";

export default function FestivalDetail() {
const { festivalId, editionId = "" } = useParams<{
festivalId: string;
editionId?: string;
const { festivalSlug, editionSlug = "" } = useParams<{
festivalSlug: string;
editionSlug?: string;
}>();
// const [selectedEditionId, setSelectedEditionId] = useState("");
const navigate = useNavigate();

const festivalQuery = useFestivalQuery(festivalId);
const festivalQuery = useFestivalBySlugQuery(festivalSlug);

if (!festivalId) {
return <div>festivalId param is missing</div>;
if (!festivalSlug) {
return <div>festivalSlug param is missing</div>;
}

if (festivalQuery.isLoading) {
Expand All @@ -39,8 +39,8 @@ export default function FestivalDetail() {
);
}

function handleEditionSelect(editionId: string) {
navigate(`/admin/festivals/${festivalId}/editions/${editionId}/stages`);
function handleEditionSelect(editionSlug: string) {
navigate(`/admin/festivals/${festivalSlug}/editions/${editionSlug}/stages`);
}

const festival = festivalQuery.data;
Expand All @@ -58,11 +58,11 @@ export default function FestivalDetail() {

<div className="mt-6">
<FestivalEditionManagement
festivalId={festivalId}
onSelect={(editionId) => {
handleEditionSelect(editionId);
festivalSlug={festivalSlug}
onSelect={(editionSlug) => {
handleEditionSelect(editionSlug);
}}
selected={editionId}
selected={editionSlug}
/>
</div>
</>
Expand Down
41 changes: 22 additions & 19 deletions src/pages/admin/festivals/FestivalEdition.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,40 @@ import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Loader2, MapPin, Music } from "lucide-react";
import { useEffect } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { useFestivalEditionsForFestivalQuery } from "@/hooks/queries/festivals/editions/useFestivalEditionsForFestival";
import { useFestivalEditionBySlugQuery } from "@/hooks/queries/festivals/editions/useFestivalEditionBySlug";

export default function FestivalEdition() {
const { festivalId, editionId } = useParams<{
festivalId: string;
editionId: string;
const { festivalSlug, editionSlug } = useParams<{
festivalSlug: string;
editionSlug: string;
}>();
const location = useLocation();
const navigate = useNavigate();

// Redirect to stages if we're at the index route
useEffect(() => {
if (
festivalId &&
editionId &&
festivalSlug &&
editionSlug &&
location.pathname ===
`/admin/festivals/${festivalId}/editions/${editionId}`
`/admin/festivals/${festivalSlug}/editions/${editionSlug}`
) {
navigate(`/admin/festivals/${festivalId}/editions/${editionId}/stages`);
navigate(
`/admin/festivals/${festivalSlug}/editions/${editionSlug}/stages`,
);
}
}, [location.pathname, festivalId, editionId, navigate]);
}, [location.pathname, festivalSlug, editionSlug, navigate]);

const editionsQuery = useFestivalEditionsForFestivalQuery(festivalId);
const editionQuery = useFestivalEditionBySlugQuery({
festivalSlug,
editionSlug,
});

if (!festivalId || !editionId) {
if (!festivalSlug || !editionSlug) {
return <div>Festival or edition not found</div>;
}

if (editionsQuery.isLoading) {
return <Card>Loading edition data</Card>;
}

if (editionsQuery.isLoading) {
if (editionQuery.isLoading) {
return (
<Card>
<CardContent className="flex items-center justify-center p-8">
Expand All @@ -46,7 +47,7 @@ export default function FestivalEdition() {
);
}

const currentEdition = editionsQuery.data?.find((f) => f.id === editionId);
const currentEdition = editionQuery.data;

if (!currentEdition) {
return (
Expand All @@ -68,7 +69,9 @@ export default function FestivalEdition() {
const currentSubTab = getCurrentSubTab();

function handleSubTabChange(value: string) {
navigate(`/admin/festivals/${festivalId}/editions/${editionId}/${value}`);
navigate(
`/admin/festivals/${festivalSlug}/editions/${editionSlug}/${value}`,
);
}

return (
Expand Down Expand Up @@ -105,7 +108,7 @@ export default function FestivalEdition() {
</TabsList>

<div className="mt-6">
<Outlet />
<Outlet context={{ edition: currentEdition }} />
</div>
</Tabs>
</div>
Expand Down
Loading
Loading