Skip to content

Commit

Permalink
feat:add new UI
Browse files Browse the repository at this point in the history
  • Loading branch information
TzuHanLiang committed Sep 20, 2024
1 parent a5a4bd3 commit 380d0fd
Show file tree
Hide file tree
Showing 14 changed files with 675 additions and 292 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "iSunFA",
"version": "0.8.1+17",
"version": "0.8.1+18",
"private": false,
"scripts": {
"dev": "next dev",
Expand Down
5 changes: 5 additions & 0 deletions public/elements/double_arrow_down.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
172 changes: 172 additions & 0 deletions src/components/filter_section/filter_section.tsx
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;
42 changes: 42 additions & 0 deletions src/components/filter_section/search_input.tsx
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;
57 changes: 57 additions & 0 deletions src/components/filter_section/select_filter.tsx
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;
31 changes: 31 additions & 0 deletions src/components/filter_section/view_toggle.tsx
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;
Loading

0 comments on commit 380d0fd

Please sign in to comment.