diff --git a/components/ClickableLogo.tsx b/components/ClickableLogo.tsx index b1bca16a77e..0ce7f081e4f 100644 --- a/components/ClickableLogo.tsx +++ b/components/ClickableLogo.tsx @@ -3,7 +3,7 @@ import Link from 'next/link'; import AsyncAPILogo from './AsyncAPILogo'; interface IClickableLogoProps { - href: string; + href?: string; className?: string; logoClassName?: string; } @@ -16,10 +16,8 @@ interface IClickableLogoProps { */ export default function ClickableLogo({ href = '/', className = 'flex', logoClassName }: IClickableLogoProps) { return ( - - - - + + ); } diff --git a/components/data/buckets.ts b/components/data/buckets.ts index c868b27c4fd..b0de1fba1c7 100644 --- a/components/data/buckets.ts +++ b/components/data/buckets.ts @@ -1,5 +1,3 @@ -import type { IconType } from '@/types/components/IconType'; - import IconGettingStarted from '../icons/GettingStarted'; import IconGuide from '../icons/Guide'; import IconMigration from '../icons/Migration'; @@ -8,17 +6,18 @@ import IconTutorials from '../icons/Tutorials'; import IconUseCases from '../icons/UseCases'; import IconUsers from '../icons/Users'; -interface BucketType { +export interface Bucket { name: string; title: string; description: string; link: string; className: string; borderClassName: string; - Icon: IconType; + href: string; + icon: React.ComponentType; } -export const buckets: BucketType[] = [ +export const buckets: Bucket[] = [ { name: 'concepts', title: 'Concepts', @@ -26,7 +25,8 @@ export const buckets: BucketType[] = [ link: '/docs/concepts', className: 'bg-secondary-200', borderClassName: 'border-secondary-200', - Icon: IconGettingStarted + href: '/docs/concepts', + icon: IconGettingStarted }, { name: 'tutorials', @@ -35,7 +35,8 @@ export const buckets: BucketType[] = [ link: '/docs/tutorials', className: 'bg-pink-100', borderClassName: 'border-pink-100', - Icon: IconTutorials + href: '/docs/tutorials', + icon: IconTutorials }, { name: 'guides', @@ -44,7 +45,8 @@ export const buckets: BucketType[] = [ link: '/docs/guides', className: 'bg-primary-200', borderClassName: 'border-primary-200', - Icon: IconGuide + href: '/docs/guides', + icon: IconGuide }, { name: 'tools', @@ -53,7 +55,8 @@ export const buckets: BucketType[] = [ link: '/docs/tools', className: 'bg-green-200', borderClassName: 'border-green-200', - Icon: IconUseCases + href: '/docs/tools', + icon: IconUseCases }, { name: 'reference', @@ -62,7 +65,8 @@ export const buckets: BucketType[] = [ link: '/docs/reference', className: 'bg-yellow-200', borderClassName: 'border-yellow-200', - Icon: IconSpec + href: '/docs/reference', + icon: IconSpec }, { name: 'migration', @@ -71,7 +75,8 @@ export const buckets: BucketType[] = [ link: '/docs/migration', className: 'bg-blue-400', borderClassName: 'border-blue-400', - Icon: IconMigration + href: '/docs/migration', + icon: IconMigration }, { name: 'community', @@ -80,13 +85,13 @@ export const buckets: BucketType[] = [ link: '/docs/community', className: 'bg-orange-200', borderClassName: 'border-orange-200', - Icon: IconUsers + href: '/docs/community', + icon: IconUsers } ].map((bucket) => { - // we need such a mapping for some parts of website, e.g navigation blocks use the `icon` property, not `Icon` etc. return { ...bucket, href: bucket.link, - icon: bucket.Icon + icon: bucket.icon }; }); diff --git a/components/docs/Card.tsx b/components/docs/Card.tsx index 94e737d0b90..0743b88fc23 100644 --- a/components/docs/Card.tsx +++ b/components/docs/Card.tsx @@ -1,6 +1,5 @@ import Link from 'next/link'; -import type { IconType } from '@/types/components/IconType'; import { HeadingLevel, HeadingTypeStyle } from '@/types/typography/Heading'; import { ParagraphTypeStyle } from '@/types/typography/Paragraph'; @@ -12,7 +11,7 @@ interface CardProps { description: string; link: string; className: string; - Icon: IconType; + Icon: React.ComponentType; } /** diff --git a/components/docs/DocsCards.tsx b/components/docs/DocsCards.tsx index 9c3834287ba..fabb77b87dc 100644 --- a/components/docs/DocsCards.tsx +++ b/components/docs/DocsCards.tsx @@ -9,8 +9,8 @@ import Card from './Card'; export default function DocsCards() { return (
- {buckets.map((card) => ( - + {buckets.map(({ title, description, link, className, icon }) => ( + ))}
); diff --git a/components/form/Select.tsx b/components/form/Select.tsx new file mode 100644 index 00000000000..05cd2d9a71e --- /dev/null +++ b/components/form/Select.tsx @@ -0,0 +1,45 @@ +import type { ChangeEvent } from 'react'; +import React from 'react'; +import { twMerge } from 'tailwind-merge'; + +export interface Option { + value: string; + text: string; +} + +export interface SelectProps { + className?: string; + onChange?: (selected: string) => void; + options: Option[]; + selected?: string; +} + +/** + * @description Select component for form dropdown. + * @param {string} [props.className=''] - Additional CSS classes for the select element. + * @param {(value: string) => void} [props.onChange=() => {}] - Function to handle onChange event. + * @param {Option[]} props.options - Array of options for the select dropdown. + * @param {string} props.selected - Value of the currently selected option. + */ +export default function Select({ className = '', onChange = () => {}, options, selected }: SelectProps) { + const handleOnChange = (event: ChangeEvent) => { + onChange(event.target.value); + }; + + return ( + + ); +} diff --git a/components/helpers/applyFilter.ts b/components/helpers/applyFilter.ts new file mode 100644 index 00000000000..eba66b522f0 --- /dev/null +++ b/components/helpers/applyFilter.ts @@ -0,0 +1,160 @@ +interface DataObject { + name: string; + [key: string]: any; +} + +interface FilterCriteria { + name: string; +} + +interface Filter { + [key: string]: string; + value: string; +} + +interface FilterOption { + value: string; + text: string; +} + +/** + * @description Sorts an array of objects based on a string property called 'value'. + * @param {{ value: string }[]} arr - Array of objects with a 'value' property of type string. + */ +export function sortFilter(arr: { value: string }[]): { value: string }[] { + return arr.sort((a, b) => { + if (a.value < b.value) { + return -1; + } + if (a.value > b.value) { + return 1; + } + + return 0; + }); +} + +/** + * @description Apply filters to data and update the filters. + * @param {FilterCriteria[]} checks - Array of filter criteria objects. + * @param {DataObject[]} data - Array of data objects to filter. + * @param {(lists: { [key: string]: FilterOption[] }) => void} setFilters - Function to update the filters. + */ +export const applyFilterList = ( + checks: FilterCriteria[], + data: DataObject[], + setFilters: (lists: { [key: string]: FilterOption[] }) => void +): void => { + if (Object.keys(checks).length) { + const lists: { [key: string]: FilterOption[] } = {}; + + checks.forEach((check) => { + lists[check.name] = []; + }); + for (let i = 0; i < data.length; i++) { + const res = data[i]; + + Object.keys(lists).forEach((key) => { + const result = data[i][key]; + + if (res) { + if (lists[key].length) { + if (Array.isArray(result)) { + result.forEach((a) => { + if (a.name) { + if (!lists[key].some((e) => e.value === a.name)) { + const newData = { + value: a.name, + text: a.name + }; + + lists[key].push(newData); + sortFilter(lists[key]); + } + } else if (!lists[key].some((e) => e.value === a)) { + const newData = { + value: a, + text: a + }; + + lists[key].push(newData); + sortFilter(lists[key]); + } + }); + } else if (!lists[key].some((e) => e.value === result)) { + const newData = { + value: result, + text: result + }; + + lists[key].push(newData); + sortFilter(lists[key]); + } + } else if (Array.isArray(result)) { + result.forEach((e) => { + if (e.name) { + const newData = { + value: e.name, + text: e.name + }; + + lists[key].push(newData); + } else { + const newData = { + value: e, + text: e + }; + + lists[key].push(newData); + } + }); + } else { + const newData = { + value: result, + text: result + }; + + lists[key].push(newData); + } + } + }); + } + setFilters(lists); + } +}; + +/** + * @description Apply filters to data and trigger the filter action. + * @param {DataObject[]} inputData - Array of data objects to filter. + * @param {(result: DataObject[], query: Filter) => void} onFilter - Function to apply the filter action. + * @param {Filter} query - Filter criteria. + */ +export const onFilterApply = ( + inputData: DataObject[], + onFilter: (result: DataObject[], query: Filter) => void, + query: Filter +): void => { + let result = inputData; + + if (query && Object.keys(query).length >= 1) { + Object.keys(query).forEach((property) => { + const res = result.filter((e) => { + if (!query[property] || e[property] === query[property]) { + return e[property]; + } + if (Array.isArray(e[property])) { + return ( + e[property].some((data: any) => data.name === query[property]) || + e[property].includes(query[property]) || + false + ); + } + + return false; // Fixing missing return value issue + }); + + result = res; + }); + } + onFilter(result, query); +}; diff --git a/components/helpers/is-mobile.ts b/components/helpers/is-mobile.ts new file mode 100644 index 00000000000..dbfa7b63cca --- /dev/null +++ b/components/helpers/is-mobile.ts @@ -0,0 +1,15 @@ +let isMobile: boolean | undefined; + +/** + * @description Checks whether the current device is a mobile device. + */ +export function isMobileDevice(): boolean { + if (typeof navigator === 'undefined') return false; + if (typeof isMobile === 'boolean') return isMobile; + + const regexp = /android|iphone|kindle|ipad/i; + + isMobile = regexp.test(navigator.userAgent); + + return isMobile; +} diff --git a/components/helpers/use-outside-click.tsx b/components/helpers/use-outside-click.tsx new file mode 100644 index 00000000000..e3fcb91a172 --- /dev/null +++ b/components/helpers/use-outside-click.tsx @@ -0,0 +1,33 @@ +import type { RefObject } from 'react'; +import { useEffect, useRef } from 'react'; + +/** + * @description Hook to detect clicks outside a specified element. + * @param {function} callback - The callback function to be called when a click outside the element is detected. + */ +export function useOutsideClick(callback: (e: MouseEvent) => void): RefObject { + const callbackRef = useRef<((e: MouseEvent) => void) | null>(null); + const innerRef = useRef(null); + + useEffect(() => { + callbackRef.current = callback; + }, [callback]); + + useEffect(() => { + /** + * @description Handles the click event outside the specified element. + * @param {MouseEvent} e - The click event. + */ + function handleClick(e: MouseEvent) { + if (innerRef.current && callbackRef.current && !innerRef.current.contains(e.target as Node)) { + callbackRef.current(e); + } + } + + document.addEventListener('click', handleClick); + + return () => document.removeEventListener('click', handleClick); + }, [innerRef]); + + return innerRef; +} diff --git a/components/icons/SearchIcon.tsx b/components/icons/SearchIcon.tsx new file mode 100644 index 00000000000..7f2ab5c4b47 --- /dev/null +++ b/components/icons/SearchIcon.tsx @@ -0,0 +1,22 @@ +/** + * @description Icon for search button + * @param {string} props.className - The class name for styling the icon. + */ +export default function SearchIcon({ className = '' }) { + return ( + + ); +} diff --git a/components/languageSelector/LanguageSelect.tsx b/components/languageSelector/LanguageSelect.tsx new file mode 100644 index 00000000000..4641a7909d5 --- /dev/null +++ b/components/languageSelector/LanguageSelect.tsx @@ -0,0 +1,29 @@ +import { twMerge } from 'tailwind-merge'; + +import type { SelectProps } from '../form/Select'; + +/** + * @description LanguageSelect component for selecting a language. + * @param {string} [props.className=''] - Additional classes for styling. + * @param {Function} [props.onChange=()=>{}] - The callback function invoked when the selection changes. + * @param {Array} [props.options=[]] - An array of options for the select dropdown. + * @param {string} props.selected - The currently selected option value. + */ +export default function LanguageSelect({ className = '', onChange = () => {}, options = [], selected }: SelectProps) { + return ( + + ); +} diff --git a/components/link.tsx b/components/link.tsx new file mode 100644 index 00000000000..d415f090ab2 --- /dev/null +++ b/components/link.tsx @@ -0,0 +1,98 @@ +import Link from 'next/link'; +import { useRouter } from 'next/router'; + +import { defaultLanguage, languages } from '../utils/i18n'; +import i18nPaths from '../utils/i18nPaths'; + +interface LinkComponentProps { + children: React.ReactNode; + locale?: string; + href?: string; + legacyBehavior?: boolean; + target?: string; + rel?: string; +} + +/** + * @description Custom Link component for handling internationalization (i18n). + * @param {Object} props - Props for the Link component. + * @param {React.ReactNode} props.children - The content to render within the Link. + * @param {string} [props.locale] - The locale for the link. + * @param {string} [props.href] - The URL the link points to. + * @param {boolean} [props.legacyBehavior=false] - Whether to use the legacy behavior for the link. + */ +export default function LinkComponent({ + children, + locale, + legacyBehavior = false, + target = '_self', + rel = '', + ...props +}: LinkComponentProps) { + const router = useRouter(); + + // If there is no router available (e.g., during server-side rendering & cypress tests), render a standard Link + if (!router) { + return ( + + {children} + + ); + } + + const { pathname, query, asPath } = router; + + // Detect current language based on the path or query parameter + const slug = asPath.split('/')[1]; + const langSlug = languages.includes(slug) && slug; + const language: string = query.lang && typeof query.lang === 'string' ? query.lang : langSlug || defaultLanguage; // Ensure language is always a string + + let href = props.href || pathname; + + /* + If explicit href is provided, and the language-specific paths for the current language do not include the href, or if the href starts with "http", render a standard Link + */ + if ((props.href && i18nPaths[language] && !i18nPaths[language].includes(href)) || href.includes('http', 0)) { + return ( + + {children} + + ); + } + // If a locale is provided, update the href with the locale + if (locale) { + if (props.href) { + href = `/${locale}${href}`; + } else { + // If the current path starts with "/404", update href to be the root path with the locale + // Otherwise, replace "[lang]" placeholder with the locale + href = pathname.startsWith('/404') ? `/${locale}` : pathname.replace('[lang]', locale); + } + } else { + // If no locale is provided, update the href with the current language or keep it as is + href = language ? `/${language}${href}` : `/${href}`; + } + + // Fix double slashes + href = href.replace(/([^:/]|^)\/{2,}/g, '$1/'); + + return ( + + {children} + + ); +} + +export const LinkText = ({ + href, + children, + legacyBehavior = false, + target = '_self', + rel = '' +}: LinkComponentProps) => { + return ( + + {children} + + ); +}; diff --git a/components/navigation/BlogPostItem.tsx b/components/navigation/BlogPostItem.tsx index 4f172108980..13c2f6fe29d 100644 --- a/components/navigation/BlogPostItem.tsx +++ b/components/navigation/BlogPostItem.tsx @@ -52,8 +52,8 @@ export default forwardRef(function BlogPostItem( return (
  • diff --git a/components/navigation/CommunityPanel.tsx b/components/navigation/CommunityPanel.tsx new file mode 100644 index 00000000000..82306db1284 --- /dev/null +++ b/components/navigation/CommunityPanel.tsx @@ -0,0 +1,9 @@ +import communityItems from './communityItems'; +import FlyoutMenu from './FlyoutMenu'; + +/** + * @description Component representing the community panel. + */ +export default function CommunityPanel() { + return ; +} diff --git a/components/navigation/DocsMobileMenu.tsx b/components/navigation/DocsMobileMenu.tsx new file mode 100644 index 00000000000..aa3c716126a --- /dev/null +++ b/components/navigation/DocsMobileMenu.tsx @@ -0,0 +1,70 @@ +import React from 'react'; + +import type { IDoc } from '@/types/post'; + +import { DOCS_INDEX_NAME, SearchButton } from '../AlgoliaSearch'; +import ClickableLogo from '../ClickableLogo'; +import IconLoupe from '../icons/Loupe'; +import DocsNav from './DocsNav'; + +export interface DocsMobileMenuProps { + post: IDoc; + navigation: { + [key: string]: any; + }; + onClickClose?: () => void; +} + +/** + * @description Component representing the mobile menu for documentation. + * @param {Object} props - Props for the DocsMobileMenu component. + * @param {DocsMobileMenuProps} props.post - The post data. + * @param {Object} props.navigation - The navigation data. + * @param {Function} [props.onClickClose] - The function to handle closing the mobile menu. + */ +export default function DocsMobileMenu({ post, navigation, onClickClose = () => {} }: DocsMobileMenuProps) { + return ( +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    + + + Search docs... + +
    + +
    +
    +
    {/* Force sidebar to shrink to fit close icon */}
    +
    +
    + ); +} diff --git a/components/navigation/DocsNav.tsx b/components/navigation/DocsNav.tsx new file mode 100644 index 00000000000..d0ee433b573 --- /dev/null +++ b/components/navigation/DocsNav.tsx @@ -0,0 +1,116 @@ +import { useEffect, useState } from 'react'; + +import { buckets } from '../data/buckets'; +import DocsArrow from '../icons/DocsArrow'; +import IconHome from '../icons/Home'; +import DocsNavItem from './DocsNavItem'; +import SubCategoryDocsNav from './SubCategoryDocsNav'; + +export interface Bucket { + name?: string; + title?: string; + description?: string; + link?: string; + className?: string; + borderClassName?: string; + href?: string; + icon?: React.ComponentType | null; +} + +export interface SerializedBuckets { + [key: string]: Bucket; +} + +export interface DocsNavProps { + item: { + children: { + [key: string]: any; + }; + item: { + rootSectionId: string; + slug: string; + title: string; + }; + }; + active: string; + onClick?: () => void; +} + +const serializedBuckets: SerializedBuckets = buckets.reduce( + (acc, bucket) => { + // Create a new entry in the accumulator for the current bucket's name (or an empty string if missing) + acc[bucket.name || ''] = { + // Spread the existing properties of the bucket object into the new entry + ...bucket, + // Combine the existing className and borderClassName properties into a single className property + className: `${bucket.className || ''} ${bucket.borderClassName || ''}` + }; + + return acc; // Return the updated accumulator for the next iteration + }, + // Define an initial accumulator object with a pre-defined entry for "welcome" bucket + { + welcome: { + icon: IconHome, + className: 'bg-gray-300 border-gray-300' + } + } as SerializedBuckets // Initial value of the accumulator +); + +/** + * @description Component representing a navigation item in the documentation sidebar. + * @param {Object} props - The props for the DocsNav component. + * @param {DocsNavProps} props.item - The navigation item. + * @param {string} props.active - The currently active navigation item. + * @param {Function} [props.onClick=() => {}] - The function to be called when the navigation item is clicked. + */ +export default function DocsNav({ item, active, onClick = () => {} }: DocsNavProps) { + const subCategories = item.children; + const bucket = serializedBuckets[item.item.rootSectionId]; + const [openSubCategory, setOpenSubCategory] = useState(active.startsWith(item.item.slug)); + + const onClickHandler = () => { + setOpenSubCategory(!openSubCategory); + onClick(); + }; + + useEffect(() => { + setOpenSubCategory(active.startsWith(item.item.slug)); + }, [active]); + + return ( +
  • +
    + 0} + activeDropDownItem={openSubCategory} + onClick={() => setOpenSubCategory(!openSubCategory)} + /> + +
    + {openSubCategory && ( +
      + {Object.values(subCategories).map((subCategory: any) => ( + + ))} +
    + )} +
  • + ); +} diff --git a/components/navigation/DocsNavItem.tsx b/components/navigation/DocsNavItem.tsx new file mode 100644 index 00000000000..43d4af17a6f --- /dev/null +++ b/components/navigation/DocsNavItem.tsx @@ -0,0 +1,84 @@ +import Link from 'next/link'; + +export interface DocsNavItemProps { + title: string; + slug: string; + href?: string; + activeSlug: string; + sectionSlug?: string; + onClick?: () => void; + defaultClassName?: string; + inactiveClassName?: string; + activeClassName?: string; + bucket?: { + className: string; + icon: React.ComponentType; + }; +} + +/** + * @description Determines if a given slug is active. + * @param {string} slug - The slug of the item. + * @param {string} activeSlug - The active slug. + * @param {string | undefined} sectionSlug - The slug of the section. + */ +function isActiveSlug(slug: string, activeSlug: string, sectionSlug?: string): boolean { + if (slug === '/docs' || (sectionSlug !== undefined && slug === sectionSlug)) { + return slug === activeSlug; + } + + const partialSlug = slug.split('/'); + const partialActiveSlug = activeSlug.split('/'); + const activeParts = partialActiveSlug.filter((a, idx) => a === partialSlug[idx]); + + return activeParts.length === partialSlug.length; +} + +/** + * @description Component representing an item in the documentation navigation. + * @param {string} props.title - The title of the navigation item. + * @param {string} props.slug - The slug representing the item. + * @param {string} [props.href] - The href for the link. + * @param {string} props.activeSlug - The active slug. + * @param {string} [props.sectionSlug] - The slug of the section. + * @param {() => void} [props.onClick] - Function to call when the item is clicked. + * @param {string} [props.defaultClassName] - Default class name for the item. + * @param {string} [props.inactiveClassName] - Class name when the item is inactive. + * @param {string} [props.activeClassName] - Class name when the item is active. + * @param {object} [props.bucket] - Optional bucket configuration. + * @param {string} props.bucket.className - Class name for the bucket. + * @param {React.ComponentType} props.bucket.icon - Icon component for the bucket. + */ +export default function DocsNavItem({ + title, + slug, + href, + activeSlug, + sectionSlug, + onClick = () => {}, + defaultClassName = '', + inactiveClassName = '', + activeClassName = '', + bucket +}: DocsNavItemProps) { + const isActive = isActiveSlug(slug, activeSlug, sectionSlug); + const classes = `${isActive ? activeClassName : inactiveClassName} ${defaultClassName} inline-block w-full`; + + return ( +
    +
    + + {bucket && ( +
    + +
    + )} + {title} + +
    +
    + ); +} diff --git a/components/navigation/EventFilter.tsx b/components/navigation/EventFilter.tsx new file mode 100644 index 00000000000..f48781808a9 --- /dev/null +++ b/components/navigation/EventFilter.tsx @@ -0,0 +1,77 @@ +import moment from 'moment'; +import React, { useEffect, useState } from 'react'; + +import { getEvents } from '../../utils/staticHelpers'; + +enum ActiveState { + All = 'All', + Upcoming = 'Upcoming', + Recorded = 'Recorded' +} + +interface Event { + date: moment.Moment; +} + +interface EventFilterProps { + data: Event[]; + setData: React.Dispatch>; +} + +/** + * @description A component for filtering events based on date. + * @param {Object} props - The props for the EventFilter component. + * @param {Event[]} props.data - The array of events to filter. + * @param {React.Dispatch>} props.setData - The function to update the filtered events. + */ +export default function EventFilter({ data, setData }: EventFilterProps) { + const localTime = moment().format('YYYY-MM-DD'); + const currentDate = `${localTime}T00:00:00.000Z`; + const filterList: string[] = ['All', 'Upcoming', 'Recorded']; + const [active, setActive] = useState('All'); + + useEffect(() => { + switch (active) { + case ActiveState.All: + setData(getEvents(data)); + break; + case ActiveState.Upcoming: + setData( + getEvents(data).filter((a: Event) => { + return a.date.format() > currentDate; + }) + ); + break; + case ActiveState.Recorded: + setData( + getEvents(data).filter((a: Event) => { + return a.date.format() < currentDate; + }) + ); + break; + default: + setData(getEvents(data)); + break; + } + }, [active, data, setData, currentDate]); + + return ( +
    + {filterList.map((list) => ( +
    setActive(list)} + > + {list} +
    + ))} +
    + ); +} diff --git a/components/navigation/EventPostItem.tsx b/components/navigation/EventPostItem.tsx new file mode 100644 index 00000000000..31fd1c48111 --- /dev/null +++ b/components/navigation/EventPostItem.tsx @@ -0,0 +1,100 @@ +import { ArrowRightIcon } from '@heroicons/react/outline'; +import moment from 'moment'; +import React from 'react'; + +import { HeadingLevel, HeadingTypeStyle } from '@/types/typography/Heading'; + +import IconCalendar from '../icons/Calendar'; +import Community from '../icons/Community'; +import Conference from '../icons/Conference'; +import Webinar from '../icons/Webinar'; +import Heading from '../typography/Heading'; + +interface Event { + title: string; + url: string; + banner?: string; + date: string; +} + +interface EventPostItemProps { + post: Event; + className?: string; + id: string; +} + +/** + * @description Component representing an event post item. + * @param {EventPostItemProps} props - The props for the EventPostItem component. + * @param {Event} post - The event post object. + * @param {string} [className] - The optional CSS class name. + * + */ +function EventPostItem({ post, className = '', id }: EventPostItemProps): JSX.Element { + const localTime = moment().format('YYYY-MM-DD'); // store localTime + const currentDate = `${localTime}T00:00:00.000Z`; + const title = post.title || ''; + let color = ''; + let icon: React.ReactElement | null = null; + let type = ''; + + if (title.includes('community')) { + icon = ; + color = 'text-green-800'; + type = 'COMMUNITY'; + } else if (title.includes('conference')) { + icon = ; + color = 'text-orange-800'; + type = 'CONFERENCE'; + } else if (title.includes('workshop')) { + icon = ; + color = 'text-blue-400'; + type = 'WORKSHOP'; + } + + const defaultCover = '/img/homepage/confBlurBg.webp'; + let active = true; + const postDate = moment(post.date); // Convert post.date to a moment object if necessary + + if (!postDate.isValid()) { + // Handle invalid date if necessary + active = false; + } else if (currentDate > postDate.format()) { + active = false; + } + + return ( +
  • + +
  • + ); +} + +export default EventPostItem; diff --git a/components/navigation/Filter.tsx b/components/navigation/Filter.tsx new file mode 100644 index 00000000000..2ef539ffe41 --- /dev/null +++ b/components/navigation/Filter.tsx @@ -0,0 +1,93 @@ +import type { NextRouter } from 'next/router'; +import { useRouter } from 'next/router'; +import { useEffect, useState } from 'react'; + +import Select from '../form/Select'; +import { applyFilterList, onFilterApply } from '../helpers/applyFilter'; + +export interface Option { + value: string; + text: string; +} + +export interface FilterProps { + data: any[]; + onFilter: (data: any[]) => void; + checks: { name: string; options?: Option[] }[]; + className: string; +} + +/** + * @description Component representing a filter for data. + * @param {Object} props - The props for the Filter component. + * @param {Object[]} props.data - The data to be filtered. + * @param {(data: Object[]) => void} props.onFilter - The callback function to handle filtering. + * @param {Object[]} props.checks - The list of filter options. + * @param {string} [props.className] - Additional CSS classes for styling. + */ +export default function Filter({ data, onFilter, checks, className }: FilterProps) { + const router: NextRouter = useRouter(); + const [filters, setFilters] = useState<{ [key: string]: Option[] }>({}); + const [query, setQuery] = useState<{ [key: string]: string }>({}); + + // Set initial query and filter options when router changes + useEffect(() => { + setQuery(router.query as { [key: string]: string }); + applyFilterList(checks, data, setFilters); + }, [router]); + + // Apply filter when query or data changes + useEffect(() => { + const filterWithValue = { value: JSON.stringify(query), ...query }; + + onFilterApply(data, onFilter, filterWithValue); + }, [query, data, onFilter]); + + return ( + <> + {checks.map((check) => { + let selected = ''; + + if (Object.keys(query).length) { + if (query[check.name]) { + selected = `${query[check.name]}`; + } + } + + const selectOptions: Option[] = [ + { + value: '', + text: `Filter by ${check.name}...` + }, + ...(filters[check.name] || []) + ]; + + return ( +