Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: migrate tools components to typescript #2774

Merged
merged 23 commits into from
Mar 29, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ec8f1bc
migrated tools/Tags.tsx to TypeScript
devilkiller-ag Mar 14, 2024
8e3d9e6
migrated tools/CardData to typescript
devilkiller-ag Mar 14, 2024
6e066b3
migrated ToolsCard component
devilkiller-ag Mar 15, 2024
01f8f95
migrated tools list to typescript
devilkiller-ag Mar 15, 2024
07c20db
Merge branch 'migrate-ts' into ag-ts-tools
devilkiller-ag Mar 18, 2024
5b859d1
migrated CategoryDropdown to TypeScript
devilkiller-ag Mar 18, 2024
9946faa
fixed lint issues
devilkiller-ag Mar 18, 2024
f252944
migrated filter dropdown
devilkiller-ag Mar 18, 2024
1903539
migrated filter display
devilkiller-ag Mar 18, 2024
07f2f08
migrated filter component
devilkiller-ag Mar 19, 2024
1b70f5b
migrated tools dashboard
devilkiller-ag Mar 20, 2024
5bbf451
Merge branch 'migrate-ts' into ag-ts-tools
devilkiller-ag Mar 20, 2024
7b05f1d
incoporated review suggestions
devilkiller-ag Mar 27, 2024
d6a7cbc
Merge branch 'migrate-ts' into ag-ts-tools
devilkiller-ag Mar 27, 2024
6e85813
fixed lint issue
devilkiller-ag Mar 27, 2024
691bb8b
Merge branch 'migrate-ts' into ag-ts-tools
akshatnema Mar 27, 2024
911b372
added comments:
devilkiller-ag Mar 27, 2024
eb99304
Merge branch 'ag-ts-tools' of https://github.com/devilkiller-ag/async…
devilkiller-ag Mar 27, 2024
84ab865
fix lint issue
devilkiller-ag Mar 27, 2024
eb4c2bc
Merge branch 'migrate-ts' into ag-ts-tools
akshatnema Mar 27, 2024
89304b6
restored the original categorylist in scripts/tool
devilkiller-ag Mar 29, 2024
55feac3
Merge branch 'migrate-ts' into ag-ts-tools
devilkiller-ag Mar 29, 2024
6c5bd6f
Updated import
akshatnema Mar 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions components/tools/CardData.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import React, { useEffect, useRef, useState } from 'react';
import TextTruncate from 'react-text-truncate';

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

import InfoIcon from '../icons/InfoIcon';

interface CardDataProps {
visible: VisibleDataListType;
heading: string;
data: string;
read: boolean;
setRead: React.Dispatch<React.SetStateAction<boolean>>;
setVisible: React.Dispatch<React.SetStateAction<VisibleDataListType>>;
type: keyof VisibleDataListType;
className?: string;
};

/**
* @description This component displays Card.
*
* @param {SelectTagsProps} props - The props for the Cards Data component.
* @param {string} props.className - Additional CSS classes for the component.
* @param {VisibleDataListType} props.visible - Visibility status for different types.
* @param {string} props.heading - The heading text.
* @param {string} props.data - The data to be displayed.
* @param {boolean} props.read - Read status.
* @param {React.Dispatch<React.SetStateAction<boolean>>} props.setRead - Function to set read status.
* @param {React.Dispatch<React.SetStateAction<VisibleDataListType>>} props.setVisible - Function to set visibility status.
* @param {string} props.type - Type of the card data.
*/
export const CardData = ({
visible,
heading,
data,
read,
setRead,
setVisible,
type,
className = ''
}: CardDataProps) => {
const [outsideClick, setOutsideClick] = useState<boolean>(true);
const [description, setShowDescription] = useState<boolean>(false);
const initial = {
lang: false,
tech: false,
category: false,
pricing: false,
ownership: false
};
const domNode = useRef<HTMLSpanElement>(null);

useEffect(() => {
const divHeight = domNode.current?.offsetHeight || 0;
const numberOfLines = divHeight / 20;

if (numberOfLines > 3) {
setShowDescription(true);
} else {
setShowDescription(false);
}
}, [visible]);
akshatnema marked this conversation as resolved.
Show resolved Hide resolved

useEffect(() => {
const maybeHandler = (event: MouseEvent) => {
setOutsideClick(true);
if (domNode.current && !domNode.current.contains(event.target as Node)) {
setOutsideClick(false);
}
};

document.addEventListener('mousedown', maybeHandler);

return () => {
document.removeEventListener('mousedown', maybeHandler);
};
}, []);
akshatnema marked this conversation as resolved.
Show resolved Hide resolved

return (
<div className={className || 'text-left text-sm text-gray-500'}>
{heading}
<span className='group relative'>
{outsideClick && visible[type] && (
<span
ref={domNode}
data-testid='Carddata-description'
className='absolute -left-2/3 -top-4 z-10 w-48 translate-x-1/3 rounded border border-gray-200
bg-white px-2 py-1 text-xs shadow-md'
>
{read ? (
data
) : (
<div>
<TextTruncate element='span' line={4} text={data} />
</div>
)}
{description && (
<button
className='cursor-pointer text-cyan-600'
onClick={() => {
setOutsideClick(true);
setRead(!read);
}}
>
{read ? ' Show Less' : ' Show More'}
</button>
)}
</span>
)}
<button
onClick={() => {
setRead(false);
setVisible({ ...initial, [type]: !visible[type] });
}}
className='mx-1'
data-testid='Carddata-button'
>
<InfoIcon />
</button>
</span>
</div>
);
};
35 changes: 35 additions & 0 deletions components/tools/CategoryDropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
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 ToolsListData;

