migrate project data from .ts to Markdown files (#453)

* migrate project data from .ts to Markdown files
This commit is contained in:
Kalidou Diagne
2025-06-13 22:05:23 +04:00
committed by GitHub
parent 4212a2c598
commit b4b1bb56e6
213 changed files with 3345 additions and 3973 deletions

View File

@@ -1,5 +1,5 @@
addReviewers: true
addAssignees: author
reviewers: [thebeyondr, AtHeartEngineer, ChialiT, heeckhau, kalidiagne]
reviewers: [kalidiagne, psedesign]
skipKeywords: [wip]
runOnDraft: false

View File

@@ -11,17 +11,17 @@ Enhancing Ethereum through cryptographic research and collective experimentation
### Add/Edit article
- For updating/adding a new article [you can follow this guide](https://github.com/privacy-scaling-explorations/pse.dev/blob/main/articles/README.md)
- For updating/adding a new article [you can follow this guide](content/articles/README.md)
### Add/Edit project list
- For updating/adding project detail [you can follow this guide](https://github.com/privacy-scaling-explorations/pse.dev/blob/main/data/projects/README.md)
- For updating/adding project detail [you can follow this guide](content/projects/README.md)
## PR Review process
#### For internal, PSE member:
- Suggest to tag: @kalidiagne, @psedesign, @AtHeartEngineer for PR review.
- Suggest to tag: @kalidiagne, @psedesign for PR review.
- If question, please reach out in discord channel #website-pse
#### For external:

View File

@@ -4,7 +4,7 @@ import { AppContent } from "@/components/ui/app-content"
import { Button } from "@/components/ui/button"
import { Label } from "@/components/ui/label"
import { Markdown } from "@/components/ui/markdown"
import { getArticles, getArticleById } from "@/lib/blog"
import { getArticles, getArticleById } from "@/lib/content"
import { cn, getBackgroundImage } from "@/lib/utils"
import { Metadata } from "next"
import Link from "next/link"

View File

@@ -8,9 +8,9 @@ import {
QueryClient,
dehydrate,
} from "@tanstack/react-query"
import ArticlesList from "@/components/blog/ArticlesList"
import { Skeleton } from "@/components/skeleton"
import { getArticles } from "@/lib/blog"
import { getArticles } from "@/lib/content"
import { ArticlesList } from "@/components/blog/articles-list"
export const dynamic = "force-dynamic"
@@ -79,21 +79,7 @@ const BlogPage = async ({ searchParams }: BlogPageProps) => {
return (
<div className="flex flex-col">
<div className="w-full bg-page-header-gradient">
<AppContent className="flex flex-col gap-4 py-10 w-full">
<Label.PageTitle label={LABELS.BLOG_PAGE.TITLE} />
{tag && (
<h2 className="text-xl font-semibold text-tuatara-800">
{`Filtered by tag: "${tag}"`}
</h2>
)}
<h6 className="font-sans text-base font-normal text-tuatara-950 md:text-[18px] md:leading-[27px] md:max-w-[700px]">
{LABELS.BLOG_PAGE.SUBTITLE}
</h6>
</AppContent>
</div>
<AppContent className="flex flex-col gap-10 lg:gap-16 pb-10 lg:py-10 lg:max-w-[978px]">
<AppContent className="flex flex-col gap-10 py-10 lg:gap-16 lg:py-16 lg:max-w-[978px]">
<Suspense fallback={<BlogLoadingSkeleton />}>
<HydrationBoundary state={dehydrate(queryClient)}>
<ArticlesList tag={tag} />

View File

@@ -2,7 +2,7 @@ import { LABELS } from "@/app/labels"
import { ArticleListCard } from "@/components/blog/article-list-card"
import { AppContent } from "@/components/ui/app-content"
import { Label } from "@/components/ui/label"
import { getArticles, Article, getArticleTags } from "@/lib/blog"
import { getArticles, Article, getArticleTags, ArticleTag } from "@/lib/content"
import { interpolate } from "@/lib/utils"
import {
HydrationBoundary,
@@ -23,22 +23,43 @@ export async function generateMetadata({
params,
}: BlogTagPageProps): Promise<Metadata> {
const { tag } = params
const tags = await getArticleTags()
const tagInfo = tags.find((t) => t.id === tag)
return {
title: interpolate(LABELS.BLOG_TAGS_PAGE.TAG_TITLE, { tag }),
title: interpolate(LABELS.BLOG_TAGS_PAGE.TAG_TITLE, {
tag: tagInfo?.name ?? tag,
}),
description: LABELS.BLOG_TAGS_PAGE.SUBTITLE,
}
}
export const generateStaticParams = async () => {
const tags = await getArticleTags()
return tags.map((tag) => ({ tag }))
return tags.map((tag) => ({ tag: tag.id }))
}
const BlogTagPage = async ({ params }: BlogTagPageProps) => {
const { tag } = params
const queryClient = new QueryClient()
// First get all tags to find the display name
await queryClient.prefetchQuery({
queryKey: ["get-articles-tags"],
queryFn: async () => {
try {
const tags = getArticleTags()
return tags
} catch (error) {
console.error("Error fetching article tags:", error)
return []
}
},
})
const tags = queryClient.getQueryData(["get-articles-tags"]) as ArticleTag[]
const tagInfo = tags.find((t) => t.id === tag)
await queryClient.prefetchQuery({
queryKey: ["get-articles-by-tag", tag],
queryFn: async () => {
@@ -59,8 +80,8 @@ const BlogTagPage = async ({ params }: BlogTagPageProps) => {
return (
<div className="flex flex-col">
<div className="w-full bg-page-header-gradient">
<AppContent className="flex flex-col gap-4 py-10 w-full">
<AppContent className="flex flex-col gap-10 pb-10 lg:py-10 lg:max-w-[978px]">
<div className="flex flex-col gap-4 py-10 w-full">
<Link
className="flex items-center gap-2 text-tuatara-950/80 hover:text-tuatara-950 mr-auto"
href="/blog"
@@ -70,15 +91,12 @@ const BlogTagPage = async ({ params }: BlogTagPageProps) => {
{LABELS.BLOG_TAGS_PAGE.BACK_TO_ARTICLES}
</span>
</Link>
<Label.PageTitle
<Label.Section
label={interpolate(LABELS.BLOG_TAGS_PAGE.TAG_TITLE, {
tag: tag,
tag: tagInfo?.name ?? tag,
})}
/>
</AppContent>
</div>
<AppContent className="flex flex-col gap-10 lg:gap-16 pb-10 lg:py-10 lg:max-w-[978px]">
</div>
<Suspense fallback={<BlogLoadingSkeleton />}>
<HydrationBoundary state={dehydrate(queryClient)}>
<div className="flex flex-col gap-5 lg:gap-14">
@@ -87,7 +105,7 @@ const BlogTagPage = async ({ params }: BlogTagPageProps) => {
))}
{articles?.length === 0 && (
<p className="text-center text-gray-500 py-10">
No articles found for tag &quot;{tag}&quot;
No articles found for tag &quot;{tagInfo?.name ?? tag}&quot;
</p>
)}
</div>

View File

@@ -2,7 +2,7 @@ import { LABELS } from "@/app/labels"
import { Icons } from "@/components/icons"
import { AppContent } from "@/components/ui/app-content"
import { Label } from "@/components/ui/label"
import { getArticleTags } from "@/lib/blog"
import { getArticleTags, ArticleTag } from "@/lib/content"
import { HydrationBoundary, QueryClient } from "@tanstack/react-query"
import Link from "next/link"
import { Suspense } from "react"
@@ -20,21 +20,21 @@ const BlogTagsPage = async () => {
queryKey: ["get-articles-tags"],
queryFn: async () => {
try {
const articles = getArticleTags()
return articles
const tags = getArticleTags()
return tags
} catch (error) {
console.error("Error fetching articles:", error)
console.error("Error fetching article tags:", error)
return []
}
},
})
const tags = queryClient.getQueryData(["get-articles-tags"]) as string[]
const tags = queryClient.getQueryData(["get-articles-tags"]) as ArticleTag[]
return (
<div className="flex flex-col pb-10v">
<div className="w-full bg-page-header-gradient">
<AppContent className="flex flex-col gap-6 py-10 w-full">
<div className="flex flex-col pb-10">
<AppContent className="flex flex-col gap-6 py-10 lg:max-w-[978px]">
<div className="flex flex-col gap-4 py-10 w-full">
<Link
className="flex items-center gap-2 text-tuatara-950/80 hover:text-tuatara-950 mr-auto"
href="/blog"
@@ -45,23 +45,22 @@ const BlogTagsPage = async () => {
</span>
</Link>
<Label.PageTitle label={LABELS.BLOG_TAGS_PAGE.TITLE} />
</AppContent>
</div>
<AppContent className="grid grid-cols-3 gap-2 lg:gap-4 lg:py-10 lg:max-w-[1200px]">
<Suspense fallback={null}>
<HydrationBoundary>
{tags?.map((tag) => (
<Link
href={`/blog/tags/${tag}`}
key={tag}
className="text-neutral-950 border-b-[2px] border-b-anakiwa-500 text-sm font-medium w-fit hover:text-anakiwa-500 duration-200"
>
{tag}
</Link>
))}
</HydrationBoundary>
</Suspense>
</div>
<div className="grid grid-cols-3 gap-2 lg:gap-10">
<Suspense fallback={null}>
<HydrationBoundary>
{tags?.map((tag) => (
<Link
href={`/blog/tags/${tag.id}`}
key={tag.id}
className="text-neutral-950 border-b-[2px] border-b-anakiwa-500 text-sm font-medium w-fit hover:text-anakiwa-500 duration-200"
>
{tag.name}
</Link>
))}
</HydrationBoundary>
</Suspense>
</div>
</AppContent>
</div>
)

View File

@@ -1,9 +1,9 @@
import Image from "next/image"
import Link from "next/link"
import { booths } from "@/data/events/devcon-7"
import { AppContent } from "@/components/ui/app-content"
import { Icons } from "@/components/icons"
import { booths } from "@/content/events/devcon-7"
export const Devcon7Booths = () => {
return (

View File

@@ -2,11 +2,11 @@
import { useState } from "react"
import Link from "next/link"
import { events } from "@/data/events/devcon-7"
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(

View File

@@ -1,52 +0,0 @@
import "@/styles/globals.css"
import Script from "next/script"
import { fontDisplay, fontSans } from "@/lib/fonts"
import { SiteFooter } from "@/components/site-footer"
import { SiteHeader } from "@/components/site-header"
import { TailwindIndicator } from "@/components/tailwind-indicator"
interface RootLayoutProps {
children: React.ReactNode
params: any
}
export default function RootLayout({ children }: RootLayoutProps) {
return (
<>
<html
lang="en"
className={`${fontSans.variable} ${fontDisplay.variable}`}
suppressHydrationWarning
>
<Script id="matomo-tracking" strategy="afterInteractive">
{`
var _paq = window._paq = window._paq || [];
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u="https://psedev.matomo.cloud/";
_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);
})();
`}
</Script>
<head />
<body
suppressHydrationWarning
className={"min-h-screen bg-background antialiased"}
>
<div className="relative flex min-h-screen flex-col">
<SiteHeader />
<div className="flex-1">{children}</div>
<SiteFooter />
</div>
<TailwindIndicator />
</body>
</html>
</>
)
}

View File

@@ -3,8 +3,6 @@
import { useCallback, useEffect, useRef, useState } from "react"
import Image from "next/image"
import Link from "next/link"
import { accelerationProgramFaq } from "@/data/programs/accelerationProgramFaq"
import { coreProgramFaq } from "@/data/programs/coreProgramFaq"
import { ReactNode } from "react-markdown/lib/ast-to-react"
import { twMerge } from "tailwind-merge"
@@ -299,8 +297,8 @@ export const ProgramPageContent = () => {
id="faq"
className="!border-anakiwa-300"
size="xs"
items={coreProgramFaq.map(
({ question: label, answer }, index) => {
items={LABELS.CORE_PROGRAM_FAQ.map(
({ QUESTION: label, ANSWER: answer }, index) => {
return {
label,
value: index.toString(),
@@ -441,14 +439,18 @@ export const ProgramPageContent = () => {
<Accordion
className="!border-anakiwa-300"
size="xs"
items={accelerationProgramFaq.map(
({ question: label, answer }, index) => {
items={LABELS.ACCELERATION_PROGRAM_FAQ.map(
({ QUESTION: label, ANSWER: answer }, index) => {
return {
label,
value: index.toString(),
children: (
<span className="flex flex-col gap-3 text-base text-tuatara-950">
{answer}
{typeof answer === "string"
? answer
: answer.map((item, index) => {
return <span key={index}>{item}</span>
})}
</span>
),
}

View File

@@ -1,8 +1,6 @@
import { Metadata } from "next"
import { getProjectById } from "@/lib/projectsUtils"
import { ProjectInterface } from "@/lib/types"
import { ProjectContent } from "../sections/ProjectContent"
type PageProps = {
@@ -14,10 +12,19 @@ export interface ProjectProps {
project: ProjectInterface
}
/*
export async function generateMetadata({
params,
}: PageProps): Promise<Metadata> {
const { project, content } = getProjectById(params.id)
const response = await fetch("/api/projects")
const projects = await response.json()
const project = projects.find(
(p: ProjectInterface) =>
String(p.id?.toLowerCase()) === params.id.toString().toLowerCase()
)
const content = project?.content
const imageUrl =
(project?.image ?? "")?.length > 0
? `/project-banners/${project?.image}`
@@ -36,7 +43,7 @@ export async function generateMetadata({
],
},
}
}
}*/
export default async function ProjectDetailPage({ params }: PageProps) {
return <ProjectContent id={params?.id?.toLowerCase()} />

View File

@@ -1,9 +1,7 @@
"use client"
import Link from "next/link"
import { useRouter } from "next/navigation"
import { siteConfig } from "@/config/site"
import { getProjectById } from "@/lib/projectsUtils"
import { ProjectCategory, ProjectStatus } from "@/lib/types"
import { cn } from "@/lib/utils"
@@ -21,14 +19,52 @@ import { WikiCard } from "@/components/cards/wiki-card"
import { ProjectTeamMembers } from "@/components/project/project-team"
import { ProjectBlogArticles } from "@/components/blog/project-blog-articles"
import { ProjectYouTubeVideos } from "@/components/sections/ProjectYouTubeVideos"
import { useProjects } from "@/app/providers/ProjectsProvider"
const markdownComponents = {
h1: ({ ...props }) =>
createMarkdownElement("h1", {
className: "text-neutral-700 text-[22px] leading-6 font-bold pt-10 pb-4",
...props,
}),
h2: ({ ...props }) =>
createMarkdownElement("h2", {
className: "text-neutral-700 text-[22px] leading-6 font-bold pt-10 pb-4",
...props,
}),
h3: ({ ...props }) =>
createMarkdownElement("h3", {
className: "text-neutral-700 text-[22px] leading-6 font-bold pt-10 pb-4",
...props,
}),
h4: ({ ...props }) =>
createMarkdownElement("h4", {
className: "text-neutral-700 text-[22px] leading-6 font-bold pt-10 pb-4",
...props,
}),
h5: ({ ...props }) =>
createMarkdownElement("h5", {
className: "text-neutral-700 text-[22px] leading-6 font-bold pt-10 pb-4",
...props,
}),
h6: ({ ...props }) =>
createMarkdownElement("h6", {
className: "text-neutral-700 text-[22px] leading-6 font-bold pt-10 pb-4",
...props,
}),
p: ({ ...props }) =>
createMarkdownElement("p", {
className: "py-2 leading-[150%] text-base text-slate-600",
...props,
}),
}
export const ProjectContent = ({ id }: { id: string }) => {
const router = useRouter()
const { project, content } = getProjectById(id) ?? {}
const { getProjectById } = useProjects()
const { project } = getProjectById(id) ?? {}
const hasSocialLinks = Object.keys(project?.links ?? {}).length > 0
const editPageURL = siteConfig?.editProjectPage(project?.id)
const editPageURL = siteConfig?.editProjectPage(project?.id as string)
const ProjectStatusMessageMap: Record<ProjectStatus, string> = {
[ProjectStatus.ACTIVE]: "",
@@ -41,14 +77,12 @@ export const ProjectContent = ({ id }: { id: string }) => {
const isResearchProject = project?.category === ProjectCategory.RESEARCH
if (!project?.id) {
router.push("/404")
}
if (!project?.id) {
return null
}
console.log(" rest", project)
return (
<section className="bg-project-page-gradient">
<div className="flex flex-col">
@@ -57,16 +91,16 @@ export const ProjectContent = ({ id }: { id: string }) => {
<div
className={cn(
"grid grid-cols-1 gap-10 lg:items-start lg:gap-12",
content?.description?.length > 0
(project?.content as any)?.length > 0
? "lg:grid-cols-[140px_1fr_290px]"
: "lg:grid-cols-[1fr_290px]"
)}
>
{content?.description?.length > 0 && (
{(project?.content as any)?.length > 0 && (
<WikiSideNavigation
className="hidden lg:block"
project={project}
content={content?.description}
content={project?.content as any}
/>
)}
@@ -87,54 +121,9 @@ export const ProjectContent = ({ id }: { id: string }) => {
<h1 className="py-2 text-3xl font-bold leading-[110%] md:text-5xl">
{project?.name}
</h1>
{content?.tldr && (
<Markdown
components={{
h1: ({ ...props }) =>
createMarkdownElement("h1", {
className:
"text-neutral-700 text-[22px] leading-6 font-bold pt-10 pb-4",
...props,
}),
h2: ({ ...props }) =>
createMarkdownElement("h2", {
className:
"text-neutral-700 text-[22px] leading-6 font-bold pt-10 pb-4",
...props,
}),
h3: ({ ...props }) =>
createMarkdownElement("h3", {
className:
"text-neutral-700 text-[22px] leading-6 font-bold pt-10 pb-4",
...props,
}),
h4: ({ ...props }) =>
createMarkdownElement("h4", {
className:
"text-neutral-700 text-[22px] leading-6 font-bold pt-10 pb-4",
...props,
}),
h5: ({ ...props }) =>
createMarkdownElement("h5", {
className:
"text-neutral-700 text-[22px] leading-6 font-bold pt-10 pb-4",
...props,
}),
h6: ({ ...props }) =>
createMarkdownElement("h6", {
className:
"text-neutral-700 text-[22px] leading-6 font-bold pt-10 pb-4",
...props,
}),
p: ({ ...props }) =>
createMarkdownElement("p", {
className:
"py-2 leading-[150%] text-base text-slate-600",
...props,
}),
}}
>
{content?.tldr}
{project?.tldr && (
<Markdown components={markdownComponents}>
{project?.tldr}
</Markdown>
)}
</div>
@@ -176,54 +165,9 @@ export const ProjectContent = ({ id }: { id: string }) => {
</span>
)}
<div className="flex flex-col w-full text-base font-normal leading-relaxed">
{content?.description && (
<Markdown
components={{
h1: ({ ...props }) =>
createMarkdownElement("h1", {
className:
"text-neutral-700 text-[22px] leading-6 font-bold pt-10 pb-4",
...props,
}),
h2: ({ ...props }) =>
createMarkdownElement("h2", {
className:
"text-neutral-700 text-[22px] leading-6 font-bold pt-10 pb-4",
...props,
}),
h3: ({ ...props }) =>
createMarkdownElement("h3", {
className:
"text-neutral-700 text-[22px] leading-6 font-bold pt-10 pb-4",
...props,
}),
h4: ({ ...props }) =>
createMarkdownElement("h4", {
className:
"text-neutral-700 text-[22px] leading-6 font-bold pt-10 pb-4",
...props,
}),
h5: ({ ...props }) =>
createMarkdownElement("h5", {
className:
"text-neutral-700 text-[22px] leading-6 font-bold pt-10 pb-4",
...props,
}),
h6: ({ ...props }) =>
createMarkdownElement("h6", {
className:
"text-neutral-700 text-[22px] leading-6 font-bold pt-10 pb-4",
...props,
}),
p: ({ ...props }) =>
createMarkdownElement("p", {
className:
"text-tuatara-700 font-sans text-base font-normal peer mt-4 first:mt-0",
...props,
}),
}}
>
{content?.description}
{(project?.content as any) && (
<Markdown components={markdownComponents}>
{project?.content as any}
</Markdown>
)}

View File

@@ -14,7 +14,7 @@ import { AppContent } from "@/components/ui/app-content"
import { Button } from "@/components/ui/button"
import { Label } from "@/components/ui/label"
import ResourcesContent from "../content/resources.md"
import ResourcesContent from "@/content/resources.md"
interface ResourceItemProps {
label: string

View File

@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from "next/server"
import { getArticles } from "@/lib/blog"
import { getArticles } from "@/lib/content"
// Cache control
export const revalidate = 60 // Revalidate cache after 60 seconds

26
app/api/projects/route.ts Normal file
View File

@@ -0,0 +1,26 @@
import { NextRequest, NextResponse } from "next/server"
import { getProjects } from "@/lib/content"
// Cache control
export const revalidate = 60 // Revalidate cache after 60 seconds
export const dynamic = "force-dynamic" // Ensure the route is always evaluated
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const tag = searchParams.get("tag") || undefined
const limit = searchParams.get("limit")
? parseInt(searchParams.get("limit") as string, 10)
: undefined
const status = searchParams.get("status") || undefined
try {
const projects = getProjects({ tag, limit, status })
return NextResponse.json(projects ?? [])
} catch (error) {
console.error("Error fetching projects:", error)
return NextResponse.json(
{ error: "Failed to fetch projects", success: false },
{ status: 500 }
)
}
}

View File

@@ -343,4 +343,75 @@ export const LABELS = {
"We are mapping the emerging zero knowledge ecosystem through collective experimentation. We collaborate, share what we learn, and welcome contributions from around the world.",
},
},
CORE_PROGRAM_FAQ: [
{
QUESTION: "Who can apply?",
ANSWER:
"The Core Program is open to university students based in Japan, South Korea, Taiwan, Costa Rica, Ecuador and Argentina with a basic understanding of programming. If you're currently enrolled in a mathematics or computer science program, you're likely an excellent fit.",
},
{
QUESTION: "What is the structure of the program?",
ANSWER:
"We use a hybrid learning model with the majority of learning happening online and weekly in-person meetings for discussions and problem-solving. The program consists of three stages: 1) self-driven exploration & guidance, 2) hands-on circuit writing, and 3) open-source project contribution.",
},
{
QUESTION: "How much time will I need to commit?",
ANSWER:
"We're looking for dedicated students who can commit 40 hours a week from mid-July to September. You will be required to attend in-person meetups once a week and make presentations.",
},
{
QUESTION: "Can I participate remotely?",
ANSWER:
"Unfortunately no, the weekly in-person sessions are required for in-depth discussions and collaborative problem-solving.",
},
{
QUESTION: "What will I gain from this program?",
ANSWER:
"Upon completing the program, you'll have comprehensive knowledge about programmable cryptography, a bolstered GitHub portfolio, and opportunities to apply for grants for further research and contributions.",
},
{
QUESTION: "What if I have more questions?",
ANSWER:
"For any further questions or additional information, you can join our <a class='underline' target='_blank' href='https://t.me/+ebGauHbpDE0yZGIx'>Telegram group</a>!",
},
],
ACCELERATION_PROGRAM_FAQ: [
{
QUESTION: "Who can apply?",
ANSWER:
"The Acceleration program is open to alumni of our entry level programs (e.g. Launch Program, ZK Playground) and other applicants at beginner to intermediate levels with programmable cryptography.",
},
{
QUESTION: "What platform does the program use?",
ANSWER:
"We will primarily utilize a Github repository for managing documents and staging of the grant proposals, grantees, and their progress. Stakeholders involved can utilize Github issues and pull requests to comment and collaborate.",
},
{
QUESTION: "How does the grant funding work?",
ANSWER: [
"While the core funding this program comes through PSE via the Ethereum Ecosystem Support Program (ESP), some bounties are supported by specific teams.",
"Selected grantees will receive a small amount of funding after the completion of the first milestone. Following milestones will be awarded larger amounts.",
],
},
{
QUESTION: "How many proposals are accepted per open task?",
ANSWER:
"Generally one proposal will be accepted. However, it is possible for applicants to form a team and work collaboratively.",
},
{
QUESTION: "How long will I have to tackle the open tasks?",
ANSWER:
"Though our rounds are run in three month periods, the duration of your grant may be shorter or longer depending on task details.",
},
{
QUESTION: "Can I propose an open task?",
ANSWER:
"If you have an interesting idea, you can submit it as an self proposed open task. Just to make sure you clearly credit to the original idea and clearly state if that idea is also funded by someone else.",
},
{
QUESTION: "What if I have more questions?",
ANSWER:
"For any further questions or additional information, you can join our Telegram group!",
},
],
}

View File

@@ -1,8 +1,13 @@
import "@/styles/globals.css"
import Script from "next/script"
import { Metadata, Viewport } from "next"
import { GlobalProviderLayout } from "@/components/layouts/GlobalProviderLayout"
import { SiteFooter } from "@/components/site-footer"
import { SiteHeader } from "@/components/site-header"
import { TailwindIndicator } from "@/components/tailwind-indicator"
import { siteConfig } from "@/config/site"
import { QueryClientProviderLayout } from "@/components/layouts/QueryProviderLayout"
import { cn } from "@/lib/utils"
import { DM_Sans, Inter, Space_Grotesk } from "next/font/google"
@@ -38,6 +43,13 @@ const sans = DM_Sans({
const fonts = [inter, display, sans]
export const viewport: Viewport = {
themeColor: [
{ media: "(prefers-color-scheme: light)", color: "white" },
{ media: "(prefers-color-scheme: dark)", color: "black" },
],
}
export const metadata: Metadata = {
title: {
default: siteConfig.name,
@@ -99,25 +111,46 @@ export const metadata: Metadata = {
},
}
export const viewport: Viewport = {
themeColor: [
{ media: "(prefers-color-scheme: light)", color: "white" },
{ media: "(prefers-color-scheme: dark)", color: "black" },
],
}
interface RootLayoutProps {
children: React.ReactNode
}
export default function RootLayout({ children }: RootLayoutProps) {
return (
<QueryClientProviderLayout>
<html lang="en" className={fonts.map((font) => font.className).join(" ")}>
<body className="min-h-screen bg-background antialiased">
{children}
</body>
</html>
</QueryClientProviderLayout>
<html
lang="en"
className={cn(inter.variable, display.variable, sans.variable)}
suppressHydrationWarning
>
<Script id="matomo-tracking" strategy="afterInteractive">
{`
var _paq = window._paq = window._paq || [];
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u="https://psedev.matomo.cloud/";
_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);
})();
`}
</Script>
<head />
<body
suppressHydrationWarning
className="min-h-screen bg-background antialiased"
>
<GlobalProviderLayout>
<div className="relative flex min-h-screen flex-col">
<SiteHeader />
<div className="flex-1">{children}</div>
<SiteFooter />
</div>
<TailwindIndicator />
</GlobalProviderLayout>
</body>
</html>
)
}

View File

@@ -0,0 +1,68 @@
"use client"
import {
createContext,
useContext,
ReactNode,
useState,
useEffect,
} from "react"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { ProjectsProvider } from "./ProjectsProvider"
interface GlobalContextType {
children?: ReactNode
isDarkMode: boolean
setIsDarkMode: (value: boolean) => void
}
const GlobalContext = createContext<GlobalContextType | undefined>(undefined)
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
retry: false,
},
},
})
export function GlobalProvider({ children }: { children: ReactNode }) {
const [isDarkMode, setIsDarkMode] = useState(false)
// Initialize dark mode from system preference
useEffect(() => {
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
setIsDarkMode(mediaQuery.matches)
const handleChange = (e: MediaQueryListEvent) => {
setIsDarkMode(e.matches)
}
mediaQuery.addEventListener("change", handleChange)
return () => mediaQuery.removeEventListener("change", handleChange)
}, [])
const value = {
isDarkMode,
setIsDarkMode,
}
return (
<QueryClientProvider client={queryClient}>
<ProjectsProvider>
<GlobalContext.Provider value={value}>
{children}
</GlobalContext.Provider>
</ProjectsProvider>
</QueryClientProvider>
)
}
export function useGlobalProvider() {
const context = useContext(GlobalContext)
if (context === undefined) {
throw new Error("useGlobalContext must be used within a GlobalProvider")
}
return context
}

View File

@@ -0,0 +1,451 @@
"use client"
import {
createContext,
useContext,
ReactNode,
useState,
useMemo,
useCallback,
} from "react"
import { useQuery } from "@tanstack/react-query"
import Fuse from "fuse.js"
import { ProjectCategory, ProjectInterface } from "@/lib/types"
import { uniq } from "@/lib/utils"
import { LABELS } from "@/app/labels"
export type ProjectSortBy = "random" | "asc" | "desc" | "relevance"
export type ProjectFilter =
| "keywords"
| "builtWith"
| "themes"
| "fundingSource"
export type FiltersProps = Record<ProjectFilter, string[]>
export const DEFAULT_PROJECT_SORT_BY: ProjectSortBy = "asc"
interface ProjectInterfaceScore extends ProjectInterface {
score: number
}
export const SortByFnMapping: Record<
ProjectSortBy,
(a: ProjectInterfaceScore, b: ProjectInterfaceScore) => number
> = {
random: () => Math.random() - 0.5,
asc: (a, b) => a.name.localeCompare(b.name),
desc: (a, b) => b.name.localeCompare(a.name),
relevance: (a, b) => b?.score - a?.score,
}
export const FilterLabelMapping: Record<ProjectFilter, string> = {
keywords: LABELS.COMMON.FILTER_LABELS.KEYWORDS,
builtWith: LABELS.COMMON.FILTER_LABELS.BUILT_WITH,
themes: LABELS.COMMON.FILTER_LABELS.THEMES,
fundingSource: LABELS.COMMON.FILTER_LABELS.FUNDING_SOURCE,
}
interface ProjectsContextType {
projects: ProjectInterface[]
researchs: ProjectInterface[]
isLoading: boolean
isError: boolean
error: Error | null
filters: FiltersProps
activeFilters: Partial<FiltersProps>
queryString: string
searchQuery: string
sortBy: ProjectSortBy
getProjectById: (id: string | number) => { project: ProjectInterface }
toggleFilter: (params: {
tag: ProjectFilter
value: string
searchQuery?: string
}) => void
setFilterFromQueryString: (filters: Partial<FiltersProps>) => void
onFilterProject: (searchPattern: string) => void
onSelectTheme: (theme: string, searchPattern?: string) => void
sortProjectBy: (sortBy: ProjectSortBy) => void
refetch: () => Promise<any>
}
const ProjectsContext = createContext<ProjectsContextType | undefined>(
undefined
)
const createURLQueryString = (params: Partial<FiltersProps>): string => {
if (Object.keys(params)?.length === 0) return ""
return Object.keys(params)
.map((key: any) => `${key}=${encodeURIComponent((params as any)[key])}`)
.join("&")
}
const filterProjects = ({
searchPattern = "",
activeFilters = {},
findAnyMatch = false,
projects: projectListItems = [],
}: {
searchPattern?: string
activeFilters?: Partial<FiltersProps>
findAnyMatch?: boolean
projects?: ProjectInterface[]
}) => {
const projectList = projectListItems.map((project: any) => ({
...project,
id: project?.id?.toLowerCase(),
}))
const keys = [
"name",
"tldr",
"tags.themes",
"tags.keywords",
"tags.builtWith",
"projectStatus",
]
const tagsFiltersQuery: Record<string, string>[] = []
Object.entries(activeFilters).forEach(([key, values]) => {
values.forEach((value) => {
if (!value) return
tagsFiltersQuery.push({
[`tags.${key}`]: value,
})
})
})
const noActiveFilters =
tagsFiltersQuery.length === 0 && searchPattern.length === 0
if (noActiveFilters) return projectList
let query: any = {}
if (findAnyMatch) {
query = {
$or: [...tagsFiltersQuery, { name: searchPattern }],
}
} else if (searchPattern?.length === 0) {
query = {
$and: [...tagsFiltersQuery],
}
} else if (tagsFiltersQuery.length === 0) {
query = {
name: searchPattern,
}
} else {
query = {
$and: [
{
$and: [...tagsFiltersQuery],
},
{ name: searchPattern },
],
}
}
const fuse = new Fuse(projectList, {
threshold: 0.3,
useExtendedSearch: true,
includeScore: true,
findAllMatches: true,
distance: 200,
keys,
})
const result = fuse.search(query)?.map(({ item, score }) => ({
...item,
score,
}))
return result ?? []
}
const sortProjectByFn = ({
projects = [],
sortBy = DEFAULT_PROJECT_SORT_BY,
category = null,
ignoreCategories = [ProjectCategory.RESEARCH],
}: {
projects: ProjectInterface[]
sortBy?: ProjectSortBy
category?: ProjectCategory | null
ignoreCategories?: ProjectCategory[]
}) => {
const sortedProjectList: ProjectInterface[] = [
...(projects as ProjectInterfaceScore[]),
]
.sort(SortByFnMapping[sortBy])
.filter(
(project) =>
!ignoreCategories
?.map((category) => category?.toLowerCase())
.includes(project.category?.toLowerCase() as any)
)
if (category) {
return sortedProjectList.filter(
(project) => project.category?.toLowerCase() === category?.toLowerCase()
)
}
return sortedProjectList.map((project: any) => ({
id: project?.id?.toLowerCase(),
...project,
}))
}
const getProjectFilters = (projects: ProjectInterface[]): FiltersProps => {
const filters: FiltersProps = {
themes: ["play", "build", "research"],
keywords: [],
builtWith: [],
fundingSource: [],
}
projects.forEach((project) => {
if (project?.tags?.builtWith) {
filters.builtWith.push(
...project.tags.builtWith.map((tag) => {
if (typeof tag === "string") {
return tag.toLowerCase()
}
return ""
})
)
}
if (project?.tags?.keywords) {
filters.keywords.push(
...project.tags.keywords.map((keyword) =>
typeof keyword === "string" ? keyword.toLowerCase() : ""
)
)
}
})
Object.entries(filters).forEach(([key, entries]) => {
filters[key as ProjectFilter] = uniq(entries)
})
return filters
}
async function fetchProjects(tag?: string) {
try {
const params = new URLSearchParams()
if (tag) params.append("tag", tag)
const response = await fetch(`/api/projects?${params.toString()}`, {
cache: "default",
})
if (!response.ok) {
throw new Error(`Failed to fetch projects: ${response.status}`)
}
const projects = await response.json()
return projects || []
} catch (error) {
console.error("Error fetching projects:", error)
throw error
}
}
interface ProjectsProviderProps {
children: ReactNode
tag?: string
}
export function ProjectsProvider({ children, tag }: ProjectsProviderProps) {
const [sortBy, setSortBy] = useState<ProjectSortBy>(DEFAULT_PROJECT_SORT_BY)
const [activeFilters, setActiveFilters] = useState<Partial<FiltersProps>>({})
const [queryString, setQueryString] = useState("")
const [searchQuery, setSearchQuery] = useState("")
const {
data: fetchedProjects = [],
isLoading,
isError,
error,
refetch,
} = useQuery({
queryKey: ["projects", tag],
queryFn: () => fetchProjects(tag),
})
const getProjectById = useCallback(
(id: string | number) => {
const project: ProjectInterface =
fetchedProjects.filter(
(project: ProjectInterface) =>
String(project.id?.toLowerCase()) === id.toString().toLowerCase()
)[0] ?? {}
const content = project?.content
return {
project,
content,
}
},
[fetchedProjects]
)
const filters = useMemo(
() => getProjectFilters(fetchedProjects),
[fetchedProjects]
)
const filteredProjects = useMemo(
() =>
filterProjects({
searchPattern: searchQuery,
activeFilters,
projects: fetchedProjects,
}),
[searchQuery, activeFilters, fetchedProjects]
)
const projects = useMemo(
() =>
sortProjectByFn({
projects: filteredProjects,
sortBy,
ignoreCategories: [ProjectCategory.RESEARCH],
}),
[filteredProjects, sortBy]
)
const researchs = useMemo(
() =>
sortProjectByFn({
projects: filteredProjects,
sortBy,
category: ProjectCategory.RESEARCH,
ignoreCategories: [
ProjectCategory.DEVTOOLS,
ProjectCategory.APPLICATION,
],
}),
[filteredProjects, sortBy]
)
const toggleFilter = useCallback(
({
tag: filterKey,
value,
searchQuery: search,
}: {
tag: ProjectFilter
value: string
searchQuery?: string
}) => {
if (!filterKey) return
setActiveFilters((prevFilters) => {
const values: string[] = prevFilters?.[filterKey] ?? []
const index = values?.indexOf(value)
const newValues = [...values]
if (index > -1) {
newValues.splice(index, 1)
} else {
newValues.push(value)
}
const activeFiltersNormalized = newValues.filter(Boolean)
const newActiveFilters = {
...prevFilters,
[filterKey]: activeFiltersNormalized,
}
setQueryString(createURLQueryString(newActiveFilters))
if (search !== undefined) setSearchQuery(search)
return newActiveFilters
})
},
[]
)
const onSelectTheme = useCallback((theme: string, searchPattern = "") => {
setActiveFilters((prevFilters) => {
const themes = prevFilters?.themes?.includes(theme) ? [] : [theme]
const newActiveFilters = {
...prevFilters,
themes,
}
setSearchQuery(searchPattern)
return newActiveFilters
})
}, [])
const onFilterProject = useCallback((searchPattern: string) => {
setSearchQuery(searchPattern)
}, [])
const setFilterFromQueryString = useCallback(
(filters: Partial<FiltersProps>) => {
setActiveFilters(filters)
setQueryString(createURLQueryString(filters))
},
[]
)
const sortProjectBy = useCallback((newSortBy: ProjectSortBy) => {
setSortBy(newSortBy)
}, [])
const contextValue = useMemo(
() => ({
projects,
researchs,
isLoading,
isError,
error: error as Error | null,
filters,
activeFilters,
queryString,
searchQuery,
sortBy,
getProjectById,
toggleFilter,
setFilterFromQueryString,
onFilterProject,
onSelectTheme,
sortProjectBy,
refetch,
}),
[
projects,
researchs,
isLoading,
isError,
error,
filters,
activeFilters,
queryString,
searchQuery,
sortBy,
getProjectById,
toggleFilter,
setFilterFromQueryString,
onFilterProject,
onSelectTheme,
sortProjectBy,
refetch,
]
)
return (
<ProjectsContext.Provider value={contextValue}>
{children}
</ProjectsContext.Provider>
)
}
export function useProjects() {
const context = useContext(ProjectsContext)
if (context === undefined) {
throw new Error("useProjects must be used within a ProjectsProvider")
}
return context
}

View File

@@ -2,7 +2,7 @@
import { Icons } from "../icons"
import { Button } from "../ui/button"
import { Article } from "@/lib/blog"
import { Article } from "@/lib/content"
import { cn } from "@/lib/utils"
import Link from "next/link"
import { LABELS } from "@/app/labels"

View File

@@ -1,7 +1,7 @@
"use client"
import Link from "next/link"
import { Markdown } from "../ui/markdown"
import { Article } from "@/lib/blog"
import { Article } from "@/lib/content"
import { getBackgroundImage } from "@/lib/utils"
export const ArticleListCard = ({

View File

@@ -1,7 +1,7 @@
"use client"
import { useQuery } from "@tanstack/react-query"
import { Article } from "@/lib/blog"
import { Article } from "@/lib/content"
import { ArticleListCard } from "./article-list-card"
import { cn, getBackgroundImage } from "@/lib/utils"
import Link from "next/link"
@@ -161,7 +161,7 @@ interface ArticlesListProps {
tag?: string
}
const ArticlesList: React.FC<ArticlesListProps> = ({
export const ArticlesList: React.FC<ArticlesListProps> = ({
tag,
}: ArticlesListProps) => {
const {
@@ -186,21 +186,23 @@ const ArticlesList: React.FC<ArticlesListProps> = ({
}
const lastArticle = articles[0]
const featuredArticles = !tag ? articles.slice(1, 5) : []
const otherArticles = !tag ? articles.slice(5) : articles
const featuredArticles = !tag ? articles.slice(1, 3) : []
const otherArticles = !tag ? articles.slice(3) : articles
const hasTag = tag !== undefined
return (
<div className="flex flex-col gap-10 lg:gap-16">
{!hasTag && (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4 lg:gap-6 items-stretch">
<ArticleInEvidenceCard
article={lastArticle}
size="sm"
className="h-full"
asLink
/>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 lg:gap-6 items-stretch">
<div className="lg:col-span-2 h-full">
<ArticleInEvidenceCard
article={lastArticle}
size="sm"
className="h-full "
asLink
/>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 lg:gap-6 lg:col-span-2 h-full">
{featuredArticles?.map((article: Article) => {
return (
@@ -225,5 +227,3 @@ const ArticlesList: React.FC<ArticlesListProps> = ({
</div>
)
}
export default ArticlesList

View File

@@ -1,4 +1,4 @@
import { Article } from "@/lib/blog"
import { Article } from "@/lib/content"
import { cn } from "@/lib/utils"
import { cva } from "class-variance-authority"
import Image from "next/image"

View File

@@ -1,6 +1,7 @@
"use client"
import { useProjectFiltersState } from "@/state/useProjectFiltersState"
import { useMemo } from "react"
import { useProjects } from "@/app/providers/ProjectsProvider"
import ProjectCard from "../project/project-card"
interface BlogArticleRelatedProjectsProps {
@@ -10,12 +11,13 @@ interface BlogArticleRelatedProjectsProps {
export const BlogArticleRelatedProjects = ({
projectsIds,
}: BlogArticleRelatedProjectsProps) => {
const { projects: allProjects, researchs: allResearchs } =
useProjectFiltersState((state) => state)
const { projects: allProjects, researchs: allResearchs } = useProjects()
const projects = [...allProjects, ...allResearchs].filter((project) =>
projectsIds.includes(project.id)
)
const projects = useMemo(() => {
return [...allProjects, ...allResearchs].filter((project) =>
projectsIds.includes(project.id)
)
}, [allProjects, allResearchs, projectsIds])
if (projects.length === 0) return null

View File

@@ -1,4 +1,4 @@
import { Article, getArticles } from "@/lib/blog"
import { Article, getArticles } from "@/lib/content"
import Link from "next/link"
import { BlogArticleCard } from "./blog-article-card"

View File

@@ -1,4 +1,4 @@
import { Article, getArticles } from "@/lib/blog"
import { Article, getArticles } from "@/lib/content"
import Link from "next/link"
import { AppContent } from "../ui/app-content"
import { Markdown } from "../ui/markdown"

View File

@@ -1,6 +1,6 @@
import { LABELS } from "@/app/labels"
import { AppContent } from "../ui/app-content"
import { getArticles, Article } from "@/lib/blog"
import { getArticles, Article } from "@/lib/content"
import Link from "next/link"
import { cn } from "@/lib/utils"
import { Button } from "../ui/button"

View File

@@ -2,7 +2,7 @@
import { ProjectInterface } from "@/lib/types"
import { AppContent } from "../ui/app-content"
import { Article } from "@/lib/blog"
import { Article } from "@/lib/content"
import { ArticleListCard } from "./article-list-card"
import { useGetProjectRelatedArticles } from "@/hooks/useGetProjectRelatedArticles"

View File

@@ -0,0 +1,18 @@
"use client"
import { GlobalProvider } from "@/app/providers/GlobalProvider"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
const queryClient = new QueryClient()
export const GlobalProviderLayout = ({
children,
}: {
children: React.ReactNode
}) => {
return (
<QueryClientProvider client={queryClient}>
<GlobalProvider>{children}</GlobalProvider>
</QueryClientProvider>
)
}

View File

@@ -1,15 +0,0 @@
"use client"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
const queryClient = new QueryClient()
export const QueryClientProviderLayout = ({
children,
}: {
children: React.ReactNode
}) => {
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)
}

View File

@@ -1,8 +1,8 @@
"use client"
import { useMemo } from "react"
import Link from "next/link"
import { projects } from "@/data/projects"
import { filterProjects } from "@/state/useProjectFiltersState"
import { useProjects } from "@/app/providers/ProjectsProvider"
import { ProjectInterface } from "@/lib/types"
import { shuffleArray } from "@/lib/utils"
@@ -13,25 +13,28 @@ import ProjectCard from "./project-card"
import { ProjectProps } from "@/app/(pages)/projects/[id]/page"
export default function DiscoverMoreProjects({ project }: ProjectProps) {
const getSuggestedProjects = () => {
const projectList = projects.filter((p) => p.id !== project.id)
const { projects: allProjects, onFilterProject } = useProjects()
const suggestedProject = filterProjects({
searchPattern: "",
activeFilters: project?.tags,
findAnyMatch: true,
projects: projectList,
const suggestedProject = useMemo(() => {
const projectList = allProjects.filter(
(p: ProjectInterface) => p.id !== project.id
)
// Filter projects by tags
onFilterProject("")
const suggestedProjects = projectList.filter((p) => {
const projectThemes = project.tags?.themes ?? []
const pThemes = p.tags?.themes ?? []
return projectThemes.some((tag) => pThemes.includes(tag))
})
// No match return random projects
if (suggestedProject?.length < 2) {
if (suggestedProjects?.length < 2) {
return shuffleArray(projectList).slice(0, 2)
}
return suggestedProject.slice(0, 2)
}
const suggestedProject = getSuggestedProjects()
return suggestedProjects.slice(0, 2)
}, [allProjects, project.id, project.tags?.themes, onFilterProject])
return (
<div className="w-full bg-cover-gradient">
@@ -40,8 +43,8 @@ export default function DiscoverMoreProjects({ project }: ProjectProps) {
{LABELS.COMMON.DISCOVER_MORE}
</h2>
<div className="grid flex-col grid-cols-1 gap-5 md:grid-cols-2 md:flex-row">
{suggestedProject?.map((project: ProjectInterface, index: number) => (
<ProjectCard key={index} border project={project} />
{suggestedProject?.map((project: ProjectInterface) => (
<ProjectCard key={project.id} border project={project} />
))}
</div>
<Link

View File

@@ -3,7 +3,6 @@ import Image from "next/image"
import { useRouter } from "next/navigation"
import { VariantProps, cva } from "class-variance-authority"
import { getProjectById } from "@/lib/projectsUtils"
import {
ProjectInterface,
ProjectLinkWebsite,
@@ -64,11 +63,9 @@ export default function ProjectCard({
}: ProjectCardProps) {
const router = useRouter()
const { id, image, links, name, imageAlt, projectStatus, cardTags } =
const { id, image, links, name, imageAlt, projectStatus, cardTags, tldr } =
project ?? {}
const { content: projectContent } = getProjectById(id)
return (
<div
className={cn(
@@ -104,11 +101,9 @@ export default function ProjectCard({
{name}
</h1>
</Link>
{projectContent?.tldr && (
{(tldr ?? "")?.length > 0 && (
<div className="flex flex-col h-24 gap-4">
<p className="text-slate-900/80 line-clamp-4">
{projectContent?.tldr}
</p>
<p className="text-slate-900/80 line-clamp-4">{tldr}</p>
</div>
)}
</div>

View File

@@ -1,11 +1,11 @@
"use client"
import { HtmlHTMLAttributes } from "react"
import { HtmlHTMLAttributes, useMemo } from "react"
import Link from "next/link"
import {
FilterLabelMapping,
ProjectFilter,
} from "@/state/useProjectFiltersState"
} from "@/app/providers/ProjectsProvider"
import { ProjectInterface } from "@/lib/types"
import { LABELS } from "@/app/labels"
@@ -16,6 +16,12 @@ interface TagsProps extends HtmlHTMLAttributes<HTMLDivElement> {
label: string
}
interface TagGroup {
key: string
label: string
tags: string[]
}
const TagsWrapper = ({ label, children }: TagsProps) => {
return (
<div className="flex flex-col items-start gap-2">
@@ -30,42 +36,53 @@ type IProjectTags = {
}
export function ProjectTags({ project }: IProjectTags) {
const FilterKeyMapping: Record<ProjectFilter, string> = {
keywords: LABELS.COMMON.FILTER_LABELS.KEYWORDS,
builtWith: LABELS.COMMON.FILTER_LABELS.BUILT_WITH,
themes: LABELS.COMMON.FILTER_LABELS.THEMES,
fundingSource: LABELS.COMMON.FILTER_LABELS.FUNDING_SOURCE,
}
const FilterKeyMapping = useMemo(
() => ({
keywords: LABELS.COMMON.FILTER_LABELS.KEYWORDS,
builtWith: LABELS.COMMON.FILTER_LABELS.BUILT_WITH,
themes: LABELS.COMMON.FILTER_LABELS.THEMES,
fundingSource: LABELS.COMMON.FILTER_LABELS.FUNDING_SOURCE,
}),
[]
)
const filteredTags = useMemo(
() =>
Object.entries(FilterLabelMapping)
.filter(([key]) => !["themes", "builtWith"].includes(key))
.map(([key]) => {
const keyTags = project?.tags?.[key as ProjectFilter]
const hasItems = keyTags && keyTags?.length > 0
if (!hasItems) return null
return {
key,
label: FilterKeyMapping[key as ProjectFilter] || key,
tags: keyTags,
}
})
.filter((item): item is TagGroup => item !== null),
[project?.tags, FilterKeyMapping]
)
return (
<div className="flex flex-col gap-4 pt-10">
{Object.entries(FilterLabelMapping).map(([key]) => {
const keyTags = project?.tags?.[key as ProjectFilter]
const hasItems = keyTags && keyTags?.length > 0
if (["themes", "builtWith"].includes(key)) return null // keys to ignore
return (
hasItems && (
<div data-section-id={key} key={key}>
<TagsWrapper
label={FilterKeyMapping?.[key as ProjectFilter] || key}
>
<div className="flex flex-wrap gap-[6px]">
{keyTags?.map((tag, index) => {
return (
<Link key={index} href={`/projects?${key}=${tag}`}>
<CategoryTag key={tag} variant="gray">
{tag}
</CategoryTag>
</Link>
)
})}
</div>
</TagsWrapper>
{filteredTags.map((tagGroup) => (
<div data-section-id={tagGroup.key} key={tagGroup.key}>
<TagsWrapper label={tagGroup.label}>
<div className="flex flex-wrap gap-[6px]">
{tagGroup.tags?.map((tag, index) => (
<Link key={index} href={`/projects?${tagGroup.key}=${tag}`}>
<CategoryTag key={tag} variant="gray">
{tag}
</CategoryTag>
</Link>
))}
</div>
)
)
})}
</TagsWrapper>
</div>
))}
</div>
)
}

View File

@@ -3,16 +3,8 @@
import React, { ChangeEvent, ReactNode, useEffect, useState } from "react"
import Image from "next/image"
import { useRouter, useSearchParams } from "next/navigation"
import { projects } from "@/data/projects"
import FiltersIcon from "@/public/icons/filters.svg"
import {
FilterLabelMapping,
FilterTypeMapping,
ProjectFilter,
useProjectFiltersState,
} from "@/state/useProjectFiltersState"
import { useDebounce } from "react-use"
import { IThemeStatus, IThemesButton } from "@/types/common"
import {
ProjectCategories,
@@ -24,7 +16,11 @@ import {
} 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"
@@ -33,6 +29,14 @@ import { Checkbox } from "../ui/checkbox"
import { Input } from "../ui/input"
import { Modal } from "../ui/modal"
// Define the mapping for filter types
const FilterTypeMapping: Record<ProjectFilter, "checkbox" | "button"> = {
keywords: "checkbox",
builtWith: "checkbox",
themes: "button",
fundingSource: "checkbox",
}
interface FilterWrapperProps {
label: string
children?: ReactNode
@@ -42,7 +46,9 @@ interface FilterWrapperProps {
const FilterWrapper = ({ label, children, className }: FilterWrapperProps) => {
return (
<div className={cn("flex flex-col gap-4 py-6", className)}>
<span className="text-xl font-bold">{label}</span>
<span className="text-sm font-medium text-tuatara-950 md:text-base">
{label}
</span>
{children}
</div>
)
@@ -81,8 +87,14 @@ export default function ProjectFiltersBar() {
const [searchQuery, setSearchQuery] = useState("")
const [filterCount, setFilterCount] = useState(0)
const { filters, toggleFilter, queryString, activeFilters, onFilterProject } =
useProjectFiltersState((state) => state)
const {
filters,
toggleFilter,
queryString,
activeFilters,
onFilterProject,
setFilterFromQueryString,
} = useProjects()
useEffect(() => {
if (!queryString) return
@@ -91,10 +103,8 @@ export default function ProjectFiltersBar() {
useEffect(() => {
// set active filters from url
useProjectFiltersState.setState({
activeFilters: queryStringToObject(searchParams),
})
}, [searchParams])
setFilterFromQueryString(queryStringToObject(searchParams))
}, [searchParams, setFilterFromQueryString])
useEffect(() => {
const count = Object.values(activeFilters).reduce((acc, curr) => {
@@ -104,11 +114,7 @@ export default function ProjectFiltersBar() {
}, [activeFilters])
const clearAllFilters = () => {
useProjectFiltersState.setState({
activeFilters: {},
queryString: "",
projects,
})
setFilterFromQueryString({})
setSearchQuery("") // clear input
router.push("/projects")
}
@@ -255,9 +261,7 @@ export default function ProjectFiltersBar() {
<Input
onChange={(e: ChangeEvent<HTMLInputElement>) => {
setSearchQuery(e?.target?.value)
useProjectFiltersState.setState({
searchQuery: e?.target?.value,
})
onFilterProject(e?.target?.value)
}}
value={searchQuery}
placeholder={LABELS.COMMON.SEARCH_PROJECT_PLACEHOLDER}

View File

@@ -3,7 +3,6 @@
import React, { useCallback, useEffect, useRef, useState } from "react"
import Image from "next/image"
import NoResultIcon from "@/public/icons/no-result.svg"
import { useProjectFiltersState } from "@/state/useProjectFiltersState"
import { cva } from "class-variance-authority"
import {
@@ -18,6 +17,7 @@ import { cn } from "@/lib/utils"
import { LABELS } from "@/app/labels"
import ProjectCard from "./project-card"
import { useProjects } from "@/app/providers/ProjectsProvider"
const sectionTitleClass = cva(
"relative font-sans text-base font-bold uppercase tracking-[3.36px] text-anakiwa-950 after:ml-8 after:absolute after:top-1/2 after:h-[1px] after:w-full after:translate-y-1/2 after:bg-anakiwa-300 after:content-['']"
@@ -47,9 +47,14 @@ export const ProjectList = () => {
const [isManualScroll, setIsManualScroll] = useState(false)
const [isMounted, setIsMounted] = useState(false)
const { projects, searchQuery, queryString } = useProjectFiltersState(
(state) => state
)
const { projects, searchQuery, activeFilters } = useProjects()
const hasSearchParams =
searchQuery?.length > 0 ||
Object.values({
keywords: activeFilters?.keywords ?? [],
builtWith: activeFilters?.builtWith ?? [],
themes: activeFilters?.themes ?? [],
}).some((arr) => arr.length > 0)
const noItems = projects?.length === 0
@@ -94,8 +99,6 @@ export const ProjectList = () => {
}
}, [])
const hasActiveFilters = searchQuery !== "" || queryString !== ""
// loading state skeleton
if (!isMounted) {
return (
@@ -129,11 +132,10 @@ export const ProjectList = () => {
{} as Record<ProjectStatus, ProjectInterface[]>
)
// show all projects without sections if there are active filters
if (hasActiveFilters) {
if (hasSearchParams) {
return (
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 md:gap-x-6 md:gap-y-10 lg:grid-cols-4">
{projects.map((project) => (
{projects?.map((project: any) => (
<ProjectCard
key={project?.id}
project={project}
@@ -165,7 +167,7 @@ export const ProjectList = () => {
className="flex justify-between gap-10"
>
<div className={cn("flex w-full flex-col gap-10 pt-10")}>
{!hasActiveFilters && (
{!hasSearchParams && (
<div className="flex flex-col gap-6 overflow-hidden">
<h3 className={cn(sectionTitleClass())}>{status}</h3>
<span className="font-sans text-base italic text-tuatara-950">
@@ -174,7 +176,7 @@ export const ProjectList = () => {
</div>
)}
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 md:gap-x-6 md:gap-y-10 lg:grid-cols-4">
{projects.map((project) => (
{projects.map((project: any) => (
<ProjectCard
key={project?.id}
project={project}

View File

@@ -1,11 +1,10 @@
"use client"
import {
DEFAULT_PROJECT_SORT_BY,
ProjectFilter,
ProjectSortBy,
useProjectFiltersState,
} from "@/state/useProjectFiltersState"
useProjects,
} from "@/app/providers/ProjectsProvider"
import { LABELS } from "@/app/labels"
import { interpolate } from "@/lib/utils"
@@ -17,7 +16,7 @@ const labelClass = "h-5 text-xs text-base md:h-6 text-slate-900/70 md:text-sm"
export const ProjectResultBar = () => {
const { activeFilters, toggleFilter, projects, sortProjectBy, sortBy } =
useProjectFiltersState((state) => state)
useProjects()
const haveActiveFilters = Object.entries(activeFilters).some(
([, values]) => values?.length > 0
@@ -61,7 +60,7 @@ export const ProjectResultBar = () => {
<span className={labelClass}>{resultLabel}</span>
<Dropdown
label={activeSortOption}
defaultItem={DEFAULT_PROJECT_SORT_BY}
defaultItem="asc"
items={projectSortItems}
onChange={(sortBy) => sortProjectBy(sortBy as ProjectSortBy)}
disabled={!projects?.length}
@@ -69,31 +68,29 @@ export const ProjectResultBar = () => {
</div>
{haveActiveFilters && (
<div className="inline-flex flex-wrap gap-1 md:gap-4">
{Object.entries(activeFilters).map(([key, filters], index) => {
return (
<>
{filters?.map((filter) => {
if (filter?.length === 0) return null
return (
<CategoryTag
closable
variant="gray"
onClose={() =>
toggleFilter({
tag: key as ProjectFilter,
value: filter,
})
}
key={`${index}-${filter}`}
>
{filter}
</CategoryTag>
)
})}
</>
{Object.entries(activeFilters)
.flatMap(([key, filters]) =>
(filters ?? []).map((filter) => ({
key,
filter,
}))
)
})}
.filter(({ filter }) => filter?.length > 0)
.map(({ key, filter }, index) => (
<CategoryTag
key={`${key}-${filter}-${index}`}
closable
variant="gray"
onClose={() =>
toggleFilter({
tag: key as ProjectFilter,
value: filter,
})
}
>
{filter}
</CategoryTag>
))}
</div>
)}
</div>

View File

@@ -3,7 +3,7 @@ import Image from "next/image"
import { useRouter } from "next/navigation"
import { VariantProps, cva } from "class-variance-authority"
import { getProjectById } from "@/lib/projectsUtils"
import { useProjects } from "@/app/providers/ProjectsProvider"
import {
ProjectInterface,
ProjectLinkWebsite,
@@ -70,10 +70,16 @@ export default function ProjectCard({
}: ProjectCardProps) {
const router = useRouter()
const { id, image, links, name, imageAlt, projectStatus, cardTags } =
project ?? {}
const { content: projectContent } = getProjectById(id)
const {
id,
image,
links,
name,
imageAlt,
projectStatus,
cardTags,
tldr = "",
} = project ?? {}
return (
<Link
@@ -118,11 +124,9 @@ export default function ProjectCard({
<h1 className="text-2xl font-bold leading-7 duration-200 cursor-pointer text-anakiwa-700 line-clamp-2">
{name}
</h1>
{projectContent?.tldr && (
{(tldr ?? "")?.length > 0 && (
<div className="flex flex-col h-24 gap-4">
<p className="text-slate-900/80 line-clamp-3">
{projectContent?.tldr}
</p>
<p className="text-slate-900/80 line-clamp-3">{tldr}</p>
</div>
)}
</div>

View File

@@ -1,17 +1,18 @@
"use client"
import React, { useEffect, useState } from "react"
import React, { useEffect, useState, useMemo } from "react"
import Image from "next/image"
import NoResultIcon from "@/public/icons/no-result.svg"
import { useProjectFiltersState } from "@/state/useProjectFiltersState"
import { cva } from "class-variance-authority"
import { ProjectStatus } from "@/lib/types"
import { ProjectInterface, ProjectStatus } from "@/lib/types"
import { cn } from "@/lib/utils"
import { LABELS } from "@/app/labels"
import { useProjects } from "@/app/providers/ProjectsProvider"
import ResearchCard from "./research-card"
import Link from "next/link"
const sectionTitleClass = cva(
"relative font-sans text-base font-bold uppercase tracking-[3.36px] text-anakiwa-950 after:ml-8 after:absolute after:top-1/2 after:h-[1px] after:w-full after:translate-y-1/2 after:bg-anakiwa-300 after:content-['']"
)
@@ -37,17 +38,28 @@ const ProjectStatusOrderList = ["active", "maintained", "inactive"]
export const ResearchList = () => {
const [isMounted, setIsMounted] = useState(false)
const { researchs, searchQuery, queryString } = useProjectFiltersState(
(state) => state
)
const { researchs, searchQuery, queryString } = useProjects()
const noItems = researchs?.length === 0
const hasActiveFilters = searchQuery !== "" || queryString !== ""
useEffect(() => {
setIsMounted(true)
}, [])
const hasActiveFilters = searchQuery !== "" || queryString !== ""
const { activeResearchs, pastResearchs } = useMemo(() => {
const active = researchs.filter(
(research: ProjectInterface) =>
research.projectStatus === ProjectStatus.ACTIVE
)
const past = researchs.filter(
(research: ProjectInterface) =>
research.projectStatus !== ProjectStatus.ACTIVE
)
return { activeResearchs: active, pastResearchs: past }
}, [researchs])
if (!isMounted) {
return (
@@ -74,14 +86,6 @@ export const ResearchList = () => {
if (noItems) return <NoResults />
const activeResearchs = researchs.filter(
(research) => research.projectStatus === ProjectStatus.ACTIVE
)
const pastResearchs = researchs.filter(
(research) => research.projectStatus !== ProjectStatus.ACTIVE
)
return (
<div className="relative grid items-start justify-between grid-cols-1">
<div
@@ -97,20 +101,18 @@ export const ResearchList = () => {
</div>
)}
<div className="grid grid-cols-1 gap-4 md:gap-x-6 md:gap-y-10 lg:grid-cols-3">
{activeResearchs.map((project) => {
return (
<ResearchCard
key={project?.id}
project={project}
className="h-[180px]"
showBanner={false}
showLinks={false}
showCardTags={false}
showStatus={false}
border
/>
)
})}
{activeResearchs.map((project: ProjectInterface) => (
<ResearchCard
key={project?.id}
project={project}
className="h-[180px]"
showBanner={false}
showLinks={false}
showCardTags={false}
showStatus={false}
border
/>
))}
</div>
</div>
<div className={cn("flex w-full flex-col gap-10 pt-10")}>
@@ -120,17 +122,15 @@ export const ResearchList = () => {
</h3>
</div>
<div className="flex flex-col gap-5">
{pastResearchs.map((project) => {
return (
<Link
href={`/projects/${project?.id}`}
key={project?.id}
className="text-neutral-950 border-b-[2px] border-b-anakiwa-500 text-sm font-medium w-fit hover:text-anakiwa-500 duration-200"
>
{project.name}
</Link>
)
})}
{pastResearchs.map((project: ProjectInterface) => (
<Link
href={`/projects/${project?.id}`}
key={project?.id}
className="text-neutral-950 border-b-[2px] border-b-anakiwa-500 text-sm font-medium w-fit hover:text-anakiwa-500 duration-200"
>
{project.name}
</Link>
))}
</div>
</div>
</div>

View File

@@ -4,7 +4,7 @@ This document explains how to add new articles to into pse.dev blog section.
## Step 1: Create the Article File
1. Duplicate the `_article-template.md` file in the `articles` directory
1. Duplicate the `_article-template.md` file in the `content/articles` directory
2. Rename it to match your article's title using kebab-case (e.g., `my-new-article.md`)
## Step 2: Fill in the Article Information
@@ -20,7 +20,7 @@ tldr: "A brief summary of your article" #Short summary
date: "YYYY-MM-DD" # Publication date in ISO format
canonical: "mirror.xyz/my-new-article" # (Optional) The original source URL, this tells search engines the primary version of the content
tags: ["tag1", "tag2"] # (Optional) Add relevant tags as an array of strings to categorize the article
projects: ["project-1"]
projects: ["project-1"] # (Optional) Link to related projects by their id
---
```
@@ -63,7 +63,7 @@ Before submitting, make sure to:
Open Pull request following the previews step and for any help
- Suggest to tag: @kalidiagne, @psedesign, @AtHeartEngineer for PR review.
- Suggest to tag: @kalidiagne, @psedesign for PR review.
- If question, please reach out in discord channel #website-pse
## Important Notes

View File

@@ -7,4 +7,4 @@ date: "YYYY-MM-DD" # Publication date in ISO format
canonical: "mirror.xyz/my-new-article" # (Optional) The original source URL, this tells search engines the primary version of the content
tags: ["tag1", "tag2"] # (Optional) Add relevant tags as an array of strings to categorize the article
projects: ["project-1"]
---
---

View File

@@ -1,5 +1,5 @@
---
authors: ["Kevin Chia","Jern Kun","Kazumune Masaki"] # Add your name or multiple authors in an array
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
tldr: "A retrospective on the MPCStats project—exploring our goals, technical challenges, demo at Devcon 2024, and the decision to sunset the platform."
@@ -20,13 +20,12 @@ We got valuable feedback after the demo, and we recognized that identifying impa
![mpcstats-demo](/articles/mpcstats-retrospective/mpcstats-demo.png)
### What was achieved
- Successfully delivered an [interactive demo at Devcon 2024](https://www.youtube.com/watch?v=wCp7Zsjou7w), where participants submitted their ETH balance from Binance and queried aggregated statistics like the Gini coefficient, mean, median, and max.
- Computations were performed using MPC, with data authenticity verified by [TLSNotary](https://tlsnotary.org/) and execution delegated to three servers for smoother UX.
- The system completed most computations within 1 minute and was secure against up to 1 out of 3 malicious parties. Full details are available in our [demo report](https://www.notion.so/3055bb69afd24d60bf8ee8d4fa5f774c?pvs=21).
- The same demo was also presented at **Taipei Blockchain Week**, with a total of 19 participants across both events.
- Computations were performed using MPC, with data authenticity verified by [TLSNotary](https://tlsnotary.org/) and execution delegated to three servers for smoother UX.
- The system completed most computations within 1 minute and was secure against up to 1 out of 3 malicious parties. Full details are available in our [demo report](https://www.notion.so/3055bb69afd24d60bf8ee8d4fa5f774c?pvs=21).
- The same demo was also presented at **Taipei Blockchain Week**, with a total of 19 participants across both events.
- Developed a reusable [infrastructure for MPC demos](https://github.com/MPCStats/mpc-demo-infra), allowing other teams to adapt for different data sources and statistical functions, using the statistics library we built with [MP-SPDZ](https://github.com/data61/MP-SPDZ).
- Built an earlier [ZK-based version of the statistics library](https://github.com/MPCStats/zk-stats-lib) using [EZKL](https://github.com/zkonduit/ezkl), enabling single-party computations and helping explore privacy-preserving techniques beyond MPC.
@@ -64,4 +63,4 @@ And write-ups:
We hope these components can serve as a reference or starting point for teams exploring privacy-preserving statistics, MPC applications, and input authentication using TLSNotary.
No active maintenance will be guaranteed going forward. However, the code and write-ups will remain publicly accessible for anyone who finds them useful.
No active maintenance will be guaranteed going forward. However, the code and write-ups will remain publicly accessible for anyone who finds them useful.

View File

@@ -1,13 +1,11 @@
---
title: 'Post-Quantum Signature Aggregation with Falcon + LaBRADOR'
title: "Post-Quantum Signature Aggregation with Falcon + LaBRADOR"
date: 2025-05-19
author: PSE Research
image: "/articles/post-quantum-signature-aggregation-with-falcon-and-LaBRADOR/cover.webp"
tagline: 'Compact lattice-based proofs for Ethereum'
description: 'Introducing our work on aggregating Falcon signatures using LaBRADOR, a lattice-based proof system optimized for Ethereum.'
canonical: 'https://ethresear.ch/t/lattice-based-signature-aggregation/22282'
tagline: "Compact lattice-based proofs for Ethereum"
description: "Introducing our work on aggregating Falcon signatures using LaBRADOR, a lattice-based proof system optimized for Ethereum."
canonical: "https://ethresear.ch/t/lattice-based-signature-aggregation/22282"
---
We demonstrate efficient aggregation of Falcon signatures using LaBRADOR, achieving compact, quantum-resistant proofs. Read the full technical breakdown on [ethresear.ch](https://ethresear.ch/t/lattice-based-signature-aggregation/22282).
@@ -18,13 +16,12 @@ Ethereum currently uses BLS signatures, but these are vulnerable to future quant
Key highlights:
* **Compact proofs:** Aggregate 10,000 Falcon-512 signatures into a 74 KB proof.
* **Efficient generation:** Proofs generated in \~6 seconds on commodity hardware.
* **Verification optimization:** Current verification time (\~2.6 seconds) identified as a primary bottleneck, with active work to reduce it further.
- **Compact proofs:** Aggregate 10,000 Falcon-512 signatures into a 74 KB proof.
- **Efficient generation:** Proofs generated in \~6 seconds on commodity hardware.
- **Verification optimization:** Current verification time (\~2.6 seconds) identified as a primary bottleneck, with active work to reduce it further.
This work is part of our broader post-quantum initiative at PSE, focusing on securing Ethereum's core infrastructure.
---
🔗 **Full technical details and discussions:** [Lattice-based signature aggregation on ethresear.ch](https://ethresear.ch/t/lattice-based-signature-aggregation/22282)

View File

@@ -8,23 +8,27 @@ projects: ["private-proof-delegation"]
---
## tl;dr
We built a TEE-based system for secure zero-knowledge proof delegation using Intel TDX. It allows clients to privately outsource large proving tasks without leaking inputs. Unlike mobile-native proving, which is constrained by hardware limits, TEE-based proving can scale to larger statements today — and continue to scale as proof systems improve. As a hardware-backed solution, TEE remains compatible with future advancements in software (e.g., faster proof systems, better engineering) and won't be invalidated by them, as long as the trust model is acceptable.
➡️ [Jump to Benchmarking section](#Benchmarking) to see how it performs in practice.
# Introduction
## Background and motivation
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)
*How naive proof delegation works*
_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.
## Private proof delegation (PPD)
**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*
_Private proof delegation_
The cryptographic techniques that we can possibly use include
@@ -35,6 +39,7 @@ The cryptographic techniques that we can possibly use include
In this article, we focus on the TEE-based approach, which we believe is practical and well-supported in today's cloud environments.
# TEE Primer: What and Why
## What is Trusted Execution Environment (TEE)?
A **Trusted Execution Environment (TEE)** is a **secure and isolated processing environment** that runs alongside the main operating system on a machine. It provides:
@@ -48,7 +53,9 @@ TEEs are critically important for scenarios where sensitive data or computations
Different TEE implementations exist today, such as [Intel SGX](https://www.intel.com/content/www/us/en/products/docs/accelerator-engines/software-guard-extensions.html), [ARM TrustZone](https://www.arm.com/technologies/trustzone-for-cortex-a), [AMD SEV](https://www.amd.com/en/developer/sev.html), and [Intel TDX](https://www.intel.com/content/www/us/en/developer/tools/trust-domain-extensions/overview.html). Each offers varying degrees of isolation, threat models, and usability.
## Key components
There are several key components of TEE:
- Hardware isolation
- Memory encryption
- Remote attestation
@@ -57,6 +64,7 @@ There are several key components of TEE:
We will take a closer look into each of the items.
### Hardware isolation
At the heart of any Trusted Execution Environment (TEE) is the concept of hardware-enforced isolation. Hardware isolation refers to the physical and logical mechanisms built directly into the CPU and platform architecture that create a boundary between the execution environment inside the TEE and the rest of the system (Rich Execution Environment, REE)—including the operating system, hypervisor, firmware, and other software running outside the TEE.
Rather than relying on software controls (which can be bypassed by privileged attackers), hardware isolation ensures that:
@@ -71,8 +79,8 @@ This isolation is enforced by mechanisms such as
- Dedicated secure memory regions (e.g., Enclave Page Cache in Intel SGX, Secure EPT in Intel TDX)
- CPU privilege separation between "trusted" and "untrusted" execution modes
### Memory encryption
While hardware isolation protects the logical boundaries of the Trusted Execution Environment (TEE), it does not by itself prevent physical attacks targeting the systems memory. For example, if the attacker has access to the physical hardware, it is possible to dump the memory to see the data inside. Memory encryption addresses this gap by ensuring that all data stored outside the CPU boundary — particularly in RAM — remains cryptographically protected by encryption schemes against unauthorized access, even in the face of direct hardware attacks. The data stored inside the memory is always encrypted with a key secured in the chip (only TEE can access the key). For example, in Intel TDX, AES-XTS with ephemeral 128-bit memory-encryption keys (TDX) are used for encrypting the data, and the integrity of the data inside memory is guaranteed by SHA-3-based MAC so that malicious data tampering can be detected.
This ensures that
@@ -82,6 +90,7 @@ This ensures that
- Side-channel leakage through unprotected memory reads/writes is minimized.
### Remote attestation
Even with hardware isolation and memory encryption, a crucial question remains:
**"How can a remote party trust that the TEE is running the intended code on a genuine, uncompromised platform?"**
@@ -93,7 +102,6 @@ Even with hardware isolation and memory encryption, a crucial question remains:
This proof enables remote clients to establish trust before transmitting sensitive data or initiating private computations.
- The booted TD image exactly as expected (secure boot).
- The measurements created/extended during runtime are as expected.
- The TEE is executed on a certified hardware platform.
@@ -122,6 +130,7 @@ In Intel TDX and similar TEEs, the root of trust is composed of
Importantly, the security of the entire TEE — including attestation, isolation, and encryption — ultimately depends on the integrity and authenticity of this Root of Trust.
## What is the trust assumption of TEE?
In traditional cryptography, security relies on mathematical hardness assumptions — for example, the difficulty of solving the Discrete Logarithm Problem (DLP) or the intractability of Learning With Errors (LWE). These assumptions are well-studied and allow cryptographic protocols to provide strong, quantifiable security guarantees.
In contrast, when relying on a Trusted Execution Environment (TEE), security instead depends on a different class of assumptions: trust in hardware, firmware, and system integrity.
@@ -131,20 +140,20 @@ While TEEs dramatically reduce the size of the trusted computing base compared t
In this section, we detail what specific components and entities must be trusted when building on a TEE.
### Trust in the Hardware Manufacturer
A fundamental trust anchor when using a TEE is the **hardware manufacturer**. In the case of Intel TDX, this is Intel Corporation, which designs and produces the processors and microcode that enforce TDX protections.
When trusting the hardware manufacturer, the following assumptions are made:
- **Correct Implementation**:
The manufacturer has correctly implemented the TEE architecture as specified, including hardware isolation, memory encryption, key management, and the attestation mechanism. Errors or backdoors in the hardware implementation could trivially undermine the TEEs guarantees.
The manufacturer has correctly implemented the TEE architecture as specified, including hardware isolation, memory encryption, key management, and the attestation mechanism. Errors or backdoors in the hardware implementation could trivially undermine the TEEs guarantees.
- **Secure Key Provisioning**:
Attestation keys (used to sign attestation quotes) are provisioned securely during the manufacturing process and cannot be extracted, replaced, or misused.
Attestation keys (used to sign attestation quotes) are provisioned securely during the manufacturing process and cannot be extracted, replaced, or misused.
- **Supply Chain Integrity**:
The physical chip that arrives at the cloud datacenter is authentic and has not been tampered with or replaced during production or delivery.
The physical chip that arrives at the cloud datacenter is authentic and has not been tampered with or replaced during production or delivery.
- **Prompt Security Response**:
If vulnerabilities are discovered (e.g., speculative execution side-channels), the manufacturer acts responsibly to issue patches, microcode updates, or mitigations.
If vulnerabilities are discovered (e.g., speculative execution side-channels), the manufacturer acts responsibly to issue patches, microcode updates, or mitigations.
**What if Broken**
If the hardware manufacturer is dishonest, coerced, or negligent, the entire security model of the TEE collapses. The TEE could leak secrets, forge attestation quotes, or fail to enforce isolation — without any detectable signals to the user.
@@ -152,6 +161,7 @@ If the hardware manufacturer is dishonest, coerced, or negligent, the entire sec
This trust assumption is not cryptographic in the traditional sense; it is institutional and engineering-based.
### Trust in the Attestation Service
The attestation process is what enables remote parties (such as clients) to verify the trustworthiness of a TEE before entrusting it with sensitive data. However, the correctness of attestation relies on the integrity of the attestation service infrastructure.
Typically, there are two main parties involved:
@@ -162,17 +172,14 @@ Typically, there are two main parties involved:
When trusting the attestation service, the following assumptions are made:
- **Authenticity of Device Identity**:
The attestation key bound to the physical device is genuine and was issued by the manufacturers trusted key hierarchy.
The attestation key bound to the physical device is genuine and was issued by the manufacturers trusted key hierarchy.
- **Correct Verification and Validation**:
The attestation service correctly verifies the quotes it receives, checking that:
- Measurements match an expected secure configuration.
- Firmware and security versions meet minimum patch levels.
- No security policy violations have occurred.
The attestation service correctly verifies the quotes it receives, checking that: - Measurements match an expected secure configuration. - Firmware and security versions meet minimum patch levels. - No security policy violations have occurred.
- **Impartial and Secure Operation**:
The attestation service must behave honestly, not issuing approvals for devices that are compromised, outdated, or misconfigured.
The attestation service must behave honestly, not issuing approvals for devices that are compromised, outdated, or misconfigured.
- **Resistance to Collusion or Coercion**:
The attestation service must resist external pressures (e.g., nation-state coercion) that might lead it to falsely attestation to an insecure environment.
The attestation service must resist external pressures (e.g., nation-state coercion) that might lead it to falsely attestation to an insecure environment.
**What if Broken**
If the attestation service incorrectly verifies a compromised TEE — or worse, issues fabricated attestation results — remote users would falsely believe they are communicating with a secure environment.
@@ -181,14 +188,16 @@ This would completely undermine the remote trust model that TEEs are designed to
---
# TEE-based private proof delegation
## Construction of TEE-based private proof delegation
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)
*Architecture diagram*
_Architecture diagram_
In the architecture diagram above, we can see several components, including
- Client
- TD Host
- TD Guest (Prover)
@@ -206,28 +215,33 @@ In the architecture diagram above, we can see several components, including
**Verifier** is anyone who verifies the generated proof.
## Execution flow
### Remote Attestation
We start the whole process with remote attestation. In this step, the client ensures that the prover VM is running on legitimate hardware and firmware, OS, and program.
### Remote Attestation
We start the whole process with remote attestation. In this step, the client ensures that the prover VM is running on legitimate hardware and firmware, OS, and program.
### What Happens
1. The **TEE environment** (such as a confidential VM or enclave) begins by measuring its **initial state** at launch time. These measurements include:
- The code and data loaded into the TEE at startup
- Configuration values (e.g., enabled security features, TEE attributes)
2. These measurements are securely recorded in **platform-protected registers**, such as:
- Platform Configuration Registers (PCRs) in a TPM
- Runtime Measurement Registers (RTMRs) or equivalent TEE-specific structures
3. The TEE then generates an **attestation report** — a structured message that includes:
- The recorded measurements
- TEE metadata (such as the identity of the code, runtime version, and security configuration)
- Optional application-specific data (e.g., a nonce or public key from the client)
4. This attestation report is sent to a trusted component inside the platform known as the **attestation signing agent** (e.g., a *quoting enclave*, *firmware-based attestation module*, or *secure co-processor*).
4. This attestation report is sent to a trusted component inside the platform known as the **attestation signing agent** (e.g., a _quoting enclave_, _firmware-based attestation module_, or _secure co-processor_).
5. The attestation signing agent signs the report using a **manufacturer-provisioned attestation key**. This key is
- Cryptographically tied to the device
- Certified by the TEE vendor (e.g., Intel, AMD, ARM)
- Stored and used in a protected environment inaccessible to untrusted software
@@ -235,6 +249,7 @@ We start the whole process with remote attestation. In this step, the client ens
6. The resulting **attestation quote** is returned to the TEE and passed to the **client** (or relying party) that requested attestation.
7. The client forwards the quote to a **remote attestation service**, which:
- Verifies the signature on the quote using the manufacturers root certificate
- Validates the integrity and freshness of the TEEs measurements
- Confirms that the platform is genuine, up-to-date, and securely configured
@@ -252,7 +267,7 @@ Only once this trust is established does the client proceed to send sensitive da
### Runtime Attestation with Linux IMA and Keylime
While launch-time attestation provides assurance that a Trusted Execution Environment (TEE) **starts in a known-good state**, it does not cover what happens *after* boot. A malicious actor could, for example, replace a binary or tamper with configuration files post-launch without affecting the original attestation quote.
While launch-time attestation provides assurance that a Trusted Execution Environment (TEE) **starts in a known-good state**, it does not cover what happens _after_ boot. A malicious actor could, for example, replace a binary or tamper with configuration files post-launch without affecting the original attestation quote.
To address this, we incorporate **runtime attestation** using the **[Linux Integrity Measurement Architecture (IMA)](https://www.redhat.com/en/blog/how-use-linux-kernels-integrity-measurement-architecture)** and the **[Keylime](https://keylime.dev/)** framework.
@@ -260,9 +275,10 @@ To address this, we incorporate **runtime attestation** using the **[Linux Integ
#### What Is Linux IMA?
**IMA** is a Linux kernel subsystem that measures files — such as executables, libraries, scripts, and configuration files — *at the time they are accessed*. It maintains a **measurement log**, which contains cryptographic hashes of all accessed files. These hashes are extended into a **Platform Configuration Register (PCR)** (typically **PCR10** in TPM-backed systems).
**IMA** is a Linux kernel subsystem that measures files — such as executables, libraries, scripts, and configuration files — _at the time they are accessed_. It maintains a **measurement log**, which contains cryptographic hashes of all accessed files. These hashes are extended into a **Platform Configuration Register (PCR)** (typically **PCR10** in TPM-backed systems).
This allows the system to:
- Detect if any critical file has been changed or tampered with.
- Compare runtime measurements against a reference "good" state.
- Bind application behavior to expected code and configuration.
@@ -274,6 +290,7 @@ This allows the system to:
**Keylime** is an open-source remote attestation framework designed to work with IMA. It allows a **remote verifier** (e.g., a client) to continuously monitor the integrity of a **running system**, including a TEE-based confidential VM.
Keylime components include:
- **Agent (inside the TEE guest)**: Reports IMA measurements and TPM quotes
- **Verifier (outside the TEE)**: Validates measurements against a policy
- **Registrar**: Manages key provisioning and trust setup
@@ -282,17 +299,17 @@ Keylime components include:
#### How Runtime Attestation Complements Launch-Time Attestation
| Launch-Time Attestation | Runtime Attestation (IMA + Keylime) |
|--------------------------------------------------|----------------------------------------------------|
| Validates integrity at the point of TD creation | Monitors integrity throughout execution |
| Covers bootloader, kernel, early OS state | Covers binaries, libraries, config files, scripts |
| Performed once at startup | Ongoing or on-demand validation |
| Bound to hardware measurement registers (e.g., RTMRs) | Bound to TPM PCRs (e.g., PCR10) |
| Launch-Time Attestation | Runtime Attestation (IMA + Keylime) |
| ----------------------------------------------------- | ------------------------------------------------- |
| Validates integrity at the point of TD creation | Monitors integrity throughout execution |
| Covers bootloader, kernel, early OS state | Covers binaries, libraries, config files, scripts |
| Performed once at startup | Ongoing or on-demand validation |
| Bound to hardware measurement registers (e.g., RTMRs) | Bound to TPM PCRs (e.g., PCR10) |
#### Benefits in a ZK Proving Context
In delegated proving, runtime attestation can ensure that:
- The ZK prover binary running inside the TEE has not been modified or replaced.
- The proving key and parameters are untampered.
- The system hasnt silently switched to an insecure configuration.
@@ -320,10 +337,12 @@ This provides **stronger guarantees to the client** — not only is the environm
### Trust Model Enhancement
By combining
- **Launch-time attestation** (via TEE quote + attestation service), and
- **Runtime attestation** (via IMA + Keylime + TPM),
We create a **two-layer trust model** where the client can verify:
1. The TEE starts securely and is properly configured.
2. The integrity of the prover binary and environment is continuously enforced.
@@ -354,15 +373,18 @@ Once attestation confirms that the TEE is in a trusted state, the client needs a
4. The proving process begins using the securely provisioned input.
## Proof generation and verification
Once all the data needed to compute the zk proof, the TD guest can now compute the proof. This part is pretty much the same as normal computation because we can run exactly the same binaries as in the normal server.
When proof is computed, TD guest sends the proof back to the client or any third party who wants to verify the proof.
# Benchmarking
We took benchmarks of the system architecture described in the previous section. The benchmark targets are **[semaphore](https://semaphore.pse.dev/)** and [proof-of-twitter](https://github.com/zkemail/proof-of-twitter/tree/main) from **[zkemail](https://prove.email/)**. Both are important client applications that utilize zk proof for privacy. The results help assess whether modern TEEs offer a viable foundation for private proof delegation in practice.
The first thing we did was to take benchmarks of the semaphore circuit using [this repo](https://github.com/vplasencia/semaphore-benchmarks).
For the semaphore we ran three benchmarking settings.
1. Benchmark for proof generation inside TEE VM
2. Benchmark for proof generation inside normal VM
3. Benchmark for E2E TEE-based private proof delegation setting
@@ -372,14 +394,15 @@ For 1 and 3, we use the Azure VM Standard_EC16eds_v5 series; for 2, we use the A
For zkemail, we used proof-of-twitter circuit to measure the number. For this, we only took E2E benchmarks to see if the whole system can provide an acceptable number.
## Semaphore benchmarks
1. Benchmark for proof generation inside TEE VM
![Benchmark1](/articles/tee-based-ppd/benchmark1.png)
2. Proof generation inside normal VM
![Benchmark2](/articles/tee-based-ppd/benchmark2.png)
1. Benchmark for proof generation inside TEE VM
![Benchmark1](/articles/tee-based-ppd/benchmark1.png)
2. Proof generation inside normal VM
![Benchmark2](/articles/tee-based-ppd/benchmark2.png)
E2E benchmarks
In our scenario, we measure the E2E performance, including
- Keylime runtime attestation
- TDX attestation
- Proof generation
@@ -393,6 +416,7 @@ In our scenario, we measure the E2E performance, including
| Total | 3166ms | 2803ms | 2809ms | 2926ms |
## zk-email benchmarks
We use the proof-of-twitter circuit as our benchmark target. This demonstrates a real-world use case for TEE-based private proof delegation. The benchmark was taken for the E2E setting, as in the semaphore benchmark.
| | 1 | 2 | 3 | Avg. |
@@ -402,19 +426,20 @@ We use the proof-of-twitter circuit as our benchmark target. This demonstrates a
| Proving | 87213ms | 57185ms | 56976ms | 67124ms |
| Total | 89224ms | 59216ms | 59462ms | 69300ms |
## Discussions
See Table 2—the ~67s proving time dominates E2E latency, so in this case, TEE overhead becomes <5 %. Our benchmarks show that TEE-based delegated proving is both **feasible and practically performant** for real-world zero-knowledge applications — with varying trade-offs depending on the circuit size and security assumptions.
See Table 2—the ~67s proving time dominates E2E latency, so in this case, TEE overhead becomes <5 %. Our benchmarks show that TEE-based delegated proving is both **feasible and practically performant** for real-world zero-knowledge applications — with varying trade-offs depending on the circuit size and security assumptions.
### Attestation Overhead Is Modest
Both **Keylime runtime attestation** and **TDX launch attestation** contribute **1.74 s total (696 ms + 1 048 ms)** in both cases. In the Semaphore benchmark, their combined overhead accounts for less than 25% of the total latency. This indicates that
- **Launch-time and runtime attestation are fast enough** to be included in every proving session.
- They can be embedded into interactive or on-demand proving scenarios without causing significant delay.
However, it's important to note that our benchmarks used a **simple attestation policy** that only performs **signature verification** on the attestation quote.
In a production setting, policies may include
- Validation of **RTMR (Runtime Measurement Register)** values
- Checks for **specific file measurements** via IMA
- Enforcement of secure configuration (e.g., Secure Boot, kernel version)
@@ -424,10 +449,12 @@ Such policies would add additional verification steps, which could modestly incr
### Input Transfer Cost Not Accounted
Our current benchmark **skips the actual transfer of secret input data** (e.g., witness values, proving keys if needed). In real deployments, this step involves
- Transmitting encrypted payloads over a secure channel (e.g., via mTLS)
- Decrypting and loading the data inside the TEE
The cost of this step depends on
- **Payload size** (some proving inputs may be tens or hundreds of megabytes)
- **Network conditions**, including latency and bandwidth
- **Disk I/O**, if data is staged or stored before proving
@@ -439,6 +466,7 @@ While not captured here, this cost should be considered for systems with large o
The **zkEmail circuit** (based on proof-of-Twitter) demonstrates a **much larger proving time** — averaging over 67 seconds — due to higher circuit complexity. In contrast, the **Semaphore proof** completes in just over 1.1 seconds on average.
This reinforces a key insight:
- The performance bottleneck shifts from attestation to proving as circuit size grows.
- TEE-induced overhead is amortized in large proofs, making the TEE setup cost relatively negligible in long-running sessions.
@@ -450,6 +478,7 @@ One advantage of using a **VM-based TEE like Intel TDX** is the ability to **sca
- This enables faster proof generation for large circuits without modifying the security model or proving logic.
Furthermore, although we have not explored it in this benchmark, **[GPU acceleration is becoming increasingly feasible](https://developer.nvidia.com/blog/confidential-computing-on-h100-gpus-for-secure-and-trustworthy-ai/)** within TEE environments. Some confidential computing frameworks are beginning to support
- GPU passthrough into confidential VMs
- Secure offloading of compute-intensive workloads (like FFTs, MSMs) into trusted GPU regions
@@ -493,18 +522,15 @@ For context, the typical hardware specs used in these mobile benchmarks are
These constraints illustrate the challenge of using mobile-native proving for larger circuits.
---
## Relative Comparison (based on our benchmarks)
| Approach | Semaphore Circuit | Notes |
| ---------------------- | ---------------------------- | ------------------------------------------- |
| Approach | Semaphore Circuit | Notes |
| ---------------------- | ----------------------------- | ------------------------------------------- |
| TEE | ~210ms (proving), ~2.9s (E2E) | Fast, attested, scalable |
| Collaborative zkSNARKs | ~ 700ms (proving) | Needs coordination; performance varies |
| MoPro | 165 ~ 1267ms (proving) | Local execution; limited by mobile hardware |
| Collaborative zkSNARKs | ~ 700ms (proving) | Needs coordination; performance varies |
| MoPro | 165 ~ 1267ms (proving) | Local execution; limited by mobile hardware |
# Conclusion
@@ -522,20 +548,20 @@ TEE-based proving is ready for real deployment — especially in cloud-based pri
- Bind attestation results directly to ZK proof outputs.
- Extend runtime integrity policies for broader trust coverage.
---
## Appendix
## Our choice of TEE
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 of different types of TEEs*
_Trust boundary of different types of TEEs_
#### **Process-Based TEEs**
Examples: Intel SGX, ARM TrustZone
Characteristics:
@@ -558,6 +584,7 @@ Limitations:
- Harder scalability: Managing large applications inside small enclaves can be extremely difficult.
#### **VM-Based TEEs**
Examples: Intel TDX, AMD SEV-SNP
Characteristics:
@@ -579,6 +606,7 @@ Limitations:
- Slightly higher performance overhead compared to native execution (due to memory encryption and virtualization).
#### Why VM-Based TEE Is Better for Our Purpose
In our private proof delegation use case — where we generate zero-knowledge proofs inside the TEE — the requirements favor VM-based TEE designs for several reasons:
**Large Memory Requirements**:
@@ -597,4 +625,4 @@ Azure Confidential VMs (ECedsv5 series) allow seamless deployment, scaling, and
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)
*Confidential VM availability on Azure*
_Confidential VM availability on Azure_

View File

@@ -21,7 +21,7 @@ tags:
projects: ["unirep", "semaphore"]
---
*Originally published on Aug 26, 2021*
_Originally published on Aug 26, 2021_
![](https://miro.medium.com/max/1106/0*nr6Aia8myVXSIZ2R)

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