diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..d0a7784 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +npx lint-staged \ No newline at end of file diff --git a/.lintstagedrc b/.lintstagedrc index 9d55be7..39d56be 100644 --- a/.lintstagedrc +++ b/.lintstagedrc @@ -1,6 +1,6 @@ { "*.{js,jsx,ts,tsx}": [ - "pnpm run lint", - "pnpm run format" + "npm run lint", + "npm run format" ] } diff --git a/biome.json b/biome.json index 5811cee..d8e432c 100644 --- a/biome.json +++ b/biome.json @@ -81,9 +81,15 @@ ".github/**", ".husky/**", ".vscode/**", + "dist/**", "coverage/**", + "playwright-report/**", + "test-results/**", "public/**", - "node_modules/**" + "node_modules/**", + "tsconfig.app.json", + "tsconfig.json", + "tsconfig.node.json" ] } } diff --git a/components.json b/components.json index f0f6cf1..0bdd0a5 100644 --- a/components.json +++ b/components.json @@ -18,4 +18,4 @@ "hooks": "@/hooks" }, "iconLibrary": "lucide" -} \ No newline at end of file +} diff --git a/playwright.config.ts b/playwright.config.ts index 5fc02bf..f25ae8f 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,4 +1,4 @@ -import { defineConfig, devices } from '@playwright/test'; +import { defineConfig, devices } from '@playwright/test' /** * Read environment variables from file. @@ -80,4 +80,4 @@ export default defineConfig({ url: 'http://localhost:3000', reuseExistingServer: !process.env.CI, }, -}); +}) diff --git a/src/App.tsx b/src/App.tsx index 5ed3ac0..39b5e2c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,17 +1,19 @@ -import { Route, Routes } from 'react-router-dom'; -import { SWRConfig } from 'swr'; -import { Layout } from './components/Layout'; -import { localCache } from './lib/cache'; -import { About } from './views/About'; -import { Detail } from './views/Detail'; -import ErrorPage from './views/Error'; -import { Home } from './views/Home'; +import { Route, Routes } from 'react-router-dom' +import { SWRConfig } from 'swr' +import { Layout } from './components/Layout' +import { localCache } from './lib/cache' +import { About } from './views/About' +import { Detail } from './views/Detail' +import ErrorPage from './views/Error' +import { Home } from './views/Home' export default function App() { return ( - + }> } /> @@ -21,5 +23,5 @@ export default function App() { - ); + ) } diff --git a/src/components/CopyButton.tsx b/src/components/CopyButton.tsx index f8afa3d..5a024f2 100644 --- a/src/components/CopyButton.tsx +++ b/src/components/CopyButton.tsx @@ -31,12 +31,12 @@ export function CopyButton({ disabled={isCopied} data-copied={`${isCopied}`} onClick={() => { - setIsCopied(true); - copyTextToClipboard(`${text}`); + setIsCopied(true) + copyTextToClipboard(`${text}`) setTimeout(() => { - setIsCopied(false); - }, 3000); + setIsCopied(false) + }, 3000) }} > {isCopied ? ( @@ -49,5 +49,5 @@ export function CopyButton({ ) : null} - ); + ) } diff --git a/src/components/EmptyState.tsx b/src/components/EmptyState.tsx index efac1c9..6b2adbd 100644 --- a/src/components/EmptyState.tsx +++ b/src/components/EmptyState.tsx @@ -1,5 +1,5 @@ -import { Link } from "react-router-dom"; -import { Button } from "./ui/button"; +import { Link } from 'react-router-dom' +import { Button } from './ui/button' export const EmptyState = ({ title, @@ -7,10 +7,10 @@ export const EmptyState = ({ actionLink = '/', actionTarget = 'Back to Homepage', }: { - title: string; - subtitle?: string; - actionTarget?: string; - actionLink?: string; + title: string + subtitle?: string + actionTarget?: string + actionLink?: string }) => { return (
@@ -34,5 +34,5 @@ export const EmptyState = ({ {actionTarget}
- ); -}; + ) +} diff --git a/src/components/GhCalendar.tsx b/src/components/GhCalendar.tsx index 4208340..e02cdb9 100644 --- a/src/components/GhCalendar.tsx +++ b/src/components/GhCalendar.tsx @@ -1,8 +1,8 @@ -import GitHubCalendar from 'react-github-calendar'; -import { useTheme } from './theme-provider'; +import GitHubCalendar from 'react-github-calendar' +import { useTheme } from './theme-provider' export const GhCalendar = ({ username }: { username: string }) => { - const { theme } = useTheme(); + const { theme } = useTheme() return ( { blockSize={16} colorScheme={theme === 'system' ? undefined : theme} /> - ); -}; + ) +} diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index ba8d535..ef5b1b0 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -1,9 +1,9 @@ -import { Analytics } from '@vercel/analytics/react'; -import { GithubIcon } from "lucide-react"; -import { Link, Outlet } from "react-router-dom" -import { ModeToggle } from "./mode-toggle"; -import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar"; -import { Button } from "./ui/button"; +import { Analytics } from '@vercel/analytics/react' +import { GithubIcon } from 'lucide-react' +import { Link, Outlet } from 'react-router-dom' +import { ModeToggle } from './mode-toggle' +import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar' +import { Button } from './ui/button' export const Layout = () => { return ( @@ -38,8 +38,10 @@ export const Layout = () => {
+ © 2023, built with ☕️ +
- Since 2023, built with ☕️ by{' '} + By{' '} {
- ); + ) } diff --git a/src/components/Spinner.tsx b/src/components/Spinner.tsx index dffe97e..c292659 100644 --- a/src/components/Spinner.tsx +++ b/src/components/Spinner.tsx @@ -1,4 +1,4 @@ -import { cn } from '@/lib/utils'; +import { cn } from '@/lib/utils' export const Spinner = ({ className = '' }: { className?: string }) => { return ( @@ -8,7 +8,7 @@ export const Spinner = ({ className = '' }: { className?: string }) => { aria-hidden="true" className={cn( 'inline w-10 h-10 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600', - className + className, )} viewBox="0 0 100 101" fill="none" @@ -26,5 +26,5 @@ export const Spinner = ({ className = '' }: { className?: string }) => { Loading... - ); -}; + ) +} diff --git a/src/components/TableUser/card-users.tsx b/src/components/TableUser/card-users.tsx index ad4c2d6..9a9a6c4 100644 --- a/src/components/TableUser/card-users.tsx +++ b/src/components/TableUser/card-users.tsx @@ -1,9 +1,8 @@ -'use client'; +'use client' -import type { User } from '@/lib/api'; -import { formatLastUpdated, formatNumber, makeInitial } from '@/lib/utils'; +import type { User } from '@/lib/api' +import { formatLastUpdated, formatNumber, makeInitial } from '@/lib/utils' import { - type ColumnDef, type ColumnFiltersState, type Row, type SortingState, @@ -15,24 +14,19 @@ import { getPaginationRowModel, getSortedRowModel, useReactTable, -} from '@tanstack/react-table'; -import { ClockIcon } from 'lucide-react'; -import { Fragment, useState } from 'react'; -import { Link } from 'react-router-dom'; -import { Avatar, AvatarFallback, AvatarImage } from '../ui/avatar'; -import { DEFAULT_CLASSNAMES_RANK, renderRank } from './column'; -import { DataTablePagination } from './data-table-pagination'; -import { DataTableRowActions } from './data-table-row-actions'; -import { DataTableToolbar } from './data-table-toolbar'; - -interface DataTableProps { - columns: ColumnDef[]; - data: TData[]; - updatedAt?: Date; -} +} from '@tanstack/react-table' +import { ClockIcon } from 'lucide-react' +import { Fragment, useState } from 'react' +import { Link } from 'react-router-dom' +import { Avatar, AvatarFallback, AvatarImage } from '../ui/avatar' +import { DEFAULT_CLASSNAMES_RANK, renderRank } from './column' +import { DataTablePagination } from './data-table-pagination' +import { DataTableRowActions } from './data-table-row-actions' +import { DataTableToolbar } from './data-table-toolbar' +import type { DataTableProps } from './types' function renderRowUser({ row }: { row: Row }) { - const user = row.original as User; + const user = row.original as User return (
@@ -80,17 +74,39 @@ function renderRowUser({ row }: { row: Row }) {
- ); + ) } export function CardUsers({ columns, data, updatedAt, + + pageIndex, + pageSize, + setPageParams, + + filterBy, + filterValue, + setFilterParams, + + sortBy, + sortDir, + setSortParams, }: DataTableProps) { - const [sorting, setSorting] = useState([]); - const [columnVisibility, setColumnVisibility] = useState({}); - const [columnFilters, setColumnFilters] = useState([]); + const [columnVisibility, setColumnVisibility] = useState({}) + const [columnFilters, setColumnFilters] = useState([ + { + id: filterBy, + value: filterValue, + }, + ]) + const [sorting, setSorting] = useState([ + { + id: sortBy, + desc: sortDir === 'desc', + }, + ]) const table = useReactTable({ data, @@ -108,6 +124,10 @@ export function CardUsers({ sorting, columnVisibility, columnFilters, + pagination: { + pageSize: pageSize, + pageIndex: pageIndex, + }, }, initialState: { columnVisibility: { @@ -115,7 +135,7 @@ export function CardUsers({ name: true, }, }, - }); + }) return (
@@ -125,6 +145,9 @@ export function CardUsers({ table={table} withTableViewOptions={false} withSortOptions={true} + filterValue={filterValue} + setFilterParams={setFilterParams} + setSortParams={setSortParams} />
@@ -144,7 +167,11 @@ export function CardUsers({
- +
@@ -153,5 +180,5 @@ export function CardUsers({ {formatLastUpdated(updatedAt)}

- ); + ) } diff --git a/src/components/TableUser/column.tsx b/src/components/TableUser/column.tsx index 4ba623a..a6c0c95 100644 --- a/src/components/TableUser/column.tsx +++ b/src/components/TableUser/column.tsx @@ -1,38 +1,38 @@ -'use client'; +'use client' -import type { User } from '@/lib/api'; -import { cn, formatNumber, makeInitial } from '@/lib/utils'; -import type { ColumnDef } from '@tanstack/react-table'; -import { Link } from 'react-router-dom'; -import { Avatar, AvatarFallback, AvatarImage } from '../ui/avatar'; -import { DataTableColumnHeader } from './data-table-column-header'; -import { DataTableRowActions } from './data-table-row-actions'; +import type { User } from '@/lib/api' +import { cn, formatNumber, makeInitial } from '@/lib/utils' +import type { ColumnDef } from '@tanstack/react-table' +import { Link } from 'react-router-dom' +import { Avatar, AvatarFallback, AvatarImage } from '../ui/avatar' +import { DataTableColumnHeader } from './data-table-column-header' +import { DataTableRowActions } from './data-table-row-actions' export const DEFAULT_CLASSNAMES_RANK = { first: '', second: '', third: '', other: '', -}; +} export const renderRank = ( rank: number, - classNames = DEFAULT_CLASSNAMES_RANK + classNames = DEFAULT_CLASSNAMES_RANK, ) => { if (rank === 1) { - return

🥇

; + return

🥇

} if (rank === 2) { - return

🥈

; + return

🥈

} if (rank === 3) { - return

🥉

; + return

🥉

} return (

#{formatNumber(rank)}

- ); -}; + ) +} export const columnsDesktop: ColumnDef[] = [ { @@ -40,9 +40,9 @@ export const columnsDesktop: ColumnDef[] = [ header: 'Avatar', enableHiding: false, cell: ({ row }) => { - const avatarUrl = row.getValue('avatarUrl') as string; - const username = row.getValue('username') as string; - const name = row.getValue('name') as string; + const avatarUrl = row.getValue('avatarUrl') as string + const username = row.getValue('username') as string + const name = row.getValue('name') as string return ( @@ -51,7 +51,7 @@ export const columnsDesktop: ColumnDef[] = [ {makeInitial(name || username)} - ); + ) }, }, { @@ -61,12 +61,12 @@ export const columnsDesktop: ColumnDef[] = [ ), cell: ({ row }) => { - const username = row.getValue('username') as string; + const username = row.getValue('username') as string return ( {username} - ); + ) }, }, { @@ -82,8 +82,8 @@ export const columnsDesktop: ColumnDef[] = [ ), cell: ({ row }) => { - const followerRank = row.getValue('followerRank') as number; - return renderRank(followerRank, DEFAULT_CLASSNAMES_RANK); + const followerRank = row.getValue('followerRank') as number + return renderRank(followerRank, DEFAULT_CLASSNAMES_RANK) }, }, { @@ -92,8 +92,8 @@ export const columnsDesktop: ColumnDef[] = [ ), cell: ({ row }) => { - const followers = row.getValue('followers') as number; - return

{formatNumber(followers)}

; + const followers = row.getValue('followers') as number + return

{formatNumber(followers)}

}, }, { @@ -103,8 +103,8 @@ export const columnsDesktop: ColumnDef[] = [ ), cell: ({ row }) => { - const contributionRank = row.getValue('contributionRank') as number; - return renderRank(contributionRank, DEFAULT_CLASSNAMES_RANK); + const contributionRank = row.getValue('contributionRank') as number + return renderRank(contributionRank, DEFAULT_CLASSNAMES_RANK) }, }, { @@ -113,8 +113,8 @@ export const columnsDesktop: ColumnDef[] = [ ), cell: ({ row }) => { - const contributions = row.getValue('contributions') as number; - return

{formatNumber(contributions)}

; + const contributions = row.getValue('contributions') as number + return

{formatNumber(contributions)}

}, }, { @@ -125,7 +125,7 @@ export const columnsDesktop: ColumnDef[] = [ id: 'actions', cell: ({ row }) => , }, -]; +] export const columnsMobile: ColumnDef[] = [ { @@ -152,4 +152,4 @@ export const columnsMobile: ColumnDef[] = [ { accessorKey: 'company', }, -]; +] diff --git a/src/components/TableUser/data-table-column-header.tsx b/src/components/TableUser/data-table-column-header.tsx index a1fb831..b788cec 100644 --- a/src/components/TableUser/data-table-column-header.tsx +++ b/src/components/TableUser/data-table-column-header.tsx @@ -1,20 +1,21 @@ -import type { Column } from '@tanstack/react-table'; -import { ArrowDown, ArrowUp, ChevronsUpDown, EyeOff } from 'lucide-react'; +import type { Column } from '@tanstack/react-table' +import { ArrowDown, ArrowUp, ChevronsUpDown, EyeOff } from 'lucide-react' -import { cn } from '@/lib/utils'; -import { Button } from '../ui/button'; +import { cn } from '@/lib/utils' +import { Button } from '../ui/button' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, -} from '../ui/dropdown-menu'; +} from '../ui/dropdown-menu' +import { useSortingSearchParams } from './search-params.sorting' interface DataTableColumnHeaderProps extends React.HTMLAttributes { - column: Column; - title: string; + column: Column + title: string } export function DataTableColumnHeader({ @@ -22,8 +23,10 @@ export function DataTableColumnHeader({ title, className, }: DataTableColumnHeaderProps) { + const [_, setSortParams] = useSortingSearchParams() + if (!column.getCanSort()) { - return
{title}
; + return
{title}
} return ( @@ -46,11 +49,29 @@ export function DataTableColumnHeader({ - column.toggleSorting(false)}> + { + column.toggleSorting(false) + + setSortParams({ + sortBy: column.id, + sortDir: 'asc', + }) + }} + > Asc - column.toggleSorting(true)}> + { + column.toggleSorting(true) + + setSortParams({ + sortBy: column.id, + sortDir: 'desc', + }) + }} + > Desc @@ -66,5 +87,5 @@ export function DataTableColumnHeader({ - ); + ) } diff --git a/src/components/TableUser/data-table-pagination.tsx b/src/components/TableUser/data-table-pagination.tsx index e2c7a56..ecc5fb3 100644 --- a/src/components/TableUser/data-table-pagination.tsx +++ b/src/components/TableUser/data-table-pagination.tsx @@ -1,28 +1,31 @@ -import type { Table } from '@tanstack/react-table'; +import type { Table } from '@tanstack/react-table' import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, -} from 'lucide-react'; +} from 'lucide-react' -import { Button } from '../ui/button'; +import { Button } from '../ui/button' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, -} from '../ui/select'; +} from '../ui/select' +import type { SetPageParams } from './types' interface DataTablePaginationProps { - table: Table; - withPageSize?: boolean; + table: Table + withPageSize?: boolean + setPageParams: SetPageParams } export function DataTablePagination({ table, withPageSize, + setPageParams, }: DataTablePaginationProps) { return (
@@ -36,7 +39,10 @@ export function DataTablePagination({ - table.getColumn('username')?.setFilterValue(event.target.value) - } + value={val} + onChange={(event) => { + const newValue = event.target.value + table.getColumn('username')?.setFilterValue(newValue) + setFilterParams({ + filterBy: 'username', + filterValue: newValue, + }) + }} className="h-8 w-full md:w-[150px] lg:w-[250px]" />
{withTableViewOptions && } + {withSortOptions && (
@@ -59,7 +80,14 @@ export function DataTableToolbar({ table.getColumn('contributions'), ].map((column) => ( column?.toggleSorting?.(true)} + onClick={() => { + column?.toggleSorting?.(true) + + setSortParams({ + sortDir: 'desc', + sortBy: column?.id, + }) + }} key={column?.id} > {column?.id === 'contributions' ? ( @@ -67,7 +95,6 @@ export function DataTableToolbar({ ) : ( )} - By {column?.id} ))} @@ -76,5 +103,5 @@ export function DataTableToolbar({
)} - ); + ) } diff --git a/src/components/TableUser/data-table-view-options.tsx b/src/components/TableUser/data-table-view-options.tsx index 8690837..e41dc85 100644 --- a/src/components/TableUser/data-table-view-options.tsx +++ b/src/components/TableUser/data-table-view-options.tsx @@ -1,20 +1,20 @@ -'use client'; +'use client' -import { DropdownMenuTrigger } from '@radix-ui/react-dropdown-menu'; -import type { Table } from '@tanstack/react-table'; -import { Settings2 } from 'lucide-react'; +import { DropdownMenuTrigger } from '@radix-ui/react-dropdown-menu' +import type { Table } from '@tanstack/react-table' +import { Settings2 } from 'lucide-react' -import { Button } from '../ui/button'; +import { Button } from '../ui/button' import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuLabel, DropdownMenuSeparator, -} from '../ui/dropdown-menu'; +} from '../ui/dropdown-menu' interface DataTableViewOptionsProps { - table: Table; + table: Table } export function DataTableViewOptions({ @@ -23,11 +23,7 @@ export function DataTableViewOptions({ return ( - @@ -39,7 +35,7 @@ export function DataTableViewOptions({ .getAllColumns() .filter( (column) => - typeof column.accessorFn !== 'undefined' && column.getCanHide() + typeof column.accessorFn !== 'undefined' && column.getCanHide(), ) .map((column) => { return ( @@ -51,9 +47,9 @@ export function DataTableViewOptions({ > {column.id} - ); + ) })} - ); + ) } diff --git a/src/components/TableUser/data-table.tsx b/src/components/TableUser/data-table.tsx index 8e841eb..3fe2aef 100644 --- a/src/components/TableUser/data-table.tsx +++ b/src/components/TableUser/data-table.tsx @@ -1,7 +1,6 @@ -'use client'; +'use client' import { - type ColumnDef, type ColumnFiltersState, type SortingState, type VisibilityState, @@ -13,7 +12,7 @@ import { getPaginationRowModel, getSortedRowModel, useReactTable, -} from '@tanstack/react-table'; +} from '@tanstack/react-table' import { Table, @@ -22,27 +21,44 @@ import { TableHead, TableHeader, TableRow, -} from '@/components/ui/table'; -import { formatLastUpdated } from '@/lib/utils'; -import { ClockIcon } from 'lucide-react'; -import { useState } from 'react'; -import { DataTablePagination } from './data-table-pagination'; -import { DataTableToolbar } from './data-table-toolbar'; - -interface DataTableProps { - columns: ColumnDef[]; - data: TData[]; - updatedAt?: Date; -} +} from '@/components/ui/table' +import { formatLastUpdated } from '@/lib/utils' +import { ClockIcon } from 'lucide-react' +import { useState } from 'react' +import { DataTablePagination } from './data-table-pagination' +import { DataTableToolbar } from './data-table-toolbar' +import type { DataTableProps } from './types' export function DataTable({ columns, data, updatedAt, + + pageIndex, + pageSize, + setPageParams, + + filterBy, + filterValue, + setFilterParams, + + sortBy, + sortDir, + setSortParams, }: DataTableProps) { - const [sorting, setSorting] = useState([]); - const [columnVisibility, setColumnVisibility] = useState({}); - const [columnFilters, setColumnFilters] = useState([]); + const [columnVisibility, setColumnVisibility] = useState({}) + const [columnFilters, setColumnFilters] = useState([ + { + id: filterBy, + value: filterValue, + }, + ]) + const [sorting, setSorting] = useState([ + { + id: sortBy, + desc: sortDir === 'desc', + }, + ]) const table = useReactTable({ data, @@ -60,6 +76,10 @@ export function DataTable({ sorting, columnVisibility, columnFilters, + pagination: { + pageSize: pageSize, + pageIndex: pageIndex, + }, }, initialState: { columnVisibility: { @@ -67,7 +87,7 @@ export function DataTable({ name: true, }, }, - }); + }) return (
@@ -77,6 +97,9 @@ export function DataTable({ table={table} withTableViewOptions={true} withSortOptions={false} + filterValue={filterValue} + setFilterParams={setFilterParams} + setSortParams={setSortParams} />
@@ -90,10 +113,10 @@ export function DataTable({ ? null : flexRender( header.column.columnDef.header, - header.getContext() + header.getContext(), )} - ); + ) })} ))} @@ -109,7 +132,7 @@ export function DataTable({ {flexRender( cell.column.columnDef.cell, - cell.getContext() + cell.getContext(), )} ))} @@ -129,7 +152,11 @@ export function DataTable({
- +

@@ -137,5 +164,5 @@ export function DataTable({ {formatLastUpdated(updatedAt)}

- ); + ) } diff --git a/src/components/TableUser/search-params.filter.ts b/src/components/TableUser/search-params.filter.ts new file mode 100644 index 0000000..2beca91 --- /dev/null +++ b/src/components/TableUser/search-params.filter.ts @@ -0,0 +1,18 @@ +import { parseAsString, useQueryStates } from 'nuqs' + +const filterParsers = { + filterBy: parseAsString.withDefault('username'), + filterValue: parseAsString.withDefault(''), +} + +const filterUrlKeys = { + filterBy: 'searchBy', + filterValue: 's', +} + +export function useFilterSearchParams() { + return useQueryStates(filterParsers, { + urlKeys: filterUrlKeys, + throttleMs: 300, + }) +} diff --git a/src/components/TableUser/search-params.pagination.ts b/src/components/TableUser/search-params.pagination.ts new file mode 100644 index 0000000..f0586a3 --- /dev/null +++ b/src/components/TableUser/search-params.pagination.ts @@ -0,0 +1,32 @@ +// Taken from: https://nuqs.47ng.com/docs/parsers/community/tanstack-table + +import { createParser, parseAsInteger, useQueryStates } from 'nuqs' + +// The page index parser is zero-indexed internally, +// but one-indexed when rendered in the URL, +// to align with your UI and what users might expect. +const pageIndexParser = createParser({ + parse: (query) => { + const page = parseAsInteger.parse(query) + return page === null ? null : page - 1 + }, + serialize: (value) => { + return parseAsInteger.serialize(value + 1) + }, +}) + +const paginationParsers = { + pageIndex: pageIndexParser.withDefault(0), + pageSize: parseAsInteger.withDefault(20), +} + +const paginationUrlKeys = { + pageIndex: 'page', + pageSize: 'perPage', +} + +export function usePaginationSearchParams() { + return useQueryStates(paginationParsers, { + urlKeys: paginationUrlKeys, + }) +} diff --git a/src/components/TableUser/search-params.sorting.ts b/src/components/TableUser/search-params.sorting.ts new file mode 100644 index 0000000..8afbd6e --- /dev/null +++ b/src/components/TableUser/search-params.sorting.ts @@ -0,0 +1,17 @@ +import { parseAsString, useQueryStates } from 'nuqs' + +const sortingParsers = { + sortBy: parseAsString.withDefault(''), + sortDir: parseAsString.withDefault('desc'), +} + +const sortingUrlKeys = { + sortBy: 'sortBy', + sortDir: 'sortDir', +} + +export function useSortingSearchParams() { + return useQueryStates(sortingParsers, { + urlKeys: sortingUrlKeys, + }) +} diff --git a/src/components/TableUser/types.ts b/src/components/TableUser/types.ts new file mode 100644 index 0000000..8cfb95f --- /dev/null +++ b/src/components/TableUser/types.ts @@ -0,0 +1,53 @@ +import type { ColumnDef } from '@tanstack/react-table' +import type { ParserBuilder, SetValues } from 'nuqs' + +export type SetPageParams = SetValues<{ + pageIndex: Omit, 'parseServerSide'> & { + readonly defaultValue: number + parseServerSide(value: string | string[] | undefined): number + } + pageSize: Omit, 'parseServerSide'> & { + readonly defaultValue: number + parseServerSide(value: string | string[] | undefined): number + } +}> + +export type SetFilterParams = SetValues<{ + filterBy: Omit, 'parseServerSide'> & { + readonly defaultValue: string + parseServerSide(value: string | string[] | undefined): string + } + filterValue: Omit, 'parseServerSide'> & { + readonly defaultValue: string + parseServerSide(value: string | string[] | undefined): string + } +}> + +export type SetSortParams = SetValues<{ + sortBy: Omit, 'parseServerSide'> & { + readonly defaultValue: string + parseServerSide(value: string | string[] | undefined): string + } + sortDir: Omit, 'parseServerSide'> & { + readonly defaultValue: string + parseServerSide(value: string | string[] | undefined): string + } +}> + +export interface DataTableProps { + columns: ColumnDef[] + data: TData[] + updatedAt?: Date + + pageIndex: number + pageSize: number + setPageParams: SetPageParams + + filterBy: string + filterValue: string + setFilterParams: SetFilterParams + + sortBy: string + sortDir: string + setSortParams: SetSortParams +} diff --git a/src/components/mode-toggle.tsx b/src/components/mode-toggle.tsx index 16662c4..fd2a30f 100644 --- a/src/components/mode-toggle.tsx +++ b/src/components/mode-toggle.tsx @@ -1,16 +1,16 @@ -import { Moon, Sun } from 'lucide-react'; +import { Moon, Sun } from 'lucide-react' -import { Button } from '@/components/ui/button'; +import { Button } from '@/components/ui/button' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; -import { useTheme } from '@/components/theme-provider'; +} from '@/components/ui/dropdown-menu' +import { useTheme } from '@/components/theme-provider' export function ModeToggle() { - const { setTheme } = useTheme(); + const { setTheme } = useTheme() return ( @@ -33,5 +33,5 @@ export function ModeToggle() { - ); + ) } diff --git a/src/components/theme-provider.tsx b/src/components/theme-provider.tsx index afc6bbc..1988bf0 100644 --- a/src/components/theme-provider.tsx +++ b/src/components/theme-provider.tsx @@ -1,24 +1,24 @@ -import { createContext, useContext, useEffect, useState } from 'react'; +import { createContext, useContext, useEffect, useState } from 'react' -type Theme = 'dark' | 'light' | 'system'; +type Theme = 'dark' | 'light' | 'system' type ThemeProviderProps = { - children: React.ReactNode; - defaultTheme?: Theme; - storageKey?: string; -}; + children: React.ReactNode + defaultTheme?: Theme + storageKey?: string +} type ThemeProviderState = { - theme: Theme; - setTheme: (theme: Theme) => void; -}; + theme: Theme + setTheme: (theme: Theme) => void +} const initialState: ThemeProviderState = { theme: 'system', setTheme: () => null, -}; +} -const ThemeProviderContext = createContext(initialState); +const ThemeProviderContext = createContext(initialState) export function ThemeProvider({ children, @@ -27,47 +27,47 @@ export function ThemeProvider({ ...props }: ThemeProviderProps) { const [theme, setTheme] = useState( - () => (localStorage.getItem(storageKey) as Theme) || defaultTheme - ); + () => (localStorage.getItem(storageKey) as Theme) || defaultTheme, + ) useEffect(() => { - const root = window.document.documentElement; + const root = window.document.documentElement - root.classList.remove('light', 'dark'); + root.classList.remove('light', 'dark') if (theme === 'system') { const systemTheme = window.matchMedia('(prefers-color-scheme: dark)') .matches ? 'dark' - : 'light'; + : 'light' - root.classList.add(systemTheme); - return; + root.classList.add(systemTheme) + return } - root.classList.add(theme); - }, [theme]); + root.classList.add(theme) + }, [theme]) const value = { theme, setTheme: (theme: Theme) => { - localStorage.setItem(storageKey, theme); - setTheme(theme); + localStorage.setItem(storageKey, theme) + setTheme(theme) }, - }; + } return ( {children} - ); + ) } export const useTheme = () => { - const context = useContext(ThemeProviderContext); + const context = useContext(ThemeProviderContext) if (context === undefined) - throw new Error('useTheme must be used within a ThemeProvider'); + throw new Error('useTheme must be used within a ThemeProvider') - return context; -}; + return context +} diff --git a/src/components/ui/accordion.tsx b/src/components/ui/accordion.tsx index e6a723d..bc7eab1 100644 --- a/src/components/ui/accordion.tsx +++ b/src/components/ui/accordion.tsx @@ -1,8 +1,8 @@ -import * as React from "react" -import * as AccordionPrimitive from "@radix-ui/react-accordion" -import { ChevronDown } from "lucide-react" +import * as React from 'react' +import * as AccordionPrimitive from '@radix-ui/react-accordion' +import { ChevronDown } from 'lucide-react' -import { cn } from "@/lib/utils" +import { cn } from '@/lib/utils' const Accordion = AccordionPrimitive.Root @@ -12,11 +12,11 @@ const AccordionItem = React.forwardRef< >(({ className, ...props }, ref) => ( )) -AccordionItem.displayName = "AccordionItem" +AccordionItem.displayName = 'AccordionItem' const AccordionTrigger = React.forwardRef< React.ElementRef, @@ -26,8 +26,8 @@ const AccordionTrigger = React.forwardRef< svg]:rotate-180", - className + 'flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180', + className, )} {...props} > @@ -47,7 +47,7 @@ const AccordionContent = React.forwardRef< className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down" {...props} > -
{children}
+
{children}
)) diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx index 41fa7e0..2b2ced8 100644 --- a/src/components/ui/alert.tsx +++ b/src/components/ui/alert.tsx @@ -1,22 +1,22 @@ -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" +import * as React from 'react' +import { cva, type VariantProps } from 'class-variance-authority' -import { cn } from "@/lib/utils" +import { cn } from '@/lib/utils' const alertVariants = cva( - "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", + 'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground', { variants: { variant: { - default: "bg-background text-foreground", + default: 'bg-background text-foreground', destructive: - "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + 'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive', }, }, defaultVariants: { - variant: "default", + variant: 'default', }, - } + }, ) const Alert = React.forwardRef< @@ -30,7 +30,7 @@ const Alert = React.forwardRef< {...props} /> )) -Alert.displayName = "Alert" +Alert.displayName = 'Alert' const AlertTitle = React.forwardRef< HTMLParagraphElement, @@ -38,11 +38,11 @@ const AlertTitle = React.forwardRef< >(({ className, ...props }, ref) => (
)) -AlertTitle.displayName = "AlertTitle" +AlertTitle.displayName = 'AlertTitle' const AlertDescription = React.forwardRef< HTMLParagraphElement, @@ -50,10 +50,10 @@ const AlertDescription = React.forwardRef< >(({ className, ...props }, ref) => (
)) -AlertDescription.displayName = "AlertDescription" +AlertDescription.displayName = 'AlertDescription' export { Alert, AlertTitle, AlertDescription } diff --git a/src/components/ui/avatar.tsx b/src/components/ui/avatar.tsx index 991f56e..467bf12 100644 --- a/src/components/ui/avatar.tsx +++ b/src/components/ui/avatar.tsx @@ -1,7 +1,7 @@ -import * as React from "react" -import * as AvatarPrimitive from "@radix-ui/react-avatar" +import * as React from 'react' +import * as AvatarPrimitive from '@radix-ui/react-avatar' -import { cn } from "@/lib/utils" +import { cn } from '@/lib/utils' const Avatar = React.forwardRef< React.ElementRef, @@ -10,8 +10,8 @@ const Avatar = React.forwardRef< @@ -24,7 +24,7 @@ const AvatarImage = React.forwardRef< >(({ className, ...props }, ref) => ( )) @@ -37,8 +37,8 @@ const AvatarFallback = React.forwardRef< diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 36496a2..ee95f41 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -1,36 +1,36 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { cva, type VariantProps } from "class-variance-authority" +import * as React from 'react' +import { Slot } from '@radix-ui/react-slot' +import { cva, type VariantProps } from 'class-variance-authority' -import { cn } from "@/lib/utils" +import { cn } from '@/lib/utils' const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', { variants: { variant: { - default: "bg-primary text-primary-foreground hover:bg-primary/90", + default: 'bg-primary text-primary-foreground hover:bg-primary/90', destructive: - "bg-destructive text-destructive-foreground hover:bg-destructive/90", + 'bg-destructive text-destructive-foreground hover:bg-destructive/90', outline: - "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', secondary: - "bg-secondary text-secondary-foreground hover:bg-secondary/80", - ghost: "hover:bg-accent hover:text-accent-foreground", - link: "text-primary underline-offset-4 hover:underline", + 'bg-secondary text-secondary-foreground hover:bg-secondary/80', + ghost: 'hover:bg-accent hover:text-accent-foreground', + link: 'text-primary underline-offset-4 hover:underline', }, size: { - default: "h-10 px-4 py-2", - sm: "h-9 rounded-md px-3", - lg: "h-11 rounded-md px-8", - icon: "h-10 w-10", + default: 'h-10 px-4 py-2', + sm: 'h-9 rounded-md px-3', + lg: 'h-11 rounded-md px-8', + icon: 'h-10 w-10', }, }, defaultVariants: { - variant: "default", - size: "default", + variant: 'default', + size: 'default', }, - } + }, ) export interface ButtonProps @@ -41,7 +41,7 @@ export interface ButtonProps const Button = React.forwardRef( ({ className, variant, size, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : "button" + const Comp = asChild ? Slot : 'button' return ( ( {...props} /> ) - } + }, ) -Button.displayName = "Button" +Button.displayName = 'Button' export { Button, buttonVariants } diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx index f62edea..1aab4ef 100644 --- a/src/components/ui/card.tsx +++ b/src/components/ui/card.tsx @@ -1,6 +1,6 @@ -import * as React from "react" +import * as React from 'react' -import { cn } from "@/lib/utils" +import { cn } from '@/lib/utils' const Card = React.forwardRef< HTMLDivElement, @@ -9,13 +9,13 @@ const Card = React.forwardRef<
)) -Card.displayName = "Card" +Card.displayName = 'Card' const CardHeader = React.forwardRef< HTMLDivElement, @@ -23,11 +23,11 @@ const CardHeader = React.forwardRef< >(({ className, ...props }, ref) => (
)) -CardHeader.displayName = "CardHeader" +CardHeader.displayName = 'CardHeader' const CardTitle = React.forwardRef< HTMLDivElement, @@ -36,13 +36,13 @@ const CardTitle = React.forwardRef<
)) -CardTitle.displayName = "CardTitle" +CardTitle.displayName = 'CardTitle' const CardDescription = React.forwardRef< HTMLDivElement, @@ -50,19 +50,19 @@ const CardDescription = React.forwardRef< >(({ className, ...props }, ref) => (
)) -CardDescription.displayName = "CardDescription" +CardDescription.displayName = 'CardDescription' const CardContent = React.forwardRef< HTMLDivElement, React.HTMLAttributes >(({ className, ...props }, ref) => ( -
+
)) -CardContent.displayName = "CardContent" +CardContent.displayName = 'CardContent' const CardFooter = React.forwardRef< HTMLDivElement, @@ -70,10 +70,10 @@ const CardFooter = React.forwardRef< >(({ className, ...props }, ref) => (
)) -CardFooter.displayName = "CardFooter" +CardFooter.displayName = 'CardFooter' export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/src/components/ui/collapsible.tsx b/src/components/ui/collapsible.tsx index a23e7a2..7cee61e 100644 --- a/src/components/ui/collapsible.tsx +++ b/src/components/ui/collapsible.tsx @@ -1,4 +1,4 @@ -import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" +import * as CollapsiblePrimitive from '@radix-ui/react-collapsible' const Collapsible = CollapsiblePrimitive.Root diff --git a/src/components/ui/dot-pattern.tsx b/src/components/ui/dot-pattern.tsx index ea42a5e..ffea606 100644 --- a/src/components/ui/dot-pattern.tsx +++ b/src/components/ui/dot-pattern.tsx @@ -1,17 +1,25 @@ -import { useId } from "react"; +import { useId } from 'react' -import { cn } from "@/lib/utils"; +import { cn } from '@/lib/utils' interface DotPatternProps { - width?: any; - height?: any; - x?: any; - y?: any; - cx?: any; - cy?: any; - cr?: any; - className?: string; - [key: string]: any; + // biome-ignore lint/suspicious/noExplicitAny: + width?: any + // biome-ignore lint/suspicious/noExplicitAny: + height?: any + // biome-ignore lint/suspicious/noExplicitAny: + x?: any + // biome-ignore lint/suspicious/noExplicitAny: + y?: any + // biome-ignore lint/suspicious/noExplicitAny: + cx?: any + // biome-ignore lint/suspicious/noExplicitAny: + cy?: any + // biome-ignore lint/suspicious/noExplicitAny: + cr?: any + className?: string + // biome-ignore lint/suspicious/noExplicitAny: + [key: string]: any } export function DotPattern({ width = 16, @@ -24,13 +32,13 @@ export function DotPattern({ className, ...props }: DotPatternProps) { - const id = useId(); + const id = useId() return ( - ); + ) } -export default DotPattern; +export default DotPattern diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx index 8e75d67..8618920 100644 --- a/src/components/ui/dropdown-menu.tsx +++ b/src/components/ui/dropdown-menu.tsx @@ -1,8 +1,8 @@ -import * as React from "react" -import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" -import { Check, ChevronRight, Circle } from "lucide-react" +import * as React from 'react' +import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu' +import { Check, ChevronRight, Circle } from 'lucide-react' -import { cn } from "@/lib/utils" +import { cn } from '@/lib/utils' const DropdownMenu = DropdownMenuPrimitive.Root @@ -25,9 +25,9 @@ const DropdownMenuSubTrigger = React.forwardRef< @@ -45,8 +45,8 @@ const DropdownMenuSubContent = React.forwardRef< @@ -63,8 +63,8 @@ const DropdownMenuContent = React.forwardRef< ref={ref} sideOffset={sideOffset} className={cn( - "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", - className + 'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', + className, )} {...props} /> @@ -81,9 +81,9 @@ const DropdownMenuItem = React.forwardRef< @@ -97,8 +97,8 @@ const DropdownMenuCheckboxItem = React.forwardRef< @@ -145,9 +145,9 @@ const DropdownMenuLabel = React.forwardRef< @@ -160,7 +160,7 @@ const DropdownMenuSeparator = React.forwardRef< >(({ className, ...props }, ref) => ( )) @@ -172,12 +172,12 @@ const DropdownMenuShortcut = ({ }: React.HTMLAttributes) => { return ( ) } -DropdownMenuShortcut.displayName = "DropdownMenuShortcut" +DropdownMenuShortcut.displayName = 'DropdownMenuShortcut' export { DropdownMenu, diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx index 68551b9..b508d5a 100644 --- a/src/components/ui/input.tsx +++ b/src/components/ui/input.tsx @@ -1,22 +1,22 @@ -import * as React from "react" +import * as React from 'react' -import { cn } from "@/lib/utils" +import { cn } from '@/lib/utils' -const Input = React.forwardRef>( +const Input = React.forwardRef>( ({ className, type, ...props }, ref) => { return ( ) - } + }, ) -Input.displayName = "Input" +Input.displayName = 'Input' export { Input } diff --git a/src/components/ui/pagination.tsx b/src/components/ui/pagination.tsx index ea40d19..7e78ef6 100644 --- a/src/components/ui/pagination.tsx +++ b/src/components/ui/pagination.tsx @@ -1,63 +1,62 @@ -import * as React from "react" -import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react" +import * as React from 'react' +import { ChevronLeft, ChevronRight, MoreHorizontal } from 'lucide-react' -import { cn } from "@/lib/utils" -import { ButtonProps, buttonVariants } from "@/components/ui/button" +import { cn } from '@/lib/utils' +import { type ButtonProps, buttonVariants } from '@/components/ui/button' -const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => ( +const Pagination = ({ className, ...props }: React.ComponentProps<'nav'>) => (