/**
* @description This component displays Category Dropdown.
*
* @param {React.Dispatch<React.SetStateAction<boolean>>} props.setopenCategory - Function to set dropdown status.
*/
export default function CategoryDropdown({ setopenCategory }: CategoryDropdownProps) {
return (
<div className='absolute z-10 h-60 w-52 origin-top-right overflow-y-auto rounded-md bg-white shadow-lg ring-1 ring-black/5 focus:outline-none lg:w-56' role='menu' aria-orientation='vertical' aria-labelledby='menu-button'
data-testid='CategoryDropdown-div'>
<div className='py-1' role='none'>
{Object.keys(ToolsData).map((categoryName, index) => {
// displaying tools category having atleast one tool
if (ToolsData[categoryName].toolsList.length > 0) {
return (
<div key={index} onClick={() => setopenCategory(false)}>
<a href={`#${categoryName}`} key={index} className='block px-4 py-2 hover:bg-gray-100' data-testid='CategoryDropdown-link'>{categoryName}</a>
</div>);
}

akshatnema marked this conversation as resolved.
Show resolved Hide resolved
return null;
})}
</div>
</div>
);
};
203 changes: 203 additions & 0 deletions components/tools/Filters.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import { useRouter } from 'next/router';
import { useContext, useEffect, useState } from 'react';
import { twMerge } from 'tailwind-merge';

import type { Language, Technology, VisibleDataListType } from '@/types/components/tools/ToolDataType';

import tags from '../../config/all-tags.json';
import ToolFilter, { ToolFilterContext } from '../../context/ToolFilterContext';
import categoryList from '../../scripts/tools/categorylist';
import Data from '../../scripts/tools/tools-schema.json';
import Button from '../buttons/Button';
import ArrowDown from '../icons/ArrowDown';
import { CardData } from './CardData';
import FiltersDisplay from './FiltersDisplay';
import FiltersDropdown from './FiltersDropdown';

interface FiltersProps {
setOpenFilter: React.Dispatch<React.SetStateAction<boolean>>;
}

