mirror of
https://github.com/privacy-scaling-explorations/pse.dev.git
synced 2026-04-23 03:01:03 -04:00
fetch projects from provider
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { useProjectFiltersState } from "@/state/useProjectFiltersState"
|
||||
import { useMemo } from "react"
|
||||
import { useProjects } from "@/app/providers/ProjectsProvider"
|
||||
import ProjectCard from "../project/project-card"
|
||||
|
||||
interface BlogArticleRelatedProjectsProps {
|
||||
@@ -10,12 +11,13 @@ interface BlogArticleRelatedProjectsProps {
|
||||
export const BlogArticleRelatedProjects = ({
|
||||
projectsIds,
|
||||
}: BlogArticleRelatedProjectsProps) => {
|
||||
const { projects: allProjects, researchs: allResearchs } =
|
||||
useProjectFiltersState((state) => state)
|
||||
const { projects: allProjects, researchs: allResearchs } = useProjects()
|
||||
|
||||
const projects = [...allProjects, ...allResearchs].filter((project) =>
|
||||
projectsIds.includes(project.id)
|
||||
)
|
||||
const projects = useMemo(() => {
|
||||
return [...allProjects, ...allResearchs].filter((project) =>
|
||||
projectsIds.includes(project.id)
|
||||
)
|
||||
}, [allProjects, allResearchs, projectsIds])
|
||||
|
||||
if (projects.length === 0) return null
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client"
|
||||
|
||||
import { useMemo } from "react"
|
||||
import Link from "next/link"
|
||||
import { projects } from "@/data/projects"
|
||||
import { filterProjects } from "@/state/useProjectFiltersState"
|
||||
import { useProjects } from "@/app/providers/ProjectsProvider"
|
||||
|
||||
import { ProjectInterface } from "@/lib/types"
|
||||
import { shuffleArray } from "@/lib/utils"
|
||||
@@ -13,25 +13,28 @@ import ProjectCard from "./project-card"
|
||||
import { ProjectProps } from "@/app/(pages)/projects/[id]/page"
|
||||
|
||||
export default function DiscoverMoreProjects({ project }: ProjectProps) {
|
||||
const getSuggestedProjects = () => {
|
||||
const projectList = projects.filter((p) => p.id !== project.id)
|
||||
const { projects: allProjects, onFilterProject } = useProjects()
|
||||
|
||||
const suggestedProject = filterProjects({
|
||||
searchPattern: "",
|
||||
activeFilters: project?.tags,
|
||||
findAnyMatch: true,
|
||||
projects: projectList,
|
||||
const suggestedProject = useMemo(() => {
|
||||
const projectList = allProjects.filter(
|
||||
(p: ProjectInterface) => p.id !== project.id
|
||||
)
|
||||
|
||||
// Filter projects by tags
|
||||
onFilterProject("")
|
||||
const suggestedProjects = projectList.filter((p) => {
|
||||
const projectThemes = project.tags?.themes ?? []
|
||||
const pThemes = p.tags?.themes ?? []
|
||||
return projectThemes.some((tag) => pThemes.includes(tag))
|
||||
})
|
||||
|
||||
// No match return random projects
|
||||
if (suggestedProject?.length < 2) {
|
||||
if (suggestedProjects?.length < 2) {
|
||||
return shuffleArray(projectList).slice(0, 2)
|
||||
}
|
||||
|
||||
return suggestedProject.slice(0, 2)
|
||||
}
|
||||
|
||||
const suggestedProject = getSuggestedProjects()
|
||||
return suggestedProjects.slice(0, 2)
|
||||
}, [allProjects, project.id, project.tags?.themes, onFilterProject])
|
||||
|
||||
return (
|
||||
<div className="w-full bg-cover-gradient">
|
||||
@@ -40,8 +43,8 @@ export default function DiscoverMoreProjects({ project }: ProjectProps) {
|
||||
{LABELS.COMMON.DISCOVER_MORE}
|
||||
</h2>
|
||||
<div className="grid flex-col grid-cols-1 gap-5 md:grid-cols-2 md:flex-row">
|
||||
{suggestedProject?.map((project: ProjectInterface, index: number) => (
|
||||
<ProjectCard key={index} border project={project} />
|
||||
{suggestedProject?.map((project: ProjectInterface) => (
|
||||
<ProjectCard key={project.id} border project={project} />
|
||||
))}
|
||||
</div>
|
||||
<Link
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
"use client"
|
||||
|
||||
import { HtmlHTMLAttributes } from "react"
|
||||
import { HtmlHTMLAttributes, useMemo } from "react"
|
||||
import Link from "next/link"
|
||||
import {
|
||||
FilterLabelMapping,
|
||||
ProjectFilter,
|
||||
} from "@/state/useProjectFiltersState"
|
||||
} from "@/app/providers/ProjectsProvider"
|
||||
|
||||
import { ProjectInterface } from "@/lib/types"
|
||||
import { LABELS } from "@/app/labels"
|
||||
@@ -16,6 +16,12 @@ interface TagsProps extends HtmlHTMLAttributes<HTMLDivElement> {
|
||||
label: string
|
||||
}
|
||||
|
||||
interface TagGroup {
|
||||
key: string
|
||||
label: string
|
||||
tags: string[]
|
||||
}
|
||||
|
||||
const TagsWrapper = ({ label, children }: TagsProps) => {
|
||||
return (
|
||||
<div className="flex flex-col items-start gap-2">
|
||||
@@ -30,42 +36,53 @@ type IProjectTags = {
|
||||
}
|
||||
|
||||
export function ProjectTags({ project }: IProjectTags) {
|
||||
const FilterKeyMapping: Record<ProjectFilter, string> = {
|
||||
keywords: LABELS.COMMON.FILTER_LABELS.KEYWORDS,
|
||||
builtWith: LABELS.COMMON.FILTER_LABELS.BUILT_WITH,
|
||||
themes: LABELS.COMMON.FILTER_LABELS.THEMES,
|
||||
fundingSource: LABELS.COMMON.FILTER_LABELS.FUNDING_SOURCE,
|
||||
}
|
||||
const FilterKeyMapping = useMemo(
|
||||
() => ({
|
||||
keywords: LABELS.COMMON.FILTER_LABELS.KEYWORDS,
|
||||
builtWith: LABELS.COMMON.FILTER_LABELS.BUILT_WITH,
|
||||
themes: LABELS.COMMON.FILTER_LABELS.THEMES,
|
||||
fundingSource: LABELS.COMMON.FILTER_LABELS.FUNDING_SOURCE,
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
const filteredTags = useMemo(
|
||||
() =>
|
||||
Object.entries(FilterLabelMapping)
|
||||
.filter(([key]) => !["themes", "builtWith"].includes(key))
|
||||
.map(([key]) => {
|
||||
const keyTags = project?.tags?.[key as ProjectFilter]
|
||||
const hasItems = keyTags && keyTags?.length > 0
|
||||
|
||||
if (!hasItems) return null
|
||||
|
||||
return {
|
||||
key,
|
||||
label: FilterKeyMapping[key as ProjectFilter] || key,
|
||||
tags: keyTags,
|
||||
}
|
||||
})
|
||||
.filter((item): item is TagGroup => item !== null),
|
||||
[project?.tags, FilterKeyMapping]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 pt-10">
|
||||
{Object.entries(FilterLabelMapping).map(([key]) => {
|
||||
const keyTags = project?.tags?.[key as ProjectFilter]
|
||||
const hasItems = keyTags && keyTags?.length > 0
|
||||
|
||||
if (["themes", "builtWith"].includes(key)) return null // keys to ignore
|
||||
return (
|
||||
hasItems && (
|
||||
<div data-section-id={key} key={key}>
|
||||
<TagsWrapper
|
||||
label={FilterKeyMapping?.[key as ProjectFilter] || key}
|
||||
>
|
||||
<div className="flex flex-wrap gap-[6px]">
|
||||
{keyTags?.map((tag, index) => {
|
||||
return (
|
||||
<Link key={index} href={`/projects?${key}=${tag}`}>
|
||||
<CategoryTag key={tag} variant="gray">
|
||||
{tag}
|
||||
</CategoryTag>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</TagsWrapper>
|
||||
{filteredTags.map((tagGroup) => (
|
||||
<div data-section-id={tagGroup.key} key={tagGroup.key}>
|
||||
<TagsWrapper label={tagGroup.label}>
|
||||
<div className="flex flex-wrap gap-[6px]">
|
||||
{tagGroup.tags?.map((tag, index) => (
|
||||
<Link key={index} href={`/projects?${tagGroup.key}=${tag}`}>
|
||||
<CategoryTag key={tag} variant="gray">
|
||||
{tag}
|
||||
</CategoryTag>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
)
|
||||
})}
|
||||
</TagsWrapper>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,16 +3,8 @@
|
||||
import React, { ChangeEvent, ReactNode, useEffect, useState } from "react"
|
||||
import Image from "next/image"
|
||||
import { useRouter, useSearchParams } from "next/navigation"
|
||||
import { projects } from "@/data/projects"
|
||||
import FiltersIcon from "@/public/icons/filters.svg"
|
||||
import {
|
||||
FilterLabelMapping,
|
||||
FilterTypeMapping,
|
||||
ProjectFilter,
|
||||
useProjectFiltersState,
|
||||
} from "@/state/useProjectFiltersState"
|
||||
import { useDebounce } from "react-use"
|
||||
|
||||
import { IThemeStatus, IThemesButton } from "@/types/common"
|
||||
import {
|
||||
ProjectCategories,
|
||||
@@ -24,7 +16,11 @@ import {
|
||||
} from "@/lib/types"
|
||||
import { cn, queryStringToObject } from "@/lib/utils"
|
||||
import { LABELS } from "@/app/labels"
|
||||
|
||||
import {
|
||||
useProjects,
|
||||
ProjectFilter,
|
||||
FilterLabelMapping,
|
||||
} from "@/app/providers/ProjectsProvider"
|
||||
import { Icons } from "../icons"
|
||||
import Badge from "../ui/badge"
|
||||
import { Button } from "../ui/button"
|
||||
@@ -33,6 +29,14 @@ import { Checkbox } from "../ui/checkbox"
|
||||
import { Input } from "../ui/input"
|
||||
import { Modal } from "../ui/modal"
|
||||
|
||||
// Define the mapping for filter types
|
||||
const FilterTypeMapping: Record<ProjectFilter, "checkbox" | "button"> = {
|
||||
keywords: "checkbox",
|
||||
builtWith: "checkbox",
|
||||
themes: "button",
|
||||
fundingSource: "checkbox",
|
||||
}
|
||||
|
||||
interface FilterWrapperProps {
|
||||
label: string
|
||||
children?: ReactNode
|
||||
@@ -42,7 +46,9 @@ interface FilterWrapperProps {
|
||||
const FilterWrapper = ({ label, children, className }: FilterWrapperProps) => {
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-4 py-6", className)}>
|
||||
<span className="text-xl font-bold">{label}</span>
|
||||
<span className="text-sm font-medium text-tuatara-950 md:text-base">
|
||||
{label}
|
||||
</span>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
@@ -81,8 +87,14 @@ export default function ProjectFiltersBar() {
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [filterCount, setFilterCount] = useState(0)
|
||||
|
||||
const { filters, toggleFilter, queryString, activeFilters, onFilterProject } =
|
||||
useProjectFiltersState((state) => state)
|
||||
const {
|
||||
filters,
|
||||
toggleFilter,
|
||||
queryString,
|
||||
activeFilters,
|
||||
onFilterProject,
|
||||
setFilterFromQueryString,
|
||||
} = useProjects()
|
||||
|
||||
useEffect(() => {
|
||||
if (!queryString) return
|
||||
@@ -91,10 +103,8 @@ export default function ProjectFiltersBar() {
|
||||
|
||||
useEffect(() => {
|
||||
// set active filters from url
|
||||
useProjectFiltersState.setState({
|
||||
activeFilters: queryStringToObject(searchParams),
|
||||
})
|
||||
}, [searchParams])
|
||||
setFilterFromQueryString(queryStringToObject(searchParams))
|
||||
}, [searchParams, setFilterFromQueryString])
|
||||
|
||||
useEffect(() => {
|
||||
const count = Object.values(activeFilters).reduce((acc, curr) => {
|
||||
@@ -104,11 +114,7 @@ export default function ProjectFiltersBar() {
|
||||
}, [activeFilters])
|
||||
|
||||
const clearAllFilters = () => {
|
||||
useProjectFiltersState.setState({
|
||||
activeFilters: {},
|
||||
queryString: "",
|
||||
projects,
|
||||
})
|
||||
setFilterFromQueryString({})
|
||||
setSearchQuery("") // clear input
|
||||
router.push("/projects")
|
||||
}
|
||||
@@ -255,9 +261,7 @@ export default function ProjectFiltersBar() {
|
||||
<Input
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
||||
setSearchQuery(e?.target?.value)
|
||||
useProjectFiltersState.setState({
|
||||
searchQuery: e?.target?.value,
|
||||
})
|
||||
onFilterProject(e?.target?.value)
|
||||
}}
|
||||
value={searchQuery}
|
||||
placeholder={LABELS.COMMON.SEARCH_PROJECT_PLACEHOLDER}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react"
|
||||
import Image from "next/image"
|
||||
import NoResultIcon from "@/public/icons/no-result.svg"
|
||||
import { useProjectFiltersState } from "@/state/useProjectFiltersState"
|
||||
import { cva } from "class-variance-authority"
|
||||
|
||||
import {
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
DEFAULT_PROJECT_SORT_BY,
|
||||
ProjectFilter,
|
||||
ProjectSortBy,
|
||||
useProjectFiltersState,
|
||||
} from "@/state/useProjectFiltersState"
|
||||
useProjects,
|
||||
} from "@/app/providers/ProjectsProvider"
|
||||
|
||||
import { LABELS } from "@/app/labels"
|
||||
import { interpolate } from "@/lib/utils"
|
||||
@@ -17,7 +16,7 @@ const labelClass = "h-5 text-xs text-base md:h-6 text-slate-900/70 md:text-sm"
|
||||
|
||||
export const ProjectResultBar = () => {
|
||||
const { activeFilters, toggleFilter, projects, sortProjectBy, sortBy } =
|
||||
useProjectFiltersState((state) => state)
|
||||
useProjects()
|
||||
|
||||
const haveActiveFilters = Object.entries(activeFilters).some(
|
||||
([, values]) => values?.length > 0
|
||||
@@ -61,7 +60,7 @@ export const ProjectResultBar = () => {
|
||||
<span className={labelClass}>{resultLabel}</span>
|
||||
<Dropdown
|
||||
label={activeSortOption}
|
||||
defaultItem={DEFAULT_PROJECT_SORT_BY}
|
||||
defaultItem="asc"
|
||||
items={projectSortItems}
|
||||
onChange={(sortBy) => sortProjectBy(sortBy as ProjectSortBy)}
|
||||
disabled={!projects?.length}
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
"use client"
|
||||
|
||||
import React, { useEffect, useState } from "react"
|
||||
import React, { useEffect, useState, useMemo } from "react"
|
||||
import Image from "next/image"
|
||||
import NoResultIcon from "@/public/icons/no-result.svg"
|
||||
import { useProjectFiltersState } from "@/state/useProjectFiltersState"
|
||||
import { cva } from "class-variance-authority"
|
||||
|
||||
import { ProjectStatus } from "@/lib/types"
|
||||
import { ProjectInterface, ProjectStatus } from "@/lib/types"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { LABELS } from "@/app/labels"
|
||||
import { useProjects } from "@/app/providers/ProjectsProvider"
|
||||
|
||||
import ResearchCard from "./research-card"
|
||||
import Link from "next/link"
|
||||
|
||||
const sectionTitleClass = cva(
|
||||
"relative font-sans text-base font-bold uppercase tracking-[3.36px] text-anakiwa-950 after:ml-8 after:absolute after:top-1/2 after:h-[1px] after:w-full after:translate-y-1/2 after:bg-anakiwa-300 after:content-['']"
|
||||
)
|
||||
@@ -37,17 +38,28 @@ const ProjectStatusOrderList = ["active", "maintained", "inactive"]
|
||||
export const ResearchList = () => {
|
||||
const [isMounted, setIsMounted] = useState(false)
|
||||
|
||||
const { researchs, searchQuery, queryString } = useProjectFiltersState(
|
||||
(state) => state
|
||||
)
|
||||
const { researchs, searchQuery, queryString } = useProjects()
|
||||
|
||||
const noItems = researchs?.length === 0
|
||||
const hasActiveFilters = searchQuery !== "" || queryString !== ""
|
||||
|
||||
useEffect(() => {
|
||||
setIsMounted(true)
|
||||
}, [])
|
||||
|
||||
const hasActiveFilters = searchQuery !== "" || queryString !== ""
|
||||
const { activeResearchs, pastResearchs } = useMemo(() => {
|
||||
const active = researchs.filter(
|
||||
(research: ProjectInterface) =>
|
||||
research.projectStatus === ProjectStatus.ACTIVE
|
||||
)
|
||||
|
||||
const past = researchs.filter(
|
||||
(research: ProjectInterface) =>
|
||||
research.projectStatus !== ProjectStatus.ACTIVE
|
||||
)
|
||||
|
||||
return { activeResearchs: active, pastResearchs: past }
|
||||
}, [researchs])
|
||||
|
||||
if (!isMounted) {
|
||||
return (
|
||||
@@ -74,14 +86,6 @@ export const ResearchList = () => {
|
||||
|
||||
if (noItems) return <NoResults />
|
||||
|
||||
const activeResearchs = researchs.filter(
|
||||
(research) => research.projectStatus === ProjectStatus.ACTIVE
|
||||
)
|
||||
|
||||
const pastResearchs = researchs.filter(
|
||||
(research) => research.projectStatus !== ProjectStatus.ACTIVE
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="relative grid items-start justify-between grid-cols-1">
|
||||
<div
|
||||
@@ -97,20 +101,18 @@ export const ResearchList = () => {
|
||||
</div>
|
||||
)}
|
||||
<div className="grid grid-cols-1 gap-4 md:gap-x-6 md:gap-y-10 lg:grid-cols-3">
|
||||
{activeResearchs.map((project) => {
|
||||
return (
|
||||
<ResearchCard
|
||||
key={project?.id}
|
||||
project={project}
|
||||
className="h-[180px]"
|
||||
showBanner={false}
|
||||
showLinks={false}
|
||||
showCardTags={false}
|
||||
showStatus={false}
|
||||
border
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{activeResearchs.map((project: ProjectInterface) => (
|
||||
<ResearchCard
|
||||
key={project?.id}
|
||||
project={project}
|
||||
className="h-[180px]"
|
||||
showBanner={false}
|
||||
showLinks={false}
|
||||
showCardTags={false}
|
||||
showStatus={false}
|
||||
border
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className={cn("flex w-full flex-col gap-10 pt-10")}>
|
||||
@@ -120,17 +122,15 @@ export const ResearchList = () => {
|
||||
</h3>
|
||||
</div>
|
||||
<div className="flex flex-col gap-5">
|
||||
{pastResearchs.map((project) => {
|
||||
return (
|
||||
<Link
|
||||
href={`/projects/${project?.id}`}
|
||||
key={project?.id}
|
||||
className="text-neutral-950 border-b-[2px] border-b-anakiwa-500 text-sm font-medium w-fit hover:text-anakiwa-500 duration-200"
|
||||
>
|
||||
{project.name}
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
{pastResearchs.map((project: ProjectInterface) => (
|
||||
<Link
|
||||
href={`/projects/${project?.id}`}
|
||||
key={project?.id}
|
||||
className="text-neutral-950 border-b-[2px] border-b-anakiwa-500 text-sm font-medium w-fit hover:text-anakiwa-500 duration-200"
|
||||
>
|
||||
{project.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user