-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
a5a4bd3
commit 380d0fd
Showing
14 changed files
with
675 additions
and
292 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
import { IAPIName } from '@/interfaces/api_connection'; | ||
import APIHandler from '@/lib/utils/api_handler'; | ||
import React, { useState, useEffect, useCallback } from 'react'; | ||
import DatePicker, { DatePickerType } from '@/components/date_picker/date_picker'; | ||
import { IDatePeriod } from '@/interfaces/date_period'; | ||
import SelectFilter from '@/components/filter_section/select_filter'; | ||
import SearchInput from '@/components/filter_section/search_input'; | ||
import ViewToggle from '@/components/filter_section/view_toggle'; | ||
import Image from 'next/image'; | ||
import { generateRandomCertificates, ICertificate } from '@/interfaces/certificate'; | ||
|
||
interface FilterSectionProps { | ||
apiName: IAPIName; | ||
params?: Record<string, string | number | boolean>; | ||
types?: string[]; | ||
statuses?: string[]; | ||
sortingOptions?: string[]; | ||
sortingByDate?: boolean; | ||
onApiResponse?: (data: ICertificate[]) => void; // Info: (20240919 - tzuhan) 回傳 API 回應資料 | ||
viewType: 'grid' | 'list'; | ||
viewToggleHandler: (viewType: 'grid' | 'list') => void; | ||
} | ||
|
||
const FilterSection: React.FC<FilterSectionProps> = ({ | ||
apiName, | ||
params, | ||
types = [], | ||
statuses = [], | ||
sortingOptions = [], | ||
sortingByDate = false, | ||
onApiResponse, | ||
viewType, | ||
viewToggleHandler, | ||
}) => { | ||
const [selectedType, setSelectedType] = useState<string | undefined>( | ||
types.length > 0 ? types[0] : undefined | ||
); | ||
const [selectedStatus, setSelectedStatus] = useState<string | undefined>( | ||
statuses.length > 0 ? statuses[0] : undefined | ||
); | ||
const [selectedDateRange, setSelectedDateRange] = useState<IDatePeriod>({ | ||
startTimeStamp: 0, | ||
endTimeStamp: 0, | ||
}); | ||
const [searchQuery, setSearchQuery] = useState<string | undefined>(); | ||
const [selectedSorting, setSelectedSorting] = useState<string | undefined>( | ||
sortingOptions.length > 0 ? sortingOptions[0] : undefined | ||
); | ||
const [isLoading, setIsLoading] = useState<boolean>(false); | ||
const { trigger } = APIHandler<ICertificate[]>(apiName); | ||
const [sorting, setSorting] = useState<boolean>(); | ||
|
||
// Info: (20240919 - tzuhan) 發送 API 請求 | ||
const fetchData = useCallback(async () => { | ||
try { | ||
if (isLoading) return; | ||
setIsLoading(true); | ||
// Deprecated: (20240920 - tzuhan) Debugging purpose only | ||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
const { success, code, data } = await trigger({ | ||
params, | ||
query: { | ||
type: selectedType, | ||
status: selectedStatus, | ||
startTimeStamp: !selectedDateRange.startTimeStamp | ||
? undefined | ||
: selectedDateRange.startTimeStamp, | ||
endTimeStamp: !selectedDateRange.endTimeStamp | ||
? undefined | ||
: selectedDateRange.endTimeStamp, | ||
search: searchQuery, | ||
sort: selectedSorting || sorting ? 'desc' : 'asc', | ||
}, | ||
}); | ||
/* Deprecated: (20240920 - tzuhan) Debugging purpose only | ||
// Info: (20240920 - tzuhan) 回傳 API 回應資料 | ||
if (success && onApiResponse) onApiResponse(data!); | ||
if (!success) { | ||
// Deprecated: (20240919 - tzuhan) Debugging purpose only | ||
// eslint-disable-next-line no-console | ||
console.error('API Request Failed:', code); | ||
} | ||
*/ | ||
if (onApiResponse) onApiResponse(generateRandomCertificates()); | ||
} catch (error) { | ||
// Deprecated: (20240919 - tzuhan) Debugging purpose only | ||
// eslint-disable-next-line no-console | ||
console.error('API Request error:', error); | ||
} finally { | ||
setIsLoading(false); | ||
} | ||
}, [ | ||
isLoading, | ||
selectedType, | ||
selectedStatus, | ||
selectedDateRange, | ||
searchQuery, | ||
selectedSorting, | ||
sorting, | ||
]); | ||
|
||
// Info: (20240919 - tzuhan) 每次狀態變更時,組合查詢條件並發送 API 請求 | ||
useEffect(() => { | ||
fetchData(); | ||
}, [selectedType, selectedStatus, selectedDateRange, searchQuery, selectedSorting, sorting]); | ||
|
||
return ( | ||
<div | ||
className="flex flex-wrap items-center justify-start space-x-4 rounded-lg bg-white p-4" | ||
style={{ maxWidth: '100%' }} | ||
> | ||
{/* Info: (20240919 - tzuhan) 類型篩選 */} | ||
{types.length > 0 && ( | ||
<SelectFilter | ||
label="Type" | ||
options={types} | ||
selectedValue={selectedType} | ||
onChange={setSelectedType} | ||
/> | ||
)} | ||
|
||
{/* Info: (20240919 - tzuhan) 狀態篩選 */} | ||
{statuses.length > 0 && ( | ||
<SelectFilter | ||
label="Status" | ||
options={statuses} | ||
selectedValue={selectedStatus} | ||
onChange={setSelectedStatus} | ||
/> | ||
)} | ||
|
||
{/* Info: (20240919 - tzuhan) 時間區間篩選 */} | ||
<div className="flex min-w-250px flex-1 flex-col"> | ||
<DatePicker | ||
period={selectedDateRange} | ||
setFilteredPeriod={setSelectedDateRange} | ||
type={DatePickerType.TEXT_PERIOD} | ||
btnClassName="mt-28px" | ||
/> | ||
</div> | ||
|
||
{/* Info: (20240919 - tzuhan) 搜索欄 */} | ||
<SearchInput searchQuery={searchQuery} onSearchChange={setSearchQuery} /> | ||
|
||
{/* Info: (20240919 - tzuhan) 顯示風格切換 */} | ||
<ViewToggle viewType={viewType} onViewTypeChange={viewToggleHandler} /> | ||
|
||
{/* Info: (20240919 - tzuhan) 排序選項 */} | ||
{sortingByDate ? ( | ||
<button | ||
type="button" | ||
className="mt-28px flex h-44px items-center space-x-2" | ||
onClick={() => setSorting((prev) => !prev)} | ||
> | ||
<Image src="/elements/double_arrow_down.svg" alt="arrow_down" width={20} height={20} /> | ||
<div className="leading-none">Newest</div> | ||
</button> | ||
) : ( | ||
sortingOptions.length > 0 && ( | ||
<SelectFilter | ||
label="Sort" | ||
options={sortingOptions} | ||
selectedValue={selectedSorting} | ||
onChange={setSelectedSorting} | ||
/> | ||
) | ||
)} | ||
</div> | ||
); | ||
}; | ||
|
||
export default FilterSection; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import React from 'react'; | ||
import { FiSearch } from 'react-icons/fi'; | ||
import { useTranslation } from 'next-i18next'; | ||
|
||
interface SearchInputProps { | ||
searchQuery: string | undefined; | ||
onSearchChange: (query: string) => void; | ||
} | ||
|
||
const SearchInput: React.FC<SearchInputProps> = ({ searchQuery, onSearchChange }) => { | ||
const { t } = useTranslation(['common']); | ||
return ( | ||
<div className="mt-28px flex min-w-200px flex-1 flex-col"> | ||
<label htmlFor="search" className="text-sm font-medium text-gray-500"> | ||
<div className="relative flex-1"> | ||
<input | ||
type="text" | ||
id="search" | ||
className={`relative flex h-44px w-full items-center justify-between rounded-sm border border-input-stroke-input bg-input-surface-input-background p-10px outline-none`} | ||
placeholder={t('common:COMMON.SEARCH')} | ||
defaultValue={searchQuery} | ||
onKeyDown={(e) => { | ||
if (e.key === 'Enter' && !e.nativeEvent.isComposing) { | ||
onSearchChange(e.currentTarget.value); | ||
} | ||
}} | ||
/> | ||
<FiSearch | ||
size={20} | ||
className="absolute right-3 top-3 cursor-pointer" | ||
onClick={() => { | ||
const query = (document.getElementById('search') as HTMLInputElement)?.value; | ||
onSearchChange(query); | ||
}} | ||
/> | ||
</div> | ||
</label> | ||
</div> | ||
); | ||
}; | ||
|
||
export default SearchInput; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import useOuterClick from '@/lib/hooks/use_outer_click'; | ||
import React from 'react'; | ||
import { FaChevronDown } from 'react-icons/fa6'; | ||
|
||
interface SelectFilterProps { | ||
label: string; // Info: (20240920 - tzuhan) 選項標籤 | ||
options: string[]; // Info: (20240920 - tzuhan) 下拉選項 | ||
selectedValue: string | undefined; // Info: (20240920 - tzuhan) 當前選中的值 | ||
onChange: (value: string) => void; // Info: (20240920 - tzuhan) 當選項改變時觸發的函數 | ||
} | ||
|
||
const SelectFilter: React.FC<SelectFilterProps> = ({ label, options, selectedValue, onChange }) => { | ||
const { | ||
targetRef: menuRef, | ||
componentVisible: menuVisibility, | ||
setComponentVisible: setMenuVisibility, | ||
} = useOuterClick<HTMLDivElement>(false); | ||
|
||
const toggleMenuHandler = () => { | ||
setMenuVisibility(!menuVisibility); | ||
}; | ||
|
||
return ( | ||
<div className="flex w-full flex-col gap-8px lg:w-200px"> | ||
<p className="text-sm font-semibold text-input-text-primary">{label}</p> | ||
<div | ||
onClick={toggleMenuHandler} | ||
className={`relative flex h-44px items-center justify-between rounded-sm border bg-input-surface-input-background ${menuVisibility ? 'border-input-stroke-selected' : 'border-input-stroke-input'} px-12px py-10px hover:cursor-pointer`} | ||
> | ||
<p className="text-input-text-input-placeholder">{selectedValue}</p> | ||
<FaChevronDown /> | ||
<div | ||
ref={menuRef} | ||
className={`absolute left-0 top-12 z-10 grid w-full rounded-sm border border-input-stroke-input ${ | ||
menuVisibility | ||
? 'grid-rows-1 border-dropdown-stroke-menu bg-input-surface-input-background shadow-dropmenu' | ||
: 'grid-rows-0 border-transparent' | ||
} overflow-hidden transition-all duration-300 ease-in-out`} | ||
> | ||
<ul className={`flex w-full flex-col items-start p-2`}> | ||
{options.map((option) => ( | ||
<li | ||
key={option} | ||
onClick={() => onChange(option)} | ||
className="w-full cursor-pointer px-3 py-2 text-dropdown-text-primary hover:text-text-brand-primary-lv2" | ||
> | ||
{option} | ||
</li> | ||
))} | ||
</ul> | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
}; | ||
|
||
export default SelectFilter; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import React from 'react'; | ||
import { FaListUl } from 'react-icons/fa6'; | ||
import { LuLayoutGrid } from 'react-icons/lu'; | ||
|
||
interface ViewToggleProps { | ||
viewType: 'grid' | 'list'; | ||
onViewTypeChange: (viewType: 'grid' | 'list') => void; | ||
} | ||
|
||
const ViewToggle: React.FC<ViewToggleProps> = ({ viewType, onViewTypeChange }) => { | ||
return ( | ||
<div className="flex items-center space-x-2"> | ||
<button | ||
type="button" | ||
className={`mt-28px rounded border border-tabs-stroke-default p-2.5 hover:bg-tabs-surface-active hover:text-stroke-neutral-solid-light ${viewType === 'grid' ? 'bg-tabs-surface-active text-stroke-neutral-solid-light' : 'bg-transparent text-tabs-stroke-secondary'}`} | ||
onClick={() => onViewTypeChange('grid')} | ||
> | ||
<LuLayoutGrid className="h-5 w-5" /> | ||
</button> | ||
<button | ||
type="button" | ||
className={`mt-28px rounded border border-tabs-stroke-default p-2.5 hover:bg-tabs-surface-active hover:text-stroke-neutral-solid-light ${viewType === 'list' ? 'bg-tabs-surface-active text-stroke-neutral-solid-light' : 'bg-transparent text-tabs-stroke-secondary'}`} | ||
onClick={() => onViewTypeChange('list')} | ||
> | ||
<FaListUl className="h-5 w-5" /> | ||
</button> | ||
</div> | ||
); | ||
}; | ||
|
||
export default ViewToggle; |
Oops, something went wrong.