Skip to content

Commit

Permalink
Merge pull request #219 from Maakaf/218-refactor-projects-page-to-use…
Browse files Browse the repository at this point in the history
…-baas-data-provider

218 refactor projects page to use baas data provider
  • Loading branch information
Darkmift authored Mar 26, 2024
2 parents 2f72c40 + 999cf39 commit bfe81b4
Show file tree
Hide file tree
Showing 13 changed files with 377 additions and 201 deletions.
39 changes: 39 additions & 0 deletions actions/fetchProjectsData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
'use server';

import {
IProjectsDataResponse,
ProjectPaginationFilter,
} from '@/types/project';

export type ProjectPaginationRequest = {
page?: number;
limit?: number;
filter?: ProjectPaginationFilter;
};

const PROJECT_API_ENDPOINT = 'https://baas-data-provider.onrender.com/projects';

async function fetchProjectsData({
page = 1,
limit = 100,
filter = ProjectPaginationFilter.ALL,
}: ProjectPaginationRequest) {
// fetch from endpoint POST with page, limit, filter as IProjectsDataResponse
const { projects, total, languages, pageLanguages, timestamp } = await fetch(
PROJECT_API_ENDPOINT,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ page, limit, filter }),
}
).then(res => res.json() as Promise<IProjectsDataResponse>);

return {
projects: projects,
pageLanguages,
};
}

export default fetchProjectsData;
3 changes: 3 additions & 0 deletions app/[locale]/projects/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ export const metadata: Metadata = {
},
};

// Layout here due to dynamic page nature as a client component
// (Layout serves metadata as rsc while page serves client component)

const ProjectsLayout = ({ children }: { children: React.ReactNode }) => {
return <section className="flex flex-col h-full gap-4">{children}</section>;
};
Expand Down
137 changes: 129 additions & 8 deletions app/[locale]/projects/page.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,144 @@
'use client';
import fetchProjectsData from '@/actions/fetchProjectsData';
import FiltersBar from '@/components/Projects/FiltersBar/FiltersBar';
import { MempmizedProjectsDisplay } from '@/components/Projects/ProjectDisplay';
import useFetchProjects from '@/hooks/useFetchProjects';
import React from 'react';
import { ProjectFilter } from '@/types';
import { Project, ProjectPaginationFilter } from '@/types/project';
import React, { useCallback, useEffect, useState } from 'react';

