feat(portal): Integrate @edr/ui-common components and centralize styling

This commit is contained in:
ghost2023
2026-05-20 11:35:44 +03:00
parent b9ef5a2b46
commit 1cf67c45ca
4 changed files with 169 additions and 255 deletions

View File

@@ -1,13 +1,15 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head>
<meta charset="UTF-8" /> <head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta charset="UTF-8" />
<title>EDR Freight Portal</title> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head> <title>EDR Freight Portal</title>
<body> </head>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script> <body>
<link rel="stylesheet" href="/index.css" /> <div id="root"></div>
</body> <script type="module" src="/src/main.tsx"></script>
</body>
</html> </html>

View File

@@ -2,8 +2,9 @@ import { StrictMode } from "react";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router-dom"; import { BrowserRouter } from "react-router-dom";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import "../index.css";
import "@tria-plc/iamui-common/styles.css"; import "@tria-plc/iamui-common/styles.css";
import "@edr/ui-common/styles.css" import "@edr/ui-common/styles.css";
import App from "./App"; import App from "./App";
import { import {

View File

@@ -1,8 +1,6 @@
import { useMemo, useState } from "react"; import { useMemo } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { import {
ChevronLeft,
ChevronRight,
Clock3, Clock3,
Eye, Eye,
Filter, Filter,
@@ -18,63 +16,155 @@ import {
import Breadcrumbs from "@/components/Breadcrumbs"; import Breadcrumbs from "@/components/Breadcrumbs";
import NewCustomerPage from "./NewCustomerPage"; import NewCustomerPage from "./NewCustomerPage";
import DeleteCustomerDialog from "./DeleteCustomerDialog"; import DeleteCustomerDialog from "./DeleteCustomerDialog";
import { customers, type CustomerStatus } from "./customers.mock"; import {
customers,
const PAGE_SIZE_OPTIONS = [5, 10, 25, 50]; type CustomerStatus,
type Customer,
} from "./customers.mock";
import {
DataTable,
DataTableFooter,
type ColumnDef,
usePagination,
Button,
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
Input,
} from "@edr/ui-common";
export default function CustomerPage() { export default function CustomerPage() {
const [pageSize, setPageSize] = useState(10); const { pagination, setPagination } = usePagination({ pageSize: 10 });
const [page, setPage] = useState(1);
const total = customers.length; const total = customers.length;
const totalPages = Math.max(1, Math.ceil(total / pageSize)); const pageCount = Math.ceil(total / pagination.pageSize);
const safePage = Math.min(page, totalPages); const start = pagination.pageIndex * pagination.pageSize;
const start = (safePage - 1) * pageSize; const end = Math.min(start + pagination.pageSize, total);
const end = Math.min(start + pageSize, total);
const paginated = useMemo( const paginatedData = useMemo(
() => customers.slice(start, end), () => customers.slice(start, end),
[start, end], [start, end],
); );
const columns: ColumnDef<Customer>[] = [
{
accessorKey: "name",
header: "Customer",
cell: ({ row }) => {
const customer = row.original;
return (
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-secondary text-secondary-foreground border">
<User className="h-5 w-5" />
</div>
<div>
<p className="font-medium text-slate-900">{customer.name}</p>
<p className="text-sm text-slate-500">ID #{customer.id}</p>
</div>
</div>
);
},
},
{
accessorKey: "company",
header: "Company",
},
{
accessorKey: "email",
header: "Email",
cell: ({ row }) => (
<span className="text-sm text-slate-700">{row.original.email}</span>
),
},
{
accessorKey: "status",
header: "Status",
cell: ({ row }) => <StatusBadge status={row.original.status} />,
},
{
id: "actions",
header: () => <div className="text-right">Actions</div>,
cell: ({ row }) => {
const customer = row.original;
return (
<div className="flex justify-end gap-2">
<Link
to={`/customers/${customer.id}`}
className="rounded-xl border border-slate-200 p-2 text-slate-600 transition hover:border-[#33578D]/30 hover:bg-[#33578D]/10 hover:text-[#33578D]"
>
<Eye className="h-4 w-4" />
</Link>
<NewCustomerPage
mode="edit"
customer={{
companyName: customer.company,
customerType: customer.customerType,
contactPerson: customer.name,
email: customer.email,
phone: customer.phone,
tinNumber: customer.tinNumber,
city: customer.city,
country: customer.country,
address: customer.address,
notes: customer.notes,
}}
>
<Button variant="outline">
<Pencil />
</Button>
</NewCustomerPage>
<DeleteCustomerDialog customerName={customer.name}>
<button
type="button"
className="rounded-xl border border-red-200 p-2 text-red-600 transition hover:bg-red-50"
>
<Trash2 className="h-4 w-4" />
</button>
</DeleteCustomerDialog>
</div>
);
},
},
];
return ( return (
<div className="min-h-screen bg-slate-50 p-6"> <div className="min-h-screen p-6">
<div className="mx-auto max-w-7xl space-y-6"> <div className="space-y-6">
<Breadcrumbs items={[{ label: "Customers" }]} /> <Breadcrumbs items={[{ label: "Customers" }]} />
{/* Header */} <Card className="p-6 flex-row justify-between ">
<div className="flex flex-col gap-4 rounded-3xl bg-white p-6 shadow-sm md:flex-row md:items-center md:justify-between">
<div> <div>
<h1 className="text-3xl font-bold tracking-tight text-slate-900"> <h1 className="text-3xl font-bold tracking-tight text-slate-900">
Customers Customers
</h1> </h1>
<p className="mt-1 text-sm text-slate-500"> <p className="mt-1 text-sm text-secondary-foreground ">
Manage and monitor your customer records. Manage and monitor your customer records.
</p> </p>
</div> </div>
<div className="flex flex-col items-stretch gap-3 sm:flex-row sm:items-center"> <div className="flex flex-col items-stretch gap-3 sm:flex-row sm:items-center">
<div className="relative w-full sm:w-80"> <div className="relative w-full sm:w-80">
<Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400" /> <Search className="pointer-events-none absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400" />
<input <Input
type="search" type="search"
placeholder="Search customers..." placeholder="Search customers..."
className="h-10 w-full rounded-2xl border border-slate-200 bg-white pl-10 pr-4 text-sm text-slate-700 outline-none transition placeholder:text-slate-400 focus:border-[#33578D]/50 focus:ring-2 focus:ring-[#33578D]/20" className="pl-8!"
/> />
</div> </div>
<NewCustomerPage> <NewCustomerPage>
<button <Button>
type="button" <Plus />
className="inline-flex items-center justify-center gap-2 rounded-2xl bg-[#33578D] px-4 py-2 text-sm font-medium text-white transition hover:bg-[#33578D]/90"
>
<Plus className="h-4 w-4" />
Add Customer Add Customer
</button> </Button>
</NewCustomerPage> </NewCustomerPage>
</div> </div>
</div> </Card>
{/* Stats */}
<div className="grid gap-4 md:grid-cols-3"> <div className="grid gap-4 md:grid-cols-3">
<StatCard <StatCard
title="Total Customers" title="Total Customers"
@@ -95,221 +185,42 @@ export default function CustomerPage() {
/> />
</div> </div>
{/* Customer Table */} <Card className="gap-0">
<div className="overflow-hidden rounded-3xl bg-white shadow-sm"> <CardHeader className="flex flex-row items-center justify-between border-b ">
<div className="flex items-center justify-between border-b border-slate-100 px-6 py-4">
<div> <div>
<h2 className="text-lg font-semibold text-slate-900"> <CardTitle>Customer List</CardTitle>
Customer List <CardDescription>
</h2>
<p className="text-sm text-slate-500">
Recent customer activities and records. Recent customer activities and records.
</p> </CardDescription>
</div> </div>
<button className="inline-flex items-center gap-2 rounded-2xl border border-slate-200 px-4 py-2 text-sm font-medium text-slate-700 transition hover:border-[#33578D]/30 hover:bg-[#33578D]/10 hover:text-[#33578D]"> <Button variant="secondary" size="sm">
<Filter className="h-4 w-4" /> <Filter />
Filter Filter
</button> </Button>
</div> </CardHeader>
<div className="overflow-x-auto"> <CardContent className="px-0">
<table className="w-full min-w-[700px] whitespace-nowrap text-left"> <DataTable
<thead className="bg-slate-50 text-sm text-slate-500"> columns={columns}
<tr> data={paginatedData}
<th className="px-6 py-4 font-medium">Customer</th> status="success"
<th className="px-6 py-4 font-medium">Company</th> onRowClick={() => { }}
<th className="px-6 py-4 font-medium">Email</th> pagination={{
<th className="px-6 py-4 font-medium">Status</th> pageIndex: pagination.pageIndex,
<th className="px-6 py-4 font-medium text-right">Actions</th> pageSize: pagination.pageSize,
</tr> pageCount: pageCount,
</thead> totalCount: total,
}}
<tbody> tableOptions={{
{paginated.map((customer) => ( state: { pagination },
<tr onPaginationChange: setPagination,
key={customer.id} }}
className="border-t border-slate-100 transition hover:bg-[#33578D]/5" containerClassName="border-b shadow-none"
> footer={DataTableFooter}
<td className="px-6 py-4"> />
<div className="flex items-center gap-3"> </CardContent>
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-[#33578D]/10 text-[#33578D]"> </Card>
<User className="h-5 w-5" />
</div>
<div>
<p className="font-medium text-slate-900">
{customer.name}
</p>
<p className="text-sm text-slate-500">
ID #{customer.id}
</p>
</div>
</div>
</td>
<td className="px-6 py-4 text-sm text-slate-700">
{customer.company}
</td>
<td className="px-6 py-4 text-sm text-slate-700">
{customer.email}
</td>
<td className="px-6 py-4">
<StatusBadge status={customer.status} />
</td>
<td className="px-6 py-4">
<div className="flex justify-end gap-2">
<Link
to={`/customers/${customer.id}`}
className="rounded-xl border border-slate-200 p-2 text-slate-600 transition hover:border-[#33578D]/30 hover:bg-[#33578D]/10 hover:text-[#33578D]"
>
<Eye className="h-4 w-4" />
</Link>
<NewCustomerPage
mode="edit"
customer={{
companyName: customer.company,
customerType: customer.customerType,
contactPerson: customer.name,
email: customer.email,
phone: customer.phone,
tinNumber: customer.tinNumber,
city: customer.city,
country: customer.country,
address: customer.address,
notes: customer.notes,
}}
>
<button
type="button"
className="rounded-xl border border-slate-200 p-2 text-slate-600 transition hover:border-[#33578D]/30 hover:bg-[#33578D]/10 hover:text-[#33578D]"
>
<Pencil className="h-4 w-4" />
</button>
</NewCustomerPage>
<DeleteCustomerDialog customerName={customer.name}>
<button
type="button"
className="rounded-xl border border-red-200 p-2 text-red-600 transition hover:bg-red-50"
>
<Trash2 className="h-4 w-4" />
</button>
</DeleteCustomerDialog>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
<Pagination
page={safePage}
pageSize={pageSize}
total={total}
totalPages={totalPages}
start={start}
end={end}
onPageChange={setPage}
onPageSizeChange={(size) => {
setPageSize(size);
setPage(1);
}}
/>
</div>
</div>
</div>
);
}
function Pagination({
page,
pageSize,
total,
totalPages,
start,
end,
onPageChange,
onPageSizeChange,
}: {
page: number;
pageSize: number;
total: number;
totalPages: number;
start: number;
end: number;
onPageChange: (page: number) => void;
onPageSizeChange: (size: number) => void;
}) {
const showingFrom = total === 0 ? 0 : start + 1;
return (
<div className="flex flex-col gap-3 border-t border-slate-100 px-6 py-4 md:flex-row md:items-center md:justify-between">
<div className="flex items-center gap-3 text-sm text-slate-500">
<label htmlFor="page-size" className="font-medium text-slate-700">
Rows per page
</label>
<select
id="page-size"
value={pageSize}
onChange={(e) => onPageSizeChange(Number(e.target.value))}
className="h-9 rounded-xl border border-slate-200 bg-white px-3 text-sm text-slate-700 outline-none transition focus:border-[#33578D]/50 focus:ring-2 focus:ring-[#33578D]/20"
>
{PAGE_SIZE_OPTIONS.map((size) => (
<option key={size} value={size}>
{size}
</option>
))}
</select>
<span>
Showing {showingFrom}{end} of {total}
</span>
</div>
<div className="flex items-center gap-1">
<button
type="button"
onClick={() => onPageChange(page - 1)}
disabled={page <= 1}
className="inline-flex h-9 items-center gap-1 rounded-xl border border-slate-200 px-3 text-sm font-medium text-slate-700 transition hover:border-[#33578D]/30 hover:bg-[#33578D]/10 hover:text-[#33578D] disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-transparent disabled:hover:text-slate-700"
>
<ChevronLeft className="h-4 w-4" />
Prev
</button>
{Array.from({ length: totalPages }, (_, i) => i + 1).map((p) => {
const isActive = p === page;
return (
<button
key={p}
type="button"
onClick={() => onPageChange(p)}
aria-current={isActive ? "page" : undefined}
className={
isActive
? "h-9 w-9 rounded-xl bg-[#33578D] text-sm font-medium text-white"
: "h-9 w-9 rounded-xl border border-slate-200 text-sm font-medium text-slate-700 transition hover:border-[#33578D]/30 hover:bg-[#33578D]/10 hover:text-[#33578D]"
}
>
{p}
</button>
);
})}
<button
type="button"
onClick={() => onPageChange(page + 1)}
disabled={page >= totalPages}
className="inline-flex h-9 items-center gap-1 rounded-xl border border-slate-200 px-3 text-sm font-medium text-slate-700 transition hover:border-[#33578D]/30 hover:bg-[#33578D]/10 hover:text-[#33578D] disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-transparent disabled:hover:text-slate-700"
>
Next
<ChevronRight className="h-4 w-4" />
</button>
</div> </div>
</div> </div>
); );
@@ -325,8 +236,8 @@ function StatCard({
icon: React.ReactNode; icon: React.ReactNode;
}) { }) {
return ( return (
<div className="rounded-3xl bg-white p-6 shadow-sm transition hover:shadow-md hover:ring-1 hover:ring-[#33578D]/20"> <Card className="transition hover:shadow-md hover:ring-1 hover:ring-[#33578D]/20">
<div className="flex items-center justify-between"> <CardContent className="flex items-center justify-between">
<div> <div>
<p className="text-sm text-slate-500">{title}</p> <p className="text-sm text-slate-500">{title}</p>
<h3 className="mt-2 text-3xl font-bold text-slate-900">{value}</h3> <h3 className="mt-2 text-3xl font-bold text-slate-900">{value}</h3>
@@ -335,8 +246,8 @@ function StatCard({
<div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-[#33578D]/10 text-[#33578D]"> <div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-[#33578D]/10 text-[#33578D]">
{icon} {icon}
</div> </div>
</div> </CardContent>
</div> </Card>
); );
} }

View File

@@ -9,9 +9,9 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog"; } from "@edr/ui-common";
import { Button } from "@/components/ui/button"; import { Button } from "@edr/ui-common";
export interface DeleteCustomerDialogProps { export interface DeleteCustomerDialogProps {
customerName: string; customerName: string;