swipeable tabs use url
This commit is contained in:
@@ -2,8 +2,14 @@ import Page from "@/components/page";
|
|||||||
import Profile from "@/features/players/components/profile";
|
import Profile from "@/features/players/components/profile";
|
||||||
import { playerQueries } from "@/features/players/queries";
|
import { playerQueries } from "@/features/players/queries";
|
||||||
import { redirect, createFileRoute } from "@tanstack/react-router";
|
import { redirect, createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const searchSchema = z.object({
|
||||||
|
tab: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
export const Route = createFileRoute("/_authed/profile/$playerId")({
|
export const Route = createFileRoute("/_authed/profile/$playerId")({
|
||||||
|
validateSearch: searchSchema,
|
||||||
beforeLoad: async ({ params, context }) => {
|
beforeLoad: async ({ params, context }) => {
|
||||||
const { queryClient } = context;
|
const { queryClient } = context;
|
||||||
const player = await queryClient.ensureQueryData(playerQueries.details(params.playerId))
|
const player = await queryClient.ensureQueryData(playerQueries.details(params.playerId))
|
||||||
|
|||||||
@@ -2,8 +2,14 @@ import Page from "@/components/page";
|
|||||||
import TeamProfile from "@/features/teams/components/team-profile";
|
import TeamProfile from "@/features/teams/components/team-profile";
|
||||||
import { teamQueries } from "@/features/teams/queries";
|
import { teamQueries } from "@/features/teams/queries";
|
||||||
import { redirect, createFileRoute } from "@tanstack/react-router";
|
import { redirect, createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const searchSchema = z.object({
|
||||||
|
tab: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
export const Route = createFileRoute("/_authed/teams/$teamId")({
|
export const Route = createFileRoute("/_authed/teams/$teamId")({
|
||||||
|
validateSearch: searchSchema,
|
||||||
beforeLoad: async ({ params, context }) => {
|
beforeLoad: async ({ params, context }) => {
|
||||||
const { queryClient } = context;
|
const { queryClient } = context;
|
||||||
const team = await queryClient.ensureQueryData(teamQueries.details(params.teamId))
|
const team = await queryClient.ensureQueryData(teamQueries.details(params.teamId))
|
||||||
|
|||||||
@@ -10,8 +10,14 @@ import TeamList from '@/features/teams/components/team-list';
|
|||||||
import Button from '@/components/button';
|
import Button from '@/components/button';
|
||||||
import Avatar from '@/components/avatar';
|
import Avatar from '@/components/avatar';
|
||||||
import Profile from '@/features/tournaments/components/profile';
|
import Profile from '@/features/tournaments/components/profile';
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const searchSchema = z.object({
|
||||||
|
tab: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
export const Route = createFileRoute('/_authed/tournaments/$tournamentId')({
|
export const Route = createFileRoute('/_authed/tournaments/$tournamentId')({
|
||||||
|
validateSearch: searchSchema,
|
||||||
beforeLoad: async ({ context, params }) => {
|
beforeLoad: async ({ context, params }) => {
|
||||||
const { queryClient } = context;
|
const { queryClient } = context;
|
||||||
await queryClient.ensureQueryData(tournamentQueries.details(params.tournamentId))
|
await queryClient.ensureQueryData(tournamentQueries.details(params.tournamentId))
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { FloatingIndicator, UnstyledButton, Box, Text, ScrollArea } from "@mantine/core";
|
import { FloatingIndicator, UnstyledButton, Box, Text, ScrollArea } from "@mantine/core";
|
||||||
import { Carousel } from "@mantine/carousel";
|
import { Carousel } from "@mantine/carousel";
|
||||||
import { useState, useEffect, ReactNode, useRef } from "react";
|
import { useState, useEffect, ReactNode, useRef } from "react";
|
||||||
|
import { useRouter } from "@tanstack/react-router";
|
||||||
|
|
||||||
interface TabItem {
|
interface TabItem {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -15,7 +16,28 @@ interface SwipeableTabsProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function SwipeableTabs({ tabs, defaultTab = 0, onTabChange, scrollPosition }: SwipeableTabsProps) {
|
function SwipeableTabs({ tabs, defaultTab = 0, onTabChange, scrollPosition }: SwipeableTabsProps) {
|
||||||
const [activeTab, setActiveTab] = useState(defaultTab);
|
const router = useRouter();
|
||||||
|
const search = router.state.location.search as any;
|
||||||
|
|
||||||
|
// manually update url tab w/o useNavigate
|
||||||
|
const updateUrlTab = (tabLabel: string) => {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
url.searchParams.set('tab', tabLabel);
|
||||||
|
window.history.replaceState(null, '', url.toString());
|
||||||
|
};
|
||||||
|
|
||||||
|
const getInitialTab = () => {
|
||||||
|
const urlTab = search?.tab;
|
||||||
|
if (typeof urlTab === 'string') {
|
||||||
|
const tabIndex = tabs.findIndex(tab => tab.label.toLowerCase() === urlTab.toLowerCase());
|
||||||
|
return tabIndex !== -1 ? tabIndex : defaultTab;
|
||||||
|
}
|
||||||
|
return defaultTab;
|
||||||
|
};
|
||||||
|
|
||||||
|
const [activeTab, setActiveTab] = useState(getInitialTab);
|
||||||
const [embla, setEmbla] = useState<any>(null);
|
const [embla, setEmbla] = useState<any>(null);
|
||||||
const [rootRef, setRootRef] = useState<HTMLDivElement | null>(null);
|
const [rootRef, setRootRef] = useState<HTMLDivElement | null>(null);
|
||||||
const [controlsRefs, setControlsRefs] = useState<Record<number, HTMLSpanElement | null>>({});
|
const [controlsRefs, setControlsRefs] = useState<Record<number, HTMLSpanElement | null>>({});
|
||||||
@@ -33,12 +55,18 @@ function SwipeableTabs({ tabs, defaultTab = 0, onTabChange, scrollPosition }: Sw
|
|||||||
const newIndex = embla.selectedScrollSnap();
|
const newIndex = embla.selectedScrollSnap();
|
||||||
setActiveTab(newIndex);
|
setActiveTab(newIndex);
|
||||||
|
|
||||||
// Update height based on active slide content
|
|
||||||
const activeSlideRef = slideRefs.current[newIndex];
|
const activeSlideRef = slideRefs.current[newIndex];
|
||||||
if (activeSlideRef) {
|
if (activeSlideRef) {
|
||||||
const height = activeSlideRef.scrollHeight;
|
const height = activeSlideRef.scrollHeight;
|
||||||
setActiveSlideHeight(height);
|
setActiveSlideHeight(height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tabLabel = tabs[newIndex]?.label.toLowerCase();
|
||||||
|
if (tabLabel) {
|
||||||
|
updateUrlTab(tabLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
onTabChange?.(newIndex, tabs[newIndex]);
|
||||||
};
|
};
|
||||||
|
|
||||||
embla.on("select", onSelect);
|
embla.on("select", onSelect);
|
||||||
@@ -48,7 +76,6 @@ function SwipeableTabs({ tabs, defaultTab = 0, onTabChange, scrollPosition }: Sw
|
|||||||
};
|
};
|
||||||
}, [embla]);
|
}, [embla]);
|
||||||
|
|
||||||
// Update height when activeTab changes
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const activeSlideRef = slideRefs.current[activeTab];
|
const activeSlideRef = slideRefs.current[activeTab];
|
||||||
if (activeSlideRef) {
|
if (activeSlideRef) {
|
||||||
@@ -57,6 +84,17 @@ function SwipeableTabs({ tabs, defaultTab = 0, onTabChange, scrollPosition }: Sw
|
|||||||
}
|
}
|
||||||
}, [activeTab]);
|
}, [activeTab]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const urlTab = search?.tab;
|
||||||
|
if (typeof urlTab === 'string') {
|
||||||
|
const tabIndex = tabs.findIndex(tab => tab.label.toLowerCase() === urlTab.toLowerCase());
|
||||||
|
if (tabIndex !== -1 && tabIndex !== activeTab) {
|
||||||
|
setActiveTab(tabIndex);
|
||||||
|
embla?.scrollTo(tabIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [search?.tab, tabs, activeTab, embla]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (scrollPosition) {
|
if (scrollPosition) {
|
||||||
setIsSticky(scrollPosition.y > stickyThreshold);
|
setIsSticky(scrollPosition.y > stickyThreshold);
|
||||||
@@ -96,6 +134,9 @@ function SwipeableTabs({ tabs, defaultTab = 0, onTabChange, scrollPosition }: Sw
|
|||||||
setActiveTab(index);
|
setActiveTab(index);
|
||||||
embla?.scrollTo(index);
|
embla?.scrollTo(index);
|
||||||
onTabChange?.(index, tabs[index]);
|
onTabChange?.(index, tabs[index]);
|
||||||
|
|
||||||
|
const tabLabel = tabs[index].label.toLowerCase();
|
||||||
|
updateUrlTab(tabLabel);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user