Skip to content

Commit 33f8f47

Browse files
authored
Merge pull request #524 from PotLock/campaign-server-2
campaign servers 2 fixed
2 parents 9857348 + bd19e55 commit 33f8f47

File tree

3 files changed

+162
-156
lines changed

3 files changed

+162
-156
lines changed

src/pages/campaign/[campaignId]/index.tsx

Lines changed: 54 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,6 @@ import { ReactElement } from "react";
33
import type { GetStaticPaths, GetStaticProps } from "next";
44
import { useRouter } from "next/router";
55

6-
import { APP_METADATA } from "@/common/constants";
7-
import { stripHtml } from "@/common/lib/datetime";
8-
import { fetchWithTimeout } from "@/common/lib/fetch-with-timeout";
96
import { CampaignBanner, CampaignDonorsTable } from "@/entities/campaign";
107
import { CampaignLayout } from "@/layout/campaign/components/layout";
118
import { RootLayout } from "@/layout/components/root-layout";
@@ -42,69 +39,74 @@ export const getStaticPaths: GetStaticPaths = async () => {
4239
};
4340
};
4441

45-
// Pre-build each campaign page with its data
42+
// Default SEO values (inline to avoid import issues in serverless)
43+
const DEFAULT_SEO = {
44+
title: "Potlock | Fund Public Goods",
45+
description:
46+
"Discover and fund public goods projects on NEAR Protocol. Support open source, community initiatives, and impactful projects.",
47+
image: "https://app.potlock.org/assets/images/meta-image.png",
48+
};
49+
50+
// Simple HTML strip function (inline to avoid import issues)
51+
const stripHtmlTags = (html: string | undefined | null): string => {
52+
if (!html) return "";
53+
return html.replace(/<[^>]*>/g, "").trim();
54+
};
55+
56+
// Generate campaign page data on-demand
4657
export const getStaticProps: GetStaticProps<SeoProps> = async ({ params }) => {
47-
try {
48-
const campaignId = params?.campaignId as string;
58+
const campaignId = params?.campaignId as string;
59+
60+
// Fallback props for any error case
61+
const fallbackProps = {
62+
props: {
63+
seoTitle: campaignId ? `Campaign ${campaignId}` : DEFAULT_SEO.title,
64+
seoDescription: DEFAULT_SEO.description,
65+
seoImage: DEFAULT_SEO.image,
66+
},
67+
revalidate: 60, // Retry sooner on error
68+
};
4969

50-
if (!campaignId) {
51-
return {
52-
notFound: true,
53-
};
54-
}
70+
if (!campaignId) {
71+
return { notFound: true };
72+
}
5573

56-
// Fetch with timeout to prevent server timeouts
57-
const res = await fetchWithTimeout(
74+
try {
75+
// Use native fetch with AbortController for timeout
76+
const controller = new AbortController();
77+
const timeoutId = setTimeout(() => controller.abort(), 8000);
78+
79+
const res = await fetch(
5880
`https://dev.potlock.io/api/v1/campaigns/${encodeURIComponent(campaignId)}`,
59-
{},
60-
8000, // 8 second timeout
81+
{ signal: controller.signal },
6182
);
6283

63-
if (!res.ok) {
64-
// If campaign not found, return 404 instead of erroring
65-
if (res.status === 404) {
66-
return {
67-
notFound: true,
68-
};
69-
}
70-
71-
throw new Error(`Failed to fetch campaign: ${res.status}`);
72-
}
73-
74-
let campaign;
84+
clearTimeout(timeoutId);
7585

76-
try {
77-
campaign = await res.json();
78-
} catch (jsonError) {
79-
console.error("Error parsing campaign JSON:", jsonError);
80-
throw new Error("Invalid campaign data format");
86+
// If campaign not found, return 404
87+
if (res.status === 404) {
88+
return { notFound: true };
8189
}
8290

83-
const seoTitle = campaign?.name ?? `Campaign ${campaignId}`;
84-
85-
const seoDescription = stripHtml(campaign?.description) ?? "Support this campaign on Potlock.";
86-
87-
// Use cover_image_url field which is the correct field for campaign images
88-
const seoImage = campaign?.cover_image_url ?? APP_METADATA.openGraph.images.url;
91+
// For other errors, return fallback props (don't throw)
92+
if (!res.ok) {
93+
console.error(`Campaign API returned ${res.status} for campaign ${campaignId}`);
94+
return fallbackProps;
95+
}
8996

90-
return {
91-
props: { seoTitle, seoDescription, seoImage },
92-
// Revalidate every 2 minutes (120 seconds) to keep data fresh
93-
revalidate: 120,
94-
};
95-
} catch (error) {
96-
console.error("Error generating static props:", error);
97+
const campaign = await res.json();
9798

98-
// Return fallback props instead of throwing error to prevent 500
99-
// This allows the page to render with default SEO data
10099
return {
101100
props: {
102-
seoTitle: `Campaign ${params?.campaignId || ""}`,
103-
seoDescription: APP_METADATA.description,
104-
seoImage: APP_METADATA.openGraph.images.url,
101+
seoTitle: campaign?.name || `Campaign`,
102+
seoDescription: stripHtmlTags(campaign?.description) || DEFAULT_SEO.description,
103+
seoImage: campaign?.cover_image_url || DEFAULT_SEO.image,
105104
},
106-
// Shorter revalidate for error cases to retry sooner
107-
revalidate: 60,
105+
revalidate: 120, // 2 minutes
108106
};
107+
} catch (error) {
108+
// Log but don't throw - return fallback props
109+
console.error(`Error fetching campaign ${campaignId}:`, error);
110+
return fallbackProps;
109111
}
110112
};

src/pages/campaign/[campaignId]/leaderboard.tsx

Lines changed: 54 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,6 @@ import { ReactElement } from "react";
33
import type { GetStaticPaths, GetStaticProps } from "next";
44
import { useRouter } from "next/router";
55

6-
import { APP_METADATA } from "@/common/constants";
7-
import { stripHtml } from "@/common/lib/datetime";
8-
import { fetchWithTimeout } from "@/common/lib/fetch-with-timeout";
96
import { CampaignDonorsTable } from "@/entities/campaign";
107
import { CampaignLayout } from "@/layout/campaign/components/layout";
118
import { RootLayout } from "@/layout/components/root-layout";
@@ -41,69 +38,74 @@ export const getStaticPaths: GetStaticPaths = async () => {
4138
};
4239
};
4340

44-
// Pre-build each campaign page with its data
41+
// Default SEO values (inline to avoid import issues in serverless)
42+
const DEFAULT_SEO = {
43+
title: "Potlock | Fund Public Goods",
44+
description:
45+
"Discover and fund public goods projects on NEAR Protocol. Support open source, community initiatives, and impactful projects.",
46+
image: "https://app.potlock.org/assets/images/meta-image.png",
47+
};
48+
49+
// Simple HTML strip function (inline to avoid import issues)
50+
const stripHtmlTags = (html: string | undefined | null): string => {
51+
if (!html) return "";
52+
return html.replace(/<[^>]*>/g, "").trim();
53+
};
54+
55+
// Generate campaign page data on-demand
4556
export const getStaticProps: GetStaticProps<SeoProps> = async ({ params }) => {
46-
try {
47-
const campaignId = params?.campaignId as string;
57+
const campaignId = params?.campaignId as string;
58+
59+
// Fallback props for any error case
60+
const fallbackProps = {
61+
props: {
62+
seoTitle: campaignId ? `Campaign ${campaignId}` : DEFAULT_SEO.title,
63+
seoDescription: DEFAULT_SEO.description,
64+
seoImage: DEFAULT_SEO.image,
65+
},
66+
revalidate: 60, // Retry sooner on error
67+
};
4868

49-
if (!campaignId) {
50-
return {
51-
notFound: true,
52-
};
53-
}
69+
if (!campaignId) {
70+
return { notFound: true };
71+
}
5472

55-
// Fetch with timeout to prevent server timeouts
56-
const res = await fetchWithTimeout(
73+
try {
74+
// Use native fetch with AbortController for timeout
75+
const controller = new AbortController();
76+
const timeoutId = setTimeout(() => controller.abort(), 8000);
77+
78+
const res = await fetch(
5779
`https://dev.potlock.io/api/v1/campaigns/${encodeURIComponent(campaignId)}`,
58-
{},
59-
8000, // 8 second timeout
80+
{ signal: controller.signal },
6081
);
6182

62-
if (!res.ok) {
63-
// If campaign not found, return 404 instead of erroring
64-
if (res.status === 404) {
65-
return {
66-
notFound: true,
67-
};
68-
}
69-
70-
throw new Error(`Failed to fetch campaign: ${res.status}`);
71-
}
72-
73-
let campaign;
83+
clearTimeout(timeoutId);
7484

75-
try {
76-
campaign = await res.json();
77-
} catch (jsonError) {
78-
console.error("Error parsing campaign JSON:", jsonError);
79-
throw new Error("Invalid campaign data format");
85+
// If campaign not found, return 404
86+
if (res.status === 404) {
87+
return { notFound: true };
8088
}
8189

82-
const seoTitle = campaign?.name ?? `Campaign ${campaignId}`;
83-
84-
const seoDescription = stripHtml(campaign?.description) ?? "Support this campaign on Potlock.";
85-
86-
// Use cover_image_url field which is the correct field for campaign images
87-
const seoImage = campaign?.cover_image_url ?? APP_METADATA.openGraph.images.url;
90+
// For other errors, return fallback props (don't throw)
91+
if (!res.ok) {
92+
console.error(`Campaign API returned ${res.status} for campaign ${campaignId}`);
93+
return fallbackProps;
94+
}
8895

89-
return {
90-
props: { seoTitle, seoDescription, seoImage },
91-
// Revalidate every 2 minutes (120 seconds) to keep data fresh
92-
revalidate: 120,
93-
};
94-
} catch (error) {
95-
console.error("Error generating static props:", error);
96+
const campaign = await res.json();
9697

97-
// Return fallback props instead of throwing error to prevent 500
98-
// This allows the page to render with default SEO data
9998
return {
10099
props: {
101-
seoTitle: `Campaign ${params?.campaignId || ""}`,
102-
seoDescription: APP_METADATA.description,
103-
seoImage: APP_METADATA.openGraph.images.url,
100+
seoTitle: campaign?.name || `Campaign ${campaignId}`,
101+
seoDescription: stripHtmlTags(campaign?.description) || DEFAULT_SEO.description,
102+
seoImage: campaign?.cover_image_url || DEFAULT_SEO.image,
104103
},
105-
// Shorter revalidate for error cases to retry sooner
106-
revalidate: 60,
104+
revalidate: 120, // 2 minutes
107105
};
106+
} catch (error) {
107+
// Log but don't throw - return fallback props
108+
console.error(`Error fetching campaign ${campaignId}:`, error);
109+
return fallbackProps;
108110
}
109111
};

0 commit comments

Comments
 (0)