/**
* @description This component displays Filters.
* @param {FiltersProps} props - Props for Filters component.
* @param {React.Dispatch<React.SetStateAction<boolean>>} props.setOpenFilter - Function to set the state of filter.
*/
export default function Filters({ setOpenFilter }: FiltersProps) {
const router = useRouter();
// all the filter state variables and functions are extracted from the Context to set filters according to the UI.
const { isPaid, isAsyncAPIOwner, languages, technologies, categories } = useContext(ToolFilterContext);

// State variables to operate dropdowns of respective filters
const [openLanguage, setopenLanguage] = useState<boolean>(false);
const [openTechnology, setopenTechnology] = useState<boolean>(false);
const [openCategory, setopenCategory] = useState<boolean>(false);

// Filter state variables for user checked values are created, initialising it with the values already set by user.
const [checkPaid, setCheckPaid] = useState<string>(isPaid);
const [checkedLanguage, setCheckedLanguage] = useState<string[]>(languages);
const [checkedTechnology, setCheckedTechnology] = useState<string[]>(technologies);
const [checkedCategory, setCheckedCategory] = useState<string[]>(categories);
const [checkOwner, setCheckOwner] = useState<boolean>(isAsyncAPIOwner);

// useEffect hook used to update the UI elements
useEffect(() => {
setCheckedLanguage(languages);
setCheckedTechnology(technologies);
setCheckedCategory(categories);
setCheckPaid(isPaid);
setCheckOwner(isAsyncAPIOwner);
}, [languages, technologies, categories, isPaid, isAsyncAPIOwner]);

// contains the list of languages and technologies
const languageList = tags.languages as Language[];
const technologyList = tags.technologies as Technology[];

// For Showing language, technology and category information
const [visible, setVisible] = useState<VisibleDataListType>({
lang: false,
tech: false,
category: false,
pricing: false,
ownership: false
});

// For showing the read more content of Language and Category information
const [readMore, setReadMore] = useState(false);

// function to apply all the filters, which are selected, when `Apply Filters` is clicked.
const handleApplyFilters = () => {
setOpenFilter(false);

const searchParams = new URLSearchParams();

// 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', isAsyncAPIOwner ? 'true' : 'false');
}
if (checkedTechnology.length > 0) {
searchParams.set('techs', checkedTechnology.join(','));
}
if (checkedLanguage.length > 0) {
searchParams.set('langs', checkedLanguage.join(','));
}
if (checkedTechnology.length > 0) {
searchParams.set('techs', checkedTechnology.join(','));
}
if (checkedCategory.length > 0) {
searchParams.set('categories', checkedCategory.join(','));
}

router.push({
pathname: '/tools',
query: searchParams.toString()
}, undefined, { shallow: true });
};

// function to undo all the filters when `Undo Changes` is clicked.
const undoChanges = () => {
setCheckedLanguage(languages);
setCheckedTechnology(technologies);
setCheckedCategory(categories);
setCheckPaid(isPaid);
setCheckOwner(isAsyncAPIOwner);
};

