Skip to content

Commit 822fa8a

Browse files
committed
feat: add new Ecosystem section
Add new Ecosystem page with ecosystem cards and list components. Includes INCO report and sorting functionality.
1 parent 0bba55a commit 822fa8a

File tree

7 files changed

+512
-4
lines changed

7 files changed

+512
-4
lines changed

app/(pages)/ecosystem/page.tsx

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { LABELS } from "@/app/labels"
2+
import { AppLink } from "@/components/app-link"
3+
import { Icons } from "@/components/icons"
4+
import { AppContent } from "@/components/ui/app-content"
5+
import { Button } from "@/components/ui/button"
6+
import { Metadata } from "next"
7+
import { EcosystemList } from "@/components/ecosystem/ecosystem-list"
8+
9+
export const metadata: Metadata = {
10+
title: "Ecosystem",
11+
description:
12+
"Explore our ecosystem exploration artifacts, from reports to maps.",
13+
}
14+
15+
const EcosystemPage = async () => {
16+
return (
17+
<div className="flex flex-col gap-10 pb-[128px] ">
18+
<AppContent className="flex flex-col gap-4 py-10 w-full">
19+
<div className="flex flex-col gap-10">
20+
<h1 className="dark:text-tuatara-100 text-tuatara-950 text-xl lg:text-3xl font-normal font-sans text-center">
21+
{LABELS.ECOSYSTEM_PAGE.TITLE}
22+
</h1>
23+
<div className="lg:!w-4/5 w-full flex lg:flex-row flex-col items-center gap-3 lg:gap-10 mx-auto justify-center">
24+
<span className="text-base lg:text-xl dark:text-tuatara-200 text-tuatara-950 font-sans text-center">
25+
{LABELS.ECOSYSTEM_PAGE.SUBTITLE}
26+
</span>
27+
<AppLink variant="button" href="/about">
28+
<Button
29+
icon={Icons.arrowRight}
30+
iconPosition="right"
31+
className="uppercase"
32+
>
33+
{LABELS.COMMON.LEARN_MORE}
34+
</Button>
35+
</AppLink>
36+
</div>
37+
</div>
38+
</AppContent>
39+
40+
<AppContent className="flex flex-col gap-10">
41+
<EcosystemList />
42+
</AppContent>
43+
</div>
44+
)
45+
}
46+
47+
export default EcosystemPage

