feat: graduated projects section (#554)

* feat: graduated projects section
This commit is contained in:
Kalidou Diagne
2025-09-11 16:30:46 +08:00
committed by GitHub
parent 439b1b5c9a
commit 2da42f240c
21 changed files with 430 additions and 173 deletions

View File

@@ -1,9 +1,9 @@
import { LABELS } from "@/app/labels"
import ProjectFiltersBar from "@/components/project/project-filters-bar"
import { ProjectGraduated } from "@/components/project/project-graduated"
import { ProjectList } from "@/components/project/project-list"
import { ProjectResultBar } from "@/components/project/project-result-bar"
import { AppContent } from "@/components/ui/app-content"
import { Label } from "@/components/ui/label"
import { Metadata } from "next"
import { Suspense } from "react"
@@ -16,26 +16,27 @@ export const metadata: Metadata = {
export default async function ProjectsPage() {
return (
<div className="flex flex-col">
<AppContent className="flex flex-col gap-10 py-10 lg:py-16 w-full">
<div className="flex flex-col gap-5">
<div className="lg:w-1/2 mx-auto w-full">
<div className="flex flex-col gap-10">
<h1 className="dark:text-tuatara-100 text-tuatara-950 text-xl lg:text-3xl font-normal font-sans text-center">
{LABELS.PROJECTS_PAGE.TITLE}
</h1>
<AppContent className="flex flex-col gap-10 pt-10 pb-20 lg:py-16 w-full">
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-10">
<div className="lg:w-1/2 mx-auto w-full">
<div className="flex flex-col gap-10">
<h1 className="dark:text-tuatara-100 text-tuatara-950 text-2xl lg:text-3xl lg:leading-[45px] font-normal font-sans text-center">
{LABELS.PROJECTS_PAGE.TITLE}
</h1>
<Suspense fallback={<div>Loading...</div>}>
<ProjectFiltersBar />
</Suspense>
</div>
</div>
</div>
<div className="flex flex-col gap-5">
<Suspense fallback={<div>Loading...</div>}>
<div className="lg:!w-1/2 mx-auto w-full">
<ProjectFiltersBar />
</div>
<ProjectResultBar />
</Suspense>
</div>
<Suspense fallback={<div>Loading...</div>}>
<ProjectResultBar />
</Suspense>
</div>
<ProjectList />
</AppContent>
<ProjectGraduated />
</div>
)
}

View File

@@ -122,6 +122,18 @@ export const LABELS = {
ERROR_LOADING_VIDEOS: "Error loading videos",
CHECK_OUT_OUR_YOUTUBE:
"Check out our YouTube to learn the latest in advanced cryptography.",
LEARN_AND_CONNECT: {
TITLE: "Learn & Connect",
DESCRIPTION:
"Join the Ethereum R&D discord to get updates on PSE projects and talk with other privacy builders and researchers about the future of privacy on Ethereum.",
ACTION: "Join the ETH R&D Discord",
},
COLLABORATE_AND_CONTRIBUTE: {
TITLE: "Collaborate & Contribute",
DESCRIPTION:
"Connect with us on the Ethereum Magicians to dive into technical discussions, give feedback, make proposals, and collaborate on research and development.",
ACTION: "Join Ethereum Magicians",
},
},
NEWS_SECTION: {
RECENT_UPDATES: "Recent Updates",
@@ -221,6 +233,8 @@ export const LABELS = {
},
PROJECTS_PAGE: {
TITLE: "Explore Our Projects",
GRADUATED_PROJECTS:
"Explore graduated projects that have spun out into the ecosystem",
},
RESEARCH_PAGE: {
TITLE: "Explore Our Research",

View File

@@ -1,4 +1,3 @@
import { criticalCSS } from "@/lib/critical-css"
import "@/globals.css"
import { ThemeProvider } from "./components/layouts/ThemeProvider"
import { GlobalProviderLayout } from "@/components/layouts/GlobalProviderLayout"
@@ -131,9 +130,6 @@ export default function RootLayout({ children }: RootLayoutProps) {
`}
</Script>
<head>
{/* Inline critical CSS for immediate render */}
<style dangerouslySetInnerHTML={{ __html: criticalCSS }} />
{/* Font preconnections for faster loading */}
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link

View File

@@ -1,5 +1,10 @@
"use client"
import { LABELS } from "@/app/labels"
import { ProjectCategory, ProjectInterface } from "@/lib/types"
import { uniq } from "@/lib/utils"
import { useQuery } from "@tanstack/react-query"
import Fuse from "fuse.js"
import {
createContext,
useContext,
@@ -8,11 +13,6 @@ import {
useMemo,
useCallback,
} from "react"
import { useQuery } from "@tanstack/react-query"
import Fuse from "fuse.js"
import { ProjectCategory, ProjectInterface } from "@/lib/types"
import { uniq } from "@/lib/utils"
import { LABELS } from "@/app/labels"
export type ProjectSortBy = "random" | "asc" | "desc" | "relevance"
export type ProjectFilter =
@@ -47,6 +47,7 @@ export const FilterLabelMapping: Record<ProjectFilter, string> = {
interface ProjectsContextType {
projects: ProjectInterface[]
researchs: ProjectInterface[]
graduatedProjects: ProjectInterface[]
isLoading: boolean
isError: boolean
error: Error | null
@@ -188,10 +189,12 @@ const sortProjectByFn = ({
)
}
return sortedProjectList.map((project: any) => ({
id: project?.id?.toLowerCase(),
...project,
}))
return sortedProjectList
.map((project: any) => ({
id: project?.id?.toLowerCase(),
...project,
}))
.filter((project: any) => project?.graduated !== true)
}
const getProjectFilters = (projects: ProjectInterface[]): FiltersProps => {
@@ -330,6 +333,12 @@ export function ProjectsProvider({ children, tag }: ProjectsProviderProps) {
[filteredProjects, sortBy]
)
const graduatedProjects = useMemo(() => {
return fetchedProjects.filter(
(project: ProjectInterface) => project.graduated === true
)
}, [fetchedProjects])
const toggleFilter = useCallback(
({
tag: filterKey,
@@ -413,6 +422,7 @@ export function ProjectsProvider({ children, tag }: ProjectsProviderProps) {
onSelectTheme,
sortProjectBy,
refetch,
graduatedProjects,
}),
[
projects,
@@ -432,6 +442,7 @@ export function ProjectsProvider({ children, tag }: ProjectsProviderProps) {
onSelectTheme,
sortProjectBy,
refetch,
graduatedProjects,
]
)

View File

@@ -1,24 +0,0 @@
"use client"
import { useEffect, useState } from "react"
export function CSSLoader() {
const [loaded, setLoaded] = useState(false)
useEffect(() => {
// Load main CSS after critical render
const loadCSS = () => {
const link = document.createElement("link")
link.rel = "stylesheet"
link.href = "/globals.css"
link.onload = () => setLoaded(true)
document.head.appendChild(link)
}
// Load CSS after a short delay to not block initial render
const timer = setTimeout(loadCSS, 100)
return () => clearTimeout(timer)
}, [])
return null
}

View File

@@ -72,7 +72,7 @@ export const ArticleInEvidenceCard = ({
{
"px-5 lg:px-16 py-6 lg:py-16": size === "lg",
"px-6 py-4 lg:p-8": size === "sm",
"px-6 lg:p-16": size === "xl",
"px-6 lg:p-16 justify-center": size === "xl",
"!p-0 lg:!p-0": !backgroundCover,
},
contentClassName
@@ -187,7 +187,7 @@ export const ArticleInEvidenceCard = ({
src={article.image ?? "/fallback.webp"}
alt="Article hero image"
fill
className="object-cover -z-10"
className="object-cover z-[0]"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
priority={false}
/>

View File

@@ -850,4 +850,34 @@ export const Icons = {
/>
</svg>
),
SliderArrowNext: ({ size = 16, ...props }: LucideProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="17"
height="17"
viewBox="0 0 17 17"
fill="none"
{...props}
>
<path
d="M12.6624 9.7091L0.489595 9.70981V7.71011L12.661 7.7094L7.29762 2.346L8.71183 0.931787L16.49 8.70996L8.71183 16.4881L7.29762 15.0739L12.6624 9.7091Z"
fill="#50C3E0"
/>
</svg>
),
SliderArrowPrev: ({ size = 16, ...props }: LucideProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="17"
height="17"
viewBox="0 0 17 17"
fill="none"
{...props}
>
<path
d="M4.33756 9.7091L16.5104 9.70981V7.71011L4.33898 7.7094L9.70238 2.346L8.28817 0.931787L0.509993 8.70996L8.28817 16.4881L9.70238 15.0739L4.33756 9.7091Z"
fill="#50C3E0"
/>
</svg>
),
}

View File

@@ -0,0 +1,55 @@
"use client"
import { Slider } from "../slider"
import { LABELS } from "@/app/labels"
import { useProjects } from "@/app/providers/ProjectsProvider"
import { ProjectInterface } from "@/lib/types"
import Link from "next/link"
export const ProjectGraduated = () => {
const { graduatedProjects } = useProjects()
return (
<div className="flex flex-col gap-10 justify-center dark:bg-anakiwa-975 py-16 lg:py-20 overflow-hidden bg-cover-gradient dark:bg-none">
<span className="dark:text-tuatara-100 text-tuatara-950 text-xl lg:text-3xl lg:leading-[45px] font-normal font-sans text-center lg:px-0 px-4">
{LABELS.PROJECTS_PAGE.GRADUATED_PROJECTS}
</span>
<div className="lg:px-4 px-4">
<Slider slidesToShow={4.2} gap="24px">
{graduatedProjects.map((project: ProjectInterface) => {
const website = project.links?.website
?.split("//")[1]
?.replace("/", "")
.replace("www.", "")
return (
<Link
href={project?.links?.website ?? "#"}
target="_blank"
rel="noopener noreferrer"
className="flex flex-col gap-4 w-full lg:max-w-[400px] p-8 rounded-[10px] bg-white border border-anakiwa-300 dark:border-anakiwa-400 dark:bg-anakiwa-975 hover:bg-anakiwa-100 dark:hover:bg-anakiwa-950 transition-all duration-300 cursor-pointer"
>
<div className="flex flex-col gap-4 h-[140px]">
<span className="font-sans text-2xl font-bold text-tuatara-950 dark:text-anakiwa-400">
{project.name}
</span>
<span className="font-sans text-base font-normal text-tuatara-500 dark:text-tuatara-100 line-clamp-2 lg:line-clamp-3">
{project.tldr}
</span>
</div>
<div className="flex flex-row items-center justify-between mt-auto w-full">
<span className="font-sans text-base font-normal text-tuatara-500 dark:text-tuatara-200 underline">
{website}
</span>
<div className="text-xs dark:text-black dark:bg-anakiwa-300 text-black bg-anakiwa-300 rounded-[3px] px-[6px] py-[2px]">
Graduated
</div>
</div>
</Link>
)
})}
</Slider>
</div>
</div>
)
}

View File

@@ -1,11 +1,11 @@
"use client"
import { Banner } from "../banner"
import { LABELS } from "@/app/labels"
import { Icons } from "../icons"
import { siteConfig } from "@/config/site"
import { Button } from "../ui/button"
import { AppLink } from "../app-link"
import { Banner } from "../banner"
import { Icons } from "../icons"
import { Button } from "../ui/button"
import { LABELS } from "@/app/labels"
import { siteConfig } from "@/config/site"
export const HomepageBanner = () => {
return (
@@ -13,23 +13,64 @@ export const HomepageBanner = () => {
title={LABELS.COMMON.CONNECT_WITH_US}
subtitle={LABELS.COMMON.CONNECT_WITH_US_DESCRIPTION}
>
<AppLink
href={siteConfig.links.discord}
external
variant="button"
passHref
className="mx-auto"
>
<Button>
<div className="flex items-center gap-2">
<Icons.discord fill="white" className="h-4" />
<span className="uppercase">
{LABELS.HOMEPAGE.JOIN_OUR_DISCORD}
</span>
<Icons.externalUrl fill="white" className="h-5" />
<div className="flex flex-col">
<div className="flex flex-col lg:flex-row gap-8 lg:gap-16 lg:text-left text-center mx-auto">
<div className="flex flex-col gap-4 lg:gap-12 w-full lg:max-w-[430px] p-10 rounded-[10px] bg-white border border-anakiwa-300 dark:border-anakiwa-400 dark:bg-anakiwa-975 transition-all">
<div className="flex flex-col gap-[10px]">
<span className="font-sans text-xl font-medium text-tuatara-950 dark:text-anakiwa-400">
{LABELS.HOMEPAGE.LEARN_AND_CONNECT.TITLE}
</span>
<span className="font-sans text-base font-normal text-tuatara-500 dark:text-tuatara-100 ">
{LABELS.HOMEPAGE.LEARN_AND_CONNECT.DESCRIPTION}
</span>
</div>
<AppLink
href={siteConfig.links.researchAndDevelopmentDiscord}
rel="noreferrer noopener"
variant="nav"
external
className="mx-auto"
>
<Button className="lg:w-full mx-auto lg:text-lg !text-xs">
<div className="flex items-center gap-2">
<Icons.discord fill="white" className="h-4" />
<span className="uppercase">
{LABELS.HOMEPAGE.LEARN_AND_CONNECT.ACTION}
</span>
<Icons.externalUrl fill="white" className="h-5" />
</div>
</Button>
</AppLink>
</div>
</Button>
</AppLink>
<div className="flex flex-col gap-4 lg:gap-12 w-full lg:max-w-[430px] p-10 rounded-[10px] bg-white border border-anakiwa-300 dark:border-anakiwa-400 dark:bg-anakiwa-975 transition-all">
<div className="flex flex-col gap-[10px]">
<span className="font-sans text-xl font-medium text-tuatara-950 dark:text-anakiwa-400">
{LABELS.HOMEPAGE.COLLABORATE_AND_CONTRIBUTE.TITLE}
</span>
<span className="font-sans text-base font-normal text-tuatara-500 dark:text-tuatara-100 ">
{LABELS.HOMEPAGE.COLLABORATE_AND_CONTRIBUTE.DESCRIPTION}
</span>
</div>
<AppLink
href={siteConfig.links.magicians}
rel="noreferrer noopener"
variant="nav"
external
className="mx-auto"
>
<Button className="lg:w-full mx-auto lg:text-lg !text-xs">
<div className="flex items-center gap-2">
<Icons.discord fill="white" className="h-4" />
<span className="uppercase">
{LABELS.HOMEPAGE.COLLABORATE_AND_CONTRIBUTE.ACTION}
</span>
<Icons.externalUrl fill="white" className="h-5" />
</div>
</Button>
</AppLink>
</div>
</div>
</div>
</Banner>
)
}

View File

@@ -49,7 +49,7 @@ export function SiteFooter() {
return (
<footer className="flex flex-col">
<div className="bg-tuatara-950 text-white py-8 text-left text-[14px] dark:bg-black">
<AppContent className="flex gap-10 py-2 lg:gap-24 justify-center">
<AppContent className="flex flex-col lg:flex-row gap-10 py-2 lg:gap-24 justify-center">
<LinksWrapper>
{MAIN_NAV.map(
(

214
components/slider.tsx Normal file
View File

@@ -0,0 +1,214 @@
"use client"
import { Icons } from "./icons"
import { Button } from "./ui/button"
import { useState, useRef, useEffect } from "react"
import { twMerge } from "tailwind-merge"
interface SliderProps {
children: React.ReactNode[]
className?: string
showNavigation?: boolean
autoSlide?: boolean
autoSlideInterval?: number
onSlideChange?: (index: number) => void
slidesToShow?: number
gap?: string
withDivider?: boolean
controlsPosition?: "top" | "bottom"
showControls?: boolean
}
export const Slider = ({
children,
className,
showNavigation = true,
autoSlide = false,
autoSlideInterval = 3000,
onSlideChange,
slidesToShow = 1,
gap = "0px",
withDivider = false,
controlsPosition = "bottom",
showControls = true,
}: SliderProps) => {
const [isMounted, setIsMounted] = useState(false)
const [isMobile, setIsMobile] = useState(false)
const [touchStart, setTouchStart] = useState(0)
const [touchEnd, setTouchEnd] = useState(0)
const [isHovered, setIsHovered] = useState(false)
const sliderRef = useRef<HTMLDivElement>(null)
// Ensure component is mounted before running client-side logic
useEffect(() => {
setIsMounted(true)
// Check if device is mobile
const checkMobile = () => {
setIsMobile(window.innerWidth < 1024) // lg breakpoint
}
checkMobile()
window.addEventListener("resize", checkMobile)
return () => window.removeEventListener("resize", checkMobile)
}, [])
const totalSlides = children.length
const effectiveSlidesToShow = isMobile ? 1 : slidesToShow
const maxSlideIndex = Math.max(
0,
totalSlides - Math.floor(effectiveSlidesToShow)
)
const [currentSlide, setCurrentSlide] = useState(0)
useEffect(() => {
if (!isMounted || !autoSlide || isHovered || currentSlide >= maxSlideIndex)
return
const interval = setInterval(() => {
setCurrentSlide((prev) => {
if (prev >= maxSlideIndex) {
return prev // Stop at the last slide
}
return prev + 1
})
}, autoSlideInterval)
return () => clearInterval(interval)
}, [
isMounted,
autoSlide,
autoSlideInterval,
maxSlideIndex,
isHovered,
currentSlide,
])
useEffect(() => {
if (onSlideChange) {
onSlideChange(currentSlide)
}
}, [currentSlide, onSlideChange])
const goToNext = () => {
if (!isMounted) return
setCurrentSlide((prev) => Math.min(prev + 1, maxSlideIndex))
}
const goToPrev = () => {
if (!isMounted) return
setCurrentSlide((prev) => Math.max(prev - 1, 0))
}
const handleTouchStart = (e: React.TouchEvent) => {
setTouchStart(e.targetTouches[0].clientX)
}
const handleTouchMove = (e: React.TouchEvent) => {
setTouchEnd(e.targetTouches[0].clientX)
}
const handleTouchEnd = () => {
if (!touchStart || !touchEnd) return
const distance = touchStart - touchEnd
const isLeftSwipe = distance > 50
const isRightSwipe = distance < -50
if (isLeftSwipe) {
goToNext()
} else if (isRightSwipe) {
goToPrev()
}
setTouchStart(0)
setTouchEnd(0)
}
const slideWidth = 100 / effectiveSlidesToShow
const translateX = currentSlide * slideWidth
// Prevent hydration mismatch
if (!isMounted) {
return (
<div className={twMerge("flex flex-col gap-5", className)}>
<div className="flex overflow-hidden">
<div
className="flex-shrink-0 h-full"
style={{ width: `${slideWidth}%` }}
>
{children[0]}
</div>
</div>
</div>
)
}
return (
<div
className={twMerge(
"flex flex-col gap-5",
controlsPosition === "top" && "flex-col-reverse"
)}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<div
className={twMerge(
"flex flex-col gap-5 relative overflow-hidden",
isMobile ? "" : "px-1",
className
)}
>
<div
ref={sliderRef}
className={twMerge(
"flex ease-in-out transition-transform duration-300",
withDivider && "divide-x divide-app-color-border"
)}
style={{
transform: `translateX(-${translateX}%)`,
gap: isMobile ? "0px" : gap,
}}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
>
{children.map((child, index) => (
<div
key={index}
className="flex-shrink-0 h-full"
style={{
width: `${slideWidth}%`,
}}
>
{child}
</div>
))}
</div>
</div>
{showNavigation && maxSlideIndex > 0 && showControls && (
<div className="flex gap-[10px] ml-auto justify-end lg:mr-4">
<Button
onClick={goToPrev}
disabled={currentSlide === 0}
aria-label="Previous slide"
className="!mx-0 !focus:outline-none !focus:ring-0 !focus:ring-offset-0 w-10 h-10"
>
<Icons.SliderArrowPrev />
</Button>
<Button
onClick={goToNext}
disabled={currentSlide >= maxSlideIndex}
aria-label="Next slide"
className="!mx-0 !focus:outline-none !focus:ring-0 !focus:ring-offset-0 w-10 h-10"
>
<Icons.SliderArrowNext />
</Button>
</div>
)}
</div>
)
}

View File

@@ -1,8 +1,7 @@
import { InputHTMLAttributes } from "react"
import { Icons } from "../icons"
import * as CheckboxComponent from "@radix-ui/react-checkbox"
import { CheckboxProps as CheckboxComponentProps } from "@radix-ui/react-checkbox"
import { Icons } from "../icons"
import { InputHTMLAttributes } from "react"
type CheckboxInputProps = InputHTMLAttributes<HTMLDivElement> &
CheckboxComponentProps
@@ -18,7 +17,7 @@ const Checkbox = ({ label, name, checked, ...props }: CheckboxProps) => {
<CheckboxComponent.Root
{...props}
checked={checked}
className="flex h-[14px] w-[14px] cursor-pointer items-center justify-center rounded-[1.5px] border-2 border-solid border-tuatara-200 bg-primary duration-100 ease-in aria-checked:border-black aria-checked:bg-black dark:border-anakiwa-800"
className="flex h-[14px] w-[14px] cursor-pointer items-center justify-center rounded-[1.5px] border-2 border-solid border-tuatara-200 bg-transparent duration-100 ease-in aria-checked:border-black aria-checked:bg-black dark:border-anakiwa-800"
id={name}
>
<CheckboxComponent.Indicator className="text-white">

View File

@@ -25,6 +25,9 @@ export const siteConfig = {
"https://github.com/privacy-ethereum/acceleration-program",
coreProgram:
"https://docs.google.com/forms/d/e/1FAIpQLSendzYY0z_z7fZ37g3jmydvzS9I7OWKbY2JrqAnyNqeaBHvMQ/viewform",
magicians: "https://ethereum-magicians.org",
researchAndDevelopmentDiscord:
"https://discord.com/channels/595666850260713488/1410354969386946560",
},
addGithubResource:
"https://github.com/privacy-ethereum/website-v2/blob/main/content/resources.md",

View File

@@ -28,6 +28,7 @@ tldr: "Brief project summary" # Short description of the project
# ========================================
category: "research" # Project category: "research" | "devtools" | "application"
license: "MIT" # Project license
graduated: false # Project graduated
tags: # Project tags
keywords: ["tag1", "tag2", "tag3"] # Relevant keywords
themes: ["privacy", "scalability"] # Project themes

View File

@@ -1,6 +1,7 @@
---
id: "cursive"
name: "Cursive"
graduated: true
image: "cursive.webp"
section: "grant"
projectStatus: "active"
@@ -23,7 +24,7 @@ tags:
themes: ["build", "play"]
links:
github: "https://github.com/cursive-team"
website: "https://www.cursive.team/"
website: "https://www.cursive.computer/"
---
What began as an experimental initiative to transform PSE's theoretical cryptography research into tangible applications has evolved into a dedicated lab that incubates practical ways to connect people through applied cryptography.

View File

@@ -1,7 +1,8 @@
---
id: "openpassport"
name: "OpenPassport"
name: "Self"
image: "openpassport.webp"
graduated: true
section: "grant"
projectStatus: "active"
category: "application"

View File

@@ -3,6 +3,7 @@ id: "zk-email"
name: "ZK Email"
image: "zk-email.webp"
section: "collaboration"
graduated: true
projectStatus: "active"
category: "application"
tldr: "ZK Email is a library that allows for anonymous verification of email signatures while masking specific data."

View File

@@ -1,6 +1,7 @@
---
id: "zkp2p"
name: "ZKP2P"
graduated: true
image: "zkp2p.webp"
section: "grant"
projectStatus: "active"

View File

@@ -15,7 +15,7 @@ html {
/* Custom responsive navigation */
.nav-responsive {
display: none;
display: none !important;
}
@media (min-width: 768px) {

View File

@@ -1,89 +0,0 @@
export const criticalCSS = `
/* Critical CSS - Base styles that prevent layout shifts */
:root {
--background: #FFFFFF;
--foreground: #0F172A;
--primary: #E1523A;
--text-primary: #242528;
--text-secondary: #166F8E;
}
.dark {
--background: #000000;
--foreground: #FFFFFF;
--text-primary: #FFFFFF;
--text-secondary: #FFFFFF;
--primary: #E1523A;
}
* {
box-sizing: border-box;
border: 0 solid #e5e7eb;
}
html {
font-family: var(--font-sans, system-ui, -apple-system, sans-serif);
line-height: 1.5;
-webkit-text-size-adjust: 100%;
text-rendering: geometricPrecision;
}
body {
margin: 0;
font-family: inherit;
line-height: inherit;
background-color: var(--background);
color: var(--foreground);
font-feature-settings: 'rlig' 1, 'calt' 1;
}
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-display, inherit);
font-weight: 700;
margin: 0;
}
/* Critical layout styles */
.min-h-screen { min-height: 100vh; }
.flex { display: flex; }
.flex-col { flex-direction: column; }
.flex-1 { flex: 1 1 0%; }
.relative { position: relative; }
.hidden { display: none; }
.block { display: block; }
/* Critical responsive utilities */
@media (min-width: 1024px) {
.lg\\:hidden { display: none; }
.lg\\:block { display: block; }
.lg\\:h-\\[600px\\] { height: 600px; }
}
/* Critical hero section styles */
.h-\\[500px\\] { height: 500px; }
.w-full { width: 100%; }
.overflow-hidden { overflow: hidden; }
.bg-cover { background-size: cover; }
.bg-no-repeat { background-repeat: no-repeat; }
.bg-center { background-position: center; }
/* Critical text utilities */
.text-center { text-align: center; }
.text-3xl { font-size: 1.875rem; line-height: 2.25rem; }
.font-medium { font-weight: 500; }
.uppercase { text-transform: uppercase; }
@media (min-width: 1024px) {
.lg\\:text-5xl { font-size: 3rem; line-height: 1; }
}
/* Critical spacing */
.gap-10 { gap: 2.5rem; }
.gap-4 { gap: 1rem; }
.py-10 { padding-top: 2.5rem; padding-bottom: 2.5rem; }
@media (min-width: 1024px) {
.lg\\:py-\\[130px\\] { padding-top: 130px; padding-bottom: 130px; }
.lg\\:gap-8 { gap: 2rem; }
}
`

View File

@@ -84,6 +84,7 @@ export interface ProjectTeamMember {
export interface ProjectInterface {
id: string
hasWiki?: boolean // show project with wiki page template
graduated?: boolean
youtubeLinks?: string[]
team?: ProjectTeamMember[]
license?: string