diff --git a/app/[lang]/projects/[id]/page.tsx b/app/[lang]/projects/[id]/page.tsx index 9d915266..514764b8 100644 --- a/app/[lang]/projects/[id]/page.tsx +++ b/app/[lang]/projects/[id]/page.tsx @@ -5,7 +5,6 @@ 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 { Divide } from "lucide-react" import { ProjectInterface } from "@/lib/types" import { Markdown } from "@/components/ui/markdown" diff --git a/app/[lang]/projects/[id]/wiki/page.tsx b/app/[lang]/projects/[id]/wiki/page.tsx new file mode 100644 index 00000000..24dbe784 --- /dev/null +++ b/app/[lang]/projects/[id]/wiki/page.tsx @@ -0,0 +1,192 @@ +import { Metadata, ResolvingMetadata } from "next" +import Image from "next/image" +import Link from "next/link" +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 { siteConfig } from "@/config/site" +import { ProjectInterface } from "@/lib/types" +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 ProjectExtraLinks from "@/components/project/project-extra-links" +import { WikiSideNavigation } from "@/components/wiki-side-navigation" +import { useTranslation } from "@/app/i18n" +import { LocaleTypes } from "@/app/i18n/settings" + +type PageProps = { + params: { id: string; lang: string } + searchParams: { [key: string]: string | string[] | undefined } +} + +export interface ProjectProps { + project: ProjectInterface + lang: LocaleTypes +} + +export async function generateMetadata( + { params }: PageProps, + parent: ResolvingMetadata +): Promise { + const currProject = projects.filter( + (project) => String(project.id) === params.id + )[0] + + const imageUrl = currProject.image + ? `/project-banners/${currProject.image}` + : "/og-image.png" + + return { + title: currProject.name, + description: currProject.tldr, + openGraph: { + images: [ + { + url: imageUrl, + width: 1200, + height: 630, + }, + ], + }, + } +} + +export default async function ProjectDetailPage({ params }: PageProps) { + 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 { github, twitter, website } = currProject.links ?? {} + const hasSocialLinks = Object.keys(currProject?.links ?? {}).length > 0 + + const editPageURL = siteConfig?.editProjectPage(currProject.id) + + return ( +
+ + +
+ +
+
+
+
+ + + + {t("projectLibrary")} + + +
+

+ {currProject.name} +

+

+ {currProject.tldr} +

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

Github

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

Website

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

Twitter

+
+ + )} +
+ )} +
+
+ +
+
+ {typeof currProject?.description === "string" && ( + + createMarkdownElement("p", { + className: + "text-tuatara-700 font-sans text-lg font-normal", + ...props, + }), + }} + > + {currProject.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/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..f970316c --- /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/ui/markdown.tsx b/components/ui/markdown.tsx index ecfd9e71..62f36e75 100644 --- a/components/ui/markdown.tsx +++ b/components/ui/markdown.tsx @@ -2,7 +2,10 @@ 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, }) @@ -15,7 +18,7 @@ const Table = (props: any) => { 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 +52,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 underline", + ...props, + }), + ol: ({ node, ...props }) => + createMarkdownElement("ol", { + className: + "ml-6 list-disc text-tuatara-700 font-sans text-lg font-normal underline", + ...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..25a3b8ea --- /dev/null +++ b/components/wiki-side-navigation.tsx @@ -0,0 +1,41 @@ +"use client" + +import { t } from "i18next" + +import { cn } from "@/lib/utils" +import { useTranslation } from "@/app/i18n/client" + +interface WikiSideNavigationProps { + className?: string + lang?: string +} + +export const WikiSideNavigation = ({ + className, + lang = "en", +}: WikiSideNavigationProps) => { + const { t } = useTranslation(lang, "common") + return ( + + ) +} diff --git a/config/site.ts b/config/site.ts index 22f41f2d..f260ad6c 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) => + `https://github.com/privacy-scaling-explorations/pse.dev/blob/main/data/projects/${id}.ts`, } diff --git a/lib/types.ts b/lib/types.ts index 3b801228..b58821d9 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