app/labels.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export const LABELS = {
2121
RESEARCH: "Research",
2222
ABOUT: "About",
2323
BLOG: "Blog",
24+
ECOSYSTEM: "Ecosystem",
2425
},
2526
FOOTER: {
2627
DESCRIPTION:
@@ -244,6 +245,14 @@ export const LABELS = {
244245
ACTIVE_RESEARCH: "Active Research",
245246
PAST_RESEARCH: "Past Research",
246247
},
248+
ECOSYSTEM_PAGE: {
249+
TITLE: "Explore Our Ecosystem",
250+
SUBTITLE:
251+
"Explore our ecosystem exploration artifacts, from reports to maps.",
252+
ARTIFACTS: "Ecosystem Artifacts",
253+
EXTERNAL_RESEARCH_REPORTS:
254+
"Explore ecosystem research reports done by other teams outside PSE",
255+
},
247256
RESOURCES_PAGE: {
248257
TITLE: "Resources",
249258
SUBTITLE:
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import { cn } from "@/lib/utils"
2+
import { VariantProps, cva } from "class-variance-authority"
3+
import Image from "next/image"
4+
import Link from "next/link"
5+
import React from "react"
6+
7+
export interface EcosystemItem {
8+
id: string
9+
title: string
10+
description: string
11+
image?: string
12+
imageAlt?: string
13+
href?: string
14+
type?: "report" | "map" | "other"
15+
date?: string
16+
team?: string
17+
cardTags?: {
18+
primary?: string
19+
secondary?: string
20+
wip?: boolean
21+
}
22+
}
23+
24+
interface EcosystemCardProps
25+
extends React.HTMLAttributes<HTMLDivElement>,
26+
VariantProps<typeof ecosystemCardVariants> {
27+
item: EcosystemItem
28+
showBanner?: boolean
29+
}
30+
31+
const tagCardVariants = cva(
32+
"text-xs font-sans text-white rounded-[3px] py-[2px] px-[6px] dark:text-black",
33+
{
34+
variants: {
35+
variant: {
36+
primary: "bg-[#D8FEA8]",
37+
secondary: "bg-[#C2E8F5]",
38+
team: "bg-[#E8D5FF]",
39+
wip: "bg-[#FFB84D] font-semibold",
40+
},
41+
},
42+
}
43+
)
44+
45+
const ecosystemCardVariants = cva(
46+
"flex flex-col overflow-hidden rounded-lg transition duration-200 ease-in border border-transparent h-full",
47+
{
48+
variants: {
49+
showBanner: {
50+
true: "min-h-[280px]",
51+
false: "min-h-[200px]",
52+
},
53+
border: {
54+
true: "border border-slate-900/20 dark:border-anakiwa-800",
55+
},
56+
},
57+
}
58+
)
59+
60+
export default function EcosystemCard({
61+
item,
62+
showBanner = true,
63+
border = false,
64+
className,
65+
}: EcosystemCardProps) {
66+
const { id, title, description, image, imageAlt, href, date, team, cardTags } = item
67+
68+
// Parse teams - split by comma and trim whitespace
69+
const teams = team
70+
? team
71+
.split(",")
72+
.map((t) => t.trim())
73+
.filter((t) => t.length > 0)
74+
: []
75+
76+
const cardContent = (
77+
<>
78+
{showBanner && (
79+
<div className="relative flex flex-col border-b border-black/10">
80+
<Image
81+
src={image ? image : "/project-banners/fallback.webp"}
82+
alt={imageAlt || title}
83+
width={1200}
84+
height={630}
85+
className="h-[160px] w-full overflow-hidden rounded-t-lg border-none object-cover"
86+
/>
87+
{!image && (
88+
<span className="absolute w-full px-5 text-xl font-bold text-center text-black -translate-x-1/2 -translate-y-1/2 left-1/2 top-1/2">
89+
{imageAlt || title}
90+
</span>
91+
)}
92+
{/* Teams and WIP tag in top-right corner of banner */}
93+
{(teams.length > 0 || cardTags?.wip) && (
94+
<div className="absolute top-2 right-2 flex items-center gap-1 flex-wrap justify-end">
95+
{cardTags?.wip && (
96+
<div className={tagCardVariants({ variant: "wip" })}>
97+
WIP
98+
</div>
99+
)}
100+
{teams.map((teamName, index) => (
101+
<div key={index} className={tagCardVariants({ variant: "team" })}>
102+
{teamName}
103+
</div>
104+
))}
105+
</div>
106+
)}
107+
</div>
108+
)}
109+
<div className="flex flex-col justify-between gap-4 p-4 bg-white rounded-b-lg dark:bg-black relative">
110+
{/* Teams and WIP tag in top-right corner when no banner */}
111+
{!showBanner && (teams.length > 0 || cardTags?.wip) && (
112+
<div className="absolute top-4 right-4 flex items-center gap-1 flex-wrap justify-end">
113+
{cardTags?.wip && (
114+
<div className={tagCardVariants({ variant: "wip" })}>
115+
WIP
116+
</div>
117+
)}
118+
{teams.map((teamName, index) => (
119+
<div key={index} className={tagCardVariants({ variant: "team" })}>
120+
{teamName}
121+
</div>
122+
))}
123+
</div>
124+
)}
125+
<div className="flex flex-col justify-start gap-2 flex-1">
126+
<h3 className="text-2xl font-bold leading-7 text-primary duration-200 cursor-pointer hover:text-anakiwa-500">
127+
{title}
128+
</h3>
129+
{date && (
130+
<p className="text-sm text-tuatara-400 dark:text-tuatara-400">
131+
{date}
132+
</p>
133+
)}
134+
{description?.length > 0 && (
135+
<div className="flex flex-col gap-4">
136+
<p className="text-tuatara-500 text-base line-clamp-4 dark:text-tuatara-200">
137+
{description}
138+
</p>
139+
</div>
140+
)}
141+
</div>
142+
<div className="flex flex-col gap-2 mt-auto">
143+
<div className="flex justify-between items-center">
144+
{cardTags && (
145+
<div className="flex items-center gap-1 flex-wrap">
146+
{cardTags.primary && (
147+
<div className={tagCardVariants({ variant: "primary" })}>
148+
{cardTags.primary}
149+
</div>
150+
)}
151+
{cardTags.secondary && (
152+
<div className={tagCardVariants({ variant: "secondary" })}>
153+
{cardTags.secondary}
154+
</div>
155+
)}
156+
{cardTags.wip && (
157+
<div className={tagCardVariants({ variant: "wip" })}>
158+
WIP
159+
</div>
160+
)}
161+
</div>
162+
)}
163+
</div>
164+
</div>
165+
</div>
166+
</>
167+
)
168+
169+
if (href) {
170+
return (
171+
<Link
172+
href={href}
173+
target={href.startsWith("http") ? "_blank" : undefined}
174+
rel={href.startsWith("http") ? "noopener noreferrer" : undefined}
175+
className={cn(
176+
"group cursor-pointer",
177+
ecosystemCardVariants({ showBanner, border, className })
178+
)}
179+
>
180+
{cardContent}
181+
</Link>
182+
)
183+
}
184+
185+
return (
186+
<div
187+
className={cn(
188+
"group",
189+
ecosystemCardVariants({ showBanner, border, className })
190+
)}
191+
>
192+
{cardContent}
193+
</div>
194+
)
195+
}

0 commit comments

Comments
 (0)