mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 17:45:42 +00:00
Merge freight/develop into feature/trains-management
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Eraser } from "lucide-react";
|
||||
|
||||
import { Button } from "@edr/ui-common";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface ContractSignaturePadProps {
|
||||
onChange: (dataUrl: string | null) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ContractSignaturePad({
|
||||
onChange,
|
||||
className,
|
||||
}: ContractSignaturePadProps) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const drawing = useRef(false);
|
||||
const [empty, setEmpty] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const w = canvas.offsetWidth;
|
||||
const h = canvas.offsetHeight;
|
||||
canvas.width = w * dpr;
|
||||
canvas.height = h * dpr;
|
||||
ctx.scale(dpr, dpr);
|
||||
ctx.strokeStyle = "#111";
|
||||
ctx.lineWidth = 2;
|
||||
ctx.lineCap = "round";
|
||||
}, []);
|
||||
|
||||
const getPos = (e: React.MouseEvent | React.TouchEvent) => {
|
||||
const canvas = canvasRef.current!;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
if ("touches" in e) {
|
||||
const t = e.touches[0];
|
||||
return { x: t.clientX - rect.left, y: t.clientY - rect.top };
|
||||
}
|
||||
return { x: e.clientX - rect.left, y: e.clientY - rect.top };
|
||||
};
|
||||
|
||||
const start = (e: React.MouseEvent | React.TouchEvent) => {
|
||||
drawing.current = true;
|
||||
const ctx = canvasRef.current?.getContext("2d");
|
||||
const { x, y } = getPos(e);
|
||||
ctx?.beginPath();
|
||||
ctx?.moveTo(x, y);
|
||||
};
|
||||
|
||||
const move = (e: React.MouseEvent | React.TouchEvent) => {
|
||||
if (!drawing.current) return;
|
||||
const ctx = canvasRef.current?.getContext("2d");
|
||||
const { x, y } = getPos(e);
|
||||
ctx?.lineTo(x, y);
|
||||
ctx?.stroke();
|
||||
setEmpty(false);
|
||||
onChange(canvasRef.current?.toDataURL("image/png") ?? null);
|
||||
};
|
||||
|
||||
const end = () => {
|
||||
drawing.current = false;
|
||||
};
|
||||
|
||||
const clear = () => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
setEmpty(true);
|
||||
onChange(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn("space-y-2", className)}>
|
||||
<div className="overflow-hidden rounded-lg border-2 border-dashed border-border bg-white">
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="h-36 w-full touch-none cursor-crosshair"
|
||||
onMouseDown={start}
|
||||
onMouseMove={move}
|
||||
onMouseUp={end}
|
||||
onMouseLeave={end}
|
||||
onTouchStart={start}
|
||||
onTouchMove={move}
|
||||
onTouchEnd={end}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs text-muted-foreground">Draw your signature above</p>
|
||||
<Button type="button" variant="ghost" size="sm" className="gap-1" onClick={clear}>
|
||||
<Eraser className="size-3.5" />
|
||||
Clear
|
||||
</Button>
|
||||
</div>
|
||||
{empty && (
|
||||
<p className="text-xs text-amber-700">Signature is required before confirming.</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
47
apps/edr-freight-web/portal/src/components/ui/badge.tsx
Normal file
47
apps/edr-freight-web/portal/src/components/ui/badge.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import * as React from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { Slot } from "radix-ui";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||
secondary: "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||
destructive:
|
||||
"bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90",
|
||||
outline:
|
||||
"border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 [a&]:hover:underline",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant = "default",
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"span"> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot.Root : "span";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="badge"
|
||||
data-variant={variant}
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants };
|
||||
8
apps/edr-freight-web/portal/src/components/ui/index.ts
Normal file
8
apps/edr-freight-web/portal/src/components/ui/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export * from './table';
|
||||
export * from './badge';
|
||||
export * from './button';
|
||||
export * from './dialog';
|
||||
export * from './input';
|
||||
export * from './label';
|
||||
export * from './textarea';
|
||||
export * from './Breadcrumbs';
|
||||
114
apps/edr-freight-web/portal/src/components/ui/table.tsx
Normal file
114
apps/edr-freight-web/portal/src/components/ui/table.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="table-container"
|
||||
className="relative w-full overflow-x-auto"
|
||||
>
|
||||
<table
|
||||
data-slot="table"
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
||||
return (
|
||||
<thead
|
||||
data-slot="table-header"
|
||||
className={cn("[&_tr]:border-b outline-ring/50", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
||||
return (
|
||||
<tbody
|
||||
data-slot="table-body"
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
||||
return (
|
||||
<tfoot
|
||||
data-slot="table-footer"
|
||||
className={cn(
|
||||
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
||||
return (
|
||||
<tr
|
||||
data-slot="table-row"
|
||||
className={cn(
|
||||
"border-b transition-colors bg-background hover:bg-muted/50 has-aria-expanded:bg-muted/50 data-[state=selected]:bg-muted",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
||||
return (
|
||||
<th
|
||||
data-slot="table-head"
|
||||
className={cn(
|
||||
"h-10 bg-muted first-of-type:pl-4 last-of-type:pr-4 first-of-type: p-2 py-4 text-left align-middle font-medium whitespace-nowrap text-secondary-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||
return (
|
||||
<td
|
||||
data-slot="table-cell"
|
||||
className={cn(
|
||||
"p-2 align-middle first-of-type:pl-4 last-of-type:pr-4 whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TableCaption({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"caption">) {
|
||||
return (
|
||||
<caption
|
||||
data-slot="table-caption"
|
||||
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
};
|
||||
Reference in New Issue
Block a user