mirror of
https://github.com/privacy-scaling-explorations/pse.dev.git
synced 2026-01-08 05:43:56 -05:00
feat: graduated projects section (#554)
* feat: graduated projects section
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import { LABELS } from "@/app/labels"
|
||||
import ProjectFiltersBar from "@/components/project/project-filters-bar"
|
||||
import { ProjectGraduated } from "@/components/project/project-graduated"
|
||||
import { ProjectList } from "@/components/project/project-list"
|
||||
import { ProjectResultBar } from "@/components/project/project-result-bar"
|
||||
import { AppContent } from "@/components/ui/app-content"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Metadata } from "next"
|
||||
import { Suspense } from "react"
|
||||
|
||||
@@ -16,26 +16,27 @@ export const metadata: Metadata = {
|
||||
export default async function ProjectsPage() {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<AppContent className="flex flex-col gap-10 py-10 lg:py-16 w-full">
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="lg:w-1/2 mx-auto w-full">
|
||||
<div className="flex flex-col gap-10">
|
||||
<h1 className="dark:text-tuatara-100 text-tuatara-950 text-xl lg:text-3xl font-normal font-sans text-center">
|
||||
{LABELS.PROJECTS_PAGE.TITLE}
|
||||
</h1>
|
||||
<AppContent className="flex flex-col gap-10 pt-10 pb-20 lg:py-16 w-full">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-10">
|
||||
<div className="lg:w-1/2 mx-auto w-full">
|
||||
<div className="flex flex-col gap-10">
|
||||
<h1 className="dark:text-tuatara-100 text-tuatara-950 text-2xl lg:text-3xl lg:leading-[45px] font-normal font-sans text-center">
|
||||
{LABELS.PROJECTS_PAGE.TITLE}
|
||||
</h1>
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<ProjectFiltersBar />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-5">
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<div className="lg:!w-1/2 mx-auto w-full">
|
||||
<ProjectFiltersBar />
|
||||
</div>
|
||||
<ProjectResultBar />
|
||||
</Suspense>
|
||||
</div>
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<ProjectResultBar />
|
||||
</Suspense>
|
||||
</div>
|
||||
<ProjectList />
|
||||
</AppContent>
|
||||
<ProjectGraduated />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -122,6 +122,18 @@ export const LABELS = {
|
||||
ERROR_LOADING_VIDEOS: "Error loading videos",
|
||||
CHECK_OUT_OUR_YOUTUBE:
|
||||
"Check out our YouTube to learn the latest in advanced cryptography.",
|
||||
LEARN_AND_CONNECT: {
|
||||
TITLE: "Learn & Connect",
|
||||
DESCRIPTION:
|
||||
"Join the Ethereum R&D discord to get updates on PSE projects and talk with other privacy builders and researchers about the future of privacy on Ethereum.",
|
||||
ACTION: "Join the ETH R&D Discord",
|
||||
},
|
||||
COLLABORATE_AND_CONTRIBUTE: {
|
||||
TITLE: "Collaborate & Contribute",
|
||||
DESCRIPTION:
|
||||
"Connect with us on the Ethereum Magicians to dive into technical discussions, give feedback, make proposals, and collaborate on research and development.",
|
||||
ACTION: "Join Ethereum Magicians",
|
||||
},
|
||||
},
|
||||
NEWS_SECTION: {
|
||||
RECENT_UPDATES: "Recent Updates",
|
||||
@@ -221,6 +233,8 @@ export const LABELS = {
|
||||
},
|
||||
PROJECTS_PAGE: {
|
||||
TITLE: "Explore Our Projects",
|
||||
GRADUATED_PROJECTS:
|
||||
"Explore graduated projects that have spun out into the ecosystem",
|
||||
},
|
||||
RESEARCH_PAGE: {
|
||||
TITLE: "Explore Our Research",
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { criticalCSS } from "@/lib/critical-css"
|
||||
import "@/globals.css"
|
||||
import { ThemeProvider } from "./components/layouts/ThemeProvider"
|
||||
import { GlobalProviderLayout } from "@/components/layouts/GlobalProviderLayout"
|
||||
@@ -131,9 +130,6 @@ export default function RootLayout({ children }: RootLayoutProps) {
|
||||
`}
|
||||
</Script>
|
||||
<head>
|
||||
{/* Inline critical CSS for immediate render */}
|
||||
<style dangerouslySetInnerHTML={{ __html: criticalCSS }} />
|
||||
|
||||
{/* Font preconnections for faster loading */}
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
"use client"
|
||||
|
||||
import { LABELS } from "@/app/labels"
|
||||
import { ProjectCategory, ProjectInterface } from "@/lib/types"
|
||||
import { uniq } from "@/lib/utils"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import Fuse from "fuse.js"
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
@@ -8,11 +13,6 @@ import {
|
||||
useMemo,
|
||||
useCallback,
|
||||
} from "react"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import Fuse from "fuse.js"
|
||||
import { ProjectCategory, ProjectInterface } from "@/lib/types"
|
||||
import { uniq } from "@/lib/utils"
|
||||
import { LABELS } from "@/app/labels"
|
||||
|
||||
export type ProjectSortBy = "random" | "asc" | "desc" | "relevance"
|
||||
export type ProjectFilter =
|
||||
@@ -47,6 +47,7 @@ export const FilterLabelMapping: Record<ProjectFilter, string> = {
|
||||
interface ProjectsContextType {
|
||||
projects: ProjectInterface[]
|
||||
researchs: ProjectInterface[]
|
||||
graduatedProjects: ProjectInterface[]
|
||||
isLoading: boolean
|
||||
isError: boolean
|
||||
error: Error | null
|
||||
@@ -188,10 +189,12 @@ const sortProjectByFn = ({
|
||||
)
|
||||
}
|
||||
|
||||
return sortedProjectList.map((project: any) => ({
|
||||
id: project?.id?.toLowerCase(),
|
||||
...project,
|
||||
}))
|
||||
return sortedProjectList
|
||||
.map((project: any) => ({
|
||||
id: project?.id?.toLowerCase(),
|
||||
...project,
|
||||
}))
|
||||
.filter((project: any) => project?.graduated !== true)
|
||||
}
|
||||
|
||||
const getProjectFilters = (projects: ProjectInterface[]): FiltersProps => {
|
||||
@@ -330,6 +333,12 @@ export function ProjectsProvider({ children, tag }: ProjectsProviderProps) {
|
||||
[filteredProjects, sortBy]
|
||||
)
|
||||
|
||||
const graduatedProjects = useMemo(() => {
|
||||
return fetchedProjects.filter(
|
||||
(project: ProjectInterface) => project.graduated === true
|
||||
)
|
||||
}, [fetchedProjects])
|
||||
|
||||
const toggleFilter = useCallback(
|
||||
({
|
||||
tag: filterKey,
|
||||
@@ -413,6 +422,7 @@ export function ProjectsProvider({ children, tag }: ProjectsProviderProps) {
|
||||
onSelectTheme,
|
||||
sortProjectBy,
|
||||
refetch,
|
||||
graduatedProjects,
|
||||
}),
|
||||
[
|
||||
projects,
|
||||
@@ -432,6 +442,7 @@ export function ProjectsProvider({ children, tag }: ProjectsProviderProps) {
|
||||
onSelectTheme,
|
||||
sortProjectBy,
|
||||
refetch,
|
||||
graduatedProjects,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
export function CSSLoader() {
|
||||
const [loaded, setLoaded] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
// Load main CSS after critical render
|
||||
const loadCSS = () => {
|
||||
const link = document.createElement("link")
|
||||
link.rel = "stylesheet"
|
||||
link.href = "/globals.css"
|
||||
link.onload = () => setLoaded(true)
|
||||
document.head.appendChild(link)
|
||||
}
|
||||
|
||||
// Load CSS after a short delay to not block initial render
|
||||
const timer = setTimeout(loadCSS, 100)
|
||||
return () => clearTimeout(timer)
|
||||
}, [])
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -72,7 +72,7 @@ export const ArticleInEvidenceCard = ({
|
||||
{
|
||||
"px-5 lg:px-16 py-6 lg:py-16": size === "lg",
|
||||
"px-6 py-4 lg:p-8": size === "sm",
|
||||
"px-6 lg:p-16": size === "xl",
|
||||
"px-6 lg:p-16 justify-center": size === "xl",
|
||||
"!p-0 lg:!p-0": !backgroundCover,
|
||||
},
|
||||
contentClassName
|
||||
@@ -187,7 +187,7 @@ export const ArticleInEvidenceCard = ({
|
||||
src={article.image ?? "/fallback.webp"}
|
||||
alt="Article hero image"
|
||||
fill
|
||||
className="object-cover -z-10"
|
||||
className="object-cover z-[0]"
|
||||
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
|
||||
priority={false}
|
||||
/>
|
||||
|
||||
@@ -850,4 +850,34 @@ export const Icons = {
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
SliderArrowNext: ({ size = 16, ...props }: LucideProps) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="17"
|
||||
height="17"
|
||||
viewBox="0 0 17 17"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M12.6624 9.7091L0.489595 9.70981V7.71011L12.661 7.7094L7.29762 2.346L8.71183 0.931787L16.49 8.70996L8.71183 16.4881L7.29762 15.0739L12.6624 9.7091Z"
|
||||
fill="#50C3E0"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
SliderArrowPrev: ({ size = 16, ...props }: LucideProps) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="17"
|
||||
height="17"
|
||||
viewBox="0 0 17 17"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M4.33756 9.7091L16.5104 9.70981V7.71011L4.33898 7.7094L9.70238 2.346L8.28817 0.931787L0.509993 8.70996L8.28817 16.4881L9.70238 15.0739L4.33756 9.7091Z"
|
||||
fill="#50C3E0"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
}
|
||||
|
||||
55
components/project/project-graduated.tsx
Normal file
55
components/project/project-graduated.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
"use client"
|
||||
|
||||
import { Slider } from "../slider"
|
||||
import { LABELS } from "@/app/labels"
|
||||
import { useProjects } from "@/app/providers/ProjectsProvider"
|
||||
import { ProjectInterface } from "@/lib/types"
|
||||
import Link from "next/link"
|
||||
|
||||
export const ProjectGraduated = () => {
|
||||
const { graduatedProjects } = useProjects()
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-10 justify-center dark:bg-anakiwa-975 py-16 lg:py-20 overflow-hidden bg-cover-gradient dark:bg-none">
|
||||
<span className="dark:text-tuatara-100 text-tuatara-950 text-xl lg:text-3xl lg:leading-[45px] font-normal font-sans text-center lg:px-0 px-4">
|
||||
{LABELS.PROJECTS_PAGE.GRADUATED_PROJECTS}
|
||||
</span>
|
||||
<div className="lg:px-4 px-4">
|
||||
<Slider slidesToShow={4.2} gap="24px">
|
||||
{graduatedProjects.map((project: ProjectInterface) => {
|
||||
const website = project.links?.website
|
||||
?.split("//")[1]
|
||||
?.replace("/", "")
|
||||
.replace("www.", "")
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={project?.links?.website ?? "#"}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex flex-col gap-4 w-full lg:max-w-[400px] p-8 rounded-[10px] bg-white border border-anakiwa-300 dark:border-anakiwa-400 dark:bg-anakiwa-975 hover:bg-anakiwa-100 dark:hover:bg-anakiwa-950 transition-all duration-300 cursor-pointer"
|
||||
>
|
||||
<div className="flex flex-col gap-4 h-[140px]">
|
||||
<span className="font-sans text-2xl font-bold text-tuatara-950 dark:text-anakiwa-400">
|
||||
{project.name}
|
||||
</span>
|
||||
<span className="font-sans text-base font-normal text-tuatara-500 dark:text-tuatara-100 line-clamp-2 lg:line-clamp-3">
|
||||
{project.tldr}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-row items-center justify-between mt-auto w-full">
|
||||
<span className="font-sans text-base font-normal text-tuatara-500 dark:text-tuatara-200 underline">
|
||||
{website}
|
||||
</span>
|
||||
<div className="text-xs dark:text-black dark:bg-anakiwa-300 text-black bg-anakiwa-300 rounded-[3px] px-[6px] py-[2px]">
|
||||
Graduated
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</Slider>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
"use client"
|
||||
|
||||
import { Banner } from "../banner"
|
||||
import { LABELS } from "@/app/labels"
|
||||
import { Icons } from "../icons"
|
||||
import { siteConfig } from "@/config/site"
|
||||
import { Button } from "../ui/button"
|
||||
import { AppLink } from "../app-link"
|
||||
import { Banner } from "../banner"
|
||||
import { Icons } from "../icons"
|
||||
import { Button } from "../ui/button"
|
||||
import { LABELS } from "@/app/labels"
|
||||
import { siteConfig } from "@/config/site"
|
||||
|
||||
export const HomepageBanner = () => {
|
||||
return (
|
||||
@@ -13,23 +13,64 @@ export const HomepageBanner = () => {
|
||||
title={LABELS.COMMON.CONNECT_WITH_US}
|
||||
subtitle={LABELS.COMMON.CONNECT_WITH_US_DESCRIPTION}
|
||||
>
|
||||
<AppLink
|
||||
href={siteConfig.links.discord}
|
||||
external
|
||||
variant="button"
|
||||
passHref
|
||||
className="mx-auto"
|
||||
>
|
||||
<Button>
|
||||
<div className="flex items-center gap-2">
|
||||
<Icons.discord fill="white" className="h-4" />
|
||||
<span className="uppercase">
|
||||
{LABELS.HOMEPAGE.JOIN_OUR_DISCORD}
|
||||
</span>
|
||||
<Icons.externalUrl fill="white" className="h-5" />
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-col lg:flex-row gap-8 lg:gap-16 lg:text-left text-center mx-auto">
|
||||
<div className="flex flex-col gap-4 lg:gap-12 w-full lg:max-w-[430px] p-10 rounded-[10px] bg-white border border-anakiwa-300 dark:border-anakiwa-400 dark:bg-anakiwa-975 transition-all">
|
||||
<div className="flex flex-col gap-[10px]">
|
||||
<span className="font-sans text-xl font-medium text-tuatara-950 dark:text-anakiwa-400">
|
||||
{LABELS.HOMEPAGE.LEARN_AND_CONNECT.TITLE}
|
||||
</span>
|
||||
<span className="font-sans text-base font-normal text-tuatara-500 dark:text-tuatara-100 ">
|
||||
{LABELS.HOMEPAGE.LEARN_AND_CONNECT.DESCRIPTION}
|
||||
</span>
|
||||
</div>
|
||||
<AppLink
|
||||
href={siteConfig.links.researchAndDevelopmentDiscord}
|
||||
rel="noreferrer noopener"
|
||||
variant="nav"
|
||||
external
|
||||
className="mx-auto"
|
||||
>
|
||||
<Button className="lg:w-full mx-auto lg:text-lg !text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<Icons.discord fill="white" className="h-4" />
|
||||
<span className="uppercase">
|
||||
{LABELS.HOMEPAGE.LEARN_AND_CONNECT.ACTION}
|
||||
</span>
|
||||
<Icons.externalUrl fill="white" className="h-5" />
|
||||
</div>
|
||||
</Button>
|
||||
</AppLink>
|
||||
</div>
|
||||
</Button>
|
||||
</AppLink>
|
||||
<div className="flex flex-col gap-4 lg:gap-12 w-full lg:max-w-[430px] p-10 rounded-[10px] bg-white border border-anakiwa-300 dark:border-anakiwa-400 dark:bg-anakiwa-975 transition-all">
|
||||
<div className="flex flex-col gap-[10px]">
|
||||
<span className="font-sans text-xl font-medium text-tuatara-950 dark:text-anakiwa-400">
|
||||
{LABELS.HOMEPAGE.COLLABORATE_AND_CONTRIBUTE.TITLE}
|
||||
</span>
|
||||
<span className="font-sans text-base font-normal text-tuatara-500 dark:text-tuatara-100 ">
|
||||
{LABELS.HOMEPAGE.COLLABORATE_AND_CONTRIBUTE.DESCRIPTION}
|
||||
</span>
|
||||
</div>
|
||||
<AppLink
|
||||
href={siteConfig.links.magicians}
|
||||
rel="noreferrer noopener"
|
||||
variant="nav"
|
||||
external
|
||||
className="mx-auto"
|
||||
>
|
||||
<Button className="lg:w-full mx-auto lg:text-lg !text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<Icons.discord fill="white" className="h-4" />
|
||||
<span className="uppercase">
|
||||
{LABELS.HOMEPAGE.COLLABORATE_AND_CONTRIBUTE.ACTION}
|
||||
</span>
|
||||
<Icons.externalUrl fill="white" className="h-5" />
|
||||
</div>
|
||||
</Button>
|
||||
</AppLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Banner>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ export function SiteFooter() {
|
||||
return (
|
||||
<footer className="flex flex-col">
|
||||
<div className="bg-tuatara-950 text-white py-8 text-left text-[14px] dark:bg-black">
|
||||
<AppContent className="flex gap-10 py-2 lg:gap-24 justify-center">
|
||||
<AppContent className="flex flex-col lg:flex-row gap-10 py-2 lg:gap-24 justify-center">
|
||||
<LinksWrapper>
|
||||
{MAIN_NAV.map(
|
||||
(
|
||||
|
||||
214
components/slider.tsx
Normal file
214
components/slider.tsx
Normal file
@@ -0,0 +1,214 @@
|
||||
"use client"
|
||||
|
||||
import { Icons } from "./icons"
|
||||
import { Button } from "./ui/button"
|
||||
import { useState, useRef, useEffect } from "react"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
interface SliderProps {
|
||||
children: React.ReactNode[]
|
||||
className?: string
|
||||
showNavigation?: boolean
|
||||
autoSlide?: boolean
|
||||
autoSlideInterval?: number
|
||||
onSlideChange?: (index: number) => void
|
||||
slidesToShow?: number
|
||||
gap?: string
|
||||
withDivider?: boolean
|
||||
controlsPosition?: "top" | "bottom"
|
||||
showControls?: boolean
|
||||
}
|
||||
|
||||
export const Slider = ({
|
||||
children,
|
||||
className,
|
||||
showNavigation = true,
|
||||
autoSlide = false,
|
||||
autoSlideInterval = 3000,
|
||||
onSlideChange,
|
||||
slidesToShow = 1,
|
||||
gap = "0px",
|
||||
withDivider = false,
|
||||
controlsPosition = "bottom",
|
||||
showControls = true,
|
||||
}: SliderProps) => {
|
||||
const [isMounted, setIsMounted] = useState(false)
|
||||
const [isMobile, setIsMobile] = useState(false)
|
||||
const [touchStart, setTouchStart] = useState(0)
|
||||
const [touchEnd, setTouchEnd] = useState(0)
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
const sliderRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Ensure component is mounted before running client-side logic
|
||||
useEffect(() => {
|
||||
setIsMounted(true)
|
||||
|
||||
// Check if device is mobile
|
||||
const checkMobile = () => {
|
||||
setIsMobile(window.innerWidth < 1024) // lg breakpoint
|
||||
}
|
||||
|
||||
checkMobile()
|
||||
window.addEventListener("resize", checkMobile)
|
||||
|
||||
return () => window.removeEventListener("resize", checkMobile)
|
||||
}, [])
|
||||
|
||||
const totalSlides = children.length
|
||||
const effectiveSlidesToShow = isMobile ? 1 : slidesToShow
|
||||
const maxSlideIndex = Math.max(
|
||||
0,
|
||||
totalSlides - Math.floor(effectiveSlidesToShow)
|
||||
)
|
||||
|
||||
const [currentSlide, setCurrentSlide] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
if (!isMounted || !autoSlide || isHovered || currentSlide >= maxSlideIndex)
|
||||
return
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setCurrentSlide((prev) => {
|
||||
if (prev >= maxSlideIndex) {
|
||||
return prev // Stop at the last slide
|
||||
}
|
||||
return prev + 1
|
||||
})
|
||||
}, autoSlideInterval)
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [
|
||||
isMounted,
|
||||
autoSlide,
|
||||
autoSlideInterval,
|
||||
maxSlideIndex,
|
||||
isHovered,
|
||||
currentSlide,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (onSlideChange) {
|
||||
onSlideChange(currentSlide)
|
||||
}
|
||||
}, [currentSlide, onSlideChange])
|
||||
|
||||
const goToNext = () => {
|
||||
if (!isMounted) return
|
||||
setCurrentSlide((prev) => Math.min(prev + 1, maxSlideIndex))
|
||||
}
|
||||
|
||||
const goToPrev = () => {
|
||||
if (!isMounted) return
|
||||
setCurrentSlide((prev) => Math.max(prev - 1, 0))
|
||||
}
|
||||
|
||||
const handleTouchStart = (e: React.TouchEvent) => {
|
||||
setTouchStart(e.targetTouches[0].clientX)
|
||||
}
|
||||
|
||||
const handleTouchMove = (e: React.TouchEvent) => {
|
||||
setTouchEnd(e.targetTouches[0].clientX)
|
||||
}
|
||||
|
||||
const handleTouchEnd = () => {
|
||||
if (!touchStart || !touchEnd) return
|
||||
|
||||
const distance = touchStart - touchEnd
|
||||
const isLeftSwipe = distance > 50
|
||||
const isRightSwipe = distance < -50
|
||||
|
||||
if (isLeftSwipe) {
|
||||
goToNext()
|
||||
} else if (isRightSwipe) {
|
||||
goToPrev()
|
||||
}
|
||||
|
||||
setTouchStart(0)
|
||||
setTouchEnd(0)
|
||||
}
|
||||
|
||||
const slideWidth = 100 / effectiveSlidesToShow
|
||||
const translateX = currentSlide * slideWidth
|
||||
|
||||
// Prevent hydration mismatch
|
||||
if (!isMounted) {
|
||||
return (
|
||||
<div className={twMerge("flex flex-col gap-5", className)}>
|
||||
<div className="flex overflow-hidden">
|
||||
<div
|
||||
className="flex-shrink-0 h-full"
|
||||
style={{ width: `${slideWidth}%` }}
|
||||
>
|
||||
{children[0]}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex flex-col gap-5",
|
||||
controlsPosition === "top" && "flex-col-reverse"
|
||||
)}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex flex-col gap-5 relative overflow-hidden",
|
||||
isMobile ? "" : "px-1",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div
|
||||
ref={sliderRef}
|
||||
className={twMerge(
|
||||
"flex ease-in-out transition-transform duration-300",
|
||||
withDivider && "divide-x divide-app-color-border"
|
||||
)}
|
||||
style={{
|
||||
transform: `translateX(-${translateX}%)`,
|
||||
gap: isMobile ? "0px" : gap,
|
||||
}}
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchMove={handleTouchMove}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
>
|
||||
{children.map((child, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex-shrink-0 h-full"
|
||||
style={{
|
||||
width: `${slideWidth}%`,
|
||||
}}
|
||||
>
|
||||
{child}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{showNavigation && maxSlideIndex > 0 && showControls && (
|
||||
<div className="flex gap-[10px] ml-auto justify-end lg:mr-4">
|
||||
<Button
|
||||
onClick={goToPrev}
|
||||
disabled={currentSlide === 0}
|
||||
aria-label="Previous slide"
|
||||
className="!mx-0 !focus:outline-none !focus:ring-0 !focus:ring-offset-0 w-10 h-10"
|
||||
>
|
||||
<Icons.SliderArrowPrev />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={goToNext}
|
||||
disabled={currentSlide >= maxSlideIndex}
|
||||
aria-label="Next slide"
|
||||
className="!mx-0 !focus:outline-none !focus:ring-0 !focus:ring-offset-0 w-10 h-10"
|
||||
>
|
||||
<Icons.SliderArrowNext />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
import { InputHTMLAttributes } from "react"
|
||||
import { Icons } from "../icons"
|
||||
import * as CheckboxComponent from "@radix-ui/react-checkbox"
|
||||
import { CheckboxProps as CheckboxComponentProps } from "@radix-ui/react-checkbox"
|
||||
|
||||
import { Icons } from "../icons"
|
||||
import { InputHTMLAttributes } from "react"
|
||||
|
||||
type CheckboxInputProps = InputHTMLAttributes<HTMLDivElement> &
|
||||
CheckboxComponentProps
|
||||
@@ -18,7 +17,7 @@ const Checkbox = ({ label, name, checked, ...props }: CheckboxProps) => {
|
||||
<CheckboxComponent.Root
|
||||
{...props}
|
||||
checked={checked}
|
||||
className="flex h-[14px] w-[14px] cursor-pointer items-center justify-center rounded-[1.5px] border-2 border-solid border-tuatara-200 bg-primary duration-100 ease-in aria-checked:border-black aria-checked:bg-black dark:border-anakiwa-800"
|
||||
className="flex h-[14px] w-[14px] cursor-pointer items-center justify-center rounded-[1.5px] border-2 border-solid border-tuatara-200 bg-transparent duration-100 ease-in aria-checked:border-black aria-checked:bg-black dark:border-anakiwa-800"
|
||||
id={name}
|
||||
>
|
||||
<CheckboxComponent.Indicator className="text-white">
|
||||
|
||||
@@ -25,6 +25,9 @@ export const siteConfig = {
|
||||
"https://github.com/privacy-ethereum/acceleration-program",
|
||||
coreProgram:
|
||||
"https://docs.google.com/forms/d/e/1FAIpQLSendzYY0z_z7fZ37g3jmydvzS9I7OWKbY2JrqAnyNqeaBHvMQ/viewform",
|
||||
magicians: "https://ethereum-magicians.org",
|
||||
researchAndDevelopmentDiscord:
|
||||
"https://discord.com/channels/595666850260713488/1410354969386946560",
|
||||
},
|
||||
addGithubResource:
|
||||
"https://github.com/privacy-ethereum/website-v2/blob/main/content/resources.md",
|
||||
|
||||
@@ -28,6 +28,7 @@ tldr: "Brief project summary" # Short description of the project
|
||||
# ========================================
|
||||
category: "research" # Project category: "research" | "devtools" | "application"
|
||||
license: "MIT" # Project license
|
||||
graduated: false # Project graduated
|
||||
tags: # Project tags
|
||||
keywords: ["tag1", "tag2", "tag3"] # Relevant keywords
|
||||
themes: ["privacy", "scalability"] # Project themes
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
id: "cursive"
|
||||
name: "Cursive"
|
||||
graduated: true
|
||||
image: "cursive.webp"
|
||||
section: "grant"
|
||||
projectStatus: "active"
|
||||
@@ -23,7 +24,7 @@ tags:
|
||||
themes: ["build", "play"]
|
||||
links:
|
||||
github: "https://github.com/cursive-team"
|
||||
website: "https://www.cursive.team/"
|
||||
website: "https://www.cursive.computer/"
|
||||
---
|
||||
|
||||
What began as an experimental initiative to transform PSE's theoretical cryptography research into tangible applications has evolved into a dedicated lab that incubates practical ways to connect people through applied cryptography.
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
---
|
||||
id: "openpassport"
|
||||
name: "OpenPassport"
|
||||
name: "Self"
|
||||
image: "openpassport.webp"
|
||||
graduated: true
|
||||
section: "grant"
|
||||
projectStatus: "active"
|
||||
category: "application"
|
||||
|
||||
@@ -3,6 +3,7 @@ id: "zk-email"
|
||||
name: "ZK Email"
|
||||
image: "zk-email.webp"
|
||||
section: "collaboration"
|
||||
graduated: true
|
||||
projectStatus: "active"
|
||||
category: "application"
|
||||
tldr: "ZK Email is a library that allows for anonymous verification of email signatures while masking specific data."
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
id: "zkp2p"
|
||||
name: "ZKP2P"
|
||||
graduated: true
|
||||
image: "zkp2p.webp"
|
||||
section: "grant"
|
||||
projectStatus: "active"
|
||||
|
||||
@@ -15,7 +15,7 @@ html {
|
||||
|
||||
/* Custom responsive navigation */
|
||||
.nav-responsive {
|
||||
display: none;
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
export const criticalCSS = `
|
||||
/* Critical CSS - Base styles that prevent layout shifts */
|
||||
:root {
|
||||
--background: #FFFFFF;
|
||||
--foreground: #0F172A;
|
||||
--primary: #E1523A;
|
||||
--text-primary: #242528;
|
||||
--text-secondary: #166F8E;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: #000000;
|
||||
--foreground: #FFFFFF;
|
||||
--text-primary: #FFFFFF;
|
||||
--text-secondary: #FFFFFF;
|
||||
--primary: #E1523A;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
border: 0 solid #e5e7eb;
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: var(--font-sans, system-ui, -apple-system, sans-serif);
|
||||
line-height: 1.5;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
text-rendering: geometricPrecision;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
line-height: inherit;
|
||||
background-color: var(--background);
|
||||
color: var(--foreground);
|
||||
font-feature-settings: 'rlig' 1, 'calt' 1;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: var(--font-display, inherit);
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Critical layout styles */
|
||||
.min-h-screen { min-height: 100vh; }
|
||||
.flex { display: flex; }
|
||||
.flex-col { flex-direction: column; }
|
||||
.flex-1 { flex: 1 1 0%; }
|
||||
.relative { position: relative; }
|
||||
.hidden { display: none; }
|
||||
.block { display: block; }
|
||||
|
||||
/* Critical responsive utilities */
|
||||
@media (min-width: 1024px) {
|
||||
.lg\\:hidden { display: none; }
|
||||
.lg\\:block { display: block; }
|
||||
.lg\\:h-\\[600px\\] { height: 600px; }
|
||||
}
|
||||
|
||||
/* Critical hero section styles */
|
||||
.h-\\[500px\\] { height: 500px; }
|
||||
.w-full { width: 100%; }
|
||||
.overflow-hidden { overflow: hidden; }
|
||||
.bg-cover { background-size: cover; }
|
||||
.bg-no-repeat { background-repeat: no-repeat; }
|
||||
.bg-center { background-position: center; }
|
||||
|
||||
/* Critical text utilities */
|
||||
.text-center { text-align: center; }
|
||||
.text-3xl { font-size: 1.875rem; line-height: 2.25rem; }
|
||||
.font-medium { font-weight: 500; }
|
||||
.uppercase { text-transform: uppercase; }
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.lg\\:text-5xl { font-size: 3rem; line-height: 1; }
|
||||
}
|
||||
|
||||
/* Critical spacing */
|
||||
.gap-10 { gap: 2.5rem; }
|
||||
.gap-4 { gap: 1rem; }
|
||||
.py-10 { padding-top: 2.5rem; padding-bottom: 2.5rem; }
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.lg\\:py-\\[130px\\] { padding-top: 130px; padding-bottom: 130px; }
|
||||
.lg\\:gap-8 { gap: 2rem; }
|
||||
}
|
||||
`
|
||||
@@ -84,6 +84,7 @@ export interface ProjectTeamMember {
|
||||
export interface ProjectInterface {
|
||||
id: string
|
||||
hasWiki?: boolean // show project with wiki page template
|
||||
graduated?: boolean
|
||||
youtubeLinks?: string[]
|
||||
team?: ProjectTeamMember[]
|
||||
license?: string
|
||||
|
||||
Reference in New Issue
Block a user