feat: PageSpeed Insights improvements (#545)

* feat: PageSpeed Insights improvements
This commit is contained in:
Kalidou Diagne
2025-09-01 12:01:00 +08:00
committed by GitHub
parent 27b23cc40c
commit 31763f7662
180 changed files with 1938 additions and 10537 deletions

View File

@@ -18,8 +18,8 @@ on:
default: "prod"
type: choice
options:
- prod
- stg
- prod
- stg
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -38,29 +38,29 @@ jobs:
steps:
- name: Check branch
run: |
if [ "${{ env.DATA_ENV }}" = "prod" ]; then
if [ "$GITHUB_REF_NAME" != "main" ]; then
echo "Operation not permitted"
exit 1
fi
if [ "${{ env.DATA_ENV }}" = "prod" ]; then
if [ "$GITHUB_REF_NAME" != "main" ]; then
echo "Operation not permitted"
exit 1
fi
fi
- name: Checkout
uses: actions/checkout@v5
with:
persist-credentials: false
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v2
with:
role-to-assume: arn:aws:iam::490752553772:role/pse-web-ecs-deploy-slc
role-duration-seconds: 1800
aws-region: eu-central-1
- name: Build and Push images to ECR
run: |
.github/scripts/build.sh ${{ env.DATA }} ${{ env.DATA_ENV }}
.github/scripts/build.sh ${{ env.DATA }} ${{ env.DATA_ENV }}
- name: Update Deployment
run: |
.github/scripts/deploy.sh ${{ env.DATA_ENV }}
.github/scripts/deploy.sh ${{ env.DATA_ENV }}

13
.swcrc Normal file
View File

@@ -0,0 +1,13 @@
{
"jsc": {
"target": "es2022"
},
"env": {
"targets": {
"chrome": "91",
"firefox": "90",
"safari": "14.1",
"edge": "91"
}
}
}

View File

@@ -34,9 +34,9 @@ export default async function AboutPage() {
<AppContent className=" w-full !max-w-[860px] mx-auto">
<div className="flex flex-col gap-16">
<div className="flex flex-col gap-10">
<h2 className="font-sans text-base font-bold uppercase tracking-[4px] text-black dark:text-white text-center">
<h1 className="font-sans text-base font-bold uppercase tracking-[4px] text-black dark:text-white text-center">
Our Mission
</h2>
</h1>
<span className="text-base lg:text-xl font-sans dark:text-tuatara-200 text-tuatara-500 mx-auto">
As Privacy Stewards of Ethereum (PSE), our mission is to
deliver privacy to the Ethereum ecosystem. <br />

View File

@@ -1,39 +0,0 @@
import React from "react"
import { Metadata } from "next"
import { Devcon7Booths } from "./sections/Devcon7Booths"
import { Devcon7Header } from "./sections/Devcon7Header"
import { Devcon7Section } from "./sections/Devcon7Section"
import { Devcon7Slider } from "./sections/Devcon7Slider"
export const metadata: Metadata = {
title: "Devcon 7",
description: "PSE x Devcon 7 Southeast Asia",
openGraph: {
images: [
{
url: "/devcon-7-cover.png",
width: 1200,
height: 630,
},
],
},
}
export default async function DevconPage() {
return (
<>
<div className="flex flex-col lg:pb-[120px]">
<div className="flex flex-col">
<Devcon7Header />
<Devcon7Slider />
</div>
<div className="flex flex-col gap-10 lg:gap-14 pt-8 lg:pt-[60px] mx-auto max-w-[950px]">
<Devcon7Section />
</div>
</div>
</>
)
}

View File

@@ -1,76 +0,0 @@
import Image from "next/image"
import Link from "next/link"
import { AppContent } from "@/components/ui/app-content"
import { Icons } from "@/components/icons"
import { booths } from "@/content/events/devcon-7"
export const Devcon7Booths = () => {
return (
<AppContent>
<div className="flex flex-col gap-10 lg:gap-14">
<h2 className="lg:max-w-[700px] mx-auto font-bold text-black text-lg lg:text-[32px] leading-[110%] font-display text-center px-6">
{`We're excited to connect and collaborate on building meaningful tools
with cryptography.`}
</h2>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-14">
{booths?.map((booth, index) => {
return (
<div
key={index}
className="flex flex-col bg-anakiwa-50 rounded-lg overflow-hidden border border-[rgba(8, 27, 26, 0.15)]"
>
<div className="h-[160px] lg:h-[240px] bg-slate-50 relative bg-[lightgray 50% / cover no-repeat]">
<Image
src={booth.image}
alt={`booth image ${index + 1}`}
fill
className="object-cover"
priority
/>
</div>
<div className="flex flex-col gap-3 p-4 lg:p-6">
<span className="text-anakiwa-500 text-xs font-sans leading-5 tracking-[2.5px] uppercase font-bold">
BOOTH
</span>
<span className="text-[22px] leading-[24px] text-primary font-display font-bold">
{booth?.title}
</span>
<span className="text-xs text-primary font-sans font-normal">
{booth?.description}
{booth?.learMore && (
<Link
href={booth?.learMore?.url ?? "#"}
target="_blank"
className="block pt-2"
>
{`${booth.learMore.description}: `}{" "}
<span className="underline">
{booth?.learMore?.label}
</span>
</Link>
)}
</span>
<div className="flex flex-col">
<div className="flex items-center gap-[6px]">
<Icons.time className="text-tuatara-500" />
<span className="font-sans text-tuatara-500 text-[10px] font-normal">
{booth?.date}
</span>
</div>
<div className="flex gap-[6px] items-center">
<Icons.eventLocation className="text-tuatara-500" />
<span className="font-sans text-tuatara-500 text-[10px] font-normal">
{booth?.location}
</span>
</div>
</div>
</div>
</div>
)
})}
</div>
</div>
</AppContent>
)
}

View File

@@ -1,22 +0,0 @@
import Image from "next/image"
export const Devcon7Header = () => {
return (
<div className="relative h-[135px] lg:h-[135px]">
<Image
src="/images/devcon-7-mobile.svg"
alt="Devcon 7 Banner"
fill
className="block object-cover md:hidden"
priority
/>
<Image
src="/images/devcon-7-desktop.svg"
alt="Devcon 7 Banner"
fill
className="hidden object-cover md:block"
priority
/>
</div>
)
}

View File

@@ -1,180 +0,0 @@
"use client"
import { useState } from "react"
import Link from "next/link"
import { cva } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { Icons } from "@/components/icons"
import { events } from "@/content/events/devcon-7"
const tableSection = cva("lg:grid lg:grid-cols-[200px_1fr_160px_20px] lg:gap-8")
const tableSectionTitle = cva(
"text-anakiwa-500 text-base lg:text-xs font-sans leading-5 tracking-[2.5px] uppercase font-bold lg:pb-3"
)
const EventCard = ({ event = {}, speakers = [], location = "" }: any) => {
const [isOpen, setIsOpen] = useState(false)
const getYouTubeEmbedURL = (url: string) => {
const match = url.match(
/(?:youtube\.com\/(?:watch\?v=|live\/)|youtu\.be\/)([^?&]+)/
)
return match ? `https://www.youtube.com/embed/${match[1]}` : null
}
return (
<div
className={cn(
"flex flex-col gap-3",
tableSection(),
"py-4 px-6 lg:p-0 lg:pt-[30px] lg:pb-5 border-b border-b-tuatara-200"
)}
>
<div className="flex flex-col gap-1 order-3 lg:order-1">
<span className="text-sm text-primary font-bold font-sans leading-5">
{event?.date}
</span>
<div className="grid grid-cols-[1fr_16px] lg:grid-cols-1">
<div className="flex flex-col">
<div className="flex items-center gap-[6px]">
<Icons.time className="text-tuatara-500" />
<span className="font-sans text-tuatara-500 text-xs lg:text-sm leading-5 font-normal">
{event?.time}
</span>
</div>
<div className="flex gap-[6px] items-center">
<Icons.eventLocation className="text-tuatara-500" />
<span className="font-sans text-tuatara-500 text-xs lg:text-sm leading-5 font-normal">
{location}
</span>
</div>
<div className="flex flex-wrap lg:flex-col gap-1 pt-1">
{speakers?.map((speaker: any, index: number) => {
return (
<Link
key={index}
className="text-sm text-anakiwa-500 underline break-all"
href={speaker.url ?? "#"}
>
{speaker.label}
</Link>
)
})}
</div>
</div>
</div>
</div>
<div className="flex flex-col justify-start gap-[10px] lg:order-2 order-2">
<div className="flex items-center justify-between">
<Link
href={event?.url ?? "#"}
target="_blank"
className="text-[22px] inline-flex leading-[24px] text-primary underline font-display hover:text-anakiwa-500 font-bold duration-200"
>
{event?.title}
</Link>
<button
className="lg:hidden flex"
onClick={() => {
setIsOpen(!isOpen)
}}
>
{isOpen ? (
<Icons.minus
className={cn(
"size-5 ml-auto",
"transition-transform duration-200"
)}
/>
) : (
<Icons.plus
className={cn(
"size-5 ml-auto",
"transition-transform duration-200"
)}
/>
)}
</button>
</div>
<div
className={cn(
"lg:max-h-none lg:opacity-100 lg:block",
"transition-all duration-300 overflow-hidden",
isOpen ? "max-h-[1000px] opacity-100" : "max-h-0 opacity-0",
"lg:transition-none lg:overflow-visible"
)}
>
<span className="text-base leading-6 text-primary font-sans font-normal">
{event?.description}
</span>
<div className="pt-2 flex lg:!hidden">
{event?.youtubeLink && (
<iframe
width="100%"
height="230"
src={getYouTubeEmbedURL(event?.youtubeLink) ?? ""}
allowFullScreen
style={{ borderRadius: "10px", overflow: "hidden" }}
/>
)}
</div>
</div>
</div>
<div className="relative lg:order-3 grid gap-5 pb-3 lg:pb-0 grid-cols-[1fr_32px] lg:grid-cols-1">
<div className="hidden lg:flex flex-wrap lg:flex-col gap-1">
{event?.youtubeLink && (
<iframe
width="240"
height="140"
src={getYouTubeEmbedURL(event?.youtubeLink) ?? ""}
allowFullScreen
style={{ borderRadius: "10px", overflow: "hidden" }}
/>
)}
</div>
</div>
<div className="order-4 lg:flex hidden"></div>
</div>
)
}
export const Devcon7Section = () => {
return (
<div className="flex flex-col gap-10 relative">
<div className="flex flex-col gap-3 lg:gap-10 lg:container">
<div
className={cn(
tableSection(),
"!hidden lg:border-b lg:border-anakiwa-200"
)}
>
<div className={cn(tableSectionTitle(), "lg:flex hidden")}>
Details
</div>
<div className={cn(tableSectionTitle(), "lg:text-left text-center")}>
Talks
</div>
<div className={cn(tableSectionTitle(), "lg:flex hidden")}>
Speakers
</div>
<div className="lg:flex hidden"></div>
</div>
<div className="flex flex-col">
{events?.map(({ event, speakers, location }, index) => {
return (
<EventCard
key={index}
event={event}
speakers={speakers}
location={location}
/>
)
})}
</div>
</div>
</div>
)
}

View File

@@ -1,88 +0,0 @@
"use client"
import React, { useEffect, useState } from "react"
import Slider from "react-slick"
import "slick-carousel/slick/slick.css"
import "slick-carousel/slick/slick-theme.css"
import Image from "next/image"
const AnySlider = Slider as any
export const Devcon7Slider = () => {
const settings = {
dots: true,
infinite: true,
speed: 500,
slidesToShow: 4.2,
slidesToScroll: 1,
autoplay: false,
responsive: [
{
breakpoint: 1400,
settings: {
slidesToShow: 5.4,
slidesToScroll: 1,
infinite: true,
dots: true,
},
},
{
breakpoint: 1024,
settings: {
slidesToShow: 4.2,
slidesToScroll: 1,
infinite: true,
dots: true,
},
},
{
breakpoint: 600,
settings: {
slidesToShow: 1,
slidesToScroll: 1,
initialSlide: 1,
},
},
],
appendDots: (dots: React.ReactNode) => (
<div
style={{
borderRadius: "10px",
padding: "10px",
}}
>
<ul style={{ margin: "0px" }}> {dots} </ul>
</div>
),
customPaging: (i: number) => <div className="h-4 w-4 rounded-full mt-5" />,
}
const images = [
"/images/devcon-7/devcon-7-overview-1.jpg",
"/images/devcon-7/devcon-7-overview-2.jpg",
"/images/devcon-7/devcon-7-overview-3.jpg",
"/images/devcon-7/devcon-7-overview-4.jpg",
"/images/devcon-7/devcon-7-overview-5.jpg",
"/images/devcon-7/devcon-7-overview-6.jpg",
]
return (
<AnySlider {...settings}>
{images.map((image, index) => (
<div key={index} className="relative h-[320px] w-full overflow-hidden">
<Image
src={image}
alt={`Devcon 7 Overview ${index + 1}`}
fill
className="object-cover"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
priority={index < 2}
quality={85}
loading={index < 2 ? "eager" : "lazy"}
/>
</div>
))}
</AnySlider>
)
}

View File

@@ -2,14 +2,39 @@ import { LABELS } from "../labels"
import { BlogRecentArticles } from "@/components/blog/blog-recent-articles"
import { Icons } from "@/components/icons"
import { HomepageBanner } from "@/components/sections/HomepageBanner"
import { HomepageVideoFeed } from "@/components/sections/HomepageVideoFeed"
import { OurWork } from "@/components/sections/OurWork"
import { ParallaxHero } from "@/components/sections/ParallaxHero"
import { AppContent } from "@/components/ui/app-content"
import { Button } from "@/components/ui/button"
import dynamic from "next/dynamic"
import Link from "next/link"
import { Suspense } from "react"
// Lazy load video feed to improve initial page load
const HomepageVideoFeed = dynamic(
() =>
import("@/components/sections/HomepageVideoFeed").then((mod) => ({
default: mod.HomepageVideoFeed,
})),
{
ssr: false, // Don't block server rendering
loading: () => (
<section className="mx-auto py-10 lg:pt-0 lg:pb-20 bg-white dark:bg-black w-full">
<div className="flex flex-col gap-8 lg:max-w-[1200px] w-full mx-auto">
<div className="animate-pulse">
<div className="h-6 bg-gray-300 rounded w-32 mx-auto mb-8"></div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{[1, 2, 3].map((i) => (
<div key={i} className="aspect-video bg-gray-300 rounded"></div>
))}
</div>
</div>
</div>
</section>
),
}
)
function BlogSection() {
return (
<Suspense
@@ -34,7 +59,7 @@ export default function IndexPage() {
<span className="font-sans text-base text-center text-tuatara-950 dark:text-tuatara-100 font-bold uppercase tracking-[3.36px]">
{LABELS.HOMEPAGE.MISSION_TITLE}
</span>
<span className="dark:text-tuatara-100 font-sans text-base lg:text-xl text-center text-tuatara-500 font-medium">
<span className="dark:text-tuatara-100 font-sans text-base lg:text-xl text-center text-tuatara-500 font-medium lg:px-0 px-4">
{LABELS.HOMEPAGE.MISSION_DESCRIPTION}
</span>
<Link href="/about">

View File

@@ -1,22 +1,21 @@
"use client"
import { useCallback, useEffect, useRef, useState } from "react"
import Image from "next/image"
import { ReactNode } from "react-markdown/lib/ast-to-react"
import { twMerge } from "tailwind-merge"
import { siteConfig } from "@/config/site"
import { cn, interpolate } from "@/lib/utils"
import { Accordion } from "@/components/ui/accordion"
import { Button } from "@/components/ui/button"
import { LABELS } from "@/app/labels"
import { AppLink } from "@/components/app-link"
import { Banner } from "@/components/banner"
import { Card } from "@/components/cards/card"
import { TableRowCard } from "@/components/cards/table-row-card"
import { Divider } from "@/components/divider"
import { Icons } from "@/components/icons"
import { PageHeader } from "@/components/page-header"
import { TableRowCard } from "@/components/cards/table-row-card"
import { LABELS } from "@/app/labels"
import { AppLink } from "@/components/app-link"
import { Accordion } from "@/components/ui/accordion"
import { Button } from "@/components/ui/button"
import { siteConfig } from "@/config/site"
import { cn, interpolate } from "@/lib/utils"
import Image from "next/image"
import { useCallback, useEffect, useRef, useState } from "react"
import { ReactNode } from "react-markdown/lib/ast-to-react"
import { twMerge } from "tailwind-merge"
type ProgramDetailProps = {
region?: string
@@ -165,8 +164,8 @@ export const ProgramPageContent = () => {
width={280}
height={280}
className="mx-auto h-[256px] w-[290px] lg:ml-auto lg:h-[428px] lg:w-[484px]"
src="/images/programs.png"
alt="computer image"
src="/images/programs.webp"
alt=""
/>
}
/>
@@ -178,9 +177,9 @@ export const ProgramPageContent = () => {
className="relative ml-auto hidden bg-background p-2 lg:block"
>
<div className="absolute right-0 mt-[80px] flex flex-col gap-4 lg:w-[220px] xl:w-[320px] xl:px-8">
<h6 className="font-display text-lg font-bold text-secondary">
<h2 className="font-display text-lg font-bold text-secondary">
{LABELS.COMMON.ON_THIS_PAGE}
</h6>
</h2>
<ul className="text-normal font-sans text-primary">
{ProgramSections.map((id: string) => {
const label = getSectionTitle(id)

View File

@@ -1,12 +1,11 @@
import { Suspense } from "react"
import { Metadata } from "next"
import { AppContent } from "@/components/ui/app-content"
import { Label } from "@/components/ui/label"
import { LABELS } from "@/app/labels"
import ProjectFiltersBar from "@/components/project/project-filters-bar"
import { ProjectList } from "@/components/project/project-list"
import { ProjectResultBar } from "@/components/project/project-result-bar"
import { LABELS } from "@/app/labels"
import { AppContent } from "@/components/ui/app-content"
import { Label } from "@/components/ui/label"
import { Metadata } from "next"
import { Suspense } from "react"
export const metadata: Metadata = {
title: "Project Library",

View File

@@ -1,8 +1,8 @@
import { NextRequest, NextResponse } from "next/server"
import { getArticles } from "@/lib/content"
import { NextRequest, NextResponse } from "next/server"
// Cache control
export const revalidate = 60 // Revalidate cache after 60 seconds
// Cache control - Extended for better performance
export const revalidate = 1800 // Revalidate cache after 30 minutes
export const dynamic = "force-dynamic" // Ensure the route is always evaluated
export async function GET(request: NextRequest) {
@@ -20,10 +20,17 @@ export async function GET(request: NextRequest) {
project,
})
return NextResponse.json({
articles,
success: true,
})
return NextResponse.json(
{
articles,
success: true,
},
{
headers: {
"Cache-Control": "public, s-maxage=1800, stale-while-revalidate=3600",
},
}
)
} catch (error) {
console.error("Error fetching articles:", error)
return NextResponse.json(

View File

@@ -1,8 +1,8 @@
import { NextRequest, NextResponse } from "next/server"
import { getProjects } from "@/lib/content"
import { NextRequest, NextResponse } from "next/server"
// Cache control
export const revalidate = 60 // Revalidate cache after 60 seconds
// Cache control - Extended for better performance
export const revalidate = 1800 // Revalidate cache after 30 minutes
export const dynamic = "force-dynamic" // Ensure the route is always evaluated
export async function GET(request: NextRequest) {
@@ -15,7 +15,11 @@ export async function GET(request: NextRequest) {
try {
const projects = getProjects({ tag, limit, status })
return NextResponse.json(projects ?? [])
return NextResponse.json(projects ?? [], {
headers: {
"Cache-Control": "public, s-maxage=1800, stale-while-revalidate=3600",
},
})
} catch (error) {
console.error("Error fetching projects:", error)
return NextResponse.json(

View File

@@ -0,0 +1,32 @@
import { NextRequest } from "next/server"
export async function GET(request: NextRequest) {
try {
// Fetch the Matomo script from the correct CDN
const response = await fetch("https://psedev.matomo.cloud/matomo.js", {
headers: {
"User-Agent": request.headers.get("user-agent") || "",
},
})
if (!response.ok) {
return new Response("Failed to fetch Matomo script", { status: 500 })
}
const script = await response.text()
return new Response(script, {
status: 200,
headers: {
"Content-Type": "application/javascript; charset=utf-8",
"Cache-Control":
"public, max-age=86400, s-maxage=86400, must-revalidate",
"X-Content-Type-Options": "nosniff",
"Referrer-Policy": "strict-origin-when-cross-origin",
},
})
} catch (error) {
console.error("Error proxying Matomo script:", error)
return new Response("Internal server error", { status: 500 })
}
}

View File

@@ -1,6 +1,9 @@
import algoliasearch from "algoliasearch"
import { NextRequest, NextResponse } from "next/server"
// Cache search results for better performance
export const revalidate = 900 // Revalidate cache after 15 minutes
const appId =
process.env.ALGOLIA_APP_ID || process.env.NEXT_PUBLIC_ALGOLIA_APP_ID || ""
const apiKey =
@@ -61,11 +64,19 @@ export async function GET(request: NextRequest) {
const index = searchClient.initIndex(indexName)
const response = await index.search(transformedQuery, { hitsPerPage })
return NextResponse.json({
hits: response.hits,
status: "success",
availableIndexes: allIndexes,
})
return NextResponse.json(
{
hits: response.hits,
status: "success",
availableIndexes: allIndexes,
},
{
headers: {
"Cache-Control":
"public, s-maxage=900, stale-while-revalidate=1800",
},
}
)
}
// Otherwise search across all configured indexes
@@ -88,11 +99,18 @@ export async function GET(request: NextRequest) {
(result) => result.hits && result.hits.length > 0
)
return NextResponse.json({
results: nonEmptyResults,
status: "success",
availableIndexes: allIndexes,
})
return NextResponse.json(
{
results: nonEmptyResults,
status: "success",
availableIndexes: allIndexes,
},
{
headers: {
"Cache-Control": "public, s-maxage=900, stale-while-revalidate=1800",
},
}
)
} catch (error: any) {
console.error("Global search error:", error)
return NextResponse.json(

View File

@@ -11,7 +11,14 @@ export async function GET() {
const videos = await getVideosFromRSS()
return NextResponse.json({ videos })
return NextResponse.json(
{ videos },
{
headers: {
"Cache-Control": "public, s-maxage=3600, stale-while-revalidate=7200",
},
}
)
} catch (error) {
console.error("Error fetching videos:", error)
return NextResponse.json({ videos: [] })

View File

@@ -1,5 +1,8 @@
import { NextRequest, NextResponse } from "next/server"
// Cache video data for better performance
export const revalidate = 1800 // Revalidate cache after 30 minutes
interface YoutubeVideoResponse {
items: {
id: string
@@ -23,11 +26,12 @@ interface YoutubeVideoResponse {
}[]
}
// Helper function to add CORS headers
// Helper function to add CORS and cache headers
function corsHeaders() {
return {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
"Cache-Control": "public, s-maxage=1800, stale-while-revalidate=3600",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
}
}

View File

@@ -1,3 +1,4 @@
import { criticalCSS } from "@/lib/critical-css"
import "@/globals.css"
import { ThemeProvider } from "./components/layouts/ThemeProvider"
import { GlobalProviderLayout } from "@/components/layouts/GlobalProviderLayout"
@@ -7,41 +8,25 @@ import { TailwindIndicator } from "@/components/tailwind-indicator"
import { siteConfig } from "@/config/site"
import { cn } from "@/lib/utils"
import { Metadata, Viewport } from "next"
import { DM_Sans, Inter, Space_Grotesk } from "next/font/google"
import { DM_Sans, Space_Grotesk } from "next/font/google"
import Script from "next/script"
const inter = Inter({
subsets: ["latin"],
variable: "--font-inter",
weight: ["400", "500", "600", "700"],
display: "swap",
preload: true,
fallback: ["system-ui", "sans-serif"],
adjustFontFallback: true,
})
const display = Space_Grotesk({
subsets: ["latin"],
variable: "--font-display",
weight: ["400", "500", "600", "700"],
display: "swap",
preload: true,
fallback: ["system-ui", "sans-serif"],
adjustFontFallback: true,
})
const sans = DM_Sans({
subsets: ["latin"],
variable: "--font-sans",
weight: ["400", "500", "600", "700"],
weight: ["400", "500", "700"],
display: "swap",
preload: true,
fallback: ["system-ui", "sans-serif"],
adjustFontFallback: true,
})
const fonts = [inter, display, sans]
export const viewport: Viewport = {
themeColor: [
{ media: "(prefers-color-scheme: light)", color: "white" },
@@ -122,7 +107,7 @@ export default function RootLayout({ children }: RootLayoutProps) {
return (
<html
lang="en"
className={cn(inter.variable, display.variable, sans.variable)}
className={cn(display.variable, sans.variable)}
suppressHydrationWarning
>
<Script id="matomo-tracking" strategy="afterInteractive">
@@ -136,11 +121,52 @@ export default function RootLayout({ children }: RootLayoutProps) {
_paq.push(['setTrackerUrl', u+'matomo.php']);
_paq.push(['setSiteId', '1']);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.async=true; g.src='//cdn.matomo.cloud/psedev.matomo.cloud/matomo.js'; s.parentNode.insertBefore(g,s);
g.async=true;
g.defer=true;
g.src='/api/proxy-matomo';
// Add cache control
g.setAttribute('data-cache', 'true');
s.parentNode.insertBefore(g,s);
})();
`}
</Script>
<head />
<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
rel="preconnect"
href="https://fonts.gstatic.com"
crossOrigin="anonymous"
/>
{/* External service preconnects */}
<link rel="dns-prefetch" href="https://psedev.matomo.cloud" />
{/* Preload critical scripts */}
<link rel="preload" href="/api/proxy-matomo" as="script" />
{/* YouTube preconnects for video content */}
<link rel="preconnect" href="https://www.youtube.com" />
<link rel="preconnect" href="https://img.youtube.com" />
<link rel="preconnect" href="https://i.ytimg.com" />
{/* Static asset preloading */}
<link rel="prefetch" href="/favicon.svg" />
{/* Critical resource preloading */}
<link rel="dns-prefetch" href="https://fonts.googleapis.com" />
<link rel="dns-prefetch" href="https://fonts.gstatic.com" />
{/* External service optimization */}
<link rel="dns-prefetch" href="https://www.googletagmanager.com" />
{/* Algolia search preconnect for faster search */}
<link rel="preconnect" href="https://latency-dsn.algolia.net" />
<link rel="dns-prefetch" href="https://search.algolia.com" />
</head>
<body suppressHydrationWarning>
<GlobalProviderLayout>
<ThemeProvider>

View File

@@ -1,5 +1,7 @@
"use client"
import { ProjectsProvider } from "./ProjectsProvider"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import {
createContext,
useContext,
@@ -7,8 +9,6 @@ import {
useState,
useEffect,
} from "react"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { ProjectsProvider } from "./ProjectsProvider"
interface GlobalContextType {
children?: ReactNode
@@ -30,7 +30,19 @@ const queryClient = new QueryClient({
const PSE_DARK_MODE_KEY = "pse-dark-mode"
export function GlobalProvider({ children }: { children: ReactNode }) {
const [isDarkMode, setIsDarkMode] = useState(false)
// Start with system preference to avoid null return
const [isDarkMode, setIsDarkMode] = useState(() => {
// Server-side safe default
if (typeof window === "undefined") return false
// Try to get system preference immediately
try {
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
return mediaQuery.matches
} catch {
return false
}
})
const [isInitialized, setIsInitialized] = useState(false)
// Initialize dark mode from localStorage or system preference
@@ -66,10 +78,6 @@ export function GlobalProvider({ children }: { children: ReactNode }) {
setIsDarkMode: handleSetIsDarkMode,
}
if (!isInitialized) {
return null
}
return (
<QueryClientProvider client={queryClient}>
<ProjectsProvider>

24
components/CSSLoader.tsx Normal file
View File

@@ -0,0 +1,24 @@
"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

@@ -1,16 +1,24 @@
import { ReactNode } from "react"
import { AppContent } from "./ui/app-content"
import { cn } from "@/lib/utils"
import { ReactNode } from "react"
type BannerProps = {
title: ReactNode
subtitle?: string
children?: ReactNode
headingLevel?: "h2" | "h3" | "h4"
className?: string
}
const Banner = ({ title, subtitle, children, className }: BannerProps) => {
const Banner = ({
title,
subtitle,
children,
headingLevel = "h2",
className = "",
}: BannerProps) => {
const HeadingTag = headingLevel
return (
<section
className={cn(
@@ -22,9 +30,9 @@ const Banner = ({ title, subtitle, children, className }: BannerProps) => {
<AppContent className="flex flex-col gap-8">
<div className="flex flex-col items-center text-center">
{typeof title === "string" ? (
<h6 className="py-4 font-sans text-base font-bold uppercase tracking-[4px] text-tuatara-950 dark:text-tuatara-100">
<HeadingTag className="py-4 font-sans text-base font-bold uppercase tracking-[4px] text-primary dark:text-white">
{title}
</h6>
</HeadingTag>
) : (
title
)}

View File

@@ -5,6 +5,7 @@ import { Button } from "../ui/button"
import { LABELS } from "@/app/labels"
import { Article } from "@/lib/content"
import { cn } from "@/lib/utils"
import Image from "next/image"
import Link from "next/link"
interface ArticleInEvidenceCardProps {
@@ -67,9 +68,9 @@ export const ArticleInEvidenceCard = ({
return (
<div
className={cn(
"duration-200 flex flex-col gap-4 text-left relative z-[1] w-full",
"duration-200 flex flex-col gap-4 text-left relative z-[1] w-full justify-center",
{
"px-5 lg:px-16 py-6 lg:py-16 ": size === "lg",
"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",
"!p-0 lg:!p-0": !backgroundCover,
@@ -146,7 +147,11 @@ export const ArticleInEvidenceCard = ({
</div>
)}
{showReadMore && (
<Link href={`/blog/${article.id}`} className="ml-auto mt-4">
<Link
href={`/blog/${article.id}`}
className="ml-auto mt-4"
aria-label={`Read more about ${article.title}`}
>
<Button className="uppercase ml-auto" variant="secondary">
<div className="flex items-center gap-2">
<span className="!text-center">
@@ -169,7 +174,7 @@ export const ArticleInEvidenceCard = ({
>
<div
className={cn(
"relative flex flex-col gap-5 w-full items-center after:absolute after:inset-0 after:content-[''] after:bg-black after:opacity-20 group-hover:after:opacity-80 transition-opacity duration-300 after:z-[0]",
"relative flex flex-col gap-5 w-full items-center overflow-hidden after:absolute after:inset-0 after:content-[''] after:bg-black after:opacity-20 group-hover:after:opacity-80 transition-opacity duration-300 after:z-[0]",
{
"aspect-video": !className?.includes("h-full"),
"min-h-[200px] max-h-[200px]": !backgroundCover,
@@ -177,12 +182,15 @@ export const ArticleInEvidenceCard = ({
},
className
)}
style={{
backgroundImage: `url(${article.image ?? "/fallback.webp"})`,
backgroundSize: "cover",
backgroundPosition: "center centers",
}}
>
<Image
src={article.image ?? "/fallback.webp"}
alt="Article hero image"
fill
className="object-cover -z-10"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
priority={false}
/>
{backgroundCover && (
<ArticleContent backgroundCover={backgroundCover} />
)}

View File

@@ -1,9 +1,11 @@
"use client"
import Link from "next/link"
import { Button } from "../ui/button"
import { Markdown } from "../ui/markdown"
import { Article } from "@/lib/content"
import { getBackgroundImage } from "@/lib/utils"
import { Button } from "../ui/button"
import Image from "next/image"
import Link from "next/link"
export const ArticleListCard = ({
article,
@@ -38,16 +40,18 @@ export const ArticleListCard = ({
rel="noreferrer"
>
<div className="grid grid-cols-[80px_1fr] lg:grid-cols-[120px_1fr] items-center gap-4 lg:gap-10">
<div
className="size-[80px] lg:size-[120px] rounded-full bg-slate-200"
style={{
backgroundImage: backgroundImage
? `url(${backgroundImage})`
: undefined,
backgroundSize: "cover",
backgroundPosition: "center",
}}
></div>
<div className="relative size-[80px] lg:size-[120px] rounded-full bg-slate-200 overflow-hidden">
{backgroundImage ? (
<Image
src={backgroundImage}
alt="Article thumbnail"
fill
className="object-cover"
sizes="(max-width: 1024px) 80px, 120px"
priority={false}
/>
) : null}
</div>
<div className="flex flex-col gap-4 lg:gap-5">
<span className="text-[10px] font-bold tracking-[2.1px] text-tuatara-400 font-sans uppercase dark:text-tuatara-100">
{formattedDate}

View File

@@ -1,31 +1,16 @@
"use client"
import { useQuery } from "@tanstack/react-query"
import { Article, ArticleTag } from "@/lib/content"
import { ArticleListCard } from "./article-list-card"
import { cva } from "class-variance-authority"
import { ArticleInEvidenceCard } from "./article-in-evidance-card"
import { Input } from "../ui/input"
import { Button } from "../ui/button"
import { Input } from "../ui/input"
import { ArticleInEvidenceCard } from "./article-in-evidance-card"
import { ArticleListCard } from "./article-list-card"
import { LABELS } from "@/app/labels"
import { Article, ArticleTag } from "@/lib/content"
import { useQuery } from "@tanstack/react-query"
import { Search as SearchIcon } from "lucide-react"
import { useRouter, useSearchParams } from "next/navigation"
import { useState } from "react"
import { useDebounce, useMedia } from "react-use"
import { useRouter, useSearchParams } from "next/navigation"
const ArticleTitle = cva(
"text-white font-display hover:text-anakiwa-400 transition-colors group-hover:text-anakiwa-400",
{
variants: {
variant: {
compact:
"text-[20px] font-semibold lg:font-bold lg:text-lg line-clamp-2 mt-auto",
default: "text-[20px] font-semibold lg:font-bold line-clamp-3 mt-auto",
xl: "text-[20px] font-bold lg:!text-[40px] lg:!leading-[44px] mt-auto",
},
},
}
)
async function fetchArticles(tag?: string) {
try {

View File

@@ -28,7 +28,7 @@ export const BlogArticleCard = ({
<div className="relative h-48 w-full overflow-hidden bg-gray-100 flex-shrink-0">
<Image
src={imageUrl}
alt={title}
alt="Article cover image"
fill
className="object-cover"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
@@ -40,9 +40,9 @@ export const BlogArticleCard = ({
<div className="p-5 flex flex-col flex-grow gap-5 lg:gap-8 min-h-[180px]">
<div className="flex flex-col gap-2">
<h2 className="text-2xl font-bold leading-7 text-black duration-200 cursor-pointer hover:text-anakiwa-500">
<h3 className="text-2xl font-bold leading-7 text-black duration-200 cursor-pointer hover:text-anakiwa-500">
{title}
</h2>
</h3>
</div>
<div className="flex justify-between mt-auto gap-4 items-center">

View File

@@ -4,6 +4,7 @@ import { Button } from "../ui/button"
import { LABELS } from "@/app/labels"
import { getArticles, Article } from "@/lib/content"
import { cn } from "@/lib/utils"
import Image from "next/image"
import Link from "next/link"
const ArticleInEvidenceCard = ({
@@ -60,24 +61,31 @@ const ArticleInEvidenceCard = ({
<AsLinkWrapper href={`/blog/${article.id}`} asLink={asLink}>
<div
className={cn(
"min-h-[177px] lg:min-h-[190px] relative w-full items-center after:absolute after:inset-0 after:content-[''] after:bg-black after:opacity-50 group-hover:after:opacity-80 transition-opacity duration-300 after:z-[0]",
"min-h-[177px] lg:min-h-[190px] rounded-[6px] relative flex flex-col gap-5 w-full items-center overflow-hidden after:absolute after:inset-0 after:content-[''] after:bg-black after:opacity-50 group-hover:after:opacity-80 transition-opacity duration-300 after:z-[1]",
{
"aspect-video": !className?.includes("h-full"),
},
className
)}
style={{
backgroundImage: `url(${article.image ?? "/fallback.webp"})`,
backgroundSize: "cover",
backgroundPosition: "center centers",
}}
>
<Image
src={article.image ?? "/fallback.webp"}
alt="Article cover image"
fill
className="object-cover -z-[0] absolute inset-0"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
priority={variant === "xl"}
/>
<div
className={cn("flex flex-col gap-[10px] h-full justify-center", {
"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",
})}
className={cn(
"duration-200 flex flex-col gap-[10px] text-left relative z-[2] w-full h-full",
{
"px-5 lg:px-16 py-6 lg:py-16 ": size === "lg",
"px-6 py-4 lg:p-8": size === "sm",
"p-6 lg:p-16": size === "xl",
},
contentClassName
)}
>
<div
className={cn(
@@ -107,7 +115,7 @@ const ArticleInEvidenceCard = ({
>
{article.title}
</Link>
<span className="text-sm text-white/80 uppercase font-inter">
<span className="text-sm text-white/80 uppercase font-sans">
{article.authors?.join(", ")}
</span>
{article.tldr && !hideTldr && (
@@ -128,13 +136,17 @@ const ArticleInEvidenceCard = ({
)}
</div>
{showReadMore && (
<Link href={`/blog/${article.id}`} className="ml-auto z-[1]">
<div className="flex items-center gap-2 group">
<span className="!text-center text-white uppercase group-hover:text-anakiwa-400 duration-200">
Read More
</span>
<Icons.arrowRight className="w-4 h-4 text-white group-hover:text-anakiwa-400 group-hover:ml-1 duration-200" />
</div>
<Link
href={`/blog/${article.id}`}
className="ml-auto mt-4"
aria-label={`Read more about ${article.title}`}
>
<Button className="uppercase ml-auto" variant="secondary">
<div className="flex items-center gap-2">
<span className="!text-center">Read More</span>
<Icons.arrowRight className="w-4 h-4" />
</div>
</Button>
</Link>
)}
</div>
@@ -153,9 +165,9 @@ export async function BlogRecentArticles() {
<div className="py-16 lg:py-20">
<AppContent>
<div className="flex flex-col gap-10">
<h3 className="font-sans text-base font-bold uppercase tracking-[3.36px] text-tuatara-950 text-center dark:text-anakiwa-400">
<h2 className="font-sans text-base font-bold uppercase tracking-[4px] text-primary text-center">
{LABELS.BLOG_PAGE.RECENT_ARTICLES}
</h3>
</h2>
<div className="grid grid-cols-1 lg:grid-cols-5 gap-10 lg:gap-x-14 lg:max-w-[1200px] mx-auto relative">
<div className="inset-0 relative lg:col-span-3">
<ArticleInEvidenceCard

View File

@@ -185,8 +185,8 @@ export const Icons = {
typeof props.height === "number"
? props.height
: typeof props.width === "number"
? props.width - 1
: 18
? props.width - 1
: 18
}
viewBox="0 0 25 24"
fill="currentColor"
@@ -222,8 +222,8 @@ export const Icons = {
typeof props.height === "number"
? props.height
: typeof props.width === "number"
? props.width - 2
: 22
? props.width - 2
: 22
}
fill="currentColor"
viewBox="0 0 24 22"
@@ -243,8 +243,8 @@ export const Icons = {
typeof props.height === "number"
? props.height
: typeof props.width === "number"
? props.width + 2
: 26
? props.width + 2
: 26
}
fill="currentColor"
viewBox="0 0 24 26"
@@ -292,8 +292,8 @@ export const Icons = {
typeof props.size === "number"
? props.size
: typeof props.size === "number"
? props.size + 1
: 17
? props.size + 1
: 17
}
viewBox="0 0 16 17"
fill="none"
@@ -318,8 +318,8 @@ export const Icons = {
typeof props.height === "number"
? props.height
: typeof props.width === "number"
? props.width - 1
: 19
? props.width - 1
: 19
}
viewBox="0 0 20 19"
fill="none"
@@ -339,8 +339,8 @@ export const Icons = {
typeof props.height === "number"
? props.height
: typeof props.width === "number"
? props.width - 1
: 19
? props.width - 1
: 19
}
viewBox="0 0 20 19"
fill="none"
@@ -360,8 +360,8 @@ export const Icons = {
typeof props.height === "number"
? props.height
: typeof props.width === "number"
? props.width + 1
: 15
? props.width + 1
: 15
}
viewBox="0 0 14 15"
fill="none"

View File

@@ -1,17 +1,16 @@
"use client"
import { Icons } from "./icons"
import { useGlobalProvider } from "@/app/providers/GlobalProvider"
import { SearchButton } from "@/components/search/search-button"
import { SearchModal } from "@/components/search/search-modal"
import { cn } from "@/lib/utils"
import { NavItem } from "@/types/nav"
import { SunMedium as SunIcon, Moon as MoonIcon } from "lucide-react"
import Link from "next/link"
import { usePathname } from "next/navigation"
import { useState } from "react"
import { NavItem } from "@/types/nav"
import { cn } from "@/lib/utils"
import { SearchButton } from "@/components/search/search-button"
import { SearchModal } from "@/components/search/search-modal"
import { Icons } from "./icons"
import { SunMedium as SunIcon, Moon as MoonIcon } from "lucide-react"
import { useGlobalProvider } from "@/app/providers/GlobalProvider"
export interface MainNavProps {
items: NavItem[]
}
@@ -22,12 +21,16 @@ export function MainNav({ items }: MainNavProps) {
const { isDarkMode, setIsDarkMode } = useGlobalProvider()
return (
<div className="flex flex-1 items-center justify-between gap-6 md:gap-10">
<div className="flex items-center gap-6 md:gap-10">
<Link href="/" className="flex items-center space-x-2">
<div className="flex flex-1 items-center justify-between gap-6 lg:gap-10">
<div className="flex items-center gap-6 lg:gap-10">
<Link
href="/"
className="flex items-center space-x-2"
aria-label="Go to homepage"
>
<Icons.Logo className="text-black dark:text-anakiwa-400" size={32} />
</Link>
<nav className="hidden items-center gap-6 md:flex">
<nav className="nav-responsive flex items-center gap-6">
{items.map((item, index) => {
if (item?.onlyFooter) return null
if (item?.onlyMobile) return null
@@ -72,7 +75,10 @@ export function MainNav({ items }: MainNavProps) {
</div>
<button
onClick={() => setIsDarkMode(!isDarkMode)}
className="text-black dark:text-anakiwa-400 ml-auto hidden lg:inline-block"
className="hidden lg:block text-black dark:text-anakiwa-400 ml-auto"
aria-label={
isDarkMode ? "Switch to light mode" : "Switch to dark mode"
}
>
{isDarkMode ? <SunIcon size={20} /> : <MoonIcon size={20} />}
</button>

View File

@@ -1,8 +1,7 @@
"use client"
import { ProjectLinkWebsite } from "@/lib/types"
import { ProjectLinkIconMap } from "./project-links"
import { ProjectLinkWebsite } from "@/lib/types"
interface ProjectLinkProps {
url: string
@@ -12,6 +11,15 @@ interface ProjectLinkProps {
export function ProjectLink({ website, url }: ProjectLinkProps) {
const icon = ProjectLinkIconMap?.[website as ProjectLinkWebsite]
// Add aria-label mapping for accessibility
const ariaLabels: Record<string, string> = {
github: "View project on GitHub",
website: "Visit project website",
twitter: "Follow project on Twitter",
telegram: "Join project Telegram",
discord: "Join project Discord",
}
if (!icon) return null
return (
<a
@@ -23,6 +31,7 @@ export function ProjectLink({ website, url }: ProjectLinkProps) {
target="_blank"
rel="noopener noreferrer"
className="text-lg"
aria-label={ariaLabels[website] || "Visit external link"}
>
{icon}
</a>

View File

@@ -1,7 +1,6 @@
import { ReactNode } from "react"
import { AppContent } from "./ui/app-content"
import { Label } from "./ui/label"
import { ReactNode } from "react"
type PageHeaderProps = {
title: ReactNode
@@ -31,9 +30,9 @@ const PageHeader = ({
<div className="flex flex-col gap-4 md:gap-8">
<Label.PageTitle label={title} size={size} />
{subtitle && (
<h6 className="font-sans text-base font-normal text-primary md:text-[18px] md:leading-[27px] dark:text-tuatara-100">
<p className="font-sans text-base font-normal text-primary md:text-[18px] md:leading-[27px] dark:text-tuatara-100">
{subtitle}
</h6>
</p>
)}
</div>
{actions}

View File

@@ -1,8 +1,4 @@
import React from "react"
import Image from "next/image"
import { useRouter } from "next/navigation"
import { VariantProps, cva } from "class-variance-authority"
import { ProjectLink } from "../mappings/project-link"
import {
ProjectInterface,
ProjectLinkWebsite,
@@ -10,9 +6,11 @@ import {
ProjectStatusLabelMapping,
} from "@/lib/types"
import { cn } from "@/lib/utils"
import { ProjectLink } from "../mappings/project-link"
import { VariantProps, cva } from "class-variance-authority"
import Image from "next/image"
import Link from "next/link"
import { useRouter } from "next/navigation"
import React from "react"
interface ProjectCardProps
extends React.HTMLAttributes<HTMLDivElement>,
@@ -82,7 +80,7 @@ export default function ProjectCard({
>
<Image
src={`/project-banners/${image ? image : "fallback.webp"}`}
alt={`${name} banner`}
alt="Project banner image"
width={1200}
height={630}
className="h-[160px] w-full overflow-hidden rounded-t-lg border-none object-cover"
@@ -97,9 +95,9 @@ export default function ProjectCard({
<div className="flex flex-col justify-between h-full gap-8 p-4 bg-white rounded-b-lg dark:bg-black">
<div className="flex flex-col justify-start gap-2">
<Link href={`/projects/${id}`}>
<h1 className="text-2xl font-bold leading-7 text-primary duration-200 cursor-pointer hover:text-anakiwa-500">
<h3 className="text-2xl font-bold leading-7 text-primary duration-200 cursor-pointer hover:text-anakiwa-500">
{name}
</h1>
</h3>
</Link>
{(tldr ?? "")?.length > 0 && (
<div className="flex flex-col h-24 gap-4">

View File

@@ -1,22 +1,5 @@
"use client"
import React, { ChangeEvent, ReactNode, useEffect, useState } from "react"
import { useRouter, useSearchParams } from "next/navigation"
import { useDebounce } from "react-use"
import { IThemeStatus, IThemesButton } from "@/types/common"
import {
ProjectSectionLabelMapping,
ProjectSections,
ProjectStatus,
ProjectStatusLabelMapping,
} from "@/lib/types"
import { cn, queryStringToObject } from "@/lib/utils"
import { LABELS } from "@/app/labels"
import {
useProjects,
ProjectFilter,
FilterLabelMapping,
} from "@/app/providers/ProjectsProvider"
import { Icons } from "../icons"
import Badge from "../ui/badge"
import { Button } from "../ui/button"
@@ -24,6 +7,23 @@ import { CategoryTag } from "../ui/categoryTag"
import { Checkbox } from "../ui/checkbox"
import { Input } from "../ui/input"
import { Modal } from "../ui/modal"
import { LABELS } from "@/app/labels"
import {
useProjects,
ProjectFilter,
FilterLabelMapping,
} from "@/app/providers/ProjectsProvider"
import {
ProjectSectionLabelMapping,
ProjectSections,
ProjectStatus,
ProjectStatusLabelMapping,
} from "@/lib/types"
import { cn, queryStringToObject } from "@/lib/utils"
import { IThemeStatus, IThemesButton } from "@/types/common"
import { useRouter, useSearchParams } from "next/navigation"
import React, { ChangeEvent, ReactNode, useEffect, useState } from "react"
import { useDebounce } from "react-use"
// Define the mapping for filter types
const FilterTypeMapping: Record<ProjectFilter, "checkbox" | "button"> = {
@@ -273,7 +273,7 @@ export default function ProjectFiltersBar() {
>
<div className="flex items-center gap-2">
<Icons.Filter className="text-anakiwa-950 dark:text-anakiwa-400" />
<span className="hidden md:block text-sm">
<span className="hidden lg:block text-sm">
{LABELS.COMMON.FILTERS}
</span>
</div>
@@ -282,7 +282,7 @@ export default function ProjectFiltersBar() {
<button
disabled={!hasActiveFilters}
onClick={clearAllFilters}
className="hidden bg-transparent cursor-pointer opacity-85 text-primary hover:opacity-100 disabled:pointer-events-none disabled:opacity-50 md:block dark:text-anakiwa-400 dark:hover:text-anakiwa-400"
className="hidden lg:block bg-transparent cursor-pointer opacity-85 text-primary hover:opacity-100 disabled:pointer-events-none disabled:opacity-50 dark:text-anakiwa-400 dark:hover:text-anakiwa-400"
>
<div className="flex items-center gap-2 border-b-2 border-black dark:border-anakiwa-800">
<span className="text-sm font-medium">

View File

@@ -1,9 +1,9 @@
"use client"
import React, { useCallback, useEffect, useRef, useState } from "react"
import Image from "next/image"
import NoResultIcon from "@/public/icons/no-result.svg"
import ProjectCard from "./project-card"
import { SectionWrapper } from "@/app/components/wrappers/SectionWrapper"
import { LABELS } from "@/app/labels"
import { useProjects } from "@/app/providers/ProjectsProvider"
import {
ProjectInterface,
ProjectSection,
@@ -13,17 +13,15 @@ import {
ProjectStatusDescriptionMapping,
} from "@/lib/types"
import { cn } from "@/lib/utils"
import { LABELS } from "@/app/labels"
import ProjectCard from "./project-card"
import { useProjects } from "@/app/providers/ProjectsProvider"
import { SectionWrapper } from "@/app/components/wrappers/SectionWrapper"
import NoResultIcon from "@/public/icons/no-result.svg"
import Image from "next/image"
import React, { useCallback, useEffect, useRef, useState } from "react"
const NoResults = () => {
return (
<div className="flex flex-col gap-2 pt-24 pb-40 text-center">
<div className="mx-auto">
<Image className="h-9 w-9" src={NoResultIcon} alt="no result icon" />
<Image className="h-9 w-9" src={NoResultIcon} alt="No projects found" />
</div>
<span className="text-2xl font-bold font-display text-primary">
{LABELS.COMMON.NO_RESULTS}
@@ -117,16 +115,13 @@ export const ProjectList = () => {
if (noItems) return <NoResults />
const projectsGroupByStatus = projects.reduce(
(acc, project) => {
acc[project.projectStatus] = [
...(acc[project.projectStatus] || []),
project,
]
return acc
},
{} as Record<ProjectStatus, ProjectInterface[]>
)
const projectsGroupByStatus = projects.reduce((acc, project) => {
acc[project.projectStatus] = [
...(acc[project.projectStatus] || []),
project,
]
return acc
}, {} as Record<ProjectStatus, ProjectInterface[]>)
if (hasSearchParams) {
return (

View File

@@ -1,8 +1,4 @@
import React from "react"
import Image from "next/image"
import { useRouter } from "next/navigation"
import { VariantProps, cva } from "class-variance-authority"
import { ProjectLink } from "../mappings/project-link"
import { useProjects } from "@/app/providers/ProjectsProvider"
import {
ProjectInterface,
@@ -11,9 +7,11 @@ import {
ProjectStatusLabelMapping,
} from "@/lib/types"
import { cn } from "@/lib/utils"
import { ProjectLink } from "../mappings/project-link"
import { VariantProps, cva } from "class-variance-authority"
import Image from "next/image"
import Link from "next/link"
import { useRouter } from "next/navigation"
import React from "react"
interface ProjectCardProps
extends React.HTMLAttributes<HTMLDivElement>,

View File

@@ -1,25 +1,27 @@
"use client"
import React, { useEffect, useState, useMemo } from "react"
import Image from "next/image"
import NoResultIcon from "@/public/icons/no-result.svg"
import { ProjectInterface, ProjectStatus } from "@/lib/types"
import { LABELS } from "@/app/labels"
import { useProjects } from "@/app/providers/ProjectsProvider"
import ResearchCard from "./research-card"
import Link from "next/link"
import {
SectionWrapper,
SectionWrapperTitle,
} from "@/app/components/wrappers/SectionWrapper"
import { LABELS } from "@/app/labels"
import { useProjects } from "@/app/providers/ProjectsProvider"
import { ProjectInterface, ProjectStatus } from "@/lib/types"
import NoResultIcon from "@/public/icons/no-result.svg"
import Image from "next/image"
import Link from "next/link"
import React, { useEffect, useState, useMemo } from "react"
const NoResults = () => {
return (
<div className="flex flex-col gap-2 pt-24 pb-40 text-center">
<div className="mx-auto">
<Image className="h-9 w-9" src={NoResultIcon} alt="no result icon" />
<Image
className="h-9 w-9"
src={NoResultIcon}
alt="No research projects found"
/>
</div>
<span className="text-2xl font-bold font-display text-primary">
{LABELS.COMMON.NO_RESULTS}

View File

@@ -1,13 +1,11 @@
import Image from "next/image"
import Link from "next/link"
import { LABELS } from "@/app/labels"
import { interpolate } from "@/lib/utils"
import { Card } from "../cards/card"
import { Icons } from "../icons"
import { AppContent } from "../ui/app-content"
import { Button } from "../ui/button"
import { LABELS } from "@/app/labels"
import { interpolate } from "@/lib/utils"
import Image from "next/image"
import Link from "next/link"
const ConnectWithUs = () => {
return (
@@ -43,8 +41,8 @@ const ConnectWithUs = () => {
width={280}
height={280}
className="mx-auto h-[153px] w-[174px] lg:ml-auto lg:h-[256px] lg:w-[290px]"
src="/images/programs.png"
alt="computer image"
src="/images/programs.webp"
alt=""
/>
</div>
</Card>

View File

@@ -0,0 +1,75 @@
"use client"
import { Icons } from "../icons"
import { PageHeader } from "../page-header"
import { Button } from "../ui/button"
import { Label } from "../ui/label"
import { LABELS } from "@/app/labels"
import PSELogo from "@/public/icons/archstar.webp"
import dynamic from "next/dynamic"
import Image from "next/image"
import Link from "next/link"
const MotionH1 = dynamic(
() => import("framer-motion").then((mod) => mod.motion.h1),
{ ssr: false, loading: () => <h1 /> }
)
export const HomepageHeader = () => {
return (
<PageHeader
title={
<MotionH1
initial={{ y: 16, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ duration: 0.8, ease: "easeOut" }}
>
<Label.PageTitle size="large" label={LABELS.HOMEPAGE.HEADER_TITLE} />
</MotionH1>
}
subtitle={LABELS.HOMEPAGE.HEADER_SUBTITLE}
image={
<div className="m-auto flex h-[320px] w-full max-w-[280px] items-center justify-center md:m-0 md:h-full md:w-full lg:max-w-[380px]">
<Image
src={PSELogo}
alt="Privacy & Scaling Explorations"
className="object-cover"
priority
quality={90}
sizes="(max-width: 768px) 280px, (max-width: 1200px) 380px, 380px"
/>
</div>
}
actions={
<div className="flex flex-col gap-4 lg:flex-row lg:gap-10">
<Link href="/research" className="flex items-center gap-2 group">
<Button className="w-full sm:w-auto">
<div className="flex items-center gap-1">
<span className="text-base font-medium uppercase">
{LABELS.COMMON.APPLIED_RESEARCH}
</span>
<Icons.arrowRight
fill="white"
className="h-5 duration-200 ease-in-out group-hover:translate-x-2"
/>
</div>
</Button>
</Link>
<Link href="/projects" className="flex items-center gap-2 group">
<Button className="w-full sm:w-auto">
<div className="flex items-center gap-1">
<span className="text-base font-medium uppercase">
{LABELS.COMMON.DEVELOPMENT_PROJECTS}
</span>
<Icons.arrowRight
fill="white"
className="h-5 duration-200 ease-in-out group-hover:translate-x-2"
/>
</div>
</Button>
</Link>
</div>
}
/>
)
}

View File

@@ -20,7 +20,13 @@ interface Video {
url?: string
}
const VideoCard = ({ video }: { video: Video }) => {
const VideoCard = ({
video,
isPriority = false,
}: {
video: Video
isPriority?: boolean
}) => {
return (
<Link
href={video.url || `https://www.youtube.com/watch?v=${video.id}`}
@@ -32,18 +38,20 @@ const VideoCard = ({ video }: { video: Video }) => {
<div className="absolute inset-0 bg-black opacity-20 group-hover:opacity-60 transition-opacity duration-300 z-10"></div>
<Image
src={video.thumbnailUrl || ""}
alt={video.title}
alt="Video thumbnail"
fill
style={{ objectFit: "cover" }}
className="transition-transform duration-300 scale-105 group-hover:scale-110"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
priority={isPriority}
/>
<div className="absolute inset-0 flex items-center justify-center z-20">
<Icons.play />
</div>
</div>
<h3 className="font-sans text-sm font-normal line-clamp-3 text-tuatara-950 dark:text-tuatara-100 group-hover:text-tuatara-400 transition-colors">
<h4 className="font-sans text-sm font-normal line-clamp-3 text-white group-hover:text-tuatara-400 transition-colors">
{video.title}
</h3>
</h4>
</Link>
)
}
@@ -91,8 +99,12 @@ export const HomepageVideoFeed = () => {
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 lg:gap-8">
{videos.slice(0, 3).map((video: Video) => (
<VideoCard key={video.id} video={video} />
{videos.slice(0, 3).map((video: Video, index: number) => (
<VideoCard
key={video.id}
video={video}
isPriority={index === 0}
/>
))}
</div>
)}

View File

@@ -35,25 +35,24 @@ export const OurWork = () => {
<div className="flex flex-col justify-center bg-cover-gradient dark:bg-anakiwa-975 dark:bg-none py-16 lg:py-20">
<AppContent className="mx-auto lg:max-w-[845px] w-full">
<section className="flex flex-col gap-10">
<h6 className="font-sans text-base font-bold uppercase tracking-[3.36px] text-primary text-center text-tuatara-950 dark:text-tuatara-100">
Our Work
</h6>
<div className="grid grid-cols-1 gap-6 lg:gap-10 lg:grid-cols-2 max-auto">
{content.map((item, index) => (
<Link
href={item.link}
key={index}
className="flex flex-col gap-6 w-full lg:max-w-[400px] p-6 lg:p-10 rounded-[10px] border border-anakiwa-300 dark:border-anakiwa-400 bg-white dark:bg-anakiwa-975 hover:scale-105 duration-200 ease-in-out dark:hover:bg-cover-gradient-dark"
<h2 className="font-sans text-base font-bold uppercase tracking-[4px] text-black dark:text-white text-center">
What we do
</h2>
<div className="grid grid-cols-1 gap-10 lg:grid-cols-2 max-auto">
{content.map((item) => (
<div
key={`${item.link}-${item.title}`}
className="flex flex-col gap-6 w-full lg:max-w-[300px]"
>
<article className="flex flex-col gap-2">
<h6 className="font-sans text-base lg:text-xl font-medium text-tuatara-950 dark:text-anakiwa-400">
<h3 className="font-sans text-xl font-medium text-tuatara-950 dark:text-tuatara-100">
{item.title}
</h6>
<p className="font-sans text-base font-normal text-tuatara-500 dark:text-tuatara-100">
</h3>
<p className="font-sans text-base font-normal text-tuatara-950 dark:text-tuatara-100">
{item.description}
</p>
</article>
</Link>
</div>
))}
</div>
</section>

View File

@@ -5,18 +5,33 @@ import { AppContent } from "../ui/app-content"
import { Button } from "../ui/button"
import { LABELS } from "@/app/labels"
import { cn } from "@/lib/utils"
import { motion, useScroll, useTransform } from "framer-motion"
import dynamic from "next/dynamic"
import Image from "next/image"
import Link from "next/link"
import { useRef } from "react"
import { useRef, useEffect, useState } from "react"
const MotionDiv = dynamic(
() => import("framer-motion").then((mod) => mod.motion.div),
{ ssr: false, loading: () => <div /> }
)
const MotionSpan = dynamic(
() => import("framer-motion").then((mod) => mod.motion.span),
{ ssr: false, loading: () => <span /> }
)
const MotionH1 = dynamic(
() => import("framer-motion").then((mod) => mod.motion.h1),
{ ssr: false, loading: () => <h1 /> }
)
export const ParallaxHero = () => {
const containerRef = useRef<HTMLDivElement>(null)
const { scrollYProgress } = useScroll({
target: containerRef,
offset: ["start start", "end start"],
})
const [isClient, setIsClient] = useState(false)
const backgroundY = useTransform(scrollYProgress, [0, 1], ["0%", "80%"])
useEffect(() => {
setIsClient(true)
}, [])
return (
<div
@@ -24,92 +39,206 @@ export const ParallaxHero = () => {
className="relative w-full overflow-hidden h-[500px] lg:h-[600px] bg-cover bg-no-repeat bg-center"
style={{ isolation: "isolate" }}
>
<motion.div
className={cn(
"absolute inset-0 w-full h-full -z-10 bg-cover bg-no-repeat bg-center",
"bg-[url('/hero/hero-mobile.jpg')] dark:bg-[url('/hero/hero-dark-mode-mobile.jpg')]", // mobile image
"lg:bg-[url('/hero/hero.jpg')] lg:dark:bg-[url('/hero/hero-dark-mode.jpg')]" // desktop image
)}
style={{
y: backgroundY,
}}
/>
{isClient ? (
<MotionDiv
className="absolute inset-0 w-full h-full -z-10"
initial={{ y: 0 }}
>
{/* Light mode images */}
<Image
src="/hero/hero-mobile.jpg"
alt=""
fill
priority
fetchPriority="high"
className="object-cover lg:hidden dark:hidden"
sizes="100vw"
/>
<Image
src="/hero/hero.jpg"
alt=""
fill
priority
fetchPriority="high"
className="object-cover hidden lg:block dark:hidden"
sizes="100vw"
/>
<motion.div
className="relative z-10 flex items-center justify-center h-full py-10 lg:py-[130px]"
style={{
y: useTransform(scrollYProgress, [0, 1], ["0%", "-25%"]),
}}
>
<AppContent className="flex flex-col gap-10 max-w-[860px]">
<motion.div
initial={{ y: 16, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ duration: 0.8, ease: [0.16, 1, 0.3, 1] }}
className="flex flex-col gap-10 text-center"
>
<div className="flex flex-col gap-10">
<div className="flex flex-col gap-[10px]">
<motion.span
style={{
y: useTransform(scrollYProgress, [0, 1], ["0%", "-40%"]),
}}
className="font-sans font-medium leading-7 uppercase text-tuatara-800 dark:text-anakiwa-400"
>
{LABELS.HOMEPAGE.HEADER_TITLE}
</motion.span>
<motion.h1
style={{
y: useTransform(scrollYProgress, [0, 1], ["0%", "-35%"]),
}}
className="font-sans text-3xl lg:text-5xl font-medium text-tuatara-950 dark:text-tuatara-100"
>
{LABELS.HOMEPAGE.HEADER_SUBTITLE}
</motion.h1>
{/* Dark mode images */}
<Image
src="/hero/hero-dark-mode-mobile.jpg"
alt=""
fill
priority
fetchPriority="high"
className="object-cover lg:hidden hidden dark:block"
sizes="100vw"
/>
<Image
src="/hero/hero-dark-mode.jpg"
alt=""
fill
priority
fetchPriority="high"
className="object-cover hidden dark:lg:block"
sizes="100vw"
/>
</MotionDiv>
) : (
<div className="absolute inset-0 w-full h-full -z-10">
{/* Light mode images */}
<Image
src="/hero/hero-mobile.jpg"
alt=""
fill
priority
fetchPriority="high"
className="object-cover lg:hidden dark:hidden"
sizes="100vw"
/>
<Image
src="/hero/hero.jpg"
alt=""
fill
priority
fetchPriority="high"
className="object-cover hidden lg:block dark:hidden"
sizes="100vw"
/>
{/* Dark mode images */}
<Image
src="/hero/hero-dark-mode-mobile.jpg"
alt=""
fill
priority
fetchPriority="high"
className="object-cover lg:hidden hidden dark:block"
sizes="100vw"
/>
<Image
src="/hero/hero-dark-mode.jpg"
alt=""
fill
priority
fetchPriority="high"
className="object-cover hidden dark:lg:block"
sizes="100vw"
/>
</div>
)}
{isClient ? (
<MotionDiv
className="relative z-10 flex items-center justify-center h-full py-10 lg:py-[130px]"
initial={{ y: 0 }}
>
<AppContent className="flex flex-col gap-10 max-w-[860px]">
<MotionDiv
initial={{ y: 16, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ duration: 0.8, ease: [0.16, 1, 0.3, 1] }}
className="flex flex-col gap-10 text-center"
>
<div className="flex flex-col gap-10">
<div className="flex flex-col gap-[10px]">
<MotionSpan className="font-sans font-medium leading-7 uppercase text-tuatara-800 dark:text-anakiwa-400">
{LABELS.HOMEPAGE.HEADER_TITLE}
</MotionSpan>
<MotionH1 className="font-sans text-3xl lg:text-5xl font-medium text-tuatara-950 dark:text-tuatara-100">
{LABELS.HOMEPAGE.HEADER_SUBTITLE}
</MotionH1>
</div>
<MotionDiv className="flex flex-wrap gap-4 lg:gap-8 mx-auto justify-center">
<Link
href="/projects"
className="flex items-center gap-2 group"
>
<Button className="w-full sm:w-auto">
<div className="flex items-center gap-1">
<span className="text-base font-medium uppercase">
{LABELS.COMMON.DEVELOPMENT_PROJECTS}
</span>
<Icons.arrowRight
fill="white"
className="h-5 duration-200 ease-in-out group-hover:translate-x-2"
/>
</div>
</Button>
</Link>
<Link
href="/research"
className="flex items-center gap-2 group"
>
<Button className="w-full sm:w-auto">
<div className="flex items-center gap-1">
<span className="text-base font-medium uppercase">
{LABELS.COMMON.APPLIED_RESEARCH}
</span>
<Icons.arrowRight
fill="white"
className="h-5 duration-200 ease-in-out group-hover:translate-x-2"
/>
</div>
</Button>
</Link>
</MotionDiv>
</div>
</MotionDiv>
</AppContent>
</MotionDiv>
) : (
<div className="relative z-10 flex items-center justify-center h-full py-10 lg:py-[130px]">
<AppContent className="flex flex-col gap-10 max-w-[860px]">
<div className="flex flex-col gap-10 text-center">
<div className="flex flex-col gap-10">
<div className="flex flex-col gap-[10px]">
<span className="font-sans font-medium leading-7 uppercase text-tuatara-800 dark:text-anakiwa-400">
{LABELS.HOMEPAGE.HEADER_TITLE}
</span>
<h1 className="font-sans text-3xl lg:text-5xl font-medium text-tuatara-950 dark:text-tuatara-100">
{LABELS.HOMEPAGE.HEADER_SUBTITLE}
</h1>
</div>
<div className="flex flex-wrap gap-4 lg:gap-8 mx-auto justify-center">
<Link
href="/projects"
className="flex items-center gap-2 group"
>
<Button className="w-full sm:w-auto">
<div className="flex items-center gap-1">
<span className="text-base font-medium uppercase">
{LABELS.COMMON.DEVELOPMENT_PROJECTS}
</span>
<Icons.arrowRight
fill="white"
className="h-5 duration-200 ease-in-out group-hover:translate-x-2"
/>
</div>
</Button>
</Link>
<Link
href="/research"
className="flex items-center gap-2 group"
>
<Button className="w-full sm:w-auto">
<div className="flex items-center gap-1">
<span className="text-base font-medium uppercase">
{LABELS.COMMON.APPLIED_RESEARCH}
</span>
<Icons.arrowRight
fill="white"
className="h-5 duration-200 ease-in-out group-hover:translate-x-2"
/>
</div>
</Button>
</Link>
</div>
</div>
<motion.div
style={{
y: useTransform(scrollYProgress, [0, 1], ["0%", "-30%"]),
}}
className="flex flex-wrap gap-4 lg:gap-8 mx-auto justify-center"
>
<Link
href="/projects"
className="flex items-center gap-2 group"
>
<Button className="w-full sm:w-auto">
<div className="flex items-center gap-1">
<span className="text-base font-medium uppercase">
{LABELS.COMMON.DEVELOPMENT_PROJECTS}
</span>
<Icons.arrowRight
fill="white"
className="h-5 duration-200 ease-in-out group-hover:translate-x-2"
/>
</div>
</Button>
</Link>
<Link
href="/research"
className="flex items-center gap-2 group"
>
<Button className="w-full sm:w-auto">
<div className="flex items-center gap-1">
<span className="text-base font-medium uppercase">
{LABELS.COMMON.APPLIED_RESEARCH}
</span>
<Icons.arrowRight
fill="white"
className="h-5 duration-200 ease-in-out group-hover:translate-x-2"
/>
</div>
</Button>
</Link>
</motion.div>
</div>
</motion.div>
</AppContent>
</motion.div>
</AppContent>
</div>
)}
</div>
)
}

View File

@@ -1,10 +1,10 @@
"use client"
import Image from "next/image"
import { Icons } from "../icons"
import { useEffect, useState } from "react"
import { LABELS } from "@/app/labels"
import { AppLink } from "../app-link"
import { Icons } from "../icons"
import { LABELS } from "@/app/labels"
import Image from "next/image"
import { useEffect, useState } from "react"
interface VideoCardProps {
videoId: string
@@ -42,7 +42,7 @@ const VideoCard = ({ videoId }: VideoCardProps) => {
<div className="absolute inset-0 bg-black opacity-20 group-hover:opacity-60 transition-opacity duration-300 z-10"></div>
<Image
src={thumbnailUrl}
alt={title || "YouTube video"}
alt="Video thumbnail"
fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
style={{ objectFit: "cover" }}

View File

@@ -1,14 +1,13 @@
"use client"
import { NavItem } from "@/types/nav"
import { siteConfig } from "@/config/site"
import { cn } from "@/lib/utils"
import { useAppSettings } from "@/hooks/useAppSettings"
import { LABELS } from "@/app/labels"
import { AppLink } from "./app-link"
import { Icons } from "./icons"
import { AppContent } from "./ui/app-content"
import { AppLink } from "./app-link"
import { LABELS } from "@/app/labels"
import { siteConfig } from "@/config/site"
import { useAppSettings } from "@/hooks/useAppSettings"
import { cn } from "@/lib/utils"
import { NavItem } from "@/types/nav"
const ItemLabel = ({
label,

View File

@@ -26,8 +26,13 @@ export const SiteHeaderMobile = () => {
const { MAIN_NAV } = useAppSettings()
return (
<div className="flex items-center md:hidden">
<button type="button" onClick={() => setHeader(true)}>
<div className="flex items-center lg:hidden">
<button
type="button"
onClick={() => setHeader(true)}
aria-label="Open navigation menu"
aria-expanded={header}
>
<Icons.Burgher
size={24}
className="text-[#171C1B] dark:text-anakiwa-400"
@@ -35,21 +40,20 @@ export const SiteHeaderMobile = () => {
</button>
{header && (
<div
className="z-5 fixed inset-0 flex justify-end bg-black opacity-50"
className="z-40 fixed inset-0 flex justify-end bg-black opacity-50"
onClick={() => setHeader(false)}
></div>
)}
{header && (
<div className="fixed inset-y-0 right-0 z-10 flex w-[257px] flex-col bg-black text-white">
<div className="fixed inset-y-0 right-0 z-50 flex w-[257px] flex-col bg-black text-white">
<div className="flex justify-end p-[37px]">
<NextImage
src={CloseVector}
alt="closeVector"
className="cursor-pointer"
<button
onClick={() => setHeader(false)}
width={24}
height={24}
/>
aria-label="Close navigation menu"
className="cursor-pointer"
>
<NextImage src={CloseVector} alt="" width={24} height={24} />
</button>
</div>
<div className="flex w-full flex-col px-[16px] text-base font-medium">
{MAIN_NAV.map((item: NavItem, index) => {
@@ -70,7 +74,10 @@ export const SiteHeaderMobile = () => {
})}
<button
onClick={() => setIsDarkMode(!isDarkMode)}
className=" ml-auto mt-10"
className="ml-auto mt-10"
aria-label={
isDarkMode ? "Switch to light mode" : "Switch to dark mode"
}
>
{isDarkMode ? (
<SunIcon
@@ -88,30 +95,46 @@ export const SiteHeaderMobile = () => {
<div className="flex h-full w-full flex-col items-center justify-end gap-5 py-[40px] text-sm">
<div className="flex gap-5">
<AppLink href={siteConfig.links.twitter} external>
<AppLink
href={siteConfig.links.twitter}
external
aria-label="Follow us on Twitter/X"
>
<Twitter color="white" />{" "}
</AppLink>
<AppLink href={siteConfig.links.discord} external>
<AppLink
href={siteConfig.links.discord}
external
aria-label="Join our Discord community"
>
<Discord color="white" />{" "}
</AppLink>
<AppLink href={siteConfig.links.github} external>
<AppLink
href={siteConfig.links.github}
external
aria-label="View our code on GitHub"
>
<Github color="white" />{" "}
</AppLink>
<AppLink href={siteConfig.links.articles} external>
<AppLink
href={siteConfig.links.articles}
external
aria-label="Read our articles on Mirror"
>
<Mirror color="white" />{" "}
</AppLink>
</div>
<div className="flex gap-5 text-white">
<h1>{LABELS.COMMON.FOOTER.PRIVACY_POLICY}</h1>
<h1>{LABELS.COMMON.FOOTER.TERMS_OF_USE}</h1>
<span>{LABELS.COMMON.FOOTER.PRIVACY_POLICY}</span>
<span>{LABELS.COMMON.FOOTER.TERMS_OF_USE}</span>
</div>
<h1 className="text-center text-gray-400">
<p className="text-center text-gray-400">
{interpolate(LABELS.COMMON.LAST_UPDATED_AT, {
date: "January 16, 2024",
})}
</h1>
</p>
</div>
</div>
)}

View File

@@ -1,9 +1,8 @@
import * as React from "react"
import { cn } from "@/lib/utils"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { LucideIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import * as React from "react"
const buttonVariants = cva(
"font-sans inline-flex items-center justify-center gap-2 duration-200 rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background w-fit",
@@ -66,6 +65,7 @@ export interface ButtonProps
VariantProps<typeof buttonVariants> {
asChild?: boolean
icon?: LucideIcon
accessibleName?: string
iconPosition?: "left" | "right"
}
@@ -78,6 +78,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
asChild = false,
children,
icon,
accessibleName,
iconPosition = "left",
...props
},
@@ -85,10 +86,39 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
) => {
const Comp = asChild ? Slot : "button"
const Icon = icon
// Generate accessible name from children text if not provided
const getAccessibleName = () => {
if (accessibleName) return accessibleName
if (typeof children === "string") return children
// If children is complex, try to extract text content
if (React.isValidElement(children)) {
// For simple elements, try to get text content
const textContent = React.Children.toArray(children)
.map((child) => {
if (typeof child === "string") return child
if (
React.isValidElement(child) &&
typeof child.props.children === "string"
) {
return child.props.children
}
return ""
})
.join(" ")
.trim()
return textContent || undefined
}
return undefined
}
const accessibleNameValue = getAccessibleName()
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
aria-label={accessibleNameValue}
{...props}
>
{Icon && iconPosition === "left" && <Icon size={18} />}

View File

@@ -25,7 +25,7 @@ const MainPageTitle = ({
size = "small",
}: LabelProps) => {
return (
<span
<h1
className={cn(
"text-4xl font-bold break-words font-display text-primary dark:text-white",
size === "small" ? "lg:text-5xl" : "lg:text-6xl xl:text-7xl",
@@ -33,7 +33,7 @@ const MainPageTitle = ({
)}
>
{label}
</span>
</h1>
)
}

View File

@@ -1,13 +1,11 @@
"use client"
import { useEffect, useRef, useState } from "react"
import { Icons } from "./icons"
import { LABELS } from "@/app/labels"
import { useGetProjectRelatedArticles } from "@/hooks/useGetProjectRelatedArticles"
import { ProjectExtraLinkType } from "@/lib/types"
import { cn } from "@/lib/utils"
import { Icons } from "./icons"
import { useGetProjectRelatedArticles } from "@/hooks/useGetProjectRelatedArticles"
import { LABELS } from "@/app/labels"
import { useEffect, useRef, useState } from "react"
interface Section {
level: number
@@ -43,7 +41,11 @@ const SideNavigationItem = ({
}
)}
>
<button onClick={onClick} className="text-left">
<button
onClick={onClick}
className="text-left"
aria-label={`Navigate to ${text} section`}
>
{text}
</button>
</li>

View File

@@ -32,7 +32,7 @@ Write your article content using Markdown formatting:
```javascript
// Your code here
```
- For images, use the Markdown image syntax: `![Alt text](/articles/your-article-name/image-name.png)`
- For images, use the Markdown image syntax: `![Alt text](/articles/your-article-name/image-name.webp)`
- For LaTeX math formulas:
- Use single dollar signs for inline math: `$E=mc^2$` will render as $E=mc^2$
- Use double dollar signs for block math:

View File

@@ -1,7 +1,7 @@
---
authors: ["Brechy"]
title: "Ethereum Privacy: Private Information Retrieval"
image: "/articles/ethereum-privacy-pir/cover.png"
image: "/articles/ethereum-privacy-pir/cover.webp"
tldr: "Private Information Retrieval can enhance Ethereum privacy."
date: "2025-06-18"
tags: ["ethereum", "privacy", "pir"]

View File

@@ -1,7 +1,7 @@
---
authors: ["Sora Suegami", "Enrico Bottazzi", "Pia Park"]
title: "Hello, World: the first signs of practical iO"
image: "/articles/hello-world-the-first-signs-of-practical-io/cover-practical-io.png"
image: "/articles/hello-world-the-first-signs-of-practical-io/cover-practical-io.webp"
tldr: "Machina iO shares the first end-to-end implementation and benchmarks of Diamond iO, demonstrating that lattice-based indistinguishability obfuscation can be made concrete and efficient."
date: "2025-05-12"
canonical: "https://machina-io.com/posts/hello_world_first.html"

View File

@@ -1,7 +1,7 @@
---
authors: ["Yanis Meziane"]
title: "Introducing Trinity"
image: "/articles/introducing-trinity/trinity.png"
image: "/articles/introducing-trinity/trinity.webp"
tldr: "Two-Party Computation for the ZK Era"
date: "2025-04-28"
tags:
@@ -89,9 +89,9 @@ export default function main(a: number, b: number) {
In the next snippet, we're going to initialise the trinity library and parse our circuit, so it can be consumed by both the garbler and the evaluator.
```ts
import * as summon from "summon-ts"
import getCircuitFiles from "./getCircuitFiles"
import { initTrinity, parseCircuit } from "@trinity-2pc/core"
import * as summon from "summon-ts"
export default async function generateProtocol() {
await summon.init()

View File

@@ -47,11 +47,11 @@ For the zero-knowledge proof of $x_1$ such that $g^{x_1} = h_1$, the [Schnorr pr
- The prover sends $z_1 = y_1 + x_1 d$ to the verifier.
- The verifier checks whether $g^{z_1} = t_1 h_1^d$.
![](/articles/lattice-based-proof-systems/image-1.png)
![](/articles/lattice-based-proof-systems/image-1.webp)
Now, the protocol can be easily extended to prove the knowledge of $x_2$ such that $g^{x_2} = h_2$. In this case, the prover would also send $t_2 = g^{y_2}$ in the first step and $z_2 = y_2 + x_1 d$ in the third one. The verifier would then check whether $g^{z_2} = t_2 h_2^d$.
![](/articles/lattice-based-proof-systems/image-2.png)
![](/articles/lattice-based-proof-systems/image-2.webp)
Note that checking the additional property $x_1 = u \cdot x_2$ is straightforward:
@@ -97,7 +97,7 @@ $$||x_1|| \leq B$$
Would the following protocol work?
![](/articles/lattice-based-proof-systems/image-3.png)
![](/articles/lattice-based-proof-systems/image-3.webp)
The last step of the protocol would be verifier checking whether $A z_1 = A y_1 + d A x_1 = t_1 + d h_1$.
@@ -165,7 +165,7 @@ $f \cdot \begin{pmatrix} \mathbf{c}_1 \\ \mathbf{c}_2 \end{pmatrix} = \begin{pma
#### Proof of opening
![](/articles/lattice-based-proof-systems/image-4.png)
![](/articles/lattice-based-proof-systems/image-4.webp)
Note that the opening for this commitment schemes is not simply $\mathbf{r}$ and $\mathbf{x}$; it also includes a polynomial $f \in \bar{C}.$ This is due to the issue with the extractor described above.

View File

@@ -41,15 +41,15 @@ Here's how zkEmail performs on Apple M3 chips:
<table>
<tr>
<td align="center">
<a href="/articles/mopro-native-packages/zkemail-flutter-app-ios.png" target="_blank" rel="noopener noreferrer">
<img src="/articles/mopro-native-packages/zkemail-flutter-app-ios.png" alt="iOS zkEmail App Example" width="300"/>
<a href="/articles/mopro-native-packages/zkemail-flutter-app-ios.webp" target="_blank" rel="noopener noreferrer">
<img src="/articles/mopro-native-packages/zkemail-flutter-app-ios.webp" alt="iOS zkEmail App Example" width="300"/>
</a>
<br />
<sub><b>iOS</b></sub>
</td>
<td align="center">
<a href="/articles/mopro-native-packages/zkemail-flutter-app-android.png" target="_blank" rel="noopener noreferrer">
<img src="/articles/mopro-native-packages/zkemail-flutter-app-android.png" alt="Android zkEmail App Example" width="300"/>
<a href="/articles/mopro-native-packages/zkemail-flutter-app-android.webp" target="_blank" rel="noopener noreferrer">
<img src="/articles/mopro-native-packages/zkemail-flutter-app-android.webp" alt="Android zkEmail App Example" width="300"/>
</a>
<br />
<sub><b>Android</b></sub>

View File

@@ -1,7 +1,7 @@
---
authors: ["Kevin Chia", "Jern Kun", "Kazumune Masaki"] # Add your name or multiple authors in an array
title: "MPCStats Retrospective: Lessons from a Privacy-Preserving Stats Platform" # The title of your article
image: "/articles/mpcstats-retrospective/cover.png" # Image used as cover, Keep in mind the image size, where possible use .webp format, possibly images less then 200/300kb
image: "/articles/mpcstats-retrospective/cover.webp" # Image used as cover, Keep in mind the image size, where possible use .webp format, possibly images less then 200/300kb
tldr: "A retrospective on the MPCStats project—exploring our goals, technical challenges, demo at Devcon 2024, and the decision to sunset the platform."
date: "2025-05-23"
tags: ["MPC", "privacy", "data analysis", "Devcon", "TLSNotary"]
@@ -18,7 +18,7 @@ Rather than building the full-fledged platform we originally planned, we decided
We got valuable feedback after the demo, and we recognized that identifying impactful, real-world use cases would be key to making our work meaningful. We began planning user interviews to validate promising application ideas and to implement them based on the findings.
![mpcstats-demo](/articles/mpcstats-retrospective/mpcstats-demo.png)
![mpcstats-demo](/articles/mpcstats-retrospective/mpcstats-demo.webp)
### What was achieved

View File

@@ -1,7 +1,7 @@
---
authors: ["PSE Comms Team"]
title: "PSE August 2025 newsletter"
image: "/articles/pse-aug-2025/cover.png"
image: "/articles/pse-aug-2025/cover.webp"
tldr: "Check out what PSE teams have been focused last month, July 2025!"
date: "2025-08-08"
tags:
@@ -111,12 +111,13 @@ We have started working on the on-chain verification of pods.
We continue the work on the zkp Wallet Unit. We completed the circuits over the P256 base field, including the first right-field circom implementation of ECDSA verification over P256. We are making progress towards compiling this for a Spartan + Hyrax backend over the T256 curve tower, and modifying the proof system to use re-randomisable commitments. This will yield a largely preprocessed and efficient prover.
Other significant strides across several key initiatives include:
- Completing the 1st draft of the zkID roadmap, outlining our upcoming milestones and long-term vision
- Completing research into OFAC compliance considerations for zk-KYC
- Launched early outreach for zkID Day at Devconnect - [express interest here](https://docs.google.com/forms/d/1fQyL-2PaXx0d5-ieiJkwI5Ypl1p5VAbBA2i0AIrSlH8/edit)
- Collected valuable community feedback on zkPDF to guide future improvements
- Integrating the Spartan Hydrax Backend
Other significant strides across several key initiatives include:
- Completing the 1st draft of the zkID roadmap, outlining our upcoming milestones and long-term vision
- Completing research into OFAC compliance considerations for zk-KYC
- Launched early outreach for zkID Day at Devconnect - [express interest here](https://docs.google.com/forms/d/1fQyL-2PaXx0d5-ieiJkwI5Ypl1p5VAbBA2i0AIrSlH8/edit)
- Collected valuable community feedback on zkPDF to guide future improvements
- Integrating the Spartan Hydrax Backend
### [Semaphore](https://pse.dev/en/projects/semaphore)

View File

@@ -1,7 +1,7 @@
---
authors: ["PSE Comms Team"]
title: "PSE July 2025 Newsletter"
image: "/articles/pse-july-2025/cover.png"
image: "/articles/pse-july-2025/cover.webp"
tldr: "A roundup of research and development updates from PSE teams in July 2025, covering client-side proving, post-quantum cryptography, zkID, TLSNotary, MPC, MACI, and more."
date: "2025-07-16"
tags:

View File

@@ -1,7 +1,7 @@
---
authors: ["PSE Comms Team"]
title: "PSE June 2025 newsletter"
image: "/articles/pse-june-2025/cover.png"
image: "/articles/pse-june-2025/cover.webp"
tldr: "Check out what PSE teams have been focused on in June 2025!"
date: "2025-07-08"
tags:

View File

@@ -64,7 +64,7 @@ This GitHub repo catalogs bugs and vulnerabilities in ZK apps.
- **Common Vulnerabilities**: Categorized, repeatable bug patterns
- Helps auditors target known failure points efficiently
![bug tracker](/articles/pse-security-what-is-new/1-bug-tracker.png)
![bug tracker](/articles/pse-security-what-is-new/1-bug-tracker.webp)
### 2. BigInt Audit with Veridise
@@ -74,7 +74,7 @@ Veridise and PSE collaborated to audit the Circom BigInt library.
- ZK circuits lend themselves well to formal methods due to their mathematical structure
- Highlights tradeoffs between traditional code audits and math-based specs
![audit report](/articles/pse-security-what-is-new/audit-report.png)
![audit report](/articles/pse-security-what-is-new/audit-report.webp)
### 3. ZK Circuit Static Analysis with Veridise
@@ -84,7 +84,7 @@ A collaboration with researchers at UCSB and UT Austin produced the paper _“Pr
- Works on Circom circuits but is language-agnostic
- Complements formal verification—fast, automatic, and scalable
![audit report](/articles/pse-security-what-is-new/security-analysis.png)
![audit report](/articles/pse-security-what-is-new/security-analysis.webp)
### 4. Bridge Bug Tracker
@@ -94,7 +94,7 @@ Created by Yufei Li, this repo documents bridge hacks and security learnings.
- Tracks vulnerabilities and security best practices
- Critical as L2s rely heavily on bridging for asset transfer
![audit report](/articles/pse-security-what-is-new/hack-tracker.png)
![audit report](/articles/pse-security-what-is-new/hack-tracker.webp)
---

View File

@@ -1,7 +1,7 @@
---
authors: ["PSE Team"]
title: "Reflecting on MACI Platform: What We Built, Learned, and Whats Next"
image: "/articles/reflecting-on-maci-platform/reflecting-on-maci-platform.png"
image: "/articles/reflecting-on-maci-platform/reflecting-on-maci-platform.webp"
tldr: "After a year of development and experimentation, the MACI Platform project is being sunset. In this retrospective, we share what we built, what we learned, and how the work can continue."
date: "2025-05-01"
canonical: ""

View File

@@ -1,6 +1,6 @@
---
title: Summon Major Update
image: "/articles/summon-major-update/cover.jpg"
image: "/articles/summon-major-update/cover.webp"
tldr: Im excited to share the biggest Summon update yet. 🎉
authors: ["Andrew Morris"]
date: 2025-05-21
@@ -32,25 +32,25 @@ Im excited to share the biggest Summon update yet. 🎉
```js
export default function main(a: number, b: number) {
const plus = a + b;
const gt = a > b;
const plus = a + b
const gt = a > b
return [plus, gt];
return [plus, gt]
}
// and separately provide mpcSettings:
const mpcSettings = [
{
name: 'alice',
inputs: ['a'],
outputs: ['main[0]', 'main[1]'],
},
{
name: 'bob',
inputs: ['b'],
outputs: ['main[0]', 'main[1]'],
},
];
{
name: "alice",
inputs: ["a"],
outputs: ["main[0]", "main[1]"],
},
{
name: "bob",
inputs: ["b"],
outputs: ["main[0]", "main[1]"],
},
]
```
### After
@@ -73,15 +73,15 @@ Per-party outputs are also coming, and will fit neatly into this API: `io.output
Type information is available via [`summon.d.ts`](https://github.com/privacy-scaling-explorations/summon/blob/main/summon.d.ts):
![intellisense demo](/articles/summon-major-update/intellisense-light.png)
![intellisense demo](/articles/summon-major-update/intellisense-light.webp)
## 2 · Typed Inputs (now with `bool`)
The third argument of `io.input` specifies the type:
![number example](/articles/summon-major-update/number-example-light.png)
![number example](/articles/summon-major-update/number-example-light.webp)
![bool example](/articles/summon-major-update/bool-example-light.png)
![bool example](/articles/summon-major-update/bool-example-light.webp)
`bool`s now work properly, so you can pass `true`/`false` instead of `1`/`0`. This is both better devX and removes unnecessary bits.
@@ -94,12 +94,12 @@ This also sets us up to support arrays/etc and grow into comprehensive typing à
Need a single program that adapts to many input sizes/participants? Public inputs let you accept these at **compile time**:
```js
const N = io.inputPublic('N', summon.number());
let votes: boolean[] = [];
const N = io.inputPublic("N", summon.number())
let votes: boolean[] = []
for (let i = 0; i < N; i++) {
const vote = io.input(`party${i}`, `vote${i}`, summon.bool());
votes.push(vote);
const vote = io.input(`party${i}`, `vote${i}`, summon.bool())
votes.push(vote)
}
```

View File

@@ -1,7 +1,7 @@
---
authors: ["Takamichi Tsutsumi"]
title: "TEE based private proof delegation"
image: "/articles/tee-based-ppd/cover.png"
image: "/articles/tee-based-ppd/cover.webp"
tldr: "Intro to trusted execution environment based private proof delegation"
date: "2025-05-20"
projects: ["private-proof-delegation"]
@@ -19,7 +19,7 @@ We built a TEE-based system for secure zero-knowledge proof delegation using Int
Typically, zk applications let consumer devices such as mobile phones or web browsers generate SNARK proofs. In such cases, the secret inputs from the clients never leave the device; therefore, the privacy of the inputs is protected. However, the complexity of the statements that weak hardware can prove is fairly limited. This means that proving more complex statements requires stronger hardware—something most users do not have access to in their daily lives.
One approach to address this issue is to **delegate the proof generation** to external servers with powerful hardware. This technique is called proof delegation. By using this technique, clients with weak devices can offload heavy computation to an external server and still obtain proof about their private data.
![Proof delegation image](/articles/tee-based-ppd/proof-delegation.png)
![Proof delegation image](/articles/tee-based-ppd/proof-delegation.webp)
_How naive proof delegation works_
In naive proof delegation, the client sends their raw inputs directly to the proving server, which then generates the proof. This approach works well for non-sensitive computations (e.g., public blockchain state transitions), but it fails to preserve the zero-knowledge property when the inputs are private. The server learns the data as is, so there is no privacy guarantee.
@@ -27,7 +27,7 @@ In naive proof delegation, the client sends their raw inputs directly to the pro
**Private proof delegation** (PPD) enhances this model by ensuring the private input remains hidden from the server. This is achieved by introducing a cryptographic layer between the client and the proving server. Instead of sending raw data, the client encrypts the private input before transmission.
![Private proof delegation image](/articles/tee-based-ppd/private-proof-delegation.png)
![Private proof delegation image](/articles/tee-based-ppd/private-proof-delegation.webp)
_Private proof delegation_
The cryptographic techniques that we can possibly use include
@@ -193,7 +193,7 @@ This would completely undermine the remote trust model that TEEs are designed to
TEE-based private proof delegation system involves a few additional components compared to naive PPD system. In this section, we describe how to construct a TEE-based private proof delegation system.
![TEE based PPD architecture](/articles/tee-based-ppd/tee-ppd.png)
![TEE based PPD architecture](/articles/tee-based-ppd/tee-ppd.webp)
_Architecture diagram_
In the architecture diagram above, we can see several components, including
@@ -396,9 +396,9 @@ For zkemail, we used proof-of-twitter circuit to measure the number. For this, w
## Semaphore benchmarks
1. Benchmark for proof generation inside TEE VM
![Benchmark1](/articles/tee-based-ppd/benchmark1.png)
![Benchmark1](/articles/tee-based-ppd/benchmark1.webp)
2. Proof generation inside normal VM
![Benchmark2](/articles/tee-based-ppd/benchmark2.png)
![Benchmark2](/articles/tee-based-ppd/benchmark2.webp)
E2E benchmarks
In our scenario, we measure the E2E performance, including
@@ -557,7 +557,7 @@ TEE-based proving is ready for real deployment — especially in cloud-based pri
Trusted Execution Environments (TEEs) come in multiple forms, distinguished primarily by the granularity of isolation they provide.
The two most prominent architectures are process-based TEEs and VM-based TEEs.
![Trust boundary image](/articles/tee-based-ppd/trust-boundary.png)
![Trust boundary image](/articles/tee-based-ppd/trust-boundary.webp)
_Trust boundary of different types of TEEs_
#### **Process-Based TEEs**
@@ -624,5 +624,5 @@ Azure Confidential VMs (ECedsv5 series) allow seamless deployment, scaling, and
**Remote Attestation Support at VM Level**:
VM-based TEEs like TDX provide attestation mechanisms that cover the entire VM, enabling strong guarantees about the full execution environment — not just a single process.
![Azure CVM](/articles/tee-based-ppd/cvm-availability.png)
![Azure CVM](/articles/tee-based-ppd/cvm-availability.webp)
_Confidential VM availability on Azure_

View File

@@ -1,7 +1,7 @@
---
authors: ["Private Governance Team at PSE"]
title: "The case for privacy in DAO voting"
image: "/articles/the-case-for-privacy-in-dao-voting/cover.png"
image: "/articles/the-case-for-privacy-in-dao-voting/cover.webp"
tldr: "Private voting prevents corruption, enables more honest participation, and unlocks the full potential of decentralized governance. This post explains why privacy matters, how MACI enables it, and what comes next."
date: "2025-08-07"
tags: ["privacy", "governance", "DAOs", "MACI", "zkSNARKs"]
@@ -33,7 +33,7 @@ Privacy-preserving voting infrastructure such as MACI provides a solution to pri
The best introduction for learning how MACI works is [this PSE article](https://maci.pse.dev/blog/maci-1-0-technical-introduction), but if youre strapped for time, here is a high level summary:
![image.png](/articles/the-case-for-privacy-in-dao-voting/1.png)
![image.webp](/articles/the-case-for-privacy-in-dao-voting/1.webp)
https://maci.pse.dev/blog/maci-1-0-technical-introduction
@@ -43,7 +43,7 @@ MACI relies on zkSNARKs or zero-knowledge proofs (ZKP) that verify a mathematica
In Vitaliks [post on privacy](https://vitalik.eth.limo/general/2025/04/14/privacy.html), he explains “Programmable cryptography techniques like zero-knowledge proofs are powerful, because **they are like Lego bricks for information flow**.” We intend to make the Lego bricks of zero-knowledge proofs easier to work with for DAOs and Ethereum in general.
![image.png](/articles/the-case-for-privacy-in-dao-voting/2.png)
![image.webp](/articles/the-case-for-privacy-in-dao-voting/2.webp)
https://maci.pse.dev/docs/introduction

View File

@@ -14,7 +14,7 @@ projects: ["zk-kit", "semaphore", "maci"]
On May 28th, the [OtterSec team](https://osec.io/) contacted the ZK-Kit team to report an under-constrained bug in the [BinaryMerkleRoot](https://github.com/privacy-scaling-explorations/zk-kit.circom/blob/binary-merkle-root.circom-v1.0.0/packages/binary-merkle-root/src/binary-merkle-root.circom) circuit.
We sincerely thank the two primary researchers from OtterSec who discovered the bug and provided code examples demonstrating it: Quasar and Tuyết.
We sincerely thank the two primary researchers from OtterSec who discovered the bug and provided code examples demonstrating it: Quasar and Tuyết.
PSE has worked with OtterSec to identify projects vulnerable and reached out to them.
@@ -27,6 +27,7 @@ After discussions with OtterSec and projects using this circuit to determine the
When using the [MultiMux1](https://github.com/iden3/circomlib/blob/master/circuits/mux1.circom#L21) circuit, it's essential to ensure that the selector is 0 or 1. The `BinaryMerkleRoot` circuit was missing the constraints to enforce this.
These constraints might not be necessary in projects that:
- Validate binary values in a separate circuit (outside this one), or
- Use a circuit like [Num2Bits](https://github.com/iden3/circomlib/blob/master/circuits/bitify.circom#L25), which ensures outputs are 0 or 1.
@@ -37,11 +38,12 @@ This issue makes it possible to generate a valid zero-knowledge proof with a lea
### Solution
Changes to `BinaryMerkleRoot`:
1. Replaced `indices[MAX_DEPTH]` with `index`:
Instead of passing an array of indices representing the Merkle path, the circuit now takes a single decimal `index`, whose binary representation defines the path.
Instead of passing an array of indices representing the Merkle path, the circuit now takes a single decimal `index`, whose binary representation defines the path.
2. Added before the `for` loop:
`signal indices[MAX_DEPTH] <== Num2Bits(MAX_DEPTH)(index);`
This line converts the decimal `index` into its binary representation.
`signal indices[MAX_DEPTH] <== Num2Bits(MAX_DEPTH)(index);`
This line converts the decimal `index` into its binary representation.
See the full update in this [pull request](https://github.com/privacy-scaling-explorations/zk-kit.circom/pull/25).
@@ -50,10 +52,10 @@ If you need the circuit without the new constraints, please continue using versi
With this bug fix, the Semaphore V4 circuit is being updated to use the latest version of `BinaryMerkleRoot`. A new trusted setup ceremony will be conducted, and all related Semaphore packages and contracts will be updated accordingly.
## Reproducing the Bug
Install [SageMath](https://www.sagemath.org/), version [10.5](https://github.com/3-manifolds/Sage_macOS/releases/tag/v2.5.0) was used for the code below, which generates test values to reproduce the bug.
Given a commitment and its Merkle siblings, it's possible to take a different commitment and find a new set of siblings and indices that produce the same root.
Given a commitment and its Merkle siblings, it's possible to take a different commitment and find a new set of siblings and indices that produce the same root.
```py
# normal pair from exploit
@@ -81,7 +83,7 @@ print("S:", v[S])
## Impact on ZK-Kit BinaryMerkleRoot circuit
The following code can be tested on [zkrepl](https://zkrepl.dev/).
Examples will be provided for proof lengths 1 and 2, but the same approach applies to proofs of greater length as well.
### Proof Length 1
@@ -93,8 +95,8 @@ Examples will be provided for proof lengths 1 and 2, but the same approach appli
3. Set `N` to the commitment you want to test. For this example, use `8501798477768465939972755925731717646123222073408967613007180932472889698337`.
4. You will get two values: `sel` (used in `evilmerkleProofIndices`) and `S` (used in `evilmerkleProofSiblings`).
![Sage Math ZK-Kit Proof Length 1](/articles/under-constrained-bug-in-binary-merkle-root-circuit-fixed-in-v200/sage-math-zk-kit-proof-length-1.png)
![Sage Math ZK-Kit Proof Length 1](/articles/under-constrained-bug-in-binary-merkle-root-circuit-fixed-in-v200/sage-math-zk-kit-proof-length-1.webp)
#### Circom Code
```circom
@@ -107,7 +109,7 @@ include "circomlib/circuits/comparators.circom";
// Copy/paste BinaryMerkleRoot circuit from https://github.com/privacy-scaling-explorations/zk-kit.circom/blob/binary-merkle-root.circom-v1.0.0/packages/binary-merkle-root/src/binary-merkle-root.circom
template Poc () {
// Root: 11531730952042319043316244572610162829336743623472270581141123607628087976577
// / \
// 5407869850562333726769604095330004527418297248703115046359956082084347839061 18699903263915756199535533399390350858126023699350081471896734858638858200219
@@ -143,9 +145,9 @@ template Poc () {
component main = Poc();
/* INPUT = {
} */
```
```
#### Result
@@ -155,7 +157,7 @@ Identical roots:
root 11531730952042319043316244572610162829336743623472270581141123607628087976577
evilroot 11531730952042319043316244572610162829336743623472270581141123607628087976577
```
### Proof Length 2
#### Steps
@@ -167,7 +169,7 @@ evilroot 11531730952042319043316244572610162829336743623472270581141123607628087
5. For N, compute the Poseidon hash of `evilIdentityCommitment` and the sibling value you provided for `evilMerkleProofSiblings[0]`.
6. The program will then generate two new values, these correspond to `evilMerkleProofIndices[1]` and `evilMerkleProofSiblings[1]`.
![Sage Math ZK-Kit Proof Length 2](/articles/under-constrained-bug-in-binary-merkle-root-circuit-fixed-in-v200/sage-math-zk-kit-proof-length-2.png)
![Sage Math ZK-Kit Proof Length 2](/articles/under-constrained-bug-in-binary-merkle-root-circuit-fixed-in-v200/sage-math-zk-kit-proof-length-2.webp)
#### Circom Code
@@ -219,7 +221,7 @@ template Poc () {
component main = Poc();
/* INPUT = {
} */
```
@@ -229,9 +231,9 @@ Identical roots:
```
root 3330844108758711782672220159612173083623710937399719017074673646455206473965
evilroot 3330844108758711782672220159612173083623710937399719017074673646455206473965
evilroot 3330844108758711782672220159612173083623710937399719017074673646455206473965
```
## Impact on Semaphore
The following code can be tested on [zkrepl](https://zkrepl.dev/).
@@ -239,15 +241,15 @@ The following code can be tested on [zkrepl](https://zkrepl.dev/).
#### Steps
1. Create a Semaphore identity and assign its `secretScalar` to `secret`.
2. Assign `merkleProofLength`, `merkleProofIndices` and `merkleProofSiblings`, according to the tree.
4. Assign the `secretScalar` of the identity you want to test to `evilsecret`. The identity for the example is a deterministic Semaphore identity with private key: `"123456"`.
5. Assign the value `18699903263915756199535533399390350858126023699350081471896734858638858200219` to `target0`.
6. Assign the value: `15684639248941018939207157301644512532843622097494605257727533950250892147976` to `target1`.
7. Set `N` to the commitment of the Semaphore identity with secret scalar `evilsecret`, in this case `6064632857532276925033625901604953426426313622216578376924090482554191077680`.
8. You will get two values: `sel` (used in `evilmerkleProofIndices`) and `S` (used in `evilmerkleProofSiblings`).
9. Use any values you prefer for `message`, `scope`, `evilmessage` and `evilscope`.
2. Assign `merkleProofLength`, `merkleProofIndices` and `merkleProofSiblings`, according to the tree.
3. Assign the `secretScalar` of the identity you want to test to `evilsecret`. The identity for the example is a deterministic Semaphore identity with private key: `"123456"`.
4. Assign the value `18699903263915756199535533399390350858126023699350081471896734858638858200219` to `target0`.
5. Assign the value: `15684639248941018939207157301644512532843622097494605257727533950250892147976` to `target1`.
6. Set `N` to the commitment of the Semaphore identity with secret scalar `evilsecret`, in this case `6064632857532276925033625901604953426426313622216578376924090482554191077680`.
7. You will get two values: `sel` (used in `evilmerkleProofIndices`) and `S` (used in `evilmerkleProofSiblings`).
8. Use any values you prefer for `message`, `scope`, `evilmessage` and `evilscope`.
![Sage Math Semaphore](/articles/under-constrained-bug-in-binary-merkle-root-circuit-fixed-in-v200/sage-math-semaphore.png)
![Sage Math Semaphore](/articles/under-constrained-bug-in-binary-merkle-root-circuit-fixed-in-v200/sage-math-semaphore.webp)
#### Circom Code
@@ -260,7 +262,7 @@ include "circomlib/circuits/comparators.circom";
include "circomlib/circuits/babyjub.circom";
// Copy/paste BinaryMerkleRoot circuit from https://github.com/privacy-scaling-explorations/zk-kit.circom/blob/binary-merkle-root.circom-v1.0.0/packages/binary-merkle-root/src/binary-merkle-root.circom
// Copy/paste Semaphore circuit from https://github.com/semaphore-protocol/semaphore/blob/v4.11.1/packages/circuits/src/semaphore.circom
template Poc () {
@@ -308,10 +310,10 @@ template Poc () {
component main = Poc();
/* INPUT = {
} */
```
#### Result
Identical roots:

View File

@@ -1,262 +0,0 @@
export const events = [
{
location: "Ethereum Cypherpunk Congress",
speakers: [{ url: "#", label: "Althea" }],
event: {
title: "Datapalooza",
description:
"Data can be useful, and also dangerous. This talk will raise questions and concerns that data enthusiasts might want to consider.",
url: "https://lu.ma/qwvo9tgo",
date: "Sunday, Nov 10",
time: "4:30 PM - 5:00 PM",
youtubeLink: "https://www.youtube.com/watch?v=aFjc4p9Eyk4",
},
},
{
location: "Ethereum Cypherpunk Congress",
speakers: [{ url: "https://x.com/samonchain", label: "@samonchain" }],
event: {
title: "What's next for PSE",
description:
"2024 has brought a lot of changes to the Privacy Stewards of Ethereum team. Hear from our new team learn about the past, present and future of PSE.",
url: "https://congress.web3privacy.info/",
date: "Monday, Nov 11",
time: "TBA",
youtubeLink: "",
},
},
{
location: "Lightning Talk - Stage 4",
speakers: [{ url: "https://x.com/0xjei", label: "@0xjei" }],
event: {
title: "The combination of ZKP +/- MPC +/- FHE",
description:
"This talk will provide you with the necessary intuition to understand when you should use ZKP, MPC or FHE, or any combination of them.",
url: "https://app.devcon.org/schedule/XPLVT8",
date: "Tuesday, Nov 12",
time: "12:40 PM - 12:50 PM",
youtubeLink: "https://www.youtube.com/watch?v=Tq7CVqDE_P4",
},
},
{
location: "Lightning Talk - Stage 4",
speakers: [{ url: "https://x.com/cperezz19", label: "@cperezz19" }],
event: {
title:
"MP-FHE experiments. Our learnings trying to find the next big tech to focus on.",
description:
"This talk mainly focuses on showcasing the work that some PSE members did while starting to dive into MPC-FHE during Q2 2024. This work is composed by various explorations within the MPC-FHE realm that move towards different directions and goals. From FHE compilers to FFT Bootstrapping GPU optimization proposals, passing by FHE Game demos and many application level implementations, the talk aims to reach beginner-advanced audience on the research/product paths that we have explored so far.",
url: "https://app.devcon.org/schedule/9JYWVP",
date: "Tuesday, Nov 12",
time: "1:10 PM - 1:20 PM",
youtubeLink: "https://www.youtube.com/watch?v=Didnvmet5Ng",
},
},
{
location: "Lightning Talk - Stage 4",
speakers: [{ url: "https://x.com/leolarav", label: "@leolarav" }],
event: {
title: "Modern ZKP Compilers",
description:
"At PSE we have done much ZKP advanced development. From that learning we are building a language and compiler, that is summarizing much of this learning. We answer questions like: Are compilers necessary in a zkVM world? What is the role of a compiler in ZKP development? What are its most common components? How different ways can this problem be approached? In this advanced talk, we will learn how we compile arbitrary boolean expressions, or how the SchwartzZippel lemma can be used to optimize.",
url: "https://app.devcon.org/schedule/CV7QXP",
date: "Tuesday, Nov 12",
time: "1:30 PM - 1:40 PM",
youtubeLink: "https://www.youtube.com/watch?v=JX9YtcG_EHk",
},
},
{
location: "Lightning Talk - Stage 4",
speakers: [{ url: "https://x.com/andyguzmaneth", label: "@andyguzmaneth" }],
event: {
title:
"The Blind Man's Elephant: a product vision towards private identities",
description:
"A short talk introducing the concepts of key principles we want to achieve in private ZK identities. Sparkling concepts like SSI and DIDs and why blockchains are the best way to ensure that. Finally it concludes with simple ZK and data-structure constructions and different alternatives that are seeking to provide this characteristics. In short, this is a lightning overview of the space of ZK, its desired features and different approaches to achieve them.",
url: "https://app.devcon.org/schedule/GSZKVK",
date: "Tuesday, Nov 12",
time: "2:10 PM - 2:20 PM",
youtubeLink: "https://www.youtube.com/watch?v=-BESF3MUM20",
},
},
{
location: "Lightning Talk - Stage 4",
speakers: [
{ url: "https://x.com/moven0831", label: "@moven0831" },
{ url: "https://x.com/vivi4322", label: "@vivi4322" },
],
event: {
title: "Mopro: Make Client-side Proving on Mobile Easy",
description:
"Mopro is a toolkit for ZK app development on mobile. Mopro makes client-side proving on mobile simple. Mopro aims to connect different adapters with different platforms. In this talk, we will share: - How to use Mopro to develop your own ZK mobile app. - What is the current development progress, including the current supported proving systems, supported platforms, and mobile GPU exploration results. - Moreover, we will share the challenges that Mopro faces and our future roadmap.",
url: "https://app.devcon.org/schedule/BZWFEM",
date: "Tuesday, Nov 12",
time: "2:50 PM - 3:00 PM",
youtubeLink: "https://www.youtube.com/watch?v=0ziKiYwhJHk",
},
},
{
location: "Talk - Stage 3",
speakers: [
{ url: "https://x.com/atheartengineer", label: "@atheartengineer" },
{ url: "#", label: "Ying Tong" },
],
event: {
title: "Introduction to Cryptography, New and Old",
description:
"Data can be useful, and also dangerous. This talk will raise questions and concerns that data enthusiasts might want to consider.",
url: "https://app.devcon.org/schedule/R3JC8U",
date: "Tuesday, Nov 12",
time: "4:30 PM - 4:55 PM",
youtubeLink: "https://www.youtube.com/watch?v=E6u3uQGP9J4",
},
},
{
location: "Talk - Stage 3",
speakers: [
{ url: "https://x.com/yush_g", label: "@yush_g" },
{ url: "https://x.com/sorasue77", label: "@sorasue77" },
],
event: {
title: "ZK Email: Fast Proofs and Production-Ready Account Recovery",
description:
"We discuss progress that ZK Email has made in making new proofs really easy, as well as interesting new on-chain directions for email-triggered transactions. Well go over proof registries, email-based multisig signers, and email guardians for account recovery in production.",
url: "https://app.devcon.org/schedule/WNQBQH",
date: "Wednesday, Nov 13",
time: "10:30 AM - 11:00 AM",
youtubeLink: "https://www.youtube.com/watch?v=YvzdNMpynZM",
},
},
{
location: "Talk - Classroom A",
speakers: [{ url: "https://x.com/viv_boops", label: "@viv_boops" }],
event: {
title: "Digital Pheromones: MPC for Human Connection & Coordination",
description:
"Recent MPC research from Cursive and PSE enables a new concept called 'digital pheromones': the ability to produce lightweight, privacy-preserving signals that people can use to coordinate safely and efficiently. The primary result we will cover is Trinity, a new 2PC scheme with nearly ideal UX/DevX, built on the trio of PLONK, Garbled Circuits, and KZG Witness Encryption. We will do a live demo with attendees and explore what a future filled with digital pheromones will enable!",
url: "https://app.devcon.org/schedule/LMCG3V",
date: "Wednesday, Nov 13",
time: "3:20 PM - 3:50 PM",
youtubeLink: "https://www.youtube.com/watch?v=TY2ZWmR_UqM",
},
},
{
location: "Panel - Stage 1",
speakers: [
{ url: "https://x.com/turboblitzzz", label: "@turboblitzzz" },
{ url: "https://x.com/7pastelblackcat", label: "@7pastelblackcat" },
{ url: "https://x.com/michaelelliot", label: "@michaelelliot" },
{ url: "https://x.com/0xnicoshark", label: "@0xnicoshark" },
{ url: "https://x.com/yanis_mezn", label: "@yanis_mezn" },
],
event: {
title: "Utilizing National IDs in the Ethereum Ecosystem",
description:
"This panel brings together developers of MynaWallet, Anon-Aadhaar, Proof of Passport, and zkPassport, who are exploring and developing applications that utilize government-issued IDs in the Ethereum ecosystem. We will discuss the characteristics of each ID system and what functions can be realized using tech stacks in the Ethereum ecosystem and cryptographic technology.",
url: "https://app.devcon.org/schedule/PR78EL",
date: "Thursday, Nov 14",
time: "9:45 AM - 10:45 AM",
youtubeLink: "https://www.youtube.com/watch?v=XsQ_DiECL0I",
},
},
{
location: "Panel - Stage 6",
speakers: [
{ url: "https://github.com/ed255", label: "@ed255" },
{ url: "https://x.com/janmajlaya_mall", label: "@janmajlaya_mall" },
{ url: "https://x.com/veronica241", label: "@veronica241" },
],
event: {
title: "Multi-Party FHE for Multi-Player Privacy",
description:
"Privacy is an unsolved challenge for blockchains and decentralized systems. ZK cryptography gets us there partially, but not all the way. ZK enables 'single-player private state,' and certain other kinds of privacy are impossible to realize with ZKPs alone. Panelists, the cryptography library devs, infrastructure builders, and application devs who have recently started to explore programmable encryption will discuss MP-FHE as one such tool for achieving more general privacy capabilities.",
url: "https://app.devcon.org/schedule/S9S8M9",
date: "Thursday, Nov 14",
time: "1:00 PM - 2:00 PM",
youtubeLink: "https://www.youtube.com/watch?v=Md1LKfuBGFo",
},
},
{
location: "Workshop - Classroom E",
speakers: [{ url: "https://x.com/yush_g", label: "@yush_g" }],
event: {
title:
"Build Your Own ZK Email Proofs, ZK Email Login, or ZK Account Recovery Module in 1.5 Hours",
description:
"We explain how to use a variety of ZK email-related SDKs, creating new proofs only using TypeScript or Solidity, whichever the developer is most familiar with. We will define new proofs in 5 minutes via sdk.prove.email, complete Solidity ZK email verifications via our generic relayer, and ideate your own projects!",
url: "https://app.devcon.org/schedule/DZVPRH",
date: "Thursday, Nov 14",
time: "1:30 PM - 3:00 PM",
youtubeLink: "https://www.youtube.com/watch?v=UsH8zwjcCD4",
},
},
{
location: "Lightning Talk - Stage 4",
speakers: [
{ url: "https://x.com/chihchengliang", label: "@chihchengliang" },
],
event: {
title: "Little Things We've Learned About FHE",
description:
"Recently, at PSE, we have been exploring the field of cryptography, specifically focusing on Fully Homomorphic Encryption (FHE). FHE enables secure interactions with encrypted data between different parties. In this presentation, we will introduce key concepts and essential information tailored for developers and application designers. This will help them quickly grasp the fundamentals without getting bogged down by complex mathematical details.",
url: "https://app.devcon.org/schedule/9JFDZA",
date: "Thursday, Nov 14",
time: "4:00 PM - 4:30 PM",
youtubeLink: "https://www.youtube.com/watch?v=wSfkpJDq8AI",
},
},
{
location: "Lightning Talk - Classroom E",
speakers: [{ url: "https://x.com/sinu_eth", label: "@sinu_eth" }],
event: {
title: "TLSNotary: Applying MPC and Interactive ZK to Prove Web2 Data",
description:
"Diving into TLSNotary, a protocol which leverages multi-party computation and interactive ZK to prove the authenticity and provenance of any data on the web to another party. Summary: 1. What it is and what it can do. 2. High-level overview of how it works 3. Details on the underlying MPC and ZK protocols that we use 4. How to use it.",
url: "https://app.devcon.org/schedule/RTVKJC",
date: "Thursday, Nov 14",
time: "4:30 PM - 4:40 PM",
youtubeLink: "https://www.youtube.com/watch?v=UPXYzWS7ZJ4&t=1s",
},
},
{
location: "Workshop - Classroom E",
speakers: [
{ url: "https://x.com/sinu_eth", label: "@sinu_eth" },
{ url: "https://x.com/heechkau", label: "@heechkau" },
{ url: "https://x.com/0xtsukino", label: "@0xtsukino" },
],
event: {
title: "Unlock Web2 Data with TLSNotary: Hands-On Workshop",
description:
"Join our hands-on workshop to master TLSNotary! Dive into multi-party-TLS and learn to prove and verify online data authenticity to a third-party verifier while ensuring privacy. Well start with small examples in Rust and build up to custom browser extensions in TypeScript to collect and verify private user data.",
url: "https://app.devcon.org/schedule/VPMQGM",
date: "Thursday, Nov 14",
time: "4:40 PM - 6:10 PM",
youtubeLink: "https://www.youtube.com/watch?v=FhKjScuaNxw",
},
},
]
export const booths = [
{
image: "/images/impact-booth.svg",
title: "PSE Impact Booth",
description:
"We are hosting open office hours at Devcon so you can ask questions, try out demos, and deep-dive into our projects. There will be representatives from MACI, TLSNotary, Semaphore, EcoDev, DevRel, Grants, Research and more all week. Come learn about the growing field of advanced cryptography!",
date: "Nov 12 - 15",
location: "S1-07 and S1-08",
},
{
image: "/images/cryptographic-connections.svg",
title: "Cryptographic Connections",
description:
"Throughout history, humans have used patterns, symbols, and codes to forge meaningful connections. This exhibit by Cursive explores cryptography not just as mathematical formulas, but as a deeply human art form.",
date: "Nov 12 - 15",
location: "Near Devcon Entrance",
learMore: {
description: "Learn more here:",
label: "cursive.team",
url: "https://x.com/cursive_team/status/1853780464512946589?s=46",
},
},
]

View File

@@ -127,7 +127,7 @@ extraLinks:
This creates themed link sections on your project detail page:
![Project links](/public/project/example-project-detail.jpg)
![Project links](/public/project/example-project-detail.webp)
## Adding YouTube Videos to a Project
@@ -142,7 +142,7 @@ youtubeLinks:
The videos will appear as clickable thumbnails with titles on the project page:
![YouTube Videos](/public/project/example-project-video.png)
![YouTube Videos](/public/project/example-project-video.webp)
## Adding Team Members to a Project
@@ -152,7 +152,7 @@ Add team members to your project using the `team` array in the frontmatter:
team:
- name: "John Doe"
role: "Lead Developer" # Optional
image: "/team/john-doe.jpg" # Optional
image: "/team/john-doe.webp" # Optional
email: "john.doe@example.com" # Optional
links: # Optional
github: "https://github.com/johndoe"
@@ -163,7 +163,7 @@ team:
Supported link types: `github`, `website`, `discord`, `twitter`, `youtube`, `telegram`
![Project Team Members](/public/project/example-project-team.png)
![Project Team Members](/public/project/example-project-team.webp)
## Tags and Themes

View File

@@ -1,7 +1,7 @@
---
id: "maci-platform"
name: "MACI Platform"
image: "maci-platform.png"
image: "maci-platform.webp"
section: "pse"
projectStatus: "inactive"
category: "application"

View File

@@ -1,7 +1,7 @@
---
id: "maci"
name: "MACI"
image: "maci.png"
image: "maci.webp"
section: "pse"
projectStatus: "active"
category: "application"

View File

@@ -1,7 +1,7 @@
---
id: "mopro"
name: "Mopro"
image: "mopro.png"
image: "mopro.webp"
section: "pse"
projectStatus: "active"
category: "devtools"

View File

@@ -1,7 +1,7 @@
---
id: "mpc-framework"
name: "MPC Framework"
image: "mpc-framework.png"
image: "mpc-framework.webp"
section: "pse"
projectStatus: "active"
category: "devtools"
@@ -18,12 +18,12 @@ links:
telegram: "https://t.me/+FKnOHTkvmX02ODVl"
team:
- name: "Andrew Morris"
image: "/avatars/andrew.png"
image: "/avatars/andrew.webp"
links:
github: "https://github.com/voltrevo"
twitter: "https://x.com/voltrevo"
- name: "Yanis Meziane"
image: "/avatars/yanis.png"
image: "/avatars/yanis.webp"
links:
github: "https://github.com/Meyanis95"
twitter: "https://x.com/yanis_mezn"

View File

@@ -1,7 +1,7 @@
---
id: "mpc-stats"
name: "MPCStats"
image: "mpc-stats.png"
image: "mpc-stats.webp"
section: "pse"
projectStatus: "inactive"
category: "applications"

View File

@@ -1,7 +1,7 @@
---
id: "mpz"
name: "mpz"
image: "mpz-cover.png"
image: "mpz-cover.webp"
section: "pse"
projectStatus: "active"
category: "devtools"

View File

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

View File

@@ -1,7 +1,7 @@
---
id: "p0tion"
name: "p0tion"
image: "p0tion.png"
image: "p0tion.webp"
section: "archived"
projectStatus: "maintained"
category: "devtools"

View File

@@ -1,7 +1,7 @@
---
id: "pod2"
name: "POD2"
image: "pod2.png"
image: "pod2.webp"
section: "collaboration"
projectStatus: "active"
category: "devtools"
@@ -17,7 +17,7 @@ links:
telegram: "https://t.me/zupass"
team:
- name: "Edu"
image: "/avatars/edu.jpeg"
image: "/avatars/edu.webp"
links:
github: "https://github.com/ed255"
extraLinks:

View File

@@ -1,7 +1,7 @@
---
id: "powers-of-tau"
name: "Perpetual Powers of Tau"
image: "powers-of-tau.png"
image: "powers-of-tau.webp"
section: "archived"
projectStatus: "maintained"
category: "devtools"

View File

@@ -1,7 +1,7 @@
---
id: "pse-security"
name: "PSE Security"
image: "pse-security.png"
image: "pse-security.webp"
section: "pse"
projectStatus: "inactive"
category: "research"

View File

@@ -1,7 +1,7 @@
---
id: "sonobe"
name: "Sonobe Folding Library"
image: "sonobe.png"
image: "sonobe.webp"
section: "pse"
projectStatus: "active"
category: "devtools"

View File

@@ -7,7 +7,7 @@ projectStatus: "inactive"
category: "application"
tldr: "A Zero-Knowledge Protocol built to handle anonymous user data."
license: "MIT"
previousBrandImage: "unirep-previousBrand.png"
previousBrandImage: "unirep-previousBrand.webp"
tags:
keywords: ["Anonymity/privacy", "Social", "Identity", "Reputation"]
themes: ["build", "play"]

View File

@@ -1,7 +1,7 @@
---
id: "zk-email"
name: "ZK Email"
image: "zk-email.png"
image: "zk-email.webp"
section: "collaboration"
projectStatus: "active"
category: "application"

View File

@@ -1,7 +1,7 @@
---
id: "zk-id"
name: "zkID"
image: "zk-id.png"
image: "zk-id.webp"
section: "pse"
projectStatus: "active"
category: "devtools"

View File

@@ -1,7 +1,7 @@
---
id: "zkevm-community"
name: "zkEVM Community Edition"
image: "zkevm.jpg"
image: "zkevm.webp"
section: "pse"
projectStatus: "inactive"
category: "devtools"
@@ -15,7 +15,7 @@ links:
github: "https://github.com/privacy-scaling-explorations/zkevm-circuits"
---
The zkEVM Community Edition was an early attempt to show that Ethereums execution layer could be verified using zero-knowledge proofs. The ambitious pursuit of a [“Type 1” or  "fully and uncompromisingly Ethereum-equivalent"](https://vitalik.eth.limo/general/2022/08/04/zkevm.html) zkEVM helped set off a race among peers and collaborators in the ecosystem, to snarkify Ethereum.
The zkEVM Community Edition was an early attempt to show that Ethereums execution layer could be verified using zero-knowledge proofs. The ambitious pursuit of a [“Type 1” or  "fully and uncompromisingly Ethereum-equivalent"](https://vitalik.eth.limo/general/2022/08/04/zkevm.html) zkEVM helped set off a race among peers and collaborators in the ecosystem, to snarkify Ethereum.
The project began as a collaboration between PSE, Scroll, and Taiko around 2021, a time when large scale ZK applications were not possible and ZK developers had very limited options. Groth16 was the only production-ready zkSNARK proof system but required per-circuit trusted setups, and the rigidity of R1CS meant constraint blowup. However, a viable path toward full validation of Ethereum blocks seemed to open up with the Zcash instantiation of PLONK called Halo2. With Halo2, universal trusted setups, more flexible constraint systems, and reduced constraint counts became feasible, providing a leap in tooling for general compute in ZK.

View File

@@ -1,7 +1,7 @@
---
id: "zkml"
name: "ZKML"
image: "zkml.png"
image: "zkml.webp"
section: "pse"
projectStatus: "inactive"
category: "research"

View File

@@ -1,7 +1,7 @@
---
id: "zkp2p"
name: "ZKP2P"
image: "zkp2p.png"
image: "zkp2p.webp"
section: "grant"
projectStatus: "active"
category: "application"

View File

@@ -10,6 +10,11 @@ export default [
{ files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"] },
{ ignores: ["dist/*", ".cache", "public", "node_modules", "*.esm.js", ".next/*"] },
{ languageOptions: { globals: globals.browser } },
// Node.js config files
{
files: ["*.config.js", "*.config.mjs", "*.config.cjs"],
languageOptions: { globals: globals.node }
},
pluginJs.configs.recommended,
...tseslint.configs.recommended,
{

View File

@@ -2,6 +2,28 @@
@tailwind components;
@tailwind utilities;
/* Performance optimizations for font loading */
.font-loading {
font-display: swap;
text-rendering: geometricPrecision !important;
}
/* Ensure fonts don't block rendering */
html {
font-synthesis: none;
}
/* Custom responsive navigation */
.nav-responsive {
display: none;
}
@media (min-width: 768px) {
.nav-responsive {
display: flex !important;
}
}
@layer base {
:root {
--background: #FFFFFF;

View File

@@ -22,5 +22,10 @@ export const useYoutube = () => {
return useQuery({
queryKey: ["pse-youtube-videos"],
queryFn: () => fetchYoutubeVideos(),
staleTime: 5 * 60 * 1000, // 5 minutes - videos don't change frequently
gcTime: 30 * 60 * 1000, // 30 minutes cache time
retry: 2,
retryDelay: 1000,
refetchOnWindowFocus: false, // Don't refetch on window focus
})
}

89
lib/critical-css.ts Normal file
View File

@@ -0,0 +1,89 @@
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

@@ -1,16 +0,0 @@
/**
* A simple fallback for zlib-sync's mask function.
* This implementation uses a basic XOR on the buffer.
*/
export const mask = (source: any, mask: any, output: any, offset: any) => {
offset = offset || 0
const length = source.length
// Allocate an output buffer if not provided.
if (!output || output.length < length) {
output = Buffer.allocUnsafe(length)
}
for (let i = 0; i < length; i++) {
output[i] = source[i] ^ mask[(i + offset) % mask.length]
}
return output
}

View File

@@ -1,18 +0,0 @@
import {
Space_Grotesk as FontDisplay,
DM_Sans as FontSans,
} from "next/font/google"
export const fontSans = FontSans({
subsets: ["latin"],
variable: "--font-sans",
weight: ["400", "500", "700"],
display: "swap",
})
export const fontDisplay = FontDisplay({
subsets: ["latin"],
variable: "--font-display",
weight: ["400", "500", "700"],
display: "swap",
})

View File

@@ -6,8 +6,6 @@ export enum ProjectCategory {
RESEARCH = "RESEARCH", // Category has now it's own page
}
export const ProjectCategories = Object.values(ProjectCategory) as string[]
// list of project groups
export const ProjectSections = [
"pse",
"grant",
@@ -21,16 +19,6 @@ export enum ProjectStatus {
INACTIVE = "inactive",
MAINTAINED = "maintained",
}
export interface Faq {
question: string
answer: ReactNode
}
export const ProjectCategoryLabelMapping: Record<ProjectCategory, string> = {
[ProjectCategory.APPLICATION]: "DEVTOOLS",
[ProjectCategory.DEVTOOLS]: "APPLICATIONS",
[ProjectCategory.RESEARCH]: "RESEARCH", // Category has now it's own page
}
export const ProjectSectionLabelMapping: Record<ProjectSection, string> = {
pse: "PSE projects",
@@ -53,35 +41,6 @@ export const ProjectStatusDescriptionMapping: Record<ProjectStatus, string> = {
"Maintenance projects are still being monitored for bug fixes, but are not under active feature development",
}
export interface AnnounceInterface {
id: number
type?: number
content: string
attachments?: string[]
timestamp: string
message_snapshots?: {
message?: {
content?: string
}
}[]
embeds?: {
type: "link" | "article"
url: string
title: string
description: string
color: number
}[]
}
export interface NewsInterface {
type: string
title: string
expires?: string
action: {
label: string
url: string
}
}
export type ProjectLinkWebsite =
| "github"
| "website"

View File

@@ -1,6 +1,7 @@
/* eslint-disable quotes */
import { ReadonlyURLSearchParams } from "next/navigation"
import { clsx, type ClassValue } from "clsx"
import { ReadonlyURLSearchParams } from "next/navigation"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
@@ -29,22 +30,6 @@ export function shuffleArray<T>(array: T[]) {
return array.sort(() => 0.5 - Math.random())
}
export function convertDirtyStringToHtml(string: string) {
const urlPattern =
// eslint-disable-next-line no-useless-escape
/\b(?:https?|ftp):\/\/[a-z0-9-+&@#\/%?=~_|!:,.;]*[a-z0-9-+&@#\/%=~_|]/gim
// eslint-disable-next-line no-useless-escape
const pseudoUrlPattern = /(^|[^\/])(www\.[\S]+(\b|$))/gim
if (!string) return ""
return string
.replace(/\n/g, "<br />")
.replace(urlPattern, '<a href="$&">$&</a>')
.replace(pseudoUrlPattern, '$1<a href="http://$2">$2</a>')
.toLowerCase()
}
// Get background image or return fallback image
export function getBackgroundImage(image: string | null | undefined = null) {
if (!image) return "/fallback.webp"

View File

@@ -1,8 +1,8 @@
import nextMdx from "@next/mdx";
import path from "path";
import { fileURLToPath } from "url";
import nextMdx from "@next/mdx"
import path from "path"
import { fileURLToPath } from "url"
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const withMDX = nextMdx({
extension: /\.mdx?$/,
@@ -12,19 +12,11 @@ const withMDX = nextMdx({
},
})
/** @type {import('next').NextConfig} */
const nextConfig = {
pageExtensions: ["js", "jsx", "mdx", "ts", "tsx", "md"],
reactStrictMode: true,
webpack: (config, { isServer }) => {
if (isServer) {
config.resolve.alias["zlib-sync"] = path.resolve(__dirname, "lib/dummy-zlib-sync.js");
config.externals.push("erlpack");
} else {
config.externals.push("discord.js", "@discordjs/rest");
}
return config;
},
images: {
remotePatterns: [
{
@@ -36,21 +28,152 @@ const nextConfig = {
protocol: "https",
hostname: "img.youtube.com",
pathname: "/**",
}
},
],
formats: ["image/avif", "image/webp"],
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
minimumCacheTTL: 60,
minimumCacheTTL: 31536000, // 1 year cache for images
dangerouslyAllowSVG: true,
contentDispositionType: "attachment",
contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
},
experimental: {
mdxRs: true,
optimizePackageImports: ["@heroicons/react", "lucide-react"],
// Static asset caching headers
async headers() {
return [
{
source: "/:path*",
headers: [
{
key: "X-DNS-Prefetch-Control",
value: "on",
},
],
},
{
source: "/favicon.(ico|svg|png)",
headers: [
{
key: "Cache-Control",
value: "public, max-age=31536000, immutable",
},
],
},
{
source: "/:path*\\.(css|js|woff|woff2|ttf|otf)",
headers: [
{
key: "Cache-Control",
value: "public, max-age=31536000, immutable",
},
],
},
{
source: "/api/proxy-matomo",
headers: [
{
key: "Content-Type",
value: "application/javascript; charset=utf-8",
},
{
key: "Cache-Control",
value: "public, max-age=86400, s-maxage=86400, must-revalidate",
},
{
key: "X-Content-Type-Options",
value: "nosniff",
},
],
},
{
source: "/:path*\\.(jpg|jpeg|png|gif|webp|svg|avif)",
headers: [
{
key: "Cache-Control",
value: "public, max-age=31536000, immutable",
},
],
},
{
source: "/articles/:path*",
headers: [
{
key: "Cache-Control",
value: "public, max-age=31536000, immutable",
},
],
},
]
},
// Configure compiler for modern browsers
compiler: {
// Remove React DevTools in production
// eslint-disable-next-line no-undef
removeConsole: process.env.NODE_ENV === "production",
// Enable modern features
emotion: false, // Disable if not using emotion
styledComponents: false, // Disable if not using styled-components
},
experimental: {
optimizePackageImports: [
"@heroicons/react",
"lucide-react",
"framer-motion",
"react-slick",
],
// Use modern compilation target
esmExternals: true,
// Enable more aggressive optimizations for modern browsers
serverComponentsExternalPackages: [],
},
// Enable SWC with modern target
swcMinify: true,
// Optimize runtime chunk size
webpack: (config, { isServer, dev }) => {
if (isServer) {
config.externals.push("erlpack")
}
// Optimize for modern browsers - reduce polyfills and transpilation
if (!isServer && !dev) {
// Reduce bundle size by excluding unnecessary polyfills (Vercel-safe)
config.resolve.fallback = {
...config.resolve.fallback,
fs: false,
path: false,
crypto: false,
stream: false,
buffer: false,
}
// Enable optimization for modern browsers (Vercel-compatible)
config.optimization = {
...config.optimization,
usedExports: true,
sideEffects: false,
// Modern module concatenation
concatenateModules: true,
// Optimize for production builds
minimize: true,
// Split CSS into smaller chunks
splitChunks: {
...config.optimization.splitChunks,
cacheGroups: {
...config.optimization.splitChunks?.cacheGroups,
styles: {
name: "styles",
type: "css/extract-css",
chunks: "all",
enforce: true,
},
},
},
}
}
return config
},
}
export default withMDX(nextConfig)

9177
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -26,7 +26,9 @@
},
"dependencies": {
"@discordjs/rest": "2.0.0",
"@next/mdx": "^13.5.0",
"@mdx-js/loader": "^3.1.1",
"@mdx-js/react": "^3.1.1",
"@next/mdx": "^14.2.32",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.4",
@@ -47,6 +49,7 @@
"gray-matter": "^4.0.3",
"gsap": "^3.12.1",
"html-to-react": "^1.7.0",
"jquery": "^3.7.1",
"js-yaml": "^4.1.0",
"lucide-react": "0.105.0-alpha.4",
"next": "14",
@@ -79,6 +82,7 @@
"@testing-library/jest-dom": "^6.6.4",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/jquery": "^3",
"@types/js-yaml": "^4.0.9",
"@types/node": "^20.19.0",
"@types/react": "^18.2.7",
@@ -118,5 +122,23 @@
]
},
"packageManager": "yarn@4.7.0",
"engineStrict": true
"engineStrict": true,
"browserslist": {
"production": [
">1%",
"last 2 versions",
"not dead",
"not ie 11",
"not op_mini all",
"Chrome >= 91",
"Firefox >= 90",
"Safari >= 15",
"Edge >= 91"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

View File

@@ -1,5 +1,5 @@
/** @type {import('postcss-load-config').Config} */
// eslint-disable-next-line no-undef
module.exports = {
plugins: {
tailwindcss: {},

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Some files were not shown because too many files have changed in this diff Show More