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

Update Logic and Migrate SortBy Component from ShadCN to Chakra UI #608

Merged
merged 12 commits into from
Jan 26, 2025
2 changes: 1 addition & 1 deletion frontend/jest.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ dotenv.config()

global.React = React
global.TextEncoder = TextEncoder

global.structuredClone = (val) => JSON.parse(JSON.stringify(val))
beforeEach(() => {
jest.spyOn(console, 'error').mockImplementation((...args) => {
throw new Error(`Console error: ${args.join(' ')}`)
Expand Down
2 changes: 1 addition & 1 deletion frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-gtm-module": "^2.0.11",
"react-icons": "^5.3.0",
"react-icons": "^5.4.0",
"react-leaflet": "^5.0.0",
"react-router-dom": "^7.0.2",
"react-tooltip": "^5.28.0",
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/SearchPageLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ const SearchPageLayout = ({
initialValue={searchQuery}
onReady={handleSearchBarReady}
/>
<div>{sortChildren}</div>
</div>
{!isSearchBarReady || !isLoaded ? (
<div className="mt-20 flex h-64 w-full items-center justify-center">
Expand All @@ -59,6 +58,7 @@ const SearchPageLayout = ({
) : (
<>
<div>
{totalPages !== 0 && <div className="flex justify-end">{sortChildren}</div>}
{totalPages === 0 && <div className="text bg:text-white m-4 text-xl">{empty}</div>}
{children}
</div>
Expand Down
130 changes: 83 additions & 47 deletions frontend/src/components/SortBy.tsx
Original file line number Diff line number Diff line change
@@ -1,56 +1,92 @@
import { faCaretDown, faCaretUp, faCheck } from '@fortawesome/free-solid-svg-icons'
import { faArrowDownShortWide, faArrowUpWideShort } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { useState } from 'react'
import { Tooltip } from 'react-tooltip'
import { SortByProps } from 'types/sortBy'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from 'components/ui/dropdownMenu'

interface SortOption {
value: string
label: string
}

interface SortByProps {
options: SortOption[]
selectedOption: string
onSortChange: (value: string) => void
}

const SortBy = ({ options, selectedOption, onSortChange }: SortByProps) => {
const [open, setOpen] = useState<boolean>(false)
const handleOpenChange = (isOpen: boolean) => {
setOpen(isOpen)
}

if (!options || options.length === 0) return null
SelectContent,
SelectItem,
SelectLabel,
SelectRoot,
SelectTrigger,
SelectValueText,
} from 'components/ui/Select'
const SortBy = ({
sortOptions,
selectedSortOption,
selectedOrder,
onSortChange,
onOrderChange,
}: SortByProps) => {
if (!sortOptions || sortOptions.items.length === 0) return null

return (
<DropdownMenu open={open} onOpenChange={handleOpenChange}>
<DropdownMenuTrigger asChild>
<button
type="button"
className="h-10 w-[100px] justify-between border-2 bg-white text-[#292e36] dark:bg-[#3C3C3C] dark:text-[#D1D5DB]"
<div className="flex items-center gap-4">
{/* Sort Attribute Dropdown */}
<div className="rounded-xl bg-gray-200 px-2 shadow-sm dark:bg-[#323232]">
<SelectRoot
collection={sortOptions}
size="sm"
onValueChange={(e) => {
onSortChange(e.value[0])
}}
>
<span>Sort By </span>
<FontAwesomeIcon icon={open ? faCaretUp : faCaretDown} className="pl-2" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-[180px] bg-white text-[#4B5563] dark:bg-[#3C3C3C] dark:text-[#D1D5DB]">
{options.map((option) => (
<DropdownMenuItem
key={option.value}
onSelect={() => onSortChange(option.value)}
className="justify-between"
<div className="flex items-center gap-2">
<SelectLabel className="font-small text-sm text-gray-600 dark:text-gray-300">
Sort By:
</SelectLabel>
<SelectTrigger className="width-auto text-sm">
<SelectValueText
paddingRight={'1.4rem'}
width={'auto'}
textWrap="nowrap"
placeholder={
sortOptions.items.find((item) => item.value === selectedSortOption)?.label
}
/>
</SelectTrigger>
</div>
<SelectContent className="text-md text-md min-w-36 dark:bg-[#323232]">
{sortOptions.items.map((attribute) => (
<SelectItem
item={attribute}
key={attribute.value}
className="p-1 hover:bg-[#D1DBE6] dark:hover:bg-[#454545]"
>
{attribute.label}
</SelectItem>
))}
</SelectContent>
</SelectRoot>
</div>

{/* Sort Order Dropdown */}
{selectedSortOption !== 'default' && (
<div className="relative flex items-center">
<button
data-tooltip-id="sort-order-tooltip"
data-tooltip-content={selectedOrder === 'asc' ? 'Ascending Order' : 'Descending Order'}
onClick={() => onOrderChange(selectedOrder === 'asc' ? 'desc' : 'asc')}
className="flex items-center justify-center rounded-lg bg-gray-200 p-2 shadow-sm hover:bg-gray-300 dark:bg-[#323232] dark:text-gray-300 dark:hover:bg-[#454545]"
>
{option.label}
{option.value === selectedOption && <FontAwesomeIcon icon={faCheck} />}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
{selectedOrder === 'asc' ? (
<FontAwesomeIcon
icon={faArrowDownShortWide}
className="h-5 w-5 text-gray-600 dark:text-gray-200"
/>
) : (
<FontAwesomeIcon
icon={faArrowUpWideShort}
className="h-5 w-5 text-gray-600 dark:text-gray-200"
/>
)}
</button>
<Tooltip
id="sort-order-tooltip"
className="rounded-lg bg-white px-1 py-0 text-sm text-gray-600 shadow-md"
/>
</div>
)}
</div>
)
}

Expand Down
16 changes: 16 additions & 0 deletions frontend/src/components/ui/CloseButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { ButtonProps } from '@chakra-ui/react'
import { IconButton as ChakraIconButton } from '@chakra-ui/react'
import * as React from 'react'
import { LuX } from 'react-icons/lu'

export type CloseButtonProps = ButtonProps

export const CloseButton = React.forwardRef<HTMLButtonElement, CloseButtonProps>(
function CloseButton(props, ref) {
return (
<ChakraIconButton variant="ghost" aria-label="Close" ref={ref} {...props}>
{props.children ?? <LuX />}
</ChakraIconButton>
)
}
)
134 changes: 134 additions & 0 deletions frontend/src/components/ui/Select.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
'use client'

import type { CollectionItem } from '@chakra-ui/react'
import { Select as ChakraSelect, Portal } from '@chakra-ui/react'
import * as React from 'react'
import { CloseButton } from './CloseButton'

interface SelectTriggerProps extends ChakraSelect.ControlProps {
clearable?: boolean
}

export const SelectTrigger = React.forwardRef<HTMLButtonElement, SelectTriggerProps>(
function SelectTrigger(props, ref) {
const { children, clearable, ...rest } = props
return (
<ChakraSelect.Control {...rest}>
<ChakraSelect.Trigger ref={ref}>{children}</ChakraSelect.Trigger>
<ChakraSelect.IndicatorGroup>
{clearable && <SelectClearTrigger />}
<ChakraSelect.Indicator />
</ChakraSelect.IndicatorGroup>
</ChakraSelect.Control>
)
}
)

const SelectClearTrigger = React.forwardRef<HTMLButtonElement, ChakraSelect.ClearTriggerProps>(
function SelectClearTrigger(props, ref) {
return (
<ChakraSelect.ClearTrigger asChild {...props} ref={ref}>
<CloseButton
size="xs"
variant="plain"
focusVisibleRing="inside"
focusRingWidth="2px"
pointerEvents="auto"
/>
</ChakraSelect.ClearTrigger>
)
}
)

interface SelectContentProps extends ChakraSelect.ContentProps {
portalled?: boolean
portalRef?: React.RefObject<HTMLElement>
}

export const SelectContent = React.forwardRef<HTMLDivElement, SelectContentProps>(
function SelectContent(props, ref) {
const { portalled = true, portalRef, ...rest } = props
return (
<Portal disabled={!portalled} container={portalRef}>
<ChakraSelect.Positioner>
<ChakraSelect.Content {...rest} ref={ref} />
</ChakraSelect.Positioner>
</Portal>
)
}
)

export const SelectItem = React.forwardRef<HTMLDivElement, ChakraSelect.ItemProps>(
function SelectItem(props, ref) {
const { item, children, ...rest } = props
return (
<ChakraSelect.Item key={item.value} item={item} {...rest} ref={ref}>
{children}
<ChakraSelect.ItemIndicator />
</ChakraSelect.Item>
)
}
)

interface SelectValueTextProps extends Omit<ChakraSelect.ValueTextProps, 'children'> {
children?(items: CollectionItem[]): React.ReactNode
}

export const SelectValueText = React.forwardRef<HTMLSpanElement, SelectValueTextProps>(
function SelectValueText(props, ref) {
const { children, ...rest } = props
return (
<ChakraSelect.ValueText {...rest} ref={ref}>
<ChakraSelect.Context>
{(select) => {
const items = select.selectedItems
if (items.length === 0) return props.placeholder
if (children) return children(items)
if (items.length === 1) return select.collection.stringifyItem(items[0])
return `${items.length} selected`
}}
</ChakraSelect.Context>
</ChakraSelect.ValueText>
)
}
)

export const SelectRoot = React.forwardRef<HTMLDivElement, ChakraSelect.RootProps>(
function SelectRoot(props, ref) {
return (
<ChakraSelect.Root
{...props}
ref={ref}
positioning={{ sameWidth: true, ...props.positioning }}
>
{props.asChild ? (
props.children
) : (
<>
<ChakraSelect.HiddenSelect />
{props.children}
</>
)}
</ChakraSelect.Root>
)
}
) as ChakraSelect.RootComponent

interface SelectItemGroupProps extends ChakraSelect.ItemGroupProps {
label: React.ReactNode
}

export const SelectItemGroup = React.forwardRef<HTMLDivElement, SelectItemGroupProps>(
function SelectItemGroup(props, ref) {
const { children, label, ...rest } = props
return (
<ChakraSelect.ItemGroup {...rest} ref={ref}>
<ChakraSelect.ItemGroupLabel>{label}</ChakraSelect.ItemGroupLabel>
{children}
</ChakraSelect.ItemGroup>
)
}
)

export const SelectLabel = ChakraSelect.Label
export const SelectItemText = ChakraSelect.ItemText
Loading
Loading