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>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>EDR Freight Portal</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
<link rel="stylesheet" href="/index.css" />
</body>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>EDR Freight Portal</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -2,8 +2,9 @@ import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import "../index.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 {

View File

@@ -1,8 +1,6 @@
import { useMemo, useState } from "react";
import { useMemo } from "react";
import { Link } from "react-router-dom";
import {
ChevronLeft,
ChevronRight,
Clock3,
Eye,
Filter,
@@ -18,63 +16,155 @@ import {
import Breadcrumbs from "@/components/Breadcrumbs";
import NewCustomerPage from "./NewCustomerPage";
import DeleteCustomerDialog from "./DeleteCustomerDialog";
import { customers, type CustomerStatus } from "./customers.mock";
const PAGE_SIZE_OPTIONS = [5, 10, 25, 50];
import {
customers,
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() {
const [pageSize, setPageSize] = useState(10);
const [page, setPage] = useState(1);
const { pagination, setPagination } = usePagination({ pageSize: 10 });
const total = customers.length;
const totalPages = Math.max(1, Math.ceil(total / pageSize));
const safePage = Math.min(page, totalPages);
const start = (safePage - 1) * pageSize;
const end = Math.min(start + pageSize, total);
const paginated = useMemo(
const pageCount = Math.ceil(total / pagination.pageSize);
const start = pagination.pageIndex * pagination.pageSize;
const end = Math.min(start + pagination.pageSize, total);
const paginatedData = useMemo(
() => customers.slice(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 (
<div className="min-h-screen bg-slate-50 p-6">
<div className="mx-auto max-w-7xl space-y-6">
<div className="min-h-screen p-6">
<div className="space-y-6">
<Breadcrumbs items={[{ label: "Customers" }]} />
{/* Header */}
<div className="flex flex-col gap-4 rounded-3xl bg-white p-6 shadow-sm md:flex-row md:items-center md:justify-between">
<Card className="p-6 flex-row justify-between ">
<div>
<h1 className="text-3xl font-bold tracking-tight text-slate-900">
Customers
</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.
</p>
</div>
<div className="flex flex-col items-stretch gap-3 sm:flex-row sm:items-center">
<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" />
<input
<Search className="pointer-events-none absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400" />
<Input
type="search"
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>
<NewCustomerPage>
<button
type="button"
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" />
<Button>
<Plus />
Add Customer
</button>
</Button>
</NewCustomerPage>
</div>
</div>
</Card>
{/* Stats */}
<div className="grid gap-4 md:grid-cols-3">
<StatCard
title="Total Customers"
@@ -95,221 +185,42 @@ export default function CustomerPage() {
/>
</div>
{/* Customer Table */}
<div className="overflow-hidden rounded-3xl bg-white shadow-sm">
<div className="flex items-center justify-between border-b border-slate-100 px-6 py-4">
<Card className="gap-0">
<CardHeader className="flex flex-row items-center justify-between border-b ">
<div>
<h2 className="text-lg font-semibold text-slate-900">
Customer List
</h2>
<p className="text-sm text-slate-500">
<CardTitle>Customer List</CardTitle>
<CardDescription>
Recent customer activities and records.
</p>
</CardDescription>
</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]">
<Filter className="h-4 w-4" />
<Button variant="secondary" size="sm">
<Filter />
Filter
</button>
</div>
</Button>
</CardHeader>
<div className="overflow-x-auto">
<table className="w-full min-w-[700px] whitespace-nowrap text-left">
<thead className="bg-slate-50 text-sm text-slate-500">
<tr>
<th className="px-6 py-4 font-medium">Customer</th>
<th className="px-6 py-4 font-medium">Company</th>
<th className="px-6 py-4 font-medium">Email</th>
<th className="px-6 py-4 font-medium">Status</th>
<th className="px-6 py-4 font-medium text-right">Actions</th>
</tr>
</thead>
<tbody>
{paginated.map((customer) => (
<tr
key={customer.id}
className="border-t border-slate-100 transition hover:bg-[#33578D]/5"
>
<td className="px-6 py-4">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-[#33578D]/10 text-[#33578D]">
<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>
<CardContent className="px-0">
<DataTable
columns={columns}
data={paginatedData}
status="success"
onRowClick={() => { }}
pagination={{
pageIndex: pagination.pageIndex,
pageSize: pagination.pageSize,
pageCount: pageCount,
totalCount: total,
}}
tableOptions={{
state: { pagination },
onPaginationChange: setPagination,
}}
containerClassName="border-b shadow-none"
footer={DataTableFooter}
/>
</CardContent>
</Card>
</div>
</div>
);
@@ -325,8 +236,8 @@ function StatCard({
icon: React.ReactNode;
}) {
return (
<div className="rounded-3xl bg-white p-6 shadow-sm transition hover:shadow-md hover:ring-1 hover:ring-[#33578D]/20">
<div className="flex items-center justify-between">
<Card className="transition hover:shadow-md hover:ring-1 hover:ring-[#33578D]/20">
<CardContent className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-500">{title}</p>
<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]">
{icon}
</div>
</div>
</div>
</CardContent>
</Card>
);
}

View File

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