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 = () => {
- );
+ )
}
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({
- );
+ )
}
diff --git a/src/components/TableUser/data-table-row-actions.tsx b/src/components/TableUser/data-table-row-actions.tsx
index c0ae5d4..f1dc23b 100644
--- a/src/components/TableUser/data-table-row-actions.tsx
+++ b/src/components/TableUser/data-table-row-actions.tsx
@@ -1,28 +1,33 @@
-'use client';
+'use client'
-import type { Row } from '@tanstack/react-table';
-import { ActivityIcon, ExternalLink, MoreHorizontal, Share2Icon, } from 'lucide-react';
+import type { Row } from '@tanstack/react-table'
+import {
+ ActivityIcon,
+ ExternalLink,
+ MoreHorizontal,
+ Share2Icon,
+} from 'lucide-react'
-import type { User } from '@/lib/api';
-import { shareToSocial } from '@/lib/utils';
-import { Link } from 'react-router-dom';
-import { Button } from '../ui/button';
+import type { User } from '@/lib/api'
+import { shareToSocial } from '@/lib/utils'
+import { Link } from 'react-router-dom'
+import { Button } from '../ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuShortcut,
DropdownMenuTrigger,
-} from '../ui/dropdown-menu';
+} from '../ui/dropdown-menu'
interface DataTableRowActionsProps {
- row: Row;
+ row: Row
}
export function DataTableRowActions({
row,
}: DataTableRowActionsProps) {
- const user = row.original as User;
+ const user = row.original as User
return (
@@ -38,7 +43,7 @@ export function DataTableRowActions({
{
- shareToSocial(user);
+ shareToSocial(user)
}}
>
Share
@@ -68,5 +73,5 @@ export function DataTableRowActions({
- );
+ )
}
diff --git a/src/components/TableUser/data-table-toolbar.tsx b/src/components/TableUser/data-table-toolbar.tsx
index 3eba991..484585d 100644
--- a/src/components/TableUser/data-table-toolbar.tsx
+++ b/src/components/TableUser/data-table-toolbar.tsx
@@ -1,45 +1,66 @@
-'use client';
+'use client'
-import type { Table } from '@tanstack/react-table';
+import type { Table } from '@tanstack/react-table'
-import { cn } from '@/lib/utils';
-import { ActivityIcon, ArrowDownNarrowWideIcon, UserRoundCheckIcon } from 'lucide-react';
-import { Button } from '../ui/button';
+import { cn } from '@/lib/utils'
+import {
+ ActivityIcon,
+ ArrowDownNarrowWideIcon,
+ UserRoundCheckIcon,
+} from 'lucide-react'
+import { Button } from '../ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
-} from '../ui/dropdown-menu';
-import { Input } from '../ui/input';
-import { DataTableViewOptions } from './data-table-view-options';
+} from '../ui/dropdown-menu'
+import { Input } from '../ui/input'
+import { DataTableViewOptions } from './data-table-view-options'
+import type { SetFilterParams, SetSortParams } from './types'
interface DataTableToolbarProps {
- table: Table;
- withTableViewOptions?: boolean;
- withSortOptions?: boolean;
+ table: Table
+ withTableViewOptions?: boolean
+ withSortOptions?: boolean
+ filterValue: string
+ setFilterParams: SetFilterParams
+ setSortParams: SetSortParams
}
export function DataTableToolbar({
table,
withTableViewOptions,
withSortOptions,
+ setFilterParams,
+ filterValue,
+ setSortParams,
}: DataTableToolbarProps) {
+ const clientFilterVal = table
+ ?.getColumn('username')
+ ?.getFilterValue() as string
+
+ const val: string = clientFilterVal ? clientFilterVal : filterValue
+
return (
- 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 (
-
+
View
@@ -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'>) => (
)
-Pagination.displayName = "Pagination"
+Pagination.displayName = 'Pagination'
const PaginationContent = React.forwardRef<
HTMLUListElement,
- React.ComponentProps<"ul">
+ React.ComponentProps<'ul'>
>(({ className, ...props }, ref) => (
))
-PaginationContent.displayName = "PaginationContent"
+PaginationContent.displayName = 'PaginationContent'
const PaginationItem = React.forwardRef<
HTMLLIElement,
- React.ComponentProps<"li">
+ React.ComponentProps<'li'>
>(({ className, ...props }, ref) => (
-
+
))
-PaginationItem.displayName = "PaginationItem"
+PaginationItem.displayName = 'PaginationItem'
type PaginationLinkProps = {
isActive?: boolean
-} & Pick &
- React.ComponentProps<"a">
+} & Pick &
+ React.ComponentProps<'a'>
const PaginationLink = ({
className,
isActive,
- size = "icon",
+ size = 'icon',
...props
}: PaginationLinkProps) => (
)
-PaginationLink.displayName = "PaginationLink"
+PaginationLink.displayName = 'PaginationLink'
const PaginationPrevious = ({
className,
@@ -66,14 +65,14 @@ const PaginationPrevious = ({
Previous
)
-PaginationPrevious.displayName = "PaginationPrevious"
+PaginationPrevious.displayName = 'PaginationPrevious'
const PaginationNext = ({
className,
@@ -82,29 +81,29 @@ const PaginationNext = ({
Next
)
-PaginationNext.displayName = "PaginationNext"
+PaginationNext.displayName = 'PaginationNext'
const PaginationEllipsis = ({
className,
...props
-}: React.ComponentProps<"span">) => (
+}: React.ComponentProps<'span'>) => (
More pages
)
-PaginationEllipsis.displayName = "PaginationEllipsis"
+PaginationEllipsis.displayName = 'PaginationEllipsis'
export {
Pagination,
diff --git a/src/components/ui/popover.tsx b/src/components/ui/popover.tsx
index bbba7e0..9c46baf 100644
--- a/src/components/ui/popover.tsx
+++ b/src/components/ui/popover.tsx
@@ -1,7 +1,7 @@
-import * as React from "react"
-import * as PopoverPrimitive from "@radix-ui/react-popover"
+import * as React from 'react'
+import * as PopoverPrimitive from '@radix-ui/react-popover'
-import { cn } from "@/lib/utils"
+import { cn } from '@/lib/utils'
const Popover = PopoverPrimitive.Root
@@ -10,15 +10,15 @@ const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverContent = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
->(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
+>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
diff --git a/src/components/ui/progress.tsx b/src/components/ui/progress.tsx
index 105fb65..dbafda0 100644
--- a/src/components/ui/progress.tsx
+++ b/src/components/ui/progress.tsx
@@ -1,7 +1,7 @@
-import * as React from "react"
-import * as ProgressPrimitive from "@radix-ui/react-progress"
+import * as React from 'react'
+import * as ProgressPrimitive from '@radix-ui/react-progress'
-import { cn } from "@/lib/utils"
+import { cn } from '@/lib/utils'
const Progress = React.forwardRef<
React.ElementRef,
@@ -10,8 +10,8 @@ const Progress = React.forwardRef<
diff --git a/src/components/ui/rainbow-button.tsx b/src/components/ui/rainbow-button.tsx
index 299fb03..5d12c31 100644
--- a/src/components/ui/rainbow-button.tsx
+++ b/src/components/ui/rainbow-button.tsx
@@ -1,6 +1,6 @@
-import React from "react";
+import type React from 'react'
-import { cn } from "@/lib/utils";
+import { cn } from '@/lib/utils'
interface RainbowButtonProps
extends React.ButtonHTMLAttributes {}
@@ -12,16 +12,16 @@ export function RainbowButton({
return (
{children}
- );
+ )
}
diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx
index fe56d4d..699fa43 100644
--- a/src/components/ui/select.tsx
+++ b/src/components/ui/select.tsx
@@ -1,8 +1,8 @@
-import * as React from "react"
-import * as SelectPrimitive from "@radix-ui/react-select"
-import { Check, ChevronDown, ChevronUp } from "lucide-react"
+import * as React from 'react'
+import * as SelectPrimitive from '@radix-ui/react-select'
+import { Check, ChevronDown, ChevronUp } from 'lucide-react'
-import { cn } from "@/lib/utils"
+import { cn } from '@/lib/utils'
const Select = SelectPrimitive.Root
@@ -17,8 +17,8 @@ const SelectTrigger = React.forwardRef<
span]:line-clamp-1",
- className
+ 'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
+ className,
)}
{...props}
>
@@ -37,8 +37,8 @@ const SelectScrollUpButton = React.forwardRef<
@@ -54,8 +54,8 @@ const SelectScrollDownButton = React.forwardRef<
@@ -68,15 +68,15 @@ SelectScrollDownButton.displayName =
const SelectContent = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
->(({ className, children, position = "popper", ...props }, ref) => (
+>(({ className, children, position = 'popper', ...props }, ref) => (
{children}
@@ -103,7 +103,7 @@ const SelectLabel = React.forwardRef<
>(({ className, ...props }, ref) => (
))
@@ -116,8 +116,8 @@ const SelectItem = React.forwardRef<
@@ -138,7 +138,7 @@ const SelectSeparator = React.forwardRef<
>(({ className, ...props }, ref) => (
))
diff --git a/src/components/ui/shimmer-button.tsx b/src/components/ui/shimmer-button.tsx
index b49dcdd..d827f27 100644
--- a/src/components/ui/shimmer-button.tsx
+++ b/src/components/ui/shimmer-button.tsx
@@ -1,26 +1,26 @@
-import React, { CSSProperties } from "react";
+import React, { type CSSProperties } from 'react'
-import { cn } from "@/lib/utils";
+import { cn } from '@/lib/utils'
export interface ShimmerButtonProps
extends React.ButtonHTMLAttributes {
- shimmerColor?: string;
- shimmerSize?: string;
- borderRadius?: string;
- shimmerDuration?: string;
- background?: string;
- className?: string;
- children?: React.ReactNode;
+ shimmerColor?: string
+ shimmerSize?: string
+ borderRadius?: string
+ shimmerDuration?: string
+ background?: string
+ className?: string
+ children?: React.ReactNode
}
const ShimmerButton = React.forwardRef(
(
{
- shimmerColor = "#ffffff",
- shimmerSize = "0.05em",
- shimmerDuration = "3s",
- borderRadius = "100px",
- background = "rgba(0, 0, 0, 1)",
+ shimmerColor = '#ffffff',
+ shimmerSize = '0.05em',
+ shimmerDuration = '3s',
+ borderRadius = '100px',
+ background = 'rgba(0, 0, 0, 1)',
className,
children,
...props
@@ -31,17 +31,17 @@ const ShimmerButton = React.forwardRef(
(
{/* spark container */}
{/* spark */}
@@ -65,32 +65,32 @@ const ShimmerButton = React.forwardRef
(
{/* Highlight */}
{/* backdrop */}
- );
+ )
},
-);
+)
-ShimmerButton.displayName = "ShimmerButton";
+ShimmerButton.displayName = 'ShimmerButton'
-export default ShimmerButton;
+export default ShimmerButton
diff --git a/src/components/ui/skeleton.tsx b/src/components/ui/skeleton.tsx
index 01b8b6d..c23a30d 100644
--- a/src/components/ui/skeleton.tsx
+++ b/src/components/ui/skeleton.tsx
@@ -1,4 +1,4 @@
-import { cn } from "@/lib/utils"
+import { cn } from '@/lib/utils'
function Skeleton({
className,
@@ -6,7 +6,7 @@ function Skeleton({
}: React.HTMLAttributes) {
return (
)
diff --git a/src/components/ui/table.tsx b/src/components/ui/table.tsx
index 7f3502f..97ef8eb 100644
--- a/src/components/ui/table.tsx
+++ b/src/components/ui/table.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 Table = React.forwardRef<
HTMLTableElement,
@@ -9,20 +9,20 @@ const Table = React.forwardRef<
))
-Table.displayName = "Table"
+Table.displayName = 'Table'
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
-
+
))
-TableHeader.displayName = "TableHeader"
+TableHeader.displayName = 'TableHeader'
const TableBody = React.forwardRef<
HTMLTableSectionElement,
@@ -30,11 +30,11 @@ const TableBody = React.forwardRef<
>(({ className, ...props }, ref) => (
))
-TableBody.displayName = "TableBody"
+TableBody.displayName = 'TableBody'
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
@@ -43,13 +43,13 @@ const TableFooter = React.forwardRef<
tr]:last:border-b-0",
- className
+ 'border-t bg-muted/50 font-medium [&>tr]:last:border-b-0',
+ className,
)}
{...props}
/>
))
-TableFooter.displayName = "TableFooter"
+TableFooter.displayName = 'TableFooter'
const TableRow = React.forwardRef<
HTMLTableRowElement,
@@ -58,13 +58,13 @@ const TableRow = React.forwardRef<
))
-TableRow.displayName = "TableRow"
+TableRow.displayName = 'TableRow'
const TableHead = React.forwardRef<
HTMLTableCellElement,
@@ -73,13 +73,13 @@ const TableHead = React.forwardRef<
|
))
-TableHead.displayName = "TableHead"
+TableHead.displayName = 'TableHead'
const TableCell = React.forwardRef<
HTMLTableCellElement,
@@ -87,11 +87,11 @@ const TableCell = React.forwardRef<
>(({ className, ...props }, ref) => (
|
))
-TableCell.displayName = "TableCell"
+TableCell.displayName = 'TableCell'
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
@@ -99,11 +99,11 @@ const TableCaption = React.forwardRef<
>(({ className, ...props }, ref) => (
))
-TableCaption.displayName = "TableCaption"
+TableCaption.displayName = 'TableCaption'
export {
Table,
diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx
index f57fffd..3f810a1 100644
--- a/src/components/ui/tabs.tsx
+++ b/src/components/ui/tabs.tsx
@@ -1,7 +1,7 @@
-import * as React from "react"
-import * as TabsPrimitive from "@radix-ui/react-tabs"
+import * as React from 'react'
+import * as TabsPrimitive from '@radix-ui/react-tabs'
-import { cn } from "@/lib/utils"
+import { cn } from '@/lib/utils'
const Tabs = TabsPrimitive.Root
@@ -12,8 +12,8 @@ const TabsList = React.forwardRef<
@@ -27,8 +27,8 @@ const TabsTrigger = React.forwardRef<
@@ -42,8 +42,8 @@ const TabsContent = React.forwardRef<
diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx
index e121f0a..8287469 100644
--- a/src/components/ui/tooltip.tsx
+++ b/src/components/ui/tooltip.tsx
@@ -1,7 +1,7 @@
-import * as React from "react"
-import * as TooltipPrimitive from "@radix-ui/react-tooltip"
+import * as React from 'react'
+import * as TooltipPrimitive from '@radix-ui/react-tooltip'
-import { cn } from "@/lib/utils"
+import { cn } from '@/lib/utils'
const TooltipProvider = TooltipPrimitive.Provider
@@ -17,8 +17,8 @@ const TooltipContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
- "z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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}
/>
diff --git a/src/lib/api.ts b/src/lib/api.ts
index e911605..a8483c1 100644
--- a/src/lib/api.ts
+++ b/src/lib/api.ts
@@ -1,53 +1,50 @@
-import useSWR from 'swr/immutable';
+import useSWR from 'swr/immutable'
// @ts-ignore
-export const fetcher = (...args) => fetch(...args).then((res) => res.json());
+export const fetcher = (...args) => fetch(...args).then((res) => res.json())
const ENDPOINT = {
MOST_ACTIVE_USERS:
'https://raw.githubusercontent.com/depapp/most-active-github-users-counter/master/indogithubers.json',
LAST_UPDATED_DATE:
'https://api.github.com/repos/depapp/most-active-github-users-counter/commits?path=indogithubers.json&per_page=1',
-};
+}
export interface User {
- avatarUrl: string;
- company: string;
- contributionRank: number;
- contributions: number;
- followerRank: number;
- followers: number;
- name: string;
- username: string;
+ avatarUrl: string
+ company: string
+ contributionRank: number
+ contributions: number
+ followerRank: number
+ followers: number
+ name: string
+ username: string
}
export interface MostActiveResponse {
- MinimumFollowerCount: number;
- users: User[];
+ MinimumFollowerCount: number
+ users: User[]
}
export const useMostActiveUsers = () => {
const { data, error, isLoading } = useSWR(
ENDPOINT.MOST_ACTIVE_USERS,
- fetcher
- );
+ fetcher,
+ )
return {
data,
isLoading,
isError: error,
- };
-};
+ }
+}
export const useLatestUpdate = () => {
- const { data, error, isLoading } = useSWR(
- ENDPOINT.LAST_UPDATED_DATE,
- fetcher
- );
+ const { data, error, isLoading } = useSWR(ENDPOINT.LAST_UPDATED_DATE, fetcher)
return {
data,
isLoading,
isError: error,
- };
-};
+ }
+}
diff --git a/src/lib/cache.ts b/src/lib/cache.ts
index a2b392b..1580f12 100644
--- a/src/lib/cache.ts
+++ b/src/lib/cache.ts
@@ -1,34 +1,36 @@
-import type { Cache, State } from "swr";
+import type { Cache, State } from 'swr'
-
-const CACHE_LIFETIME = 1_000 * 60 * 60 * 24;
+const CACHE_LIFETIME = 1_000 * 60 * 60 * 24
export const localCache = (): Cache => {
- const map = new Map, timestamp: number }>(JSON.parse(localStorage.getItem('swr-cache') || '[]'));
-
- window.addEventListener('beforeunload', () => {
- const cache = Array.from(map.entries());
-
- localStorage.setItem('swr-cache', JSON.stringify(cache));
- })
-
- return {
- keys: () => map.keys(),
- get: (key: string) => {
- const value = map.get(key);
- if (!value) {
- return undefined;
- }
-
- const { data, timestamp } = value;
- if (Date.now() - timestamp > CACHE_LIFETIME) {
- map.delete(key);
- return undefined;
- }
-
- return data;
- },
- set: (key: string, value: State) => map.set(key, { data: value, timestamp: Date.now() }),
- delete: (key: string) => map.delete(key),
- };
+ const map = new Map; timestamp: number }>(
+ JSON.parse(localStorage.getItem('swr-cache') || '[]'),
+ )
+
+ window.addEventListener('beforeunload', () => {
+ const cache = Array.from(map.entries())
+
+ localStorage.setItem('swr-cache', JSON.stringify(cache))
+ })
+
+ return {
+ keys: () => map.keys(),
+ get: (key: string) => {
+ const value = map.get(key)
+ if (!value) {
+ return undefined
+ }
+
+ const { data, timestamp } = value
+ if (Date.now() - timestamp > CACHE_LIFETIME) {
+ map.delete(key)
+ return undefined
+ }
+
+ return data
+ },
+ set: (key: string, value: State) =>
+ map.set(key, { data: value, timestamp: Date.now() }),
+ delete: (key: string) => map.delete(key),
+ }
}
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index 193c60c..02392a9 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -1,56 +1,56 @@
-import { type ClassValue, clsx } from 'clsx';
-import { twMerge } from 'tailwind-merge';
-import type { User } from './api';
+import { type ClassValue, clsx } from 'clsx'
+import { twMerge } from 'tailwind-merge'
+import type { User } from './api'
export function cn(...inputs: ClassValue[]) {
- return twMerge(clsx(inputs));
+ return twMerge(clsx(inputs))
}
export const range = (start: number, stop: number, step = 1) => {
return Array.from(
{ length: (stop - start) / step + 1 },
- (_, i) => start + i * step
- );
-};
+ (_, i) => start + i * step,
+ )
+}
export const formatNumber = (value: number) =>
- new Intl.NumberFormat('id-ID', {}).format(value);
+ new Intl.NumberFormat('id-ID', {}).format(value)
export const formatLastUpdated = (date?: Date) => {
if (!date) {
- return '';
+ return ''
}
- const today = new Date();
- const yesterday = new Date();
- yesterday.setDate(yesterday.getDate() - 1);
- const twoDaysAgo = new Date();
- twoDaysAgo.setDate(twoDaysAgo.getDate() - 2);
+ const today = new Date()
+ const yesterday = new Date()
+ yesterday.setDate(yesterday.getDate() - 1)
+ const twoDaysAgo = new Date()
+ twoDaysAgo.setDate(twoDaysAgo.getDate() - 2)
- const isToday = date.toDateString() === today.toDateString();
- const isYesterday = date.toDateString() === yesterday.toDateString();
- const isTwoDaysAgo = date.toDateString() === twoDaysAgo.toDateString();
+ const isToday = date.toDateString() === today.toDateString()
+ const isYesterday = date.toDateString() === yesterday.toDateString()
+ const isTwoDaysAgo = date.toDateString() === twoDaysAgo.toDateString()
const formattedTime = date.toLocaleTimeString(undefined, {
hour: '2-digit',
minute: '2-digit',
hour12: false,
- });
+ })
if (isToday) {
- return `Today at ${formattedTime} WIB`;
+ return `Today at ${formattedTime} WIB`
}
if (isYesterday) {
- return `Yesterday at ${formattedTime} WIB`;
+ return `Yesterday at ${formattedTime} WIB`
}
if (isTwoDaysAgo) {
- return `2 Days Ago at ${formattedTime} WIB`;
+ return `2 Days Ago at ${formattedTime} WIB`
}
- return date.toLocaleDateString();
-};
+ return date.toLocaleDateString()
+}
// Taken from: https://stackoverflow.com/a/66239174
export const makeInitial = (name: string) => {
@@ -59,23 +59,23 @@ export const makeInitial = (name: string) => {
.replace(/[^\x00-\x7F]/g, '')
.trim()
// Split by either dot, space or dash chars
- .split(/\.|-|\s+/);
+ .split(/\.|-|\s+/)
if (allNames.length === 1) {
- return allNames[0].substring(0, 2).toUpperCase();
+ return allNames[0].substring(0, 2).toUpperCase()
}
const initials = allNames.reduce((acc, curr, index) => {
if (index === 0 || index === allNames.length - 1) {
- acc = `${acc}${curr.charAt(0).toUpperCase()}`;
+ acc = `${acc}${curr.charAt(0).toUpperCase()}`
}
- return acc;
- }, '');
+ return acc
+ }, '')
// Max is 3 chars
- return initials.substring(0, 3);
-};
+ return initials.substring(0, 3)
+}
export const shareToSocial = (user: User) => {
const shareData = {
@@ -83,20 +83,21 @@ export const shareToSocial = (user: User) => {
text: `Hey, checkout my GitHub stats:\n\nUsername: ${user.username}\n\n🏅\nFollowers Rank: #${user.followerRank}\nContribution Rank: #${user.contributionRank}\n\n🏆\nTotal Followers: ${user.followers}\nTotal Contribution: ${user.contributions}\n\nGo check yours at https://indogithubers.vercel.app/u/${user.username} #IndoGitHubers`,
url: `https://indogithubers.vercel.app/u/${user.username}`,
}
+
if (navigator.canShare(shareData)) {
- return navigator.share(shareData);
- } else {
- const url = `https://twitter.com/intent/tweet?text=${encodeURIComponent(
- shareData.text
- )}`;
- window.open(url, '_blank');
+ return navigator.share(shareData)
}
-};
+
+ const url = `https://twitter.com/intent/tweet?text=${encodeURIComponent(
+ shareData.text,
+ )}`
+ window.open(url, '_blank')
+}
export async function copyTextToClipboard(text: string) {
if ('clipboard' in navigator) {
- return await navigator.clipboard.writeText(text);
+ return await navigator.clipboard.writeText(text)
}
- return document.execCommand('copy', true, text);
-}
\ No newline at end of file
+ return document.execCommand('copy', true, text)
+}
diff --git a/src/main.tsx b/src/main.tsx
index 325795f..21ff9ef 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -1,14 +1,14 @@
// @ts-ignore
-import './index.css';
+import './index.css'
import { NuqsAdapter } from 'nuqs/adapters/react-router'
-import React from 'react';
-import { createRoot } from 'react-dom/client';
+import React from 'react'
+import { createRoot } from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
-import App from './App';
-import { ThemeProvider } from './components/theme-provider';
+import App from './App'
+import { ThemeProvider } from './components/theme-provider'
-const domNode = document.getElementById('root') as HTMLElement;
+const domNode = document.getElementById('root') as HTMLElement
createRoot(domNode).render(
@@ -19,5 +19,5 @@ createRoot(domNode).render(
-
-);
\ No newline at end of file
+ ,
+)
diff --git a/src/views/About.tsx b/src/views/About.tsx
index 3b0f136..92bd7ec 100644
--- a/src/views/About.tsx
+++ b/src/views/About.tsx
@@ -1,3 +1,3 @@
export const About = () => {
- return Welcome to About
;
-};
+ return Welcome to About
+}
diff --git a/src/views/Detail.tsx b/src/views/Detail.tsx
index 383f9fd..2020431 100644
--- a/src/views/Detail.tsx
+++ b/src/views/Detail.tsx
@@ -1,20 +1,23 @@
-import { CopyButton } from '@/components/CopyButton';
-import { EmptyState } from '@/components/EmptyState';
-import { GhCalendar } from '@/components/GhCalendar';
-import { Spinner } from '@/components/Spinner';
-import { DEFAULT_CLASSNAMES_RANK, renderRank } from '@/components/TableUser/column';
-import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
-import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
-import { Button } from '@/components/ui/button';
+import { CopyButton } from '@/components/CopyButton'
+import { EmptyState } from '@/components/EmptyState'
+import { GhCalendar } from '@/components/GhCalendar'
+import { Spinner } from '@/components/Spinner'
+import {
+ DEFAULT_CLASSNAMES_RANK,
+ renderRank,
+} from '@/components/TableUser/column'
+import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
+import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
+import { Button } from '@/components/ui/button'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
-} from '@/components/ui/select';
-import { useMostActiveUsers } from '@/lib/api';
-import { formatNumber, makeInitial, shareToSocial } from '@/lib/utils';
+} from '@/components/ui/select'
+import { useMostActiveUsers } from '@/lib/api'
+import { formatNumber, makeInitial, shareToSocial } from '@/lib/utils'
import {
ActivityIcon,
ArrowLeftIcon,
@@ -23,33 +26,33 @@ import {
InfoIcon,
Share2Icon,
UserCheck2Icon,
-} from 'lucide-react';
-import { useState } from 'react';
-import { Link, useParams } from 'react-router-dom';
+} from 'lucide-react'
+import { useState } from 'react'
+import { Link, useParams } from 'react-router-dom'
export const Detail = () => {
- const { data, isLoading, isError } = useMostActiveUsers();
- const { username } = useParams();
+ const { data, isLoading, isError } = useMostActiveUsers()
+ const { username } = useParams()
- const [badgeType, setBadgeType] = useState('markdown');
- const [styleType, setStyleType] = useState('flat');
+ const [badgeType, setBadgeType] = useState('markdown')
+ const [styleType, setStyleType] = useState('flat')
if (isLoading)
return (
- );
+ )
if (isError)
return (
- );
+ )
- const currentUser = data?.users?.find((u) => u.username === username);
+ const currentUser = data?.users?.find((u) => u.username === username)
if (!currentUser) {
- return ;
+ return
}
return (
@@ -86,7 +89,7 @@ export const Detail = () => {
{renderRank(
currentUser?.contributionRank,
- DEFAULT_CLASSNAMES_RANK
+ DEFAULT_CLASSNAMES_RANK,
)}{' '}
• {formatNumber(currentUser?.contributions)}
@@ -109,7 +112,7 @@ export const Detail = () => {
variant="outline"
size="icon"
onClick={() => {
- shareToSocial(currentUser);
+ shareToSocial(currentUser)
}}
>
@@ -180,7 +183,7 @@ export const Detail = () => {
- );
-};
+ )
+}
diff --git a/src/views/Error.tsx b/src/views/Error.tsx
index df015e1..1e06038 100644
--- a/src/views/Error.tsx
+++ b/src/views/Error.tsx
@@ -1,8 +1,8 @@
-import { Link, useRouteError } from 'react-router-dom';
+import { Link, useRouteError } from 'react-router-dom'
export default function ErrorPage() {
- const error = useRouteError() as Error;
- console.error(error);
+ const error = useRouteError() as Error
+ console.error(error)
return (
@@ -14,5 +14,5 @@ export default function ErrorPage() {
Go to the home page
- );
+ )
}
diff --git a/src/views/Home.tsx b/src/views/Home.tsx
index fa4c41c..22c468d 100644
--- a/src/views/Home.tsx
+++ b/src/views/Home.tsx
@@ -1,48 +1,71 @@
-import { EmptyState } from '@/components/EmptyState';
-import { Spinner } from '@/components/Spinner';
-import { CardUsers } from '@/components/TableUser/card-users';
-import { columnsDesktop, columnsMobile } from '@/components/TableUser/column';
-import { DataTable } from '@/components/TableUser/data-table';
-import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion';
-import { } from '@/components/ui/collapsible';
-import DotPattern from '@/components/ui/dot-pattern';
-import ShimmerButton from '@/components/ui/shimmer-button';
-import { useLatestUpdate, useMostActiveUsers } from '@/lib/api';
-import { cn, } from '@/lib/utils';
-import { useMediaQuery } from 'usehooks-ts';
+import { EmptyState } from '@/components/EmptyState'
+import { Spinner } from '@/components/Spinner'
+import { CardUsers } from '@/components/TableUser/card-users'
+import { columnsDesktop, columnsMobile } from '@/components/TableUser/column'
+import { DataTable } from '@/components/TableUser/data-table'
+import { useFilterSearchParams } from '@/components/TableUser/search-params.filter'
+import { usePaginationSearchParams } from '@/components/TableUser/search-params.pagination'
+import { useSortingSearchParams } from '@/components/TableUser/search-params.sorting'
+import {
+ Accordion,
+ AccordionContent,
+ AccordionItem,
+ AccordionTrigger,
+} from '@/components/ui/accordion'
+import {} from '@/components/ui/collapsible'
+import DotPattern from '@/components/ui/dot-pattern'
+import ShimmerButton from '@/components/ui/shimmer-button'
+import { useLatestUpdate, useMostActiveUsers } from '@/lib/api'
+import { cn } from '@/lib/utils'
+import { useMediaQuery } from 'usehooks-ts'
export const Home = () => {
- const isMd = useMediaQuery('(min-width: 768px)');
- const { data, isLoading, isError } = useMostActiveUsers();
- const { data: lastUpdatedData } = useLatestUpdate();
+ const isMd = useMediaQuery('(min-width: 768px)')
+ const [{ pageIndex, pageSize }, setPageParams] = usePaginationSearchParams()
+ const [{ filterBy, filterValue }, setFilterParams] = useFilterSearchParams()
+ const [{ sortBy, sortDir }, setSortParams] = useSortingSearchParams()
+
+ const { data, isLoading, isError } = useMostActiveUsers()
+ const { data: lastUpdatedData } = useLatestUpdate()
if (isLoading)
return (
- );
+ )
if (isError)
return (
-
- );
+
+ )
+
+ const tableProps = {
+ data: data?.users || [],
+ updatedAt: new Date(lastUpdatedData?.[0]?.commit?.committer?.date),
+ // Paginations
+ pageIndex,
+ pageSize,
+ setPageParams,
+ // Filters
+ filterBy,
+ filterValue,
+ setFilterParams,
+ // Sort
+ sortBy,
+ sortDir,
+ setSortParams,
+ }
return (
- {isMd ? (
-
- ) : (
-
- )}
+
+ {isMd ? (
+
+ ) : (
+
+ )}
+
@@ -94,7 +117,7 @@ export const Home = () => {
@@ -113,5 +136,5 @@ export const Home = () => {
- );
-};
+ )
+}
diff --git a/tailwind.config.js b/tailwind.config.js
index 6b5f06a..47280a4 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -3,112 +3,113 @@ module.exports = {
darkMode: ['class'],
content: ['./index.html', './src/**/*.{ts,tsx,js,jsx}'],
theme: {
- extend: {
- borderRadius: {
- lg: 'var(--radius)',
- md: 'calc(var(--radius) - 2px)',
- sm: 'calc(var(--radius) - 4px)'
- },
- colors: {
- background: 'hsl(var(--background))',
- foreground: 'hsl(var(--foreground))',
- card: {
- DEFAULT: 'hsl(var(--card))',
- foreground: 'hsl(var(--card-foreground))'
- },
- popover: {
- DEFAULT: 'hsl(var(--popover))',
- foreground: 'hsl(var(--popover-foreground))'
- },
- primary: {
- DEFAULT: 'hsl(var(--primary))',
- foreground: 'hsl(var(--primary-foreground))'
- },
- secondary: {
- DEFAULT: 'hsl(var(--secondary))',
- foreground: 'hsl(var(--secondary-foreground))'
- },
- muted: {
- DEFAULT: 'hsl(var(--muted))',
- foreground: 'hsl(var(--muted-foreground))'
- },
- accent: {
- DEFAULT: 'hsl(var(--accent))',
- foreground: 'hsl(var(--accent-foreground))'
- },
- destructive: {
- DEFAULT: 'hsl(var(--destructive))',
- foreground: 'hsl(var(--destructive-foreground))'
- },
- border: 'hsl(var(--border))',
- input: 'hsl(var(--input))',
- ring: 'hsl(var(--ring))',
- chart: {
- '1': 'hsl(var(--chart-1))',
- '2': 'hsl(var(--chart-2))',
- '3': 'hsl(var(--chart-3))',
- '4': 'hsl(var(--chart-4))',
- '5': 'hsl(var(--chart-5))'
- },
- 'color-1': 'hsl(var(--color-1))',
- 'color-2': 'hsl(var(--color-2))',
- 'color-3': 'hsl(var(--color-3))',
- 'color-4': 'hsl(var(--color-4))',
- 'color-5': 'hsl(var(--color-5))'
- },
- keyframes: {
- 'accordion-down': {
- from: {
- height: '0'
- },
- to: {
- height: 'var(--radix-accordion-content-height)'
- }
- },
- 'accordion-up': {
- from: {
- height: 'var(--radix-accordion-content-height)'
- },
- to: {
- height: '0'
- }
- },
- rainbow: {
- '0%': {
- 'background-position': '0%'
- },
- '100%': {
- 'background-position': '200%'
- }
- },
- 'shimmer-slide': {
- to: {
- transform: 'translate(calc(100cqw - 100%), 0)'
- }
- },
- 'spin-around': {
- '0%': {
- transform: 'translateZ(0) rotate(0)'
- },
- '15%, 35%': {
- transform: 'translateZ(0) rotate(90deg)'
- },
- '65%, 85%': {
- transform: 'translateZ(0) rotate(270deg)'
- },
- '100%': {
- transform: 'translateZ(0) rotate(360deg)'
- }
- }
- },
- animation: {
- 'accordion-down': 'accordion-down 0.2s ease-out',
- 'accordion-up': 'accordion-up 0.2s ease-out',
- rainbow: 'rainbow var(--speed, 2s) infinite linear',
- 'shimmer-slide': 'shimmer-slide var(--speed) ease-in-out infinite alternate',
- 'spin-around': 'spin-around calc(var(--speed) * 2) infinite linear'
- }
- }
+ extend: {
+ borderRadius: {
+ lg: 'var(--radius)',
+ md: 'calc(var(--radius) - 2px)',
+ sm: 'calc(var(--radius) - 4px)',
+ },
+ colors: {
+ background: 'hsl(var(--background))',
+ foreground: 'hsl(var(--foreground))',
+ card: {
+ DEFAULT: 'hsl(var(--card))',
+ foreground: 'hsl(var(--card-foreground))',
+ },
+ popover: {
+ DEFAULT: 'hsl(var(--popover))',
+ foreground: 'hsl(var(--popover-foreground))',
+ },
+ primary: {
+ DEFAULT: 'hsl(var(--primary))',
+ foreground: 'hsl(var(--primary-foreground))',
+ },
+ secondary: {
+ DEFAULT: 'hsl(var(--secondary))',
+ foreground: 'hsl(var(--secondary-foreground))',
+ },
+ muted: {
+ DEFAULT: 'hsl(var(--muted))',
+ foreground: 'hsl(var(--muted-foreground))',
+ },
+ accent: {
+ DEFAULT: 'hsl(var(--accent))',
+ foreground: 'hsl(var(--accent-foreground))',
+ },
+ destructive: {
+ DEFAULT: 'hsl(var(--destructive))',
+ foreground: 'hsl(var(--destructive-foreground))',
+ },
+ border: 'hsl(var(--border))',
+ input: 'hsl(var(--input))',
+ ring: 'hsl(var(--ring))',
+ chart: {
+ 1: 'hsl(var(--chart-1))',
+ 2: 'hsl(var(--chart-2))',
+ 3: 'hsl(var(--chart-3))',
+ 4: 'hsl(var(--chart-4))',
+ 5: 'hsl(var(--chart-5))',
+ },
+ 'color-1': 'hsl(var(--color-1))',
+ 'color-2': 'hsl(var(--color-2))',
+ 'color-3': 'hsl(var(--color-3))',
+ 'color-4': 'hsl(var(--color-4))',
+ 'color-5': 'hsl(var(--color-5))',
+ },
+ keyframes: {
+ 'accordion-down': {
+ from: {
+ height: '0',
+ },
+ to: {
+ height: 'var(--radix-accordion-content-height)',
+ },
+ },
+ 'accordion-up': {
+ from: {
+ height: 'var(--radix-accordion-content-height)',
+ },
+ to: {
+ height: '0',
+ },
+ },
+ rainbow: {
+ '0%': {
+ 'background-position': '0%',
+ },
+ '100%': {
+ 'background-position': '200%',
+ },
+ },
+ 'shimmer-slide': {
+ to: {
+ transform: 'translate(calc(100cqw - 100%), 0)',
+ },
+ },
+ 'spin-around': {
+ '0%': {
+ transform: 'translateZ(0) rotate(0)',
+ },
+ '15%, 35%': {
+ transform: 'translateZ(0) rotate(90deg)',
+ },
+ '65%, 85%': {
+ transform: 'translateZ(0) rotate(270deg)',
+ },
+ '100%': {
+ transform: 'translateZ(0) rotate(360deg)',
+ },
+ },
+ },
+ animation: {
+ 'accordion-down': 'accordion-down 0.2s ease-out',
+ 'accordion-up': 'accordion-up 0.2s ease-out',
+ rainbow: 'rainbow var(--speed, 2s) infinite linear',
+ 'shimmer-slide':
+ 'shimmer-slide var(--speed) ease-in-out infinite alternate',
+ 'spin-around': 'spin-around calc(var(--speed) * 2) infinite linear',
+ },
+ },
},
plugins: [require('tailwindcss-animate')],
-};
+}
diff --git a/tests/home.spec.ts b/tests/home.spec.ts
index 93215b5..3ebd527 100644
--- a/tests/home.spec.ts
+++ b/tests/home.spec.ts
@@ -1,15 +1,15 @@
-import { expect, test } from '@playwright/test';
-import { HomePage } from './models/home.page';
+import { expect, test } from '@playwright/test'
+import { HomePage } from './models/home.page'
test.describe('Homepage', () => {
- let homePage: HomePage;
+ let homePage: HomePage
test.beforeEach(async ({ page }) => {
await test.step('Given navigate to the homepage', async () => {
- homePage = new HomePage(page);
- await homePage.navigate();
- });
- });
+ homePage = new HomePage(page)
+ await homePage.navigate()
+ })
+ })
test(
'should contains expected elements',
@@ -18,24 +18,24 @@ test.describe('Homepage', () => {
},
async ({ page }) => {
await test.step('should has data in the row', async () => {
- await homePage.assertContentInRowIsVisible();
- });
+ await homePage.assertContentInRowIsVisible()
+ })
await test.step('should has last update text', async () => {
- await homePage.assertLastUpdatedTextIsVisible();
- });
+ await homePage.assertLastUpdatedTextIsVisible()
+ })
await test.step('should has faq section', async () => {
- await homePage.assertFaqSectionIsVisible();
- });
+ await homePage.assertFaqSectionIsVisible()
+ })
await test.step('should match homepage visual snapshot', async () => {
await expect(page).toHaveScreenshot('homepage.png', {
maxDiffPixelRatio: 0.04,
- });
- });
- }
- );
+ })
+ })
+ },
+ )
test(
'should validate the search function',
@@ -44,35 +44,35 @@ test.describe('Homepage', () => {
},
async ({ page }) => {
await test.step('When user search with a valid keyword', async () => {
- await homePage.fillAndSearch('depapp');
- });
+ await homePage.fillAndSearch('depapp')
+ })
await test.step('Then table should show expected result', async () => {
- await expect(homePage.getUsername('depapp')).toBeVisible();
- await expect(homePage.emptyState).not.toBeVisible();
- });
+ await expect(homePage.getUsername('depapp')).toBeVisible()
+ await expect(homePage.emptyState).not.toBeVisible()
+ })
await test.step('Then verify the visual from the valid search result page', async () => {
await expect(page).toHaveScreenshot('search-results-valid.png', {
maxDiffPixelRatio: 0.04,
- });
- });
+ })
+ })
await test.step('When user search with non existance keyword', async () => {
- await homePage.fillAndSearch('non-existing-username');
- });
+ await homePage.fillAndSearch('non-existing-username')
+ })
await test.step('Then it should show the empty state', async () => {
- await expect(homePage.emptyState).toBeVisible();
- });
+ await expect(homePage.emptyState).toBeVisible()
+ })
await test.step('Then verify the visual from the empty state', async () => {
await expect(page).toHaveScreenshot('search-results-empty.png', {
maxDiffPixelRatio: 0.04,
- });
- });
- }
- );
+ })
+ })
+ },
+ )
/**
* Sample test case that only run on certain project
@@ -84,36 +84,36 @@ test.describe('Homepage', () => {
tag: ['@smoke', '@desktop'],
},
async ({ isMobile, page }) => {
- test.skip(isMobile, '// NOTE: TEST CASE FOR DESKTOP ONLY');
+ test.skip(isMobile, '// NOTE: TEST CASE FOR DESKTOP ONLY')
await test.step('When user click toggle column button', async () => {
- await expect(homePage.toggleColumnVisibilityBtn).toBeVisible();
- await homePage.toggleColumnVisibilityBtn.click();
- });
+ await expect(homePage.toggleColumnVisibilityBtn).toBeVisible()
+ await homePage.toggleColumnVisibilityBtn.click()
+ })
await test.step('Then validate the visual before changing the visibility', async () => {
await expect(page).toHaveScreenshot('column-visibility-menu.png', {
maxDiffPixelRatio: 0.04,
- });
- });
+ })
+ })
await test.step('When user perform toggle hide column "Name"', async () => {
- await expect(homePage.columnNameCheckbox).toBeVisible();
- await homePage.columnNameCheckbox.click();
- await expect(homePage.columnNameCheckbox).not.toBeVisible();
- });
+ await expect(homePage.columnNameCheckbox).toBeVisible()
+ await homePage.columnNameCheckbox.click()
+ await expect(homePage.columnNameCheckbox).not.toBeVisible()
+ })
await test.step('Then column "Name" should become invisible', async () => {
- await expect(homePage.getName('Sandhika Galih')).not.toBeVisible();
- });
+ await expect(homePage.getName('Sandhika Galih')).not.toBeVisible()
+ })
await test.step('Then validate the visual after column become invisible', async () => {
await expect(page).toHaveScreenshot('column-name-hidden.png', {
maxDiffPixelRatio: 0.04,
- });
- });
- }
- );
+ })
+ })
+ },
+ )
test(
'should sort by contributions on desktop',
@@ -121,37 +121,37 @@ test.describe('Homepage', () => {
tag: ['@smoke', '@desktop'],
},
async ({ isMobile, page }) => {
- test.skip(isMobile, '// NOTE: TEST CASE FOR DESKTOP ONLY');
+ test.skip(isMobile, '// NOTE: TEST CASE FOR DESKTOP ONLY')
await test.step('Should show initial data', async () => {
- await homePage.assertContentInRowIsVisible();
- });
+ await homePage.assertContentInRowIsVisible()
+ })
await test.step('Then validate the initial visually', async () => {
await expect(page).toHaveScreenshot('before-sort.png', {
maxDiffPixelRatio: 0.04,
- });
- });
+ })
+ })
await test.step('When user click header "Contributions"', async () => {
- await homePage.contributionsHeader.click();
- });
+ await homePage.contributionsHeader.click()
+ })
await test.step('And select sort direction Asc', async () => {
- await homePage.sortAscButton.click();
- });
+ await homePage.sortAscButton.click()
+ })
await test.step('Then data row should sort by contributions', async () => {
- await homePage.assertContributionsDesktopAreSorted();
- });
+ await homePage.assertContributionsDesktopAreSorted()
+ })
await test.step('Then verify the visual snapshot', async () => {
await expect(page).toHaveScreenshot('contributions-sorted.png', {
maxDiffPixelRatio: 0.04,
- });
- });
- }
- );
+ })
+ })
+ },
+ )
test(
'should sort by contributions on mobile',
@@ -159,35 +159,35 @@ test.describe('Homepage', () => {
tag: ['@smoke', '@mobile'],
},
async ({ isMobile, page }) => {
- test.skip(!isMobile, '// NOTE: TEST CASE FOR MOBILE ONLY');
+ test.skip(!isMobile, '// NOTE: TEST CASE FOR MOBILE ONLY')
await test.step('Should show initial data', async () => {
- await homePage.assertContentInRowIsVisible();
- });
+ await homePage.assertContentInRowIsVisible()
+ })
await test.step('Then validate the initial visually', async () => {
await expect(page).toHaveScreenshot('before-sort.png', {
maxDiffPixelRatio: 0.04,
- });
- });
+ })
+ })
await test.step('When user click "Sort" button', async () => {
- await homePage.sortButton.click();
- });
+ await homePage.sortButton.click()
+ })
await test.step('And select sort "By Contributions"', async () => {
- await homePage.sortByContributionsButton.click();
- });
+ await homePage.sortByContributionsButton.click()
+ })
await test.step('Then data row should sort by contributions', async () => {
- await expect(homePage.getName('Sandhika Galih')).not.toBeVisible();
- });
+ await expect(homePage.getName('Sandhika Galih')).not.toBeVisible()
+ })
await test.step('Then verify the visual snapshot', async () => {
await expect(page).toHaveScreenshot('contributions-sorted.png', {
maxDiffPixelRatio: 0.04,
- });
- });
- }
- );
-});
+ })
+ })
+ },
+ )
+})
diff --git a/tests/models/home.page.ts b/tests/models/home.page.ts
index ed4ddd9..aa4a916 100644
--- a/tests/models/home.page.ts
+++ b/tests/models/home.page.ts
@@ -1,91 +1,103 @@
-import { type Locator, type Page, expect } from '@playwright/test';
+import { type Locator, type Page, expect } from '@playwright/test'
export class HomePage {
- emptyState: Locator;
- lastUpdate: Locator;
- faqHeading: Locator;
- searchInput: Locator;
- toggleColumnVisibilityBtn: Locator;
- columnNameCheckbox: Locator;
- contributionsHeader: Locator;
- sortButton: Locator;
- sortByContributionsButton: Locator;
- sortAscButton: Locator;
- firstRowContributions: Locator;
+ emptyState: Locator
+ lastUpdate: Locator
+ faqHeading: Locator
+ searchInput: Locator
+ toggleColumnVisibilityBtn: Locator
+ columnNameCheckbox: Locator
+ contributionsHeader: Locator
+ sortButton: Locator
+ sortByContributionsButton: Locator
+ sortAscButton: Locator
+ firstRowContributions: Locator
constructor(private readonly page: Page) {
- this.searchInput = page.getByPlaceholder(/search username/i);
- this.lastUpdate = page.getByText(/last updated at/i);
+ this.searchInput = page.getByPlaceholder(/search username/i)
+ this.lastUpdate = page.getByText(/last updated at/i)
this.faqHeading = page.getByRole('heading', {
name: /frequently asked questions/i,
- });
- this.emptyState = page.getByText(/no results/i);
+ })
+ this.emptyState = page.getByText(/no results/i)
this.toggleColumnVisibilityBtn = page.getByRole('button', {
name: /view/i,
- exact: true
- });
+ exact: true,
+ })
this.columnNameCheckbox = page.getByRole('menuitemcheckbox', {
name: /name/i,
- });
- this.contributionsHeader = page.getByRole('button', { name: /# contributions/i, exact: true });
- this.sortButton = page.getByRole('button', { name: /sort/i });
- this.sortByContributionsButton = page.getByRole('menuitem', { name: /by contributions/i });
- this.sortAscButton = page.getByRole('menuitem', { name: /asc/i });
- this.firstRowContributions = page.locator('tbody tr').first().locator('td').nth(6);
+ })
+ this.contributionsHeader = page.getByRole('button', {
+ name: /# contributions/i,
+ exact: true,
+ })
+ this.sortButton = page.getByRole('button', { name: /sort/i })
+ this.sortByContributionsButton = page.getByRole('menuitem', {
+ name: /by contributions/i,
+ })
+ this.sortAscButton = page.getByRole('menuitem', { name: /asc/i })
+ this.firstRowContributions = page
+ .locator('tbody tr')
+ .first()
+ .locator('td')
+ .nth(6)
}
async navigate() {
- await this.page.goto('/');
+ await this.page.goto('/')
}
async fillAndSearch(username: string) {
- await this.searchInput.fill(username);
+ await this.searchInput.fill(username)
}
getUsername(username: string) {
- return this.page.getByRole('link', { name: username });
+ return this.page.getByRole('link', { name: username })
}
getName(name: string) {
- return this.page.getByText(name);
+ return this.page.getByText(name)
}
async assertContentInRowIsVisible() {
- await expect(this.getUsername('sandhikagalih')).toBeVisible();
- await expect(this.emptyState).not.toBeVisible();
+ await expect(this.getUsername('sandhikagalih')).toBeVisible()
+ await expect(this.emptyState).not.toBeVisible()
}
async assertLastUpdatedTextIsVisible() {
- await expect(this.lastUpdate).toBeVisible();
+ await expect(this.lastUpdate).toBeVisible()
}
async assertFaqSectionIsVisible() {
- await expect(this.faqHeading).toBeVisible();
+ await expect(this.faqHeading).toBeVisible()
}
async sortByContributionsDesktop() {
- await this.contributionsHeader.waitFor({ state: 'visible' });
- await this.contributionsHeader.click();
- await this.sortAscButton.waitFor({ state: 'visible' });
- await this.sortAscButton.click();
+ await this.contributionsHeader.waitFor({ state: 'visible' })
+ await this.contributionsHeader.click()
+ await this.sortAscButton.waitFor({ state: 'visible' })
+ await this.sortAscButton.click()
}
async sortByContributionsMobile() {
- await this.sortButton.waitFor({ state: 'visible' });
- await this.sortButton.click();
- await this.sortByContributionsButton.waitFor({ state: 'visible' });
- await this.sortByContributionsButton.click();
+ await this.sortButton.waitFor({ state: 'visible' })
+ await this.sortButton.click()
+ await this.sortByContributionsButton.waitFor({ state: 'visible' })
+ await this.sortByContributionsButton.click()
}
async assertContributionsDesktopAreSorted() {
// Wait for sorting to complete
- await this.page.waitForTimeout(1000);
+ await this.page.waitForTimeout(1000)
// Take a snapshot of the sorted table
// Verify first row has the lowest contribution count
- await this.firstRowContributions.waitFor({ state: 'visible' });
- const firstContribution = await this.firstRowContributions.textContent();
- const contributionCount = parseInt(firstContribution?.replace('.', '') || '0', 10);
+ await this.firstRowContributions.waitFor({ state: 'visible' })
+ const firstContribution = await this.firstRowContributions.textContent()
+ const contributionCount = Number.parseInt(
+ firstContribution?.replace('.', '') || '0',
+ 10,
+ )
- await expect(contributionCount).toBeGreaterThanOrEqual(100);
+ await expect(contributionCount).toBeGreaterThanOrEqual(100)
}
}
diff --git a/vercel.json b/vercel.json
index 29c0269..1323cda 100644
--- a/vercel.json
+++ b/vercel.json
@@ -5,4 +5,4 @@
"destination": "/index.html"
}
]
-}
\ No newline at end of file
+}
diff --git a/vite.config.ts b/vite.config.ts
index e98d52a..cc45b96 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,7 +1,7 @@
-import path from 'node:path';
-import react from '@vitejs/plugin-react';
-import { defineConfig } from 'vite';
-import { VitePWA } from 'vite-plugin-pwa';
+import path from 'node:path'
+import react from '@vitejs/plugin-react'
+import { defineConfig } from 'vite'
+import { VitePWA } from 'vite-plugin-pwa'
export default defineConfig({
server: {
@@ -70,4 +70,4 @@ export default defineConfig({
'@': path.resolve(__dirname, './src'),
},
},
-});
+})