diff --git a/fakemui/fakemui/x/DataGrid.tsx b/fakemui/fakemui/x/DataGrid.tsx new file mode 100644 index 000000000..0b8a7b7e9 --- /dev/null +++ b/fakemui/fakemui/x/DataGrid.tsx @@ -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>(new Set()) + const [internalSortModel, setInternalSortModel] = useState(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 ( +
+ {loading && ( +
+
+
+ )} + +
+ + + + {checkboxSelection && ( + + )} + {columns.map((col) => { + const sort = currentSortModel.find(s => s.field === col.field) + return ( + + ) + })} + + + + {paginatedRows.map((row) => { + const id = getRowId(row) + const isSelected = selectedIds.has(id) + + return ( + handleRowClick(row)} + className={classNames( + 'fakemui-datagrid-row', + isSelected && 'fakemui-datagrid-row--selected' + )} + > + {checkboxSelection && ( + + )} + {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 ( + + ) + })} + + ) + })} + +
+ 0} + onChange={handleSelectAll} + /> + col.sortable !== false && handleSort(col.field)} + className={classNames( + 'fakemui-datagrid-header-cell', + col.sortable !== false && 'fakemui-datagrid-header-cell--sortable' + )} + > + {col.headerName} + {sort && ( + + {sort.sort === 'asc' ? '↑' : '↓'} + + )} +
+ {}} + /> + + {cellContent} +
+
+ +
+
+ + {page * pageSize + 1}–{Math.min((page + 1) * pageSize, rows.length)} of {rows.length} + + + +
+
+
+ ) +} + +// Aliases for compatibility +export const DataGridPro = DataGrid +export const DataGridPremium = DataGrid diff --git a/fakemui/fakemui/x/DatePicker.tsx b/fakemui/fakemui/x/DatePicker.tsx new file mode 100644 index 000000000..ee0691d65 --- /dev/null +++ b/fakemui/fakemui/x/DatePicker.tsx @@ -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, Omit { + 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(null) + + const currentValue = value !== undefined ? value : internalValue + + useEffect(() => { + if (value !== undefined) { + setInternalValue(value) + } + }, [value]) + + const handleChange = (e: React.ChangeEvent) => { + 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 ( +
+ {label && } + +
+ ) +} + +/** + * 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(null) + + const currentValue = value !== undefined ? value : internalValue + + useEffect(() => { + if (value !== undefined) { + setInternalValue(value) + } + }, [value]) + + const handleChange = (e: React.ChangeEvent) => { + 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 ( +
+ {label && } + +
+ ) +} + +/** + * 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(null) + + const currentValue = value !== undefined ? value : internalValue + + useEffect(() => { + if (value !== undefined) { + setInternalValue(value) + } + }, [value]) + + const handleChange = (e: React.ChangeEvent) => { + 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 ( +
+ {label && } + +
+ ) +} + +// Aliases for API compatibility +export const DesktopDatePicker = DatePicker +export const MobileDatePicker = DatePicker +export const StaticDatePicker = DatePicker +export const CalendarPicker = DatePicker +export const ClockPicker = TimePicker diff --git a/frontends/nextjs/src/components/ui/organisms/data/Table.tsx b/frontends/nextjs/src/components/ui/organisms/data/Table.tsx index 6e72c032c..95737743e 100644 --- a/frontends/nextjs/src/components/ui/organisms/data/Table.tsx +++ b/frontends/nextjs/src/components/ui/organisms/data/Table.tsx @@ -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' diff --git a/frontends/nextjs/src/lib/db/core/entities.ts b/frontends/nextjs/src/lib/db/core/entities.ts index 76dde5476..a74051659 100644 --- a/frontends/nextjs/src/lib/db/core/entities.ts +++ b/frontends/nextjs/src/lib/db/core/entities.ts @@ -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' diff --git a/frontends/nextjs/src/lib/db/database-admin/seed-default-data/index.ts b/frontends/nextjs/src/lib/db/database-admin/seed-default-data/index.ts index ec9bb2c05..f540c063c 100644 --- a/frontends/nextjs/src/lib/db/database-admin/seed-default-data/index.ts +++ b/frontends/nextjs/src/lib/db/database-admin/seed-default-data/index.ts @@ -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 => { - 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,