Skip to content
Merged
5 changes: 5 additions & 0 deletions client/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ export default defineConfig([
message:
'Use Tooltip from @/components/ui/overrides/tooltip',
},
{
name: '@/components/ui/card',
message:
'Use Card from @/components/ui/overrides/card',
},
{
name: 'sonner',
importNames: ['toast'],
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/data-table/DataTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export function DataTable<TData, TValue>({
})

return (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2 md:mt-2 md:gap-3">
{toolbarConfig && (
<DataTableToolbar table={table} config={toolbarConfig} />
)}
Expand Down
73 changes: 51 additions & 22 deletions client/src/components/data-table/DataTablePagination.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,56 +19,85 @@ interface DataTablePaginationProps<TData> {
table: Table<TData>
}

const getDisplayedPagesText = <TData,>(table: Table<TData>) => {
const pageCount = table.getPageCount()
if (pageCount === 0) return `Page 1 of 1`

const pageIndex = table.getState().pagination.pageIndex
return `Page ${String(pageIndex + 1)} of ${String(pageCount)}`
}

const getDisplayedRowsText = <TData,>(table: Table<TData>) => {
const totalRows = table.getFilteredRowModel().rows.length
if (totalRows === 0) return `0 rows`

const pageIndex = table.getState().pagination.pageIndex
const pageSize = table.getState().pagination.pageSize
const startRow = pageIndex * pageSize + 1
const endRow = Math.min((pageIndex + 1) * pageSize, totalRows)

return `${String(startRow)}-${String(endRow)} of ${String(totalRows)} row(s)`
}

export function DataTablePagination<TData>({
table,
}: DataTablePaginationProps<TData>) {
return (
<div className="flex flex-col gap-2 px-2 sm:flex-row sm:items-center sm:justify-between">
{table.getAllColumns().find((column) => column.id === 'select') ? (
<div className="text-sm text-muted-foreground">
{table.getFilteredSelectedRowModel().rows.length} of{' '}
{table.getFilteredRowModel().rows.length} row(s) selected
<div className="flex items-center gap-2 text-xs md:px-4 md:text-sm">
<div>
<div className="text-muted-foreground">
{getDisplayedPagesText(table)}
</div>
<div className="hidden md:flex">
{table
.getAllColumns()
.find((column) => column.id === 'select') ? (
<div className="text-muted-foreground">
{table.getFilteredSelectedRowModel().rows.length} of{' '}
{table.getFilteredRowModel().rows.length} row(s)
selected
</div>
) : (
<div className="text-muted-foreground">
{getDisplayedRowsText(table)}
</div>
)}
</div>
) : (
<div></div>
)}
<div className="flex items-center space-x-6 lg:space-x-8">
<div className="flex items-center space-x-2">
<p className="text-sm font-medium">Rows per page</p>
</div>
<div className="ml-auto flex items-center gap-2">
<div className="flex items-center gap-2 md:gap-2">
<p className="text-muted-foreground">Page size</p>
<Select
value={String(table.getState().pagination.pageSize)}
onValueChange={(value) => {
table.setPageSize(Number(value))
}}
>
<SelectTrigger className="h-8 w-17.5">
<SelectTrigger className="h-8! w-17 text-xs md:text-sm">
<SelectValue
placeholder={
table.getState().pagination.pageSize
}
/>
</SelectTrigger>
<SelectContent side="top">
<SelectContent side="top" className="min-w-0">
{[5, 10, 25, 50].map((pageSize) => (
<SelectItem
key={pageSize}
value={String(pageSize)}
className="text-xs md:text-sm"
>
{pageSize}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex w-25 items-center justify-center text-sm font-medium">
Page {table.getState().pagination.pageIndex + 1} of{' '}
{table.getPageCount()}
</div>
<div className="flex items-center space-x-2">
<div className="flex">
<Button
variant="outline"
size="icon"
className="hidden size-8 lg:flex"
className="me-1 hidden size-8 md:flex"
onClick={() => {
table.setPageIndex(0)
}}
Expand All @@ -80,7 +109,7 @@ export function DataTablePagination<TData>({
<Button
variant="outline"
size="icon"
className="size-8"
className="me-1 size-8"
onClick={() => {
table.previousPage()
}}
Expand All @@ -92,7 +121,7 @@ export function DataTablePagination<TData>({
<Button
variant="outline"
size="icon"
className="size-8"
className="size-8 md:me-1"
onClick={() => {
table.nextPage()
}}
Expand All @@ -104,7 +133,7 @@ export function DataTablePagination<TData>({
<Button
variant="outline"
size="icon"
className="hidden size-8 lg:flex"
className="hidden size-8 md:flex"
onClick={() => {
table.setPageIndex(table.getPageCount() - 1)
}}
Expand Down
102 changes: 63 additions & 39 deletions client/src/components/data-table/DataTableToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,55 +17,79 @@ export function DataTableToolbar<TData>({
config,
}: DataTableToolbarProps<TData>) {
const isFiltered = table.getState().columnFilters.length > 0
const hasSecondaryControls = (config.filters?.length ?? 0) > 0 || isFiltered
const searchColumn = config.search
? table.getColumn(config.search.columnId)
: undefined
const searchValue =
(searchColumn?.getFilterValue() as string | undefined) ?? ''

return (
<div className="flex items-center justify-between">
<div className="flex flex-1 items-center gap-2">
{config.search && (
<Input
placeholder={config.search.placeholder}
value={searchValue}
onChange={(event) =>
searchColumn?.setFilterValue(event.target.value)
}
className={
config.search.className ?? 'h-8 w-37.5 lg:w-62.5'
}
/>
)}
{config.filters?.map((filter) => {
const column = table.getColumn(filter.columnId)
return column ? (
<DataTableFacetedFilter
key={filter.columnId}
column={column}
title={filter.title}
options={filter.options}
<div className="space-y-2">
<div className="flex items-center">
<div className="min-w-0 flex-1">
{config.search && (
<Input
placeholder={config.search.placeholder}
value={searchValue}
onChange={(event) =>
searchColumn?.setFilterValue(event.target.value)
}
className={config.search.className ?? 'h-8 w-full'}
/>
) : null
})}
{isFiltered && (
<Button
variant="ghost"
size="sm"
onClick={() => {
table.resetColumnFilters()
}}
>
Reset
<X />
</Button>
)}
</div>
<div className="flex items-center gap-2">
)}
</div>
{(config.showViewOptions ?? true) && (
<DataTableViewOptions table={table} />
<div className="ms-2">
<DataTableViewOptions table={table} />
</div>
)}
{config.actions && (
<div className="ms-2! ml-auto hidden items-center gap-2 sm:flex">
{config.actions.map((action, index) => (
<Button
key={index}
size="sm"
variant={action.variant ?? 'default'}
onClick={() => void action.onClick()}
>
{action.icon && (
<action.icon className="size-4" />
)}
{action.label}
</Button>
))}
</div>
)}
</div>
{hasSecondaryControls && (
<div className="flex flex-wrap items-center gap-2">
{config.filters?.map((filter) => {
const column = table.getColumn(filter.columnId)
return column ? (
<DataTableFacetedFilter
key={filter.columnId}
column={column}
title={filter.title}
options={filter.options}
/>
) : null
})}
{isFiltered && (
<Button
variant="ghost"
size="sm"
onClick={() => {
table.resetColumnFilters()
}}
>
Reset
<X />
</Button>
)}
</div>
)}
<div className="flex gap-2 sm:hidden">
{config.actions?.map((action, index) => (
<Button
key={index}
Expand Down
5 changes: 3 additions & 2 deletions client/src/components/data-table/DataTableViewOptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,11 @@ export function DataTableViewOptions<TData>({
<Button
variant="outline"
size="sm"
className="ml-auto hidden h-8 lg:flex"
className="h-8"
aria-label="View options"
>
<Settings2 />
View
<span className="hidden sm:inline">View</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-37.5">
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/exercises/ExercisesTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ export function ExercisesTable({
<Lock className={`size-3 shrink-0 ${blueText}`} />
)}
<DataTableTruncatedCell
value={row.original.name}
value={capitalizeWords(row.original.name)}
className="max-w-40 min-w-25"
/>
</div>
Expand Down
95 changes: 95 additions & 0 deletions client/src/components/ui/overrides/card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import * as React from 'react'

import {
Card as UICard,
CardAction as UICardAction,
CardContent as UICardContent,
CardDescription as UICardDescription,
CardFooter as UICardFooter,
CardHeader as UICardHeader,
CardTitle as UICardTitle,
} from '@/components/ui/card'
import { cn } from '@/lib/utils'

type CardProps = React.ComponentProps<typeof UICard>
type CardHeaderProps = React.ComponentProps<typeof UICardHeader>
type CardTitleProps = React.ComponentProps<typeof UICardTitle>
type CardDescriptionProps = React.ComponentProps<typeof UICardDescription>
type CardActionProps = React.ComponentProps<typeof UICardAction>
type CardContentProps = React.ComponentProps<typeof UICardContent>
type CardFooterProps = React.ComponentProps<typeof UICardFooter>

function Card({ className, ...props }: CardProps) {
return (
<UICard
className={cn(
'gap-0 py-1 max-md:mb-0 max-md:rounded-none',
className
)}
{...props}
/>
)
}

function CardHeader({ className, ...props }: CardHeaderProps) {
return (
<UICardHeader
className={cn('-mb-1 pt-1 text-center md:pt-3', className)}
{...props}
/>
)
}

function CardTitle({ className, ...props }: CardTitleProps) {
return (
<UICardTitle
className={cn('text-xl font-bold', className)}
{...props}
/>
)
}

function CardDescription({ className, ...props }: CardDescriptionProps) {
return <UICardDescription className={cn('', className)} {...props} />
}

function CardAction({ className, ...props }: CardActionProps) {
return <UICardAction className={cn('', className)} {...props} />
}

function CardContent({ className, ...props }: CardContentProps) {
return (
<UICardContent
className={cn('px-2 pb-1 md:px-4 md:pb-3', className)}
{...props}
/>
)
}

function CardFooter({ className, ...props }: CardFooterProps) {
return (
<UICardFooter
className={cn('px-2 pb-1 md:px-4 md:pb-3', className)}
{...props}
/>
)
}

export {
Card,
CardAction,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
}
export type {
CardActionProps,
CardContentProps,
CardDescriptionProps,
CardFooterProps,
CardHeaderProps,
CardProps,
CardTitleProps,
}
Loading