Skip to content

Commit

Permalink
migrated tools dashboard
Browse files Browse the repository at this point in the history
  • Loading branch information
devilkiller-ag committed Mar 20, 2024
1 parent 07f2f08 commit 1b70f5b
Show file tree
Hide file tree
Showing 5 changed files with 256 additions and 11 deletions.
4 changes: 2 additions & 2 deletions components/tools/CategoryDropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import type { ToolsListType } from '@/types/components/tools/ToolDataType';
import type { ToolsListData } from '@/types/components/tools/ToolDataType';

import ToolsDataList from '../../config/tools.json';

interface CategoryDropdownProps {
setopenCategory: React.Dispatch<React.SetStateAction<boolean>>;
};

const ToolsData = ToolsDataList as ToolsListType;
const ToolsData = ToolsDataList as ToolsListData;

/**
* @description This component displays Category Dropdown.
Expand Down
4 changes: 2 additions & 2 deletions components/tools/Filters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export default function Filters({ setOpenFilter }: FiltersProps) {
// Set the params key only when the default value of the key changes. This is to know when the user actually applies filter(s).

if (checkOwner) {
searchParams.set('owned', checkOwner.toString());
searchParams.set('owned', isAsyncAPIOwner ? 'true' : 'false');
}
if (checkedTechnology.length > 0) {
searchParams.set('techs', checkedTechnology.join(','));
Expand Down Expand Up @@ -134,7 +134,7 @@ export default function Filters({ setOpenFilter }: FiltersProps) {
</div>
<div className='flex gap-4'>
<label className='relative inline-flex cursor-pointer items-center'>
<input type='checkbox' value={checkOwner.toString()} className='peer sr-only' onChange={() => setCheckOwner(!checkOwner)} />
<input type='checkbox' value={isAsyncAPIOwner ? 'true' : 'false'} className='peer sr-only' onChange={() => setCheckOwner(!checkOwner)} />
<div className={twMerge(`w-11 h-6 bg-gray-200 peer-focus:outline-none rounded-full peer after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all ${checkOwner ? 'after:translate-x-full after:border-white bg-secondary-500' : ''}`)}></div>
</label>
<div className='text-sm font-medium'>
Expand Down
245 changes: 245 additions & 0 deletions components/tools/ToolsDashboard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
import { useRouter } from 'next/router';
import { useContext, useEffect, useRef, useState } from 'react';

import type { ToolsListData } from '@/types/components/tools/ToolDataType';

import ToolsDataList from '../../config/tools.json';
import ToolFilter, { ToolFilterContext } from '../../context/ToolFilterContext';
import ArrowDown from '../icons/ArrowDown';
import Cross from '../icons/Cross';
import FilterIcon from '../icons/Filter';
import SearchIcon from '../icons/Search';
import CategoryDropdown from './CategoryDropdown';
import Filters from './Filters';
import ToolsList from './ToolsList';

const ToolsData = ToolsDataList as ToolsListData;

/**
* @description This component displays Tools Dashboard.
*/
export default function ToolsDashboard() {
const router = useRouter();
const loader = 'img/loaders/loader.png'; // preloader image for the tools

const [loading, setLoading] = useState<boolean>(false); // used to handle the preloader on the page
const filterRef = useRef<HTMLDivElement>(); // used to provide ref to the Filter menu and outside click close feature
const categoryRef = useRef<HTMLDivElement>(); // used to provide ref to the Category menu and outside click close feature
const [openFilter, setOpenFilter] = useState<boolean>(false);
const [openCategory, setopenCategory] = useState<boolean>(false);
// filter parameters extracted from the context
const { isPaid, isAsyncAPIOwner, languages, technologies, categories } = useContext(ToolFilterContext);
const [searchName, setSearchName] = useState<string>(''); // state variable used to get the search name
const [toolsList, setToolsList] = useState<ToolsListData>({}); // state variable used to set the list of tools according to the filters applied
const [checkToolsList, setCheckToolsList] = useState<boolean>(true); // state variable used to check whether any tool is available according to the needs of the user.

// useEffect function to enable the close Modal feature when clicked outside of the modal
useEffect(() => {
const checkIfClickOutside = (event: MouseEvent) => {
if (openFilter && filterRef.current && !filterRef.current.contains(event.target as Node)) {
setOpenFilter(false);
}
};

document.addEventListener('mousedown', checkIfClickOutside);

return () => {
document.removeEventListener('mousedown', checkIfClickOutside);
};
});

// sets the preloader on the page for 1 second
useEffect(() => {
setLoading(true);
setTimeout(() => {
setLoading(false);
}, 1000);
}, []);

// useEffect function to enable the close Category dropdown Modal feature when clicked outside of the modal
useEffect(() => {
const checkIfClickOutside = (event: MouseEvent) => {
if (openCategory && categoryRef.current && !categoryRef.current.contains(event.target as Node)) {
setopenCategory(false);
}
};

document.addEventListener('mousedown', checkIfClickOutside);

return () => {
document.removeEventListener('mousedown', checkIfClickOutside);
};
});

// Function to update the list of tools according to the current filters applied
const updateToolsList = () => {
let tempToolsList: ToolsListData = {};

// Tools data list is first filtered according to the category filter if applied by the user.
// Hence if any category is selected, then only respective tools will be selected for further check on filters
if (categories.length > 0) {
for (const category of categories) {
// eslint-disable-next-line @typescript-eslint/no-loop-func
Object.keys(ToolsData).forEach((key) => {
if (key === category) {
tempToolsList[key] = JSON.parse(JSON.stringify(ToolsData[key]));
}
});
}
} else {
// if no category is selected, then all tools are selected for further check on filters
tempToolsList = JSON.parse(JSON.stringify(ToolsData));
}

// checkToolsList is initially made false to check whether any tools are present according to the filters.
setCheckToolsList(false);

// Each tool selected is then traversed to check against each filter variable (only if the filter is applied),
// whether they match with the filter applied or not.
Object.keys(tempToolsList).forEach((category) => {
tempToolsList[category].toolsList = tempToolsList[category].toolsList.filter((tool) => {
// These are filter check variables for respective filters, which are initially made true.
// If the particular filter is applied by the user, the respective check variable is made false first,
// and then tool parameters are checked against the filter variable value to decide if it matches the filter criteria or not.
let isLanguageTool = true;
let isTechnologyTool = true;
let isSearchTool = true;
let isAsyncAPITool = true;
let isPaidTool = true;

if (languages.length) {
isLanguageTool = false;
for (const language of languages) {
if (tool?.filters?.language && tool.filters.language.find((item) => item.name === language)) {
isLanguageTool = true;
}
}
}

if (technologies.length) {
isTechnologyTool = false;
for (const technology of technologies) {
if (tool?.filters?.technology && tool.filters.technology.find((item) => item.name === technology)) {
isTechnologyTool = true;
}
}
}

if (searchName) {
isSearchTool = tool.title.toLowerCase().includes(searchName.toLowerCase());
}

if (isAsyncAPIOwner) {
isAsyncAPITool = tool.filters.isAsyncAPIOwner === isAsyncAPIOwner;
}

if (isPaid !== 'all') {
if (isPaid === 'free') {
isPaidTool = tool.filters.hasCommercial === false;
} else {
isPaidTool = tool.filters.hasCommercial === true;
}
}

return isLanguageTool && isTechnologyTool && isSearchTool && isAsyncAPITool && isPaidTool;
});

if (tempToolsList[category].toolsList.length) {
setCheckToolsList(true);
}
});

setToolsList(tempToolsList);
};

const clearFilters = () => {
setOpenFilter(false);
router.push('/tools', undefined, { shallow: true });
};

useEffect(() => {
updateToolsList();
}, [isPaid, isAsyncAPIOwner, languages, technologies, categories, searchName]);

const isFiltered = Boolean(isPaid !== 'all' || isAsyncAPIOwner || languages.length || technologies.length || categories.length);

return (
<ToolFilter>
<div>
<div className='my-10 flex flex-wrap justify-between gap-4 lg:flex-nowrap'>
<div className='flex h-auto w-[47%] gap-5 lg:w-1/5'>
<div className='relative h-auto w-full' ref={filterRef as React.LegacyRef<HTMLDivElement>}>
<div
className='flex h-14 w-full cursor-pointer items-center justify-center gap-2 rounded-lg border border-gray-300 px-4 py-1 text-sm text-gray-700 shadow hover:border-gray-600 hover:shadow-md'
onClick={() => setOpenFilter(!openFilter)} data-testid='ToolsDashboard-Filters-Click'
>
<FilterIcon />
<div>Filter</div>
</div>
{openFilter && (
<button className='absolute top-16 z-20 min-w-[20rem]'>
<Filters setOpenFilter={setOpenFilter} />
</button>
)}
</div>
</div>
<div className='flex h-auto w-[47%] gap-5 lg:w-1/5'>
<div className='relative h-auto w-full' ref={categoryRef as React.LegacyRef<HTMLDivElement>}>
<div
className='flex h-14 w-full cursor-pointer items-center justify-center gap-2 rounded-lg border border-gray-300 px-4 py-1 text-sm text-gray-700 shadow hover:border-gray-600 hover:shadow-md'
onClick={() => setopenCategory(!openCategory)}
data-testid='ToolsDashboard-category'
>
<div>Jump to Category</div>
<ArrowDown className={`my-auto ${openCategory ? 'rotate-180' : ''}`} />
</div>
{openCategory && (
<div className='absolute right-52 top-16 z-20'>
<CategoryDropdown setopenCategory={setopenCategory} />
</div>
)}
</div>
</div>
<div className='flex h-14 w-[100%] rounded-lg border border-gray-300 px-4 py-1 text-sm text-gray-700 shadow hover:border-gray-600 focus:border-gray-600 lg:w-4/5'>
<SearchIcon className='my-auto opacity-70' />
<input
className='w-11/12 flex-1 border-none outline-none focus:ring-0'
placeholder='Search by name'
type='text'
value={searchName}
onChange={(e) => setSearchName(e.target.value)}
/>
{searchName && (
<button className='my-auto h-fit rounded-full p-2 hover:bg-gray-100' onClick={() => setSearchName('')}>
<img src='/img/illustrations/icons/close-icon.svg' alt='close' width='10' />
</button>
)}
</div>
</div>
{isFiltered && (
<div className='mt-4 flex cursor-pointer items-center text-gray-600 hover:text-black' onClick={clearFilters} >
<Cross />
<span className='ml-3' >Clear Filters</span>
</div>
)}
{loading ? (
<div className='mx-auto my-24 flex w-fit animate-pulse gap-4 text-black'>
<img src={loader} alt='loading' className='mx-auto w-16' />
<div className='my-auto text-xl'>Loading Tools...</div>
</div>
) : (
<div className='mt-0'>
{checkToolsList ? (
<ToolsList toolsListData={toolsList} />
) : (
<div className='p-4'>
<img src='/img/illustrations/not-found.webp' alt='not found' className='m-auto w-1/2' />
<div className='text-center text-lg'> Sorry, we don&apos;t have tools according to your needs. </div>
</div>
)}
</div>
)}
</div>
</ToolFilter>
);
};
6 changes: 3 additions & 3 deletions components/tools/ToolsList.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ToolsListType } from '@/types/components/tools/ToolDataType';
import type { ToolsListData } from '@/types/components/tools/ToolDataType';
import { HeadingTypeStyle } from '@/types/typography/Heading';
import { ParagraphTypeStyle } from '@/types/typography/Paragraph';

Expand All @@ -7,14 +7,14 @@ import Paragraph from '../typography/Paragraph';
import ToolsCard from './ToolsCard';

interface ToolsListProp {
toolsListData: ToolsListType;
toolsListData: ToolsListData;
}

/**
* @description This component displays list of tools.
*
* @param {ToolsListProp} props - Props for the ToolsList component.
* @param {ToolsListType} props.toolsListData - List of Tools.
* @param {ToolsListData} props.toolsListData - List of Tools.
*/
export default function ToolsList({ toolsListData }: ToolsListProp) {
return (
Expand Down
8 changes: 4 additions & 4 deletions types/components/tools/ToolDataType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,19 @@ export interface Language {
name: string;
color: string;
borderColor: string;
}
};

export interface Technology {
name: string;
color: string;
borderColor: string;
}
};

export interface Category {
name: string;
tag: string;
description: string;
}
};

export interface Filter {
categories: string[];
Expand All @@ -37,7 +37,7 @@ export interface ToolData {
filters: Filter;
};

export interface ToolsListType {
export interface ToolsListData {
[category: string]: {
description: string;
toolsList: ToolData[];
Expand Down

0 comments on commit 1b70f5b

Please sign in to comment.