const ProjectsPage = () => {
const { projects, loading, tags, setTagsToFilterBy } = useFetchProjects();
const [projects, setProjects] = useState<Project[]>([]);
const [loading, setLoading] = useState(true);
// tags to filter by state based on fetched languages
const [tags, setTags] = useState<ProjectFilter[]>([]);
const [filter, setFilter] = useState(ProjectPaginationFilter.ALL);
const [searchByProjectNameValue, setSearchByProjectNameValue] = useState('');

if (loading) return <div>Loading...</div>;
/**
* @param {Project} project
* @returns {boolean}
* @description checks if project includes active tags
*/
const projectIncludesActiveTags = useCallback(
(project: Project) => {
return project.item.data.repository.languages.edges.some(edge =>
tags.some(tag => tag.isActive && tag.name === edge.node.name)
);
},
[tags]
);

/**
*
* @param {ProjectFilter} tag
* @description toggles tag active state
*/
const toggleTagActive = (tag: ProjectFilter) => {
setLoading(true);
const existingTags = structuredClone(tags);

// we set tag active to true
for (const exisitingTag of existingTags) {
if (exisitingTag.name === tag.name) {
exisitingTag.isActive = !exisitingTag.isActive;
break;
}
}

setTags(existingTags);
};

useEffect(() => {
setLoading(false);
}, [tags]);

/**
*
* @param {ProjectPaginationFilter} filter
* @description sets filter to fetch projects by
*/
const setFetchByCategoryHandler: (
filter: ProjectPaginationFilter
) => void = filter => {
setFilter(filter);
};

// setSearchByProjectName handler
const setNewSearchInputValueHandler = (value: string) => {
setSearchByProjectNameValue(value);
};

const debouncedFetchProjectsData = useCallback(async () => {
console.log('first', Date.now());
setLoading(true);
try {
const { projects, pageLanguages } = await fetchProjectsData({
page: 1,
limit: 100,
filter,
});

setProjects(
projects.filter(p =>
p.item.data.repository.name
.toLocaleLowerCase()
.trim()
.includes(searchByProjectNameValue.toLocaleLowerCase().trim())
)
);

const newTags: ProjectFilter[] = [];
pageLanguages.forEach(lang => {
newTags.push({ name: lang, isActive: true });
});
setTags(newTags);
} catch (error) {
console.error('Failed to fetch projects:', error);
} finally {
setLoading(false);
}
}, [filter, searchByProjectNameValue]);

useEffect(() => {
debouncedFetchProjectsData();

// eslint-disable-next-line react-hooks/exhaustive-deps
}, [filter, searchByProjectNameValue]);

return (
<div className="projects flex flex-col gap-4">
<div className="w-full max-w-[1240px] mx-auto flex flex-col justify-center items-center gap-[51px]">
<div className="flex flex-col items-center gap-[5px]">
<h1 className="h1 font-bold">הפרויקטים</h1>
<h2 className="h4-roman text-xl text-center">
עמוד הפרויקטים של הקהילה. תתפנקו...
</h2>
</div>
</div>
{projects && (
<>
<FiltersBar filters={tags} setTagsToFilterBy={setTagsToFilterBy} />
{projects?.length ? (
<MempmizedProjectsDisplay projects={projects} />
<FiltersBar
filters={tags}
setTagsToFilterBy={toggleTagActive}
setFetchByCategory={setFetchByCategoryHandler}
setSearchByProjectName={setNewSearchInputValueHandler}
/>
{/* Project list */}
{loading ? (
<div className="flex flex-col gap-4 h-[75vh] mb-10 w-[90%] md:w-full max-w-[1240px] mx-auto pl-2">
Populating projects...
</div>
) : projects?.length ? (
<MempmizedProjectsDisplay
projects={projects.filter(projectIncludesActiveTags)}
activeLanguagesNames={tags
.filter(tag => tag.isActive)
.map(tag => tag.name)}
/>
) : (
<>No projects found</>
<div className="flex flex-col gap-4 h-[75vh] mb-10 w-[90%] md:w-full max-w-[1240px] mx-auto pl-2">
No projects found
</div>
)}
</>
)}
Expand Down
29 changes: 21 additions & 8 deletions components/Common/inputs/SearchInput.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,39 @@
import React from 'react';
import React, { useState } from 'react';
import Image from 'next/image';

interface SearchInputProps {
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
type SearchInputProps = {
onChange: (value: string) => void;
placeHolderText: string;
backgroundColor?: string;
darkBackgroundColor?: string;
}
};

export const SearchInput = ({
onChange,
placeHolderText = 'חיפוש...',
backgroundColor = 'bg-gray-50',
darkBackgroundColor = 'bg-gray-600',
onChange,
}: SearchInputProps) => {
const timeOutIdRef = React.useRef<number>();
const [lastSearchTerm, setLastSearchTerm] = useState<string>('');

const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (timeOutIdRef.current) clearTimeout(timeOutIdRef.current);

timeOutIdRef.current = setTimeout(() => {
if (event.target.value === lastSearchTerm) return;
onChange(event.target.value);
setLastSearchTerm(event.target.value);
}, 500) as unknown as number;
};

return (
<>
<div className="relative flex-1 h-10">
<input
type="text"
className={`h-full w-full pl-[42px] pr-[18px] rounded-[50px] ${backgroundColor} dark:${darkBackgroundColor} outline-none focus:outline-2 focus:outline-purple-500`}
placeholder={placeHolderText}
onChange={onChange}
onChange={handleSearchChange}
/>

<Image
Expand All @@ -30,6 +43,6 @@ export const SearchInput = ({
width={24}
height={24}
/>
</>
</div>
);
};
2 changes: 1 addition & 1 deletion components/Projects/FiltersBar/FilterBtn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const FilterBtn: React.FC<FilterBtnProps> = ({ filter, onBtnClick }) => {
onClick={onBtnClick}
type="button"
>
&#10005;&nbsp;&nbsp;{filter.name}
{filter.name}
</button>
);
};
Expand Down
16 changes: 7 additions & 9 deletions components/Projects/FiltersBar/FilterBtnsGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,13 @@ const FilterBtnsGroup = ({

return (
<div className="flex gap-2 flex-wrap">
{filters.map(filter =>
filter.isActive ? (
<FilterBtn
key={filter.name}
filter={filter}
onBtnClick={() => handleBtnFilterClick(filter)}
/>
) : null
)}
{filters.map(filter => (
<FilterBtn
key={filter.name}
filter={filter}
onBtnClick={() => handleBtnFilterClick(filter)}
/>
))}
</div>
);
};
Expand Down
Loading

0 comments on commit bfe81b4

Please sign in to comment.