197 lines
5.5 KiB
TypeScript
197 lines
5.5 KiB
TypeScript
import React from 'react';
|
|
|
|
type BannerProps = {
|
|
kind?: 'error' | 'info' | 'success';
|
|
children: React.ReactNode;
|
|
};
|
|
|
|
const styles = {
|
|
error: 'bg-red-50 text-red-800 border-red-200',
|
|
info: 'bg-sky-50 text-sky-800 border-sky-200',
|
|
success: 'bg-green-50 text-green-800 border-green-200',
|
|
};
|
|
|
|
export const Banner: React.FC<BannerProps> = ({ kind = 'info', children }) => (
|
|
<div className={`rounded-lg border px-4 py-3 text-sm ${styles[kind]}`}>{children}</div>
|
|
);
|
|
|
|
export const PageHeader: React.FC<{
|
|
title: string;
|
|
subtitle?: string;
|
|
icon?: React.ReactNode;
|
|
actions?: React.ReactNode;
|
|
}> = ({ title, subtitle, icon, actions }) => (
|
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 mb-6">
|
|
<div className="flex items-center">
|
|
{icon && (
|
|
<div className="bg-red-600 text-white p-3 rounded-lg mr-4">
|
|
<span className="text-2xl sm:text-3xl">{icon}</span>
|
|
</div>
|
|
)}
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-gray-800">{title}</h1>
|
|
{subtitle && <p className="text-sm text-gray-500 mt-1">{subtitle}</p>}
|
|
</div>
|
|
</div>
|
|
{actions}
|
|
</div>
|
|
);
|
|
|
|
export const Spinner: React.FC = () => (
|
|
<div className="flex items-center justify-center py-12 text-gray-400 gap-2">
|
|
<i className="fa-solid fa-spinner fa-spin" />
|
|
<span>Memuat...</span>
|
|
</div>
|
|
);
|
|
|
|
export const EmptyFarm: React.FC<{ onSettings: () => void }> = ({ onSettings }) => (
|
|
<Banner>
|
|
Pilih site, kandang, dan siklus, atau buat data di{' '}
|
|
<button type="button" className="font-semibold underline" onClick={onSettings}>
|
|
Pengaturan
|
|
</button>
|
|
.
|
|
</Banner>
|
|
);
|
|
|
|
type ConfirmDialogProps = {
|
|
open: boolean;
|
|
title: string;
|
|
message: React.ReactNode;
|
|
confirmLabel?: string;
|
|
cancelLabel?: string;
|
|
busy?: boolean;
|
|
onConfirm: () => void;
|
|
onCancel: () => void;
|
|
};
|
|
|
|
export const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
|
|
open,
|
|
title,
|
|
message,
|
|
confirmLabel = 'Lanjutkan',
|
|
cancelLabel = 'Batal',
|
|
busy = false,
|
|
onConfirm,
|
|
onCancel,
|
|
}) => {
|
|
if (!open) return null;
|
|
|
|
return (
|
|
<div
|
|
className="fixed inset-0 z-50 flex items-center justify-center bg-slate-900/50 p-4"
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-labelledby="confirm-dialog-title"
|
|
onClick={() => {
|
|
if (!busy) onCancel();
|
|
}}
|
|
>
|
|
<div
|
|
className="w-full max-w-md rounded-xl bg-white p-5 shadow-xl"
|
|
onClick={(event) => event.stopPropagation()}
|
|
>
|
|
<div className="mb-4 flex items-start gap-3">
|
|
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-red-600 text-white">
|
|
<i className="fa-solid fa-triangle-exclamation" aria-hidden />
|
|
</div>
|
|
<div className="min-w-0 pt-0.5">
|
|
<h3 id="confirm-dialog-title" className="text-lg font-semibold text-gray-800">
|
|
{title}
|
|
</h3>
|
|
<div className="mt-1 text-sm text-gray-600">{message}</div>
|
|
</div>
|
|
</div>
|
|
<div className="flex flex-wrap justify-end gap-2">
|
|
<button
|
|
type="button"
|
|
className="rounded-lg border border-gray-300 px-4 py-2 text-sm font-semibold text-gray-700 hover:bg-gray-50 disabled:opacity-50"
|
|
disabled={busy}
|
|
onClick={onCancel}
|
|
>
|
|
{cancelLabel}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="rounded-lg bg-red-600 px-4 py-2 text-sm font-semibold text-white hover:bg-red-700 disabled:opacity-50"
|
|
disabled={busy}
|
|
onClick={onConfirm}
|
|
>
|
|
{busy ? 'Memproses...' : confirmLabel}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
type AlertDialogProps = {
|
|
open: boolean;
|
|
title: string;
|
|
message: React.ReactNode;
|
|
dismissLabel?: string;
|
|
kind?: 'error' | 'info' | 'success';
|
|
onDismiss: () => void;
|
|
};
|
|
|
|
const alertIconStyles = {
|
|
error: 'bg-red-600',
|
|
info: 'bg-sky-600',
|
|
success: 'bg-green-600',
|
|
};
|
|
|
|
const alertIconClass = {
|
|
error: 'fa-triangle-exclamation',
|
|
info: 'fa-circle-info',
|
|
success: 'fa-circle-check',
|
|
};
|
|
|
|
export const AlertDialog: React.FC<AlertDialogProps> = ({
|
|
open,
|
|
title,
|
|
message,
|
|
dismissLabel = 'Tutup',
|
|
kind = 'error',
|
|
onDismiss,
|
|
}) => {
|
|
if (!open) return null;
|
|
|
|
return (
|
|
<div
|
|
className="fixed inset-0 z-50 flex items-center justify-center bg-slate-900/50 p-4"
|
|
role="alertdialog"
|
|
aria-modal="true"
|
|
aria-labelledby="alert-dialog-title"
|
|
onClick={onDismiss}
|
|
>
|
|
<div
|
|
className="w-full max-w-md rounded-xl bg-white p-5 shadow-xl"
|
|
onClick={(event) => event.stopPropagation()}
|
|
>
|
|
<div className="mb-4 flex items-start gap-3">
|
|
<div
|
|
className={`flex h-10 w-10 shrink-0 items-center justify-center rounded-lg text-white ${alertIconStyles[kind]}`}
|
|
>
|
|
<i className={`fa-solid ${alertIconClass[kind]}`} aria-hidden />
|
|
</div>
|
|
<div className="min-w-0 pt-0.5">
|
|
<h3 id="alert-dialog-title" className="text-lg font-semibold text-gray-800">
|
|
{title}
|
|
</h3>
|
|
<div className="mt-1 text-sm text-gray-600">{message}</div>
|
|
</div>
|
|
</div>
|
|
<div className="flex justify-end">
|
|
<button
|
|
type="button"
|
|
className="rounded-lg bg-red-600 px-4 py-2 text-sm font-semibold text-white hover:bg-red-700"
|
|
onClick={onDismiss}
|
|
>
|
|
{dismissLabel}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|