1- import { useCallback , useMemo , useState } from "react" ;
1+ import { useCallback , useState } from "react" ;
22
3+ import Link from "next/link" ;
34import { useRouter } from "next/router" ;
45
56import { PageWithBanner } from "@/common/ui/layout/components" ;
67import { TabOption } from "@/common/ui/layout/types" ;
78import { cn } from "@/common/ui/layout/utils" ;
8- import { CampaignBanner , CampaignDonorsTable , CampaignSettings } from "@/entities/campaign" ;
9+ import { CampaignBanner } from "@/entities/campaign" ;
910
10- const CAMPAIGN_TABS : { label : string ; id : string } [ ] = [
11+ const CAMPAIGN_TAB_ROUTES : TabOption [ ] = [
1112 {
1213 label : "Donation History" ,
1314 id : "leaderboard" ,
15+ href : "/leaderboard" ,
1416 } ,
15- { label : "Settings" , id : "settings" } ,
17+ { label : "Settings" , id : "settings" , href : "/settings" } ,
1618] ;
1719
18- type TabsProps = {
19- options : { label : string ; id : string } [ ] ;
20+ type Props = {
21+ options : TabOption [ ] ;
2022 selectedTab : string ;
21- onSelect : ( tabId : string ) => void ;
23+ onSelect ?: ( tabId : string ) => void ;
24+ asLink ?: boolean ;
2225} ;
2326
24- const Tabs = ( { options, selectedTab, onSelect } : TabsProps ) => {
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+
2535 return (
2636 < div className = "mb-8 flex w-full flex-row flex-wrap gap-2" >
2737 < div className = "w-full px-2 md:px-8" >
@@ -32,13 +42,35 @@ const Tabs = ({ options, selectedTab, onSelect }: TabsProps) => {
3242 ) }
3343 >
3444 { options . map ( ( option ) => {
35- const selected = option . id === selectedTab ;
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+ }
3664
3765 return (
3866 < button
3967 key = { option . id }
4068 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]" } ` }
41- onClick = { ( ) => onSelect ( option . id ) }
69+ onClick = { ( ) => {
70+ if ( onSelect ) {
71+ onSelect ( option . id ) ;
72+ }
73+ } }
4274 >
4375 { option . label }
4476 </ button >
@@ -56,63 +88,31 @@ type ReactLayoutProps = {
5688
5789export const CampaignLayout : React . FC < ReactLayoutProps > = ( { children } ) => {
5890 const router = useRouter ( ) ;
59- const { campaignId, tab } = router . query as { campaignId : string ; tab ?: string } ;
60-
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 ;
91+ const { campaignId } = router . query as { campaignId : string } ;
92+ const tabs = CAMPAIGN_TAB_ROUTES ;
7593
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 ] ,
94+ const [ selectedTab , setSelectedTab ] = useState (
95+ tabs . find ( ( tab ) => router . pathname . includes ( tab . href ) ) || tabs [ 0 ] ,
8896 ) ;
8997
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- }
98+ const handleSelectedTab = useCallback (
99+ ( tabId : string ) => setSelectedTab ( tabs . find ( ( tabRoute ) => tabRoute . id === tabId ) ! ) ,
100+ [ tabs ] ,
101+ ) ;
105102
106103 return (
107104 < PageWithBanner >
108105 < div className = "md:p-8" >
109- < CampaignBanner campaignId = { numericCampaignId } />
106+ < CampaignBanner campaignId = { parseInt ( campaignId ) } />
110107 </ div >
111108
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 >
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 >
114116 </ PageWithBanner >
115117 ) ;
116118} ;
117-
118- export { CAMPAIGN_TABS } ;
0 commit comments