feat: add more filters, refactor filters state logic

This commit is contained in:
MTG2000
2022-10-20 17:31:30 +03:00
parent 8a86c132e4
commit fc1a84f3bb
6 changed files with 177 additions and 79 deletions

View File

@@ -12,7 +12,7 @@ export default function ProjectCardMini({ project, onClick }: Props) {
return (
<div
className="py-16 select-none px-16 flex items-center gap-16 rounded-16 hover:bg-gray-100"
className="py-16 select-none px-16 flex items-center gap-16 rounded-16 hover:bg-gray-50 hover:outline outline-1 outline-gray-200"
onKeyDown={e => {
e.key !== 'Enter' || onClick(project?.id!)
}}
@@ -20,7 +20,7 @@ export default function ProjectCardMini({ project, onClick }: Props) {
tabIndex={0}
role='button'
>
<img src={project?.logo?.[0]['thumbnails']['large'].url} alt={project?.title ?? ''} draggable="false" className="flex-shrink-0 w-64 h-64 object-cover bg-gray-200 border-0 rounded-full hover:cursor-pointer"></img>
<img src={project?.logo?.[0]['thumbnails']['large'].url} alt={project?.title ?? ''} draggable="false" className="flex-shrink-0 w-64 h-64 object-cover bg-gray-200 border border-gray-200 rounded-full hover:cursor-pointer"></img>
<div className="justify-around items-start min-w-0 flex-1 hover:cursor-pointer"
>
<p className="text-body4 w-full font-bold overflow-ellipsis overflow-hidden whitespace-nowrap">{project?.title}</p>

View File

@@ -1,7 +1,6 @@
import { useParams, Navigate } from 'react-router-dom'
import ErrorMessage from 'src/Components/Errors/ErrorMessage/ErrorMessage';
import { useExplorePageQuery } from 'src/graphql';
import HeaderImage from './Header/Header';
import ProjectsGrid from './ProjectsGrid/ProjectsGrid';
import { Helmet } from "react-helmet";
import Categories, { Category } from '../../Components/Categories/Categories';
@@ -13,36 +12,53 @@ import { openModal } from 'src/redux/features/modals.slice';
import { createAction } from '@reduxjs/toolkit';
import { useReduxEffect } from 'src/utils/hooks/useReduxEffect';
import { NetworkStatus } from '@apollo/client';
import { GoSettings } from 'react-icons/go'
import { FiSliders } from 'react-icons/fi';
import { ProjectsFilters } from './Filters/FiltersModal';
const UPDATE_FILTERS_ACTION = createAction<{ categoriesIds?: string[], tagsIds?: string[], yearFounded?: string }>('PROJECTS_FILTERS_UPDATED')({})
const UPDATE_FILTERS_ACTION = createAction<Partial<ProjectsFilters>>('PROJECTS_FILTERS_UPDATED')({})
type QueryFilter = Partial<{
categoryId: string[] | null
tags: string[] | null
yearFounded: number | null
dead: boolean | null
license: string | null
}>
export default function ExplorePage() {
const dispatch = useAppDispatch();
const [filters, setFilters] = useState<Partial<ProjectsFilters>>()
const [filters, setFilters] = useState<Partial<ProjectsFilters> | null>(null)
const [selectedCategory, setSelectedCategory] = useState<Category | null>(null)
const queryFilters = useMemo(() => {
let filter: any = {}
let filter: QueryFilter = {}
if (!filters) return null;
if (filters.categoriesIds && filters.categoriesIds?.length > 0)
filter['categoryId'] = filters.categoriesIds
filter.categoryId = filters.categoriesIds;
if (selectedCategory?.id) filter.categoryId = [selectedCategory?.id]
if (filters.tagsIds && filters.tagsIds?.length > 0)
filter['tags'] = filters.tagsIds
filter.tags = filters.tagsIds
if (filters.yearFounded && filters.yearFounded !== "Any")
filter['yearFounded'] = Number(filters.yearFounded)
if (filters.yearFounded && filters.yearFounded !== 'any')
filter.yearFounded = Number(filters.yearFounded)
if (Object.keys(filter).length === 0) return null
if (filters.projectStatus && filters.projectStatus !== 'any')
filter.dead = filters.projectStatus === 'alive' ? false : true;
if (filters.projectLicense && filters.projectLicense !== 'any')
filter.license = filters.projectLicense
if (Object.keys(filter).length === 0)
return null
return filter;
}, [filters])
}, [filters, selectedCategory?.id])
const { data, networkStatus, error } = useExplorePageQuery({
variables: {
@@ -55,11 +71,12 @@ export default function ExplorePage() {
const onFiltersUpdated = useCallback(({ payload }: typeof UPDATE_FILTERS_ACTION) => {
setSelectedCategory(null)
setFilters(payload);
if (Object.keys(payload).length === 0)
setFilters(null);
else
setFilters(payload);
}, [])
useReduxEffect(onFiltersUpdated, UPDATE_FILTERS_ACTION.type)
@@ -83,13 +100,8 @@ export default function ExplorePage() {
}))
}
const selectCategoryTab = (category?: Category | null) => {
if (!category?.id)
return;
const selectCategoryTab = (category: Category | null) => {
setSelectedCategory(category);
setFilters({
categoriesIds: [category.id]
})
}
if (error) {
@@ -98,7 +110,6 @@ export default function ExplorePage() {
</div>
}
console.log(networkStatus, NetworkStatus.loading, NetworkStatus.refetch, NetworkStatus.setVariables);
const isLoading = networkStatus === NetworkStatus.loading || networkStatus === NetworkStatus.refetch || networkStatus === NetworkStatus.setVariables;
@@ -115,7 +126,14 @@ export default function ExplorePage() {
/>
<div className="grid grid-cols-[1fr_auto] items-center gap-32">
<div className="min-w-0"><Categories value={selectedCategory} onChange={v => selectCategoryTab(v)} /></div>
<Button className='self-center' variant='outline' color='white' onClick={openFilters}><FiSliders className="scale-150 mr-8 text-primary-500" /> <span className='align-middle'>Filters</span></Button>
<Button
className={`self-center ${!!queryFilters ? "!font-bold !bg-primary-50 !text-primary-600 !border-2 !border-primary-400" : "!text-gray-600"}`}
variant='outline'
color='white'
onClick={openFilters}>
<FiSliders className="scale-150 mr-12" />
<span className='align-middle'>Filters</span>
</Button>
</div>
<div className="mt-40">
<ProjectsGrid

View File

@@ -1,15 +1,11 @@
import React, { FormEvent, useState } from 'react'
import { useState } from 'react'
import { ModalCard, modalCardVariants } from 'src/Components/Modals/ModalsContainer/ModalsContainer'
import { motion } from 'framer-motion'
import { IoClose } from 'react-icons/io5'
import Button from 'src/Components/Button/Button'
import { useAppDispatch } from 'src/utils/hooks'
import { PayloadAction } from '@reduxjs/toolkit'
import * as yup from "yup";
import { SubmitHandler, useForm } from "react-hook-form"
import { yupResolver } from "@hookform/resolvers/yup";
import IconButton from 'src/Components/IconButton/IconButton'
import Badge from 'src/Components/Badge/Badge'
import { useGetFiltersQuery } from 'src/graphql'
import Skeleton from 'react-loading-skeleton';
import { random } from 'src/utils/helperFunctions';
@@ -29,10 +25,11 @@ export interface IFormInputs {
export type ProjectsFilters = {
categoriesIds: string[]
tagsIds: string[]
yearFounded: string
yearFounded: typeof yearsFoundedOptions[number]['value'],
projectStatus: typeof projectStatusOptions[number]['value']
projectLicense: typeof licensesOptions[number]['value']
}
const yearsFounded = ['Any', '2016', '2017', '2018', '2019', '2020', '2021', '2022']
export default function FiltersModal({ onClose, direction, initFilters, callbackAction, ...props }: Props) {
@@ -41,13 +38,15 @@ export default function FiltersModal({ onClose, direction, initFilters, callback
const [categoriesFilter, setCategoriesFilter] = useState<string[]>(initFilters?.categoriesIds ?? []);
const [tagsFilter, setTagsFilter] = useState<string[]>(initFilters?.tagsIds ?? []);
const [yearFoundedFilter, setYearFoundedFilter] = useState(initFilters?.yearFounded ?? yearsFounded[0]);
const [yearFoundedFilter, setYearFoundedFilter] = useState(initFilters?.yearFounded ?? "any");
const [projectStatusFilter, setProjectStatusFilter] = useState(initFilters?.projectStatus ?? "any");
const [projectLicenseFilter, setProjectLicenseFilter] = useState(initFilters?.projectLicense ?? "any");
const clickCategory = (id: string) => {
if (categoriesFilter.includes(id))
setCategoriesFilter(categoriesFilter.filter(v => v !== id));
setCategoriesFilter([]);
else
setCategoriesFilter([...categoriesFilter, id]);
setCategoriesFilter([id]);
}
@@ -58,16 +57,30 @@ export default function FiltersModal({ onClose, direction, initFilters, callback
setTagsFilter([...tagsFilter, id]);
}
const clearFilters = () => {
setCategoriesFilter([]);
setTagsFilter([]);
setYearFoundedFilter(yearsFounded[0]);
const createActionPayload = (filters: Partial<ProjectsFilters>) => {
const action = Object.assign({}, callbackAction);
let payload: any = {};
for (const [key, value] of Object.entries(filters)) {
if (filters[(key as keyof typeof filters)] != null) payload[key] = value;
}
action.payload = payload;
return action
}
const applyFilters = () => {
const action = Object.assign({}, callbackAction);
action.payload = { categoriesIds: categoriesFilter, tagsIds: tagsFilter, yearFounded: yearFoundedFilter }
dispatch(action)
dispatch(createActionPayload({
categoriesIds: categoriesFilter,
tagsIds: tagsFilter,
yearFounded: yearFoundedFilter,
projectStatus: projectStatusFilter,
projectLicense: projectLicenseFilter
}))
onClose?.();
}
const clearFilters = () => {
dispatch(dispatch(createActionPayload({ categoriesIds: [], tagsIds: [], yearFounded: 'any', projectStatus: 'any', projectLicense: "any" })))
onClose?.();
}
@@ -155,20 +168,57 @@ export default function FiltersModal({ onClose, direction, initFilters, callback
<div>
<h3 className="text-body2 font-bolder">📆 Founded</h3>
<p className='text-gray-600 mt-8'>Select the year you wish to see companies founded in.</p>
<div className="flex flex-wrap gap-48 mt-24">
{yearsFounded.map(year =>
<div key={year} className='flex gap-16 items-center'>
<div className="flex flex-wrap gap-x-48 gap-y-24 mt-24">
{yearsFoundedOptions.map(year =>
<label key={year.text} className='flex gap-16 items-center'>
<input
name='yearFounded'
value={year}
checked={year === yearFoundedFilter}
value={year.value}
checked={year.value === yearFoundedFilter}
className='input-radio self-center'
onChange={e => setYearFoundedFilter(e.target.value)}
onChange={e => setYearFoundedFilter(e.target.value as typeof year.value)}
type="radio" />
<label className="text-body4 text-gray-800" >
{year}
</label>
</div>)}
<span className="text-body4 text-gray-800">{year.text}</span>
</label>)}
</div>
</div>
<hr className="bg-gray-100" />
<div>
<h3 className="text-body2 font-bolder">💙 Project status</h3>
<p className='text-gray-600 mt-8'>Select an option from below.</p>
<div className="flex flex-wrap gap-x-48 gap-y-24 mt-24">
{projectStatusOptions.map(status =>
<label key={status.text} className='flex gap-16 items-center'>
<input
name='projectStatus'
value={status.value}
checked={status.value === projectStatusFilter}
className='input-radio self-center'
onChange={e => setProjectStatusFilter(e.target.value as typeof status.value)}
type="radio" />
<span className="text-body4 text-gray-800">{status.text}</span>
</label>)}
</div>
</div>
<hr className="bg-gray-100" />
<div>
<h3 className="text-body2 font-bolder">💻 License type</h3>
<p className='text-gray-600 mt-8'>What type of license does this open source project have?</p>
<div className="flex flex-wrap gap-16 mt-24">
{licensesOptions.map(license =>
<button
key={license.value}
className={`
px-12 py-8 border rounded-10 text-body5 font-medium
active:scale-95 transition-transform
${projectLicenseFilter !== license.value ? "bg-gray-100 hover:bg-gray-200 border-gray-200" : "bg-primary-100 text-primary-600 border-primary-200"}
`}
onClick={() => setProjectLicenseFilter(license.value)}
>{license.text}
</button>)}
</div>
</div>
@@ -184,3 +234,34 @@ export default function FiltersModal({ onClose, direction, initFilters, callback
</motion.div>
)
}
const yearsFoundedOptions = [
{ value: "any", text: "Any" },
{ value: "2016", text: "2016" },
{ value: "2017", text: "2017" },
{ value: "2018", text: "2018" },
{ value: "2019", text: "2019" },
{ value: "2020", text: "2020" },
{ value: "2021", text: "2021" },
{ value: "2022", text: "2022" },
] as const
const projectStatusOptions = [
{ value: "any", text: "Any" },
{ value: 'alive', text: "Alive 🌱" },
{ value: 'dead', text: "RIP 💀" },
] as const
const licensesOptions = [
{ value: "any", text: "Any" },
{ value: 'MIT License', text: "MIT License" },
{ value: 'ISC License', text: "ISC License" },
{ value: 'Public domain', text: "Public domain" },
{ value: 'Apache License 2.0', text: "Apache License 2.0" },
{ value: 'GNU General Public License v2.0', text: "GNU General Public License v2.0" },
{ value: 'GNU General Public License v3.0', text: "GNU General Public License v3.0" },
{ value: 'Creative Commons Zero v1.0 Universal', text: "Creative Commons Zero v1.0 Universal" },
{ value: 'GNU Affero General Public License v3.0', text: "GNU Affero General Public License v3.0" },
{ value: 'Other', text: "Other" },
] as const

View File

@@ -1,5 +1,5 @@
query ProjectDetails($projectsId: String) {
projects(id: $projectsId) {
getProject: projects(id: $projectsId) {
id
title
dead

View File

@@ -42,12 +42,14 @@ export default function ProjectDetailsCard({ direction, projectId, ...props }: P
projectsId: projectId!,
},
onCompleted: data => {
dispatch(setProject((data.projects?.[0] as any) ?? null))
dispatch(setProject((data.getProject?.[0] as any) ?? null))
},
onError: () => {
dispatch(setProject(null));
},
skip: !Boolean(projectId)
skip: !Boolean(projectId),
fetchPolicy: "no-cache"
});
@@ -72,7 +74,7 @@ export default function ProjectDetailsCard({ direction, projectId, ...props }: P
return <ProjectCardSkeleton onClose={closeModal} direction={direction} isPageModal={props.isPageModal} />;
const project = data?.projects?.[0];
const project = data?.getProject?.[0];
if (!project) return <p>404</p>
@@ -126,8 +128,8 @@ export default function ProjectDetailsCard({ direction, projectId, ...props }: P
{/* Title & Basic Info */}
<div className="flex flex-col mt-[-80px] md:flex-row md:mt-0 gap-24 md:items-center relative">
<div className="flex-shrink-0 w-[108px] h-[108px]">
<img className="w-full h-full border-2 border-white rounded-24 object-cover" src={project?.logo?.[0]['thumbnails']['large'].url} alt="" />
<div className="flex-shrink-0 w-[108px] h-[108px] border-2 border-gray-200 rounded-24 overflow-hidden ">
<img className="w-full h-full object-cover" src={project?.logo?.[0]['thumbnails']['large'].url} alt="" />
</div>
<div className='flex flex-col gap-8 items-start justify-between'>
<a href={project?.website!} target='_blank' rel="noreferrer"><h3 className="text-body1 font-bold">{project?.title}</h3></a>
@@ -173,26 +175,23 @@ export default function ProjectDetailsCard({ direction, projectId, ...props }: P
</div>
</div>
<div>
<p className="text-body6 uppercase font-medium text-gray-400 mb-8">DATA</p>
<div className="flex flex-wrap gap-8">
{project?.dead && <Badge size='sm'>{project.dead}</Badge>}
{project?.createdAt && <Badge size='sm'>{project.createdAt}</Badge>}
{project?.companyName && <Badge size='sm'>{project.companyName}</Badge>}
{project?.endDate && <Badge size='sm'>{project.endDate}</Badge>}
{project?.updatedAt && <Badge size='sm'>{project.updatedAt}</Badge>}
{project?.watchers && <Badge size='sm'>{project.watchers}</Badge>}
{project?.yearFounded && <Badge size='sm'>{project.yearFounded}</Badge>}
{project?.subcategory && <Badge size='sm'>{project.subcategory}</Badge>}
{project?.stars && <Badge size='sm'>{project.stars}</Badge>}
{project?.repository && <Badge size='sm'>{project.repository}</Badge>}
{project?.openSource && <Badge size='sm'>{project.openSource}</Badge>}
{project?.linkedIn && <Badge size='sm'>{project.linkedIn}</Badge>}
{project?.license && <Badge size='sm'>{project.license}</Badge>}
{project?.language && <Badge size='sm'>{project.language}</Badge>}
{project?.forks && <Badge size='sm'>{project.forks}</Badge>}
</div>
<div>
<p className="text-body6 uppercase font-medium text-gray-400 mb-8">DATA</p>
<div className="flex flex-wrap gap-8">
{project?.dead !== null && <Badge color='none' className='bg-red-100' size='sm'>Dead</Badge>}
{project?.createdAt !== null && <Badge size='sm'>Created at: {new Date(project.createdAt).toLocaleDateString()}</Badge>}
{project?.companyName !== null && <Badge size='sm'>Company Name: {project.companyName}</Badge>}
{project?.endDate !== null && <Badge size='sm'>End date: {new Date(project.endDate).toLocaleDateString()}</Badge>}
{project?.repository !== null && <Badge size='sm'><a href={project.repository} target='_blank' className='text-blue-500' rel="noreferrer">Repository</a> </Badge>}
{project?.stars !== null && <Badge size='sm'>Stars: {project.stars}</Badge>}
{project?.openSource !== null && <Badge size='sm'>Open source: {project.openSource}</Badge>}
{project?.watchers !== null && <Badge size='sm'>Watchers: {project.watchers}</Badge>}
{project?.forks !== null && <Badge size='sm'>Number of forks: {project.forks}</Badge>}
{project?.license !== null && <Badge size='sm'>License: {project.license}</Badge>}
{project?.language !== null && <Badge size='sm'>Language: {project.language}</Badge>}
{project?.updatedAt !== null && <Badge size='sm'>Last updated at:{new Date(project.updatedAt).toLocaleDateString()}</Badge>}
</div>
</div>
<div className="text-center">
<h3 className="text-body4 font-regular">Want to suggest any changes to this project?</h3>

View File

@@ -425,7 +425,7 @@ export type ProjectDetailsQueryVariables = Exact<{
}>;
export type ProjectDetailsQuery = { __typename?: 'Query', projects: Array<{ __typename?: 'projects', id: string | null, title: string | null, dead: boolean | null, createdAt: string | null, companyName: string | null, category: string | null, description: string | null, discord: string | null, endDate: string | null, twitter: string | null, updatedAt: string | null, watchers: number | null, website: string | null, yearFounded: number | null, telegram: string | null, stars: number | null, repository: string | null, openSource: string | null, logo: Array<any | null> | null, linkedIn: string | null, license: string | null, language: string | null, forks: number | null, categoryList: Array<{ __typename?: 'categoryList', name: string | null } | null> | null, tags: Array<{ __typename?: 'tags', id: string | null, name: string | null, icon: string | null } | null> | null } | null> | null };
export type ProjectDetailsQuery = { __typename?: 'Query', getProject: Array<{ __typename?: 'projects', id: string | null, title: string | null, dead: boolean | null, createdAt: string | null, companyName: string | null, category: string | null, description: string | null, discord: string | null, endDate: string | null, twitter: string | null, updatedAt: string | null, watchers: number | null, website: string | null, yearFounded: number | null, telegram: string | null, stars: number | null, repository: string | null, openSource: string | null, logo: Array<any | null> | null, linkedIn: string | null, license: string | null, language: string | null, forks: number | null, categoryList: Array<{ __typename?: 'categoryList', name: string | null } | null> | null, tags: Array<{ __typename?: 'tags', id: string | null, name: string | null, icon: string | null } | null> | null } | null> | null };
export const AllCategoriesDocument = gql`
@@ -570,7 +570,7 @@ export type ExplorePageLazyQueryHookResult = ReturnType<typeof useExplorePageLaz
export type ExplorePageQueryResult = Apollo.QueryResult<ExplorePageQuery, ExplorePageQueryVariables>;
export const ProjectDetailsDocument = gql`
query ProjectDetails($projectsId: String) {
projects(id: $projectsId) {
getProject: projects(id: $projectsId) {
id
title
dead