mirror of
https://github.com/privacy-scaling-explorations/pse.dev.git
synced 2026-01-10 14:48:13 -05:00
Merge pull request #115 from privacy-scaling-explorations/110-lang-switcher
Introduce multi-language
This commit is contained in:
120
app/[lang]/about/page.tsx
Normal file
120
app/[lang]/about/page.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import React from "react"
|
||||
import Image from "next/image"
|
||||
|
||||
import { Accordion } from "@/components/ui/accordion"
|
||||
import { useTranslation } from "@/app/i18n"
|
||||
|
||||
interface PrincipleContentProps {
|
||||
image: string
|
||||
children: React.ReactNode
|
||||
width?: number
|
||||
height?: number
|
||||
}
|
||||
|
||||
const PrincipleContent = ({
|
||||
image,
|
||||
children,
|
||||
width = 300,
|
||||
height = 300,
|
||||
}: PrincipleContentProps) => {
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-6 py-4 md:mb-8 md:grid-cols-2 md:items-center md:gap-2 md:py-6">
|
||||
<div className="m-auto py-6 md:py-0">
|
||||
<Image
|
||||
width={width}
|
||||
height={height}
|
||||
src={image}
|
||||
alt="principle image"
|
||||
/>
|
||||
</div>
|
||||
<span className="flex flex-col gap-4 break-words font-sans text-lg font-normal leading-[150%]">
|
||||
{children}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const PrincipleImageSizes: Record<string, { width: number; height: number }> = {
|
||||
"principle-1": {
|
||||
width: 126,
|
||||
height: 114,
|
||||
},
|
||||
"principle-2": {
|
||||
width: 176,
|
||||
height: 260,
|
||||
},
|
||||
"principle-3": {
|
||||
width: 236,
|
||||
height: 260,
|
||||
},
|
||||
"principle-4": {
|
||||
width: 238,
|
||||
height: 260,
|
||||
},
|
||||
}
|
||||
|
||||
export default async function AboutPage({ params: { lang } }: any) {
|
||||
const { t } = await useTranslation(lang, "about-page")
|
||||
|
||||
const principles: any[] =
|
||||
t("principles", {
|
||||
returnObjects: true,
|
||||
}) ?? []
|
||||
|
||||
return (
|
||||
<div className="bg-anakiwa-200">
|
||||
<div className="bg-second-gradient">
|
||||
<div className="container mx-auto grid grid-cols-1 gap-16 py-10 lg:grid-cols-[1fr_300px] lg:gap-2 lg:py-20">
|
||||
<div className="flex flex-col gap-8 lg:w-4/5">
|
||||
<h6 className="break-words font-display text-4xl font-bold text-tuatara-950 md:py-4 md:text-5xl">
|
||||
{t("title")}
|
||||
</h6>
|
||||
<span className="font-sans text-base font-normal leading-[27px] text-tuatara-950">
|
||||
{t("description")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4 px-8 py-16 md:px-32 md:py-24">
|
||||
<div className="mx-auto pb-4">
|
||||
<Image
|
||||
width={280}
|
||||
height={280}
|
||||
src="/logos/pse-logo-bg.svg"
|
||||
alt="pse logo"
|
||||
/>
|
||||
</div>
|
||||
<h6 className="font-display text-4xl">{t("our-principles-title")}</h6>
|
||||
<Accordion
|
||||
type="multiple"
|
||||
items={[
|
||||
...principles?.map((principle: any, index: number) => {
|
||||
const imageIndex = index + 1
|
||||
const { width, height } =
|
||||
PrincipleImageSizes[`principle-${imageIndex}`] ?? {}
|
||||
|
||||
return {
|
||||
label: principle?.title,
|
||||
value: imageIndex.toString(),
|
||||
children: (
|
||||
<PrincipleContent
|
||||
width={width}
|
||||
height={height}
|
||||
image={`/logos/principle-${imageIndex}.svg`}
|
||||
>
|
||||
{principle.description?.map(
|
||||
(description: string, index: number) => {
|
||||
return <p key={index}>{description}</p>
|
||||
}
|
||||
)}
|
||||
</PrincipleContent>
|
||||
),
|
||||
}
|
||||
}),
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
86
app/[lang]/layout.tsx
Normal file
86
app/[lang]/layout.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import "@/styles/globals.css"
|
||||
import { Metadata } from "next"
|
||||
import Script from "next/script"
|
||||
|
||||
import { siteConfig } from "@/config/site"
|
||||
import { fontDisplay, fontSans } from "@/lib/fonts"
|
||||
import { SiteFooter } from "@/components/site-footer"
|
||||
import { SiteHeader } from "@/components/site-header"
|
||||
import { TailwindIndicator } from "@/components/tailwind-indicator"
|
||||
|
||||
import { fallbackLng, languages } from "../i18n/settings"
|
||||
|
||||
export async function generateStaticParams() {
|
||||
return languages.map((language) => ({ language }))
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL("https://appliedzkp.org"),
|
||||
title: {
|
||||
default: siteConfig.name,
|
||||
template: `%s - ${siteConfig.name}`,
|
||||
},
|
||||
description: siteConfig.description,
|
||||
themeColor: [
|
||||
{ media: "(prefers-color-scheme: light)", color: "white" },
|
||||
{ media: "(prefers-color-scheme: dark)", color: "black" },
|
||||
],
|
||||
icons: {
|
||||
icon: "/favicon.svg",
|
||||
shortcut: "/favicon.svg",
|
||||
apple: "/apple-touch-icon.png",
|
||||
},
|
||||
openGraph: {
|
||||
images: [
|
||||
{
|
||||
url: "/og-image.png",
|
||||
width: 1200,
|
||||
height: 630,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
interface RootLayoutProps {
|
||||
children: React.ReactNode
|
||||
params: any
|
||||
}
|
||||
|
||||
export default function RootLayout({ children, params }: RootLayoutProps) {
|
||||
const lang = params.lang ?? fallbackLng
|
||||
|
||||
return (
|
||||
<>
|
||||
<html
|
||||
lang={lang}
|
||||
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 className={"min-h-screen bg-background antialiased"}>
|
||||
<div className="relative flex min-h-screen flex-col">
|
||||
<SiteHeader lang={lang} />
|
||||
<div className="flex-1">{children}</div>
|
||||
<SiteFooter lang={lang} />
|
||||
</div>
|
||||
<TailwindIndicator />
|
||||
</body>
|
||||
</html>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -2,22 +2,29 @@
|
||||
|
||||
import Image from "next/image"
|
||||
import Link from "next/link"
|
||||
import { useParams } from "next/navigation"
|
||||
import PSELogo from "@/public/icons/archstar.webp"
|
||||
import ArrowRightVector from "@/public/icons/arrow-right.svg"
|
||||
import { motion } from "framer-motion"
|
||||
|
||||
import { siteConfig } from "@/config/site"
|
||||
import News from "@/components/sections/News"
|
||||
import WhatWeDo from "@/components/sections/WhatWeDo"
|
||||
import { News } from "@/components/sections/News"
|
||||
import { WhatWeDo } from "@/components/sections/WhatWeDo"
|
||||
import { ArrowRightUp } from "@/components/svgs/arrows"
|
||||
|
||||
export default function IndexPage() {
|
||||
import { useTranslation } from "../i18n/client"
|
||||
import { LocaleTypes } from "../i18n/settings"
|
||||
|
||||
export default function IndexPage({ params: { lang } }: any) {
|
||||
const { t } = useTranslation(lang, "homepage")
|
||||
const { t: ct } = useTranslation(lang, "common")
|
||||
|
||||
return (
|
||||
<section className="flex flex-col bg-main-gradient">
|
||||
<div className="flex w-full flex-col justify-between gap-5 p-7 md:flex-row md:px-20">
|
||||
<div className="flex w-full flex-col justify-center gap-6 md:w-[660px]">
|
||||
<h6 className="font-sans text-sm uppercase tracking-widest text-orange xl:text-lg">
|
||||
Privacy + Scaling Explorations
|
||||
{t("headerTitle")}
|
||||
</h6>
|
||||
<motion.h1
|
||||
className="text-4xl font-bold lg:text-5xl xl:text-7xl"
|
||||
@@ -25,11 +32,14 @@ export default function IndexPage() {
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
transition={{ duration: 0.8, cubicBezier: "easeOut" }}
|
||||
>
|
||||
Programmable cryptography for people like you
|
||||
{t("headerSubtitle")}
|
||||
</motion.h1>
|
||||
<Link href={"/projects"} className="group flex items-center gap-2">
|
||||
<Link
|
||||
href={`${lang}/projects`}
|
||||
className="group flex items-center gap-2"
|
||||
>
|
||||
<span className="border-b-2 border-orange text-base font-medium uppercase">
|
||||
Explore Project Library
|
||||
{ct("exploreProjectLibrary")}
|
||||
</span>
|
||||
<Image
|
||||
src={ArrowRightVector}
|
||||
@@ -45,34 +55,27 @@ export default function IndexPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<News />
|
||||
<News lang={lang} />
|
||||
|
||||
<div className="bg-radial-gradient flex flex-col gap-32 px-6 py-24 md:px-12">
|
||||
<section className="relative grid w-full grid-cols-1 gap-10 overflow-hidden lg:grid-cols-3 lg:gap-0">
|
||||
<h6 className="flex w-full justify-start text-xl uppercase text-orange lg:justify-center">
|
||||
Who we are
|
||||
{t("whoWeAre")}
|
||||
</h6>
|
||||
<div className="col-span-0 flex flex-col lg:col-span-1">
|
||||
<h3 className="text-3xl font-bold">
|
||||
PSE is a research lab building free tools that expand the world of
|
||||
cryptography.
|
||||
</h3>
|
||||
<h3 className="text-3xl font-bold">{t("whoWeAreDescription")}</h3>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<WhatWeDo />
|
||||
<WhatWeDo lang={lang} />
|
||||
|
||||
<section className="relative grid w-full grid-cols-1 gap-10 overflow-hidden lg:grid-cols-3 lg:gap-0">
|
||||
<h6 className="flex w-full justify-start text-xl uppercase text-orange lg:justify-center">
|
||||
How To Plug In
|
||||
{t("howToPlugIn")}
|
||||
</h6>
|
||||
<div className="col-span-0 flex flex-col lg:col-span-1">
|
||||
<p className="max-w-2xl xl:text-lg">
|
||||
PSE is a growing team of developers, researchers, designers,
|
||||
communicators, artists, and organizers. There are so many ways to
|
||||
get involved- you can try out our apps, build with our tools,
|
||||
contribute to projects, or join our team. We’d love to hear from
|
||||
you!
|
||||
{t("howToPlugInDescription")}
|
||||
</p>
|
||||
<div className="p-3"></div>
|
||||
<Link
|
||||
@@ -83,7 +86,7 @@ export default function IndexPage() {
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<div className="border-b-2 border-orange text-base font-medium uppercase">
|
||||
Say Hi On Discord
|
||||
{t("sayHiToDiscord")}
|
||||
</div>
|
||||
<ArrowRightUp color="black" />
|
||||
</Link>
|
||||
@@ -5,23 +5,24 @@ import { projects } from "@/data/projects"
|
||||
import GithubVector from "@/public/social-medias/github-fill.svg"
|
||||
import GlobalVector from "@/public/social-medias/global-line.svg"
|
||||
import TwitterVector from "@/public/social-medias/twitter-fill.svg"
|
||||
import { filterProjects } from "@/state/useProjectFiltersState"
|
||||
|
||||
import { ProjectInterface } from "@/lib/types"
|
||||
import { shuffleArray } from "@/lib/utils"
|
||||
import { Markdown } from "@/components/ui/markdown"
|
||||
import { Icons } from "@/components/icons"
|
||||
import ProjectCard from "@/components/project/project-card"
|
||||
import DiscoverMoreProjects from "@/components/project/discover-more-projects"
|
||||
import { ProjectTags } from "@/components/project/project-detail-tags"
|
||||
import ProjectExtraLinks from "@/components/project/project-extra-links"
|
||||
import { useTranslation } from "@/app/i18n"
|
||||
import { LocaleTypes } from "@/app/i18n/settings"
|
||||
|
||||
type PageProps = {
|
||||
params: { id: string }
|
||||
params: { id: string; lang: string }
|
||||
searchParams: { [key: string]: string | string[] | undefined }
|
||||
}
|
||||
|
||||
interface ProjectProps {
|
||||
export interface ProjectProps {
|
||||
project: ProjectInterface
|
||||
lang: LocaleTypes
|
||||
}
|
||||
|
||||
export async function generateMetadata(
|
||||
@@ -51,50 +52,12 @@ export async function generateMetadata(
|
||||
}
|
||||
}
|
||||
|
||||
function DiscoverMoreProjects({ project }: ProjectProps) {
|
||||
const getSuggestedProjects = () => {
|
||||
const projectList = projects.filter((p) => p.id !== project.id)
|
||||
|
||||
const suggestedProject = filterProjects({
|
||||
searchPattern: "",
|
||||
activeFilters: project?.tags,
|
||||
findAnyMatch: true,
|
||||
projects: projectList,
|
||||
})
|
||||
|
||||
// No match return random projects
|
||||
if (suggestedProject?.length < 2) {
|
||||
return shuffleArray(projectList).slice(0, 2)
|
||||
}
|
||||
|
||||
return suggestedProject.slice(0, 2)
|
||||
}
|
||||
|
||||
const suggestedProject = getSuggestedProjects()
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col items-center justify-center gap-14 bg-anakiwa-200 px-6 py-32 md:px-0">
|
||||
<h2 className="text-center text-3xl font-bold">Discover more</h2>
|
||||
<div className="flex flex-col gap-5 md:flex-row">
|
||||
{suggestedProject?.map((project: ProjectInterface) => (
|
||||
<ProjectCard project={project} />
|
||||
))}
|
||||
</div>
|
||||
<Link
|
||||
className="flex items-center gap-2 text-tuatara-950/80 hover:text-tuatara-950"
|
||||
href="/projects"
|
||||
>
|
||||
<Icons.arrowLeft />
|
||||
<span className="font-sans text-base">Back to project library</span>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function ProjectDetailPage({ params }: PageProps) {
|
||||
export default async function ProjectDetailPage({ params }: PageProps) {
|
||||
const currProject = projects.filter(
|
||||
(project) => String(project.id) === params.id
|
||||
)[0]
|
||||
const lang = params?.lang as LocaleTypes
|
||||
const { t } = await useTranslation(lang, "common")
|
||||
|
||||
const { github, twitter, website } = currProject.links ?? {}
|
||||
const hasSocialLinks = Object.keys(currProject?.links ?? {}).length > 0
|
||||
@@ -107,10 +70,12 @@ export default function ProjectDetailPage({ params }: PageProps) {
|
||||
<div className="flex flex-col gap-6 text-left">
|
||||
<Link
|
||||
className="flex items-center gap-2 text-tuatara-950/80 hover:text-tuatara-950"
|
||||
href="/projects"
|
||||
href={`/${lang}/projects`}
|
||||
>
|
||||
<Icons.arrowLeft />
|
||||
<span className="font-sans text-base">Project library</span>
|
||||
<span className="font-sans text-base">
|
||||
{t("projectLibrary")}
|
||||
</span>
|
||||
</Link>
|
||||
<div className="flex flex-col gap-2">
|
||||
<h1 className="py-2 text-3xl font-bold leading-[110%] md:text-5xl">
|
||||
@@ -168,15 +133,15 @@ export default function ProjectDetailPage({ params }: PageProps) {
|
||||
className="w-full rounded-t-lg object-cover"
|
||||
/>
|
||||
</div>
|
||||
<ProjectTags project={currProject} />
|
||||
<ProjectTags project={currProject} lang={lang} />
|
||||
<div className="flex w-full flex-col gap-5 text-base font-normal leading-relaxed">
|
||||
<Markdown>{currProject.description}</Markdown>
|
||||
</div>
|
||||
<ProjectExtraLinks project={currProject} />
|
||||
<ProjectExtraLinks project={currProject} lang={lang} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DiscoverMoreProjects project={currProject} />
|
||||
<DiscoverMoreProjects project={currProject} lang={lang} />
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { Metadata } from "next"
|
||||
import ProjectFiltersBar from "@/components/project/project-filters-bar"
|
||||
import ProjectList from "@/components/project/project-list"
|
||||
import { ProjectResultBar } from "@/components/project/project-result-bar"
|
||||
import { useTranslation } from "@/app/i18n"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Project Library",
|
||||
@@ -10,24 +11,23 @@ export const metadata: Metadata = {
|
||||
"PSE is home to many projects, from cryptography research to developer tools, protocols, and proof-of-concept applications.",
|
||||
}
|
||||
|
||||
export default function ProjectsPage() {
|
||||
export default async function ProjectsPage({ params: { lang } }: any) {
|
||||
const { t } = await useTranslation(lang, "projects-page")
|
||||
|
||||
return (
|
||||
<section>
|
||||
<div className="bg-anakiwa-200">
|
||||
<div className="container mx-auto py-10 lg:py-20">
|
||||
<div className="flex flex-col justify-between gap-10">
|
||||
<div>
|
||||
<h1 className="font-display text-4xl font-bold text-tuatara-950 md:text-5xl">
|
||||
Explore the project library
|
||||
</h1>
|
||||
<h1 className="font-display text-4xl font-bold text-tuatara-950 md:text-5xl"></h1>
|
||||
<p className="p-2"></p>
|
||||
<p className="w-full text-lg md:w-[612px] md:text-xl">
|
||||
PSE is home to many projects, from cryptography research to
|
||||
developer tools, protocols and proof-of-concept applications.
|
||||
{t("subtitle")}
|
||||
</p>
|
||||
</div>
|
||||
<div className=" h-[1px] w-20 bg-anakiwa-500"></div>
|
||||
<ProjectFiltersBar />
|
||||
<div className="h-[1px] w-20 bg-anakiwa-500"></div>
|
||||
<ProjectFiltersBar lang={lang} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -35,9 +35,9 @@ export default function ProjectsPage() {
|
||||
<div className="w-full bg-anakiwa-100">
|
||||
<div className="container">
|
||||
<div className="px-3 py-8">
|
||||
<ProjectResultBar />
|
||||
<ProjectResultBar lang={lang} />
|
||||
</div>
|
||||
<ProjectList />
|
||||
<ProjectList lang={lang} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -1,25 +1,27 @@
|
||||
"use client"
|
||||
|
||||
import Link from "next/link"
|
||||
import { useCallback, useEffect, useRef, useState } from "react"
|
||||
|
||||
import { Icons } from "@/components/icons"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
import Link from "next/link"
|
||||
import { ArrowUpRight } from "lucide-react"
|
||||
|
||||
import { LangProps } from "@/types/common"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Icons } from "@/components/icons"
|
||||
import { useTranslation } from "@/app/i18n/client"
|
||||
|
||||
import ResourcesContent from "../content/resources.md"
|
||||
|
||||
interface ResourceItemProps {
|
||||
label: string
|
||||
icon?:
|
||||
| "globe"
|
||||
| "discord"
|
||||
| "twitter"
|
||||
| "gitHub"
|
||||
| "notion"
|
||||
| "figma"
|
||||
| "drive"
|
||||
| "globe"
|
||||
| "discord"
|
||||
| "twitter"
|
||||
| "gitHub"
|
||||
| "notion"
|
||||
| "figma"
|
||||
| "drive"
|
||||
description: string
|
||||
url: string
|
||||
}
|
||||
@@ -50,16 +52,15 @@ const ResourceItem = ({
|
||||
<div className="h-6 w-6 text-anakiwa-500 opacity-50 transition group-hover:text-tuatara-950 group-hover:opacity-100">
|
||||
<Icon />
|
||||
</div>
|
||||
<span className="text-lg font-medium">
|
||||
{label}
|
||||
</span>
|
||||
<span className="text-lg font-medium">{label}</span>
|
||||
</div>
|
||||
<ArrowUpRight size={24} className="text-tuatara-950 opacity-0 transition duration-500 group-hover:opacity-100" />
|
||||
<ArrowUpRight
|
||||
size={24}
|
||||
className="text-tuatara-950 opacity-0 transition duration-500 group-hover:opacity-100"
|
||||
/>
|
||||
</div>
|
||||
<div className="p-[2px]"></div>
|
||||
<p className="text-sm text-tuatara-500">
|
||||
{description}
|
||||
</p>
|
||||
<p className="text-sm text-tuatara-500">{description}</p>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
@@ -82,24 +83,27 @@ const ResourceCard = ({ id, title, children }: ResourceCardProps) => {
|
||||
)
|
||||
}
|
||||
|
||||
const ResourceNav = () => {
|
||||
const ResourceNav = ({ lang }: LangProps["params"]) => {
|
||||
const { t } = useTranslation(lang, "resources-page")
|
||||
|
||||
const SCROLL_OFFSET = 80
|
||||
const [activeId, setActiveId] = useState("")
|
||||
const [isManualScroll, setIsManualScroll] = useState(false)
|
||||
const ID_LABELS_MAPPING: Record<string, string> = {
|
||||
"get-involved": "Get involved",
|
||||
learn: "Learn",
|
||||
build: "Build",
|
||||
design: "Design",
|
||||
"get-involved": t("nav.getInvolved"),
|
||||
learn: t("nav.learn"),
|
||||
build: t("nav.build"),
|
||||
design: t("nav.design"),
|
||||
}
|
||||
const sectionsRef = useRef<NodeListOf<HTMLElement> | null>(null) // sections are constant so useRef might be better here
|
||||
|
||||
useEffect(() => {
|
||||
if (sectionsRef.current === null) sectionsRef.current = document.querySelectorAll(`div[data-section]`)
|
||||
if (!activeId) setActiveId('get-involved')
|
||||
if (sectionsRef.current === null)
|
||||
sectionsRef.current = document.querySelectorAll(`div[data-section]`)
|
||||
if (!activeId) setActiveId("get-involved")
|
||||
|
||||
const handleScroll = () => {
|
||||
if (isManualScroll) return;
|
||||
if (isManualScroll) return
|
||||
|
||||
sectionsRef.current?.forEach((section: any) => {
|
||||
const sectionTop = section.offsetTop - SCROLL_OFFSET
|
||||
@@ -109,8 +113,8 @@ const ResourceNav = () => {
|
||||
})
|
||||
}
|
||||
|
||||
window.addEventListener('scroll', handleScroll)
|
||||
return () => window.removeEventListener('scroll', handleScroll)
|
||||
window.addEventListener("scroll", handleScroll)
|
||||
return () => window.removeEventListener("scroll", handleScroll)
|
||||
}, [activeId, isManualScroll])
|
||||
|
||||
const scrollToId = useCallback((id: string) => {
|
||||
@@ -133,7 +137,7 @@ const ResourceNav = () => {
|
||||
<div className="flex flex-col gap-6 p-8">
|
||||
<div className="flex flex-col gap-4">
|
||||
<h6 className="font-display text-lg font-bold text-tuatara-700">
|
||||
On this page
|
||||
{t("onThisPage")}
|
||||
</h6>
|
||||
<ul className="text-normal font-sans text-black">
|
||||
{Object.entries(ID_LABELS_MAPPING).map(([id, label]) => {
|
||||
@@ -159,27 +163,35 @@ const ResourceNav = () => {
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
<Link href={"https://github.com/privacy-scaling-explorations/website-v2/blob/main/app/content/resources.md?plain=1"} target="_blank">
|
||||
<Link
|
||||
href={
|
||||
"https://github.com/privacy-scaling-explorations/website-v2/blob/main/app/content/resources.md?plain=1"
|
||||
}
|
||||
target="_blank"
|
||||
>
|
||||
<Button size="lg" icon={Icons.gitHub}>
|
||||
<span className="pl-2 text-left text-sm font-medium">Edit resources</span>
|
||||
<span className="pl-2 text-left text-sm font-medium">
|
||||
{t("editResources")}
|
||||
</span>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function ResourcePage() {
|
||||
export default function ResourcePage({ params: { lang } }: LangProps) {
|
||||
const { t } = useTranslation(lang, "resources-page")
|
||||
|
||||
return (
|
||||
<main className="bg-second-gradient">
|
||||
<div className="container grid grid-cols-1 grid-rows-[auto_1fr] gap-6 px-4 py-10 md:grid-cols-[3fr_1fr] md:pb-20 lg:grid-cols-[1fr_3fr_1fr]">
|
||||
<section className="hidden lg:block"></section>
|
||||
<section className=" flex flex-col gap-8 lg:col-start-2">
|
||||
<h1 className="break-words font-display text-4xl font-bold text-tuatara-950 md:text-5xl">
|
||||
Resources
|
||||
{t("title")}
|
||||
</h1>
|
||||
<p className="font-sans text-base font-normal leading-[27px] text-tuatara-950">
|
||||
This list was compiled by our community. Submit an issue on our
|
||||
Github page to add a resource to this list.
|
||||
{t("subtitle")}
|
||||
</p>
|
||||
</section>
|
||||
<article className="row-start-2 flex flex-col space-y-8 lg:col-start-2">
|
||||
@@ -196,7 +208,7 @@ export default function ResourcePage() {
|
||||
</article>
|
||||
<section className="relative col-start-2 row-start-2 hidden md:block lg:col-start-3">
|
||||
<div className="sticky right-0 top-16 ml-auto">
|
||||
<ResourceNav />
|
||||
<ResourceNav lang={lang} />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -1,176 +0,0 @@
|
||||
import React from "react"
|
||||
import Image from "next/image"
|
||||
|
||||
import { Accordion } from "@/components/ui/accordion"
|
||||
|
||||
interface PrincipleContentProps {
|
||||
image: string
|
||||
children: React.ReactNode
|
||||
width?: number
|
||||
height?: number
|
||||
}
|
||||
const PrincipleContent = ({
|
||||
image,
|
||||
children,
|
||||
width = 300,
|
||||
height = 300,
|
||||
}: PrincipleContentProps) => {
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-6 py-4 md:mb-8 md:grid-cols-2 md:items-center md:gap-2 md:py-6">
|
||||
<div className="m-auto py-6 md:py-0">
|
||||
<Image
|
||||
width={width}
|
||||
height={height}
|
||||
src={image}
|
||||
alt="principle image"
|
||||
/>
|
||||
</div>
|
||||
<span className="flex flex-col gap-4 break-words font-sans text-lg font-normal leading-[150%]">
|
||||
{children}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default function AboutPage() {
|
||||
return (
|
||||
<div className="bg-anakiwa-200">
|
||||
<div className="bg-second-gradient">
|
||||
<div className="container mx-auto grid grid-cols-1 gap-16 py-10 lg:grid-cols-[1fr_300px] lg:gap-2 lg:py-20">
|
||||
<div className="flex flex-col gap-8 lg:w-4/5">
|
||||
<h6 className="break-words font-display text-4xl font-bold text-tuatara-950 md:py-4 md:text-5xl">
|
||||
About our team
|
||||
</h6>
|
||||
<span className="font-sans text-base font-normal leading-[27px] text-tuatara-950">
|
||||
PSE is a multi-disciplinary research and development lab supported
|
||||
by the Ethereum Foundation. We create open source infrastructure,
|
||||
tools and educational resources for building cryptography into
|
||||
real world applications.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4 px-8 py-16 md:px-32 md:py-24">
|
||||
<div className="mx-auto pb-4">
|
||||
<Image
|
||||
width={280}
|
||||
height={280}
|
||||
src="/logos/pse-logo-bg.svg"
|
||||
alt="pse logo"
|
||||
/>
|
||||
</div>
|
||||
<h6 className="font-display text-4xl">Our principles</h6>
|
||||
<Accordion
|
||||
type="multiple"
|
||||
items={[
|
||||
{
|
||||
label: "01. Cryptography for people",
|
||||
value: "1",
|
||||
children: (
|
||||
<PrincipleContent
|
||||
width={126}
|
||||
height={114}
|
||||
image="/logos/principle-1.svg"
|
||||
>
|
||||
<p>
|
||||
{`Cryptography is everywhere: every time you connect to a
|
||||
secure site, log in with a password or unlock your phone,
|
||||
you're seeing cryptography in action.`}
|
||||
</p>
|
||||
<p>
|
||||
{`With “programmable” cryptography (like zero knowledge
|
||||
proofs, multi-party computation or homomorphic encryption)
|
||||
we can make verifiable claims about secret information
|
||||
without revealing the information itself. This can be
|
||||
applied to identity management, collusion resistance,
|
||||
anonymous communication and so much more.`}
|
||||
</p>
|
||||
<p>
|
||||
{`We're building a library of dev tools, research papers, and
|
||||
prototypes that are open source and free for everyone to
|
||||
use. We hope our resources inspire people to innovate the
|
||||
technologies that their communities need.`}
|
||||
</p>
|
||||
</PrincipleContent>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "02. Privacy by default",
|
||||
value: "2",
|
||||
children: (
|
||||
<PrincipleContent
|
||||
image="/logos/principle-2.svg"
|
||||
width={176}
|
||||
height={260}
|
||||
>
|
||||
<p>
|
||||
We believe that privacy is a fundamental right. We want to
|
||||
be part of building an internet that divests from invasive
|
||||
data practices, and instead gives people real choices about
|
||||
who has access to their personal information. Permission
|
||||
should be purpose specific, revocable, informed and
|
||||
uncoerced.
|
||||
</p>
|
||||
<p>
|
||||
We make tools that help people to securely authenticate
|
||||
themselves, make confidential transactions on the
|
||||
blockchain, and respect and preserve user privacy.
|
||||
</p>
|
||||
</PrincipleContent>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "03. Scaling for communities",
|
||||
value: "3",
|
||||
children: (
|
||||
<PrincipleContent
|
||||
image="/logos/principle-3.svg"
|
||||
width={236}
|
||||
height={260}
|
||||
>
|
||||
<p>
|
||||
Zero knowledge proofs can verify computations quickly and
|
||||
cheaply, helping decentralized systems like Ethereum become
|
||||
more efficient. We research, design and share scaling
|
||||
solutions that anyone can use to contribute to a stronger
|
||||
and more practical digital public infrastructure.
|
||||
</p>
|
||||
<p>
|
||||
We also grow our community by supporting the next generation
|
||||
of builders. We host immersive summer programs for students
|
||||
and regular live lectures that anyone in the world can join
|
||||
and learn from.
|
||||
</p>
|
||||
</PrincipleContent>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "04. Open source public goods",
|
||||
value: "4",
|
||||
children: (
|
||||
<PrincipleContent
|
||||
image="/logos/principle-4.svg"
|
||||
width={238}
|
||||
height={260}
|
||||
>
|
||||
<p>
|
||||
We are cultivating a diverse and multidisciplinary team to
|
||||
explore the emerging zero knowledge ecosystem. PSE is made
|
||||
up of programmers, engineers, and mathematicians working
|
||||
alongside creatives and community organizers to
|
||||
collaboratively discover the potential of programmable
|
||||
cryptography.
|
||||
</p>
|
||||
<p>
|
||||
We experiment in the open and welcome contributions,
|
||||
integrations, forks, or feedback on all of our projects.
|
||||
</p>
|
||||
</PrincipleContent>
|
||||
),
|
||||
},
|
||||
]}
|
||||
></Accordion>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
68
app/i18n/client.ts
Normal file
68
app/i18n/client.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import i18next from "i18next"
|
||||
import LanguageDetector from "i18next-browser-languagedetector"
|
||||
import resourcesToBackend from "i18next-resources-to-backend"
|
||||
import { useCookies } from "react-cookie"
|
||||
import {
|
||||
initReactI18next,
|
||||
useTranslation as useTranslationOrg,
|
||||
} from "react-i18next"
|
||||
|
||||
import { LocaleTypes, cookieName, getOptions, languages } from "./settings"
|
||||
|
||||
const runsOnServerSide = typeof window === "undefined"
|
||||
|
||||
//
|
||||
export const i18n = i18next
|
||||
.use(initReactI18next)
|
||||
.use(LanguageDetector)
|
||||
.use(
|
||||
resourcesToBackend(
|
||||
(language: string, namespace: string) =>
|
||||
import(`./locales/${language}/${namespace}.json`)
|
||||
)
|
||||
)
|
||||
.init({
|
||||
...getOptions(),
|
||||
debug: false,
|
||||
lng: undefined, // let detect the language on client side
|
||||
detection: {
|
||||
order: ["path", "htmlTag", "cookie", "navigator"],
|
||||
},
|
||||
preload: runsOnServerSide ? languages : [],
|
||||
})
|
||||
|
||||
export function useTranslation(
|
||||
lng: LocaleTypes | string,
|
||||
ns: string,
|
||||
options = {}
|
||||
) {
|
||||
const [cookies, setCookie] = useCookies([cookieName ?? "i18next"])
|
||||
const ret = useTranslationOrg(ns, options)
|
||||
const { i18n } = ret
|
||||
if (runsOnServerSide && lng && i18n.resolvedLanguage !== lng) {
|
||||
i18n.changeLanguage(lng)
|
||||
} else {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const [activeLng, setActiveLng] = useState(i18n.resolvedLanguage)
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
useEffect(() => {
|
||||
if (activeLng === i18n.resolvedLanguage) return
|
||||
setActiveLng(i18n.resolvedLanguage)
|
||||
}, [activeLng, i18n.resolvedLanguage])
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
useEffect(() => {
|
||||
if (!lng || i18n.resolvedLanguage === lng) return
|
||||
i18n.changeLanguage(lng)
|
||||
}, [lng, i18n])
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
useEffect(() => {
|
||||
if (cookies.i18next === lng) return
|
||||
if (!lng) return
|
||||
setCookie(cookieName, lng, { path: "/" })
|
||||
}, [lng, cookies.i18next]) // tofix: set cookies.i18next as deps and fix issue with re-rendering
|
||||
}
|
||||
return ret
|
||||
}
|
||||
27
app/i18n/index.ts
Normal file
27
app/i18n/index.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { createInstance, init } from "i18next"
|
||||
import resourcesToBackend from "i18next-resources-to-backend"
|
||||
import { initReactI18next } from "react-i18next/initReactI18next"
|
||||
|
||||
import { getOptions } from "./settings"
|
||||
|
||||
const initI18next = async (lng: string, ns: string) => {
|
||||
const i18nInstance = createInstance()
|
||||
await i18nInstance
|
||||
.use(initReactI18next)
|
||||
.use(
|
||||
resourcesToBackend(
|
||||
(language: string, namespace: string) =>
|
||||
import(`./locales/${language}/${namespace}.json`)
|
||||
)
|
||||
)
|
||||
.init(getOptions(lng, ns))
|
||||
return i18nInstance
|
||||
}
|
||||
|
||||
export async function useTranslation(lng: string, ns: string, options = {}) {
|
||||
const i18nextInstance = await initI18next(lng, ns)
|
||||
return {
|
||||
t: i18nextInstance.getFixedT(lng, Array.isArray(ns) ? ns[0] : ns),
|
||||
i18n: i18nextInstance,
|
||||
}
|
||||
}
|
||||
36
app/i18n/locales/en/about-page.json
Normal file
36
app/i18n/locales/en/about-page.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"title": "About our team",
|
||||
"description": "PSE is a multi-disciplinary research and development lab supported by the Ethereum Foundation. We create open source infrastructure, tools and educational resources for building cryptography into real world applications.",
|
||||
"our-principles-title": "Our principles",
|
||||
"principles": [
|
||||
{
|
||||
"title": "01. Cryptography for people",
|
||||
"description": [
|
||||
"Cryptography is everywhere: every time you connect to a secure site, log in with a password or unlock your phone, you're seeing cryptography in action.",
|
||||
"With “programmable” cryptography (like zero knowledge proofs, multi-party computation or homomorphic encryption) we can make verifiable claims about secret information without revealing the information itself. This can be applied to identity management, collusion resistance, anonymous communication and so much more.",
|
||||
"We're building a library of dev tools, research papers, and prototypes that are open source and free for everyone to use. We hope our resources inspire people to innovate the technologies that their communities need."
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "02. Privacy by default",
|
||||
"description": [
|
||||
"We believe that privacy is a fundamental right. We want to be part of building an internet that divests from invasive data practices, and instead gives people real choices about who has access to their personal information. Permission should be purpose specific, revocable, informed and uncoerced.",
|
||||
"We make tools that help people to securely authenticate themselves, make confidential transactions on the blockchain, and respect and preserve user privacy."
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "03. Scaling for communities",
|
||||
"description": [
|
||||
"Zero knowledge proofs can verify computations quickly and cheaply, helping decentralized systems like Ethereum become more efficient. We research, design and share scaling solutions that anyone can use to contribute to a stronger and more practical digital public infrastructure.",
|
||||
"We also grow our community by supporting the next generation of builders. We host immersive summer programs for students and regular live lectures that anyone in the world can join and learn from."
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "04. Open source public goods",
|
||||
"description": [
|
||||
"We are cultivating a diverse and multidisciplinary team to explore the emerging zero knowledge ecosystem. PSE is made up of programmers, engineers, and mathematicians working alongside creatives and community organizers to collaboratively discover the potential of programmable cryptography.",
|
||||
"We experiment in the open and welcome contributions, integrations, forks, or feedback on all of our projects."
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
65
app/i18n/locales/en/common.json
Normal file
65
app/i18n/locales/en/common.json
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"siteTitle": "Privacy & Scaling Explorations",
|
||||
"siteDescription": "Enhancing Ethereum through cryptographic research and collective experimentation.",
|
||||
"menu": {
|
||||
"home": "Home",
|
||||
"projectLibrary": "Project Library",
|
||||
"about": "About",
|
||||
"resources": "Resources",
|
||||
"jobs": "Jobs",
|
||||
"languages": "Languages {{locale}}"
|
||||
},
|
||||
"footer": {
|
||||
"description": "Privacy + Scaling Explorations is a multidisciplinary team supported by the Ethereum Foundation.",
|
||||
"privacyPolicy": "Privacy Policy",
|
||||
"termsOfUse": "Terms of use"
|
||||
},
|
||||
"filterOptions": {
|
||||
"random": "Random",
|
||||
"asc": "Title: A-Z",
|
||||
"desc": "Title: Z-A",
|
||||
"relevance": "Relevance"
|
||||
},
|
||||
"filterLabels": {
|
||||
"keywords": "Keywords",
|
||||
"builtWith": "Built with",
|
||||
"themes": "Themes selected",
|
||||
"projectStatus": "Project status"
|
||||
},
|
||||
"error": {
|
||||
"404": {
|
||||
"title": "404: Page not found.",
|
||||
"description": "The page you are looking for might have been removed, had its name changed or is temporarily unavailable."
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"build": "Build",
|
||||
"play": "Play",
|
||||
"research": "Research"
|
||||
},
|
||||
"status": {
|
||||
"archived": "Archived",
|
||||
"active": "Active"
|
||||
},
|
||||
"sortBy": "Sort by: {{option}}",
|
||||
"tryItOut": "Try it out!",
|
||||
"learnMore": "Learn more",
|
||||
"buildWithThisTool": "Build with this tool",
|
||||
"deepDiveResearch": "Dive deeper into the research",
|
||||
"searchProjectPlaceholder": "Search project title or keyword",
|
||||
"close": "Close",
|
||||
"lastUpdatedAt": "Last updated {{date}}",
|
||||
"projectLibrary": "Project Library",
|
||||
"backToProjectLibrary": "Back to project library",
|
||||
"discoverMore": "Discover more",
|
||||
"goToHome": "Go to homepage",
|
||||
"noResults": "No results found.",
|
||||
"noResultsDescription": "Sorry, we couldn't find any results for your search. Please try again with different keywords.",
|
||||
"exploreProjectLibrary": "Explore Project Library",
|
||||
"clearAll": "Clear all",
|
||||
"showProjects": "Show projects",
|
||||
"filters": "Filters",
|
||||
"whatDoYouWantDoToday": "What do you want to do today?",
|
||||
"showingProjects": "Showing {{count}} projects",
|
||||
"showingProjectsWith": "Showing {{count}} projects with:"
|
||||
}
|
||||
9
app/i18n/locales/en/homepage.json
Normal file
9
app/i18n/locales/en/homepage.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"headerTitle": "Privacy + Scaling Explorations",
|
||||
"headerSubtitle": "Programmable cryptography for people like you",
|
||||
"whoWeAre": "Who we are",
|
||||
"whoWeAreDescription": "PSE is a research lab building free tools that expand the world of cryptography.",
|
||||
"howToPlugIn": "How to plug in",
|
||||
"howToPlugInDescription": "PSE is a growing team of developers, researchers, designers, communicators, artists, and organizers. There are so many ways to get involved- you can try out our apps, build with our tools, contribute to projects, or join our team. We'd love to hear from you!",
|
||||
"sayHiToDiscord": "Say Hi On Discord"
|
||||
}
|
||||
34
app/i18n/locales/en/news-section.json
Normal file
34
app/i18n/locales/en/news-section.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"learnAndShare": "Learn & Share",
|
||||
"event": "Event",
|
||||
"blogPost": "Blog Post",
|
||||
"watch": "Watch",
|
||||
"read": "Read",
|
||||
"attend": "Attend",
|
||||
"news": [
|
||||
{
|
||||
"type": "post",
|
||||
"title": "Learnings from the KZG Ceremony",
|
||||
"action": {
|
||||
"label": "Read",
|
||||
"url": "https://mirror.xyz/privacy-scaling-explorations.eth/naTdx-u7kyirczTLSAnWwH6ZdedfTQu1yCWQj1m_n-E"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "learn",
|
||||
"title": "Public events hosted by PSE members to learn about each other's projects, share ideas, and get feedback.",
|
||||
"action": {
|
||||
"label": "See full schedule",
|
||||
"url": "https://pse-team.notion.site/50dcf22c5191485e93406a902ae9e93b?v=453023f8227646dd949abc34a7a4a138&pvs=4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "learn",
|
||||
"title": "Folding Circom Circuit: A ZKML Case Study by Dr. Cathie So",
|
||||
"action": {
|
||||
"label": "Watch",
|
||||
"url": "https://www.youtube.com/live/jb6HDEtY4CI"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
3
app/i18n/locales/en/projects-page.json
Normal file
3
app/i18n/locales/en/projects-page.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"subtitle": "PSE is home to many projects, from cryptography research to developer tools, protocols and proof-of-concept applications."
|
||||
}
|
||||
12
app/i18n/locales/en/resources-page.json
Normal file
12
app/i18n/locales/en/resources-page.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"title": "Resources",
|
||||
"subtitle": "This list was compiled by our community. Submit an issue on our Github page to add a resource to this list.",
|
||||
"onThisPage": "On this page",
|
||||
"editResources": "Edit resources",
|
||||
"nav": {
|
||||
"getInvolved": "Get involved",
|
||||
"learn": "Learn",
|
||||
"build": "Build",
|
||||
"design": "Design"
|
||||
}
|
||||
}
|
||||
15
app/i18n/locales/en/what-we-do-section.json
Normal file
15
app/i18n/locales/en/what-we-do-section.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"whatWeDo": "What we do",
|
||||
"privacy": {
|
||||
"title": "Privacy",
|
||||
"description": "We believe privacy is a social good that should be accessible to everyone. That's why we're creating open source tools that help people choose what, how, when, and where they share."
|
||||
},
|
||||
"scaling": {
|
||||
"title": "Scaling",
|
||||
"description": "Our infrastructure helps communities grow by making Ethereum more efficient and accessible. From account abstraction and reducing transaction costs, to rollups and zkEVM, we are building towards an interoperable future."
|
||||
},
|
||||
"explorations": {
|
||||
"title": "Explorations",
|
||||
"description": "We are mapping the emerging zero knowledge ecosystem through collective experimentation. We collaborate, share what we learn, and welcome contributions from around the world."
|
||||
}
|
||||
}
|
||||
36
app/i18n/locales/it/about-page.json
Normal file
36
app/i18n/locales/it/about-page.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"title": "Il nostro team",
|
||||
"description": "PSE è un team di ricerca e sviluppo multidisciplinare supportato dalla Fondazione Ethereum. Creiamo infrastrutture, strumenti e risorse formative open source per integrare la crittografia in applicazioni del mondo reale.",
|
||||
"our-principles-title": "I nostri principi",
|
||||
"principles": [
|
||||
{
|
||||
"title": "01. Crittografia per le persone",
|
||||
"description": [
|
||||
"La crittografia è ovunque: ogni volta che ti connetti a un sito sicuro, accedi con una password o sblocchi il telefono, vedi la crittografia in azione.",
|
||||
"Con la crittografia “programmabile” (come le prove di conoscenza zero, il calcolo multi-party o la crittografia omomorfica) possiamo fare affermazioni verificabili sulle informazioni segrete senza rivelare le informazioni stesse. Ciò può essere applicato alla gestione dell’identità, alla resistenza alla collusione, alla comunicazione anonima e molto altro ancora.",
|
||||
"Stiamo creando una libreria di strumenti di sviluppo, documenti di ricerca e prototipi open source e gratuiti per tutti. Ci auguriamo che le nostre risorse ispirino le persone a innovare le tecnologie di cui le loro comunità hanno bisogno."
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "02. Privacy di default",
|
||||
"description": [
|
||||
"Crediamo che la privacy sia un diritto fondamentale. Vogliamo contribuire alla costruzione di un Internet che si allontani dalle pratiche invasive relative ai dati e offra invece alle persone scelte reali su chi ha accesso alle loro informazioni personali. L’autorizzazione dovrebbe essere specifica per lo scopo, revocabile, informata e non coercitiva.",
|
||||
"Realizziamo strumenti che aiutano le persone ad autenticarsi in modo sicuro, a effettuare transazioni riservate sulla blockchain e a rispettare e preservare la privacy degli utenti."
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "03. Scalabilità per le comunità",
|
||||
"description": [
|
||||
"Le prove di conoscenza zero possono verificare i calcoli in modo rapido ed economico, aiutando i sistemi decentralizzati come Ethereum a diventare più efficienti. Ricerchiamo, progettiamo e condividiamo soluzioni scalabili che chiunque può utilizzare per contribuire a un'infrastruttura pubblica digitale più forte e più pratica.",
|
||||
"Inoltre, facciamo crescere la nostra comunità supportando la prossima generazione di costruttori. Ospitiamo programmi estivi coinvolgenti per studenti e lezioni regolari dal vivo a cui chiunque nel mondo può partecipare e da cui imparare."
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "04. Open-source un bene pubblico",
|
||||
"description": [
|
||||
"Stiamo coltivando un team diversificato e multidisciplinare per esplorare l'ecosistema emergente della conoscenza zero. PSE è composta da programmatori, ingegneri e matematici che lavorano a fianco di creativi e organizzatori di comunità per scoprire in modo collaborativo il potenziale della crittografia programmabile.",
|
||||
"Sperimentiamo apertamente e accogliamo con favore contributi, integrazioni, fork o feedback su tutti i nostri progetti."
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
63
app/i18n/locales/it/common.json
Normal file
63
app/i18n/locales/it/common.json
Normal file
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"menu": {
|
||||
"home": "Home",
|
||||
"projectLibrary": "Libreria progetti",
|
||||
"about": "Chi siamo",
|
||||
"resources": "Risorse",
|
||||
"jobs": "Lavora con noi",
|
||||
"languages": "Lingue {{locale}}"
|
||||
},
|
||||
"footer": {
|
||||
"description": "Privacy + Scaling Explorations è un team multidisciplinare supportato da Ethereum Foundation.",
|
||||
"privacyPolicy": "Privacy Policy",
|
||||
"termsOfUse": "Termini di utilizzo"
|
||||
},
|
||||
"filterOptions": {
|
||||
"random": "Casuale",
|
||||
"asc": "Titolo: A-Z",
|
||||
"desc": "Titolo: Z-A",
|
||||
"relevance": "Rilevanza"
|
||||
},
|
||||
"filterLabels": {
|
||||
"keywords": "Parole chiave",
|
||||
"builtWith": "Sviluppato con",
|
||||
"themes": "Temi selezionati",
|
||||
"projectStatus": "Stato progetto"
|
||||
},
|
||||
"error": {
|
||||
"404": {
|
||||
"title": "404: Pagina non trovata.",
|
||||
"description": "La pagina che stai cercando potrebbe essere stata rimossa o è temporaneamente non disponibile."
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"build": "Sviluppa",
|
||||
"play": "Sperimenta",
|
||||
"research": "Ricerca"
|
||||
},
|
||||
"status": {
|
||||
"archived": "Archiviato",
|
||||
"active": "Attivo"
|
||||
},
|
||||
"sortBy": "Ordina per: {{option}}",
|
||||
"tryItOut": "Prova!",
|
||||
"learnMore": "Scopri di piú",
|
||||
"buildWithThisTool": "Sviluppato con questi strumenti",
|
||||
"deepDiveResearch": "Immergiti più a fondo nella ricerca",
|
||||
"searchProjectPlaceholder": "Ricerca progetto per parola chiave",
|
||||
"close": "Chiudi",
|
||||
"lastUpdatedAt": "Ultimo aggiornamento {{date}}",
|
||||
"projectLibrary": "Libreria progetti",
|
||||
"backToProjectLibrary": "Ritorna alla libreria progetti",
|
||||
"discoverMore": "Scopri di piú",
|
||||
"goToHome": "Va alla pagina iniziale",
|
||||
"noResults": "Nessun risultato trovato.",
|
||||
"noResultsDescription": "Siamo spiacenti, non siamo riusciti a trovare alcun risultato per la tua ricerca. Riprova con parole chiave diverse.",
|
||||
"exploreProjectLibrary": "Esplora la libreria dei progetti",
|
||||
"clearAll": "Rimuovi tutti",
|
||||
"showProjects": "Mostra progetti",
|
||||
"filters": "Filtri",
|
||||
"whatDoYouWantDoToday": "Cosa vuoi fare oggi?",
|
||||
"showingProjects": "Mostrando {{count}} progetti",
|
||||
"showingProjectsWith": "Mostrando {{count}} progetti con:"
|
||||
}
|
||||
9
app/i18n/locales/it/homepage.json
Normal file
9
app/i18n/locales/it/homepage.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"headerTitle": "Privacy + Scaling Explorations",
|
||||
"headerSubtitle": "Crittografia programmabile per persone come te",
|
||||
"whoWeAre": "Chi siamo",
|
||||
"whoWeAreDescription": "PSE è un laboratorio di ricerca che crea strumenti gratuiti che espandono il mondo della crittografia.",
|
||||
"howToPlugIn": "Come contribuire",
|
||||
"howToPlugInDescription": "PSE è un team in crescita di sviluppatori, ricercatori, designer, comunicatori, artisti e organizzatori. Esistono tanti modi per essere coinvolti: puoi provare le nostre app, creare con i nostri strumenti, contribuire a progetti o unirti al nostro team. Ci piacerebbe sentire la tua opinione!",
|
||||
"sayHiToDiscord": "Vieni a trovarci su Discord"
|
||||
}
|
||||
3
app/i18n/locales/it/projects-page.json
Normal file
3
app/i18n/locales/it/projects-page.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"subtitle": "PSE ospita numerosi progetti, dalla ricerca sulla crittografia agli strumenti di sviluppo, ai protocolli e alle applicazioni proof-of-concept."
|
||||
}
|
||||
12
app/i18n/locales/it/resources-page.json
Normal file
12
app/i18n/locales/it/resources-page.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"title": "Risorse",
|
||||
"subtitle": "Questo elenco è stato compilato dalla nostra comunità. Invia un problema sulla nostra pagina Github per aggiungere una risorsa a questo elenco.",
|
||||
"onThisPage": "In questa pagina",
|
||||
"editResources": "Modifica risorse",
|
||||
"nav": {
|
||||
"getInvolved": "Coinvolgiti",
|
||||
"learn": "Impara",
|
||||
"build": "Sviluppa",
|
||||
"design": "Design"
|
||||
}
|
||||
}
|
||||
15
app/i18n/locales/it/what-we-do-section.json
Normal file
15
app/i18n/locales/it/what-we-do-section.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"whatWeDo": "Cosa facciamo",
|
||||
"privacy": {
|
||||
"title": "Privacy",
|
||||
"description": "Crediamo che la privacy sia un bene sociale che dovrebbe essere accessibile a tutti. Ecco perché stiamo creando strumenti open source che aiutano le persone a scegliere cosa, come, quando e dove condividere."
|
||||
},
|
||||
"scaling": {
|
||||
"title": "Scaling",
|
||||
"description": "La nostra infrastruttura aiuta le comunità a crescere rendendo Ethereum più efficiente e accessibile. Dall'astrazione degli account e dalla riduzione dei costi di transazione, ai rollup e zkEVM, stiamo costruendo verso un futuro interoperabile."
|
||||
},
|
||||
"explorations": {
|
||||
"title": "Explorations",
|
||||
"description": "Stiamo mappando l'ecosistema emergente della conoscenza zero attraverso la sperimentazione collettiva. Collaboriamo, condividiamo ciò che impariamo e accogliamo con favore contributi da tutto il mondo."
|
||||
}
|
||||
}
|
||||
36
app/i18n/settings.ts
Normal file
36
app/i18n/settings.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type { InitOptions } from "i18next"
|
||||
|
||||
export const fallbackLng = "en"
|
||||
export const languages = [fallbackLng, "it", "ko", "fr", "zh-CN", "es"] as const
|
||||
export type LocaleTypes = (typeof languages)[number]
|
||||
export const defaultNS = "translation"
|
||||
export const cookieName = "i18next"
|
||||
|
||||
export const LanguageMapping: Record<string, string> = {
|
||||
en: "English",
|
||||
it: "Italiano",
|
||||
ko: "한국어",
|
||||
fr: "Français",
|
||||
"zh-CN": "中文",
|
||||
es: "Español",
|
||||
}
|
||||
|
||||
export const languagesItems: { label: string; value: string }[] =
|
||||
Object.entries(LanguageMapping).map(([value, label]) => {
|
||||
return {
|
||||
label,
|
||||
value,
|
||||
}
|
||||
}) ?? []
|
||||
|
||||
export function getOptions(lng = fallbackLng, ns = defaultNS): InitOptions {
|
||||
return {
|
||||
debug: false,
|
||||
supportedLngs: languages,
|
||||
fallbackLng,
|
||||
lng,
|
||||
fallbackNS: defaultNS,
|
||||
defaultNS,
|
||||
ns,
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
import "@/styles/globals.css"
|
||||
import { Metadata } from "next"
|
||||
import Script from "next/script"
|
||||
|
||||
import { siteConfig } from "@/config/site"
|
||||
import { fontDisplay, fontSans } from "@/lib/fonts"
|
||||
import { SiteFooter } from "@/components/site-footer"
|
||||
import { SiteHeader } from "@/components/site-header"
|
||||
import { TailwindIndicator } from "@/components/tailwind-indicator"
|
||||
|
||||
// import { ThemeProvider } from "@/components/theme-provider"
|
||||
import { languages } from "./i18n/settings"
|
||||
|
||||
export async function generateStaticParams() {
|
||||
return languages.map((language) => ({ language }))
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL("https://appliedzkp.org"),
|
||||
@@ -39,41 +38,9 @@ export const metadata: Metadata = {
|
||||
|
||||
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 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>
|
||||
</>
|
||||
)
|
||||
export default function RootLayout({ children, params }: RootLayoutProps) {
|
||||
return <>{children}</>
|
||||
}
|
||||
|
||||
@@ -1,24 +1,31 @@
|
||||
"use client"
|
||||
|
||||
import "@/styles/globals.css"
|
||||
import React from "react"
|
||||
import Image from "next/image"
|
||||
import Link from "next/link"
|
||||
|
||||
import { LangProps } from "@/types/common"
|
||||
import { fontDisplay, fontSans } from "@/lib/fonts"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { SiteHeader } from "@/components/site-header"
|
||||
import { TailwindIndicator } from "@/components/tailwind-indicator"
|
||||
|
||||
export default function NotFound() {
|
||||
import { useTranslation } from "./i18n/client"
|
||||
|
||||
export default function NotFound({ lang }: LangProps["params"]) {
|
||||
const { t } = useTranslation(lang, "common")
|
||||
|
||||
return (
|
||||
<html
|
||||
lang="en"
|
||||
lang={lang}
|
||||
className={`${fontSans.variable} ${fontDisplay.variable}`}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<head />
|
||||
<body className="min-h-screen">
|
||||
<div className="relative flex h-screen flex-col bg-anakiwa-50">
|
||||
<SiteHeader />
|
||||
<SiteHeader lang={lang} />
|
||||
<div className="container m-auto">
|
||||
<div className="-mt-16 flex flex-col gap-7">
|
||||
<div className="flex flex-col items-center justify-center gap-3 text-center">
|
||||
@@ -33,16 +40,15 @@ export default function NotFound() {
|
||||
</div>
|
||||
<div className="flex flex-col gap-5">
|
||||
<span className="font-display text-2xl font-bold text-tuatara-950 md:text-6xl">
|
||||
404: Page not found.
|
||||
{t("error.404.title")}
|
||||
</span>
|
||||
<span className="font-sans text-base font-normal md:text-lg">
|
||||
The page you are looking for might have been removed, had
|
||||
its name changed or is temporarily unavailable.
|
||||
{t("error.404.description")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Link href="/" className="mx-auto">
|
||||
<Button variant="black">Go to homepage</Button>
|
||||
<Button variant="black">{t("goToHome")}</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -42,7 +42,13 @@ export const Icons = {
|
||||
globe: (props: LucideProps) => (
|
||||
<svg
|
||||
width={props.width}
|
||||
height={typeof props.height === 'number' ? props.height : typeof props.width === 'number' ? props.width - 1 : 24}
|
||||
height={
|
||||
typeof props.height === "number"
|
||||
? props.height
|
||||
: typeof props.width === "number"
|
||||
? props.width - 1
|
||||
: 24
|
||||
}
|
||||
viewBox="0 0 25 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -73,7 +79,13 @@ export const Icons = {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={props.width}
|
||||
height={typeof props.height === 'number' ? props.height : typeof props.width === 'number' ? props.width - 2 : 22}
|
||||
height={
|
||||
typeof props.height === "number"
|
||||
? props.height
|
||||
: typeof props.width === "number"
|
||||
? props.width - 2
|
||||
: 22
|
||||
}
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 22"
|
||||
{...props}
|
||||
@@ -88,7 +100,13 @@ export const Icons = {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={props.width}
|
||||
height={typeof props.height === 'number' ? props.height : typeof props.width === 'number' ? props.width + 2 : 26}
|
||||
height={
|
||||
typeof props.height === "number"
|
||||
? props.height
|
||||
: typeof props.width === "number"
|
||||
? props.width + 2
|
||||
: 26
|
||||
}
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 26"
|
||||
{...props}
|
||||
@@ -102,17 +120,30 @@ export const Icons = {
|
||||
</svg>
|
||||
),
|
||||
gitHub: (props: LucideProps) => (
|
||||
<svg viewBox="0 0 64 64" width={props.size} height={props.size} fill="currentColor" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path fill="currentColor" d="M59.707 16.72c-2.862-4.903-6.743-8.784-11.645-11.646C43.159 2.213 37.806.782 32 .782c-5.805 0-11.16 1.431-16.062 4.292-4.903 2.861-8.784 6.743-11.646 11.646C1.431 21.623 0 26.977 0 32.782c0 6.973 2.034 13.243 6.104 18.812 4.069 5.569 9.326 9.423 15.77 11.562.75.139 1.305.041 1.666-.291a1.63 1.63 0 0 0 .542-1.25l-.021-2.25a372.109 372.109 0 0 1-.021-3.708l-.958.166c-.611.112-1.382.159-2.312.146-.93-.013-1.896-.11-2.896-.292-1-.18-1.931-.596-2.792-1.249-.861-.653-1.472-1.507-1.833-2.562l-.417-.959c-.278-.638-.715-1.347-1.312-2.125-.597-.778-1.201-1.305-1.812-1.583l-.292-.209a3.096 3.096 0 0 1-.542-.5 2.302 2.302 0 0 1-.375-.583c-.083-.195-.014-.355.208-.48.223-.125.625-.186 1.208-.186l.833.124c.556.111 1.243.444 2.063 1a6.731 6.731 0 0 1 2.021 2.166c.639 1.139 1.409 2.007 2.312 2.605.902.597 1.812.895 2.729.895.916 0 1.708-.069 2.375-.208a8.33 8.33 0 0 0 1.875-.625c.25-1.862.931-3.292 2.041-4.292-1.583-.166-3.006-.417-4.271-.75-1.264-.334-2.569-.875-3.916-1.626-1.348-.75-2.466-1.681-3.354-2.791-.889-1.111-1.618-2.57-2.187-4.375-.569-1.806-.854-3.889-.854-6.25 0-3.362 1.097-6.222 3.292-8.584-1.028-2.527-.931-5.361.291-8.499.806-.25 2-.062 3.583.562 1.583.625 2.743 1.16 3.479 1.604.736.444 1.326.82 1.771 1.125 2.584-.722 5.25-1.083 8-1.083s5.417.361 8.001 1.083l1.583-1c1.083-.667 2.361-1.278 3.833-1.834 1.472-.555 2.598-.708 3.376-.458 1.25 3.139 1.361 5.972.333 8.499 2.194 2.361 3.292 5.223 3.292 8.584 0 2.361-.286 4.451-.854 6.27-.569 1.82-1.305 3.277-2.207 4.375-.903 1.098-2.029 2.021-3.376 2.771s-2.653 1.292-3.917 1.625c-1.264.334-2.687.584-4.271.751 1.444 1.25 2.166 3.222 2.166 5.916v8.791c0 .499.174.916.521 1.25.347.333.895.431 1.646.291 6.445-2.138 11.702-5.992 15.771-11.562 4.069-5.569 6.104-11.839 6.104-18.812-.001-5.804-1.433-11.157-4.293-16.06z" />
|
||||
<svg
|
||||
viewBox="0 0 64 64"
|
||||
width={props.size}
|
||||
height={props.size}
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M59.707 16.72c-2.862-4.903-6.743-8.784-11.645-11.646C43.159 2.213 37.806.782 32 .782c-5.805 0-11.16 1.431-16.062 4.292-4.903 2.861-8.784 6.743-11.646 11.646C1.431 21.623 0 26.977 0 32.782c0 6.973 2.034 13.243 6.104 18.812 4.069 5.569 9.326 9.423 15.77 11.562.75.139 1.305.041 1.666-.291a1.63 1.63 0 0 0 .542-1.25l-.021-2.25a372.109 372.109 0 0 1-.021-3.708l-.958.166c-.611.112-1.382.159-2.312.146-.93-.013-1.896-.11-2.896-.292-1-.18-1.931-.596-2.792-1.249-.861-.653-1.472-1.507-1.833-2.562l-.417-.959c-.278-.638-.715-1.347-1.312-2.125-.597-.778-1.201-1.305-1.812-1.583l-.292-.209a3.096 3.096 0 0 1-.542-.5 2.302 2.302 0 0 1-.375-.583c-.083-.195-.014-.355.208-.48.223-.125.625-.186 1.208-.186l.833.124c.556.111 1.243.444 2.063 1a6.731 6.731 0 0 1 2.021 2.166c.639 1.139 1.409 2.007 2.312 2.605.902.597 1.812.895 2.729.895.916 0 1.708-.069 2.375-.208a8.33 8.33 0 0 0 1.875-.625c.25-1.862.931-3.292 2.041-4.292-1.583-.166-3.006-.417-4.271-.75-1.264-.334-2.569-.875-3.916-1.626-1.348-.75-2.466-1.681-3.354-2.791-.889-1.111-1.618-2.57-2.187-4.375-.569-1.806-.854-3.889-.854-6.25 0-3.362 1.097-6.222 3.292-8.584-1.028-2.527-.931-5.361.291-8.499.806-.25 2-.062 3.583.562 1.583.625 2.743 1.16 3.479 1.604.736.444 1.326.82 1.771 1.125 2.584-.722 5.25-1.083 8-1.083s5.417.361 8.001 1.083l1.583-1c1.083-.667 2.361-1.278 3.833-1.834 1.472-.555 2.598-.708 3.376-.458 1.25 3.139 1.361 5.972.333 8.499 2.194 2.361 3.292 5.223 3.292 8.584 0 2.361-.286 4.451-.854 6.27-.569 1.82-1.305 3.277-2.207 4.375-.903 1.098-2.029 2.021-3.376 2.771s-2.653 1.292-3.917 1.625c-1.264.334-2.687.584-4.271.751 1.444 1.25 2.166 3.222 2.166 5.916v8.791c0 .499.174.916.521 1.25.347.333.895.431 1.646.291 6.445-2.138 11.702-5.992 15.771-11.562 4.069-5.569 6.104-11.839 6.104-18.812-.001-5.804-1.433-11.157-4.293-16.06z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
readme: (props: LucideProps) => (
|
||||
<svg
|
||||
width={props.size}
|
||||
height={typeof props.size === 'number'
|
||||
? props.size
|
||||
: typeof props.size === 'number'
|
||||
? props.size + 1 : 17}
|
||||
height={
|
||||
typeof props.size === "number"
|
||||
? props.size
|
||||
: typeof props.size === "number"
|
||||
? props.size + 1
|
||||
: 17
|
||||
}
|
||||
viewBox="0 0 16 17"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -132,7 +163,13 @@ export const Icons = {
|
||||
hand: (props: LucideProps) => (
|
||||
<svg
|
||||
width={props.width}
|
||||
height={typeof props.height === 'number' ? props.height : typeof props.width === 'number' ? props.width - 1 : 19}
|
||||
height={
|
||||
typeof props.height === "number"
|
||||
? props.height
|
||||
: typeof props.width === "number"
|
||||
? props.width - 1
|
||||
: 19
|
||||
}
|
||||
viewBox="0 0 20 19"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -147,7 +184,13 @@ export const Icons = {
|
||||
hammer: (props: LucideProps) => (
|
||||
<svg
|
||||
width={props.width}
|
||||
height={typeof props.height === 'number' ? props.height : typeof props.width === 'number' ? props.width - 1 : 19}
|
||||
height={
|
||||
typeof props.height === "number"
|
||||
? props.height
|
||||
: typeof props.width === "number"
|
||||
? props.width - 1
|
||||
: 19
|
||||
}
|
||||
viewBox="0 0 20 19"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -162,7 +205,13 @@ export const Icons = {
|
||||
checkActive: (props: LucideProps) => (
|
||||
<svg
|
||||
width={props.width}
|
||||
height={typeof props.height === 'number' ? props.height : typeof props.width === 'number' ? props.width + 1 : 15}
|
||||
height={
|
||||
typeof props.height === "number"
|
||||
? props.height
|
||||
: typeof props.width === "number"
|
||||
? props.width + 1
|
||||
: 15
|
||||
}
|
||||
viewBox="0 0 14 15"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
12
components/language-switcher.tsx
Normal file
12
components/language-switcher.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import React from "react"
|
||||
|
||||
export default function LanguageSwitcher() {
|
||||
return (
|
||||
<div>
|
||||
<select>
|
||||
<option value="en">English</option>
|
||||
<option value="es">Español</option>
|
||||
</select>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -6,17 +6,19 @@ import { usePathname } from "next/navigation"
|
||||
import PSELogo from "@/public/logos/header-logo.svg"
|
||||
|
||||
import { NavItem } from "@/types/nav"
|
||||
import { LocaleTypes, fallbackLng } from "@/app/i18n/settings"
|
||||
|
||||
interface MainNavProps {
|
||||
export interface MainNavProps {
|
||||
items: NavItem[]
|
||||
lang?: LocaleTypes
|
||||
}
|
||||
|
||||
export function MainNav({ items }: MainNavProps) {
|
||||
export function MainNav({ items, lang = fallbackLng }: MainNavProps) {
|
||||
const router = usePathname()
|
||||
|
||||
return (
|
||||
<div className="flex gap-6 md:gap-10">
|
||||
<Link href="/" className="flex items-center space-x-2">
|
||||
<Link href={`/${lang}`} className="flex items-center space-x-2">
|
||||
<Image src={PSELogo} alt="PSE Logo" width={32} height={32} />
|
||||
</Link>
|
||||
<nav className="hidden items-center gap-6 md:flex">
|
||||
|
||||
55
components/project/discover-more-projects.tsx
Normal file
55
components/project/discover-more-projects.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
"use client"
|
||||
|
||||
import Link from "next/link"
|
||||
import { projects } from "@/data/projects"
|
||||
import { filterProjects } from "@/state/useProjectFiltersState"
|
||||
|
||||
import { ProjectInterface } from "@/lib/types"
|
||||
import { shuffleArray } from "@/lib/utils"
|
||||
import { ProjectProps } from "@/app/[lang]/projects/[id]/page"
|
||||
import { useTranslation } from "@/app/i18n/client"
|
||||
|
||||
import { Icons } from "../icons"
|
||||
import ProjectCard from "./project-card"
|
||||
|
||||
export default function DiscoverMoreProjects({ project, lang }: ProjectProps) {
|
||||
const { t } = useTranslation(lang, "common")
|
||||
|
||||
const getSuggestedProjects = () => {
|
||||
const projectList = projects.filter((p) => p.id !== project.id)
|
||||
|
||||
const suggestedProject = filterProjects({
|
||||
searchPattern: "",
|
||||
activeFilters: project?.tags,
|
||||
findAnyMatch: true,
|
||||
projects: projectList,
|
||||
})
|
||||
|
||||
// No match return random projects
|
||||
if (suggestedProject?.length < 2) {
|
||||
return shuffleArray(projectList).slice(0, 2)
|
||||
}
|
||||
|
||||
return suggestedProject.slice(0, 2)
|
||||
}
|
||||
|
||||
const suggestedProject = getSuggestedProjects()
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col items-center justify-center gap-14 bg-anakiwa-200 px-6 py-32 md:px-0">
|
||||
<h2 className="text-center text-3xl font-bold">{t("discoverMore")}</h2>
|
||||
<div className="flex flex-col gap-5 md:flex-row">
|
||||
{suggestedProject?.map((project: ProjectInterface) => (
|
||||
<ProjectCard project={project} lang={lang} />
|
||||
))}
|
||||
</div>
|
||||
<Link
|
||||
className="flex items-center gap-2 text-tuatara-950/80 hover:text-tuatara-950"
|
||||
href={`/${lang}/projects`}
|
||||
>
|
||||
<Icons.arrowLeft />
|
||||
<span className="font-sans text-base">{t("backToProjectLibrary")}</span>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -5,8 +5,10 @@ import Image from "next/image"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { VariantProps, cva } from "class-variance-authority"
|
||||
|
||||
import { LangProps } from "@/types/common"
|
||||
import { ProjectInterface, ProjectLinkWebsite } from "@/lib/types"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { LocaleTypes } from "@/app/i18n/settings"
|
||||
|
||||
import { Icons } from "../icons"
|
||||
import { CategoryTag } from "../ui/categoryTag"
|
||||
@@ -48,7 +50,8 @@ export default function ProjectCard({
|
||||
showBanner = false,
|
||||
border = false,
|
||||
className,
|
||||
}: ProjectCardProps) {
|
||||
lang,
|
||||
}: ProjectCardProps & { lang: LocaleTypes }) {
|
||||
const router = useRouter()
|
||||
|
||||
const { id, image, links, name, tldr, tags } = project
|
||||
@@ -64,14 +67,14 @@ export default function ProjectCard({
|
||||
alt={`${name} banner`}
|
||||
width={1200}
|
||||
height={630}
|
||||
className="w-full rounded-t-lg object-cover"
|
||||
className="w-full rounded-t-lg object-cover min-h-[160px]"
|
||||
/>
|
||||
)}
|
||||
<div className="flex h-full flex-col justify-between gap-5 rounded-b-lg bg-white p-5">
|
||||
<div className="flex flex-col justify-start gap-2">
|
||||
<div className="mb-2 flex gap-2">
|
||||
{tags?.themes?.map((theme, index) => {
|
||||
const { label } = ThemesButtonMapping?.[theme]
|
||||
const { label } = ThemesButtonMapping(lang)?.[theme]
|
||||
const icon = TagsIconMapping?.[theme]
|
||||
|
||||
return (
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
} from "@/state/useProjectFiltersState"
|
||||
|
||||
import { ProjectInterface } from "@/lib/types"
|
||||
import { useTranslation } from "@/app/i18n/client"
|
||||
import { LocaleTypes } from "@/app/i18n/settings"
|
||||
|
||||
import { CategoryTag } from "../ui/categoryTag"
|
||||
import { ThemesStatusMapping } from "./project-filters-bar"
|
||||
@@ -25,25 +27,31 @@ const TagsWrapper = ({ label, children }: TagsProps) => {
|
||||
)
|
||||
}
|
||||
|
||||
export function ProjectTags({ project }: { project: ProjectInterface }) {
|
||||
const { label, icon } = ThemesStatusMapping?.[project?.projectStatus] ?? {}
|
||||
type IProjectTags = {
|
||||
project: ProjectInterface
|
||||
lang: LocaleTypes
|
||||
}
|
||||
|
||||
export function ProjectTags({ project, lang }: IProjectTags) {
|
||||
const statusItem = ThemesStatusMapping(lang)
|
||||
const { label, icon } = statusItem?.[project?.projectStatus] ?? {}
|
||||
const { t } = useTranslation(lang, "common")
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{Object.entries(FilterLabelMapping).map(([key, label]) => {
|
||||
{Object.entries(FilterLabelMapping(lang)).map(([key]) => {
|
||||
const keyTags = project?.tags?.[key as ProjectFilter]
|
||||
const hasItems = keyTags && keyTags?.length > 0
|
||||
|
||||
if (key === "themes") return null // ignore themes
|
||||
|
||||
return (
|
||||
hasItems && (
|
||||
<div>
|
||||
<TagsWrapper label={label}>
|
||||
<TagsWrapper label={t(`filterLabels.${key}`)}>
|
||||
<div className="flex flex-wrap gap-[6px]">
|
||||
{keyTags?.map((tag) => {
|
||||
return (
|
||||
<Link href={`/projects?${key}=${tag}`}>
|
||||
<Link href={`${lang}/projects?${key}=${tag}`}>
|
||||
<CategoryTag key={tag} variant="gray">
|
||||
{tag}
|
||||
</CategoryTag>
|
||||
@@ -56,7 +64,7 @@ export function ProjectTags({ project }: { project: ProjectInterface }) {
|
||||
)
|
||||
)
|
||||
})}
|
||||
<TagsWrapper label="Project status">
|
||||
<TagsWrapper label={t("filterLabels.projectStatus")}>
|
||||
<CategoryTag variant="gray" size="default">
|
||||
<div className="flex items-center gap-1">
|
||||
{icon}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client"
|
||||
|
||||
import React from "react"
|
||||
import Link from "next/link"
|
||||
|
||||
@@ -6,35 +8,14 @@ import {
|
||||
ProjectExtraLinkType,
|
||||
ProjectInterface,
|
||||
} from "@/lib/types"
|
||||
import { useTranslation } from "@/app/i18n/client"
|
||||
import { LocaleTypes } from "@/app/i18n/settings"
|
||||
|
||||
import { Icons } from "../icons"
|
||||
|
||||
const ExtraLinkLabelMapping: Record<
|
||||
ProjectExtraLinkType,
|
||||
{
|
||||
label: string
|
||||
icon?: any
|
||||
}
|
||||
> = {
|
||||
buildWith: {
|
||||
label: "Build with this tool",
|
||||
icon: <Icons.hammer />,
|
||||
},
|
||||
play: {
|
||||
label: "Try it out!",
|
||||
icon: <Icons.hand />,
|
||||
},
|
||||
research: {
|
||||
label: "Dive deeper into the research",
|
||||
icon: <Icons.readme />,
|
||||
},
|
||||
learn: {
|
||||
label: "Learn more",
|
||||
},
|
||||
}
|
||||
|
||||
interface ProjectExtraLinksProps {
|
||||
project: ProjectInterface
|
||||
lang: LocaleTypes
|
||||
}
|
||||
|
||||
interface ExtraLinkItemsProps {
|
||||
@@ -42,43 +23,71 @@ interface ExtraLinkItemsProps {
|
||||
links: ActionLinkTypeLink[]
|
||||
}
|
||||
|
||||
const ExtraLinkItems = ({ id, links = [] }: ExtraLinkItemsProps) => {
|
||||
const { label, icon } = ExtraLinkLabelMapping[id]
|
||||
|
||||
if (!links.length) return null // no links hide the section
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
{icon && <span className="text-anakiwa-500">{icon}</span>}
|
||||
<p className="font-sans text-xl font-medium text-tuatara-700">
|
||||
{label}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col items-start gap-2">
|
||||
{links.map(({ label, url }: ActionLinkTypeLink) => {
|
||||
return (
|
||||
<Link
|
||||
href={url}
|
||||
target="_blank"
|
||||
className="flex cursor-pointer items-center gap-1 overflow-hidden border-b-2 border-transparent font-sans font-normal text-tuatara-950 duration-200 ease-in-out hover:border-orange"
|
||||
>
|
||||
{label}
|
||||
<Icons.externalUrl />
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function ProjectExtraLinks({ project }: ProjectExtraLinksProps) {
|
||||
export default function ProjectExtraLinks({
|
||||
project,
|
||||
lang,
|
||||
}: ProjectExtraLinksProps) {
|
||||
const { t } = useTranslation(lang, "common")
|
||||
const { extraLinks = {} } = project
|
||||
const hasExtraLinks = Object.keys(extraLinks).length > 0
|
||||
|
||||
const ExtraLinkLabelMapping: Record<
|
||||
ProjectExtraLinkType,
|
||||
{
|
||||
label: string
|
||||
icon?: any
|
||||
}
|
||||
> = {
|
||||
buildWith: {
|
||||
label: t("buildWithThisTool"),
|
||||
icon: <Icons.hammer />,
|
||||
},
|
||||
play: {
|
||||
label: t("tryItOut"),
|
||||
icon: <Icons.hand />,
|
||||
},
|
||||
research: {
|
||||
label: t("deepDiveResearch"),
|
||||
icon: <Icons.readme />,
|
||||
},
|
||||
learn: {
|
||||
label: t("learnMore"),
|
||||
},
|
||||
}
|
||||
|
||||
if (!hasExtraLinks) return null
|
||||
|
||||
const ExtraLinkItems = ({ id, links = [] }: ExtraLinkItemsProps) => {
|
||||
const { label, icon } = ExtraLinkLabelMapping[id]
|
||||
|
||||
if (!links.length) return null // no links hide the section
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
{icon && <span className="text-anakiwa-500">{icon}</span>}
|
||||
<p className="font-sans text-xl font-medium text-tuatara-700">
|
||||
{label}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col items-start gap-2">
|
||||
{links.map(({ label, url }: ActionLinkTypeLink) => {
|
||||
return (
|
||||
<Link
|
||||
href={url}
|
||||
target="_blank"
|
||||
className="flex cursor-pointer items-center gap-1 overflow-hidden border-b-2 border-transparent font-sans font-normal text-tuatara-950 duration-200 ease-in-out hover:border-orange"
|
||||
>
|
||||
{label}
|
||||
<Icons.externalUrl />
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8 py-4">
|
||||
{Object.entries(ExtraLinkLabelMapping).map(([key]) => {
|
||||
|
||||
@@ -11,10 +11,13 @@ import {
|
||||
ProjectFilter,
|
||||
useProjectFiltersState,
|
||||
} from "@/state/useProjectFiltersState"
|
||||
import i18next from "i18next"
|
||||
import { useDebounce } from "react-use"
|
||||
|
||||
import { ProjectStatusType } from "@/lib/types"
|
||||
import { IThemeStatus, IThemesButton, LangProps } from "@/types/common"
|
||||
import { cn, queryStringToObject } from "@/lib/utils"
|
||||
import { useTranslation } from "@/app/i18n/client"
|
||||
import { LocaleTypes } from "@/app/i18n/settings"
|
||||
|
||||
import { Icons } from "../icons"
|
||||
import Badge from "../ui/badge"
|
||||
@@ -38,49 +41,45 @@ const FilterWrapper = ({ label, children }: FilterWrapperProps) => {
|
||||
)
|
||||
}
|
||||
|
||||
export const ThemesButtonMapping: Record<
|
||||
string,
|
||||
{
|
||||
label: string
|
||||
icon: any
|
||||
export const ThemesButtonMapping = (lang: LocaleTypes): IThemesButton => {
|
||||
const t = i18next.getFixedT(lang, "common")
|
||||
|
||||
return {
|
||||
build: {
|
||||
label: t("tags.build"),
|
||||
icon: <Icons.hammer />,
|
||||
},
|
||||
play: {
|
||||
label: t("tags.play"),
|
||||
icon: <Icons.hand />,
|
||||
},
|
||||
research: {
|
||||
label: t("tags.research"),
|
||||
icon: <Icons.readme />,
|
||||
},
|
||||
}
|
||||
> = {
|
||||
build: {
|
||||
label: "Build",
|
||||
icon: <Icons.hammer />,
|
||||
},
|
||||
play: {
|
||||
label: "Play",
|
||||
icon: <Icons.hand />,
|
||||
},
|
||||
research: {
|
||||
label: "Research",
|
||||
icon: <Icons.readme />,
|
||||
},
|
||||
}
|
||||
|
||||
export const ThemesStatusMapping: Partial<
|
||||
Record<
|
||||
ProjectStatusType,
|
||||
{
|
||||
label: string
|
||||
icon: any
|
||||
}
|
||||
>
|
||||
> = {
|
||||
active: {
|
||||
label: "Active",
|
||||
icon: <Icons.checkActive />,
|
||||
},
|
||||
archived: {
|
||||
label: "Archived",
|
||||
icon: <Icons.archived />,
|
||||
},
|
||||
export const ThemesStatusMapping = (lang: LocaleTypes): IThemeStatus => {
|
||||
const t = i18next.getFixedT(lang, "common")
|
||||
|
||||
return {
|
||||
active: {
|
||||
label: t("status.active"),
|
||||
icon: <Icons.checkActive />,
|
||||
},
|
||||
archived: {
|
||||
label: t("status.archived"),
|
||||
icon: <Icons.archived />,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const FilterButtons = ({
|
||||
searchQuery,
|
||||
lang,
|
||||
}: {
|
||||
lang: LocaleTypes
|
||||
searchQuery?: string
|
||||
}): JSX.Element => {
|
||||
const { activeFilters, onSelectTheme } = useProjectFiltersState(
|
||||
@@ -89,30 +88,33 @@ const FilterButtons = ({
|
||||
|
||||
return (
|
||||
<div className="relative col-span-1 grid grid-cols-3 gap-2 after:absolute after:right-[-25px] after:h-11 after:w-[1px] after:content-none md:col-span-2 md:gap-4 md:after:content-['']">
|
||||
{Object.entries(ThemesButtonMapping).map(([key, { label, icon }]) => {
|
||||
const isActive = activeFilters?.themes?.includes(key)
|
||||
const variant = isActive ? "blue" : "white"
|
||||
return (
|
||||
<Button
|
||||
key={key}
|
||||
variant={variant}
|
||||
size="lg"
|
||||
onClick={() => {
|
||||
onSelectTheme(key, searchQuery ?? "")
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{icon}
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
{Object.entries(ThemesButtonMapping(lang)).map(
|
||||
([key, { label, icon }]) => {
|
||||
const isActive = activeFilters?.themes?.includes(key)
|
||||
const variant = isActive ? "blue" : "white"
|
||||
return (
|
||||
<Button
|
||||
key={key}
|
||||
variant={variant}
|
||||
size="lg"
|
||||
onClick={() => {
|
||||
onSelectTheme(key, searchQuery ?? "")
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{icon}
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function ProjectFiltersBar() {
|
||||
export default function ProjectFiltersBar({ lang }: LangProps["params"]) {
|
||||
const { t } = useTranslation(lang as LocaleTypes, "common")
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
@@ -125,7 +127,7 @@ export default function ProjectFiltersBar() {
|
||||
useEffect(() => {
|
||||
if (!queryString) return
|
||||
router.push(`/projects?${queryString}`)
|
||||
}, [queryString, router])
|
||||
}, [queryString, router, lang])
|
||||
|
||||
useEffect(() => {
|
||||
// set active filters from url
|
||||
@@ -148,7 +150,7 @@ export default function ProjectFiltersBar() {
|
||||
projects,
|
||||
})
|
||||
setSearchQuery("") // clear input
|
||||
router.push("/projects")
|
||||
router.push(`/projects`)
|
||||
}
|
||||
|
||||
useDebounce(
|
||||
@@ -172,7 +174,7 @@ export default function ProjectFiltersBar() {
|
||||
size="sm"
|
||||
onClick={clearAllFilters}
|
||||
>
|
||||
Clear all
|
||||
{t("clearAll")}
|
||||
</Button>
|
||||
<div className="ml-auto">
|
||||
<Button
|
||||
@@ -180,7 +182,7 @@ export default function ProjectFiltersBar() {
|
||||
size="sm"
|
||||
onClick={() => setShowModal(false)}
|
||||
>
|
||||
Show projects
|
||||
{t("showProjects")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -189,7 +191,8 @@ export default function ProjectFiltersBar() {
|
||||
setOpen={setShowModal}
|
||||
>
|
||||
{Object.entries(filters).map(([key, items]) => {
|
||||
const filterLabel = FilterLabelMapping?.[key as ProjectFilter] ?? ""
|
||||
const filterLabel =
|
||||
FilterLabelMapping(lang)?.[key as ProjectFilter] ?? ""
|
||||
const type = FilterTypeMapping?.[key as ProjectFilter]
|
||||
const hasItems = items.length > 0
|
||||
|
||||
@@ -231,7 +234,7 @@ export default function ProjectFiltersBar() {
|
||||
}
|
||||
|
||||
if (type === "button") {
|
||||
const { icon, label } = ThemesButtonMapping[item]
|
||||
const { icon, label } = ThemesButtonMapping(lang)[item]
|
||||
if (!isActive) return null
|
||||
return (
|
||||
<div>
|
||||
@@ -266,18 +269,16 @@ export default function ProjectFiltersBar() {
|
||||
})}
|
||||
</Modal>
|
||||
<div className="flex flex-col gap-6">
|
||||
<span className="text-lg font-medium">
|
||||
What do you want to do today?
|
||||
</span>
|
||||
<span className="text-lg font-medium">{t("whatDoYouWantDoToday")}</span>
|
||||
<div className="grid grid-cols-1 items-center justify-between gap-3 md:grid-cols-5 md:gap-12">
|
||||
<FilterButtons />
|
||||
<FilterButtons lang={lang} />
|
||||
<div className="col-span-1 grid grid-cols-[1fr_auto] gap-2 md:col-span-3 md:gap-3">
|
||||
<Input
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
setSearchQuery(e?.target?.value)
|
||||
}
|
||||
value={searchQuery}
|
||||
placeholder="Search project title or keyword"
|
||||
placeholder={t("searchProjectPlaceholder")}
|
||||
/>
|
||||
<div className="flex items-center gap-3">
|
||||
<Badge value={filterCount}>
|
||||
@@ -290,7 +291,7 @@ export default function ProjectFiltersBar() {
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Image src={FiltersIcon} alt="filter icon" />
|
||||
<span className="hidden md:block">Filters</span>
|
||||
<span className="hidden md:block">{t("filters")}</span>
|
||||
</div>
|
||||
</Button>
|
||||
</Badge>
|
||||
@@ -300,7 +301,7 @@ export default function ProjectFiltersBar() {
|
||||
className="opacity-85 hidden cursor-pointer bg-transparent text-primary hover:opacity-100 disabled:pointer-events-none disabled:opacity-50 md:block"
|
||||
>
|
||||
<div className="flex items-center gap-2 border-b-2 border-black">
|
||||
<span className="text-sm font-medium">Clear all</span>
|
||||
<span className="text-sm font-medium">{t("clearAll")}</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -5,31 +5,35 @@ import Image from "next/image"
|
||||
import NoResultIcon from "@/public/icons/no-result.svg"
|
||||
import { useProjectFiltersState } from "@/state/useProjectFiltersState"
|
||||
|
||||
import { LangProps } from "@/types/common"
|
||||
import { useTranslation } from "@/app/i18n/client"
|
||||
|
||||
import ProjectCard from "./project-card"
|
||||
|
||||
const NoResults = () => {
|
||||
const NoResults = ({ lang }: LangProps["params"]) => {
|
||||
const { t } = useTranslation(lang, "common")
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 pb-40 pt-24 text-center">
|
||||
<div className="mx-auto">
|
||||
<Image className="h-9 w-9" src={NoResultIcon} alt="no result icon" />
|
||||
</div>
|
||||
<span className="font-display text-2xl font-bold text-tuatara-950">
|
||||
No results found.
|
||||
{t("noResults")}
|
||||
</span>
|
||||
<span className="text-lg font-normal text-tuatara-950">
|
||||
{`Sorry, we couldn't find any results for your search. Please try again
|
||||
with different keywords.`}
|
||||
{t("noResultsDescription")}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function ProjectList() {
|
||||
export default function ProjectList({ lang }: LangProps["params"]) {
|
||||
const { projects } = useProjectFiltersState((state) => state)
|
||||
|
||||
const noItems = projects?.length === 0
|
||||
|
||||
if (noItems) return <NoResults />
|
||||
if (noItems) return <NoResults lang={lang} />
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap justify-center gap-6 pb-6">
|
||||
@@ -37,6 +41,7 @@ export default function ProjectList() {
|
||||
<ProjectCard
|
||||
key={project?.id}
|
||||
project={project}
|
||||
lang={lang}
|
||||
showBanner
|
||||
showLinks
|
||||
border
|
||||
|
||||
@@ -6,23 +6,16 @@ import {
|
||||
useProjectFiltersState,
|
||||
} from "@/state/useProjectFiltersState"
|
||||
|
||||
import { LangProps } from "@/types/common"
|
||||
import { useTranslation } from "@/app/i18n/client"
|
||||
|
||||
import { CategoryTag } from "../ui/categoryTag"
|
||||
import { Dropdown } from "../ui/dropdown"
|
||||
|
||||
const labelClass = "h-5 text-xs text-base md:h-6 text-slate-900/70 md:text-lg"
|
||||
|
||||
const projectSortItems: { label: string; value: ProjectSortBy }[] = [
|
||||
{ label: "Random", value: "random" },
|
||||
{ label: "Title: A-Z", value: "asc" },
|
||||
{ label: "Title: Z-A", value: "desc" },
|
||||
{ label: "Relevance", value: "relevance" },
|
||||
]
|
||||
|
||||
const getSortLabel = (sortBy: ProjectSortBy) => {
|
||||
return projectSortItems.find((item) => item.value === sortBy)?.label || sortBy
|
||||
}
|
||||
|
||||
export const ProjectResultBar = () => {
|
||||
export const ProjectResultBar = ({ lang }: LangProps["params"]) => {
|
||||
const { t } = useTranslation(lang, "common")
|
||||
const { activeFilters, toggleFilter, projects, sortProjectBy, sortBy } =
|
||||
useProjectFiltersState((state) => state)
|
||||
|
||||
@@ -30,16 +23,30 @@ export const ProjectResultBar = () => {
|
||||
([_key, values]) => values?.length > 0
|
||||
)
|
||||
|
||||
const resultLabel = haveActiveFilters
|
||||
? `Showing ${projects?.length} projects with:`
|
||||
: `Showing ${projects.length} projects`
|
||||
const resultLabel = t(
|
||||
haveActiveFilters ? "showingProjectsWith" : "showingProjects",
|
||||
{
|
||||
count: projects?.length,
|
||||
}
|
||||
)
|
||||
|
||||
const projectSortItems: { label: string; value: ProjectSortBy }[] = [
|
||||
{ label: t("filterOptions.random"), value: "random" },
|
||||
{ label: t("filterOptions.asc"), value: "asc" },
|
||||
{ label: t("filterOptions.desc"), value: "desc" },
|
||||
{ label: t("filterOptions.relevance"), value: "relevance" },
|
||||
]
|
||||
|
||||
const activeSortOption = t("sortBy", {
|
||||
option: t(`filterOptions.${sortBy}`),
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className={labelClass}>{resultLabel}</span>
|
||||
<Dropdown
|
||||
label={`Sort by: ${getSortLabel(sortBy)}`}
|
||||
label={activeSortOption}
|
||||
defaultItem="random"
|
||||
items={projectSortItems}
|
||||
onChange={(sortBy) => sortProjectBy(sortBy as ProjectSortBy)}
|
||||
|
||||
@@ -1,41 +1,57 @@
|
||||
"use client"
|
||||
|
||||
import Image from "next/image"
|
||||
import { newsItems } from "@/data/news"
|
||||
import PSELearnIcon from "@/public/icons/pseicon1.svg"
|
||||
import PSEEventIcon from "@/public/icons/pseicon2.svg"
|
||||
import PSEPostIcon from "@/public/icons/pseicon3.svg"
|
||||
import { motion } from "framer-motion"
|
||||
import { ArrowUpRight } from "lucide-react"
|
||||
|
||||
import { LangProps } from "@/types/common"
|
||||
import { NewsInterface } from "@/lib/types"
|
||||
import { useTranslation } from "@/app/i18n/client"
|
||||
|
||||
const newsTypes = {
|
||||
learn: {
|
||||
type: "Learn & Share",
|
||||
icon: PSELearnIcon,
|
||||
defaultActionLabel: "Watch",
|
||||
},
|
||||
event: {
|
||||
type: "Event",
|
||||
icon: PSEEventIcon,
|
||||
defaultActionLabel: "Attend",
|
||||
},
|
||||
post: {
|
||||
type: "Blog Post",
|
||||
icon: PSEPostIcon,
|
||||
defaultActionLabel: "Read",
|
||||
},
|
||||
type NewsTypeProps = {
|
||||
type: string
|
||||
icon: any
|
||||
defaultActionLabel: string
|
||||
}
|
||||
type NewsType = {
|
||||
learn: NewsTypeProps
|
||||
event: NewsTypeProps
|
||||
post: NewsTypeProps
|
||||
}
|
||||
|
||||
type NewsType = typeof newsTypes
|
||||
|
||||
const showMove = {
|
||||
initial: { opacity: 0, y: 10 },
|
||||
hover: { opacity: 1, y: 0, transition: { duration: 0.2 } },
|
||||
}
|
||||
|
||||
const News = () => {
|
||||
export const News = ({ lang }: LangProps["params"]) => {
|
||||
const { t } = useTranslation(lang, "news-section")
|
||||
|
||||
const newsTypes = {
|
||||
learn: {
|
||||
type: t("learnAndShare"),
|
||||
icon: PSELearnIcon,
|
||||
defaultActionLabel: t("watch"),
|
||||
},
|
||||
event: {
|
||||
type: "Event",
|
||||
icon: PSEEventIcon,
|
||||
defaultActionLabel: t("attend"),
|
||||
},
|
||||
post: {
|
||||
type: "Blog Post",
|
||||
icon: PSEPostIcon,
|
||||
defaultActionLabel: t("read"),
|
||||
},
|
||||
}
|
||||
|
||||
const newsItems: any[] = t("news", {
|
||||
returnObjects: true,
|
||||
})
|
||||
|
||||
return (
|
||||
<ul className="bg-white">
|
||||
{/* <h4 className="bg-slate-500 px-6 py-2 text-lg font-bold uppercase tracking-wide text-slate-50 backdrop-blur-sm lg:px-20">
|
||||
@@ -84,5 +100,3 @@ const News = () => {
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
export default News
|
||||
|
||||
@@ -3,66 +3,45 @@
|
||||
import Image from "next/image"
|
||||
import PSELogoCircle from "@/public/logos/pse-logo-circle.svg"
|
||||
|
||||
import { LangProps } from "@/types/common"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useTranslation } from "@/app/i18n/client"
|
||||
|
||||
const content = [
|
||||
{
|
||||
title: "Privacy",
|
||||
description:
|
||||
"We believe privacy is a social good that should be accessible to everyone. That's why we're creating open source tools that help people choose what, how, when, and where they share.",
|
||||
className: "privacyDesc",
|
||||
},
|
||||
{
|
||||
title: "Scaling",
|
||||
description:
|
||||
"Our infrastructure helps communities grow by making Ethereum more efficient and accessible. From account abstraction and reducing transaction costs, to rollups and zkEVM, we are building towards an interoperable future.",
|
||||
className: "scalingDesc",
|
||||
},
|
||||
{
|
||||
title: "Explorations",
|
||||
description:
|
||||
"We are mapping the emerging zero knowledge ecosystem through collective experimentation. We collaborate, share what we learn, and welcome contributions from around the world.",
|
||||
className: "explorationsDesc",
|
||||
},
|
||||
]
|
||||
type WhatWeDoContent = {
|
||||
title: string
|
||||
description: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
const WhatWeDo = () => {
|
||||
// useEffect(() => {
|
||||
// const mm = gsap.matchMedia()
|
||||
export const WhatWeDo = ({ lang }: LangProps["params"]) => {
|
||||
const { t } = useTranslation(lang, "what-we-do-section")
|
||||
|
||||
// mm.add("(min-width: 1024px)", () => {
|
||||
// gsap.registerPlugin(ScrollTrigger)
|
||||
// const tl = gsap.timeline({
|
||||
// scrollTrigger: {
|
||||
// trigger: ".badge-start-trigger",
|
||||
// start: "top center",
|
||||
// end: "+=550",
|
||||
// scrub: 1,
|
||||
// // markers: true,
|
||||
// },
|
||||
// })
|
||||
|
||||
// tl.from("#privacy", { fill: "#E1523A" }, "+=0.5")
|
||||
// .to(".badge-right", { y: 50, ease: "ease-out" }, "+=1")
|
||||
// .to("#privacy", { fill: "#111827", ease: "ease-out" }, "+=0.5")
|
||||
// .to(".badge-right", { y: 100, ease: "ease-out" })
|
||||
// .to("#scaling", { fill: "#E1523A", ease: "ease-in" })
|
||||
// .to(".badge-right", { y: 250, ease: "ease-out" }, "+=1")
|
||||
// .to(".badge-right", { y: 350, ease: "ease-out" })
|
||||
// .to("#scaling", { fill: "#111827", ease: "ease-out" })
|
||||
// .to("#explorations", { fill: "#E1523A", ease: "ease-in" })
|
||||
// .to(".badge-right", { y: 550, ease: "ease-out" })
|
||||
// })
|
||||
// }, [])
|
||||
const content: WhatWeDoContent[] = [
|
||||
{
|
||||
title: t("privacy.title"),
|
||||
description: t("privacy.description"),
|
||||
className: "privacyDesc",
|
||||
},
|
||||
{
|
||||
title: t("scaling.title"),
|
||||
description: t("scaling.description"),
|
||||
className: "scalingDesc",
|
||||
},
|
||||
{
|
||||
title: t("explorations.title"),
|
||||
description: t("explorations.description"),
|
||||
className: "explorationsDesc",
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<section className="badge-start-trigger relative grid w-full grid-cols-1 gap-10 overflow-hidden md:grid-cols-2 lg:grid-cols-3 lg:gap-0">
|
||||
<h6 className="hidden w-full justify-start text-xl uppercase text-orange lg:flex lg:justify-center">
|
||||
What we do
|
||||
{t("whatWeDo")}
|
||||
</h6>
|
||||
<div className="flex flex-col gap-10">
|
||||
<h6 className="flex w-full justify-start text-xl uppercase text-orange lg:hidden lg:justify-center">
|
||||
What we do
|
||||
{t("whatWeDo")}
|
||||
</h6>
|
||||
|
||||
<div className="flex flex-col gap-6">
|
||||
@@ -87,159 +66,14 @@ const WhatWeDo = () => {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="lg-order-none relative order-2 hidden md:flex lg:mx-auto">
|
||||
<svg
|
||||
viewBox="0 0 213 213"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="badge-right h-52 lg:w-52"
|
||||
>
|
||||
<g id="pse-logo-badge">
|
||||
<path
|
||||
id="Vector"
|
||||
d="M106.404 212.807C77.9803 212.807 51.263 201.739 31.1635 181.644C11.0684 161.544 0 134.822 0 106.404C0 77.9848 11.0684 51.263 31.1635 31.1635C51.2585 11.0639 77.9803 0 106.404 0C134.827 0 161.544 11.0684 181.644 31.1635C201.739 51.2585 212.807 77.9803 212.807 106.404C212.807 134.827 201.739 161.544 181.644 181.644C161.549 201.739 134.827 212.807 106.404 212.807ZM106.404 5.37301C79.4176 5.37301 54.048 15.8817 34.9649 34.9649C15.8817 54.048 5.37301 79.4176 5.37301 106.404C5.37301 133.39 15.8817 158.759 34.9649 177.842C54.048 196.925 79.4176 207.434 106.404 207.434C133.39 207.434 158.759 196.925 177.842 177.842C196.925 158.759 207.434 133.39 207.434 106.404C207.434 79.4176 196.925 54.048 177.842 34.9649C158.759 15.8817 133.39 5.37301 106.404 5.37301Z"
|
||||
fill="#171C1B"
|
||||
/>
|
||||
<g id="privacy">
|
||||
<path
|
||||
id="Vector_2"
|
||||
d="M30.5641 95.574L16.4062 92.6233L17.4764 87.4876C18.2196 83.928 20.5345 82.1638 23.5927 82.7996L23.9016 82.8623C26.9239 83.4937 28.3567 86.0414 27.6179 89.601L27.206 91.5845L31.2268 92.4218L30.5686 95.5785L30.5641 95.574ZM19.7957 90.0353L24.4702 91.0114L24.9135 88.8756C25.2225 87.3936 24.4837 86.3145 23.0777 86.0235C21.636 85.7235 20.5479 86.4175 20.239 87.8995L19.7957 90.0353Z"
|
||||
/>
|
||||
<path
|
||||
id="Vector_3"
|
||||
d="M33.7553 81.917L20.4258 76.3112L22.6108 71.1128C24.0033 67.7995 26.3585 66.3577 29.3092 67.598L29.6002 67.7189C31.539 68.5338 32.4434 69.98 32.4345 71.8292L38.5776 70.4501L37.1314 73.8933L31.6822 74.9903L30.7688 77.1619L35.009 78.944L33.7598 81.9126L33.7553 81.917ZM24.1914 74.3993L28.4316 76.1814L29.4659 73.7187C30.0077 72.4337 29.533 71.2292 28.2435 70.6919C26.9764 70.1591 25.7675 70.6516 25.2257 71.9367L24.1914 74.3993Z"
|
||||
/>
|
||||
<path
|
||||
id="Vector_4"
|
||||
d="M40.3113 67.9456L28.0742 60.4592L29.7578 57.71L41.9948 65.1964L40.3113 67.9456Z"
|
||||
/>
|
||||
<path
|
||||
id="Vector_5"
|
||||
d="M46.332 59.0753L32.5547 53.165L34.6949 50.6262L45.8305 55.6186L46.2603 55.1082L39.2933 45.1726L41.3709 42.71L49.717 55.0679L46.3365 59.0797L46.332 59.0753Z"
|
||||
/>
|
||||
<path
|
||||
id="Vector_6"
|
||||
d="M52.7696 51.4423L47.4727 37.2934L51.5338 34.0293L64.3484 42.1336L61.7425 44.2246L58.6082 42.1784L53.9382 45.935L55.277 49.423L52.7651 51.4423H52.7696ZM56.2486 40.6515L51.4218 37.4994L50.8532 37.9561L52.9263 43.3246L56.2486 40.6515Z"
|
||||
/>
|
||||
<path
|
||||
id="Vector_7"
|
||||
d="M62.3967 35.1645L62.1997 34.7795C60.5878 31.6094 60.9012 27.1945 65.3294 24.9424C68.8174 23.1693 72.1979 24.0513 74.0695 26.9572L71.1771 28.4259C70.1562 27.0289 68.4234 26.7378 66.6906 27.6199C64.4116 28.7796 63.9728 31.0765 65.2086 33.5123C66.4309 35.9123 68.5712 36.9152 70.9174 35.7242C72.686 34.8242 73.5546 33.2347 73.0442 31.578L75.9188 30.1184C77.0381 33.1004 75.9456 36.5436 72.283 38.4062C67.6578 40.7569 63.8608 38.0615 62.3922 35.169L62.3967 35.1645Z"
|
||||
/>
|
||||
<path
|
||||
id="Vector_8"
|
||||
d="M84.4868 33.1348L83.0271 27.8917L82.3465 28.0797L75.9258 20.6291L79.1048 19.7426L83.6674 25.2052L84.2719 25.035L85.0689 18.0814L88.0777 17.2441L86.7569 26.8529L86.0763 27.0409L87.536 32.2841L84.4868 33.1348Z"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
id="Vector_9"
|
||||
d="M102.894 28.2006L102.796 24.5469L99.3211 24.641L99.2539 22.0888L102.728 21.9947L102.63 18.3232L105.419 18.2471L105.518 21.9186L109.015 21.8246L109.082 24.3768L105.585 24.4708L105.684 28.1245L102.894 28.2006Z"
|
||||
fill="#171C1B"
|
||||
/>
|
||||
<g id="scaling">
|
||||
<path
|
||||
id="Vector_10"
|
||||
d="M118.192 26.1981L121.241 26.8742C121.004 27.9488 121.617 29.0727 123.596 29.5115C125.342 29.8965 126.448 29.3592 126.685 28.2846C126.887 27.3846 126.274 26.7041 124.599 26.1712L123.565 25.8399C120.815 24.9892 119.378 23.0191 119.933 20.5251C120.529 17.8386 122.933 16.6207 126.059 17.3147C129.359 18.0446 130.93 20.3057 130.321 23.0504L127.25 22.3698C127.474 21.3534 126.927 20.2833 125.468 19.9609C124.066 19.652 123.202 20.2833 122.996 21.2057C122.817 22.0116 123.189 22.7773 124.456 23.1579L125.508 23.4937C128.499 24.4384 130.326 26.3727 129.748 28.9652C129.148 31.6696 126.551 32.9457 123.001 32.1577C119.181 31.3114 117.578 28.9204 118.183 26.1981H118.192Z"
|
||||
/>
|
||||
<path
|
||||
id="Vector_11"
|
||||
d="M133.996 27.3194L134.17 26.9254C135.621 23.6792 139.248 21.1449 143.788 23.1732C147.356 24.7672 148.798 27.9507 147.719 31.2327L144.759 29.9119C145.207 28.2373 144.339 26.7104 142.561 25.9179C140.228 24.8747 138.164 25.9761 137.054 28.4701C135.957 30.9283 136.521 33.2252 138.925 34.2998C140.739 35.1103 142.516 34.7834 143.483 33.3461L146.425 34.6625C144.813 37.4072 141.446 38.7236 137.698 37.049C132.961 34.9356 132.67 30.2835 133.996 27.3239V27.3194Z"
|
||||
/>
|
||||
<path
|
||||
id="Vector_12"
|
||||
d="M145.583 40.3281L157.887 31.5566L162.087 34.6372L157.564 49.113L154.869 47.1384L156.038 43.5832L151.202 40.037L148.18 42.231L145.578 40.3236L145.583 40.3281ZM156.906 40.9057L158.706 35.4252L158.12 34.9954L153.468 38.3848L156.906 40.9057Z"
|
||||
/>
|
||||
<path
|
||||
id="Vector_13"
|
||||
d="M159.457 51.2407L169.912 41.417L172.119 43.7677L163.729 51.6571L167.579 55.754L165.515 57.6927L159.457 51.2496V51.2407Z"
|
||||
/>
|
||||
<path
|
||||
id="Vector_14"
|
||||
d="M167.461 60.3691L179.085 51.9648L180.974 54.5752L169.35 62.9795L167.461 60.3691Z"
|
||||
/>
|
||||
<path
|
||||
id="Vector_15"
|
||||
d="M171.942 66.4119L184.815 60.0762L187.148 64.8179L179.007 74.8475L179.128 75.0937L189.655 69.9133L190.989 72.6311L178.116 78.9668L175.766 74.1893L183.906 64.1597L183.785 63.9134L173.258 69.0939L171.938 66.4119H171.942Z"
|
||||
/>
|
||||
<path
|
||||
id="Vector_16"
|
||||
d="M186.102 80.4898L186.523 80.3913C190.047 79.5809 194.22 81.1614 195.29 85.818C196.158 89.5926 194.497 92.7358 191.332 93.7835L190.611 90.6448C192.084 90.0448 192.796 88.3881 192.357 86.4941C191.793 84.0449 189.626 83.0285 187.02 83.6285C184.28 84.2599 182.833 86.2031 183.447 88.8672C183.774 90.2866 184.557 91.3343 185.556 91.8089L186.724 91.5403L185.883 87.8822L188.144 87.3628L189.912 95.0596L187.651 95.579L187.387 94.4283L185.014 94.9745C182.806 94.2313 181.235 92.297 180.595 89.5164C179.444 84.4972 182.636 81.2868 186.102 80.4898Z"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
id="Vector_17"
|
||||
d="M145.925 109.369C145.925 110.749 144.949 111.899 143.646 112.163C143.463 112.204 143.27 112.222 143.073 112.222H133.558C131.982 112.222 130.706 113.498 130.706 115.074V124.589C130.706 125.574 130.209 126.442 129.448 126.953C129.144 127.159 128.799 127.306 128.427 127.382C128.244 127.418 128.051 127.441 127.854 127.441H118.339C116.763 127.441 115.487 128.717 115.487 130.293V139.808C115.487 141.384 114.211 142.66 112.635 142.66H103.12C101.544 142.66 100.268 141.384 100.268 139.808V130.293C100.268 128.717 101.544 127.441 103.12 127.441H112.635C114.211 127.441 115.487 126.165 115.487 124.589V115.074C115.487 113.498 116.763 112.222 118.339 112.222H127.854C129.43 112.222 130.706 110.946 130.706 109.369V97.0026C130.706 88.6162 123.923 81.8149 115.541 81.7835C107.164 81.8149 100.376 88.6162 100.376 97.0026V109.369C100.376 110.946 99.0995 112.222 97.5234 112.222H88.0087C86.4326 112.222 85.1566 113.498 85.1566 115.074V127.441V139.808C85.1566 141.384 83.8805 142.66 82.3044 142.66H72.7897C71.2136 142.66 69.9375 141.384 69.9375 139.808V115.074C69.9375 113.498 71.2136 112.222 72.7897 112.222H82.3044C83.8805 112.222 85.1566 110.946 85.1566 109.369V96.9488C85.1566 80.1671 98.7592 66.5645 115.541 66.5645C132.323 66.5645 145.925 80.1671 145.925 96.9488V109.369Z"
|
||||
fill="#171C1B"
|
||||
/>
|
||||
<path
|
||||
id="Vector_18"
|
||||
d="M106.412 173.343C69.4994 173.343 39.4688 143.312 39.4688 106.399C39.4688 69.4867 69.4994 39.4561 106.412 39.4561C143.325 39.4561 173.355 69.4867 173.355 106.399C173.355 143.312 143.325 173.343 106.412 173.343ZM106.412 42.1426C70.9815 42.1426 42.1553 70.9688 42.1553 106.399C42.1553 141.83 70.9815 170.656 106.412 170.656C141.843 170.656 170.669 141.83 170.669 106.399C170.669 70.9688 141.843 42.1426 106.412 42.1426Z"
|
||||
fill="#171C1B"
|
||||
/>
|
||||
<g id="explorations">
|
||||
<path
|
||||
id="Vector_19"
|
||||
d="M27.1562 151.57L39.2724 143.882L44.032 151.382L41.7753 152.814L38.6993 147.97L36.1292 149.6L39.0351 154.18L36.7784 155.613L33.8725 151.032L31.1009 152.792L34.2486 157.753L31.992 159.186L27.1607 151.57H27.1562Z"
|
||||
/>
|
||||
<path
|
||||
id="Vector_20"
|
||||
d="M34.4732 162.732L43.2626 161.219L45.3401 153.356L47.7267 155.967L46.3028 161.049L46.6073 161.385L51.7698 160.391L54.0892 162.929L45.9446 164.331L43.8044 172.946L41.4044 170.322L42.9447 164.492L42.6268 164.143L36.7881 165.271L34.4688 162.732H34.4732Z"
|
||||
/>
|
||||
<path
|
||||
id="Vector_21"
|
||||
d="M47.6094 176.155L56.3226 164.616L60.5091 167.777C63.4105 169.967 64.015 172.815 62.1344 175.309L61.9464 175.559C60.0882 178.022 57.1689 178.232 54.2719 176.043L52.6555 174.825L50.1795 178.102L47.6094 176.159V176.155ZM57.2136 168.785L54.3346 172.595L56.0764 173.907C57.2853 174.82 58.5748 174.61 59.439 173.468C60.3255 172.291 60.1643 171.01 58.9554 170.097L57.2136 168.785Z"
|
||||
/>
|
||||
<path
|
||||
id="Vector_22"
|
||||
d="M60.8086 185.385L67.4577 172.674L70.3143 174.169L64.9772 184.374L69.9561 186.979L68.6442 189.487L60.8086 185.39V185.385Z"
|
||||
/>
|
||||
<path
|
||||
id="Vector_23"
|
||||
d="M74.5391 184.629L74.6645 184.217C75.6988 180.814 79.2897 177.94 84.121 179.413C88.9343 180.877 90.3492 185.274 89.3104 188.677L89.185 189.089C88.2448 192.174 84.6672 195.42 79.7062 193.907C74.7406 192.393 73.5988 187.714 74.5391 184.629ZM86.1224 187.952C86.9015 185.395 85.8403 183.08 83.2434 182.287C80.6509 181.499 78.4972 182.834 77.7181 185.39C76.9838 187.799 77.9868 190.24 80.5837 191.028C83.1762 191.816 85.3926 190.356 86.1224 187.947V187.952Z"
|
||||
/>
|
||||
<path
|
||||
id="Vector_24"
|
||||
d="M92.9648 196.675L94.2006 182.262L99.8199 182.745C103.402 183.054 105.497 184.85 105.224 188.042L105.197 188.356C105.018 190.451 103.921 191.759 102.157 192.314L105.359 197.736L101.638 197.418L98.9199 192.569L96.5692 192.367L96.1752 196.948L92.9648 196.67V196.675ZM97.1782 185.262L96.7842 189.842L99.4483 190.07C100.836 190.191 101.835 189.368 101.956 187.979C102.072 186.609 101.23 185.611 99.8423 185.49L97.1782 185.262Z"
|
||||
/>
|
||||
<path
|
||||
id="Vector_25"
|
||||
d="M109.172 197.746L112.293 182.961L117.469 182.384L123.939 196.098L120.616 196.47L119.072 193.063L113.112 193.725L112.373 197.388L109.172 197.742V197.746ZM117.899 190.506L115.517 185.254L114.796 185.334L113.663 190.976L117.903 190.506H117.899Z"
|
||||
/>
|
||||
<path
|
||||
id="Vector_26"
|
||||
d="M130.23 194.354L127.019 183.273L123.151 184.392L122.367 181.692L133.221 178.549L134.004 181.249L130.113 182.373L133.324 193.454L130.23 194.35V194.354Z"
|
||||
/>
|
||||
<path
|
||||
id="Vector_27"
|
||||
d="M141.645 190.428L135.855 177.3L138.806 175.997L144.596 189.125L141.645 190.428Z"
|
||||
/>
|
||||
<path
|
||||
id="Vector_28"
|
||||
d="M145.805 181.087L145.568 180.725C143.625 177.743 143.728 173.149 147.963 170.395C152.177 167.65 156.457 169.388 158.396 172.37L158.633 172.732C160.393 175.432 160.581 180.259 156.233 183.089C151.886 185.919 147.565 183.787 145.805 181.087ZM155.79 174.349C154.33 172.11 151.877 171.434 149.602 172.916C147.332 174.398 146.969 176.905 148.429 179.144C149.804 181.253 152.316 182.055 154.59 180.572C156.865 179.09 157.165 176.458 155.79 174.349Z"
|
||||
/>
|
||||
<path
|
||||
id="Vector_29"
|
||||
d="M167.051 174.701L156.73 164.734L160.402 160.932L172.594 165.195L172.787 164.998L164.347 156.849L166.451 154.673L176.772 164.635L173.073 168.468L160.881 164.205L160.689 164.402L169.129 172.552L167.056 174.701H167.051Z"
|
||||
/>
|
||||
<path
|
||||
id="Vector_30"
|
||||
d="M176.42 157.841L178.095 155.203C179.022 155.794 180.289 155.597 181.377 153.887C182.335 152.378 182.205 151.156 181.274 150.565C180.495 150.068 179.644 150.417 178.574 151.81L177.911 152.669C176.174 154.966 173.832 155.642 171.679 154.272C169.355 152.799 169.028 150.122 170.747 147.417C172.561 144.565 175.22 143.857 177.593 145.362L175.91 148.017C175.032 147.457 173.841 147.61 173.04 148.872C172.27 150.081 172.57 151.111 173.367 151.617C174.065 152.06 174.911 151.971 175.7 150.91L176.371 150.032C178.279 147.542 180.719 146.486 182.958 147.905C185.295 149.392 185.613 152.266 183.661 155.338C181.565 158.638 178.771 159.332 176.416 157.836L176.42 157.841Z"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
id="Vector_31"
|
||||
d="M185.508 119.682L184.648 123.893C184.506 124.589 184.954 125.267 185.65 125.409L189.861 126.269C190.557 126.411 191.235 125.963 191.377 125.267L192.237 121.056C192.379 120.36 191.931 119.682 191.235 119.54L187.024 118.68C186.328 118.538 185.65 118.986 185.508 119.682Z"
|
||||
fill="#171C1B"
|
||||
/>
|
||||
<path
|
||||
id="Vector_32"
|
||||
d="M25.7675 118.681L21.5559 119.541C20.8606 119.683 20.412 120.362 20.554 121.057L21.4139 125.268C21.5558 125.964 22.2346 126.412 22.93 126.27L27.1415 125.411C27.8369 125.269 28.2855 124.59 28.1435 123.894L27.2836 119.683C27.1416 118.988 26.4628 118.539 25.7675 118.681Z"
|
||||
fill="#171C1B"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
<div className="lg-order-none relative order-2 hidden md:flex self-start lg:mx-auto">
|
||||
<Image
|
||||
src="/logos/pse-logo-circle.svg"
|
||||
height={213}
|
||||
width={213}
|
||||
alt="pse logo circle"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default WhatWeDo
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
"use client"
|
||||
|
||||
import Image from "next/image"
|
||||
import Link from "next/link"
|
||||
import PSELogo from "@/public/logos/pse-logo-circle.svg"
|
||||
|
||||
import { LangProps } from "@/types/common"
|
||||
import { siteConfig } from "@/config/site"
|
||||
import {
|
||||
Discord,
|
||||
@@ -9,6 +12,7 @@ import {
|
||||
Mirror,
|
||||
Twitter,
|
||||
} from "@/components/svgs/social-medias"
|
||||
import { useTranslation } from "@/app/i18n/client"
|
||||
|
||||
import { ArrowRightUp } from "./svgs/arrows"
|
||||
|
||||
@@ -19,37 +23,42 @@ const SocialMedia = ({ label }: { label: string }) => {
|
||||
</span>
|
||||
)
|
||||
}
|
||||
export function SiteFooter() {
|
||||
|
||||
export function SiteFooter({ lang }: LangProps["params"]) {
|
||||
const { t } = useTranslation(lang, "common")
|
||||
|
||||
return (
|
||||
<footer className="flex flex-col">
|
||||
<div className="flex flex-col divide-y divide-tuatara-200 px-8">
|
||||
<div className="flex w-full flex-col items-center gap-5 py-8">
|
||||
<Image src={PSELogo} alt="logo" width={133} height={133} />
|
||||
<h1 className="py-2 text-center font-sans text-sm font-normal text-tuatara-950">
|
||||
Privacy + Scaling Explorations is a multidisciplinary team supported
|
||||
by the Ethereum Foundation.
|
||||
{t("footer.description")}
|
||||
</h1>
|
||||
</div>
|
||||
<div className="flex w-full flex-col items-center gap-5 py-8 text-base font-medium md:flex-row md:justify-center">
|
||||
<Link href={"/"} className="link px-[10px]">
|
||||
HOME
|
||||
<Link href={`/${lang}`} className="link px-[10px] uppercase">
|
||||
{t("menu.home")}
|
||||
</Link>
|
||||
<Link href={"/projects"} className="link px-[10px]">
|
||||
PROJECT LIBRARY
|
||||
<Link href={`/${lang}/projects`} className="link px-[10px] uppercase">
|
||||
{t("menu.projectLibrary")}
|
||||
</Link>
|
||||
<Link href={"/about"} className="link px-[10px]">
|
||||
ABOUT
|
||||
<Link href={`/${lang}/about`} className="link px-[10px] uppercase">
|
||||
{t("menu.about")}
|
||||
</Link>
|
||||
<Link href={"/resources"} className="link px-[10px]">
|
||||
RESOURCES
|
||||
<Link
|
||||
href={`/${lang}/resources`}
|
||||
className="link px-[10px] uppercase"
|
||||
>
|
||||
{t("menu.resources")}
|
||||
</Link>
|
||||
<Link
|
||||
href={siteConfig.links.jobs}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="link flex items-center gap-5 px-[10px]"
|
||||
className="link flex items-center gap-5 px-[10px] uppercase"
|
||||
>
|
||||
JOBS
|
||||
{t("menu.jobs")}
|
||||
<ArrowRightUp color="black" />
|
||||
</Link>
|
||||
</div>
|
||||
@@ -101,7 +110,7 @@ export function SiteFooter() {
|
||||
rel="noreferrer"
|
||||
>
|
||||
<span className="font-sans font-normal leading-[21px]">
|
||||
Privacy Policy
|
||||
{t("footer.privacyPolicy")}
|
||||
</span>
|
||||
</Link>
|
||||
<Link
|
||||
@@ -110,12 +119,14 @@ export function SiteFooter() {
|
||||
rel="noreferrer"
|
||||
>
|
||||
<span className="font-sans font-normal leading-[21px]">
|
||||
Terms of use
|
||||
{t("footer.termsOfUse")}
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
<span className="py-2 font-sans font-normal text-white opacity-50 ">
|
||||
Last updated June 8, 2023
|
||||
{t("lastUpdatedAt", {
|
||||
date: "January 16, 2024",
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -3,20 +3,73 @@
|
||||
import { useState } from "react"
|
||||
import NextImage from "next/image"
|
||||
import NextLink from "next/link"
|
||||
import Link from "next/link"
|
||||
import ArrowVector from "@/public/icons/arrow-right-up.svg"
|
||||
import CloseVector from "@/public/icons/close-fill.svg"
|
||||
import HeaderVector from "@/public/icons/menu-burger.svg"
|
||||
|
||||
import { LangProps } from "@/types/common"
|
||||
import { siteConfig } from "@/config/site"
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
Discord,
|
||||
Github,
|
||||
Mirror,
|
||||
Twitter,
|
||||
} from "@/components/svgs/social-medias"
|
||||
import { useTranslation } from "@/app/i18n/client"
|
||||
import { LanguageMapping, languagesItems } from "@/app/i18n/settings"
|
||||
|
||||
export function SiteHeaderMobile() {
|
||||
import { Icons } from "./icons"
|
||||
|
||||
const LanguageSwitcher = ({ lang }: LangProps["params"]) => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
if (!siteConfig?.showLanguageSwitcher) return null
|
||||
|
||||
return (
|
||||
<div className="flex flex-col border-b-2 border-white px-[14px] py-[16px] pt-0">
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsOpen(!isOpen)
|
||||
}}
|
||||
type="button"
|
||||
className="flex items-center gap-2 uppercase"
|
||||
>
|
||||
<Icons.globe className="text-white" size={22} fill="white" />
|
||||
<span className="text-base font-medium uppercase text-white">
|
||||
{LanguageMapping[lang] ?? LanguageMapping["en"]}
|
||||
</span>
|
||||
<Icons.arrowDown />
|
||||
</button>
|
||||
{isOpen && (
|
||||
<div className="ml-8 mt-4 flex flex-col gap-1">
|
||||
{languagesItems?.map(({ label, value: languageKey }, index) => {
|
||||
const isActive = languageKey === lang
|
||||
return (
|
||||
<Link
|
||||
className={cn(
|
||||
"py-2 uppercase",
|
||||
isActive
|
||||
? "font-medium text-anakiwa-500"
|
||||
: "font-normal text-white"
|
||||
)}
|
||||
href={`/${languageKey}`}
|
||||
key={index}
|
||||
>
|
||||
{label}
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function SiteHeaderMobile({ lang }: LangProps["params"]) {
|
||||
const [header, setHeader] = useState(false)
|
||||
const { t } = useTranslation(lang, "common")
|
||||
|
||||
return (
|
||||
<div className="flex items-center md:hidden">
|
||||
@@ -48,42 +101,44 @@ export function SiteHeaderMobile() {
|
||||
</div>
|
||||
<div className="flex w-full flex-col gap-5 px-[16px] text-base font-medium">
|
||||
<NextLink
|
||||
href={"/"}
|
||||
href={`/${lang}`}
|
||||
onClick={() => setHeader(false)}
|
||||
className="border-y-2 border-white p-[16px]"
|
||||
className="border-y-2 border-white p-[16px] uppercase"
|
||||
>
|
||||
HOME
|
||||
{t("menu.home")}
|
||||
</NextLink>
|
||||
<NextLink
|
||||
onClick={() => setHeader(false)}
|
||||
href={"/projects"}
|
||||
className="border-b-2 border-white p-[16px] pt-0"
|
||||
href={`${lang}/projects`}
|
||||
className="border-b-2 border-white p-[16px] pt-0 uppercase"
|
||||
>
|
||||
PROJECT LIBRARY
|
||||
{t("menu.projectLibrary")}
|
||||
</NextLink>
|
||||
<NextLink
|
||||
onClick={() => setHeader(false)}
|
||||
href={"/about"}
|
||||
className="border-b-2 border-white p-[16px] pt-0"
|
||||
href={`/${lang}/about`}
|
||||
className="border-b-2 border-white p-[16px] pt-0 uppercase"
|
||||
>
|
||||
ABOUT
|
||||
{t("menu.about")}
|
||||
</NextLink>
|
||||
<NextLink
|
||||
onClick={() => setHeader(false)}
|
||||
href={"/resources"}
|
||||
className="border-b-2 border-white p-[16px] pt-0"
|
||||
href={`/${lang}/resources`}
|
||||
className="border-b-2 border-white p-[16px] pt-0 uppercase"
|
||||
>
|
||||
RESOURCES
|
||||
{t("menu.resources")}
|
||||
</NextLink>
|
||||
<NextLink
|
||||
href={siteConfig.links.jobs}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="flex items-center gap-5 border-b-2 border-white p-[16px] pt-0"
|
||||
className="flex items-center gap-5 border-b-2 border-white p-[16px] pt-0 uppercase"
|
||||
>
|
||||
JOBS
|
||||
{t("menu.jobs")}
|
||||
<NextImage src={ArrowVector} alt="logo" width={24} height={24} />
|
||||
</NextLink>
|
||||
|
||||
<LanguageSwitcher lang={lang} />
|
||||
</div>
|
||||
<div className="flex h-full w-full flex-col items-center justify-end gap-5 py-[40px] text-sm">
|
||||
<div className="flex gap-5">
|
||||
@@ -118,10 +173,14 @@ export function SiteHeaderMobile() {
|
||||
</NextLink>
|
||||
</div>
|
||||
<div className="flex gap-5 text-white">
|
||||
<h1>Privacy Policy</h1>
|
||||
<h1>Terms of use</h1>
|
||||
<h1>{t("footer.privacyPolicy")}</h1>
|
||||
<h1>{t("footer.termsOfUse")}</h1>
|
||||
</div>
|
||||
<h1 className="text-gray-400">Last updated June 8, 2023</h1>
|
||||
<h1 className="text-center text-gray-400">
|
||||
{t("lastUpdatedAt", {
|
||||
date: "January 16, 2024",
|
||||
})}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,54 +1,74 @@
|
||||
"use client"
|
||||
|
||||
import Link from "next/link"
|
||||
import { useRouter } from "next/navigation"
|
||||
import * as DropdownMenu from "@radix-ui/react-dropdown-menu"
|
||||
import i18next from "i18next"
|
||||
import { Trans } from "react-i18next/TransWithoutContext"
|
||||
|
||||
import { siteConfig } from "@/config/site"
|
||||
import { MainNav } from "@/components/main-nav"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { MainNav, MainNavProps } from "@/components/main-nav"
|
||||
import { useTranslation } from "@/app/i18n/client"
|
||||
import {
|
||||
Discord,
|
||||
Github,
|
||||
Mirror,
|
||||
Twitter,
|
||||
} from "@/components/svgs/social-medias"
|
||||
LanguageMapping,
|
||||
LocaleTypes,
|
||||
languagesItems,
|
||||
} from "@/app/i18n/settings"
|
||||
|
||||
import { Icons } from "./icons"
|
||||
import { SiteHeaderMobile } from "./site-header-mobile"
|
||||
import { Dropdown } from "./ui/dropdown"
|
||||
|
||||
type SiteHeaderProps = {
|
||||
lang: LocaleTypes
|
||||
}
|
||||
|
||||
export function SiteHeader({ lang }: SiteHeaderProps) {
|
||||
const { t: i18n } = useTranslation(lang, "common")
|
||||
const MAIN_NAV: MainNavProps["items"] = [
|
||||
{
|
||||
title: i18n("menu.home"),
|
||||
href: `/${lang}`,
|
||||
},
|
||||
{
|
||||
title: i18n("menu.projectLibrary"),
|
||||
href: `/${lang}/projects`,
|
||||
},
|
||||
{
|
||||
title: i18n("menu.about"),
|
||||
href: `/${lang}/about`,
|
||||
},
|
||||
{
|
||||
title: i18n("menu.resources"),
|
||||
href: `/${lang}/resources`,
|
||||
},
|
||||
]
|
||||
|
||||
export function SiteHeader() {
|
||||
return (
|
||||
<header className="sticky top-0 z-40 w-full bg-white px-6 shadow-sm xl:px-20">
|
||||
<div className="flex h-16 justify-between space-x-4 sm:space-x-0">
|
||||
<MainNav items={siteConfig.mainNav} />
|
||||
<SiteHeaderMobile />
|
||||
<div className="hidden flex-1 items-center justify-end space-x-4 md:flex">
|
||||
<nav className="flex items-center gap-5 space-x-1">
|
||||
<Link
|
||||
href={siteConfig.links.twitter}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<Twitter color="black" />
|
||||
</Link>
|
||||
<Link
|
||||
href={siteConfig.links.discord}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<Discord color="black" />
|
||||
</Link>
|
||||
<Link
|
||||
href={siteConfig.links.github}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<Github color="black" />
|
||||
</Link>
|
||||
<Link
|
||||
href={siteConfig.links.articles}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<Mirror color="black" />
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
<div className="flex h-16 items-center justify-between space-x-4 sm:space-x-0">
|
||||
<MainNav items={MAIN_NAV} lang={lang} />
|
||||
<SiteHeaderMobile lang={lang} />
|
||||
{siteConfig?.showLanguageSwitcher && (
|
||||
<div className="hidden outline-none md:block">
|
||||
<Dropdown
|
||||
label={
|
||||
<div className="flex items-center gap-1">
|
||||
<Icons.globe size={22} />
|
||||
<span className="!text-base !font-normal text-tuatara-950">
|
||||
{LanguageMapping[lang] ?? LanguageMapping["en"]}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
defaultItem={lang}
|
||||
items={languagesItems}
|
||||
onChange={(lang) => {
|
||||
window?.location?.replace(`/${lang}`)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
|
||||
@@ -26,10 +26,14 @@ const Accordion = ({
|
||||
<RadixAccordion.Root
|
||||
type={type as any}
|
||||
defaultValue={defaultValue}
|
||||
collapsible
|
||||
collapsible={true}
|
||||
>
|
||||
{items?.map(({ label, children, value }) => (
|
||||
<RadixAccordion.Item className="group" value={value}>
|
||||
{items?.map(({ label, children, value }, accordionIndex) => (
|
||||
<RadixAccordion.Item
|
||||
className="group"
|
||||
value={value}
|
||||
key={accordionIndex}
|
||||
>
|
||||
<RadixAccordion.Trigger className="flex w-full items-center justify-between border-t border-t-black py-6 ring-0 focus:outline-none">
|
||||
<span className="block text-left font-sans text-base font-bold uppercase tracking-[3.36px] text-black md:text-xl md:tracking-[4.2px]">
|
||||
{label}
|
||||
|
||||
@@ -11,7 +11,7 @@ interface DropdownItemProps {
|
||||
}
|
||||
|
||||
interface DropdownProps {
|
||||
label: string
|
||||
label: React.ReactNode
|
||||
items?: DropdownItemProps[]
|
||||
defaultItem?: string | number
|
||||
onChange?: (value: DropdownItemProps["value"]) => void
|
||||
@@ -53,7 +53,7 @@ const Dropdown = ({
|
||||
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content
|
||||
className="max-h-[250px] min-w-[136px] overflow-scroll rounded-md border border-tuatara-200 bg-white py-2"
|
||||
className="z-[50] max-h-[250px] min-w-[136px] overflow-scroll rounded-md border border-tuatara-200 bg-white py-2"
|
||||
sideOffset={5}
|
||||
>
|
||||
{items?.map((item, index) => {
|
||||
|
||||
@@ -1,27 +1,10 @@
|
||||
export type SiteConfig = typeof siteConfig
|
||||
|
||||
export const siteConfig = {
|
||||
showLanguageSwitcher: false, // TODO: enable when we have more languages
|
||||
name: "Privacy & Scaling Explorations",
|
||||
description:
|
||||
"Enhancing Ethereum through cryptographic research and collective experimentation.",
|
||||
mainNav: [
|
||||
{
|
||||
title: "Home",
|
||||
href: "/",
|
||||
},
|
||||
{
|
||||
title: "Project Library",
|
||||
href: "/projects",
|
||||
},
|
||||
{
|
||||
title: "About",
|
||||
href: "/about",
|
||||
},
|
||||
{
|
||||
title: "Resources",
|
||||
href: "/resources",
|
||||
},
|
||||
],
|
||||
links: {
|
||||
twitter: "https://twitter.com/privacyscaling",
|
||||
github: "https://github.com/privacy-scaling-explorations",
|
||||
|
||||
28
data/news.ts
28
data/news.ts
@@ -1,28 +0,0 @@
|
||||
import { NewsInterface } from "@/lib/types"
|
||||
|
||||
export const newsItems: NewsInterface[] = [
|
||||
{
|
||||
type: "post",
|
||||
title: "Learnings from the KZG Ceremony",
|
||||
action: {
|
||||
label: "Read",
|
||||
url: "https://mirror.xyz/privacy-scaling-explorations.eth/naTdx-u7kyirczTLSAnWwH6ZdedfTQu1yCWQj1m_n-E",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "learn",
|
||||
title: "Public events hosted by PSE members to learn about each other’s projects, share ideas, and get feedback.",
|
||||
action: {
|
||||
label: "See full schedule",
|
||||
url: "https://pse-team.notion.site/50dcf22c5191485e93406a902ae9e93b?v=453023f8227646dd949abc34a7a4a138&pvs=4",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "learn",
|
||||
title: "Folding Circom Circuit: A ZKML Case Study by Dr. Cathie So",
|
||||
action: {
|
||||
label: "Watch",
|
||||
url: "https://www.youtube.com/live/jb6HDEtY4CI",
|
||||
},
|
||||
},
|
||||
]
|
||||
48
middleware.ts
Normal file
48
middleware.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import acceptLanguage from "accept-language"
|
||||
|
||||
import { cookieName, fallbackLng, languages } from "./app/i18n/settings"
|
||||
|
||||
acceptLanguage.languages(languages as any)
|
||||
|
||||
export const config = {
|
||||
//matcher: "/:lang*",
|
||||
matcher: ["/((?!api|_next/static|_next/image|assets|favicon.ico|sw.js).*)"],
|
||||
}
|
||||
const PUBLIC_FILE = /\.(.*)$/
|
||||
|
||||
export function middleware(req: any) {
|
||||
const COOKIE_NAME = cookieName ?? "i18next"
|
||||
let lang
|
||||
if (req.cookies.has(COOKIE_NAME))
|
||||
lang = acceptLanguage.get(req.cookies.get(COOKIE_NAME).value)
|
||||
if (!lang) lang = acceptLanguage.get(req.headers.get("Accept-Language"))
|
||||
if (!lang) lang = fallbackLng
|
||||
|
||||
// Keep the file from public folder
|
||||
if (PUBLIC_FILE.test(req.nextUrl.pathname)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Redirect if lang in path is not supported
|
||||
if (
|
||||
!languages.some((loc) => req.nextUrl.pathname.startsWith(`/${loc}`)) &&
|
||||
!req.nextUrl.pathname.startsWith("/_next")
|
||||
) {
|
||||
return NextResponse.redirect(
|
||||
new URL(`/${lang}${req.nextUrl.pathname}`, req.url)
|
||||
)
|
||||
}
|
||||
|
||||
if (req.headers.has("referer")) {
|
||||
const refererUrl = new URL(req.headers.get("referer"))
|
||||
const langInReferer = languages.find((l) =>
|
||||
refererUrl.pathname.startsWith(`/${l}`)
|
||||
)
|
||||
const response = NextResponse.next()
|
||||
if (langInReferer) response.cookies.set(COOKIE_NAME, langInReferer)
|
||||
return response
|
||||
}
|
||||
|
||||
return NextResponse.next()
|
||||
}
|
||||
@@ -26,16 +26,22 @@
|
||||
"@radix-ui/react-dialog": "^1.0.4",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.5",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"accept-language": "^3.0.18",
|
||||
"class-variance-authority": "^0.4.0",
|
||||
"clsx": "^1.2.1",
|
||||
"framer-motion": "^10.12.17",
|
||||
"fuse.js": "^6.6.2",
|
||||
"gsap": "^3.12.1",
|
||||
"i18next": "^23.7.16",
|
||||
"i18next-browser-languagedetector": "^7.2.0",
|
||||
"i18next-resources-to-backend": "^1.2.0",
|
||||
"lucide-react": "0.105.0-alpha.4",
|
||||
"next": "^13.4.10",
|
||||
"next-themes": "^0.2.1",
|
||||
"react": "^18.2.0",
|
||||
"react-cookie": "^7.0.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-i18next": "^14.0.0",
|
||||
"react-markdown": "^8.0.7",
|
||||
"react-use": "^17.4.0",
|
||||
"remark-gfm": "^3.0.1",
|
||||
|
||||
135
pnpm-lock.yaml
generated
135
pnpm-lock.yaml
generated
@@ -23,6 +23,9 @@ dependencies:
|
||||
'@radix-ui/react-slot':
|
||||
specifier: ^1.0.2
|
||||
version: 1.0.2(@types/react@18.2.7)(react@18.2.0)
|
||||
accept-language:
|
||||
specifier: ^3.0.18
|
||||
version: 3.0.18
|
||||
class-variance-authority:
|
||||
specifier: ^0.4.0
|
||||
version: 0.4.0(typescript@4.9.5)
|
||||
@@ -38,6 +41,15 @@ dependencies:
|
||||
gsap:
|
||||
specifier: ^3.12.1
|
||||
version: 3.12.1
|
||||
i18next:
|
||||
specifier: ^23.7.16
|
||||
version: 23.7.16
|
||||
i18next-browser-languagedetector:
|
||||
specifier: ^7.2.0
|
||||
version: 7.2.0
|
||||
i18next-resources-to-backend:
|
||||
specifier: ^1.2.0
|
||||
version: 1.2.0
|
||||
lucide-react:
|
||||
specifier: 0.105.0-alpha.4
|
||||
version: 0.105.0-alpha.4(react@18.2.0)
|
||||
@@ -50,9 +62,15 @@ dependencies:
|
||||
react:
|
||||
specifier: ^18.2.0
|
||||
version: 18.2.0
|
||||
react-cookie:
|
||||
specifier: ^7.0.1
|
||||
version: 7.0.1(react@18.2.0)
|
||||
react-dom:
|
||||
specifier: ^18.2.0
|
||||
version: 18.2.0(react@18.2.0)
|
||||
react-i18next:
|
||||
specifier: ^14.0.0
|
||||
version: 14.0.0(i18next@23.7.16)(react-dom@18.2.0)(react@18.2.0)
|
||||
react-markdown:
|
||||
specifier: ^8.0.7
|
||||
version: 8.0.7(@types/react@18.2.7)(react@18.2.0)
|
||||
@@ -282,6 +300,13 @@ packages:
|
||||
dependencies:
|
||||
regenerator-runtime: 0.13.11
|
||||
|
||||
/@babel/runtime@7.23.8:
|
||||
resolution: {integrity: sha512-Y7KbAP984rn1VGMbGqKmBLio9V7y5Je9GvU4rQPCPinCyNfUcToxIXl06d59URp/F3LwinvODxab5N/G6qggkw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
regenerator-runtime: 0.14.1
|
||||
dev: false
|
||||
|
||||
/@babel/template@7.21.9:
|
||||
resolution: {integrity: sha512-MK0X5k8NKOuWRamiEfc3KEJiHMTkGZNUjzMipqCGDDc6ijRl/B7RGSKVGncu4Ro/HdyzzY6cmoXuKI2Gffk7vQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -1205,6 +1230,10 @@ packages:
|
||||
tslib: 2.5.2
|
||||
dev: false
|
||||
|
||||
/@types/cookie@0.6.0:
|
||||
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
|
||||
dev: false
|
||||
|
||||
/@types/debug@4.1.8:
|
||||
resolution: {integrity: sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==}
|
||||
dependencies:
|
||||
@@ -1217,6 +1246,13 @@ packages:
|
||||
'@types/unist': 2.0.7
|
||||
dev: false
|
||||
|
||||
/@types/hoist-non-react-statics@3.3.5:
|
||||
resolution: {integrity: sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==}
|
||||
dependencies:
|
||||
'@types/react': 18.2.7
|
||||
hoist-non-react-statics: 3.3.2
|
||||
dev: false
|
||||
|
||||
/@types/js-cookie@2.2.7:
|
||||
resolution: {integrity: sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==}
|
||||
dev: false
|
||||
@@ -1327,6 +1363,13 @@ packages:
|
||||
resolution: {integrity: sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==}
|
||||
dev: false
|
||||
|
||||
/accept-language@3.0.18:
|
||||
resolution: {integrity: sha512-sUofgqBPzgfcF20sPoBYGQ1IhQLt2LSkxTnlQSuLF3n5gPEqd5AimbvOvHEi0T1kLMiGVqPWzI5a9OteBRth3A==}
|
||||
dependencies:
|
||||
bcp47: 1.1.2
|
||||
stable: 0.1.8
|
||||
dev: false
|
||||
|
||||
/acorn-jsx@5.3.2(acorn@8.8.2):
|
||||
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
||||
peerDependencies:
|
||||
@@ -1498,6 +1541,11 @@ packages:
|
||||
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||
dev: false
|
||||
|
||||
/bcp47@1.1.2:
|
||||
resolution: {integrity: sha512-JnkkL4GUpOvvanH9AZPX38CxhiLsXMBicBY2IAtqiVN8YulGDQybUydWA4W6yAMtw6iShtw+8HEF6cfrTHU+UQ==}
|
||||
engines: {node: '>=0.10'}
|
||||
dev: false
|
||||
|
||||
/binary-extensions@2.2.0:
|
||||
resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1673,6 +1721,11 @@ packages:
|
||||
/convert-source-map@1.9.0:
|
||||
resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==}
|
||||
|
||||
/cookie@0.6.0:
|
||||
resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: false
|
||||
|
||||
/copy-to-clipboard@3.3.3:
|
||||
resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==}
|
||||
dependencies:
|
||||
@@ -2554,10 +2607,40 @@ packages:
|
||||
resolution: {integrity: sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng==}
|
||||
dev: false
|
||||
|
||||
/hoist-non-react-statics@3.3.2:
|
||||
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
|
||||
dependencies:
|
||||
react-is: 16.13.1
|
||||
dev: false
|
||||
|
||||
/html-parse-stringify@3.0.1:
|
||||
resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
|
||||
dependencies:
|
||||
void-elements: 3.1.0
|
||||
dev: false
|
||||
|
||||
/hyphenate-style-name@1.0.4:
|
||||
resolution: {integrity: sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==}
|
||||
dev: false
|
||||
|
||||
/i18next-browser-languagedetector@7.2.0:
|
||||
resolution: {integrity: sha512-U00DbDtFIYD3wkWsr2aVGfXGAj2TgnELzOX9qv8bT0aJtvPV9CRO77h+vgmHFBMe7LAxdwvT/7VkCWGya6L3tA==}
|
||||
dependencies:
|
||||
'@babel/runtime': 7.23.8
|
||||
dev: false
|
||||
|
||||
/i18next-resources-to-backend@1.2.0:
|
||||
resolution: {integrity: sha512-8f1l03s+QxDmCfpSXCh9V+AFcxAwIp0UaroWuyOx+hmmv8484GcELHs+lnu54FrNij8cDBEXvEwhzZoXsKcVpg==}
|
||||
dependencies:
|
||||
'@babel/runtime': 7.23.8
|
||||
dev: false
|
||||
|
||||
/i18next@23.7.16:
|
||||
resolution: {integrity: sha512-SrqFkMn9W6Wb43ZJ9qrO6U2U4S80RsFMA7VYFSqp7oc7RllQOYDCdRfsse6A7Cq/V8MnpxKvJCYgM8++27n4Fw==}
|
||||
dependencies:
|
||||
'@babel/runtime': 7.23.8
|
||||
dev: false
|
||||
|
||||
/ieee754@1.2.1:
|
||||
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
||||
dev: false
|
||||
@@ -3746,6 +3829,17 @@ packages:
|
||||
strip-json-comments: 2.0.1
|
||||
dev: false
|
||||
|
||||
/react-cookie@7.0.1(react@18.2.0):
|
||||
resolution: {integrity: sha512-SmDjy2TFp+vS6BGOqW7HyaWKyJzVmIH74uP3mxq6kswlwLJEBtIbhkrioozdvQL9r81yprHYFQkSmcO4HiXPdA==}
|
||||
peerDependencies:
|
||||
react: '>= 16.3.0'
|
||||
dependencies:
|
||||
'@types/hoist-non-react-statics': 3.3.5
|
||||
hoist-non-react-statics: 3.3.2
|
||||
react: 18.2.0
|
||||
universal-cookie: 7.0.1
|
||||
dev: false
|
||||
|
||||
/react-dom@18.2.0(react@18.2.0):
|
||||
resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==}
|
||||
peerDependencies:
|
||||
@@ -3756,6 +3850,26 @@ packages:
|
||||
scheduler: 0.23.0
|
||||
dev: false
|
||||
|
||||
/react-i18next@14.0.0(i18next@23.7.16)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-OCrS8rHNAmnr8ggGRDxjakzihrMW7HCbsplduTm3EuuQ6fyvWGT41ksZpqbduYoqJurBmEsEVZ1pILSUWkHZng==}
|
||||
peerDependencies:
|
||||
i18next: '>= 23.2.3'
|
||||
react: '>= 16.8.0'
|
||||
react-dom: '*'
|
||||
react-native: '*'
|
||||
peerDependenciesMeta:
|
||||
react-dom:
|
||||
optional: true
|
||||
react-native:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/runtime': 7.23.8
|
||||
html-parse-stringify: 3.0.1
|
||||
i18next: 23.7.16
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/react-is@16.13.1:
|
||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||
|
||||
@@ -3906,6 +4020,10 @@ packages:
|
||||
/regenerator-runtime@0.13.11:
|
||||
resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==}
|
||||
|
||||
/regenerator-runtime@0.14.1:
|
||||
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
|
||||
dev: false
|
||||
|
||||
/regexp.prototype.flags@1.5.0:
|
||||
resolution: {integrity: sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -4125,6 +4243,11 @@ packages:
|
||||
resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
|
||||
dev: false
|
||||
|
||||
/stable@0.1.8:
|
||||
resolution: {integrity: sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==}
|
||||
deprecated: 'Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility'
|
||||
dev: false
|
||||
|
||||
/stack-generator@2.0.10:
|
||||
resolution: {integrity: sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==}
|
||||
dependencies:
|
||||
@@ -4514,6 +4637,13 @@ packages:
|
||||
unist-util-visit-parents: 5.1.3
|
||||
dev: false
|
||||
|
||||
/universal-cookie@7.0.1:
|
||||
resolution: {integrity: sha512-6OuX9xELF6dsVJeADJAYNDOxQf/NR3Na5bGCRd+hkysMDkSt79jJ4tdv5OBe+ZgAks3ExHBdCXkD2SjqLyK59w==}
|
||||
dependencies:
|
||||
'@types/cookie': 0.6.0
|
||||
cookie: 0.6.0
|
||||
dev: false
|
||||
|
||||
/update-browserslist-db@1.0.11(browserslist@4.21.7):
|
||||
resolution: {integrity: sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==}
|
||||
hasBin: true
|
||||
@@ -4599,6 +4729,11 @@ packages:
|
||||
vfile-message: 3.1.4
|
||||
dev: false
|
||||
|
||||
/void-elements@3.1.0:
|
||||
resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: false
|
||||
|
||||
/watchpack@2.4.0:
|
||||
resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 206 KiB After Width: | Height: | Size: 206 KiB |
@@ -1,9 +1,11 @@
|
||||
import { projects } from "@/data/projects"
|
||||
import Fuse from "fuse.js"
|
||||
import i18next from "i18next"
|
||||
import { create } from "zustand"
|
||||
|
||||
import { ProjectInterface } from "@/lib/types"
|
||||
import { uniq } from "@/lib/utils"
|
||||
import { LocaleTypes, fallbackLng } from "@/app/i18n/settings"
|
||||
|
||||
export type ProjectSortBy = "random" | "asc" | "desc" | "relevance"
|
||||
export type ProjectFilter = "keywords" | "builtWith" | "themes"
|
||||
@@ -23,10 +25,15 @@ export const SortByFnMapping: Record<
|
||||
relevance: (a, b) => b?.score - a?.score, // sort from most relevant to least relevant
|
||||
}
|
||||
|
||||
export const FilterLabelMapping: Record<ProjectFilter, string> = {
|
||||
keywords: "Keywords",
|
||||
builtWith: "Built with",
|
||||
themes: "Themes selected",
|
||||
export const FilterLabelMapping = (
|
||||
lang?: LocaleTypes
|
||||
): Record<ProjectFilter, string> => {
|
||||
const t = i18next.getFixedT(lang ?? fallbackLng, "common")
|
||||
return {
|
||||
keywords: t("filterLabels.keyword"),
|
||||
builtWith: t("filterLabels.builtWith"),
|
||||
themes: t("filterLabels.themes"),
|
||||
}
|
||||
}
|
||||
|
||||
export const FilterTypeMapping: Record<ProjectFilter, "checkbox" | "button"> = {
|
||||
|
||||
@@ -24,6 +24,6 @@
|
||||
],
|
||||
"strictNullChecks": true
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "middleware.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
26
types/common.ts
Normal file
26
types/common.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { ProjectStatusType } from "@/lib/types"
|
||||
import { LocaleTypes } from "@/app/i18n/settings"
|
||||
|
||||
export interface LangProps {
|
||||
params: {
|
||||
lang: LocaleTypes
|
||||
}
|
||||
}
|
||||
|
||||
export type IThemeStatus = Partial<
|
||||
Record<
|
||||
ProjectStatusType,
|
||||
{
|
||||
label: string
|
||||
icon: any
|
||||
}
|
||||
>
|
||||
>
|
||||
|
||||
export type IThemesButton = Record<
|
||||
string,
|
||||
{
|
||||
label: string
|
||||
icon: any
|
||||
}
|
||||
>
|
||||
Reference in New Issue
Block a user