11import { ReactElement , useMemo } from "react" ;
22
3- import type { GetStaticPaths , GetStaticProps } from "next" ;
43import { useRouter } from "next/router" ;
54
6- import { CAMPAIGNS_CONTRACT_ACCOUNT_ID , NETWORK } from "@/common/_config" ;
75import { APP_METADATA } from "@/common/constants" ;
8- import type { Campaign as ContractCampaign } from "@/common/contracts/core/campaigns/interfaces" ;
96import { CampaignDonorsTable , CampaignSettings } from "@/entities/campaign" ;
107import { CampaignLayout } from "@/layout/campaign/components/layout" ;
118import { RootLayout } from "@/layout/components/root-layout" ;
129
13- type SeoProps = {
14- seoTitle : string ;
15- seoDescription : string ;
16- seoImage ?: string ;
17- } ;
18-
19- type CampaignPageProps = {
20- seo : SeoProps ;
21- campaignId : number ;
22- } ;
23-
24- export default function CampaignPage ( { seo, campaignId } : CampaignPageProps ) {
10+ export default function CampaignPage ( ) {
2511 const router = useRouter ( ) ;
26- const { tab } = router . query as { tab ?: string } ;
12+ const { tab, campaignId } = router . query as { tab ?: string ; campaignId ?: string } ;
2713
28- const parsedCampaignId = campaignId ;
14+ const parsedCampaignId = campaignId ? parseInt ( campaignId ) : undefined ;
2915
3016 // Determine which content to show based on tab param
3117 const content = useMemo ( ( ) => {
18+ if ( ! parsedCampaignId || Number . isNaN ( parsedCampaignId ) ) {
19+ return null ;
20+ }
21+
3222 switch ( tab ) {
3323 case "settings" :
3424 return < CampaignSettings campaignId = { parsedCampaignId } /> ;
@@ -39,7 +29,11 @@ export default function CampaignPage({ seo, campaignId }: CampaignPageProps) {
3929 } , [ tab , parsedCampaignId ] ) ;
4030
4131 return (
42- < RootLayout title = { seo . seoTitle } description = { seo . seoDescription } image = { seo . seoImage } >
32+ < RootLayout
33+ title = { `Campaign ${ parsedCampaignId } ` }
34+ description = { APP_METADATA . description }
35+ image = { APP_METADATA . openGraph . images . url }
36+ >
4337 { content }
4438 </ RootLayout >
4539 ) ;
@@ -48,126 +42,3 @@ export default function CampaignPage({ seo, campaignId }: CampaignPageProps) {
4842CampaignPage . getLayout = function getLayout ( page : ReactElement ) {
4943 return < CampaignLayout > { page } </ CampaignLayout > ;
5044} ;
51-
52- export const getStaticPaths : GetStaticPaths = async ( ) => {
53- return {
54- paths : [ ] ,
55- fallback : "blocking" ,
56- } ;
57- } ;
58-
59- export const getStaticProps : GetStaticProps < CampaignPageProps > = async ( context ) => {
60- const { campaignId } = context . params as { campaignId : string } ;
61-
62- const parsedCampaignId = parseInt ( campaignId ) ;
63-
64- if ( Number . isNaN ( parsedCampaignId ) || parsedCampaignId <= 0 ) {
65- return {
66- notFound : true ,
67- } ;
68- }
69-
70- // Fallback SEO in case of error
71- const defaultSeo : SeoProps = {
72- seoTitle : `Campaign ${ campaignId } | Potlock` ,
73- seoDescription : APP_METADATA . description ,
74- seoImage : APP_METADATA . openGraph . images . url ,
75- } ;
76-
77- try {
78- const rpcUrl =
79- NETWORK === "mainnet" ? "https://free.rpc.fastnear.com" : "https://test.rpc.fastnear.com" ;
80-
81- const controller = new AbortController ( ) ;
82- const timeoutMs = 5000 ;
83- const timeoutId = setTimeout ( ( ) => controller . abort ( ) , timeoutMs ) ;
84-
85- let campaign : ContractCampaign | null | undefined ;
86-
87- try {
88- const response = await fetch ( rpcUrl , {
89- method : "POST" ,
90- headers : { "content-type" : "application/json" } ,
91- signal : controller . signal ,
92- body : JSON . stringify ( {
93- jsonrpc : "2.0" ,
94- id : `campaign-${ parsedCampaignId } ` ,
95- method : "query" ,
96- params : {
97- request_type : "call_function" ,
98- account_id : CAMPAIGNS_CONTRACT_ACCOUNT_ID ,
99- method_name : "get_campaign" ,
100- args_base64 : Buffer . from ( JSON . stringify ( { campaign_id : parsedCampaignId } ) ) . toString (
101- "base64" ,
102- ) ,
103- finality : "optimistic" ,
104- } ,
105- } ) ,
106- } ) ;
107-
108- if ( ! response . ok ) {
109- throw new Error ( `RPC response not ok: ${ response . status } ` ) ;
110- }
111-
112- const payload = ( await response . json ( ) ) as {
113- error ?: { message ?: string } ;
114- result ?: { result ?: number [ ] } ;
115- } ;
116-
117- if ( payload . error ?. message ) {
118- throw new Error ( payload . error . message ) ;
119- }
120-
121- const resultBytes = payload . result ?. result
122- ? Uint8Array . from ( payload . result . result )
123- : undefined ;
124-
125- if ( ! resultBytes ) {
126- throw new Error ( "RPC returned empty result" ) ;
127- }
128-
129- campaign = JSON . parse ( Buffer . from ( resultBytes ) . toString ( ) ) as ContractCampaign ;
130- } finally {
131- clearTimeout ( timeoutId ) ;
132- }
133-
134- if ( ! campaign ) {
135- return { notFound : true , revalidate : 60 } ;
136- }
137-
138- const seo : SeoProps = {
139- seoTitle : `${ campaign . name } | Potlock` ,
140- seoDescription :
141- campaign . description && campaign . description . trim ( )
142- ? campaign . description . substring ( 0 , 160 )
143- : APP_METADATA . description ,
144- seoImage : campaign . cover_image_url ?? APP_METADATA . openGraph . images . url ,
145- } ;
146-
147- return {
148- props : {
149- seo,
150- campaignId : parsedCampaignId ,
151- } ,
152- revalidate : 300 , // 5 minutes
153- } ;
154- } catch ( error ) {
155- // Handle timeout or other errors by returning fallback SEO
156- // This ensures the page doesn't break
157- const message = ( error as Error ) ?. message ?? "Unknown error" ;
158-
159- if ( message . toLowerCase ( ) . includes ( "not found" ) ) {
160- return { notFound : true , revalidate : 60 } ;
161- }
162-
163- console . error ( `Error fetching campaign ${ campaignId } for SEO:` , { message } ) ;
164-
165- return {
166- props : {
167- seo : defaultSeo ,
168- campaignId : parsedCampaignId ,
169- } ,
170- revalidate : 60 , // Try again sooner on error
171- } ;
172- }
173- } ;
0 commit comments