project-nomad/admin/inertia/components/StyledTable.tsx
Martin Seener 134d1642af
Added initial i18n framework and most german translations
- Add i18next, react-i18next, i18next-browser-languagedetector packages
- Configure i18n initialization with language detector in lib/i18n.ts
- Created en/de translation files and moved most hard-coded strings into the files and translated them
- Uses locale-aware date formatting where applicable
- Added language-specific Wikipedia content files (wikipedia.en.json, wikipedia.de.json) and updated download URLs
- Added NOMAD_REPO_URL env variable for fork-friendly URL resolution (easier testing and rollout independent of Crosstalk repo)
2026-03-24 13:21:31 +01:00

208 lines
7.5 KiB
TypeScript

import { capitalizeFirstLetter } from '~/lib/util'
import classNames from '~/lib/classNames'
import LoadingSpinner from '~/components/LoadingSpinner'
import React, { RefObject, useState } from 'react'
import { useTranslation } from 'react-i18next'
export type StyledTableProps<T extends { [key: string]: any }> = {
loading?: boolean
tableProps?: React.HTMLAttributes<HTMLTableElement>
tableRowStyle?: React.CSSProperties
tableBodyClassName?: string
tableBodyStyle?: React.CSSProperties
data?: T[]
noDataText?: string
onRowClick?: (record: T) => void
columns?: {
accessor: keyof T
title?: React.ReactNode
render?: (record: T, index: number) => React.ReactNode
className?: string
}[]
className?: string
rowLines?: boolean
ref?: RefObject<HTMLDivElement | null>
containerProps?: React.HTMLAttributes<HTMLDivElement>
compact?: boolean
expandable?: {
expandedRowRender: (record: T, index: number) => React.ReactNode
defaultExpandedRowKeys?: (string | number)[]
onExpandedRowsChange?: (expandedKeys: (string | number)[]) => void
expandIconColumnIndex?: number
}
}
function StyledTable<T extends { [key: string]: any }>({
loading = false,
tableProps = {},
tableRowStyle = {},
tableBodyClassName = '',
tableBodyStyle = {},
data = [],
noDataText,
onRowClick,
columns = [],
className = '',
ref,
containerProps = {},
rowLines = true,
compact = false,
expandable,
}: StyledTableProps<T>) {
const { t } = useTranslation()
const resolvedNoDataText = noDataText ?? t('common.noRecordsFound')
const { className: tableClassName, ...restTableProps } = tableProps
const [expandedRowKeys, setExpandedRowKeys] = useState<(string | number)[]>(
expandable?.defaultExpandedRowKeys || []
)
const leftPadding = compact ? 'pl-2' : 'pl-4 sm:pl-6'
const isRowExpanded = (record: T, index: number) => {
const key = record.id ?? index
return expandedRowKeys.includes(key)
}
const toggleRowExpansion = (record: T, index: number, event: React.MouseEvent) => {
event.stopPropagation()
const key = record.id ?? index
const newExpandedKeys = expandedRowKeys.includes(key)
? expandedRowKeys.filter((k) => k !== key)
: [...expandedRowKeys, key]
setExpandedRowKeys(newExpandedKeys)
expandable?.onExpandedRowsChange?.(newExpandedKeys)
}
return (
<div
className={classNames(
'w-full overflow-x-auto bg-surface-primary ring-1 ring-border-default sm:mx-0 sm:rounded-lg p-1 shadow-md',
className
)}
ref={ref}
{...containerProps}
>
<table className="min-w-full overflow-auto" {...restTableProps}>
<thead className='border-b border-border-subtle '>
<tr>
{expandable && (
<th
className={classNames(
'whitespace-nowrap text-left font-semibold text-text-primary w-12',
compact ? `${leftPadding} py-2` : `${leftPadding} py-4 pr-3`
)}
/>
)}
{columns.map((column, index) => (
<th
key={index}
className={classNames(
'whitespace-nowrap text-left font-semibold text-text-primary',
compact ? `${leftPadding} py-2` : `${leftPadding} py-4 pr-3`
)}
>
{column.title ?? capitalizeFirstLetter(column.accessor.toString())}
</th>
))}
</tr>
</thead>
<tbody className={tableBodyClassName} style={tableBodyStyle}>
{!loading &&
data.length !== 0 &&
data.map((record, recordIdx) => {
const isExpanded = expandable && isRowExpanded(record, recordIdx)
return (
<React.Fragment key={record.id || recordIdx}>
<tr
data-index={'index' in record ? record.index : recordIdx}
onClick={() => onRowClick?.(record)}
style={{
...tableRowStyle,
height: 'height' in record ? record.height : 'auto',
transform:
'translateY' in record ? 'translateY(' + record.transformY + 'px)' : undefined,
}}
className={classNames(
rowLines ? 'border-b border-border-subtle' : '',
onRowClick ? `cursor-pointer hover:bg-surface-secondary ` : ''
)}
>
{expandable && (
<td
className={classNames(
'text-sm whitespace-nowrap text-left w-12',
compact ? `${leftPadding} py-2` : `${leftPadding} py-4 pr-3`
)}
onClick={(e) => toggleRowExpansion(record, recordIdx, e)}
>
<button
className="text-text-muted hover:text-text-primary focus:outline-none"
aria-label={isExpanded ? 'Collapse row' : 'Expand row'}
>
<svg
className={classNames(
'w-5 h-5 transition-transform',
isExpanded ? 'rotate-90' : ''
)}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
/>
</svg>
</button>
</td>
)}
{columns.map((column, index) => (
<td
key={index}
className={classNames(
'relative text-sm whitespace-nowrap max-w-72 truncate break-words text-left',
column.className || '',
compact ? `${leftPadding} py-2` : `${leftPadding} py-4 pr-3`
)}
>
{column.render
? column.render(record, index)
: (record[column.accessor] as React.ReactNode)}
</td>
))}
</tr>
{expandable && isExpanded && (
<tr className="bg-surface-secondary">
<td colSpan={columns.length + 1}>
{expandable.expandedRowRender(record, recordIdx)}
</td>
</tr>
)}
</React.Fragment>
)
})}
{!loading && data.length === 0 && (
<tr>
<td colSpan={columns.length + (expandable ? 1 : 0)} className="!text-center py-8 text-text-muted">
{resolvedNoDataText}
</td>
</tr>
)}
{loading && (
<tr className="!h-16">
<td colSpan={columns.length + (expandable ? 1 : 0)} className="!text-center">
<LoadingSpinner fullscreen={false} />
</td>
</tr>
)}
</tbody>
</table>
</div>
)
}
export default StyledTable