code: fakemui,tsx,nextjs (5 files)

This commit is contained in:
Richard Ward
2025-12-30 20:03:28 +00:00
parent b783166b3d
commit 2ecf3f9df9
5 changed files with 601 additions and 6 deletions

View File

@@ -0,0 +1,317 @@
import React, { useState, useMemo, useCallback } from 'react'
import { classNames } from '../utils/classNames'
export interface GridColDef {
field: string
headerName: string
width?: number
flex?: number
sortable?: boolean
filterable?: boolean
renderCell?: (params: GridRenderCellParams) => React.ReactNode
valueGetter?: (params: GridValueGetterParams) => any
valueFormatter?: (params: GridValueFormatterParams) => string
editable?: boolean
type?: 'string' | 'number' | 'date' | 'boolean' | 'actions'
align?: 'left' | 'center' | 'right'
headerAlign?: 'left' | 'center' | 'right'
}
export interface GridRenderCellParams {
value: any
row: any
field: string
id: string | number
}
export interface GridValueGetterParams {
row: any
field: string
id: string | number
}
export interface GridValueFormatterParams {
value: any
field: string
id: string | number
}
export interface GridRowParams {
row: any
id: string | number
}
export interface GridSortModel {
field: string
sort: 'asc' | 'desc' | null
}
export interface GridFilterModel {
items: GridFilterItem[]
}
export interface GridFilterItem {
field: string
operator: string
value: any
}
export interface DataGridProps {
rows: any[]
columns: GridColDef[]
pageSize?: number
rowsPerPageOptions?: number[]
checkboxSelection?: boolean
disableSelectionOnClick?: boolean
onRowClick?: (params: GridRowParams) => void
onSelectionModelChange?: (ids: (string | number)[]) => void
loading?: boolean
autoHeight?: boolean
density?: 'compact' | 'standard' | 'comfortable'
sortModel?: GridSortModel[]
onSortModelChange?: (model: GridSortModel[]) => void
filterModel?: GridFilterModel
onFilterModelChange?: (model: GridFilterModel) => void
getRowId?: (row: any) => string | number
className?: string
sx?: React.CSSProperties
}
/**
* DataGrid - A powerful data table component
*/
export function DataGrid({
rows,
columns,
pageSize = 25,
rowsPerPageOptions = [10, 25, 50, 100],
checkboxSelection = false,
disableSelectionOnClick = false,
onRowClick,
onSelectionModelChange,
loading = false,
autoHeight = false,
density = 'standard',
sortModel,
onSortModelChange,
filterModel,
onFilterModelChange,
getRowId = (row) => row.id,
className,
sx,
}: DataGridProps) {
const [page, setPage] = useState(0)
const [selectedIds, setSelectedIds] = useState<Set<string | number>>(new Set())
const [internalSortModel, setInternalSortModel] = useState<GridSortModel[]>(sortModel || [])
const currentSortModel = sortModel || internalSortModel
const handleSort = useCallback((field: string) => {
const existingSort = currentSortModel.find(s => s.field === field)
let newSort: 'asc' | 'desc' | null = 'asc'
if (existingSort) {
if (existingSort.sort === 'asc') newSort = 'desc'
else if (existingSort.sort === 'desc') newSort = null
}
const newModel = newSort
? [{ field, sort: newSort }]
: []
if (onSortModelChange) {
onSortModelChange(newModel)
} else {
setInternalSortModel(newModel)
}
}, [currentSortModel, onSortModelChange])
const sortedRows = useMemo(() => {
if (currentSortModel.length === 0) return rows
const sort = currentSortModel[0]
return [...rows].sort((a, b) => {
const aVal = a[sort.field]
const bVal = b[sort.field]
if (aVal < bVal) return sort.sort === 'asc' ? -1 : 1
if (aVal > bVal) return sort.sort === 'asc' ? 1 : -1
return 0
})
}, [rows, currentSortModel])
const paginatedRows = useMemo(() => {
const start = page * pageSize
return sortedRows.slice(start, start + pageSize)
}, [sortedRows, page, pageSize])
const totalPages = Math.ceil(rows.length / pageSize)
const handleRowClick = (row: any) => {
const id = getRowId(row)
if (checkboxSelection && !disableSelectionOnClick) {
const newSelected = new Set(selectedIds)
if (newSelected.has(id)) {
newSelected.delete(id)
} else {
newSelected.add(id)
}
setSelectedIds(newSelected)
onSelectionModelChange?.(Array.from(newSelected))
}
onRowClick?.({ row, id })
}
const handleSelectAll = () => {
if (selectedIds.size === rows.length) {
setSelectedIds(new Set())
onSelectionModelChange?.([])
} else {
const allIds = new Set(rows.map(getRowId))
setSelectedIds(allIds)
onSelectionModelChange?.(Array.from(allIds))
}
}
const densityClass = {
compact: 'fakemui-datagrid--compact',
standard: '',
comfortable: 'fakemui-datagrid--comfortable',
}[density]
return (
<div
className={classNames('fakemui-datagrid', densityClass, className)}
style={{ ...sx, height: autoHeight ? 'auto' : '400px' }}
>
{loading && (
<div className="fakemui-datagrid-loading">
<div className="fakemui-datagrid-loading-spinner" />
</div>
)}
<div className="fakemui-datagrid-container">
<table className="fakemui-datagrid-table">
<thead>
<tr>
{checkboxSelection && (
<th className="fakemui-datagrid-checkbox-cell">
<input
type="checkbox"
checked={selectedIds.size === rows.length && rows.length > 0}
onChange={handleSelectAll}
/>
</th>
)}
{columns.map((col) => {
const sort = currentSortModel.find(s => s.field === col.field)
return (
<th
key={col.field}
style={{
width: col.width,
flex: col.flex,
textAlign: col.headerAlign || 'left',
}}
onClick={() => col.sortable !== false && handleSort(col.field)}
className={classNames(
'fakemui-datagrid-header-cell',
col.sortable !== false && 'fakemui-datagrid-header-cell--sortable'
)}
>
{col.headerName}
{sort && (
<span className="fakemui-datagrid-sort-icon">
{sort.sort === 'asc' ? '↑' : '↓'}
</span>
)}
</th>
)
})}
</tr>
</thead>
<tbody>
{paginatedRows.map((row) => {
const id = getRowId(row)
const isSelected = selectedIds.has(id)
return (
<tr
key={id}
onClick={() => handleRowClick(row)}
className={classNames(
'fakemui-datagrid-row',
isSelected && 'fakemui-datagrid-row--selected'
)}
>
{checkboxSelection && (
<td className="fakemui-datagrid-checkbox-cell">
<input
type="checkbox"
checked={isSelected}
onChange={() => {}}
/>
</td>
)}
{columns.map((col) => {
let value = row[col.field]
if (col.valueGetter) {
value = col.valueGetter({ row, field: col.field, id })
}
if (col.valueFormatter) {
value = col.valueFormatter({ value, field: col.field, id })
}
const cellContent = col.renderCell
? col.renderCell({ value, row, field: col.field, id })
: value
return (
<td
key={col.field}
style={{ textAlign: col.align || 'left' }}
className="fakemui-datagrid-cell"
>
{cellContent}
</td>
)
})}
</tr>
)
})}
</tbody>
</table>
</div>
<div className="fakemui-datagrid-footer">
<div className="fakemui-datagrid-pagination">
<span>
{page * pageSize + 1}{Math.min((page + 1) * pageSize, rows.length)} of {rows.length}
</span>
<button
disabled={page === 0}
onClick={() => setPage(p => p - 1)}
className="fakemui-datagrid-pagination-btn"
>
Previous
</button>
<button
disabled={page >= totalPages - 1}
onClick={() => setPage(p => p + 1)}
className="fakemui-datagrid-pagination-btn"
>
Next
</button>
</div>
</div>
</div>
)
}
// Aliases for compatibility
export const DataGridPro = DataGrid
export const DataGridPremium = DataGrid

