feat: General fixes 4 (#388)

* feat: general fixes wip

* feat: complete general fixes

- align side wiki
- Enforce kebab-case on URLs (fix #309)
- Search result don’t render markdown (fix #362)
- Move “related projects” before “more articles”in blog page
- Display research projects on projects page (fix #387)
- Add support for youtube videos for projects (fix #384)
- Add support for team member for projects (fix #385)

* feat: "new articles" use new blog card component

* feat: make articles card link more visible
This commit is contained in:
Kalidou Diagne
2025-05-12 13:41:32 +03:00
committed by GitHub
parent 9967073087
commit 5356ea521d
26 changed files with 617 additions and 112 deletions

View File

@@ -43,5 +43,5 @@ export async function generateMetadata({
export default async function ProjectDetailPage({ params }: PageProps) {
const lang = params?.lang as LocaleTypes
return <ProjectContent lang={lang} id={params?.id} />
return <ProjectContent lang={lang} id={params?.id?.toLowerCase()} />
}

View File

@@ -20,6 +20,9 @@ import { WikiSideNavigation } from "@/components/wiki-side-navigation"
import { useTranslation } from "@/app/i18n/client"
import { LocaleTypes } from "@/app/i18n/settings"
import { ProjectBlogArticles } from "@/components/blog/project-blog-articles"
import { ProjectYouTubeVideos } from "@/components/sections/ProjectYouTubeVideos"
import { ProjectTeamMembers } from "@/components/project/project-team"
export const ProjectContent = ({
id,
lang = "en",
@@ -231,8 +234,25 @@ export const ProjectContent = ({
{content?.description}
</Markdown>
)}
{project?.youtubeLinks &&
project.youtubeLinks.length > 0 && (
<div className="mt-5">
<ProjectYouTubeVideos
youtubeLinks={project.youtubeLinks}
lang={lang}
/>
</div>
)}
{project?.team && project.team.length > 0 && (
<div className="mt-5">
<ProjectTeamMembers team={project.team} lang={lang} />
</div>
)}
<ProjectTags project={project} lang={lang} />
</div>
<ProjectExtraLinks project={project} lang={lang} />
</div>

View File

@@ -0,0 +1,97 @@
import { NextRequest, NextResponse } from "next/server"
interface YoutubeVideoResponse {
items: {
id: string
snippet: {
title: string
description: string
publishedAt: string
channelTitle: string
thumbnails: {
high: {
url: string
}
standard?: {
url: string
}
maxres?: {
url: string
}
}
}
}[]
}
// Helper function to add CORS headers
function corsHeaders() {
return {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
}
}
// Handle OPTIONS requests for CORS preflight
export async function OPTIONS() {
return NextResponse.json({}, { headers: corsHeaders() })
}
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams
const ids = searchParams.get("ids")
if (!ids) {
return NextResponse.json(
{ error: "No video IDs provided" },
{ status: 400, headers: corsHeaders() }
)
}
// API key should be in env variables
const apiKey = process.env.YOUTUBE_API_KEY
if (!apiKey) {
return NextResponse.json(
{ error: "YouTube API key not configured" },
{ status: 500, headers: corsHeaders() }
)
}
const youtubeApiUrl = `https://www.googleapis.com/youtube/v3/videos?part=snippet&id=${ids}&key=${apiKey}`
const response = await fetch(youtubeApiUrl)
if (!response.ok) {
throw new Error(`YouTube API responded with status: ${response.status}`)
}
const data: YoutubeVideoResponse = await response.json()
const formattedVideos = data.items.map((item) => {
// Get best available thumbnail
const thumbnailUrl =
item.snippet.thumbnails.maxres?.url ||
item.snippet.thumbnails.standard?.url ||
item.snippet.thumbnails.high.url
return {
id: item.id,
title: item.snippet.title,
description: item.snippet.description,
thumbnailUrl,
publishedAt: item.snippet.publishedAt,
channelTitle: item.snippet.channelTitle,
}
})
return NextResponse.json(formattedVideos, { headers: corsHeaders() })
} catch (error) {
console.error("Error fetching YouTube videos:", error)
return NextResponse.json(
{ error: "Failed to fetch videos from YouTube API" },
{ status: 500, headers: corsHeaders() }
)
}
}

View File

@@ -99,5 +99,9 @@
"joinOurDiscord": "Join our discord",
"prevBrandImage": "Previous branding",
"editThisPage": "Edit this page",
"contents": "Contents"
"contents": "Contents",
"projectVideos": "Project Videos",
"errorLoadingVideos": "Error loading videos",
"projectTeam": "Team",
"youtubeVideos": "YouTube Videos"
}

View File

@@ -7,9 +7,11 @@ import { useRouter } from "next/navigation"
export const ArticleListCard = ({
lang,
article,
lineClamp = false,
}: {
lang: string
article: Article
lineClamp?: boolean
}) => {
const url = `/${lang}/blog/${article.id}`
@@ -21,10 +23,15 @@ export const ArticleListCard = ({
const router = useRouter()
const tldr = lineClamp
? (article.tldr || "").replace(/\n/g, " ").substring(0, 120) +
(article.tldr && article.tldr.length > 120 ? "..." : "")
: article.tldr || ""
return (
<div className="flex h-full">
<div
className="flex-1 w-full group cursor-pointer"
className="full group cursor-pointer hover:scale-105 duration-300"
onClick={() => {
router.push(url)
}}
@@ -88,7 +95,7 @@ export const ArticleListCard = ({
img: ({ src, alt }) => null,
}}
>
{article.tldr || ""}
{tldr}
</Markdown>
</div>
</div>

View File

@@ -5,6 +5,7 @@ import { Markdown } from "../ui/markdown"
import { BlogArticleCard } from "./blog-article-card"
import { BlogArticleRelatedProjects } from "./blog-article-related-projects"
import { LocaleTypes } from "@/app/i18n/settings"
import { ArticleListCard } from "./article-list-card"
interface BlogContentProps {
post: Article
@@ -53,6 +54,11 @@ export function BlogContent({ post, lang }: BlogContentProps) {
<Markdown>{post?.content ?? ""}</Markdown>
</div>
<BlogArticleRelatedProjects
projectsIds={post.projects ?? []}
lang={lang}
/>
{moreArticles?.length > 0 && (
<div className="flex flex-col gap-8">
<div className="flex items-center justify-between">
@@ -66,50 +72,20 @@ export function BlogContent({ post, lang }: BlogContentProps) {
View all
</Link>
</div>
<div className="grid grid-cols-1 gap-8 md:grid-cols-2">
{moreArticles.map(
({
id,
title,
image,
tldr = "",
date,
content,
authors,
}: Article) => {
const url = `/blog/${id}`
return (
<div
key={id}
className="flex flex-col min-h-[400px] w-full"
>
<Link
href={url}
className="flex h-full w-full group hover:opacity-90 transition-opacity duration-300 rounded-xl overflow-hidden bg-white shadow-sm border border-slate-900/10"
style={{ display: "flex", flexDirection: "column" }}
>
<BlogArticleCard
id={id}
image={image}
title={title}
date={date}
content={content}
authors={authors}
tldr={tldr}
/>
</Link>
</div>
)
}
)}
<div className="flex flex-col gap-10">
{moreArticles.map((article: Article) => {
return (
<ArticleListCard
key={article.id}
lang={lang}
article={article}
lineClamp
/>
)
})}
</div>
</div>
)}
<BlogArticleRelatedProjects
projectsIds={post.projects ?? []}
lang={lang}
/>
</div>
</AppContent>
)

View File

@@ -11,6 +11,21 @@ export type Icon = LucideIcon
export const Icons = {
sun: SunMedium,
email: (props: LucideProps) => (
<svg
className="h-4 w-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
{...props}
>
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path>
<polyline points="22,6 12,13 2,6"></polyline>
</svg>
),
time: (props: LucideProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"

View File

@@ -0,0 +1,89 @@
import Image from "next/image"
import Link from "next/link"
import { ProjectTeamMember, ProjectLinkWebsite } from "@/lib/types"
import { Icons } from "../icons"
import { useTranslation } from "@/app/i18n/client"
import { LocaleTypes } from "@/app/i18n/settings"
import { ProjectLinkIconMap } from "../mappings/project-links"
interface ProjectTeamMembersProps {
team: ProjectTeamMember[]
lang: LocaleTypes
}
export const ProjectTeamMembers = ({ team, lang }: ProjectTeamMembersProps) => {
const { t } = useTranslation(lang, "common")
if (!team || team.length === 0) {
return null
}
return (
<div className="w-full" id="team" data-section-id="team">
<h2 className="text-[22px] font-bold text-tuatara-700 mb-6">
{t("projectTeam") || "Project Team"}
</h2>
<div className="grid grid-cols-1 xl:grid-cols-3 gap-6">
{team.map((member, index) => (
<div
key={`${member.name}-${index}`}
className="flex flex-col gap-2 p-4 border border-black/10 rounded-md bg-white"
>
<div className="flex items-center gap-4">
{member.image ? (
<div className="relative h-16 w-16 rounded-full overflow-hidden flex-shrink-0">
<Image
src={member.image}
alt={member.name}
fill
className="object-cover"
/>
</div>
) : (
<div className="flex items-center justify-center h-16 w-16 rounded-full bg-slate-200 flex-shrink-0"></div>
)}
<div className="flex flex-col">
<h3 className="font-sans text-base font-medium text-tuatara-900">
{member.name}
</h3>
{member.role && (
<p className="font-sans text-sm text-tuatara-600">
{member.role}
</p>
)}
</div>
</div>
{member.email && (
<a
href={`mailto:${member.email}`}
className="font-sans text-sm text-tuatara-600 hover:text-orange transition-colors mt-1 flex items-center gap-1.5"
>
<Icons.email className="h-4 w-4" />
<span>{member.email}</span>
</a>
)}
{member.links && Object.keys(member.links).length > 0 && (
<div className="flex flex-wrap gap-1">
{Object.entries(member.links).map(([key, value]) => (
<Link
key={key}
href={value ?? ""}
target="_blank"
rel="noreferrer"
className="group flex items-center gap-1.5 text-sm text-tuatara-600 hover:text-orange transition-colors"
>
{ProjectLinkIconMap?.[key as ProjectLinkWebsite]}
<span className="capitalize">{key}</span>
</Link>
))}
</div>
)}
</div>
))}
</div>
</div>
)
}

View File

@@ -83,14 +83,14 @@ export default function ProjectCard({
projectCardVariants({ showLinks, border, className })
)}
onClick={() => {
router.push(`/projects/${id}`)
router.push(`/projects/${id?.toLowerCase()}`)
}}
>
{showBanner && (
<div
className="relative flex flex-col border-b border-black/10 cursor-pointer"
onClick={() => {
router.push(`/projects/${id}`)
router.push(`/projects/${id?.toLowerCase()}`)
}}
>
<Image

View File

@@ -12,6 +12,7 @@ import {
} from "@/hooks/useGlobalSearch"
import { CategoryTag } from "../ui/categoryTag"
import { Markdown } from "../ui/markdown"
import React from "react"
interface SearchModalProps {
open: boolean
@@ -69,7 +70,9 @@ function Hit({
const content = hit.content || hit.description || hit.excerpt || ""
const snippet =
content.length > 150 ? content.substring(0, 150) + "..." : content
content.length > 200
? content.substring(0, 200).replace(/\S+$/, "") + "..."
: content
return (
<Link
@@ -91,11 +94,26 @@ function Hit({
h5: ({ children }) => null,
h6: ({ children }) => null,
img: ({ src, alt }) => null,
a: ({ href, children }) => (
<span className="text-tuatara-700 font-sans text-base font-normal">
{children}
</span>
),
a: ({ href, children }) => {
let textContent = ""
React.Children.forEach(children, (child) => {
if (typeof child === "string") {
textContent += child
} else if (React.isValidElement(child)) {
const childText = child.props?.children
if (typeof childText === "string") {
textContent += childText
}
}
})
return (
<span className="text-tuatara-700 font-sans text-base font-normal">
{textContent}
</span>
)
},
}}
>
{snippet}

View File

@@ -0,0 +1,126 @@
"use client"
import { useTranslation } from "@/app/i18n/client"
import Image from "next/image"
import Link from "next/link"
import { Icons } from "../icons"
import { useEffect, useState } from "react"
interface VideoCardProps {
videoId: string
}
const VideoCard = ({ videoId }: VideoCardProps) => {
const [title, setTitle] = useState<string>("")
const [isLoading, setIsLoading] = useState<boolean>(true)
const thumbnailUrl = `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`
useEffect(() => {
// Use YouTube's oEmbed API to get video title (no CORS issues)
fetch(
`https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v=${videoId}&format=json`
)
.then((response) => response.json())
.then((data) => {
setTitle(data.title)
setIsLoading(false)
})
.catch(() => {
// If oEmbed fails, we'll just show "YouTube Video"
setIsLoading(false)
})
}, [videoId])
return (
<Link
href={`https://www.youtube.com/watch?v=${videoId}`}
target="_blank"
rel="noopener noreferrer"
className="group flex flex-col gap-4"
>
<div className="relative overflow-hidden aspect-video rounded-sm">
<div className="absolute inset-0 bg-black opacity-20 group-hover:opacity-60 transition-opacity duration-300 z-10"></div>
<Image
src={thumbnailUrl}
alt={title || "YouTube video"}
fill
style={{ objectFit: "cover" }}
className="transition-transform duration-300 scale-105 group-hover:scale-110"
onError={(e) => {
// Fallback to medium quality if maxresdefault is not available
const target = e.target as HTMLImageElement
target.src = `https://img.youtube.com/vi/${videoId}/mqdefault.jpg`
}}
/>
<div className="absolute inset-0 flex items-center justify-center z-20">
<Icons.play />
</div>
</div>
<h3 className="font-sans text-sm font-normal line-clamp-3 text-tuatara-800 group-hover:text-tuatara-600 transition-colors">
{isLoading ? (
<div className="h-5 w-full bg-slate-200 animate-pulse rounded-sm"></div>
) : (
title || "YouTube Video"
)}
</h3>
</Link>
)
}
export const ProjectYouTubeVideos = ({
youtubeLinks,
lang,
}: {
youtubeLinks: string[]
lang: string
}) => {
const { t } = useTranslation(lang, "common")
const [videoIds, setVideoIds] = useState<string[]>([])
const extractVideoId = (url: string): string => {
if (url.includes("youtube.com/watch?v=")) {
const urlObj = new URL(url)
return urlObj.searchParams.get("v") || ""
} else if (url.includes("youtu.be/")) {
return url.split("youtu.be/")[1]?.split("?")[0] || ""
} else if (/^[a-zA-Z0-9_-]{11}$/.test(url)) {
return url
}
return ""
}
useEffect(() => {
if (!youtubeLinks || youtubeLinks.length === 0) {
return
}
// Extract valid video IDs
const ids = youtubeLinks.map(extractVideoId).filter((id) => id)
setVideoIds(ids)
}, [youtubeLinks])
if (!youtubeLinks || youtubeLinks.length === 0 || videoIds.length === 0) {
return null
}
return (
<section
className="w-full py-6"
id="youtube-videos"
data-section-id="youtube-videos"
>
<div className="flex flex-col gap-6">
<h2 className="text-[22px] font-bold text-tuatara-700">
{t("projectVideos") || "Project Videos"}
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{videoIds.map((videoId) => (
<VideoCard key={videoId} videoId={videoId} />
))}
</div>
</div>
</section>
)
}

View File

@@ -37,14 +37,16 @@ const SideNavigationItem = ({
<li
key={id}
className={cn(
"flex min-h-8 items-center border-l-2 border-l-anakiwa-200 px-2 duration-200 cursor-pointer w-full",
"flex min-h-8 items-center border-l-2 border-l-anakiwa-200 px-2 duration-200 cursor-pointer w-full pb-2",
{
"border-l-anakiwa-500 text-anakiwa-500 font-medium":
activeSection === id,
}
)}
>
<button onClick={onClick}>{text}</button>
<button onClick={onClick} className="text-left">
{text}
</button>
</li>
)
}
@@ -156,9 +158,14 @@ export const WikiSideNavigation = ({
},
}
const { extraLinks = {} } = project
const { extraLinks = {}, team = [], youtubeLinks = [] } = project
const hasRelatedArticles = articles.length > 0 && !loading
const hasTeam = Array.isArray(team) && team.length > 0
const hasYoutubeVideos =
Array.isArray(youtubeLinks) && youtubeLinks.length > 0
console.log(hasTeam, hasYoutubeVideos, youtubeLinks)
if (sections.length === 0 || content.length === 0) return null
@@ -193,6 +200,27 @@ export const WikiSideNavigation = ({
/>
)
})}
{hasYoutubeVideos && (
<SideNavigationItem
key="youtube-videos"
onClick={() => scrollToSection("youtube-videos")}
activeSection={activeSection}
text={t("youtubeVideos") || "YouTube Videos"}
id="youtube-videos"
/>
)}
{hasTeam && (
<SideNavigationItem
key="team"
onClick={() => scrollToSection("team")}
activeSection={activeSection}
text={t("projectTeam") || "Team"}
id="team"
/>
)}
{hasRelatedArticles && (
<SideNavigationItem
key="related-articles"

View File

@@ -50,7 +50,7 @@ import { privateProofDelegation } from "./projects/private-proof-delegation"
import { pod2 } from "./projects/pod2"
import { postQuantumCryptography } from "./projects/post-quantum-cryptography"
import { machinaIo } from "./projects/machina-iO"
import { plasmaFold } from "./projects/plasma_fold"
import { plasmaFold } from "./projects/plasma-fold"
import { vOPRF } from "./projects/vOPRF"
import { mpz } from "./projects/mpz"
import { clientSideProving } from "./projects/client-side-proving"
@@ -116,5 +116,5 @@ export const projects: ProjectInterface[] = [
machinaIo,
plasmaFold,
vOPRF,
mpz
mpz,
]

View File

@@ -100,6 +100,51 @@ This is the result
![Project links](/public/project/example-project-detail.jpg)
## Adding YouTube Videos to a Project
Add YouTube videos to your project by including a `youtubeLinks` array:
```js
export const projectName: ProjectInterface = {
// other properties...
youtubeLinks: [
"https://www.youtube.com/watch?v=XXXXXXXXXXX",
"https://youtu.be/XXXXXXXXXXX",
"XXXXXXXXXXX" // Just the YouTube video ID
],
}
```
The videos will appear as clickable thumbnails with titles on the project page:
![YouTube Videos](/public/project/example-project-video.png)
## Adding Team Members to a Project
Add team members to your project using the `team` array:
```js
export const projectName: ProjectInterface = {
// other properties...
team: [
{
name: "John Doe",
role: "Lead Developer",
image: "/team/john-doe.jpg", // Optional
email: "john.doe@example.com", // Optional
links: { // Optional
github: "https://github.com/johndoe",
twitter: "https://twitter.com/johndoe"
}
}
],
}
```
Supported link types: `github`, `website`, `discord`, `twitter`, `youtube`, `telegram`
![Project Team Members](/public/project/example-project-team.png)
### Project detail now supports markdown
Please note the keyword and theme is curated by the comms & design team. If you wish to change, please create a PR.

View File

@@ -1,14 +1,14 @@
import {
ProjectCategory,
ProjectContent,
ProjectInterface,
ProjectStatus,
} from "@/lib/types"
const content: ProjectContent = {
en: {
tldr: "Developing efficient zero-knowledge proving systems for mobile devices, enabling private digital ID and secure communication with minimal resources.",
description: `
ProjectCategory,
ProjectContent,
ProjectInterface,
ProjectStatus,
} from "@/lib/types"
const content: ProjectContent = {
en: {
tldr: "Developing efficient zero-knowledge proving systems for mobile devices, enabling private digital ID and secure communication with minimal resources.",
description: `
### Project Overview
The Client-Side Proving project aims to develop practical and efficient zero-knowledge (ZK) proving systems tailored specifically for mobile devices. By exploring various proving systems - including Binius, Spartan, Plonky2, Scribe, and WHIR - we provide benchmarks, insights, and optimized implementations that enable performant client-side applications.
@@ -58,25 +58,32 @@ import {
Benchmark findings and technical write-ups will be released regularly, highlighting the project's research outcomes and performance evaluations.
`,
},
}
export const clientSideProving: ProjectInterface = {
id: "client-side-proving",
category: ProjectCategory.RESEARCH,
section: "pse",
content,
projectStatus: ProjectStatus.ACTIVE,
image: "",
license: "MIT",
name: "Client-Side Proving",
team: [
{
name: "Alex Kuzmin",
email: "alex.kuzmin@pse.dev",
},
}
export const clientSideProving: ProjectInterface = {
id: "client-side-proving",
category: ProjectCategory.RESEARCH,
section: "pse",
content,
projectStatus: ProjectStatus.ACTIVE,
image: "",
license: "MIT",
name: "Client-Side Proving",
tags: {
keywords: ["Zero Knowledge", "Mobile", "Privacy", "Digital Identity"],
themes: ["build", "research"],
types: ["Legos/dev tools", "Benchmarking", "Proof systems"],
{
name: "Guorong Du",
email: "dgr009@pse.dev",
},
extraLinks: {},
}
],
tags: {
keywords: ["Zero Knowledge", "Mobile", "Privacy", "Digital Identity"],
themes: ["build", "research"],
types: ["Legos/dev tools", "Benchmarking", "Proof systems"],
},
extraLinks: {},
}

View File

@@ -56,15 +56,32 @@ MachinaIO can transform blockchain and cryptographic applications, such as trust
- **GitHub Repository:** [Machina iO](https://github.com/MachinaIO/)
- **Project Plan:** [HackMD Plan](https://hackmd.io/@MachinaIO/H1w5iwmDke)
### Contact
- **Enrico Bottazzi:** [enrico@pse.dev](mailto:enrico@pse.dev)
- **Sora Suegami:** [sorasuegami@pse.dev](mailto:sorasuegami@pse.dev)
- [**PSE Discord**](https://discord.com/invite/sF5CT5rzrR)
- On X: [@machina__io](https://x.com/machina__io) `,
`,
},
},
links: {
twitter: "https://x.com/machina__io",
github: "https://github.com/MachinaIO/",
website: "https://hackmd.io/@MachinaIO/H1w5iwmDke",
},
team: [
{
name: "Enrico Bottazzi",
email: "enrico@pse.dev",
},
{
name: "Sora Suegami",
email: "sorasuegami@pse.dev",
},
{
name: "Pia",
email: "pia@pse.dev",
},
{
name: "Pia",
email: "pia@pse.dev",
},
],
tags: {
keywords: [
"indistinguishability obfuscation",
@@ -76,6 +93,5 @@ MachinaIO can transform blockchain and cryptographic applications, such as trust
],
themes: ["cryptography", "privacy", "scalability"],
types: ["research", "development"],
},
}

View File

@@ -2,15 +2,14 @@ import { ProjectCategory, ProjectInterface, ProjectStatus } from "@/lib/types"
export const scalingSemaphore: ProjectInterface = {
id: "scaling-semaphore-pir",
image: "", // add cover image or leave blank
image: "", // add cover image or leave blank
name: "Scaling Semaphore PIR Merkle-Path Retrieval",
section: "pse",
projectStatus: ProjectStatus.ACTIVE,
category: ProjectCategory.RESEARCH,
content: {
en: {
tldr:
"Private Information Retrieval lets Semaphore users fetch their Merkle path from a server without revealing which identity they own, enabling truly private proofs for groups with millions of members.",
tldr: "Private Information Retrieval lets Semaphore users fetch their Merkle path from a server without revealing which identity they own, enabling truly private proofs for groups with millions of members.",
description: `
### Scaling Semaphore Private Merkle-Path Retrieval with PIR
@@ -59,6 +58,15 @@ Traditional PIR protocols were too heavy for on-chain use. Recent schemes—e.g.
`,
},
},
links: {
discord: "https://discord.com/invite/sF5CT5rzrR",
},
team: [
{
name: "Brechy",
email: "brechy@pse.dev",
},
],
tags: {
keywords: [
"Semaphore",

View File

@@ -53,16 +53,23 @@ Most L2s today struggle to scale without relying on increasingly expensive data
## Get Involved
Plasma Fold is part of the Privacy & Scaling Explorations initiative. If youre interested in collaborating, contributing research, or running your own Plasma Fold client, wed love to hear from you.
On X: [@xyz_pierre](https://twitter.com/xyz_pierre)
Email: Pierre Daix-Moreux [pierre@pse.dev](mailto:pierre@pse.dev), Chengru Zhang [winderica@pse.dev](mailto:winderica@pse.dev)
Join the conversation on [PSE Discord](https://discord.com/invite/sF5CT5rzrR)
`,
},
},
team: [
{
name: "Pierre Daix-Moreux",
email: "pierre@pse.dev",
links: {
twitter: "https://twitter.com/xyz_pierre",
},
},
{
name: "Chengru Zhang",
email: "winderica@pse.dev",
},
],
tags: {
keywords: [
"plasma",

View File

@@ -85,8 +85,21 @@ export const privateProofDelegation: ProjectInterface = {
image: "",
imageAlt: "Private proof delegation",
name: "Private proof delegation",
team: [
{
name: "Takamichi Tsutsumi",
email: "tkmct@pse.dev",
},
{
name: "Wanseob Lim",
email: "wanseob@pse.dev",
},
{
name: "Shouki Tsuda",
email: "shouki@pse.dev",
},
],
links: {
github:
"https://github.com/privacy-scaling-explorations/private-proof-delegation-docs",
},

View File

@@ -73,13 +73,18 @@ The protocol enables a broad range of applications requiring privacy and pseudon
## Contact
- **Magamedrasul Ibragimov:** [curryrasul@pse.dev](mailto:curryrasul@pse.dev) | [@curryrasul](https://twitter.com/curryrasul)
- **Join** the [PSE Discord](https://discord.com/invite/sF5CT5rzrR)`,
- **Join** the PSE Discord](https://discord.com/invite/sF5CT5rzrR)`,
},
},
team: [
{
name: "Magamedrasul Ibragimov",
email: "curryrasul@pse.dev",
},
],
tags: {
keywords: ["vOPRF", "nullifiers", "Web2", "privacy", "ZK proofs", "MPC"],
themes: ["privacy", "identity", "zero-knowledge proofs"],
types: ["research", "development"],
},
}

View File

@@ -4,7 +4,8 @@ import { ProjectInterface } from "./types"
export const getProjectById = (id: string | number, lang = "en") => {
const project: ProjectInterface =
projects.filter((project) => String(project.id) === id)[0] ?? {}
projects.filter((project) => String(project.id?.toLowerCase()) === id)[0] ??
{}
const content = project?.content?.[lang]

View File

@@ -113,9 +113,20 @@ export interface ProjectContent {
description: string
}
}
export interface ProjectTeamMember {
name: string
role?: string
image?: string
links?: ProjectLinkType
email?: string
}
export interface ProjectInterface {
id: string
hasWiki?: boolean // show project with wiki page template
youtubeLinks?: string[]
team?: ProjectTeamMember[]
license?: string
content: ProjectContent // project tldr and description with support for multiple language
category?: ProjectCategory // project category used as filter to replace section

View File

@@ -25,7 +25,7 @@ const nextConfig = {
pageExtensions: ["js", "jsx", "mdx", "ts", "tsx", "md"],
reactStrictMode: true,
images: {
domains: ["i.ytimg.com"],
domains: ["i.ytimg.com", "img.youtube.com"],
},
experimental: {

Binary file not shown.

After

Width:  |  Height:  |  Size: 597 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -132,8 +132,14 @@ export const filterProjects = ({
searchPattern = "",
activeFilters = {},
findAnyMatch = false,
projects: projectList = projects,
projects: projectListItems = projects,
}: SearchMatchByParamsProps) => {
const projectList = projectListItems.map((project: any) => {
return {
...project,
id: project?.id?.toLowerCase(), // force lowercase id
}
})
// keys that will be used for search
const keys = [
"name",
@@ -225,7 +231,12 @@ const sortProjectByFn = ({
return sortedProjectList.filter((project) => project.category === category)
}
return sortedProjectList
return sortedProjectList.map((project: any) => {
return {
id: project?.id?.toLowerCase(), // force lowercase id
...project,
}
})
}
export const useProjectFiltersState = create<
@@ -320,6 +331,7 @@ export const useProjectFiltersState = create<
projects: sortProjectByFn({
projects: filteredProjects,
sortBy: state.sortBy,
ignoreCategories: [], // when filtering, show all projects regardless of category
}),
}
})