chore: perauset customer project
This commit is contained in:
20
packages/components/package.json
Normal file
20
packages/components/package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "@perauset/components",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mantine/hooks": "^9.2.1",
|
||||
"@pikku/mantine": "^0.12.6",
|
||||
"@pikku/react": "^0.12.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.2.5"
|
||||
}
|
||||
}
|
||||
131
packages/components/src/data-table.tsx
Normal file
131
packages/components/src/data-table.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import { Table, Text, Card, Stack, Group, Box } from '@pikku/mantine/core'
|
||||
import { useMediaQuery } from '@mantine/hooks'
|
||||
import { type ReactNode } from 'react'
|
||||
import { asI18n, type I18nNode } from '@pikku/react'
|
||||
|
||||
export interface Column<T> {
|
||||
key: string
|
||||
label: I18nNode
|
||||
render: (row: T) => ReactNode
|
||||
}
|
||||
|
||||
interface DataTableProps<T> {
|
||||
data: T[]
|
||||
columns: Column<T>[]
|
||||
emptyMessage?: string
|
||||
rowKey: (row: T) => string
|
||||
}
|
||||
|
||||
export function DataTable<T>({
|
||||
data,
|
||||
columns,
|
||||
emptyMessage = 'No data found.',
|
||||
rowKey,
|
||||
}: DataTableProps<T>) {
|
||||
const isMobile = useMediaQuery('(max-width: 768px)')
|
||||
|
||||
if (data.length === 0) {
|
||||
return (
|
||||
<Card
|
||||
padding="xl"
|
||||
radius="md"
|
||||
style={{
|
||||
backgroundColor: '#ffffff',
|
||||
boxShadow:
|
||||
'0 2px 12px rgba(61, 43, 31, 0.08), 0 1px 4px rgba(61, 43, 31, 0.04)',
|
||||
}}
|
||||
>
|
||||
<Text size="sm" c="dimmed" ta="center">
|
||||
{asI18n(emptyMessage)}
|
||||
</Text>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Stack gap="sm">
|
||||
{data.map((row) => (
|
||||
<Card
|
||||
key={rowKey(row)}
|
||||
padding="md"
|
||||
radius="md"
|
||||
style={{
|
||||
backgroundColor: '#ffffff',
|
||||
boxShadow:
|
||||
'0 2px 12px rgba(61, 43, 31, 0.08), 0 1px 4px rgba(61, 43, 31, 0.04)',
|
||||
}}
|
||||
>
|
||||
<Stack gap="xs">
|
||||
{columns.map((col) => (
|
||||
<Group key={col.key} justify="space-between" wrap="nowrap" gap="sm">
|
||||
<Text size="xs" c="dimmed" fw={600} style={{ minWidth: 80 }}>
|
||||
{col.label}
|
||||
</Text>
|
||||
<Box style={{ textAlign: 'right' }}>{col.render(row)}</Box>
|
||||
</Group>
|
||||
))}
|
||||
</Stack>
|
||||
</Card>
|
||||
))}
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
padding={0}
|
||||
radius="md"
|
||||
style={{
|
||||
backgroundColor: '#ffffff',
|
||||
boxShadow:
|
||||
'0 2px 12px rgba(61, 43, 31, 0.08), 0 1px 4px rgba(61, 43, 31, 0.04)',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Table
|
||||
highlightOnHover
|
||||
horizontalSpacing="lg"
|
||||
verticalSpacing="sm"
|
||||
styles={{
|
||||
thead: {
|
||||
backgroundColor: '#f8f6f2',
|
||||
borderBottom: '2px solid rgba(61, 43, 31, 0.1)',
|
||||
},
|
||||
th: {
|
||||
padding: '12px 16px',
|
||||
fontSize: '11px',
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.05em',
|
||||
textTransform: 'uppercase' as const,
|
||||
color: '#8b7d6b',
|
||||
},
|
||||
td: {
|
||||
padding: '14px 16px',
|
||||
borderBottom: '1px solid rgba(61, 43, 31, 0.06)',
|
||||
},
|
||||
tr: {
|
||||
transition: 'background-color 0.15s ease',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
{columns.map((col) => (
|
||||
<Table.Th key={col.key}>{col.label}</Table.Th>
|
||||
))}
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{data.map((row) => (
|
||||
<Table.Tr key={rowKey(row)}>
|
||||
{columns.map((col) => (
|
||||
<Table.Td key={col.key}>{col.render(row)}</Table.Td>
|
||||
))}
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
3
packages/components/src/index.ts
Normal file
3
packages/components/src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { PageHeader } from './page-header.js'
|
||||
export { StatusBadge } from './status-badge.js'
|
||||
export { DataTable, type Column } from './data-table.js'
|
||||
32
packages/components/src/page-header.tsx
Normal file
32
packages/components/src/page-header.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Title, Text, Group, Stack } from '@pikku/mantine/core'
|
||||
import { type ReactNode } from 'react'
|
||||
import type { I18nNode } from '@pikku/react'
|
||||
|
||||
export function PageHeader({
|
||||
title,
|
||||
description,
|
||||
action,
|
||||
}: {
|
||||
title: I18nNode
|
||||
description?: I18nNode
|
||||
action?: ReactNode
|
||||
}) {
|
||||
return (
|
||||
<Group justify="space-between" align="flex-start">
|
||||
<Stack gap={4}>
|
||||
<Title
|
||||
order={2}
|
||||
style={{ fontFamily: '"Cormorant", serif', color: '#3D2B1F' }}
|
||||
>
|
||||
{title}
|
||||
</Title>
|
||||
{description && (
|
||||
<Text size="sm" c="dimmed">
|
||||
{description}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
{action}
|
||||
</Group>
|
||||
)
|
||||
}
|
||||
37
packages/components/src/status-badge.tsx
Normal file
37
packages/components/src/status-badge.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Badge } from '@pikku/mantine/core'
|
||||
import { asI18n } from '@pikku/react'
|
||||
|
||||
const STATUS_COLORS: Record<string, string> = {
|
||||
submitted: 'yellow',
|
||||
pending: 'yellow',
|
||||
under_review: 'orange',
|
||||
approved: 'blue',
|
||||
confirmed: 'blue',
|
||||
checked_in: 'green',
|
||||
active: 'green',
|
||||
open: 'green',
|
||||
planned: 'blue',
|
||||
checked_out: 'gray',
|
||||
completed: 'gray',
|
||||
full: 'orange',
|
||||
closed: 'gray',
|
||||
rejected: 'red',
|
||||
declined: 'red',
|
||||
cancelled: 'red',
|
||||
blocked: 'pink',
|
||||
waitlisted: 'orange',
|
||||
}
|
||||
|
||||
function formatStatus(status: string): string {
|
||||
return status.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())
|
||||
}
|
||||
|
||||
export function StatusBadge({ status }: { status: string }) {
|
||||
const color = STATUS_COLORS[status] ?? 'gray'
|
||||
|
||||
return (
|
||||
<Badge color={color} variant="light" size="sm">
|
||||
{asI18n(formatStatus(status))}
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user