diff --git a/package.json b/package.json index 9a6bcb3..9329dbe 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "next lint" + "lint": "next lint", + "seed:sponsors": "node scripts/seed-sponsors.mjs" }, "dependencies": { "@radix-ui/react-dropdown-menu": "^2.1.16", diff --git a/scripts/seed-sponsors.mjs b/scripts/seed-sponsors.mjs new file mode 100644 index 0000000..8bbba90 --- /dev/null +++ b/scripts/seed-sponsors.mjs @@ -0,0 +1,211 @@ +import path from "node:path" +import { fileURLToPath } from "node:url" +import { readFile } from "node:fs/promises" + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const projectRoot = path.resolve(__dirname, "..") + +const STRAPI_API_URL = + process.env.STRAPI_API_URL || + process.env.NEXT_PUBLIC_STRAPI_API_URL || + "https://innovative-luck-8fe8e1c24e.strapiapp.com/api" + +const STRAPI_API_TOKEN = process.env.STRAPI_API_TOKEN +const SPONSORS_ENDPOINT = process.env.STRAPI_SPONSORS_ENDPOINT || "sponsors" + +if (!STRAPI_API_TOKEN) { + console.error("Missing STRAPI_API_TOKEN in environment.") + process.exit(1) +} + +const SPONSOR_TIER_DEFAULT = process.env.SPONSOR_TIER_DEFAULT || "sponsor" +const DRY_RUN = process.env.DRY_RUN === "true" + +const sponsors = [ + { + name: "QLS", + website_url: "https://www.quarrylane.org/", + logoPath: "public/static/sponsors/qls.png", + display_order: 1 + }, + { + name: "Notion", + website_url: "https://www.notion.so", + logoPath: "public/static/sponsors/notion-logo.png", + display_order: 2 + }, + { + name: "Intuitive Foundation", + website_url: "https://www.intuitive-foundation.org/first-robotics/", + logoPath: "public/static/sponsors/IntuitiveFoundation.png", + display_order: 3 + }, + { + name: "FIRST NorCal", + website_url: "https://www.firstinspires.org/robotics/frc", + logoPath: "public/static/sponsors/FIRST-NorCal.png", + display_order: 4 + }, + { + name: "Google", + website_url: "https://about.google/brand-resource-center/guidance/sponsorships/", + logoPath: "public/static/sponsors/googleLogo.png", + display_order: 5 + }, + { + name: "LDL", + website_url: "https://littledesignlab.org/", + logoPath: "public/static/sponsors/ldl.svg", + display_order: 6 + } +] + +function normalizeBaseUrl(url) { + return url.replace(/\/+$/, "") +} + +async function fetchJson(url, options = {}) { + const response = await fetch(url, { + headers: { + Authorization: `Bearer ${STRAPI_API_TOKEN}`, + ...options.headers + }, + ...options + }) + + if (!response.ok) { + const text = await response.text() + const error = new Error(`Request failed ${response.status}: ${text}`) + error.status = response.status + throw error + } + + return response.json() +} + +function buildEndpoint(pathSegment) { + return `${normalizeBaseUrl(STRAPI_API_URL)}/${pathSegment.replace(/^\/+/, "")}` +} + +async function findSponsorByName(name) { + const params = new URLSearchParams({ + "filters[name][$eq]": name + }) + const url = `${buildEndpoint(SPONSORS_ENDPOINT)}?${params.toString()}` + const result = await fetchJson(url) + const data = Array.isArray(result?.data) ? result.data : [] + return data.length > 0 ? data[0] : null +} + +async function uploadLogo(logoPath) { + const filePath = path.resolve(projectRoot, logoPath) + const buffer = await readFile(filePath) + const formData = new FormData() + const filename = path.basename(filePath) + const blob = new Blob([buffer]) + + formData.append("files", blob, filename) + + if (DRY_RUN) { + console.log(`[dry-run] upload ${logoPath}`) + return { id: `dry-run-${filename}` } + } + + const url = `${normalizeBaseUrl(STRAPI_API_URL)}/upload` + const response = await fetch(url, { + method: "POST", + headers: { + Authorization: `Bearer ${STRAPI_API_TOKEN}` + }, + body: formData + }) + + if (!response.ok) { + const text = await response.text() + throw new Error(`Upload failed ${response.status}: ${text}`) + } + + const uploaded = await response.json() + const file = Array.isArray(uploaded) ? uploaded[0] : uploaded + if (!file?.id) { + throw new Error("Upload response missing file id.") + } + + return file +} + +async function createSponsor(sponsor, logoId) { + const payload = { + data: { + name: sponsor.name, + website_url: sponsor.website_url, + display_order: sponsor.display_order, + is_active: true, + tier: SPONSOR_TIER_DEFAULT, + logo: logoId + } + } + + const attemptEndpoints = [SPONSORS_ENDPOINT] + if (!attemptEndpoints.includes("sponsors")) attemptEndpoints.push("sponsors") + if (!attemptEndpoints.includes("sponsor")) attemptEndpoints.push("sponsor") + + const tryCreate = async (endpoint, method = "POST") => { + const url = buildEndpoint(endpoint) + if (DRY_RUN) { + console.log(`[dry-run] ${method} ${endpoint}`, payload) + return { ok: true } + } + + await fetchJson(url, { + method, + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(payload) + }) + } + + for (const endpoint of attemptEndpoints) { + try { + await tryCreate(endpoint, "POST") + return + } catch (error) { + if (error?.status === 405) { + await tryCreate(endpoint, "PUT") + return + } + throw error + } + } + + throw new Error( + "Unable to create sponsor. Set STRAPI_SPONSORS_ENDPOINT to the correct API path." + ) +} + +async function main() { + console.log("Seeding sponsors into Strapi...") + + for (const sponsor of sponsors) { + const existing = await findSponsorByName(sponsor.name) + if (existing) { + console.log(`Skipping ${sponsor.name} (already exists).`) + continue + } + + console.log(`Uploading ${sponsor.name} logo...`) + const uploadedLogo = await uploadLogo(sponsor.logoPath) + const logoId = uploadedLogo.id + + console.log(`Creating sponsor ${sponsor.name}...`) + await createSponsor(sponsor, logoId) + } + + console.log("Sponsor seed complete.") +} + +main().catch(error => { + console.error(error) + process.exit(1) +}) diff --git a/src/app/api/strapi/[...path]/route.ts b/src/app/api/strapi/[...path]/route.ts index 2323e5b..ff09600 100644 --- a/src/app/api/strapi/[...path]/route.ts +++ b/src/app/api/strapi/[...path]/route.ts @@ -40,8 +40,7 @@ export async function GET( headers: { Accept: 'application/json', }, - // Cache Strapi responses briefly at the Next.js layer - next: { revalidate: 60 }, + cache: 'no-store', }) const contentType = upstreamRes.headers.get('content-type') || 'application/json' @@ -51,8 +50,7 @@ export async function GET( status: upstreamRes.status, headers: { 'content-type': contentType, - // Encourage browsers/CDNs to cache lightly too (safe for public content) - 'cache-control': 'public, s-maxage=60, stale-while-revalidate=300', + 'cache-control': 'no-store', }, }) } catch (error) { diff --git a/src/app/leadership/page.tsx b/src/app/leadership/page.tsx index c26bf7d..cd73d6a 100644 --- a/src/app/leadership/page.tsx +++ b/src/app/leadership/page.tsx @@ -231,14 +231,8 @@ export default function Leadership() { fetchLeaders() }, []) - // Combine original team with Strapi leaders - const allLeaders = [ - ...originalTeam.map(member => ({ - ...member, - id: member.name, - isOriginal: true - })), - ...strapiLeaders.map(leader => { + const strapiLeadersWithImages = strapiLeaders + .map(leader => { const imageUrl = getStrapiMediaUrl(leader.profile_picture) return { id: leader.id, @@ -250,7 +244,16 @@ export default function Leadership() { isOriginal: false } }) - ] + .filter(leader => leader.img) + + const useFallbackLeaders = !strapiLoading && strapiLeadersWithImages.length === 0 + const allLeaders = useFallbackLeaders + ? originalTeam.map(member => ({ + ...member, + id: member.name, + isOriginal: true + })) + : strapiLeadersWithImages return (
@@ -276,36 +279,50 @@ export default function Leadership() { {/* Items */}
- {allLeaders.map((member, index) => ( -
- {member.name} -
-
-

- {member.name} '{member.year} -

+ {strapiLoading + ? Array.from({ length: 8 }).map((_, index) => ( +
+
+
+
+
+
+
+ )) + : allLeaders.map((member, index) => ( +
+ {member.name} +
+
+

+ {member.name} '{member.year} +

+
+

+ {member.position} +

+ {'bio' in member && member.bio && ( +

+ {member.bio} +

+ )} +
-

- {member.position} -

- {'bio' in member && member.bio && ( -

- {member.bio} -

- )} -
-
- ))} + ))}
diff --git a/src/components/HeroSection.tsx b/src/components/HeroSection.tsx index 0669dc4..961d134 100644 --- a/src/components/HeroSection.tsx +++ b/src/components/HeroSection.tsx @@ -62,9 +62,9 @@ export function HeroSection() { "/static/team/teamphoto.avif" ] - const carouselImages = carouselMedia && carouselMedia.length > 0 + const carouselImages = !carouselLoading && carouselMedia && carouselMedia.length > 0 ? carouselMedia.map(item => getStrapiMediaUrl(item.image)) - : fallbackImages + : (!carouselLoading ? fallbackImages : []) return (
@@ -156,19 +156,23 @@ export function HeroSection() { className="relative mt-8 lg:mt-0" > - - {carouselImages.map((src, index) => ( -
- {`Robot -
- ))} -
+ {carouselLoading ? ( +
+ ) : ( + + {carouselImages.map((src, index) => ( +
+ {`Robot +
+ ))} +
+ )} {/* Decorative elements */} diff --git a/src/components/OurRobot.tsx b/src/components/OurRobot.tsx index ad94e4c..d528caa 100644 --- a/src/components/OurRobot.tsx +++ b/src/components/OurRobot.tsx @@ -193,9 +193,9 @@ const OurRobot = () => { "/static/robot/robot5.JPG", ] - const carouselImages = robotMedia && robotMedia.length > 0 + const carouselImages = !robotLoading && robotMedia && robotMedia.length > 0 ? robotMedia.map(item => getStrapiMediaUrl(item.image)) - : fallbackImages + : (!robotLoading ? fallbackImages : []) return ( { - - {carouselImages.map((src, index) => ( -
- {`Robot -
- ))} -
+ {robotLoading ? ( +
+ ) : ( + + {carouselImages.map((src, index) => ( +
+ {`Robot +
+ ))} +
+ )}
diff --git a/src/components/Sponsors.tsx b/src/components/Sponsors.tsx index 5cb972f..5124f55 100644 --- a/src/components/Sponsors.tsx +++ b/src/components/Sponsors.tsx @@ -45,21 +45,24 @@ function Sponsors() { fetchSponsors() }, []) - // Combine original sponsors with Strapi sponsors - const allSponsors = [ - ...originalSponsors.map(sponsor => ({ - ...sponsor, - id: sponsor.name, - isOriginal: true - })), - ...strapiSponsors.map(sponsor => ({ + const strapiSponsorsWithLogos = strapiSponsors + .map(sponsor => ({ id: sponsor.id, name: sponsor.name, logo: getStrapiMediaUrl(sponsor.logo), url: sponsor.website_url, isOriginal: false })) - ] + .filter(sponsor => sponsor.logo) + + const useFallbackSponsors = !strapiLoading && strapiSponsorsWithLogos.length === 0 + const allSponsors = useFallbackSponsors + ? originalSponsors.map(sponsor => ({ + ...sponsor, + id: sponsor.name, + isOriginal: true + })) + : strapiSponsorsWithLogos return (
@@ -79,36 +82,45 @@ function Sponsors() {
- {allSponsors.map((sponsor, index) => ( - - {sponsor.url ? ( - - {`${sponsor.name} - - ) : ( - {`${sponsor.name} - )} - - ))} + {strapiLoading + ? Array.from({ length: 6 }).map((_, index) => ( +
+
+
+ )) + : allSponsors.map((sponsor, index) => ( + + {sponsor.url ? ( + + {`${sponsor.name} + + ) : ( + {`${sponsor.name} + )} + + ))}