1- import { useCallback , useState } from "react" ;
1+ import { useCallback , useMemo , useState } from "react" ;
22
3- import Link from "next/link" ;
43import { useRouter } from "next/router" ;
54
65import { PageWithBanner } from "@/common/ui/layout/components" ;
76import { TabOption } from "@/common/ui/layout/types" ;
87import { cn } from "@/common/ui/layout/utils" ;
9- import { CampaignBanner } from "@/entities/campaign" ;
8+ import { CampaignBanner , CampaignDonorsTable , CampaignSettings } from "@/entities/campaign" ;
109
11- const CAMPAIGN_TAB_ROUTES : TabOption [ ] = [
10+ const CAMPAIGN_TABS : { label : string ; id : string } [ ] = [
1211 {
1312 label : "Donation History" ,
1413 id : "leaderboard" ,
15- href : "/leaderboard" ,
1614 } ,
17- { label : "Settings" , id : "settings" , href : "/settings" } ,
15+ { label : "Settings" , id : "settings" } ,
1816] ;
1917
20- type Props = {
21- options : TabOption [ ] ;
18+ type TabsProps = {
19+ options : { label : string ; id : string } [ ] ;
2220 selectedTab : string ;
23- onSelect ?: ( tabId : string ) => void ;
24- asLink ?: boolean ;
21+ onSelect : ( tabId : string ) => void ;
2522} ;
2623
27- const Tabs = ( { options, selectedTab, onSelect, asLink } : Props ) => {
28- const _selectedTab = selectedTab || options [ 0 ] . id ;
29-
30- const router = useRouter ( ) ;
31- const { campaignId : campaignIdParam } = router . query ;
32-
33- const campaignId = typeof campaignIdParam === "string" ? campaignIdParam : campaignIdParam ?. at ( 0 ) ;
34-
24+ const Tabs = ( { options, selectedTab, onSelect } : TabsProps ) => {
3525 return (
3626 < div className = "mb-8 flex w-full flex-row flex-wrap gap-2" >
3727 < div className = "w-full px-2 md:px-8" >
@@ -42,35 +32,13 @@ const Tabs = ({ options, selectedTab, onSelect, asLink }: Props) => {
4232 ) }
4333 >
4434 { options . map ( ( option ) => {
45- const selected = option . id == _selectedTab ;
46-
47- if ( asLink ) {
48- return (
49- < Link
50- href = { `/campaign/${ campaignId } ${ option . href } ` }
51- prefetch
52- key = { option . id }
53- className = { `font-500 border-b-solid transition-duration-300 whitespace-nowrap border-b-[2px] px-4 py-[10px] text-sm text-[#7b7b7b] transition-all hover:border-b-[#292929] hover:text-[#292929] ${ selected ? "border-b-[#292929] text-[#292929]" : "border-b-[transparent]" } ` }
54- onClick = { ( ) => {
55- if ( onSelect ) {
56- onSelect ( option . id ) ;
57- }
58- } }
59- >
60- { option . label }
61- </ Link >
62- ) ;
63- }
35+ const selected = option . id === selectedTab ;
6436
6537 return (
6638 < button
6739 key = { option . id }
6840 className = { `font-500 border-b-solid transition-duration-300 whitespace-nowrap border-b-[2px] px-4 py-[10px] text-sm text-[#7b7b7b] transition-all hover:border-b-[#292929] hover:text-[#292929] ${ selected ? "border-b-[#292929] text-[#292929]" : "border-b-[transparent]" } ` }
69- onClick = { ( ) => {
70- if ( onSelect ) {
71- onSelect ( option . id ) ;
72- }
73- } }
41+ onClick = { ( ) => onSelect ( option . id ) }
7442 >
7543 { option . label }
7644 </ button >
@@ -88,31 +56,63 @@ type ReactLayoutProps = {
8856
8957export const CampaignLayout : React . FC < ReactLayoutProps > = ( { children } ) => {
9058 const router = useRouter ( ) ;
91- const { campaignId } = router . query as { campaignId : string } ;
92- const tabs = CAMPAIGN_TAB_ROUTES ;
59+ const { campaignId, tab } = router . query as { campaignId : string ; tab ?: string } ;
9360
94- const [ selectedTab , setSelectedTab ] = useState (
95- tabs . find ( ( tab ) => router . pathname . includes ( tab . href ) ) || tabs [ 0 ] ,
96- ) ;
61+ // Derive active tab directly from URL - no state needed
62+ const activeTab = useMemo ( ( ) => {
63+ if ( tab && CAMPAIGN_TABS . find ( ( t ) => t . id === tab ) ) {
64+ return tab ;
65+ }
66+
67+ return CAMPAIGN_TABS [ 0 ] . id ;
68+ } , [ tab ] ) ;
69+
70+ // Track if user has manually changed tabs (to prevent URL sync issues)
71+ const [ userSelectedTab , setUserSelectedTab ] = useState < string | null > ( null ) ;
72+
73+ // Use userSelectedTab if set, otherwise use URL-derived activeTab
74+ const currentTab = userSelectedTab ?? activeTab ;
9775
98- const handleSelectedTab = useCallback (
99- ( tabId : string ) => setSelectedTab ( tabs . find ( ( tabRoute ) => tabRoute . id === tabId ) ! ) ,
100- [ tabs ] ,
76+ const handleTabChange = useCallback (
77+ ( tabId : string ) => {
78+ if ( tabId === currentTab ) return ;
79+
80+ setUserSelectedTab ( tabId ) ;
81+
82+ // Update URL without triggering Next.js navigation
83+ const newUrl = `/campaign/${ campaignId } ?tab=${ tabId } ` ;
84+
85+ window . history . replaceState ( { ...window . history . state , as : newUrl , url : newUrl } , "" , newUrl ) ;
86+ } ,
87+ [ campaignId , currentTab ] ,
10188 ) ;
10289
90+ const numericCampaignId = parseInt ( campaignId || "0" , 10 ) ;
91+
92+ // Render content based on current tab
93+ const renderTabContent = ( ) => {
94+ if ( currentTab === "settings" ) {
95+ return < CampaignSettings campaignId = { numericCampaignId } /> ;
96+ }
97+
98+ return < CampaignDonorsTable campaignId = { numericCampaignId } /> ;
99+ } ;
100+
101+ // Don't render until we have a campaignId
102+ if ( ! campaignId ) {
103+ return null ;
104+ }
105+
103106 return (
104107 < PageWithBanner >
105108 < div className = "md:p-8" >
106- < CampaignBanner campaignId = { parseInt ( campaignId ) } />
109+ < CampaignBanner campaignId = { numericCampaignId } />
107110 </ div >
108111
109- < Tabs
110- asLink
111- options = { tabs }
112- selectedTab = { selectedTab . id }
113- onSelect = { ( tabId : string ) => handleSelectedTab ( tabId ) }
114- />
115- < div className = "flex w-full flex-row flex-wrap gap-2 md:px-8" > { children } </ div >
112+ < Tabs options = { CAMPAIGN_TABS } selectedTab = { currentTab } onSelect = { handleTabChange } />
113+ < div className = "flex w-full flex-row flex-wrap gap-2 md:px-8" > { renderTabContent ( ) } </ div >
116114 </ PageWithBanner >
117115 ) ;
118116} ;
117+
118+ export { CAMPAIGN_TABS } ;
0 commit comments