fetch projects from provider

This commit is contained in:
Kalidou Diagne
2025-06-13 19:48:29 +04:00
parent 286c3ffc5e
commit c4f5c8f3db
13 changed files with 585 additions and 561 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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>
)
}

View File

@@ -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}

View File

@@ -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 {

View File

@@ -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}

View File

@@ -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>