Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
211 changes: 211 additions & 0 deletions scripts/seed-sponsors.mjs
Original file line number Diff line number Diff line change
@@ -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)
})
6 changes: 2 additions & 4 deletions src/app/api/strapi/[...path]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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) {
Expand Down
93 changes: 55 additions & 38 deletions src/app/leadership/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 (
<main className="flex-grow" style={{ backgroundColor: "#1b2947" }}>
Expand All @@ -276,36 +279,50 @@ export default function Leadership() {
{/* Items */}
<div className="my-5 mx-auto">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-8">
{allLeaders.map((member, index) => (
<div
key={member.id}
className="flex space-x-6"
style={{ marginLeft: "25%" }}
>
<Image
src={(member.isOriginal ? member.img : member.img) || ''}
height={100}
width={100}
className="h-16 object-cover w-16 rounded-xl bg-gray-800 border-none shadow-sm"
alt={member.name}
/>
<div className="block text-lg">
<div className="hover:text-gray-400">
<p className="font-semibold break-normal text-gray-200">
{member.name} &apos;{member.year}
</p>
{strapiLoading
? Array.from({ length: 8 }).map((_, index) => (
<div
key={`leader-skeleton-${index}`}
className="flex space-x-6"
style={{ marginLeft: "25%" }}
>
<div className="h-16 w-16 rounded-xl bg-gray-700 animate-pulse" />
<div className="space-y-2 w-40">
<div className="h-4 bg-gray-700 rounded animate-pulse" />
<div className="h-3 bg-gray-700 rounded w-24 animate-pulse" />
</div>
</div>
))
: allLeaders.map((member, index) => (
<div
key={member.id}
className="flex space-x-6"
style={{ marginLeft: "25%" }}
>
<Image
src={(member.isOriginal ? member.img : member.img) || ''}
height={100}
width={100}
className="h-16 object-cover w-16 rounded-xl bg-gray-800 border-none shadow-sm"
alt={member.name}
/>
<div className="block text-lg">
<div className="hover:text-gray-400">
<p className="font-semibold break-normal text-gray-200">
{member.name} &apos;{member.year}
</p>
</div>
<p className="mt-1 text-base inline text-transparent bg-clip-text bg-gradient-to-r from-[hsl(var(--brand-gold))] to-[#d59a25] font-medium">
{member.position}
</p>
{'bio' in member && member.bio && (
<p className="text-sm text-gray-300 mt-1">
{member.bio}
</p>
)}
</div>
</div>
<p className="mt-1 text-base inline text-transparent bg-clip-text bg-gradient-to-r from-[hsl(var(--brand-gold))] to-[#d59a25] font-medium">
{member.position}
</p>
{'bio' in member && member.bio && (
<p className="text-sm text-gray-300 mt-1">
{member.bio}
</p>
)}
</div>
</div>
))}
))}
</div>
</div>
</div>
Expand Down
Loading