diff --git a/app/[lang]/projects/[id]/page.tsx b/app/[lang]/projects/[id]/page.tsx index b27af73e..7d6c7589 100644 --- a/app/[lang]/projects/[id]/page.tsx +++ b/app/[lang]/projects/[id]/page.tsx @@ -6,13 +6,18 @@ 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 { siteConfig } from "@/config/site" import { ProjectInterface } from "@/lib/types" -import { Markdown } from "@/components/ui/markdown" +import { AppContent } from "@/components/ui/app-content" +import { Markdown, createMarkdownElement } from "@/components/ui/markdown" +import { WikiCard } from "@/components/cards/wiki-card" import { Divider } from "@/components/divider" import { Icons } from "@/components/icons" 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 { ThemesStatusMapping } from "@/components/project/project-filters-bar" +import { WikiSideNavigation } from "@/components/wiki-side-navigation" import { useTranslation } from "@/app/i18n" import { LocaleTypes } from "@/app/i18n/settings" @@ -54,12 +59,12 @@ export async function generateMetadata( } export default async function ProjectDetailPage({ params }: PageProps) { - const currProject = projects.filter( + const currProject: ProjectInterface = projects.filter( (project) => String(project.id) === params.id )[0] const lang = params?.lang as LocaleTypes const { t } = await useTranslation(lang, "common") - const { t: tProject } = await useTranslation( + const { t: projectTranslation } = await useTranslation( lang, "projects/" + currProject.id ) @@ -67,102 +72,132 @@ export default async function ProjectDetailPage({ params }: PageProps) { const { github, twitter, website } = currProject.links ?? {} const hasSocialLinks = Object.keys(currProject?.links ?? {}).length > 0 + const editPageURL = siteConfig?.editProjectPage(currProject.id, lang) + return (
-
-
-
-
- - - - {t("projectLibrary")} - - -
-

- {currProject.name} -

-

- {tProject("tldr")} -

-
-
- {hasSocialLinks && ( -
- {github && ( - -
- -

Github

-
- - )} - {website && ( - -
- -

Website

-
- - )} - {twitter && ( - -
- -

Twitter

-
+ +
+
+ +
+ +
+
+
+
+ + + + {t("projectLibrary")} + +
+

+ {currProject.name} +

+

+ {projectTranslation("tldr")} +

+
+
+ {hasSocialLinks && ( +
+ {github && ( + +
+ +

Github

+
+ + )} + {website && ( + +
+ +

Website

+
+ + )} + {twitter && ( + +
+ +

Twitter

+
+ + )} +
)} +
- )} -
-
-
-
- {`${currProject.name} - {!currProject?.image && ( - - {currProject?.imageAlt || currProject?.name} - - )} -
- -
- {tProject("description")} +
+
+ {typeof currProject?.description === "string" && ( + + createMarkdownElement("p", { + className: + "text-tuatara-700 font-sans text-lg font-normal", + ...props, + }), + }} + > + {projectTranslation("description")} + + )} + +
+ +
- +
+ +
+ + + + {t("editThisPage")} + +
-
+
+
diff --git a/app/i18n/locales/en/common.json b/app/i18n/locales/en/common.json index 77809a60..c2fbc847 100644 --- a/app/i18n/locales/en/common.json +++ b/app/i18n/locales/en/common.json @@ -31,7 +31,10 @@ "builtWith": "Built with", "themes": "Themes selected", "projectStatus": "Project status", - "fundingSource": "Funding source" + "fundingSource": "Funding source", + "funding": "Funding", + "license": "License", + "projectType": "Project type" }, "error": { "404": { @@ -83,5 +86,8 @@ "connectWithUsOnPlatform": "Connect with us on {{platform}}", "addResource": "Add a resource", "notCurrentlyActive": "Not Currently Active", - "joinOurDiscord": "Join our discord" -} \ No newline at end of file + "joinOurDiscord": "Join our discord", + "prevBrandImage": "Previous branding", + "editThisPage": "Edit this page", + "contents": "Contents" +} diff --git a/app/i18n/locales/en/projects/semaphore.json b/app/i18n/locales/en/projects/semaphore.json index 2165e1f8..395d977d 100644 --- a/app/i18n/locales/en/projects/semaphore.json +++ b/app/i18n/locales/en/projects/semaphore.json @@ -1,4 +1,4 @@ { - "description": "Semaphore is a protocol that allows users to prove their membership in a group and transmit anonymous data, such as votes or feedback, without revealing their identities. It is designed for developers aiming to build privacy-preserving applications. Semaphore enables the creation of identities and their corresponding public value, which can be added to Merkle trees. This facilitates the authentication of anonymous user messages through zero-knowledge proofs, where membership is proven using Merkle proofs within the circuit. Key use cases include anonymous voting applications, receiving anonymous feedback from event attendees, and anonymous text messages. It is currently in production and is being used in a wide variety of projects.", + "description": "### Overview\n\n[Semaphore](https://github.com/semaphore-protocol/semaphore/tree/main) is a [zero-knowledge](https://z.cash/learn/what-are-zk-snarks/) protocol that allows you to cast a message (for example, a vote or endorsement) as a provable group member without revealing your identity. Additionally, it provides a simple mechanism to prevent double-signaling. Use cases include private voting, whistleblowing, anonymous DAOs and mixers.\n\nSemaphore is designed to be a simple and generic privacy layer for decentralized applications (dApps) on Ethereum. It encourages modular application design, allowing dApp developers to choose and customize the on-chain and off-chain components they need.\n\nThe core of the protocol is the circuit logic. In addition to circuits, Semaphore provides [Solidity contracts](https://github.com/semaphore-protocol/semaphore/tree/main/packages/contracts) and [JavaScript libraries](https://github.com/semaphore-protocol/semaphore/tree/main#-packages) that allow developers to generate zero-knowledge proofs and verify them with minimal effort.\n\n### Features\n\nWith Semaphore, you can allow your users to do the following:\n\n1. [Create a Semaphore identity](https://docs.semaphore.pse.dev/guides/identities)\n2. [Add their Semaphore identity to a group (i.e. Merkle tree)](https://docs.semaphore.pse.dev/guides/groups)\n3. [Send a verifiable, anonymous message (e.g., a vote or endorsement)](https://docs.semaphore.pse.dev/guides/proofs)\n\nWhen a user broadcasts a message, Semaphore zero-knowledge proofs can ensure that the user has joined the group and hasn't already cast a message with their nullifier.\nSemaphore uses on-chain Solidity contracts and off-chain JavaScript libraries that work in tandem.\n\n* Off chain, JavaScript libraries can be used to create identities, manage groups, and generate proofs.\n* On chain, Solidity contracts can be used to manage groups and verify proofs.", "tldr": "A zero-knowledge protocol for anonymous interactions." } diff --git a/app/i18n/locales/en/projects/unirep-protocol.json b/app/i18n/locales/en/projects/unirep-protocol.json index 669f23d1..d1b73bb5 100644 --- a/app/i18n/locales/en/projects/unirep-protocol.json +++ b/app/i18n/locales/en/projects/unirep-protocol.json @@ -1,4 +1,4 @@ { - "description": "UniRep is a zero-knowledge protocol that securely manages user data through anonymous identifiers, enabling trustless interactions and enhanced user privacy in applications. UniRep expands the notion of reputation to include various user data aspects, such as preferences, activity, alignments, and ownership.\n\nUsing anonymous identifiers (epoch keys), the protocol allows for trustless engagement with applications while preserving user privacy. This approach promotes non-custodial applications that don't hold user data, reducing data breach risks and emphasizing security for both users and developers.", + "description": "### Overview\nUniRep is a zero-knowledge protocol that securely manages user data through anonymous identifiers, enabling trustless interactions and enhanced user privacy in applications. UniRep expands the notion of reputation to include various user data aspects, such as preferences, activity, alignments, and ownership.\n\nUsing anonymous identifiers [(epoch keys)](https://developer.unirep.io/docs/protocol/epoch-key) the protocol allows for trustless engagement with applications while preserving user privacy. This approach promotes non-custodial applications that don't hold user data, reducing data breach risks and emphasizing security for both users and developers.\n\nUniRep was originally proposed by BarryWhiteHat in this [ethresear.ch](https://ethresear.ch) post\n\n### Features\n\nUniRep aims to be the ultimate foundation for constructing tailored, yet fully compatible, zero-knowledge (zk) applications. It functions as a powerful memory layer for zk, offering private, non-repudiable data storage and retrieval capabilities. With UniRep, users can effortlessly receive data, prove facts about their information, and store the results while enjoying robust privacy assurances. The protocol empowers developers to create bespoke zk applications without compromising on interoperability and efficiency.\n\nKey UniRep features include:\n\n| | |\n| -------- | ------- |\n| **Data Storage** | Unirep allows small amounts of data to be associated with anonymous users. Applications can conditionally associate data, like requiring a user to prove control of an Ethereum address before attesting to it. |\n| **Extensible Proofs** | The system is designed to be extended with custom application logic. For example, an application might require proof of Ethereum address control to [sign up](https://github.com/Unirep/zketh/blob/b7e0fdf3dcc1b3f97673da20837ed9c7d3e27c9f/packages/circuits/circuits/signupWithAddress.circom). |\n| **Trustless Interoperability** | Applications can interconnect by having users create proofs using publicly available state. |\n| **No Forced Data Sharing** | Unirep applications cannot see what data belongs to what user, unless the user reveals it. User data also cannot be changed unless the user provides the application with an [epoch key](https://developer.unirep.io/docs/protocol/epoch-key). |\n\n### Applications\n\n - Anon Transfer - [Website](https://anon-transfer.online/) | [GitHub](https://github.com/vivianjeng/anon-transfer)\n - Trustlist - [Website](https://trustlist.xyz/) | [GitHub](https://github.com/trustlist/trustlist)\n - Unirep Social TW - [GitHub](https://github.com/social-tw/social-tw-website)\n - Unirep Social - [Website](https://unirep.social/) | [GitHub](https://github.com/Unirep/Unirep-Social)\n - Sacred Protocol - [Website](https://www.sacredprotocol.com/)\n - My-Badge - [GitHub](https://github.com/kittybest/my-badge)\n - Voteathon - [Website](https://voteathon.org/) | [GitHub](https://github.com/NicoSerranoP/voteathon)", "tldr": "A Zero-Knowledge Protocol built to handle anonymous user data." } diff --git a/components/app-link.tsx b/components/app-link.tsx new file mode 100644 index 00000000..6483c71e --- /dev/null +++ b/components/app-link.tsx @@ -0,0 +1,43 @@ +"use client" + +import React from "react" +import Link from "next/link" + +import { Icons } from "./icons" + +interface LinkProps extends React.AnchorHTMLAttributes { + children: React.ReactNode + href: string + to?: string + external?: boolean +} + +/** + * This component easily manages internal and external links and adds the necessary attributes. + * + * @param {string} href - The URL of the link. + * @param {React.ReactNode} children - The content of the link. + * @param {boolean} external - If the link is external, in this case it will open in a new tab and also add rel="noreferrer noopener nofollow". + */ +export const AppLink = ({ + href, + children, + external, + className, + ...props +}: LinkProps) => { + return ( + +
+ {children} + {external && } +
+ + ) +} diff --git a/components/cards/wiki-card.tsx b/components/cards/wiki-card.tsx new file mode 100644 index 00000000..0c376122 --- /dev/null +++ b/components/cards/wiki-card.tsx @@ -0,0 +1,161 @@ +"use client" + +import { ReactNode } from "react" +import Image from "next/image" + +import { ProjectInterface, ProjectSectionLabelMapping } from "@/lib/types" +import { cn } from "@/lib/utils" +import { useTranslation } from "@/app/i18n/client" + +import { AppLink } from "../app-link" +import { ThemesStatusMapping } from "../project/project-filters-bar" +import { Card } from "./card" + +interface WikiDetailProps { + label: string + value?: ReactNode +} + +interface WikiCardProps { + lang?: string + project: ProjectInterface + className?: string +} + +interface WikiLinkProps { + href: string + external?: boolean + children: ReactNode +} + +const WikiDetail = ({ label, value }: WikiDetailProps) => { + if (!value) return null + + return ( +
+
+ {label} +
+ {typeof value === "string" ? ( + + {value} + + ) : ( +
{value}
+ )} +
+ ) +} + +const WikiLink = ({ href, external, children }: WikiLinkProps) => { + return ( + + {children} + + ) +} + +export const WikiCard = ({ + project, + className = "", + lang = "en", +}: WikiCardProps) => { + const { t } = useTranslation(lang, "common") + const statusItem = ThemesStatusMapping(lang) + + const { website } = project.links ?? {} + + const projectType = ProjectSectionLabelMapping[project?.section] + const { label: projectStatus } = statusItem?.[project?.projectStatus] ?? {} + const builtWithKeys: string[] = project?.tags?.builtWith ?? [] + const previousBrandImage = project?.previousBrandImage + + return ( +
+
+ +
+ {`${project.name} + {!project?.image && ( + + {project?.imageAlt || project?.name} + + )} +
+
+ + + {builtWithKeys.map((key) => ( + + {key} + + ))} +
+ } + /> + + + + {website && ( + + {website} + + } + /> + )} +
+ + {previousBrandImage && ( + +
+ {`${project.name} + {!project?.image && ( + + {project?.imageAlt || project?.name} + + )} +
+
+ + {t("prevBrandImage")} + +
+
+ )} +
+ + ) +} diff --git a/components/icons.tsx b/components/icons.tsx index 28e2b69d..f530c140 100644 --- a/components/icons.tsx +++ b/components/icons.tsx @@ -646,4 +646,34 @@ export const Icons = { /> ), + externalPageUrl: ({ size = 10, ...props }: LucideProps) => ( + + + + ), + edit: ({ size = 10, ...props }: LucideProps) => ( + + + + ), } diff --git a/components/project/project-detail-tags.tsx b/components/project/project-detail-tags.tsx index cde29fc2..6d8bda3e 100644 --- a/components/project/project-detail-tags.tsx +++ b/components/project/project-detail-tags.tsx @@ -20,8 +20,10 @@ interface TagsProps extends HtmlHTMLAttributes { const TagsWrapper = ({ label, children }: TagsProps) => { return ( -
- {label} +
+ + {label} + {children}
) diff --git a/components/ui/markdown.tsx b/components/ui/markdown.tsx index ecfd9e71..324b1a67 100644 --- a/components/ui/markdown.tsx +++ b/components/ui/markdown.tsx @@ -2,20 +2,27 @@ import React from "react" import ReactMarkdown, { Components } from "react-markdown" import remarkGfm from "remark-gfm" -const createMarkdownElement = (tag: keyof JSX.IntrinsicElements, props: any) => +export const createMarkdownElement = ( + tag: keyof JSX.IntrinsicElements, + props: any +) => React.createElement(tag, { ...props, }) const Table = (props: any) => { - return {props.children}
+ return ( +
+ {props.children}
+
+ ) } // Styling for HTML attributes for markdown component const REACT_MARKDOWN_CONFIG: Components = { a: ({ node, ...props }) => createMarkdownElement("a", { - className: "text-orange", + className: "text-anakiwa-500 hover:text-orange duration-200", target: "_blank", ...props, }), @@ -49,18 +56,39 @@ const REACT_MARKDOWN_CONFIG: Components = { className: "text-neutral-800 text-md font-bold", ...props, }), + p: ({ node, ...props }) => + createMarkdownElement("p", { + className: "text-tuatara-700 font-sans text-lg font-normal", + ...props, + }), + ul: ({ node, ...props }) => + createMarkdownElement("ul", { + className: + "ml-6 list-disc text-tuatara-700 font-sans text-lg font-normal", + ...props, + }), + ol: ({ node, ...props }) => + createMarkdownElement("ol", { + className: + "ml-6 list-disc text-tuatara-700 font-sans text-lg font-normal", + ...props, + }), table: Table, } interface MarkdownProps { children: string + components?: Components // components overrides the default components } -export const Markdown = ({ children }: MarkdownProps) => { +export const Markdown = ({ children, components }: MarkdownProps) => { return ( {children} diff --git a/components/wiki-side-navigation.tsx b/components/wiki-side-navigation.tsx new file mode 100644 index 00000000..553a6205 --- /dev/null +++ b/components/wiki-side-navigation.tsx @@ -0,0 +1,85 @@ +"use client" + +import { useEffect, useState } from "react" +import { t } from "i18next" + +import { cn } from "@/lib/utils" +import { useTranslation } from "@/app/i18n/client" + +interface WikiSideNavigationProps { + className?: string + lang?: string + content?: string +} + +export const WikiSideNavigation = ({ + className, + lang = "en", + content = "", +}: WikiSideNavigationProps) => { + const { t } = useTranslation(lang, "common") + const [sections, setSections] = useState([]) + + // get content section with regex from the content + useEffect(() => { + if (!content) return + let match + const sectionsRegex = /^(#{1}|#{2}|#{3})\s(.+)/gm + + while ((match = sectionsRegex.exec(content)) !== null) { + const hasTitle = sections + ?.map((item: any) => item?.text) + .includes(match[2]) + if (hasTitle) return + sections.push({ level: match[1].length, text: match[2] }) + } + setSections(sections) + }, [content]) + + console.log("sectionTitles ", sections) + + if (sections?.length === 0) return null + return ( + + ) +} diff --git a/config/site.ts b/config/site.ts index c06ab5c7..1484fd15 100644 --- a/config/site.ts +++ b/config/site.ts @@ -29,4 +29,6 @@ export const siteConfig = { }, addGithubResource: "https://github.com/privacy-scaling-explorations/website-v2/blob/main/app/%5Blang%5D/content/resources.md", + editProjectPage: (id: string, locale = "en") => + `https://github.com/privacy-scaling-explorations/pse.dev/blob/main/app/i18n/locales/${locale}/projects/${id}.json`, } diff --git a/data/projects/semaphore.ts b/data/projects/semaphore.ts index 28e26e27..1e468e53 100644 --- a/data/projects/semaphore.ts +++ b/data/projects/semaphore.ts @@ -9,6 +9,8 @@ export const semaphore: ProjectInterface = { section: "pse", projectStatus: "active", image: "semaphore.webp", + previousBrandImage: "semaphorePrevious.jpg", + license: "MIT", name: "Semaphore", tldr: "A zero-knowledge protocol for anonymous interactions.", description, diff --git a/data/projects/unirep-protocol.ts b/data/projects/unirep-protocol.ts index a292234b..f415775b 100644 --- a/data/projects/unirep-protocol.ts +++ b/data/projects/unirep-protocol.ts @@ -11,6 +11,8 @@ export const unirepProtocol: ProjectInterface = { section: "pse", projectStatus: "inactive", image: "unirep.svg", + license: "MIT", + previousBrandImage: "unirep-previousBrand.png", name: "UniRep Protocol", tldr: "A Zero-Knowledge Protocol built to handle anonymous user data.", description, diff --git a/lib/types.ts b/lib/types.ts index 3d9af1aa..074b3c5e 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -72,8 +72,11 @@ export type ActionLinkType = Partial< export interface ProjectInterface { id: string + hasWiki?: boolean // show project with wiki page template + license?: string section: ProjectSection image: string + previousBrandImage?: string imageAlt?: string name: string tldr: string // this is managed by the specific translation file diff --git a/public/project-banners/semaphorePrevious.jpg b/public/project-banners/semaphorePrevious.jpg new file mode 100644 index 00000000..4bb92734 Binary files /dev/null and b/public/project-banners/semaphorePrevious.jpg differ diff --git a/public/project-banners/unirep-previousBrand.png b/public/project-banners/unirep-previousBrand.png new file mode 100644 index 00000000..75820d03 Binary files /dev/null and b/public/project-banners/unirep-previousBrand.png differ diff --git a/styles/globals.css b/styles/globals.css index a924f9c5..f95f37cd 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -101,20 +101,24 @@ */ @layer base { table[data-component="table"] { - @apply overflow-hidden rounded-lg table-auto bg-anakiwa-100; + @apply overflow-hidden !border !border-solid !border-tuatara-300 rounded-lg table-auto bg-white; + } + + table[data-component="table"] tr { + @apply !divide-solid !divide-x !divide-tuatara-300; } table[data-component="table"] thead > tr > th { @apply px-4 py-1 text-sm font-medium text-left text-anakiwa-500; - @apply border-b border-b-white; + @apply border border-tuatara-300; } table[data-component="table"] tbody > tr > td { - @apply px-4 py-1 text-xs font-medium text-left text-tuatara-700; + @apply px-4 py-1 text-sm font-medium text-left text-tuatara-700; } table[data-component="table"] tbody > tr:not(:last-child) { - @apply border-b border-b-white; + @apply border-b border-tuatara-300; } }