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/components/icons.tsx b/components/icons.tsx index 75136da..f14dd88 100644 --- a/components/icons.tsx +++ b/components/icons.tsx @@ -143,6 +143,21 @@ export const Icons = { /> ), + arrowDown: (props: LucideProps) => ( + + + + ), plus: (props: LucideProps) => ( { + 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/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/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 7452f79..c5f4122 100644 --- a/data/projects/bandada.ts +++ b/data/projects/bandada.ts @@ -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 4763ac6..9069ab5 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -19,6 +19,8 @@ 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 @@ -36,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 d555424..dd4d8d3 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@radix-ui/react-accordion": "^1.1.2", "@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 441631f..66318bb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,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) @@ -360,6 +363,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'} @@ -574,6 +605,35 @@ packages: react-dom: 18.2.0(react@18.2.0) 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/primitive': 1.0.1 + '@radix-ui/react-collapsible': 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-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-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-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: @@ -755,6 +815,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: @@ -807,6 +894,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: @@ -871,6 +1026,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: @@ -958,6 +1142,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: @@ -973,6 +1172,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/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 e81e7d5..c5bea3d 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -30,10 +30,12 @@ module.exports = { input: "hsl(var(--input))", ring: "hsl(var(--ring))", anakiwa: { + 50: "#F2FAFD", 100: "hsl(var(--anakiwa))", 100: "#E4F3FA", 200: "#C2E8F5", 300: "#A3DFF0", + 400: "#50C3E0", 500: "#29ACCE", 950: "#103241", },