create driver page and client page also create oll driver , client layer
This commit is contained in:
22
Components/UI/Badge.tsx
Normal file
22
Components/UI/Badge.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
interface BadgeProps {
|
||||
label: string;
|
||||
color?: "indigo" | "green" | "amber" | "red" | "slate";
|
||||
}
|
||||
|
||||
const BADGE_COLORS: Record<string, string> = {
|
||||
indigo: "bg-indigo-100 text-indigo-700",
|
||||
green: "bg-emerald-100 text-emerald-700",
|
||||
amber: "bg-amber-100 text-amber-700",
|
||||
red: "bg-red-100 text-red-700",
|
||||
slate: "bg-slate-100 text-slate-600",
|
||||
};
|
||||
|
||||
export function Badge({ label, color = "slate" }: BadgeProps) {
|
||||
return (
|
||||
<span
|
||||
className={`inline-block rounded-full px-2.5 py-0.5 text-xs font-semibold ${BADGE_COLORS[color]}`}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
43
Components/UI/ConfirmDialog.tsx
Normal file
43
Components/UI/ConfirmDialog.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Button } from "./Button";
|
||||
import { Modal } from "./ModalProps";
|
||||
|
||||
interface ConfirmDialogProps {
|
||||
open: boolean;
|
||||
title: string;
|
||||
description: string;
|
||||
confirmLabel?: string;
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export function ConfirmDialog({
|
||||
open,
|
||||
title,
|
||||
description,
|
||||
confirmLabel = "Delete",
|
||||
onConfirm,
|
||||
onCancel,
|
||||
loading = false,
|
||||
}: ConfirmDialogProps) {
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
title={title}
|
||||
onClose={onCancel}
|
||||
size="sm"
|
||||
footer={
|
||||
<>
|
||||
<Button variant="secondary" onClick={onCancel} disabled={loading}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="danger" onClick={onConfirm} loading={loading}>
|
||||
{confirmLabel}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<p className="text-sm text-slate-600">{description}</p>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
17
Components/UI/EmptyState.tsx
Normal file
17
Components/UI/EmptyState.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
interface EmptyStateProps {
|
||||
icon?: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
action?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function EmptyState({ icon = "📋", title, description, action }: EmptyStateProps) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center gap-3 py-16 text-center">
|
||||
<span className="text-5xl" aria-hidden="true">{icon}</span>
|
||||
<h3 className="text-base font-semibold text-slate-700">{title}</h3>
|
||||
{description && <p className="text-sm text-slate-500 max-w-xs">{description}</p>}
|
||||
{action && <div className="mt-2">{action}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
54
Components/UI/ModalProps.tsx
Normal file
54
Components/UI/ModalProps.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
interface ModalProps {
|
||||
open: boolean;
|
||||
title: string;
|
||||
onClose: () => void;
|
||||
children: React.ReactNode;
|
||||
/** Footer slot – typically action buttons */
|
||||
footer?: React.ReactNode;
|
||||
size?: "sm" | "md" | "lg";
|
||||
}
|
||||
|
||||
const MODAL_SIZES = { sm: "max-w-sm", md: "max-w-lg", lg: "max-w-2xl" };
|
||||
|
||||
export function Modal({ open, title, onClose, children, footer, size = "md" }: ModalProps) {
|
||||
if (!open) return null;
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="absolute inset-0 bg-black/40 backdrop-blur-sm"
|
||||
onClick={onClose}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{/* Panel */}
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="modal-title"
|
||||
className={`relative w-full ${MODAL_SIZES[size]} rounded-2xl bg-white shadow-2xl flex flex-col max-h-[90vh]`}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between border-b border-slate-100 px-6 py-4">
|
||||
<h2 id="modal-title" className="text-base font-semibold text-slate-800">
|
||||
{title}
|
||||
</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
aria-label="Close"
|
||||
className="text-slate-400 hover:text-slate-600 transition-colors text-xl leading-none"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
{/* Body */}
|
||||
<div className="flex-1 overflow-y-auto px-6 py-5">{children}</div>
|
||||
{/* Footer */}
|
||||
{footer && (
|
||||
<div className="flex justify-end gap-2 border-t border-slate-100 px-6 py-4">
|
||||
{footer}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
29
Components/UI/PageHeader.tsx
Normal file
29
Components/UI/PageHeader.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
interface PageHeaderProps {
|
||||
title: string;
|
||||
description?: string;
|
||||
action?: React.ReactNode;
|
||||
backHref?: string;
|
||||
backLabel?: string;
|
||||
}
|
||||
|
||||
export function PageHeader({ title, description, action, backHref, backLabel }: PageHeaderProps) {
|
||||
return (
|
||||
<div className="mb-6 flex flex-col gap-1 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div>
|
||||
{backHref && (
|
||||
<a
|
||||
href={backHref}
|
||||
className="mb-2 inline-flex items-center gap-1 text-xs text-indigo-600 hover:text-indigo-800 font-medium"
|
||||
>
|
||||
← {backLabel ?? "Back"}
|
||||
</a>
|
||||
)}
|
||||
<h1 className="text-2xl font-bold tracking-tight text-slate-900">{title}</h1>
|
||||
{description && (
|
||||
<p className="mt-1 text-sm text-slate-500">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
{action && <div className="mt-3 sm:mt-0 shrink-0">{action}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
52
Components/UI/Select.tsx
Normal file
52
Components/UI/Select.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import React, { forwardRef, SelectHTMLAttributes } from "react";
|
||||
|
||||
|
||||
|
||||
|
||||
// ─── Select ──────────────────────────────────────────────────────────────────
|
||||
|
||||
interface SelectProps extends SelectHTMLAttributes<HTMLSelectElement> {
|
||||
label?: string;
|
||||
error?: string;
|
||||
wrapperClassName?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const Select = forwardRef<HTMLSelectElement, SelectProps>(function Select(
|
||||
{ label, error, wrapperClassName = "", className = "", id, children, ...rest },
|
||||
ref
|
||||
) {
|
||||
const selectId = id ?? label?.toLowerCase().replace(/\s+/g, "-");
|
||||
return (
|
||||
<div className={`flex flex-col gap-1 ${wrapperClassName}`}>
|
||||
{label && (
|
||||
<label
|
||||
htmlFor={selectId}
|
||||
className="text-xs font-semibold uppercase tracking-wide text-slate-500"
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<select
|
||||
ref={ref}
|
||||
id={selectId}
|
||||
className={`
|
||||
w-full rounded-lg border bg-white px-3 py-2 text-sm text-slate-800
|
||||
focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500
|
||||
disabled:bg-slate-50 disabled:cursor-not-allowed
|
||||
${error ? "border-red-400" : "border-slate-300"}
|
||||
${className}
|
||||
`}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</select>
|
||||
{error && <p className="text-xs text-red-600">{error}</p>}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
41
Components/UI/Textarea.tsx
Normal file
41
Components/UI/Textarea.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { forwardRef } from "react";
|
||||
|
||||
interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
|
||||
label?: string;
|
||||
error?: string;
|
||||
wrapperClassName?: string;
|
||||
}
|
||||
|
||||
export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(function Textarea(
|
||||
{ label, error, wrapperClassName = "", className = "", id, ...rest },
|
||||
ref
|
||||
) {
|
||||
const taId = id ?? label?.toLowerCase().replace(/\s+/g, "-");
|
||||
return (
|
||||
<div className={`flex flex-col gap-1 ${wrapperClassName}`}>
|
||||
{label && (
|
||||
<label
|
||||
htmlFor={taId}
|
||||
className="text-xs font-semibold uppercase tracking-wide text-slate-500"
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<textarea
|
||||
ref={ref}
|
||||
id={taId}
|
||||
rows={3}
|
||||
className={`
|
||||
w-full rounded-lg border bg-white px-3 py-2 text-sm text-slate-800
|
||||
placeholder:text-slate-400 resize-none
|
||||
focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500
|
||||
disabled:bg-slate-50 disabled:text-slate-400
|
||||
${error ? "border-red-400" : "border-slate-300"}
|
||||
${className}
|
||||
`}
|
||||
{...rest}
|
||||
/>
|
||||
{error && <p className="text-xs text-red-600">{error}</p>}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -9,4 +9,8 @@ export { Button } from "./Button";
|
||||
export type { ButtonProps } from "./Button";
|
||||
|
||||
export { Input } from "./Input";
|
||||
export type { InputProps } from "./Input";
|
||||
export type { InputProps } from "./Input";
|
||||
|
||||
export { Textarea } from "./Textarea";
|
||||
|
||||
export { Select } from "./Select";
|
||||
|
||||
Reference in New Issue
Block a user