return (
<ToolFilter>
<div className='z-20 rounded-lg border border-gray-300 bg-white py-4 shadow-md' data-testid='Filters-div'>
<div className='mx-4 flex flex-col gap-2'>
<div className='flex items-baseline justify-between gap-2'>
<div className='text-sm text-gray-500'>
<CardData heading='PRICING' data={Data.properties.filters.properties.hasCommercial.description} type='pricing' visible={visible} setVisible={setVisible} read={readMore} setRead={setReadMore} />
</div>
<div className='mb-0 flex cursor-pointer gap-0.5 text-xs hover:underline' onClick={undoChanges}>
Undo Changes
</div>
</div>
<div className='flex gap-2' data-testid='Applied-filters'>
<div className={twMerge(`bg-gray-200 px-4 py-2 flex gap-1 rounded-md hover:bg-secondary-100 border hover:border-secondary-500 cursor-pointer ${checkPaid === 'free' ? 'bg-secondary-100 border-secondary-500' : ''}`)} onClick={() => (checkPaid === 'free' ? setCheckPaid('all') : setCheckPaid('free'))}>
<div className='text-sm'>Open Source</div>
<img src='/img/illustrations/icons/FreeIcon.svg' alt='Free' />
</div>
<div className={`flex cursor-pointer gap-1 rounded-md border bg-gray-200 px-4 py-2 hover:border-secondary-500 hover:bg-secondary-100 ${checkPaid === 'paid' ? 'border-secondary-500 bg-secondary-100' : ''}`} onClick={() => (checkPaid === 'paid' ? setCheckPaid('all') : setCheckPaid('paid'))}>
<div className='text-sm'>Commercial</div>
<img src='/img/illustrations/icons/PaidIcon.svg' alt='Paid' />
</div>
</div>
</div>
<hr className='my-4' />
<div className='mx-4 flex flex-col gap-2'>
<div className='text-left text-sm text-gray-500'>
<CardData heading='OWNERSHIP' data='It describes whether the tools are maintained by AsyncAPI organization or not.' type='ownership' visible={visible} setVisible={setVisible} read={readMore} setRead={setReadMore} />
</div>
<div className='flex gap-4'>
<label className='relative inline-flex cursor-pointer items-center'>
<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'>
Show only AsyncAPI-owned tools
</div>
</div>
</div>
<hr className='my-4' />
<div className='mx-4 flex flex-col gap-2' data-testid='Filters-Language-dropdown'>
<CardData heading='LANGUAGE' data={Data.properties.filters.properties.language.description} type='lang' visible={visible} setVisible={setVisible} read={readMore} setRead={setReadMore} />
<div className='w-full'>
<div className={twMerge(`px-4 py-2 flex justify-between rounded-lg border border-gray-400 w-full bg-gray-200 text-gray-700 shadow text-sm cursor-pointer ${openLanguage ? 'rounded-b-none' : ''}`)} onClick={() => setopenLanguage(!openLanguage)}>
<div className='flex items-center text-dark'>
{/* eslint-disable-next-line no-nested-ternary */}
{checkedLanguage.length > 0 ? (checkedLanguage.length === 1 ? '1 option selected' : `${checkedLanguage.length} options selected`) : 'Select Languages...'}
</div>
<ArrowDown className={`my-auto ${openLanguage ? 'rotate-180' : ''}`} />
</div>
{openLanguage && <div className='w-auto overflow-x-auto rounded-b-lg border border-gray-400 bg-gray-200 duration-150'>
<FiltersDropdown dataList={languageList} checkedOptions={checkedLanguage} setCheckedOptions={setCheckedLanguage} />
</div>}
<FiltersDisplay checkedOptions={checkedLanguage} setCheckedOptions={setCheckedLanguage} />
</div>
</div>
<hr className='my-4' />
<div className='mx-4 flex flex-col gap-2' data-testid='Filters-Technology-dropdown'>
<CardData heading='TECHNOLOGY' data={Data.properties.filters.properties.technology.description} type='tech' visible={visible} setVisible={setVisible} read={readMore} setRead={setReadMore} />
<div className='w-full'>
<div className={twMerge(`px-4 py-2 flex justify-between rounded-lg border border-gray-400 w-full bg-gray-200 text-gray-700 shadow text-sm cursor-pointer ${openTechnology ? 'rounded-b-none' : ''}`)} onClick={() => setopenTechnology(!openTechnology)}>
<div className='flex items-center text-dark'>
{/* eslint-disable-next-line no-nested-ternary */}
{checkedTechnology.length > 0 ? (checkedTechnology.length === 1 ? '1 option selected' : `${checkedTechnology.length} options selected`) : 'Select Technologies...'}
</div>
<ArrowDown className={`my-auto ${openTechnology ? 'rotate-180' : ''}`} />
</div>
{openTechnology && <div className='w-auto overflow-x-auto rounded-b-lg border border-gray-400 bg-gray-200 duration-150'>
<FiltersDropdown dataList={technologyList} checkedOptions={checkedTechnology} setCheckedOptions={setCheckedTechnology} />
</div>}
<FiltersDisplay checkedOptions={checkedTechnology} setCheckedOptions={setCheckedTechnology} />
</div>
</div>
<hr className='my-4' />
<div className='mx-4 flex flex-col gap-2' data-testid='Filters-Category-dropdown'>
<CardData heading='CATEGORY' data={Data.properties.filters.properties.categories.description} type='category' visible={visible} setVisible={setVisible} read={readMore} setRead={setReadMore} />
<div className='w-full'>
<div className={twMerge(`px-4 py-2 flex justify-between rounded-lg border border-gray-400 w-full bg-gray-200 text-gray-700 shadow text-sm cursor-pointer ${openCategory ? 'rounded-b-none' : ''}`)} onClick={() => setopenCategory(!openCategory)}>
<div className='flex items-center text-dark'>
{/* eslint-disable-next-line no-nested-ternary */}
{checkedCategory.length > 0 ? (checkedCategory.length === 1 ? '1 option selected' : `${checkedCategory.length} options selected`) : 'Select Categories...'}
</div>
<ArrowDown className={`my-auto ${openCategory ? 'rotate-180' : ''}`} />
</div>
{openCategory && <div className='w-auto overflow-x-auto rounded-b-lg border border-gray-400 bg-gray-200 duration-150'>
<FiltersDropdown dataList={categoryList} checkedOptions={checkedCategory} setCheckedOptions={setCheckedCategory} />
</div>}
<FiltersDisplay checkedOptions={checkedCategory} setCheckedOptions={setCheckedCategory} />
</div>
</div>
<hr className='my-4' />
<div className='mx-4 my-6 mb-0 w-auto' onClick={handleApplyFilters}>
<Button text='Apply Filters' className='w-full' />
</div>
</div>
</ToolFilter>
);
};
Loading
Loading