diff --git a/package.json b/package.json index 2fa96c0..629f331 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ }, "dependencies": { "@hello-pangea/dnd": "^18.0.1", + "@mantine/carousel": "^8.2.4", "@mantine/charts": "^8.2.4", "@mantine/core": "^8.2.4", "@mantine/dates": "^8.2.4", @@ -28,6 +29,7 @@ "@types/ioredis": "^4.28.10", "drizzle-orm": "^0.44.4", "drizzle-zod": "^0.8.3", + "embla-carousel-react": "^8.6.0", "framer-motion": "^12.23.12", "ioredis": "^5.7.0", "pg": "^8.16.3", diff --git a/src/app/routes/__root.tsx b/src/app/routes/__root.tsx index 26cd8b3..f6a9f0b 100644 --- a/src/app/routes/__root.tsx +++ b/src/app/routes/__root.tsx @@ -1,5 +1,6 @@ import '@mantine/core/styles.css'; import '@mantine/dates/styles.css'; +import '@mantine/carousel/styles.css'; import { HeadContent, Navigate, diff --git a/src/app/routes/_authed/tournaments/$tournamentId.tsx b/src/app/routes/_authed/tournaments/$tournamentId.tsx index 05cff52..687b0ce 100644 --- a/src/app/routes/_authed/tournaments/$tournamentId.tsx +++ b/src/app/routes/_authed/tournaments/$tournamentId.tsx @@ -17,7 +17,7 @@ export const Route = createFileRoute('/_authed/tournaments/$tournamentId')({ header: { collapsed: true, withBackButton: true, - settingsLink: context.auth.roles.includes("Admin") ? `/admin/tournaments/${params.tournamentId}` : undefined + settingsLink: context.auth.roles?.includes("Admin") ? `/admin/tournaments/${params.tournamentId}` : undefined }, refresh: { toRefresh: tournamentQueries.details(params.tournamentId).queryKey, diff --git a/src/components/swipeable-tabs.tsx b/src/components/swipeable-tabs.tsx new file mode 100644 index 0000000..602bde4 --- /dev/null +++ b/src/components/swipeable-tabs.tsx @@ -0,0 +1,127 @@ +import { FloatingIndicator, UnstyledButton, Box, Text } from "@mantine/core"; +import { Carousel } from "@mantine/carousel"; +import { useState, useEffect, ReactNode } from "react"; + +interface TabItem { + label: string; + content: ReactNode; +} + +interface SwipeableTabsProps { + tabs: TabItem[]; + defaultTab?: number; + onTabChange?: (index: number, tab: TabItem) => void; +} + +function SwipeableTabs({ tabs, defaultTab = 0, onTabChange }: SwipeableTabsProps) { + const [activeTab, setActiveTab] = useState(defaultTab); + const [embla, setEmbla] = useState(null); + const [rootRef, setRootRef] = useState(null); + const [controlsRefs, setControlsRefs] = useState>({}); + + useEffect(() => { + if (!embla) return; + + const onSelect = () => { + const newIndex = embla.selectedScrollSnap(); + setActiveTab(newIndex); + }; + + embla.on("select", onSelect); + + return () => { + embla.off("select", onSelect); + }; + }, [embla]); + + const handleTabChange = (index: number) => { + if (index !== activeTab && index >= 0 && index < tabs.length) { + setActiveTab(index); + embla?.scrollTo(index); + onTabChange?.(index, tabs[index]); + } + }; + + const setControlRef = (index: number) => (node: HTMLSpanElement | null) => { + controlsRefs[index] = node; + setControlsRefs(controlsRefs); + }; + + return ( + + + + {tabs.map((tab, index) => ( + handleTabChange(index)} + style={{ + flex: 1, + padding: 'var(--mantine-spacing-sm) var(--mantine-spacing-md)', + textAlign: 'center', + color: activeTab === index + ? 'var(--mantine-color-blue-6)' + : 'var(--mantine-color-text)', + fontWeight: activeTab === index ? 600 : 400, + transition: 'color 200ms ease, font-weight 200ms ease', + backgroundColor: 'transparent', + border: 'none', + borderRadius: 0, + + }} + > + + {tab.label} + + + ))} + + + + {tabs.map((tab, index) => ( + + + {tab.content} + + + ))} + + + ); +} + +export default SwipeableTabs; \ No newline at end of file diff --git a/src/features/players/components/profile/index.tsx b/src/features/players/components/profile/index.tsx index d36aa69..e7acc85 100644 --- a/src/features/players/components/profile/index.tsx +++ b/src/features/players/components/profile/index.tsx @@ -3,18 +3,38 @@ import Header from "./header"; import { testEvent } from "@/utils/test-event"; import { Player } from "@/features/players/types"; import TeamList from "@/features/teams/components/team-list"; +import SwipeableTabs from "@/components/swipeable-tabs"; interface ProfileProps { player: Player; } const Profile = ({ player }: ProfileProps) => { + const tabs = [ + { + label: "Overview", + content: Panel 1 content + }, + { + label: "Teams", + content: Panel 2 content + }, + { + label: "Stats", + content: Panel 3 content + } + ]; return <>
- Teams - + { + console.log(`Switched to ${tab.label} tab`); + }} + /> ; }; diff --git a/src/utils/supertokens.ts b/src/utils/supertokens.ts index d6a4f05..f0cbcd1 100644 --- a/src/utils/supertokens.ts +++ b/src/utils/supertokens.ts @@ -47,7 +47,6 @@ export const superTokensRequestMiddleware = createMiddleware({ type: 'request' } if (!session.context.userAuthId) { logger.error('Unauthenticated user in API call.', session.context) - throw new Error('Unauthenticated') } const context = { @@ -66,7 +65,6 @@ export const superTokensFunctionMiddleware = createMiddleware({ type: 'function' if (!session.context.userAuthId) { logger.error('Unauthenticated user in server function.', session.context) - throw new Error('Unauthenticated') } const context = {