reinit repo with sources from zipped dir
This commit is contained in:
12
apps/admin/eslint.config.mjs
Normal file
12
apps/admin/eslint.config.mjs
Normal file
@@ -0,0 +1,12 @@
|
||||
import nx from '@nx/eslint-plugin'
|
||||
import baseConfig from '../../eslint.config.mjs'
|
||||
|
||||
export default [
|
||||
...baseConfig,
|
||||
...nx.configs['flat/react'],
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
|
||||
// Override or add rules here
|
||||
rules: {},
|
||||
},
|
||||
]
|
||||
5
apps/admin/package.json
Normal file
5
apps/admin/package.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "@klx/admin",
|
||||
"version": "0.0.1",
|
||||
"private": true
|
||||
}
|
||||
25
apps/admin/project.json
Normal file
25
apps/admin/project.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "@klx/admin",
|
||||
"targets": {
|
||||
"serve": {
|
||||
"continuous": true,
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "apps/admin",
|
||||
"args": [
|
||||
"--node-env=development"
|
||||
],
|
||||
"env": {},
|
||||
"command": "rspack serve"
|
||||
},
|
||||
"syncGenerators": [
|
||||
"@nx/js:typescript-sync"
|
||||
],
|
||||
"executor": "nx:run-commands",
|
||||
"configurations": {},
|
||||
"parallelism": true
|
||||
}
|
||||
}
|
||||
}
|
||||
35
apps/admin/rspack.config.js
Normal file
35
apps/admin/rspack.config.js
Normal file
@@ -0,0 +1,35 @@
|
||||
const { NxAppRspackPlugin } = require('@nx/rspack/app-plugin')
|
||||
const { NxReactRspackPlugin } = require('@nx/rspack/react-plugin')
|
||||
const { join } = require('path')
|
||||
|
||||
module.exports = {
|
||||
output: {
|
||||
path: join(__dirname, 'dist'),
|
||||
},
|
||||
devServer: {
|
||||
port: 4201,
|
||||
open: true,
|
||||
historyApiFallback: {
|
||||
index: '/index.html',
|
||||
disableDotRule: true,
|
||||
htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'],
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
new NxAppRspackPlugin({
|
||||
tsConfig: './tsconfig.app.json',
|
||||
main: './src/main.tsx',
|
||||
index: './src/index.html',
|
||||
baseHref: '/',
|
||||
assets: ['./src/assets'],
|
||||
styles: ['./src/styles.css'],
|
||||
outputHashing: process.env['NODE_ENV'] === 'production' ? 'all' : 'none',
|
||||
optimization: process.env['NODE_ENV'] === 'production',
|
||||
}),
|
||||
new NxReactRspackPlugin({
|
||||
// Uncomment this line if you don't want to use SVGR
|
||||
// See: https://react-svgr.com/
|
||||
// svgr: false
|
||||
}),
|
||||
],
|
||||
}
|
||||
15
apps/admin/src/api/fetchUsers.ts
Normal file
15
apps/admin/src/api/fetchUsers.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import {faker} from '@faker-js/faker'
|
||||
import { User } from '../types'
|
||||
|
||||
|
||||
const waitMs = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
|
||||
|
||||
export const fetchUsers = async (): Promise<User[]> => {
|
||||
await waitMs(30)
|
||||
return Array.from({length: 2000}).map(() => ({
|
||||
uuid: faker.string.uuid(),
|
||||
name: faker.person.fullName(),
|
||||
email: faker.internet.email(),
|
||||
job: faker.person.jobTitle(),
|
||||
}))
|
||||
}
|
||||
0
apps/admin/src/assets/.gitkeep
Normal file
0
apps/admin/src/assets/.gitkeep
Normal file
18
apps/admin/src/components/App.tsx
Normal file
18
apps/admin/src/components/App.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import {UsersTable} from '../components/UsersTable'
|
||||
import {Header, Footer, Main} from '@klx/ui'
|
||||
import {ClockWrapper} from './ClockWrapper'
|
||||
|
||||
const APP_NAME = '@klx/admin'
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
<div>
|
||||
<Header title={APP_NAME} />
|
||||
<Main>
|
||||
<ClockWrapper />
|
||||
<UsersTable />
|
||||
</Main>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
20
apps/admin/src/components/Clock.tsx
Normal file
20
apps/admin/src/components/Clock.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
export const Clock = () => {
|
||||
const [time, setTime] = useState(new Date())
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setTime(new Date())
|
||||
}, 1000)
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
const pad = (n: number) => n.toString().padStart(2, '0')
|
||||
|
||||
return (
|
||||
<div className="text-sm font-mono text-gray-700 font-semibold">
|
||||
{pad(time.getHours())}:{pad(time.getMinutes())}:{pad(time.getSeconds())}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
9
apps/admin/src/components/ClockWrapper.tsx
Normal file
9
apps/admin/src/components/ClockWrapper.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Clock } from './Clock'
|
||||
|
||||
export const ClockWrapper = () => {
|
||||
return (
|
||||
<div className="flex items-center px-4 py-2 m-2">
|
||||
<Clock />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
35
apps/admin/src/components/SortHeader.tsx
Normal file
35
apps/admin/src/components/SortHeader.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { SortDirection } from '../hooks/useSort'
|
||||
|
||||
interface SortHeaderProps {
|
||||
label: string
|
||||
isActive: boolean
|
||||
direction: SortDirection
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
export const SortHeader = ({ label, isActive, direction, onClick }: SortHeaderProps) => {
|
||||
const getSortIndicator = () => {
|
||||
if (!isActive) return '↑↓'
|
||||
if (direction === 'asc') return '↑'
|
||||
if (direction === 'desc') return '↓'
|
||||
return '↑↓'
|
||||
}
|
||||
|
||||
return (
|
||||
<th
|
||||
onClick={onClick}
|
||||
className={`
|
||||
px-4 py-3 text-left cursor-pointer
|
||||
transition-all duration-200 ease-out select-none text-sm font-semibold whitespace-nowrap
|
||||
${isActive
|
||||
? 'bg-blue-50 text-blue-700 border-b-2 border-blue-600'
|
||||
: 'bg-gradient-to-r from-gray-50 to-gray-100 text-gray-700 hover:bg-gray-200 border-b-2 border-gray-200'
|
||||
}
|
||||
`}
|
||||
title="Click to sort"
|
||||
>
|
||||
{label}
|
||||
<span className="ml-2 text-xs font-normal opacity-75">{getSortIndicator()}</span>
|
||||
</th>
|
||||
)
|
||||
}
|
||||
53
apps/admin/src/components/UsersTable.tsx
Normal file
53
apps/admin/src/components/UsersTable.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import {User} from '../types'
|
||||
import {useEffect, useState} from 'react'
|
||||
import {fetchUsers} from '../api/fetchUsers'
|
||||
import {useSort} from '../hooks/useSort'
|
||||
import {useFilters} from '../hooks/useFilters'
|
||||
import {VirtualizedTable} from './VirtualizedTable'
|
||||
import {filterUsers} from '../utils/filterUsers'
|
||||
|
||||
export const UsersTable = () => {
|
||||
const [users, setUsers] = useState<User[]>([])
|
||||
const [selectedUser, setSelectedUser] = useState<User | null>(null)
|
||||
const {filters, updateFilters, clearFilters} = useFilters()
|
||||
|
||||
// Filtrer puis trier
|
||||
const filteredUsers = filterUsers(users, filters)
|
||||
const {sortedItems, sortKey, direction, handleSort} = useSort(filteredUsers)
|
||||
|
||||
useEffect(() => {
|
||||
fetchUsers()
|
||||
.then(data => setUsers(data))
|
||||
.catch(console.error)
|
||||
}, [])
|
||||
|
||||
const onUserClick = (user: User) => {
|
||||
setSelectedUser(user)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-white rounded-xl shadow-md hover:shadow-lg transition-shadow duration-300 border border-gray-100">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-2xl font-bold text-gray-900">
|
||||
Users Management
|
||||
</h2>
|
||||
<span className="text-xs font-medium text-gray-500 bg-gray-100 px-3 py-1 rounded-full">
|
||||
{sortedItems.length} / {users.length} users
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<VirtualizedTable
|
||||
items={sortedItems}
|
||||
sortKey={sortKey}
|
||||
direction={direction}
|
||||
onSort={handleSort}
|
||||
selectedUser={selectedUser}
|
||||
onSelectUser={onUserClick}
|
||||
filters={filters}
|
||||
onFilterChange={updateFilters}
|
||||
onClearFilters={clearFilters}
|
||||
totalUsers={users.length}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
130
apps/admin/src/components/VirtualizedTable.tsx
Normal file
130
apps/admin/src/components/VirtualizedTable.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
import { User } from '../types'
|
||||
import { SortDirection } from '../hooks/useSort'
|
||||
import { Filters } from '../hooks/useFilters'
|
||||
import { SortHeader } from './SortHeader'
|
||||
|
||||
interface VirtualizedTableProps {
|
||||
items: User[]
|
||||
sortKey: keyof User | null
|
||||
direction: SortDirection
|
||||
onSort: (key: keyof User) => void
|
||||
selectedUser: User | null
|
||||
onSelectUser: (user: User) => void
|
||||
filters: Filters
|
||||
onFilterChange: (filters: Partial<Filters>) => void
|
||||
onClearFilters: () => void
|
||||
totalUsers: number
|
||||
}
|
||||
|
||||
export const VirtualizedTable = ({
|
||||
items,
|
||||
sortKey,
|
||||
direction,
|
||||
onSort,
|
||||
selectedUser,
|
||||
onSelectUser,
|
||||
filters,
|
||||
onFilterChange,
|
||||
onClearFilters,
|
||||
totalUsers,
|
||||
}: VirtualizedTableProps) => {
|
||||
const cellStyles = 'border-b border-gray-200 px-4 py-3.5 text-sm text-gray-700'
|
||||
const inputClass = 'px-2 py-1 border border-gray-300 rounded text-xs focus:outline-none focus:ring-1 focus:ring-blue-500'
|
||||
|
||||
const hasActiveFilters = Object.values(filters).some(v => v !== '')
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-gray-200 overflow-hidden shadow-sm">
|
||||
{/* Filter Bar */}
|
||||
<div className="bg-white border-b border-gray-200 flex sticky top-0 z-9 w-full px-4 py-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Name..."
|
||||
value={filters.name}
|
||||
onChange={(e) => onFilterChange({ name: e.target.value })}
|
||||
className={`flex-1 ${inputClass}`}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Email..."
|
||||
value={filters.email}
|
||||
onChange={(e) => onFilterChange({ email: e.target.value })}
|
||||
className={`flex-1 ${inputClass} mx-2`}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Job..."
|
||||
value={filters.job}
|
||||
onChange={(e) => onFilterChange({ job: e.target.value })}
|
||||
className={`flex-1 ${inputClass}`}
|
||||
/>
|
||||
{hasActiveFilters && (
|
||||
<button
|
||||
onClick={onClearFilters}
|
||||
className="ml-2 px-2 py-1 bg-red-100 text-red-700 rounded text-xs hover:bg-red-200 transition-colors font-medium whitespace-nowrap"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Table with scrollable container */}
|
||||
<div className="overflow-y-auto" style={{ maxHeight: '500px' }}>
|
||||
<table className="w-full border-collapse">
|
||||
<thead className="bg-gradient-to-r from-gray-50 to-gray-100 border-b-2 border-gray-200 sticky top-0 z-10">
|
||||
<tr>
|
||||
<SortHeader
|
||||
label="Name"
|
||||
isActive={sortKey === 'name'}
|
||||
direction={direction}
|
||||
onClick={() => onSort('name')}
|
||||
/>
|
||||
<SortHeader
|
||||
label="Email"
|
||||
isActive={sortKey === 'email'}
|
||||
direction={direction}
|
||||
onClick={() => onSort('email')}
|
||||
/>
|
||||
<SortHeader
|
||||
label="Job Title"
|
||||
isActive={sortKey === 'job'}
|
||||
direction={direction}
|
||||
onClick={() => onSort('job')}
|
||||
/>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.length > 0 ? (
|
||||
items.map((user, index) => (
|
||||
<tr
|
||||
key={user.uuid}
|
||||
onClick={() => onSelectUser(user)}
|
||||
className={`
|
||||
cursor-pointer transition-all duration-150 ease-out
|
||||
border-b border-gray-100 last:border-b-0
|
||||
${selectedUser?.uuid === user.uuid
|
||||
? 'bg-gradient-to-r from-blue-50 to-blue-50 shadow-sm'
|
||||
: index % 2 === 0
|
||||
? 'bg-white hover:bg-blue-50'
|
||||
: 'bg-gray-50 hover:bg-blue-50'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<td className={`${cellStyles}`}>{user.name}</td>
|
||||
<td className={`${cellStyles}`}>{user.email}</td>
|
||||
<td className={`${cellStyles}`}>{user.job}</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={3} className="text-center py-12 text-gray-400 text-sm font-medium">
|
||||
No users found
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
48
apps/admin/src/hooks/useFilters.ts
Normal file
48
apps/admin/src/hooks/useFilters.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
export interface Filters {
|
||||
name: string
|
||||
email: string
|
||||
job: string
|
||||
}
|
||||
|
||||
export const useFilters = () => {
|
||||
const [filters, setFilters] = useState<Filters>({ name: '', email: '', job: '' })
|
||||
|
||||
// Lire les filtres depuis l'URL au montage
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
const urlFilters: Filters = {
|
||||
name: params.get('name') || '',
|
||||
email: params.get('email') || '',
|
||||
job: params.get('job') || '',
|
||||
}
|
||||
setFilters(urlFilters)
|
||||
}, [])
|
||||
|
||||
// Updater les filtres et l'URL
|
||||
const updateFilters = (newFilters: Partial<Filters>) => {
|
||||
const updated = { ...filters, ...newFilters }
|
||||
setFilters(updated)
|
||||
|
||||
// Construire les query params
|
||||
const params = new URLSearchParams()
|
||||
if (updated.name) params.set('name', updated.name)
|
||||
if (updated.email) params.set('email', updated.email)
|
||||
if (updated.job) params.set('job', updated.job)
|
||||
|
||||
// Updater l'URL sans rechargement
|
||||
const queryString: string = params.toString()
|
||||
const newUrl: string = queryString
|
||||
? `${window.location.pathname}?${queryString}`
|
||||
: window.location.pathname
|
||||
window.history.replaceState({}, '', newUrl)
|
||||
}
|
||||
|
||||
// Réinitialiser les filtres
|
||||
const clearFilters = () => {
|
||||
updateFilters({ name: '', email: '', job: '' })
|
||||
}
|
||||
|
||||
return { filters, updateFilters, clearFilters }
|
||||
}
|
||||
77
apps/admin/src/hooks/useSort.ts
Normal file
77
apps/admin/src/hooks/useSort.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { useState, useCallback } from 'react'
|
||||
|
||||
export type SortDirection = 'asc' | 'desc' | null
|
||||
export type SortableKey<T> = keyof T
|
||||
|
||||
interface UseSortOptions<T> {
|
||||
initialSortKey?: SortableKey<T>
|
||||
initialDirection?: SortDirection
|
||||
}
|
||||
|
||||
export const useSort = <T extends Record<string, string | number>>(
|
||||
items: T[],
|
||||
options?: UseSortOptions<T>
|
||||
) => {
|
||||
const [sortKey, setSortKey] = useState<SortableKey<T> | null>(
|
||||
options?.initialSortKey ?? null
|
||||
)
|
||||
const [direction, setDirection] = useState<SortDirection>(
|
||||
options?.initialDirection ?? null
|
||||
)
|
||||
|
||||
const handleSort = useCallback(
|
||||
(key: SortableKey<T>) => {
|
||||
// Si on clique sur la même colonne, on change de direction
|
||||
if (sortKey === key) {
|
||||
if (direction === 'asc') {
|
||||
setDirection('desc')
|
||||
} else if (direction === 'desc') {
|
||||
// Cycle: asc -> desc -> null
|
||||
setSortKey(null)
|
||||
setDirection(null)
|
||||
} else {
|
||||
setDirection('asc')
|
||||
}
|
||||
} else {
|
||||
// Si on clique sur une nouvelle colonne, on sort en asc
|
||||
setSortKey(key)
|
||||
setDirection('asc')
|
||||
}
|
||||
},
|
||||
[sortKey, direction]
|
||||
)
|
||||
|
||||
const sortedItems = [...items].sort((a, b) => {
|
||||
if (sortKey === null) return 0
|
||||
|
||||
const valueA = a[sortKey]
|
||||
const valueB = b[sortKey]
|
||||
|
||||
// Handle null/undefined
|
||||
if (valueA === null || valueA === undefined) return direction === 'asc' ? 1 : -1
|
||||
if (valueB === null || valueB === undefined) return direction === 'asc' ? -1 : 1
|
||||
|
||||
// Handle strings
|
||||
if (typeof valueA === 'string' && typeof valueB === 'string') {
|
||||
const comparison = valueA.localeCompare(valueB)
|
||||
return direction === 'asc' ? comparison : -comparison
|
||||
}
|
||||
|
||||
// Handle numbers
|
||||
if (typeof valueA === 'number' && typeof valueB === 'number') {
|
||||
return direction === 'asc' ? valueA - valueB : valueB - valueA
|
||||
}
|
||||
|
||||
// Default comparison
|
||||
if (valueA < valueB) return direction === 'asc' ? -1 : 1
|
||||
if (valueA > valueB) return direction === 'asc' ? 1 : -1
|
||||
return 0
|
||||
})
|
||||
|
||||
return {
|
||||
sortedItems,
|
||||
sortKey,
|
||||
direction,
|
||||
handleSort,
|
||||
}
|
||||
}
|
||||
14
apps/admin/src/index.html
Normal file
14
apps/admin/src/index.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Admin</title>
|
||||
<base href="/" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
11
apps/admin/src/main.tsx
Normal file
11
apps/admin/src/main.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import {StrictMode} from 'react'
|
||||
import * as ReactDOM from 'react-dom/client'
|
||||
import {App} from './components/App'
|
||||
import './styles.css'
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>
|
||||
)
|
||||
89
apps/admin/src/styles.css
Normal file
89
apps/admin/src/styles.css
Normal file
@@ -0,0 +1,89 @@
|
||||
html {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
|
||||
'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif,
|
||||
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
|
||||
'Noto Color Emoji';
|
||||
line-height: 1.5;
|
||||
tab-size: 4;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: inherit;
|
||||
line-height: inherit;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
p,
|
||||
pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
box-sizing: border-box;
|
||||
border-width: 0;
|
||||
border-style: solid;
|
||||
border-color: currentColor;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
pre {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||
'Liberation Mono', 'Courier New', monospace;
|
||||
}
|
||||
|
||||
svg {
|
||||
display: block;
|
||||
vertical-align: middle;
|
||||
shape-rendering: auto;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: rgba(55, 65, 81, 1);
|
||||
border-radius: 0.25rem;
|
||||
color: rgba(229, 231, 235, 1);
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||
'Liberation Mono', 'Courier New', monospace;
|
||||
overflow: auto;
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
.shadow {
|
||||
box-shadow: 0 0 #0000, 0 0 #0000, 0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
||||
0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.rounded {
|
||||
border-radius: 1.5rem;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: 768px;
|
||||
padding-bottom: 3rem;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
color: rgba(55, 65, 81, 1);
|
||||
width: 100%;
|
||||
}
|
||||
2
apps/admin/src/types.ts
Normal file
2
apps/admin/src/types.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
// Re-export User from models package for backward compatibility
|
||||
export type {User} from '@klx/models'
|
||||
11
apps/admin/src/utils/filterUsers.ts
Normal file
11
apps/admin/src/utils/filterUsers.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { User } from '../types'
|
||||
import { Filters } from '../hooks/useFilters'
|
||||
|
||||
export const filterUsers = (users: User[], filters: Filters): User[] => {
|
||||
return users.filter(user => {
|
||||
const nameMatch = user.name.toLowerCase().includes(filters.name.toLowerCase())
|
||||
const emailMatch = user.email.toLowerCase().includes(filters.email.toLowerCase())
|
||||
const jobMatch = user.job.toLowerCase().includes(filters.job.toLowerCase())
|
||||
return nameMatch && emailMatch && jobMatch
|
||||
})
|
||||
}
|
||||
35
apps/admin/tsconfig.app.json
Normal file
35
apps/admin/tsconfig.app.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"tsBuildInfoFile": "dist/tsconfig.app.tsbuildinfo",
|
||||
"jsx": "react-jsx",
|
||||
"lib": ["dom"],
|
||||
"types": ["node", "@nx/react/typings/cssmodule.d.ts", "@nx/react/typings/image.d.ts"],
|
||||
"rootDir": "src"
|
||||
},
|
||||
"exclude": [
|
||||
"out-tsc",
|
||||
"dist",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.tsx",
|
||||
"src/**/*.test.tsx",
|
||||
"src/**/*.spec.js",
|
||||
"src/**/*.test.js",
|
||||
"src/**/*.spec.jsx",
|
||||
"src/**/*.test.jsx",
|
||||
"eslint.config.js",
|
||||
"eslint.config.cjs",
|
||||
"eslint.config.mjs"
|
||||
],
|
||||
"include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../packages/models/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/ui/tsconfig.lib.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
16
apps/admin/tsconfig.json
Normal file
16
apps/admin/tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../packages/models"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/ui"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
}
|
||||
],
|
||||
"extends": "../../tsconfig.base.json"
|
||||
}
|
||||
Reference in New Issue
Block a user