diff --git a/Containers/RepoList/RepoList.module.css b/Containers/RepoList/RepoList.module.css index 11ff66b..087d90b 100644 --- a/Containers/RepoList/RepoList.module.css +++ b/Containers/RepoList/RepoList.module.css @@ -86,7 +86,6 @@ flex-direction: column; width: 90%; margin: 5px auto; - padding: 15px; } .unfoldButton { @@ -123,3 +122,77 @@ display: none; } } + +/* Toolbar */ + +.toolbar { + display: flex; + justify-content: space-between; + align-items: center; + width: 90%; + margin: 20px auto 10px; + flex-wrap: wrap; + gap: 10px; +} + +.searchInput { + padding: 10px 15px; + border: 1px solid #ccc; + border-radius: 8px; + font-size: 14px; + width: 100%; + max-width: 300px; +} + +.sortIcons { + display: flex; + gap: 10px; + align-items: center; +} + +.icon { + cursor: pointer; + color: #a6a6b8; + transition: transform 0.2s ease; +} + +.icon:hover { + transform: scale(1.1); + color: #6d4aff; +} + +.iconActive { + color: #6d4aff; + transform: scale(1.2); +} + +.searchContainer { + position: relative; + display: flex; + align-items: center; + width: 100%; + max-width: 300px; +} + +.searchInput { + width: 100%; + padding: 8px 32px 8px 12px; + border-radius: 8px; + border: 1px solid #ccc; + font-size: 14px; + outline: none; + background-color: white; +} + +.clearButton { + position: absolute; + right: 8px; + background: transparent; + border: none; + cursor: pointer; + color: #999; +} + +.clearButton:hover { + color: #333; +} diff --git a/Containers/RepoList/RepoList.tsx b/Containers/RepoList/RepoList.tsx index ba00f05..0048a5b 100644 --- a/Containers/RepoList/RepoList.tsx +++ b/Containers/RepoList/RepoList.tsx @@ -1,22 +1,50 @@ import classes from './RepoList.module.css'; import React, { useState, useEffect } from 'react'; -import { IconPlus } from '@tabler/icons-react'; +import { + IconPlus, + IconSortAscendingLetters, + IconSortDescendingLetters, + IconSortAscending2, + IconSortDescending2, + IconDatabase, + IconX, + IconClock, + IconCalendarUp, + IconCalendarDown, + IconSortAscendingSmallBig, + IconSortDescendingSmallBig, + IconSortDescending2Filled, +} from '@tabler/icons-react'; import { useRouter } from 'next/router'; import Link from 'next/link'; import useSWR, { useSWRConfig } from 'swr'; import { ToastContainer, ToastOptions, toast } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; -//Composants import Repo from '~/Components/Repo/Repo'; import RepoManage from '../RepoManage/RepoManage'; import ShimmerRepoList from '~/Components/UI/ShimmerRepoList/ShimmerRepoList'; import { Repository, WizardEnvType, Optional } from '~/types'; +type SortOption = + | 'alias-asc' + | 'alias-desc' + | 'status-true' + | 'status-false' + | 'storage-used-asc' + | 'storage-used-desc' + | 'last-save-asc' + | 'last-save-desc'; + export default function RepoList() { - ////Var const router = useRouter(); const { mutate } = useSWRConfig(); + const [displayRepoAdd, setDisplayRepoAdd] = useState(false); + const [displayRepoEdit, setDisplayRepoEdit] = useState(false); + const [wizardEnv, setWizardEnv] = useState>(); + const [sortOption, setSortOption] = useState('alias-asc'); + const [searchQuery, setSearchQuery] = useState(''); + const toastOptions: ToastOptions = { position: 'top-right', autoClose: 8000, @@ -27,110 +55,134 @@ export default function RepoList() { progress: undefined, }; - ////Datas - //Write a fetcher function to wrap the native fetch function and return the result of a call to url in json format - const fetcher = async (url: string) => await fetch(url).then((res) => res.json()); - const { data, error } = useSWR('/api/v1/repositories', fetcher); - - ////LifeCycle - //Component did mount + // Load filters from localStorage + useEffect(() => { + const savedSort = localStorage.getItem('repoSort'); + const savedSearch = localStorage.getItem('repoSearch'); + if (savedSort) setSortOption(savedSort as SortOption); + if (savedSearch) setSearchQuery(savedSearch); + }, []); + useEffect(() => { - //If the route is home/manage-repo/add, open the RepoAdd box. if (router.pathname === '/manage-repo/add') { setDisplayRepoAdd(!displayRepoAdd); } - //If the route is home/manage-repo/edit, open the RepoAdd box. if (router.pathname.startsWith('/manage-repo/edit')) { setDisplayRepoEdit(!displayRepoEdit); } - //Fetch wizardEnv to hydrate Repo components + const fetchWizardEnv = async () => { try { - const response = await fetch('/api/v1/account/wizard-env', { - method: 'GET', - headers: { - 'Content-type': 'application/json', - }, - }); + const response = await fetch('/api/v1/account/wizard-env'); const data: WizardEnvType = await response.json(); setWizardEnv(data); } catch (error) { - console.log('Fetching datas error'); + console.log('Fetching wizard env error'); } }; fetchWizardEnv(); }, []); - ////States - const [displayRepoAdd, setDisplayRepoAdd] = useState(false); - const [displayRepoEdit, setDisplayRepoEdit] = useState(false); - const [wizardEnv, setWizardEnv] = useState>(); + const fetcher = async (url: string) => await fetch(url).then((res) => res.json()); + const { data, error } = useSWR('/api/v1/repositories', fetcher); - ////Functions - - //Firstly, check the availability of data and condition it. if (!data) { - //Force mutate after login (force a API GET on /api/v1/repositories to load repoList) mutate('/api/v1/repositories'); return ; } - if (error) { - toast.error('An error has occurred.', toastOptions); - return ; - } - if (data.status == 500) { - toast.error('API Error !', toastOptions); + + if (error || data.status == 500) { + toast.error('Error loading repositories.', toastOptions); return ; } - //BUTTON : Display RepoManage component box for ADD - const manageRepoAddHandler = () => { - router.replace('/manage-repo/add'); + const handleSortChange = (option: SortOption) => { + setSortOption(option); + localStorage.setItem('repoSort', option); }; - //BUTTON : Display RepoManage component box for EDIT - const manageRepoEditHandler = (id: number) => { - router.replace('/manage-repo/edit/' + id); + const handleSearchChange = (e: React.ChangeEvent) => { + const query = e.target.value; + setSearchQuery(query); + localStorage.setItem('repoSearch', query); }; - //BUTTON : Close RepoManage component box (when cross is clicked) - const closeRepoManageBoxHandler = () => { - router.replace('/'); - }; + const getSortedRepoList = () => { + let repoList = [...data.repoList]; - // UI EFFECT : Display blur when display add repo modale - const displayBlur = () => { - if (displayRepoAdd || displayRepoEdit) { - return classes.containerBlur; - } else { - return classes.container; + // Filter + if (searchQuery) { + repoList = repoList.filter((repo) => + `${repo.alias} ${repo.comment} ${repo.repositoryName}` + .toLowerCase() + .includes(searchQuery.toLowerCase()) + ); + } + + // Sort + switch (sortOption) { + case 'alias-asc': + return repoList.sort((a, b) => a.alias.localeCompare(b.alias)); + case 'alias-desc': + return repoList.sort((a, b) => b.alias.localeCompare(a.alias)); + case 'status-true': + return repoList.sort((a, b) => Number(b.status) - Number(a.status)); + case 'status-false': + return repoList.sort((a, b) => Number(a.status) - Number(b.status)); + case 'storage-used-asc': + return repoList.sort((a, b) => { + const aRatio = a.storageSize ? a.storageUsed / a.storageSize : 0; + const bRatio = b.storageSize ? b.storageUsed / b.storageSize : 0; + return aRatio - bRatio; + }); + case 'storage-used-desc': + return repoList.sort((a, b) => { + const aRatio = a.storageSize ? a.storageUsed / a.storageSize : 0; + const bRatio = b.storageSize ? b.storageUsed / b.storageSize : 0; + return bRatio - aRatio; + }); + case 'last-save-asc': + return repoList.sort((a, b) => { + const aDate = a.lastSave ? new Date(a.lastSave).getTime() : 0; + const bDate = b.lastSave ? new Date(b.lastSave).getTime() : 0; + return aDate - bDate; + }); + case 'last-save-desc': + return repoList.sort((a, b) => { + const aDate = a.lastSave ? new Date(a.lastSave).getTime() : 0; + const bDate = b.lastSave ? new Date(b.lastSave).getTime() : 0; + return bDate - aDate; + }); + default: + return repoList; } }; - //Dynamic list of repositories (with a map of Repo components) - const renderRepoList = data.repoList.map((repo: Repository) => { - return ( - - manageRepoEditHandler(repo.id)} - wizardEnv={wizardEnv} - > - - ); - }); + const manageRepoAddHandler = () => router.replace('/manage-repo/add'); + const manageRepoEditHandler = (id: number) => router.replace('/manage-repo/edit/' + id); + const closeRepoManageBoxHandler = () => router.replace('/'); + const displayBlur = () => + displayRepoAdd || displayRepoEdit ? classes.containerBlur : classes.container; + + const renderRepoList = getSortedRepoList().map((repo: Repository) => ( + manageRepoEditHandler(repo.id)} + wizardEnv={wizardEnv} + /> + )); return ( <> @@ -145,10 +197,80 @@ export default function RepoList() { Add a repository + +
+
+ + {searchQuery && ( + + )} +
+ +
+ handleSortChange('alias-asc')} + title='Alias A-Z' + /> + handleSortChange('alias-desc')} + title='Alias Z-A' + /> + handleSortChange('status-true')} + title='Status OK → KO' + /> + handleSortChange('status-false')} + title='Status KO → OK' + /> + handleSortChange('last-save-desc')} + title='Last save (recent → old)' + /> + handleSortChange('last-save-asc')} + title='Last save (old → recent)' + /> + handleSortChange('storage-used-asc')} + title='Storage usage % low → high' + /> + handleSortChange('storage-used-desc')} + title='Storage usage % high → low' + /> +
+
+
{renderRepoList}
+ {displayRepoAdd && ( )}