wip add filters

This commit is contained in:
Kalidou Diagne
2023-08-17 01:20:17 +02:00
parent 025482ea45
commit 55ed19404e
15 changed files with 603 additions and 71 deletions

View File

@@ -7,6 +7,7 @@ import GlobalVector from "@/public/social-medias/global-line.svg"
import TwitterVector from "@/public/social-medias/twitter-fill.svg"
import { Markdown } from "@/components/ui/markdown"
import { ProjectTags } from "@/components/project/project-detail-tags"
type PageProps = {
params: { id: string }
@@ -86,7 +87,7 @@ export default function ProjectDetailPage({ params }: PageProps) {
<p className="text-slate-600">{currProject.tldr}</p>
</div>
</div>
<div className="flex flex-col items-center justify-center w-full gap-5 px-6 py-10 bg-anakiwa md:px-0">
<div className="flex flex-col items-center justify-center w-full gap-5 px-6 py-10 bg-anakiwa-100 md:px-0">
<div className="w-full md:w-[700px]">
<div className="relative flex items-center justify-center overflow-hidden rounded-lg">
<Image
@@ -99,6 +100,9 @@ export default function ProjectDetailPage({ params }: PageProps) {
className="object-cover w-full rounded-t-lg"
/>
</div>
<div className="mt-8">
<ProjectTags project={currProject} />
</div>
<div className="flex flex-col w-full gap-5 py-10 text-base font-normal leading-relaxed">
<Markdown>{currProject.description}</Markdown>
</div>

View File

@@ -1,10 +1,8 @@
import { Metadata } from "next"
import Image from "next/image"
import Link from "next/link"
import { ProjectLinkIconMap, projects } from "@/data/projects"
import { ProjectLinkWebsite } from "@/lib/types"
import { ProjectLink } from "@/components/project/project-link"
import ProjectFiltersBar from "@/components/project/project-filters-bar"
import ProjectList from "@/components/project/project-list"
import { ProjectResultBar } from "@/components/project/project-result-bar"
export const metadata: Metadata = {
title: "Project Library",
@@ -13,7 +11,6 @@ export const metadata: Metadata = {
}
// TODO: MAKE IT RANDOM - This would prob need to be state and so metadata would get cut
// const randomizeProjects = (projects: any[]) => {
// // efficient fisher-yates shuffle
// const array = [...projects]
// let currentIndex = array.length,
@@ -32,8 +29,8 @@ export const metadata: Metadata = {
export default function ProjectsPage() {
return (
<section>
<div className="bg-second-gradient">
<div className="container py-12 mx-auto lg:py-24">
<div className="bg-anakiwa-200">
<div className="container py-8 mx-auto md:py-12 lg:px-24 lg:py-16">
<h1 className="text-4xl font-bold md:text-5xl">
Explore the project library
</h1>
@@ -42,62 +39,15 @@ export default function ProjectsPage() {
PSE is home to many projects, from cryptography research to
developer tools, protocols and proof-of-concept applications.
</p>
<ProjectFiltersBar />
</div>
</div>
<div className="w-full bg-anakiwa">
<div className="w-full bg-anakiwa-100">
<div className="container">
<p className="p-3"></p>
<p className="text-base text-slate-900/70 md:text-lg">{`Showing ${projects.length} projects`}</p>
<div className="flex flex-wrap justify-center gap-6 py-6">
{projects.map((project, index) => {
const { id, image, links, name, tldr } = project
return (
<div key={index}>
<Link href={`/projects/${id}`}>
<div className="flex h-[419px] w-[310px] cursor-pointer flex-col overflow-hidden rounded-lg border border-slate-900/20 transition duration-150 ease-in hover:scale-105">
<Image
src={`/project-banners/${
image ? image : "fallback.webp"
}`}
alt={`${name} banner`}
width={1200}
height={630}
className="object-cover w-full rounded-t-lg"
/>
<div className="flex flex-col justify-between h-full gap-5 p-5 bg-white rounded-b-lg">
<div className="flex flex-col justify-start gap-2">
<h1 className="text-xl font-bold text-black">
{name}
</h1>
<p className="text-slate-900/80">{tldr}</p>
</div>
<div className="flex items-center justify-start gap-2 mr-auto">
{Object.entries(links ?? {})?.map(
([website, url], index) => {
const image =
ProjectLinkIconMap?.[
website as ProjectLinkWebsite
]
if (!image) return null // no icon mapping for this website
return (
<ProjectLink
key={index}
url={url}
image={image}
website={website as ProjectLinkWebsite}
/>
)
}
)}
</div>
</div>
</div>
</Link>
</div>
)
})}
<div className="px-3 py-8">
<ProjectResultBar />
</div>
<ProjectList />
</div>
</div>
</section>

View File

@@ -0,0 +1,41 @@
import Link from "next/link"
import {
FilterLabelMapping,
ProjectFilter,
} from "@/state/useProjectFiltersState"
import { ProjectInterface } from "@/lib/types"
import { CategoryTag } from "../ui/categoryTag"
export function ProjectTags({ project }: { project: ProjectInterface }) {
return (
<div className="flex flex-col gap-4 mt-8">
{Object.entries(FilterLabelMapping).map(([key, label]) => {
const keyTags = project?.tags?.[key as ProjectFilter]
const hasItems = keyTags && keyTags?.length > 0
return (
hasItems && (
<div>
<div className="flex gap-2">
<span className="py-2 text-base font-medium ">{label}</span>
<div className="flex gap-[6px]">
{keyTags?.map((tag) => {
return (
<Link href={`/projects?${key}=${tag}`}>
<CategoryTag key={tag} variant="gray">
{tag}
</CategoryTag>
</Link>
)
})}
</div>
</div>
</div>
)
)
})}
</div>
)
}

View File

@@ -0,0 +1,166 @@
"use client"
import React, { ReactNode, useEffect, useState } from "react"
import Image from "next/image"
import { useRouter, useSearchParams } from "next/navigation"
import FiltersIcon from "@/public/icons/filters.svg"
import {
FilterLabelMapping,
ProjectFilter,
useProjectFiltersState,
} from "@/state/useProjectFiltersState"
import { cn, queryStringToObject } from "@/lib/utils"
import Badge from "../ui/badge"
import { Button } from "../ui/button"
import { Checkbox } from "../ui/checkbox"
import { Input } from "../ui/input"
import { Modal } from "../ui/modal"
interface FilterWrapperProps {
label: string
children?: ReactNode
}
const FilterWrapper = ({ label, children }: FilterWrapperProps) => {
return (
<div className="flex flex-col gap-4 py-6">
<span className="text-xl font-bold">{label}</span>
<div className="grid grid-cols-1 md:grid-cols-3 gap-y-2">{children}</div>
</div>
)
}
export default function ProjectFiltersBar() {
const [showModal, setShowModal] = useState(false)
const router = useRouter()
const searchParams = useSearchParams()
const { filters, toggleFilter, queryString, activeFilters } =
useProjectFiltersState((state) => state)
useEffect(() => {
if (!queryString) return
router.push(`/projects?${queryString}`)
}, [queryString, router])
useEffect(() => {
// set active filters from url
useProjectFiltersState.setState({
activeFilters: queryStringToObject(searchParams),
})
}, [])
const clearAllFilters = () => {
useProjectFiltersState.setState({ activeFilters: {}, queryString: "" })
router.push("/projects")
}
const hasActiveFilters = queryString && queryString.length > 0
return (
<>
<Modal
title="Filters"
footer={
<div className="flex">
<Button
disabled={!hasActiveFilters}
variant="ghost"
size="sm"
onClick={clearAllFilters}
>
Clear all
</Button>
<div className="ml-auto">
<Button
variant="black"
size="sm"
onClick={() => setShowModal(false)}
>
Show project
</Button>
</div>
</div>
}
open={showModal}
setOpen={setShowModal}
>
{Object.entries(filters).map(([key, items]) => {
const filterLabel = FilterLabelMapping?.[key as ProjectFilter] ?? ""
const hasItems = items.length > 0
return (
hasItems && (
<FilterWrapper key={key} label={filterLabel}>
<>
{items.map((item) => {
const isActive =
activeFilters?.[key as ProjectFilter]?.includes(item)
return (
<Checkbox
key={item}
onClick={() => toggleFilter(key as ProjectFilter, item)}
name={item}
label={item}
checked={isActive}
/>
)
})}
</>
</FilterWrapper>
)
)
})}
</Modal>
<div className="flex flex-col gap-6 mt-10">
<span className="text-lg font-medium">
What do you want to do today?
</span>
<div className="grid items-center justify-between grid-cols-1 gap-3 md:gap-12 md:grid-cols-5">
<div className="grid grid-cols-3 col-span-1 gap-4 md:gap-6 md:col-span-2">
<Button variant="white" size="lg">
Build
</Button>
<Button variant="white" size="lg">
Play
</Button>
<Button variant="white" size="lg">
Research
</Button>
</div>
<div className="grid grid-cols-[1fr_auto] col-span-1 gap-3 md:col-span-3">
<Input placeholder="Search project title or keyword" />
<div className="flex items-center gap-3">
<Badge>
<Button
onClick={() => setShowModal(true)}
variant="white"
className={cn({
"border-2 border-anakiwa-950": hasActiveFilters,
})}
>
<div className="flex items-center gap-2">
<Image src={FiltersIcon} alt="filter icon" />
<span className="hidden md:block">Filters</span>
</div>
</Button>
</Badge>
<button
disabled={!hasActiveFilters}
onClick={clearAllFilters}
className="hidden bg-transparent cursor-pointer md:block text-primary opacity-85 hover:opacity-100 disabled:opacity-50 disabled:pointer-events-none"
>
<div className="flex items-center gap-2 border-b-2 border-black">
<span className="text-sm font-medium">Clear all</span>
</div>
</button>
</div>
</div>
</div>
</div>
</>
)
}

View File

@@ -1,16 +1,20 @@
"use client"
import Image from "next/image"
import { ProjectLinkIconMap } from "@/data/projects"
import { ProjectLinkWebsite } from "@/lib/types"
interface ProjectLinkProps {
url: string
image: string
website: ProjectLinkWebsite
}
export function ProjectLink({ image, website, url }: ProjectLinkProps) {
export function ProjectLink({ website, url }: ProjectLinkProps) {
const image = ProjectLinkIconMap?.[website as ProjectLinkWebsite]
// TODO: add support for youtube, discord and other links
if (!image) return null
return (
<a
href={url}

View File

@@ -0,0 +1,57 @@
"use client"
import React from "react"
import Image from "next/image"
import { useRouter } from "next/navigation"
import { useProjectFiltersState } from "@/state/useProjectFiltersState"
import { ProjectLinkWebsite } from "@/lib/types"
import { ProjectLink } from "./project-link"
export default function ProjectList() {
const { projects } = useProjectFiltersState((state) => state)
const router = useRouter()
return (
<div className="flex flex-wrap justify-center gap-6 pb-6">
{projects.map((project) => {
const { id, image, links, name, tldr } = project
return (
<div key={id}>
<div
onClick={() => router.push(`/projects/${id}`)}
className="flex h-[419px] w-[310px] cursor-pointer flex-col overflow-hidden rounded-lg border border-slate-900/20 transition duration-150 ease-in hover:scale-105"
>
<Image
src={`/project-banners/${image ? image : "fallback.webp"}`}
alt={`${name} banner`}
width={1200}
height={630}
className="object-cover w-full rounded-t-lg"
/>
<div className="flex flex-col justify-between h-full gap-5 p-5 bg-white rounded-b-lg">
<div className="flex flex-col justify-start gap-2">
<h1 className="text-xl font-bold text-black">{name}</h1>
<p className="text-slate-900/80">{tldr}</p>
</div>
<div className="flex items-center justify-start gap-2 mr-auto">
{Object.entries(links ?? {})?.map(([website, url], index) => {
return (
<ProjectLink
key={index}
url={url}
website={website as ProjectLinkWebsite}
/>
)
})}
</div>
</div>
</div>
</div>
)
})}
</div>
)
}

View File

@@ -0,0 +1,54 @@
"use client"
import { useProjectFiltersState } from "@/state/useProjectFiltersState"
import { CategoryTag } from "../ui/categoryTag"
const labelClass = "h-5 text-xs text-base md:h-6 text-slate-900/70 md:text-lg"
export const ProjectResultBar = () => {
const { activeFilters, toggleFilter, projects } = useProjectFiltersState(
(state) => state
)
const haveActiveFilters = Object.entries(activeFilters).some(
([_key, values]) => values.length > 0
)
if (!haveActiveFilters)
return (
<span className={labelClass}>
{`Showing ${projects.length} projects`}{" "}
</span>
)
return (
<div className="flex flex-col gap-2">
<span className={labelClass}>
{`Showing ${projects?.length} projects with:`}{" "}
</span>
<div className="inline-flex flex-wrap gap-4">
{Object.entries(activeFilters).map(([key, filters], index) => {
return (
<>
{filters?.map((filter) => {
if (filter?.length === 0) return null
return (
<CategoryTag
closable
variant="gray"
onClose={() => toggleFilter(key as any, filter)}
key={`${index}-${filter}`}
>
{filter}
</CategoryTag>
)
})}
</>
)
})}
</div>
</div>
)
}

View File

@@ -1,11 +1,12 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { LucideIcon } from "lucide-react"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center 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",
"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: {
@@ -18,6 +19,8 @@ const buttonVariants = cva(
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "underline-offset-4 hover:underline text-primary",
black: "bg-tuatara-950 text-white hover:bg-black",
white: "bg-zinc-50 text-black hover:bg-zinc-100",
},
size: {
default: "h-10 py-2 px-4",
@@ -36,17 +39,25 @@ export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
icon?: LucideIcon
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
(
{ className, variant, size, asChild = false, children, icon, ...props },
ref
) => {
const Comp = asChild ? Slot : "button"
const Icon = icon
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
>
{Icon && <Icon size={24} />}
<span>{children}</span>
</Comp>
)
}
)

39
components/ui/input.tsx Normal file
View File

@@ -0,0 +1,39 @@
import { InputHTMLAttributes, forwardRef } from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const inputVariants = cva(
"py-2 px-4 text-anakiwa-950 transition-colors duration-100 animate border-[1.5px] rounded-md focus:ring-0 border-tuatara-200 focus-visible:border-2 focus-visible:border-tuatara-950 bg-zinc-50 focus-visible:ring-0 focus-visible:none focus-visible:outline-none placeholder-anakiwa-950/50",
{
variants: {
size: {
default: "text-sm",
sm: "text-xs",
},
},
defaultVariants: {
size: "default",
},
}
)
interface InputProps
extends VariantProps<typeof inputVariants>,
Omit<InputHTMLAttributes<HTMLInputElement>, "size" | "id" | "children"> {}
const Input = forwardRef<HTMLInputElement, InputProps>(
({ size, className, ...props }, ref) => {
return (
<input
ref={ref}
{...props}
className={cn(inputVariants({ size, className }))}
/>
)
}
)
Input.displayName = "Input"
export { Input }

View File

@@ -1,6 +1,25 @@
import { ReadonlyURLSearchParams } from "next/navigation"
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
export function uniq(arr: any[], removeEmpty = true) {
const uniqArray = Array.from(new Set(arr))
return removeEmpty ? uniqArray.filter(Boolean) : uniqArray
}
export function queryStringToObject(
searchParams: ReadonlyURLSearchParams
): Record<string, any> {
const obj = Object.fromEntries(searchParams.entries())
const object: Record<string, any> = {}
Object.keys(obj).forEach((key) => {
object[key] = obj[key]?.split(",")
})
return object
}

View File

@@ -26,6 +26,7 @@
"class-variance-authority": "^0.4.0",
"clsx": "^1.2.1",
"framer-motion": "^10.12.17",
"fuse.js": "^6.6.2",
"gsap": "^3.12.1",
"lucide-react": "0.105.0-alpha.4",
"next": "^13.4.10",
@@ -36,7 +37,8 @@
"remark-gfm": "^3.0.1",
"sharp": "^0.31.3",
"tailwind-merge": "^1.12.0",
"tailwindcss-animate": "^1.0.5"
"tailwindcss-animate": "^1.0.5",
"zustand": "^4.4.1"
},
"devDependencies": {
"@ianvs/prettier-plugin-sort-imports": "^3.7.2",

39
pnpm-lock.yaml generated
View File

@@ -23,6 +23,9 @@ dependencies:
framer-motion:
specifier: ^10.12.17
version: 10.12.17(react-dom@18.2.0)(react@18.2.0)
fuse.js:
specifier: ^6.6.2
version: 6.6.2
gsap:
specifier: ^3.12.1
version: 3.12.1
@@ -56,6 +59,9 @@ dependencies:
tailwindcss-animate:
specifier: ^1.0.5
version: 1.0.5(tailwindcss@3.3.2)
zustand:
specifier: ^4.4.1
version: 4.4.1(@types/react@18.2.7)(react@18.2.0)
devDependencies:
'@ianvs/prettier-plugin-sort-imports':
@@ -2003,6 +2009,11 @@ packages:
resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
dev: true
/fuse.js@6.6.2:
resolution: {integrity: sha512-cJaJkxCCxC8qIIcPBF9yGxY0W/tVZS3uEISDxhYIdtk8OL93pe+6Zj7LjCqVV4dzbqcriOZ+kQ/NE4RXZHsIGA==}
engines: {node: '>=10'}
dev: false
/gensync@1.0.0-beta.2:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
engines: {node: '>=6.9.0'}
@@ -4029,6 +4040,14 @@ packages:
tslib: 2.5.2
dev: false
/use-sync-external-store@1.2.0(react@18.2.0):
resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
dependencies:
react: 18.2.0
dev: false
/util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
@@ -4133,6 +4152,26 @@ packages:
resolution: {integrity: sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==}
dev: false
/zustand@4.4.1(@types/react@18.2.7)(react@18.2.0):
resolution: {integrity: sha512-QCPfstAS4EBiTQzlaGP1gmorkh/UL1Leaj2tdj+zZCZ/9bm0WS7sI2wnfD5lpOszFqWJ1DcPnGoY8RDL61uokw==}
engines: {node: '>=12.7.0'}
peerDependencies:
'@types/react': '>=16.8'
immer: '>=9.0'
react: '>=16.8'
peerDependenciesMeta:
'@types/react':
optional: true
immer:
optional: true
react:
optional: true
dependencies:
'@types/react': 18.2.7
react: 18.2.0
use-sync-external-store: 1.2.0(react@18.2.0)
dev: false
/zwitch@2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
dev: false

3
public/icons/filters.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15 12.75C13.6065 12.75 12.4418 13.71 12.1065 15H3V16.5H12.1065C12.4418 17.79 13.6065 18.75 15 18.75C16.3935 18.75 17.5583 17.79 17.8935 16.5H21V15H17.8935C17.5583 13.71 16.3935 12.75 15 12.75ZM9 6C7.6065 6 6.44175 6.96 6.1065 8.25H3V9.75H6.1065C6.44175 11.04 7.6065 12 9 12C10.3935 12 11.5583 11.04 11.8935 9.75H21V8.25H11.8935C11.5583 6.96 10.3935 6 9 6Z" fill="#103241"/>
</svg>

After

Width:  |  Height:  |  Size: 487 B

View File

@@ -0,0 +1,139 @@
import { projects } from "@/data/projects"
import Fuse from "fuse.js"
import { create } from "zustand"
import { ProjectInterface } from "@/lib/types"
import { uniq } from "@/lib/utils"
export type ProjectFilter = "keywords" | "builtWith" | "themes"
export type FiltersProps = Record<ProjectFilter, string[]>
export const FilterLabelMapping: Record<ProjectFilter, string> = {
keywords: "Keywords",
builtWith: "Built with",
themes: "Themes",
}
interface ProjectStateProps {
projects: ProjectInterface[]
filters: FiltersProps
activeFilters: Partial<FiltersProps>
queryString: string
}
interface SearchMatchByParamsProps {
list: any[]
searchPattern: string
}
interface ProjectActionsProps {
toggleFilter: (projectFilter: ProjectFilter, value: string) => void
setFilterFromQueryString: (filters: Partial<FiltersProps>) => void
onFilterProject: (searchPattern: string) => void
}
const createURLQueryString = (params: Partial<FiltersProps>) => {
const qs = Object.keys(params)
.map((key: any) => `${key}=${encodeURIComponent((params as any)[key])}`)
.join("&")
return qs
}
const getProjectFilters = (): FiltersProps => {
const filters: FiltersProps = {
keywords: [],
builtWith: [],
themes: [],
}
// get list of all tags from project list
projects.forEach((project) => {
if (project?.tags?.builtWith) {
filters.builtWith.push(...project?.tags?.builtWith)
}
if (project?.tags?.keywords) {
filters.keywords.push(...project?.tags?.keywords)
}
})
// duplicate-free array for every tags
Object.entries(filters).forEach(([key, entries]) => {
filters[key as ProjectFilter] = uniq(entries)
})
return filters
}
const filterProjects = ({ list, searchPattern }: SearchMatchByParamsProps) => {
const keys = ["title", "tldr", "tags.keywords", "tags.builtWith"]
const fuseOptions = {
keys,
$or: [
{
$path: keys,
val: searchPattern,
},
],
}
const fuse = new Fuse(list, fuseOptions)
const result = fuse.search(searchPattern)?.map(({ item }) => item)
return result ?? []
}
export const useProjectFiltersState = create<
ProjectStateProps & ProjectActionsProps
>()((set) => ({
projects,
queryString: "",
filters: getProjectFilters(), // list of filters with all possible values from projects
activeFilters: {}, // list of filters active in the current view by the user
toggleFilter: (filterKey: ProjectFilter, value: string) =>
set((state: any) => {
if (!filterKey) return
const values: string[] = state?.activeFilters?.[filterKey] ?? []
const index = values?.indexOf(value)
if (index > -1) {
values.splice(index, 1)
} else {
values.push(value)
}
const activeFilters: Partial<FiltersProps> = {
...state.activeFilters,
[filterKey]: values,
}
return {
...state,
activeFilters,
queryString: createURLQueryString(activeFilters),
}
}),
onFilterProject: (searchPattern: string) => {
set((state: any) => {
if (!searchPattern?.length) return state
const filteredProjects = filterProjects({
list: state.projects,
searchPattern,
})
return {
...state,
projects: filteredProjects,
}
})
},
setFilterFromQueryString: (filters: Partial<FiltersProps>) => {
set((state: any) => {
return {
...state,
activeFilters: filters,
queryString: createURLQueryString(filters),
}
})
},
}))

View File

@@ -26,13 +26,17 @@ module.exports = {
corduroy: "#4A5754",
orange: "#E1523A",
orangeDark: "#E3533A",
anakiwa: "hsl(var(--anakiwa))",
"anakiwa-100": "#E4F3FA",
"anakiwa-300": "#A3DFF0",
"anakiwa-500": "#29ACCE",
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
anakiwa: {
100: "hsl(var(--anakiwa))",
100: "#E4F3FA",
200: "#C2E8F5",
300: "#A3DFF0",
500: "#29ACCE",
950: "#103241",
},
tuatara: {
100: "#E5E6E8",
200: "#CDCFD4",