mirror of
https://github.com/privacy-scaling-explorations/pse.dev.git
synced 2026-01-09 22:28:03 -05:00
fix layout and implement new design (#470)
* fix layout and implement new design + fix dark mode styles
This commit is contained in:
48
app/(pages)/not-found.tsx
Normal file
48
app/(pages)/not-found.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
"use client"
|
||||
|
||||
import "@/styles/globals.css"
|
||||
import React from "react"
|
||||
import Image from "next/image"
|
||||
import Link from "next/link"
|
||||
import { Metadata } from "next"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
|
||||
import { LABELS } from "../labels"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "404: Page Not Found",
|
||||
}
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<div className="relative flex h-screen flex-col bg-anakiwa-50 dark:bg-black">
|
||||
<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">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Image
|
||||
width={176}
|
||||
height={256}
|
||||
src="/icons/404-search.svg"
|
||||
alt="emotion sad"
|
||||
className="mx-auto h-12 w-12 text-anakiwa-400 md:h-64 md:w-44"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-5">
|
||||
<span className="font-display text-2xl font-bold text-primary md:text-6xl">
|
||||
{LABELS.COMMON.ERROR["404"].TITLE}
|
||||
</span>
|
||||
<span className="font-sans text-base font-normal md:text-lg">
|
||||
{LABELS.COMMON.ERROR["404"].DESCRIPTION}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Link href="/" className="mx-auto">
|
||||
<Button variant="black">{LABELS.COMMON.GO_TO_HOME}</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -162,7 +162,7 @@ export const ProjectContent = ({ id }: { id: string }) => {
|
||||
{projectStatusMessage?.length > 0 && (
|
||||
<span className="relative pl-6 text-tuatara-500">
|
||||
<div className="border-l-[4px] border-l-orangeDark absolute left-0 top-0 bottom-0"></div>
|
||||
<span className="text-tuatara-500">
|
||||
<span className="text-tuatara-500 dark:text-white">
|
||||
{projectStatusMessage}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
50
app/components/wrappers/SectionWrapper.tsx
Normal file
50
app/components/wrappers/SectionWrapper.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
import { classed } from "@tw-classed/react"
|
||||
import { ReactNode } from "react"
|
||||
|
||||
interface SectionWrapperProps {
|
||||
title: string
|
||||
description?: string
|
||||
showHeader?: boolean
|
||||
children?: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const SectionWrapperTitle = classed.h3(
|
||||
"relative font-sans text-base font-bold uppercase tracking-[3.36px] text-anakiwa-950 dark:text-anakiwa-400 dark:text-white",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"after:ml-8 after:absolute after:top-1/2 after:h-[1px] after:w-full after:translate-y-1/2 after:bg-anakiwa-300 after:content-['']",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export const SectionWrapper = ({
|
||||
title = "",
|
||||
description = "",
|
||||
showHeader = true,
|
||||
children = null,
|
||||
className = "",
|
||||
}: SectionWrapperProps) => {
|
||||
return (
|
||||
<div className={cn("flex w-full flex-col gap-10", className)}>
|
||||
{showHeader && (
|
||||
<div className="flex flex-col gap-6 overflow-hidden">
|
||||
<SectionWrapperTitle>{title}</SectionWrapperTitle>
|
||||
{description?.length > 0 && (
|
||||
<span className="font-sans text-base italic text-primary">
|
||||
{description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -47,6 +47,7 @@ export const LABELS = {
|
||||
RECENT_ARTICLES: "Recent articles",
|
||||
SEE_MORE: "See more",
|
||||
READ_MORE: "Read more",
|
||||
SEARCH_PLACEHOLDER: "Search PSE's blog",
|
||||
},
|
||||
BLOG_TAGS_PAGE: {
|
||||
TITLE: "Blog tags",
|
||||
@@ -168,6 +169,7 @@ export const LABELS = {
|
||||
ERROR_LOADING_VIDEOS: "Error loading videos",
|
||||
PROJECT_TEAM: "Team",
|
||||
YOUTUBE_VIDEOS: "YouTube Videos",
|
||||
MORE_POSTS: "More posts",
|
||||
},
|
||||
HOMEPAGE: {
|
||||
HEADER_TITLE: "Privacy + Scaling Explorations",
|
||||
|
||||
@@ -17,27 +17,30 @@ interface ArticleInEvidenceCardProps {
|
||||
titleClassName?: string
|
||||
contentClassName?: string
|
||||
showDate?: boolean
|
||||
backgroundCover?: boolean
|
||||
}
|
||||
|
||||
const AsLinkWrapper = ({
|
||||
children,
|
||||
href,
|
||||
asLink,
|
||||
className = "",
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
href: string
|
||||
asLink: boolean
|
||||
className?: string
|
||||
}) => {
|
||||
return asLink ? (
|
||||
<Link className="group" href={href}>
|
||||
<Link className={cn("group", className)} href={href}>
|
||||
{children}
|
||||
</Link>
|
||||
) : (
|
||||
<>{children}</>
|
||||
<div className={cn("group", className)}>{children}</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const ArticleInEvidenceCard = async ({
|
||||
export const ArticleInEvidenceCard = ({
|
||||
article,
|
||||
showReadMore = false,
|
||||
size = "lg",
|
||||
@@ -47,6 +50,7 @@ export const ArticleInEvidenceCard = async ({
|
||||
titleClassName = "",
|
||||
contentClassName = "",
|
||||
showDate = true,
|
||||
backgroundCover = true,
|
||||
}: ArticleInEvidenceCardProps) => {
|
||||
const hideTldr = variant === "compact"
|
||||
|
||||
@@ -59,42 +63,40 @@ export const ArticleInEvidenceCard = async ({
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<AsLinkWrapper href={`/blog/${article.id}`} asLink={asLink}>
|
||||
const ArticleContent = ({ backgroundCover = true }: any) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"min-h-[177px] lg:min-h-[190px] relative flex flex-col gap-5 w-full items-center after:absolute after:inset-0 after:content-[''] after:bg-black after:opacity-20 group-hover:after:opacity-80 transition-opacity duration-300 after:z-[0]",
|
||||
"duration-200 flex flex-col gap-4 text-left relative z-[1] w-full h-full",
|
||||
{
|
||||
"aspect-video": !className?.includes("h-full"),
|
||||
"px-5 lg:px-16 py-6 lg:py-16 ": size === "lg",
|
||||
"px-6 py-4 lg:p-8": size === "sm",
|
||||
"px-6 lg:p-16": size === "xl",
|
||||
"!p-0 lg:!p-0": !backgroundCover,
|
||||
},
|
||||
className
|
||||
contentClassName
|
||||
)}
|
||||
style={{
|
||||
backgroundImage: `url(${article.image ?? "/fallback.webp"})`,
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center centers",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"duration-200 flex flex-col gap-[10px] text-left relative z-[1] w-full h-full",
|
||||
{
|
||||
"px-5 lg:px-16 py-6 lg:py-16 ": size === "lg",
|
||||
"px-6 py-4 lg:p-8": size === "sm",
|
||||
"px-6 lg:p-16": size === "xl",
|
||||
},
|
||||
contentClassName
|
||||
)}
|
||||
>
|
||||
{article.date && showDate && (
|
||||
<span className="text-white text-xs font-sans font-bold tracking-[2.5px] text-left uppercase">
|
||||
{formatDate(article.date)}
|
||||
</span>
|
||||
)}
|
||||
{article.date && showDate && (
|
||||
<span
|
||||
className={cn(
|
||||
"text-white text-xs font-sans font-bold tracking-[2.5px] text-left uppercase",
|
||||
backgroundCover
|
||||
? "text-white dark:text-black"
|
||||
: "text-black dark:text-white"
|
||||
)}
|
||||
>
|
||||
{formatDate(article.date)}
|
||||
</span>
|
||||
)}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Link
|
||||
href={`/blog/${article.id}`}
|
||||
className={cn(
|
||||
" text-white font-display hover:text-anakiwa-400 transition-colors",
|
||||
"font-display hover:text-anakiwa-400 group-hover:text-anakiwa-400 transition-colors",
|
||||
backgroundCover
|
||||
? "text-white dark:text-white"
|
||||
: "text-black dark:text-white group-hover:underline",
|
||||
{
|
||||
"text-[20px] font-semibold lg:font-bold lg:text-lg line-clamp-2 mt-auto":
|
||||
variant === "compact",
|
||||
@@ -108,32 +110,84 @@ export const ArticleInEvidenceCard = async ({
|
||||
>
|
||||
{article.title}
|
||||
</Link>
|
||||
<span className="text-sm text-white/80 uppercase font-inter">
|
||||
{article.authors?.join(", ")}
|
||||
</span>
|
||||
{(article?.authors ?? [])?.length > 0 && (
|
||||
<span
|
||||
className={cn("text-sm font-sans", {
|
||||
"text-white/80": backgroundCover,
|
||||
"text-tuatara-600 dark:text-white/80": !backgroundCover,
|
||||
})}
|
||||
>
|
||||
{article.authors?.join(", ")}
|
||||
</span>
|
||||
)}
|
||||
{article.tldr && !hideTldr && (
|
||||
<span
|
||||
className={
|
||||
"text-sm font-sans text-white font-normal line-clamp-2 lg:line-clamp-5 mt-auto hidden lg:block"
|
||||
}
|
||||
className={cn(
|
||||
"text-sm font-san font-normal line-clamp-2 lg:line-clamp-5 mt-auto hidden lg:block",
|
||||
{
|
||||
"text-white/80": backgroundCover,
|
||||
"text-tuatara-600 dark:text-white/80": !backgroundCover,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{article.tldr}
|
||||
</span>
|
||||
)}
|
||||
{showReadMore && (
|
||||
<Link href={`/blog/${article.id}`} className="ml-auto mt-4">
|
||||
<Button className="uppercase ml-auto" variant="secondary">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="!text-center">
|
||||
{LABELS.BLOG_PAGE.READ_MORE}
|
||||
</span>
|
||||
<Icons.arrowRight className="w-4 h-4" />
|
||||
</div>
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
{(article?.tags ?? [])?.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{article?.tags?.slice(0, 3)?.map((tag) => (
|
||||
<Link key={tag.id} href={`/blog/tags/${tag.id}`}>
|
||||
<Button size="xs" variant="secondary">
|
||||
{tag.name}
|
||||
</Button>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{showReadMore && (
|
||||
<Link href={`/blog/${article.id}`} className="ml-auto mt-4">
|
||||
<Button className="uppercase ml-auto" variant="secondary">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="!text-center">
|
||||
{LABELS.BLOG_PAGE.READ_MORE}
|
||||
</span>
|
||||
<Icons.arrowRight className="w-4 h-4" />
|
||||
</div>
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<AsLinkWrapper
|
||||
href={`/blog/${article.id}`}
|
||||
asLink={asLink}
|
||||
className="flex flex-col gap-4"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"relative flex flex-col gap-5 w-full items-center after:absolute after:inset-0 after:content-[''] after:bg-black after:opacity-20 group-hover:after:opacity-80 transition-opacity duration-300 after:z-[0]",
|
||||
{
|
||||
"aspect-video": !className?.includes("h-full"),
|
||||
"min-h-[148px]": !backgroundCover,
|
||||
"min-h-[177px] lg:min-h-[190px]": backgroundCover,
|
||||
},
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
backgroundImage: `url(${article.image ?? "/fallback.webp"})`,
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center centers",
|
||||
}}
|
||||
>
|
||||
{backgroundCover && (
|
||||
<ArticleContent backgroundCover={backgroundCover} />
|
||||
)}
|
||||
</div>
|
||||
{!backgroundCover && <ArticleContent backgroundCover={backgroundCover} />}
|
||||
</AsLinkWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -56,9 +56,11 @@ export const ArticleListCard = ({
|
||||
<span className="text-xs font-display lg:text-[22px] font-bold text-primary group-hover:text-anakiwa-500 group-hover:underline duration-200 lg:leading-6">
|
||||
{article.title}
|
||||
</span>
|
||||
<span className="lg:uppercase text-tuatara-400 lg:text-xs text-[10px] leading-none font-sans dark:text-tuatara-300">
|
||||
{article.authors?.map((author) => author).join(", ")}
|
||||
</span>
|
||||
{(article?.authors ?? [])?.length > 0 && (
|
||||
<span className="text-tuatara-400 lg:text-xs text-[10px] leading-none font-sans dark:text-tuatara-300">
|
||||
{article.authors?.map((author) => author).join(", ")}
|
||||
</span>
|
||||
)}
|
||||
<div className="hidden lg:block">
|
||||
<Markdown
|
||||
components={{
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
"use client"
|
||||
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { Article } from "@/lib/content"
|
||||
import { Article, ArticleTag } from "@/lib/content"
|
||||
import { ArticleListCard } from "./article-list-card"
|
||||
import { cn, getBackgroundImage } from "@/lib/utils"
|
||||
import Link from "next/link"
|
||||
import { cva } from "class-variance-authority"
|
||||
import { ArticleInEvidenceCard } from "./article-in-evidance-card"
|
||||
import { Input } from "../ui/input"
|
||||
import { Button } from "../ui/button"
|
||||
import { LABELS } from "@/app/labels"
|
||||
import { Search as SearchIcon } from "lucide-react"
|
||||
import { useState } from "react"
|
||||
import { useDebounce } from "react-use"
|
||||
import { useParams, useRouter, useSearchParams } from "next/navigation"
|
||||
|
||||
const ArticleTitle = cva(
|
||||
"text-white font-display hover:text-anakiwa-400 transition-colors group-hover:text-anakiwa-400",
|
||||
@@ -21,121 +28,6 @@ const ArticleTitle = cva(
|
||||
}
|
||||
)
|
||||
|
||||
const ArticleInEvidenceCard = ({
|
||||
article,
|
||||
size = "lg",
|
||||
variant = "default",
|
||||
className,
|
||||
asLink = false,
|
||||
titleClassName = "",
|
||||
contentClassName = "",
|
||||
showDate = true,
|
||||
}: {
|
||||
article: Article
|
||||
showReadMore?: boolean
|
||||
size?: "sm" | "lg" | "xl"
|
||||
variant?: "default" | "compact" | "xl"
|
||||
className?: string
|
||||
asLink?: boolean
|
||||
titleClassName?: string
|
||||
contentClassName?: string
|
||||
showDate?: boolean
|
||||
}) => {
|
||||
const hideTldr = variant === "compact"
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleDateString("en-US", {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
})
|
||||
}
|
||||
|
||||
const AsLinkWrapper = ({
|
||||
children,
|
||||
href,
|
||||
asLink,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
href: string
|
||||
asLink: boolean
|
||||
}) => {
|
||||
return asLink ? (
|
||||
<Link className="group" href={href}>
|
||||
{children}
|
||||
</Link>
|
||||
) : (
|
||||
<div className="group">{children}</div>
|
||||
)
|
||||
}
|
||||
|
||||
const backgroundImage = getBackgroundImage(article?.image)
|
||||
|
||||
return (
|
||||
<AsLinkWrapper href={`/blog/${article.id}`} asLink={asLink}>
|
||||
<div
|
||||
className={cn(
|
||||
"min-h-[177px] lg:min-h-[190px] relative flex flex-col gap-5 w-full items-center after:absolute after:inset-0 after:content-[''] after:bg-black after:opacity-20 group-hover:after:opacity-80 transition-opacity duration-300 after:z-[0]",
|
||||
{
|
||||
"aspect-video": !className?.includes("h-full"),
|
||||
},
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
backgroundImage: backgroundImage
|
||||
? `url(${backgroundImage})`
|
||||
: undefined,
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center centers",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"duration-200 flex flex-col gap-[10px] text-left relative z-[1] w-full h-full",
|
||||
{
|
||||
"px-5 lg:px-16 py-6 lg:py-16 ": size === "lg",
|
||||
"px-6 py-4 lg:p-8": size === "sm",
|
||||
"px-6 lg:p-16": size === "xl",
|
||||
},
|
||||
contentClassName
|
||||
)}
|
||||
>
|
||||
{article.date && showDate && (
|
||||
<span className="text-white text-xs font-sans font-bold tracking-[2.5px] text-left uppercase">
|
||||
{formatDate(article.date)}
|
||||
</span>
|
||||
)}
|
||||
{asLink === false ? (
|
||||
<Link
|
||||
href={`/blog/${article.id}`}
|
||||
className={cn(ArticleTitle({ variant }), titleClassName)}
|
||||
>
|
||||
{article.title}
|
||||
</Link>
|
||||
) : (
|
||||
<span className={cn(ArticleTitle({ variant }), titleClassName)}>
|
||||
{article.title}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-sm text-white/80 uppercase font-inter">
|
||||
{article.authors?.join(", ")}
|
||||
</span>
|
||||
{article.tldr && !hideTldr && (
|
||||
<span
|
||||
className={
|
||||
"text-sm font-sans text-white font-normal line-clamp-2 lg:line-clamp-5 mt-auto hidden lg:block"
|
||||
}
|
||||
>
|
||||
{article.tldr}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</AsLinkWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
async function fetchArticles(tag?: string) {
|
||||
try {
|
||||
const params = new URLSearchParams()
|
||||
@@ -164,6 +56,12 @@ interface ArticlesListProps {
|
||||
export const ArticlesList: React.FC<ArticlesListProps> = ({
|
||||
tag,
|
||||
}: ArticlesListProps) => {
|
||||
const router = useRouter()
|
||||
|
||||
const params = useSearchParams()
|
||||
const query = params.get("query")
|
||||
const [searchQuery, setSearchQuery] = useState(query ?? "")
|
||||
|
||||
const {
|
||||
data: articles = [],
|
||||
isLoading,
|
||||
@@ -185,25 +83,70 @@ export const ArticlesList: React.FC<ArticlesListProps> = ({
|
||||
)
|
||||
}
|
||||
|
||||
const hasSearchParams =
|
||||
(searchQuery ?? "")?.length > 0 && searchQuery !== "all"
|
||||
|
||||
const lastArticle = articles[0]
|
||||
const featuredArticles = !tag ? articles.slice(1, 3) : []
|
||||
const otherArticles = !tag ? articles.slice(3) : articles
|
||||
let otherArticles = !tag && !hasSearchParams ? articles.slice(3, 6) : []
|
||||
|
||||
if (searchQuery === "all") {
|
||||
otherArticles = articles
|
||||
} else if (searchQuery?.length > 0) {
|
||||
otherArticles = articles.filter((article: Article) => {
|
||||
const title = article.title.toLowerCase()
|
||||
const content = article.content.toLowerCase()
|
||||
const tags =
|
||||
article.tags?.map((tag: ArticleTag) => tag.name.toLowerCase()) ?? []
|
||||
return (
|
||||
title.includes(searchQuery.toLowerCase()) ||
|
||||
tags.some((tag: string) => tag.includes(searchQuery.toLowerCase()))
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const hasTag = tag !== undefined
|
||||
|
||||
const onSearchArticles = (query: string) => {
|
||||
setSearchQuery(query)
|
||||
router.push(`/blog?query=${query}`)
|
||||
}
|
||||
|
||||
useDebounce(
|
||||
() => {
|
||||
if (searchQuery === "") return null
|
||||
onSearchArticles(searchQuery)
|
||||
},
|
||||
500, // debounce timeout in ms when user is typing
|
||||
[searchQuery]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-10 lg:gap-16">
|
||||
{!hasTag && (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 lg:gap-6 items-stretch">
|
||||
<div className="lg:col-span-2 h-full">
|
||||
{!hasTag && !hasSearchParams && searchQuery !== "all" && (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 lg:gap-10 items-stretch">
|
||||
<div className="lg:col-span-2 h-full flex flex-col gap-4">
|
||||
<ArticleInEvidenceCard
|
||||
article={lastArticle}
|
||||
size="sm"
|
||||
className="h-full "
|
||||
asLink
|
||||
/>
|
||||
<>
|
||||
{featuredArticles?.map((article: Article) => {
|
||||
return (
|
||||
<ArticleInEvidenceCard
|
||||
key={article.id}
|
||||
article={article}
|
||||
size="sm"
|
||||
className="h-full lg:hidden"
|
||||
asLink
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 lg:gap-6 lg:col-span-2 h-full">
|
||||
<div className="hidden lg:grid grid-cols-1 lg:grid-cols-2 gap-4 lg:gap-10 lg:col-span-2 h-full">
|
||||
{featuredArticles?.map((article: Article) => {
|
||||
return (
|
||||
<ArticleInEvidenceCard
|
||||
@@ -212,6 +155,7 @@ export const ArticlesList: React.FC<ArticlesListProps> = ({
|
||||
variant="compact"
|
||||
size="sm"
|
||||
className="h-full"
|
||||
backgroundCover={false}
|
||||
asLink
|
||||
/>
|
||||
)
|
||||
@@ -219,10 +163,37 @@ export const ArticlesList: React.FC<ArticlesListProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-5 lg:gap-14">
|
||||
{otherArticles.map((article: Article) => {
|
||||
return <ArticleListCard key={article.id} article={article} />
|
||||
})}
|
||||
<div className="flex flex-col gap-10 lg:gap-16 lg:px-12">
|
||||
<div className="flex flex-col gap-10 ">
|
||||
<Input
|
||||
className="max-w-[500px] mx-auto w-full"
|
||||
placeholder={LABELS.BLOG_PAGE.SEARCH_PLACEHOLDER}
|
||||
icon={SearchIcon}
|
||||
onChange={(e) => {
|
||||
onSearchArticles(e?.target?.value ?? "")
|
||||
}}
|
||||
onIconClick={() => {
|
||||
onSearchArticles(searchQuery)
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-col gap-5 lg:gap-14 ">
|
||||
{otherArticles
|
||||
.filter((article: Article) => article.id !== lastArticle.id)
|
||||
.map((article: Article) => {
|
||||
return <ArticleListCard key={article.id} article={article} />
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
{searchQuery?.length === 0 && (
|
||||
<Button
|
||||
className="mx-auto uppercase"
|
||||
onClick={() => {
|
||||
onSearchArticles("all")
|
||||
}}
|
||||
>
|
||||
{LABELS.COMMON.MORE_POSTS}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -75,7 +75,7 @@ export function BlogContent({ post, isNewsletter = false }: BlogContentProps) {
|
||||
</span>
|
||||
<Link
|
||||
href="/blog"
|
||||
className="text-black font-bold text-base leading-6 hover:underline hover:text-anakiwa-500"
|
||||
className="text-black font-bold text-lg leading-6 hover:underline hover:text-anakiwa-500"
|
||||
>
|
||||
View all
|
||||
</Link>
|
||||
|
||||
@@ -70,7 +70,7 @@ export const WikiCard = ({ project, className = "" }: WikiCardProps) => {
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-6", className)}>
|
||||
<div className="mx-auto flex max-w-[290px] flex-col gap-6">
|
||||
<div className="mx-auto flex max-w-[290px] flex-col gap-6 w-full">
|
||||
<Card className="bg-background" padding="none">
|
||||
<div className="relative flex h-[140px] items-center justify-center overflow-hidden rounded-t-lg">
|
||||
<Image
|
||||
|
||||
@@ -126,7 +126,7 @@ export default function ProjectCard({
|
||||
)}
|
||||
|
||||
<div
|
||||
className="px-[6px] py-[2px] text-xs font-normal leading-none flex items-center justify-center rounded-[3px] text-white dark:text-black"
|
||||
className="px-[6px] py-[2px] text-xs font-normal leading-none flex items-center justify-center rounded-[3px] text-black dark:text-black"
|
||||
style={{
|
||||
backgroundColor: ProjectStatusColorMapping[projectStatus],
|
||||
}}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react"
|
||||
import Image from "next/image"
|
||||
import NoResultIcon from "@/public/icons/no-result.svg"
|
||||
import { cva } from "class-variance-authority"
|
||||
|
||||
import {
|
||||
ProjectInterface,
|
||||
@@ -18,21 +17,7 @@ import { LABELS } from "@/app/labels"
|
||||
|
||||
import ProjectCard from "./project-card"
|
||||
import { useProjects } from "@/app/providers/ProjectsProvider"
|
||||
|
||||
const sectionTitleClass = cva(
|
||||
"relative font-sans text-base font-bold uppercase tracking-[3.36px] text-anakiwa-950 dark:text-anakiwa-400 dark:text-white",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"after:ml-8 after:absolute after:top-1/2 after:h-[1px] after:w-full after:translate-y-1/2 after:bg-anakiwa-300 after:content-['']",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
import { SectionWrapper } from "@/app/components/wrappers/SectionWrapper"
|
||||
|
||||
const NoResults = () => {
|
||||
return (
|
||||
@@ -178,14 +163,11 @@ export const ProjectList = () => {
|
||||
className="flex justify-between gap-10"
|
||||
>
|
||||
<div className={cn("flex w-full flex-col gap-10 pt-10")}>
|
||||
{!hasSearchParams && (
|
||||
<div className="flex flex-col gap-6 overflow-hidden">
|
||||
<h3 className={cn(sectionTitleClass())}>{status}</h3>
|
||||
<span className="font-sans text-base italic text-primary">
|
||||
{description}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<SectionWrapper
|
||||
title={status}
|
||||
description={description}
|
||||
showHeader={!hasSearchParams}
|
||||
/>
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 md:gap-x-6 md:gap-y-10 lg:grid-cols-4">
|
||||
{projects.map((project: any) => (
|
||||
<ProjectCard
|
||||
|
||||
@@ -3,19 +3,17 @@
|
||||
import React, { useEffect, useState, useMemo } from "react"
|
||||
import Image from "next/image"
|
||||
import NoResultIcon from "@/public/icons/no-result.svg"
|
||||
import { cva } from "class-variance-authority"
|
||||
|
||||
import { ProjectInterface, ProjectStatus } from "@/lib/types"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { LABELS } from "@/app/labels"
|
||||
import { useProjects } from "@/app/providers/ProjectsProvider"
|
||||
|
||||
import ResearchCard from "./research-card"
|
||||
import Link from "next/link"
|
||||
|
||||
const sectionTitleClass = cva(
|
||||
"relative font-sans text-base font-bold uppercase tracking-[3.36px] text-anakiwa-950 after:ml-8 after:absolute after:top-1/2 after:h-[1px] after:w-full after:translate-y-1/2 after:bg-anakiwa-300 after:content-[''] dark:text-white"
|
||||
)
|
||||
import {
|
||||
SectionWrapper,
|
||||
SectionWrapperTitle,
|
||||
} from "@/app/components/wrappers/SectionWrapper"
|
||||
|
||||
const NoResults = () => {
|
||||
return (
|
||||
@@ -38,10 +36,9 @@ const ProjectStatusOrderList = ["active", "maintained", "inactive"]
|
||||
export const ResearchList = () => {
|
||||
const [isMounted, setIsMounted] = useState(false)
|
||||
|
||||
const { researchs, searchQuery, queryString } = useProjects()
|
||||
const { researchs } = useProjects()
|
||||
|
||||
const noItems = researchs?.length === 0
|
||||
const hasActiveFilters = searchQuery !== "" || queryString !== ""
|
||||
|
||||
useEffect(() => {
|
||||
setIsMounted(true)
|
||||
@@ -65,14 +62,11 @@ export const ResearchList = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-10">
|
||||
<div className="flex flex-col gap-6 overflow-hidden">
|
||||
<div
|
||||
className={cn(
|
||||
"after:left-[100px] lg:after:left-[200px]",
|
||||
sectionTitleClass()
|
||||
)}
|
||||
<SectionWrapperTitle
|
||||
className={"after:left-[100px] lg:after:left-[200px]"}
|
||||
>
|
||||
<div className="h-3 lg:h-4 w-[120px] lg:w-[220px] bg-gray-200 animate-pulse rounded-lg"></div>
|
||||
</div>
|
||||
</SectionWrapperTitle>
|
||||
</div>
|
||||
<div className="grid items-start justify-between w-full grid-cols-1 gap-2 md:grid-cols-3 md:gap-6 ">
|
||||
<div className="min-h-[200px] border border-gray-200 bg-gray-200 animate-pulse rounded-lg overflow-hidden"></div>
|
||||
@@ -92,14 +86,7 @@ export const ResearchList = () => {
|
||||
data-section="active-researchs"
|
||||
className="flex flex-col justify-between gap-10"
|
||||
>
|
||||
<div className={cn("flex w-full flex-col gap-10")}>
|
||||
{!hasActiveFilters && (
|
||||
<div className="flex flex-col gap-6 overflow-hidden">
|
||||
<h3 className={cn(sectionTitleClass())}>
|
||||
{LABELS.RESEARCH_PAGE.ACTIVE_RESEARCH}
|
||||
</h3>
|
||||
</div>
|
||||
)}
|
||||
<SectionWrapper title={LABELS.RESEARCH_PAGE.ACTIVE_RESEARCH}>
|
||||
<div className="grid grid-cols-1 gap-4 md:gap-x-6 md:gap-y-10 lg:grid-cols-3">
|
||||
{activeResearchs.map((project: ProjectInterface) => (
|
||||
<ResearchCard
|
||||
@@ -114,13 +101,12 @@ export const ResearchList = () => {
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className={cn("flex w-full flex-col gap-10 pt-10")}>
|
||||
<div className="flex flex-col gap-6 overflow-hidden">
|
||||
<h3 className={cn(sectionTitleClass())}>
|
||||
{LABELS.RESEARCH_PAGE.PAST_RESEARCH}
|
||||
</h3>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
|
||||
<SectionWrapper
|
||||
title={LABELS.RESEARCH_PAGE.PAST_RESEARCH}
|
||||
className="pt-10"
|
||||
>
|
||||
<div className="flex flex-col gap-5">
|
||||
{pastResearchs.map((project: ProjectInterface) => (
|
||||
<Link
|
||||
@@ -132,7 +118,7 @@ export const ResearchList = () => {
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</SectionWrapper>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -109,7 +109,7 @@ function Hit({
|
||||
})
|
||||
|
||||
return (
|
||||
<span className="text-secondary font-sans text-base font-normal">
|
||||
<span className="text-secondary font-sans text-lg font-normal">
|
||||
{textContent}
|
||||
</span>
|
||||
)
|
||||
|
||||
@@ -73,7 +73,7 @@ export const HomepageVideoFeed = () => {
|
||||
<section className="mx-auto px-6 lg:px-8 py-10 lg:py-16 bg-tuatara-950 dark:bg-black">
|
||||
<AppContent className="flex flex-col gap-8 lg:max-w-[1200px] w-full">
|
||||
<div className="col-span-1 lg:col-span-4">
|
||||
<h2 className="font-sans text-base font-bold uppercase tracking-[4px] text-primary text-center">
|
||||
<h2 className="font-sans text-base font-bold uppercase tracking-[4px] text-white text-center">
|
||||
{LABELS.HOMEPAGE.VIDEOS}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
@@ -34,7 +34,6 @@ export const SiteHeaderMobile = () => {
|
||||
className="text-[#171C1B] dark:text-anakiwa-400"
|
||||
/>
|
||||
</button>
|
||||
|
||||
{header && (
|
||||
<div
|
||||
className="z-5 fixed inset-0 flex justify-end bg-black opacity-50"
|
||||
@@ -71,9 +70,19 @@ export const SiteHeaderMobile = () => {
|
||||
})}
|
||||
<button
|
||||
onClick={() => setIsDarkMode(!isDarkMode)}
|
||||
className="text-black dark:text-anakiwa-400 ml-auto mt-10"
|
||||
className=" ml-auto mt-10"
|
||||
>
|
||||
{isDarkMode ? <SunIcon size={20} /> : <MoonIcon size={20} />}
|
||||
{isDarkMode ? (
|
||||
<SunIcon
|
||||
className="text-white dark:text-anakiwa-400"
|
||||
size={20}
|
||||
/>
|
||||
) : (
|
||||
<MoonIcon
|
||||
className="text-white dark:text-anakiwa-400"
|
||||
size={20}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from "react"
|
||||
|
||||
interface AppContentProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
children: React.ReactNode
|
||||
children?: React.ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const AppContent = ({ children, className }: AppContentProps) => {
|
||||
export const AppContent = ({ children = null, className }: AppContentProps) => {
|
||||
return <div className={`container ${className}`}>{children}</div>
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { InputHTMLAttributes, forwardRef } from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { LucideIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const inputVariants = cva(
|
||||
[
|
||||
"rounded-md bg-zinc-50",
|
||||
"w-full rounded-md bg-zinc-50",
|
||||
"text-anakiwa-950 placeholder-anakiwa-950",
|
||||
"border-[1.5px] border-tuatara-200",
|
||||
"transition-colors duration-100 animate",
|
||||
@@ -30,16 +31,63 @@ const inputVariants = cva(
|
||||
|
||||
interface InputProps
|
||||
extends VariantProps<typeof inputVariants>,
|
||||
Omit<InputHTMLAttributes<HTMLInputElement>, "size" | "id" | "children"> {}
|
||||
Omit<InputHTMLAttributes<HTMLInputElement>, "size" | "id" | "children"> {
|
||||
icon?: LucideIcon
|
||||
iconPosition?: "left" | "right"
|
||||
onIconClick?: () => void
|
||||
}
|
||||
|
||||
const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
({ size, className, ...props }, ref) => {
|
||||
(
|
||||
{
|
||||
size,
|
||||
className,
|
||||
icon: Icon,
|
||||
iconPosition = "left",
|
||||
onIconClick,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
if (!Icon) {
|
||||
return (
|
||||
<input
|
||||
ref={ref}
|
||||
{...props}
|
||||
className={cn(inputVariants({ size, className }))}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<input
|
||||
ref={ref}
|
||||
{...props}
|
||||
className={cn(inputVariants({ size, className }))}
|
||||
/>
|
||||
<div className={cn("relative w-full", className)}>
|
||||
<input
|
||||
ref={ref}
|
||||
{...props}
|
||||
className={cn(
|
||||
inputVariants({ size, className }),
|
||||
iconPosition === "left" ? "pl-10" : "pr-10"
|
||||
)}
|
||||
/>
|
||||
{onIconClick ? (
|
||||
<button
|
||||
onClick={onIconClick}
|
||||
className={cn(
|
||||
"flex items-center justify-center absolute top-1/2 -translate-y-1/2 text-anakiwa-950 dark:text-anakiwa-300 size-4",
|
||||
iconPosition === "left" ? "left-3" : "right-3"
|
||||
)}
|
||||
>
|
||||
<Icon size={16} />
|
||||
</button>
|
||||
) : (
|
||||
<Icon
|
||||
className={cn(
|
||||
"absolute top-1/2 -translate-y-1/2 text-anakiwa-950 dark:text-anakiwa-300 size-4",
|
||||
iconPosition === "left" ? "left-3" : "right-3"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -563,7 +563,7 @@ const REACT_MARKDOWN_CONFIG = (darkMode: boolean): CustomComponents => ({
|
||||
if (containsMath(text)) {
|
||||
return (
|
||||
<p
|
||||
className={`${darkMode ? "text-white" : "text-secondary"} font-sans text-base font-normal ${isMathOnly ? "math-only" : ""}`}
|
||||
className={`${darkMode ? "text-white" : "text-secondary"} font-sans text-lg font-normal ${isMathOnly ? "math-only" : ""}`}
|
||||
>
|
||||
<MathText text={text} />
|
||||
</p>
|
||||
@@ -572,7 +572,7 @@ const REACT_MARKDOWN_CONFIG = (darkMode: boolean): CustomComponents => ({
|
||||
|
||||
return (
|
||||
<p
|
||||
className={`${darkMode ? "text-white" : "text-secondary"} font-sans text-base font-normal`}
|
||||
className={`${darkMode ? "text-white" : "text-secondary"} font-sans text-lg font-normal`}
|
||||
>
|
||||
{children}
|
||||
</p>
|
||||
@@ -591,31 +591,27 @@ const REACT_MARKDOWN_CONFIG = (darkMode: boolean): CustomComponents => ({
|
||||
|
||||
if (containsMath(text)) {
|
||||
return (
|
||||
<li
|
||||
className="text-secondary font-sans text-base font-normal"
|
||||
{...props}
|
||||
>
|
||||
<li className="text-secondary font-sans text-lg font-normal" {...props}>
|
||||
<MathText text={text} />
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<li className="text-secondary font-sans text-base font-normal" {...props}>
|
||||
<li className="text-secondary font-sans text-lg font-normal" {...props}>
|
||||
{children}
|
||||
</li>
|
||||
)
|
||||
},
|
||||
ul: ({ ordered, ...props }) =>
|
||||
createMarkdownElement(ordered ? "ol" : "ul", {
|
||||
className:
|
||||
"ml-6 list-disc text-secondary font-sans text-base font-normal",
|
||||
className: "ml-6 list-disc text-secondary font-sans text-lg font-normal",
|
||||
...props,
|
||||
}),
|
||||
ol: ({ ordered, ...props }) =>
|
||||
createMarkdownElement(ordered ? "ol" : "ul", {
|
||||
className:
|
||||
"list-decimal text-secondary font-sans text-base font-normal mt-3",
|
||||
"list-decimal text-secondary font-sans text-lg font-normal mt-3",
|
||||
...props,
|
||||
}),
|
||||
table: Table,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export type SiteConfig = typeof siteConfig
|
||||
|
||||
export const siteConfig = {
|
||||
name: "Privacy & Scaling Explorations",
|
||||
name: "PSE",
|
||||
description:
|
||||
"Enhancing Ethereum through cryptographic research and collective experimentation.",
|
||||
url: "https://pse.dev",
|
||||
|
||||
@@ -8,8 +8,6 @@ tags: ["ethereum", "privacy", "pir"]
|
||||
projects: ["semaphore", "scaling-semaphore-pir"]
|
||||
---
|
||||
|
||||
# Ethereum Privacy: Private Information Retrieval
|
||||
|
||||
_Thanks to [Cperezz](https://github.com/cperezz), [Vivian](https://github.com/vplasencia) and [Oskar](https://github.com/oskarth) for feedback and review._
|
||||
|
||||
Ethereum presents several [privacy challenges](https://hackmd.io/@pcaversaccio/ethereum-privacy-the-road-to-self-sovereignty#Status-Quo). One of them is that read operations can expose user data and behavior patterns, potentially leading to [deanonymization](https://en.wikipedia.org/wiki/Data_re-identification). In this scenario, users are safe only if they run their own node or host the data locally. Otherwise, all requests go through third parties, such as relayers, providers, and [wallets that process IP addresses](https://consensys.io/privacy-notice).
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.5",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@tanstack/react-query": "^5.74.4",
|
||||
"@tw-classed/react": "^1.8.0",
|
||||
"@types/prismjs": "^1.26.5",
|
||||
"algoliasearch": "^4",
|
||||
"class-variance-authority": "^0.4.0",
|
||||
|
||||
@@ -13,11 +13,6 @@
|
||||
--gradient-research-card: linear-gradient(84deg, #FFF -1.95%, #EAFAFF 59.98%, #FFF 100.64%);
|
||||
--skeleton-background: #f3f4f6;
|
||||
|
||||
|
||||
/* TODO: */
|
||||
|
||||
|
||||
|
||||
--active-selection: #50C3E0;
|
||||
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
@@ -66,12 +61,6 @@
|
||||
--gradient-research-card: linear-gradient(179deg, #29ACCE -202.54%, rgba(0, 0, 0, 0.00) 192.47%);
|
||||
--skeleton-background: #175e75;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* TODO: */
|
||||
--active-selection: #E1523A;
|
||||
|
||||
--primary: #E1523A;
|
||||
|
||||
19
yarn.lock
19
yarn.lock
@@ -1804,6 +1804,24 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tw-classed/core@npm:1.7.0":
|
||||
version: 1.7.0
|
||||
resolution: "@tw-classed/core@npm:1.7.0"
|
||||
checksum: 10/7a3e1fb154a2297276d46d0be5847124b01dcea30e13b2b6b82cf2b99b8dacd59f6cbb88124a1dfffae274d71eb62c38df1086abd9485d69e0327186c990f272
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tw-classed/react@npm:^1.8.0":
|
||||
version: 1.8.0
|
||||
resolution: "@tw-classed/react@npm:1.8.0"
|
||||
dependencies:
|
||||
"@tw-classed/core": "npm:1.7.0"
|
||||
peerDependencies:
|
||||
react: ">=16.8.0"
|
||||
checksum: 10/7bfc82d7d099da8429e8178860a92b6f08987e84e8b94d3167aa03bae1619c4479712a9696f1fcaede54267b20856aed9914cb958978a4cf6a72ca19767fcb05
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tybys/wasm-util@npm:^0.9.0":
|
||||
version: 0.9.0
|
||||
resolution: "@tybys/wasm-util@npm:0.9.0"
|
||||
@@ -6795,6 +6813,7 @@ __metadata:
|
||||
"@radix-ui/react-dropdown-menu": "npm:^2.0.5"
|
||||
"@radix-ui/react-slot": "npm:^1.0.2"
|
||||
"@tanstack/react-query": "npm:^5.74.4"
|
||||
"@tw-classed/react": "npm:^1.8.0"
|
||||
"@types/js-yaml": "npm:^4.0.9"
|
||||
"@types/node": "npm:^17.0.45"
|
||||
"@types/prismjs": "npm:^1.26.5"
|
||||
|
||||
Reference in New Issue
Block a user