diff --git a/app/not-found.tsx b/app/not-found.tsx new file mode 100644 index 0000000..c3d7796 --- /dev/null +++ b/app/not-found.tsx @@ -0,0 +1,57 @@ +import "@/styles/globals.css" +import React from "react" +import Image from "next/image" +import Link from "next/link" + +import { fontDisplay, fontSans } from "@/lib/fonts" +import { Button } from "@/components/ui/button" +import { SiteHeader } from "@/components/site-header" +import { TailwindIndicator } from "@/components/tailwind-indicator" + +export default function NotFound() { + return ( + + + +
+ +
+
+
+
+ emotion sad + + 404 + +
+
+ + Oops! Page not found + + + The page you are looking for might have been removed, had + its name changed or is temporarily unavailable. + +
+
+ + + +
+
+
+ + + + ) +} diff --git a/app/projects/[id]/page.tsx b/app/projects/[id]/page.tsx index e82f926..4201edf 100644 --- a/app/projects/[id]/page.tsx +++ b/app/projects/[id]/page.tsx @@ -5,18 +5,15 @@ 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 { - FilterLabelMapping, - ProjectFilter, - filterProjects, -} from "@/state/useProjectFiltersState" +import { filterProjects } from "@/state/useProjectFiltersState" import { ProjectInterface } from "@/lib/types" import { shuffleArray } from "@/lib/utils" -import { CategoryTag } from "@/components/ui/categoryTag" import { Markdown } from "@/components/ui/markdown" import { Icons } from "@/components/icons" import ProjectCard from "@/components/project/project-card" +import { ProjectTags } from "@/components/project/project-detail-tags" +import ProjectExtraLinks from "@/components/project/project-extra-links" type PageProps = { params: { id: string } @@ -54,38 +51,6 @@ export async function generateMetadata( } } -function ProjectTags({ project }: ProjectProps) { - return ( -
- {Object.entries(FilterLabelMapping).map(([key, label]) => { - const keyTags = project?.tags?.[key as ProjectFilter] - const hasItems = keyTags && keyTags?.length > 0 - - return ( - hasItems && ( -
-
- {label} -
- {keyTags?.map((tag) => { - return ( - - - {tag} - - - ) - })} -
-
-
- ) - ) - })} -
- ) -} - function DiscoverMoreProjects({ project }: ProjectProps) { const getSuggestedProjects = () => { const projectList = projects.filter((p) => p.id !== project.id) @@ -194,9 +159,12 @@ export default function ProjectDetailPage({ params }: PageProps) {
-
+
{currProject.description}
+
+ +
diff --git a/components/icons.tsx b/components/icons.tsx index 6b71900..8b86d8c 100644 --- a/components/icons.tsx +++ b/components/icons.tsx @@ -220,4 +220,19 @@ export const Icons = { /> ), + arrowDown: (props: LucideProps) => ( + + + + ), } diff --git a/components/project/project-detail-tags.tsx b/components/project/project-detail-tags.tsx index 49fdc6f..b30cdf5 100644 --- a/components/project/project-detail-tags.tsx +++ b/components/project/project-detail-tags.tsx @@ -18,7 +18,7 @@ interface TagsProps extends HtmlHTMLAttributes { const TagsWrapper = ({ label, children }: TagsProps) => { return ( -
+
{label} {children}
@@ -40,7 +40,7 @@ export function ProjectTags({ project }: { project: ProjectInterface }) { hasItems && (
-
+
{keyTags?.map((tag) => { return ( @@ -57,7 +57,7 @@ export function ProjectTags({ project }: { project: ProjectInterface }) { ) })} - +
{icon} {label} diff --git a/components/project/project-extra-links.tsx b/components/project/project-extra-links.tsx new file mode 100644 index 0000000..2a4d692 --- /dev/null +++ b/components/project/project-extra-links.tsx @@ -0,0 +1,96 @@ +import React from "react" +import Link from "next/link" + +import { + ActionLinkTypeLink, + ProjectExtraLinkType, + ProjectInterface, +} from "@/lib/types" + +import { Icons } from "../icons" + +const ExtraLinkLabelMapping: Record< + ProjectExtraLinkType, + { + label: string + icon?: any + } +> = { + buildWith: { + label: "Build with this tool", + icon: , + }, + play: { + label: "Try it out!", + icon: , + }, + research: { + label: "Dive deeper into the research", + icon: , + }, + learn: { + label: "Learn more", + }, +} + +interface ProjectExtraLinksProps { + project: ProjectInterface +} + +interface ExtraLinkItemsProps { + id: ProjectExtraLinkType + links: ActionLinkTypeLink[] +} + +const ExtraLinkItems = ({ id, links = [] }: ExtraLinkItemsProps) => { + const { label, icon } = ExtraLinkLabelMapping[id] + + if (!links.length) return null // no links hide the section + + return ( +
+
+ {icon && {icon}} +

+ {label} +

+
+
+ {links.map(({ label, url }: ActionLinkTypeLink) => { + return ( + + {label} + + + ) + })} +
+
+ ) +} + +export default function ProjectExtraLinks({ project }: ProjectExtraLinksProps) { + const { extraLinks = {} } = project + const hasExtraLinks = Object.keys(extraLinks).length > 0 + + if (!hasExtraLinks) return null + + return ( +
+ {Object.entries(ExtraLinkLabelMapping).map(([key]) => { + const links = extraLinks[key as ProjectExtraLinkType] ?? [] + return ( + + ) + })} +
+ ) +} diff --git a/components/project/project-list.tsx b/components/project/project-list.tsx index a788bd8..4157e89 100644 --- a/components/project/project-list.tsx +++ b/components/project/project-list.tsx @@ -17,7 +17,8 @@ const NoResults = () => { No results found. - {`Sorry, we couldn't find any results for your search. Please try again with different keywords.`} + {`Sorry, we couldn't find any results for your search. Please try again + with different keywords.`}
) diff --git a/components/project/project-result-bar.tsx b/components/project/project-result-bar.tsx index aed40ba..7bc837e 100644 --- a/components/project/project-result-bar.tsx +++ b/components/project/project-result-bar.tsx @@ -2,61 +2,79 @@ import { ProjectFilter, + ProjectSortBy, useProjectFiltersState, } from "@/state/useProjectFiltersState" import { CategoryTag } from "../ui/categoryTag" +import { Dropdown } from "../ui/dropdown" const labelClass = "h-5 text-xs text-base md:h-6 text-slate-900/70 md:text-lg" +const projectSortItems: { label: string; value: ProjectSortBy }[] = [ + { label: "Random", value: "random" }, + { label: "Title: A-Z", value: "asc" }, + { label: "Title: Z-A", value: "desc" }, + { label: "Relevance", value: "relevance" }, +] + +const getSortLabel = (sortBy: ProjectSortBy) => { + return projectSortItems.find((item) => item.value === sortBy)?.label || sortBy +} + export const ProjectResultBar = () => { - const { activeFilters, toggleFilter, projects } = useProjectFiltersState( - (state) => state - ) + const { activeFilters, toggleFilter, projects, sortProjectBy, sortBy } = + useProjectFiltersState((state) => state) const haveActiveFilters = Object.entries(activeFilters).some( ([_key, values]) => values?.length > 0 ) - if (!haveActiveFilters) - return ( - - {`Showing ${projects.length} projects`}{" "} - - ) + const resultLabel = haveActiveFilters + ? `Showing ${projects?.length} projects with:` + : `Showing ${projects.length} projects` return (
- - {`Showing ${projects?.length} projects with:`}{" "} - -
- {Object.entries(activeFilters).map(([key, filters], index) => { - return ( - <> - {filters?.map((filter) => { - if (filter?.length === 0) return null - - return ( - - toggleFilter({ - tag: key as ProjectFilter, - value: filter, - }) - } - key={`${index}-${filter}`} - > - {filter} - - ) - })} - - ) - })} +
+ {resultLabel} + sortProjectBy(sortBy as ProjectSortBy)} + disabled={!projects?.length} + />
+ {haveActiveFilters && ( +
+ {Object.entries(activeFilters).map(([key, filters], index) => { + return ( + <> + {filters?.map((filter) => { + if (filter?.length === 0) return null + + return ( + + toggleFilter({ + tag: key as ProjectFilter, + value: filter, + }) + } + key={`${index}-${filter}`} + > + {filter} + + ) + })} + + ) + })} +
+ )}
) } diff --git a/components/ui/button.tsx b/components/ui/button.tsx index 57ecf84..1ee0b5f 100644 --- a/components/ui/button.tsx +++ b/components/ui/button.tsx @@ -6,7 +6,7 @@ import { LucideIcon } from "lucide-react" import { cn } from "@/lib/utils" const buttonVariants = cva( - "inline-flex items-center justify-center duration-100 rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background", + "font-sans inline-flex items-center justify-center duration-100 rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background", { variants: { variant: { diff --git a/components/ui/categoryTag.tsx b/components/ui/categoryTag.tsx index c3b65b1..6e87518 100644 --- a/components/ui/categoryTag.tsx +++ b/components/ui/categoryTag.tsx @@ -6,7 +6,7 @@ import { cn } from "@/lib/utils" import { Icons } from "../icons" const categoryTagVariants = cva( - "flex p-[6px] gap-2 rounded-[6px] inline-flex items-center", + "flex gap-2 rounded-[6px] inline-flex items-center", { variants: { variant: { diff --git a/components/ui/dropdown.tsx b/components/ui/dropdown.tsx new file mode 100644 index 0000000..fce7dbd --- /dev/null +++ b/components/ui/dropdown.tsx @@ -0,0 +1,87 @@ +import React, { useState } from "react" +import * as DropdownMenu from "@radix-ui/react-dropdown-menu" + +import { cn } from "@/lib/utils" + +import { Icons } from "../icons" + +interface DropdownItemProps { + label: string + value?: string | number +} + +interface DropdownProps { + label: string + items?: DropdownItemProps[] + defaultItem?: string | number + onChange?: (value: DropdownItemProps["value"]) => void + disabled?: boolean +} + +const Dropdown = ({ + label, + onChange, + defaultItem, + disabled, + items, +}: DropdownProps) => { + const [selected, setSelected] = + useState(defaultItem) + + const onSelectCallback = ({ value }: DropdownItemProps) => { + setSelected(value) + if (typeof onChange === "function") onChange(value) + } + + return ( + + + + + + + + {items?.map((item, index) => { + const active = selected === item.value + return ( + onSelectCallback(item)} + > + {active && ( +
+ )} + {item.label} +
+ ) + })} +
+
+
+ ) +} + +Dropdown.displayName = "Dropdown" +export { Dropdown } diff --git a/data/projects/README.md b/data/projects/README.md new file mode 100644 index 0000000..7add7b7 --- /dev/null +++ b/data/projects/README.md @@ -0,0 +1,73 @@ +## Add new project to projects list + +1. Add new file inside `[...]/data/projects.ts` folder +2. Add project details inside the file already created, to easily include all the required parameters make sure to use the `ProjectInterface` + +```js +export const example: ProjectInterface = { + id: "example", + image: "", + name: "This is an example of the project", + tags: { + keywords: [], + themes: [], + types: [], + builtWith: [], + }, +} +``` + +3. Include the exported constant of the project in `projects.ts` + +## Show badges in the project card + +Badges can be set by setting the `themes` params, by looking at this example + +```js +export const example: ProjectInterface = { + id: "example", + image: "", + name: "This is an example of the project", + tags: { + themes: ["play", "build"], + }, +} +``` + +This is the result + +![Project card badge](/public/project/example-project-badge.png) + +## Show links in project page detail + +To add extra link to projects we need to add `extraLinks` for the projects we are going to add links for. +Make sure that for every "themes" value there is a specific "extraLinks" object will all the links. + +```js +export const example: ProjectInterface = { + id: "example", + image: "", + name: "This is an example of the project", + tags: { + themes: ["play", "buildWith"], + }, + extraLinks: { + buildWith: [ + { + label: "Link to get started", + url: 'https://google.it"', + }, + ], + play: [ + { + label: "Link to get started", + url: 'https://google.it"', + }, + ], + }, +} +``` + +This is the result + +![Project links](/public/project/example-project-extra-link.png) diff --git a/data/projects/anon-aadhaar.ts b/data/projects/anon-aadhaar.ts index f39d558..d621b94 100644 --- a/data/projects/anon-aadhaar.ts +++ b/data/projects/anon-aadhaar.ts @@ -7,7 +7,7 @@ Anon Aadhaar is a project that allows individuals to prove their citizenship ano export const anonAadhaar: ProjectInterface = { id: "anon-aadhaar", projectStatus: "active", - image: "", + image: "anon-aadhaar.svg", name: "Anon Aadhaar", tldr: "Tools for building build privacy-preserving applications using government ID cards, specifically Aadhaar cards in India.", description, @@ -17,7 +17,7 @@ export const anonAadhaar: ProjectInterface = { tags: { keywords: ["Anonymity/privacy", "Social", "Identity", "Voting/governance"], themes: ["build", "play"], - type: ["Legos/dev tools", "Lego sets/toolkits", "Proof of concept"], + types: ["Legos/dev tools", "Lego sets/toolkits", "Proof of concept"], builtWith: ["circom", "rsa"], }, } diff --git a/data/projects/anon-klub.ts b/data/projects/anon-klub.ts index 2d98868..0e192c5 100644 --- a/data/projects/anon-klub.ts +++ b/data/projects/anon-klub.ts @@ -24,7 +24,7 @@ export const anonKlub: ProjectInterface = { "Voting/governance", ], themes: ["build", "play"], - type: ["Infrastructure/protocol", "Prototype", "Proof of concept"], + types: ["Infrastructure/protocol", "Prototype", "Proof of concept"], builtWith: ["circom", "snarkjs", "halo2"], }, } diff --git a/data/projects/bandada.ts b/data/projects/bandada.ts index d94cfbe..c5f4122 100644 --- a/data/projects/bandada.ts +++ b/data/projects/bandada.ts @@ -13,7 +13,7 @@ export const bandada: ProjectInterface = { description, links: { github: "https://github.com/privacy-scaling-explorations/bandada", - website: "https://bandada.appliedzkp.org/", + website: "https://bandada.pse.dev", }, tags: { keywords: [ @@ -27,7 +27,7 @@ export const bandada: ProjectInterface = { "Scaling", "Key management", ], - type: [ + types: [ "Legos/dev tools", "Lego sets/toolkits", "Prototype", diff --git a/data/projects/channel-4.ts b/data/projects/channel-4.ts index 20fce33..121f9de 100644 --- a/data/projects/channel-4.ts +++ b/data/projects/channel-4.ts @@ -19,7 +19,7 @@ export const channel4: ProjectInterface = { tags: { keywords: ["Scaling"], themes: ["play"], - type: ["Application"], + types: ["Application"], builtWith: ["state channel", "smart contract"], }, } diff --git a/data/projects/dsl-working-group.ts b/data/projects/dsl-working-group.ts index cc28ba2..1728b3e 100644 --- a/data/projects/dsl-working-group.ts +++ b/data/projects/dsl-working-group.ts @@ -15,7 +15,7 @@ export const dslWorkingGroup: ProjectInterface = { github: "https://github.com/privacy-scaling-explorations/chiquito/", }, tags: { - type: ["Legos/dev tools", "Proof of concept", "Developer tooling"], + types: ["Legos/dev tools", "Proof of concept", "Developer tooling"], keywords: [], themes: ["research"], builtWith: [], diff --git a/data/projects/eigen-trust.ts b/data/projects/eigen-trust.ts index edbe216..d39187e 100644 --- a/data/projects/eigen-trust.ts +++ b/data/projects/eigen-trust.ts @@ -17,7 +17,7 @@ export const eigenTrust: ProjectInterface = { tags: { keywords: ["Reputation", "Identity"], themes: ["build"], - type: ["Infrastructure/protocol"], + types: ["Infrastructure/protocol"], builtWith: ["ethereum attestation service", "halo2", "ethers.rs"], }, } diff --git a/data/projects/pollen-labs.ts b/data/projects/pollen-labs.ts index a2ffbea..60816ae 100644 --- a/data/projects/pollen-labs.ts +++ b/data/projects/pollen-labs.ts @@ -18,7 +18,7 @@ export const pollenLabs: ProjectInterface = { tags: { keywords: ["Anonymity/privacy", "Scaling"], themes: ["play"], - type: ["Application"], + types: ["Application"], builtWith: [], }, } diff --git a/data/projects/rln.ts b/data/projects/rln.ts index 80beecf..7fe248b 100644 --- a/data/projects/rln.ts +++ b/data/projects/rln.ts @@ -7,7 +7,7 @@ Rate-Limiting Nullifier (RLN) is a protocol designed to combat spam and denial o export const rln: ProjectInterface = { id: "rln", projectStatus: "active", - image: "rln.webp", + image: "rln.svg", name: "Rate-Limiting Nullifier", tldr: "A protocol for deterring spam and maintaining anonymity in communication systems.", description, diff --git a/data/projects/summa.ts b/data/projects/summa.ts index 6071b03..a1dbdea 100644 --- a/data/projects/summa.ts +++ b/data/projects/summa.ts @@ -17,7 +17,7 @@ export const summa: ProjectInterface = { tags: { keywords: ["Anonymity/privacy", "Computational Integrity"], themes: ["build", "play"], - type: ["Infrastructure/protocol", "Application"], + types: ["Infrastructure/protocol", "Application"], builtWith: ["halo2"], }, } diff --git a/data/projects/tlsn.ts b/data/projects/tlsn.ts index e7dc2b8..fbe14b2 100644 --- a/data/projects/tlsn.ts +++ b/data/projects/tlsn.ts @@ -18,7 +18,7 @@ export const tlsn: ProjectInterface = { }, tags: { themes: ["build"], - type: [], + types: [], builtWith: [], keywords: [], }, diff --git a/data/projects/trusted-setups.ts b/data/projects/trusted-setups.ts index 9560cf3..e08b792 100644 --- a/data/projects/trusted-setups.ts +++ b/data/projects/trusted-setups.ts @@ -15,7 +15,7 @@ export const trustedSetups: ProjectInterface = { }, tags: { themes: ["play"], - type: ["Legos/dev tools", "Lego sets/toolkits"], + types: ["Legos/dev tools", "Lego sets/toolkits"], builtWith: [], keywords: ["Scaling", "Education"], }, diff --git a/data/projects/unirep-protocol.ts b/data/projects/unirep-protocol.ts index 1496ced..12299dd 100644 --- a/data/projects/unirep-protocol.ts +++ b/data/projects/unirep-protocol.ts @@ -20,7 +20,7 @@ export const unirepProtocol: ProjectInterface = { tags: { keywords: ["Anonymity/privacy", "Social", "Identity", "Reputation"], themes: ["build"], - type: ["Legos/dev tools, Protocol"], + types: ["Legos/dev tools, Protocol"], builtWith: ["semaphore", "circom"], }, } diff --git a/data/projects/zk3.ts b/data/projects/zk3.ts index 7950a27..1b4feb5 100644 --- a/data/projects/zk3.ts +++ b/data/projects/zk3.ts @@ -18,7 +18,7 @@ export const zk3: ProjectInterface = { }, tags: { themes: ["play"], - type: [ + types: [ "Legos/dev tools", "Lego sets/toolkits", "Infrastructure/protocol", diff --git a/data/projects/zkml.ts b/data/projects/zkml.ts index a40396d..6c7e43b 100644 --- a/data/projects/zkml.ts +++ b/data/projects/zkml.ts @@ -17,7 +17,7 @@ export const zkml: ProjectInterface = { tags: { keywords: ["Anonymity/privacy", "Scaling"], themes: ["research"], - type: ["Proof of concept", "Infrastructure/protocol"], + types: ["Proof of concept", "Infrastructure/protocol"], builtWith: ["circom", "halo2", "nova"], }, } diff --git a/data/projects/zkp2p.ts b/data/projects/zkp2p.ts index 2deffc1..12d5a56 100644 --- a/data/projects/zkp2p.ts +++ b/data/projects/zkp2p.ts @@ -18,8 +18,8 @@ export const zkp2p: ProjectInterface = { }, tags: { keywords: ["Private communications"], - theme: ["play"], - type: ["Proof of concept", "Application"], + themes: ["play"], + types: ["Proof of concept", "Application"], builtWith: ["circom", "halo2"], }, } diff --git a/lib/types.ts b/lib/types.ts index 8af98b9..9069ab5 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,3 +1,5 @@ +import { type } from "os" + export interface NewsInterface { type: string title: string @@ -17,15 +19,14 @@ export type ProjectLinkWebsite = export type ProjectLinkType = Partial> export type ProjectExtraLinkType = "buildWith" | "play" | "research" | "learn" - +export type TagType = "types" | "themes" | "builtWith" | "keywords" +export type ProjectTags = Partial> +export type ActionLinkTypeLink = { + label: string + url: string +} export type ActionLinkType = Partial< - Record< - ProjectExtraLinkType, - Array<{ - label: string - url: string - }> - > + Record> > export type ProjectStatusType = "active" | "inactive" | "archived" @@ -37,6 +38,6 @@ export interface ProjectInterface { description: string links?: ProjectLinkType projectStatus: ProjectStatusType - tags?: Record + tags?: ProjectTags extraLinks?: ActionLinkType } diff --git a/package.json b/package.json index 1f1e0fa..6355a62 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "dependencies": { "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-dialog": "^1.0.4", + "@radix-ui/react-dropdown-menu": "^2.0.5", "@radix-ui/react-slot": "^1.0.2", "class-variance-authority": "^0.4.0", "clsx": "^1.2.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 22d8ba9..6327498 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ dependencies: '@radix-ui/react-dialog': specifier: ^1.0.4 version: 1.0.4(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-dropdown-menu': + specifier: ^2.0.5 + version: 2.0.5(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-slot': specifier: ^1.0.2 version: 1.0.2(@types/react@18.2.7)(react@18.2.0) @@ -357,6 +360,34 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true + /@floating-ui/core@1.4.1: + resolution: {integrity: sha512-jk3WqquEJRlcyu7997NtR5PibI+y5bi+LS3hPmguVClypenMsCY3CBa3LAQnozRCtCrYWSEtAdiskpamuJRFOQ==} + dependencies: + '@floating-ui/utils': 0.1.1 + dev: false + + /@floating-ui/dom@1.5.1: + resolution: {integrity: sha512-KwvVcPSXg6mQygvA1TjbN/gh///36kKtllIF8SUm0qpFj8+rvYrpvlYdL1JoA71SHpDqgSSdGOSoQ0Mp3uY5aw==} + dependencies: + '@floating-ui/core': 1.4.1 + '@floating-ui/utils': 0.1.1 + dev: false + + /@floating-ui/react-dom@2.0.2(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-5qhlDvjaLmAst/rKb3VdlCinwTF4EYMiVxuuc/HVUjs46W0zgtbMmAZ1UTsDrRTxRmUEzl92mOtWbeeXL26lSQ==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + dependencies: + '@floating-ui/dom': 1.5.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /@floating-ui/utils@0.1.1: + resolution: {integrity: sha512-m0G6wlnhm/AX0H12IOWtK8gASEMffnX08RtKkCgTdHb9JpHKGloI7icFfLg9ZmQeavcvR0PKmzxClyuFPSjKWw==} + dev: false + /@humanwhocodes/config-array@0.11.8: resolution: {integrity: sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==} engines: {node: '>=10.10.0'} @@ -542,6 +573,27 @@ packages: '@babel/runtime': 7.22.3 dev: false + /@radix-ui/react-arrow@1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.22.3 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.7 + '@types/react-dom': 18.2.4 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-checkbox@1.0.4(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-CBuGQa52aAYnADZVt/KBQzXrwx6TqnlwtcIPGtVt5JkkzQwMOLJjPukimhfKEr4GQNd43C+djUh5Ikopj8pSLg==} peerDependencies: @@ -570,6 +622,30 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-collection@1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.22.3 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.7)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.7)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.7)(react@18.2.0) + '@types/react': 18.2.7 + '@types/react-dom': 18.2.4 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-compose-refs@1.0.1(@types/react@18.2.7)(react@18.2.0): resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==} peerDependencies: @@ -632,6 +708,20 @@ packages: react-remove-scroll: 2.5.5(@types/react@18.2.7)(react@18.2.0) dev: false + /@radix-ui/react-direction@1.0.1(@types/react@18.2.7)(react@18.2.0): + resolution: {integrity: sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.22.3 + '@types/react': 18.2.7 + react: 18.2.0 + dev: false + /@radix-ui/react-dismissable-layer@1.0.4(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-7UpBa/RKMoHJYjie1gkF1DlK8l1fdU/VKDpoS3rCCo8YBJR294GwcEHyxHw72yvphJ7ld0AXEcSLAzY2F/WyCg==} peerDependencies: @@ -657,6 +747,33 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-dropdown-menu@2.0.5(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-xdOrZzOTocqqkCkYo8yRPCib5OkTkqN7lqNCdxwPOdE466DOaNl4N8PkUIlsXthQvW5Wwkd+aEmWpfWlBoDPEw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.22.3 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.7)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.7)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.7)(react@18.2.0) + '@radix-ui/react-menu': 2.0.5(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.7)(react@18.2.0) + '@types/react': 18.2.7 + '@types/react-dom': 18.2.4 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-focus-guards@1.0.1(@types/react@18.2.7)(react@18.2.0): resolution: {integrity: sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==} peerDependencies: @@ -709,6 +826,74 @@ packages: react: 18.2.0 dev: false + /@radix-ui/react-menu@2.0.5(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-Gw4f9pwdH+w5w+49k0gLjN0PfRDHvxmAgG16AbyJZ7zhwZ6PBHKtWohvnSwfusfnK3L68dpBREHpVkj8wEM7ZA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.22.3 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.7)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.7)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.7)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.0.4(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.2.7)(react@18.2.0) + '@radix-ui/react-focus-scope': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.7)(react@18.2.0) + '@radix-ui/react-popper': 1.1.2(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-portal': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.7)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.7)(react@18.2.0) + '@types/react': 18.2.7 + '@types/react-dom': 18.2.4 + aria-hidden: 1.2.3 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-remove-scroll: 2.5.5(@types/react@18.2.7)(react@18.2.0) + dev: false + + /@radix-ui/react-popper@1.1.2(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-1CnGGfFi/bbqtJZZ0P/NQY20xdG3E0LALJaLUEoKwPLwl6PPPfbeiCqMVQnhoFRAxjJj4RpBRJzDmUgsex2tSg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.22.3 + '@floating-ui/react-dom': 2.0.2(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-arrow': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.7)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.7)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.7)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.7)(react@18.2.0) + '@radix-ui/react-use-rect': 1.0.1(@types/react@18.2.7)(react@18.2.0) + '@radix-ui/react-use-size': 1.0.1(@types/react@18.2.7)(react@18.2.0) + '@radix-ui/rect': 1.0.1 + '@types/react': 18.2.7 + '@types/react-dom': 18.2.4 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-portal@1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-xLYZeHrWoPmA5mEKEfZZevoVRK/Q43GfzRXkWV6qawIWWK8t6ifIiLQdd7rmQ4Vk1bmI21XhqF9BN3jWf+phpA==} peerDependencies: @@ -773,6 +958,35 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-roving-focus@1.0.4(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.22.3 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.7)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.7)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.7)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.7)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.7)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.7)(react@18.2.0) + '@types/react': 18.2.7 + '@types/react-dom': 18.2.4 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-slot@1.0.2(@types/react@18.2.7)(react@18.2.0): resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==} peerDependencies: @@ -860,6 +1074,21 @@ packages: react: 18.2.0 dev: false + /@radix-ui/react-use-rect@1.0.1(@types/react@18.2.7)(react@18.2.0): + resolution: {integrity: sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.22.3 + '@radix-ui/rect': 1.0.1 + '@types/react': 18.2.7 + react: 18.2.0 + dev: false + /@radix-ui/react-use-size@1.0.1(@types/react@18.2.7)(react@18.2.0): resolution: {integrity: sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g==} peerDependencies: @@ -875,6 +1104,12 @@ packages: react: 18.2.0 dev: false + /@radix-ui/rect@1.0.1: + resolution: {integrity: sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ==} + dependencies: + '@babel/runtime': 7.22.3 + dev: false + /@rushstack/eslint-patch@1.3.0: resolution: {integrity: sha512-IthPJsJR85GhOkp3Hvp8zFOPK5ynKn6STyHa/WZpioK7E1aYDiBzpqQPrngc14DszIUkIrdd3k9Iu0XSzlP/1w==} dev: true diff --git a/public/icons/404-search.svg b/public/icons/404-search.svg new file mode 100644 index 0000000..cd5f8fd --- /dev/null +++ b/public/icons/404-search.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/project-banners/anon-aadhaar.svg b/public/project-banners/anon-aadhaar.svg new file mode 100644 index 0000000..63244af --- /dev/null +++ b/public/project-banners/anon-aadhaar.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/project-banners/discreetly.svg b/public/project-banners/discreetly.svg index ab5727b..449beaf 100644 --- a/public/project-banners/discreetly.svg +++ b/public/project-banners/discreetly.svg @@ -1,4 +1,13 @@ - - - + + + + + + + + + + + + diff --git a/public/project-banners/rln.svg b/public/project-banners/rln.svg new file mode 100644 index 0000000..90f9940 --- /dev/null +++ b/public/project-banners/rln.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/public/project/example-project-badge.png b/public/project/example-project-badge.png new file mode 100644 index 0000000..b43cacc Binary files /dev/null and b/public/project/example-project-badge.png differ diff --git a/public/project/example-project-extra-link.png b/public/project/example-project-extra-link.png new file mode 100644 index 0000000..8ad0a1d Binary files /dev/null and b/public/project/example-project-extra-link.png differ diff --git a/state/useProjectFiltersState.ts b/state/useProjectFiltersState.ts index 08b2dae..399eb28 100644 --- a/state/useProjectFiltersState.ts +++ b/state/useProjectFiltersState.ts @@ -5,9 +5,24 @@ import { create } from "zustand" import { ProjectInterface } from "@/lib/types" import { uniq } from "@/lib/utils" +export type ProjectSortBy = "random" | "asc" | "desc" | "relevance" export type ProjectFilter = "keywords" | "builtWith" | "themes" export type FiltersProps = Record +interface ProjectInterfaceScore extends ProjectInterface { + score: number +} + +export const SortByFnMapping: Record< + ProjectSortBy, + (a: ProjectInterfaceScore, b: ProjectInterfaceScore) => number +> = { + random: () => Math.random() - 0.5, + asc: (a, b) => a.name.localeCompare(b.name), + desc: (a, b) => b.name.localeCompare(a.name), + relevance: (a, b) => b?.score - a?.score, // sort from most relevant to least relevant +} + export const FilterLabelMapping: Record = { keywords: "Keywords", builtWith: "Built with", @@ -20,6 +35,7 @@ export const FilterTypeMapping: Record = { themes: "button", } interface ProjectStateProps { + sortBy: ProjectSortBy projects: ProjectInterface[] filters: FiltersProps activeFilters: Partial @@ -43,6 +59,7 @@ interface ProjectActionsProps { setFilterFromQueryString: (filters: Partial) => void onFilterProject: (searchPattern: string) => void onSelectTheme: (theme: string, searchPattern?: string) => void + sortProjectBy: (sortBy: ProjectSortBy) => void } const createURLQueryString = (params: Partial): string => { @@ -141,17 +158,34 @@ export const filterProjects = ({ const fuse = new Fuse(projectList, { threshold: 0.2, useExtendedSearch: true, + includeScore: true, keys, }) - const result = fuse.search(query)?.map(({ item }) => item) - + const result = fuse.search(query)?.map(({ item, score }) => { + return { + ...item, + score, // 0 indicates a perfect match, while a score of 1 indicates a complete mismatch. + } + }) return result ?? [] } +const sortProjectByFn = ( + projects: ProjectInterface[], + sortBy: ProjectSortBy +) => { + const sortedProjectList: ProjectInterface[] = [ + ...(projects as ProjectInterfaceScore[]), + ].sort(SortByFnMapping[sortBy]) + + return sortedProjectList +} + export const useProjectFiltersState = create< ProjectStateProps & ProjectActionsProps >()((set) => ({ + sortBy: "random", projects, queryString: "", filters: getProjectFilters(), // list of filters with all possible values from projects @@ -183,7 +217,7 @@ export const useProjectFiltersState = create< ...state, activeFilters, queryString, - projects: filteredProjects, + projects: sortProjectByFn(filteredProjects, state.sortBy), } }), onSelectTheme: (theme: string, searchQuery = "") => { @@ -206,7 +240,7 @@ export const useProjectFiltersState = create< return { ...state, activeFilters, - projects: filteredProjects, + projects: sortProjectByFn(filteredProjects, state.sortBy), } }) }, @@ -219,7 +253,7 @@ export const useProjectFiltersState = create< return { ...state, - projects: filteredProjects, + projects: sortProjectByFn(filteredProjects, state.sortBy), } }) }, @@ -232,4 +266,13 @@ export const useProjectFiltersState = create< } }) }, + sortProjectBy(sortBy: ProjectSortBy) { + set((state: any) => { + return { + ...state, + sortBy, + projects: sortProjectByFn(state.projects, sortBy), + } + }) + }, })) diff --git a/tailwind.config.js b/tailwind.config.js index 4e69abd..5b3f64e 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -30,6 +30,7 @@ module.exports = { input: "hsl(var(--input))", ring: "hsl(var(--ring))", anakiwa: { + 50: "#F2FAFD", 100: "hsl(var(--anakiwa))", 100: "#E4F3FA", 200: "#C2E8F5",