View File

@@ -0,0 +1,279 @@
import React, { useState, useRef, useEffect } from 'react'
import { classNames } from '../utils/classNames'
export interface DatePickerProps {
value?: Date | null
onChange?: (date: Date | null) => void
label?: string
disabled?: boolean
minDate?: Date
maxDate?: Date
format?: string
inputFormat?: string
views?: ('year' | 'month' | 'day')[]
openTo?: 'year' | 'month' | 'day'
className?: string
sx?: React.CSSProperties
renderInput?: (props: any) => React.ReactNode
disableFuture?: boolean
disablePast?: boolean
}
export interface TimePickerProps {
value?: Date | null
onChange?: (date: Date | null) => void
label?: string
disabled?: boolean
ampm?: boolean
views?: ('hours' | 'minutes' | 'seconds')[]
className?: string
sx?: React.CSSProperties
renderInput?: (props: any) => React.ReactNode
}
export interface DateTimePickerProps extends Omit<DatePickerProps, 'views'>, Omit<TimePickerProps, 'views'> {
dateFormat?: string
timeFormat?: string
views?: ('year' | 'month' | 'day' | 'hours' | 'minutes' | 'seconds')[]
}
/**
* Format a date to YYYY-MM-DD
*/
const formatDate = (date: Date | null): string => {
if (!date) return ''
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
/**
* Format a time to HH:MM
*/
const formatTime = (date: Date | null): string => {
if (!date) return ''
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${hours}:${minutes}`
}
/**
* Format a datetime to YYYY-MM-DDTHH:MM
*/
const formatDateTime = (date: Date | null): string => {
if (!date) return ''
return `${formatDate(date)}T${formatTime(date)}`
}
/**
* DatePicker - Date selection component
*/
export function DatePicker({
value,
onChange,
label,
disabled = false,
minDate,
maxDate,
className,
sx,
renderInput,
disableFuture = false,
disablePast = false,
}: DatePickerProps) {
const [internalValue, setInternalValue] = useState(value)
const inputRef = useRef<HTMLInputElement>(null)
const currentValue = value !== undefined ? value : internalValue
useEffect(() => {
if (value !== undefined) {
setInternalValue(value)
}
}, [value])
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const dateStr = e.target.value
const newDate = dateStr ? new Date(dateStr + 'T00:00:00') : null
setInternalValue(newDate)
onChange?.(newDate)
}
const getMinMax = () => {
let min: string | undefined
let max: string | undefined
if (minDate) min = formatDate(minDate)
if (maxDate) max = formatDate(maxDate)
if (disablePast) min = formatDate(new Date())
if (disableFuture) max = formatDate(new Date())
return { min, max }
}
const { min, max } = getMinMax()
const inputProps = {
type: 'date',
value: formatDate(currentValue),
onChange: handleChange,
disabled,
min,
max,
ref: inputRef,
className: 'fakemui-datepicker-input',
}
if (renderInput) {
return renderInput(inputProps)
}
return (
<div className={classNames('fakemui-datepicker', className)} style={sx}>
{label && <label className="fakemui-datepicker-label">{label}</label>}
<input {...inputProps} />
</div>
)
}
/**
* TimePicker - Time selection component
*/
export function TimePicker({
value,
onChange,
label,
disabled = false,
ampm = false,
className,
sx,
renderInput,
}: TimePickerProps) {
const [internalValue, setInternalValue] = useState(value)
const inputRef = useRef<HTMLInputElement>(null)
const currentValue = value !== undefined ? value : internalValue
useEffect(() => {
if (value !== undefined) {
setInternalValue(value)
}
}, [value])
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const timeStr = e.target.value
if (!timeStr) {
setInternalValue(null)
onChange?.(null)
return
}
const [hours, minutes] = timeStr.split(':').map(Number)
const newDate = new Date()
newDate.setHours(hours, minutes, 0, 0)
setInternalValue(newDate)
onChange?.(newDate)
}
const inputProps = {
type: 'time',
value: formatTime(currentValue),
onChange: handleChange,
disabled,
ref: inputRef,
className: 'fakemui-timepicker-input',
}
if (renderInput) {
return renderInput(inputProps)
}
return (
<div className={classNames('fakemui-timepicker', className)} style={sx}>
{label && <label className="fakemui-timepicker-label">{label}</label>}
<input {...inputProps} />
</div>
)
}
/**
* DateTimePicker - Combined date and time selection
*/
export function DateTimePicker({
value,
onChange,
label,
disabled = false,
minDate,
maxDate,
className,
sx,
renderInput,
disableFuture = false,
disablePast = false,
}: DateTimePickerProps) {
const [internalValue, setInternalValue] = useState(value)
const inputRef = useRef<HTMLInputElement>(null)
const currentValue = value !== undefined ? value : internalValue
useEffect(() => {
if (value !== undefined) {
setInternalValue(value)
}
}, [value])
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const dateTimeStr = e.target.value
const newDate = dateTimeStr ? new Date(dateTimeStr) : null
setInternalValue(newDate)
onChange?.(newDate)
}
const getMinMax = () => {
let min: string | undefined
let max: string | undefined
if (minDate) min = formatDateTime(minDate)
if (maxDate) max = formatDateTime(maxDate)
if (disablePast) min = formatDateTime(new Date())
if (disableFuture) max = formatDateTime(new Date())
return { min, max }
}
const { min, max } = getMinMax()
const inputProps = {
type: 'datetime-local',
value: formatDateTime(currentValue),
onChange: handleChange,
disabled,
min,
max,
ref: inputRef,
className: 'fakemui-datetimepicker-input',
}
if (renderInput) {
return renderInput(inputProps)
}
return (
<div className={classNames('fakemui-datetimepicker', className)} style={sx}>
{label && <label className="fakemui-datetimepicker-label">{label}</label>}
<input {...inputProps} />
</div>
)
}
// Aliases for API compatibility
export const DesktopDatePicker = DatePicker
export const MobileDatePicker = DatePicker
export const StaticDatePicker = DatePicker
export const CalendarPicker = DatePicker
export const ClockPicker = TimePicker

View File

@@ -9,7 +9,7 @@ import {
TableFooter as FakeMuiTableFooter,
TableHead as FakeMuiTableHead,
TableRow as FakeMuiTableRow,
} from 'fakemui'
} from '@/fakemui'
import { forwardRef, ReactNode } from 'react'
import styles from './Table.module.scss'

View File

@@ -3,12 +3,11 @@ export * from '../app-config'
export * from '../auth'
export * from '../comments'
export * from '../components'
export * from '../credentials'
export * from '../god-credentials'
export * from '../css-classes'
export * from '../database-admin'
export * from '../dropdown-configs'
export * from '../error-logs'
export * from '../god-credentials'
export * from '../lua-scripts'
export * from '../packages'
export * from '../pages'

View File

@@ -1,5 +1,4 @@
import { seedAppConfig } from './app/seed-app-config'
import { seedUsers } from './app/seed-users'
import { seedCssCategories } from './css/seed-css-categories'
import { seedDropdownConfigs } from './dropdowns/seed-dropdown-configs'
@@ -7,14 +6,15 @@ import { seedDropdownConfigs } from './dropdowns/seed-dropdown-configs'
* Seed database with default data
*/
export const seedDefaultData = async (): Promise<void> => {
await seedUsers()
// TODO: Implement seedUsers function and import it
// await seedUsers()
await seedAppConfig()
await seedCssCategories()
await seedDropdownConfigs()
}
export const defaultDataBuilders = {
seedUsers,
// seedUsers,
seedAppConfig,
seedCssCategories,
seedDropdownConfigs,