Fitur lengkap: - Auth JWT + RBAC (Admin/Teacher) + halaman login - Shell 3 kolom (left nav, topbar, right rail dashboard) - CRUD siswa + follow up kanban + status calon/student/ex - Dashboard: siswa aktif, kehadiran, bar sumber lead, perlu follow up - Pembayaran prorata per pertemuan (cash/transfer/qris) - Kelas, enrollment, generate sesi, kehadiran bulk - Jurnal aktivitas + upload multi-foto - Raport + placement test (tampil di detail siswa) - Pengaturan & seed data contoh Stack: Next.js 16, TypeScript, Tailwind v4, Prisma (SQLite), Recharts, Zod
58 lines
1.8 KiB
TypeScript
58 lines
1.8 KiB
TypeScript
import type { InputHTMLAttributes, LabelHTMLAttributes, SelectHTMLAttributes, TextareaHTMLAttributes } from "react";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
export function Label({ className, ...props }: LabelHTMLAttributes<HTMLLabelElement>) {
|
|
return <label className={cn("mb-1.5 block text-[13px] font-medium text-ink-700", className)} {...props} />;
|
|
}
|
|
|
|
const baseField =
|
|
"w-full rounded-[12px] border border-ink-300 bg-surface px-3 text-sm text-ink-900 placeholder:text-ink-500 transition-colors focus:border-brand-500 focus:outline-none focus:ring-[3px] focus:ring-brand-500/25 disabled:bg-ink-100";
|
|
|
|
export function Input({ className, ...props }: InputHTMLAttributes<HTMLInputElement>) {
|
|
return <input className={cn(baseField, "h-10", className)} {...props} />;
|
|
}
|
|
|
|
export function Textarea({ className, ...props }: TextareaHTMLAttributes<HTMLTextAreaElement>) {
|
|
return <textarea className={cn(baseField, "min-h-[88px] py-2.5", className)} {...props} />;
|
|
}
|
|
|
|
export function Select({ className, children, ...props }: SelectHTMLAttributes<HTMLSelectElement>) {
|
|
return (
|
|
<select className={cn(baseField, "h-10 appearance-none bg-[length:16px] pr-9", className)} {...props}>
|
|
{children}
|
|
</select>
|
|
);
|
|
}
|
|
|
|
export function Field({
|
|
label,
|
|
required,
|
|
hint,
|
|
error,
|
|
children,
|
|
className,
|
|
}: {
|
|
label?: string;
|
|
required?: boolean;
|
|
hint?: string;
|
|
error?: string;
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
}) {
|
|
return (
|
|
<div className={className}>
|
|
{label ? (
|
|
<Label>
|
|
{label} {required ? <span className="text-danger-500">*</span> : null}
|
|
</Label>
|
|
) : null}
|
|
{children}
|
|
{error ? (
|
|
<p className="mt-1 text-[12px] text-danger-500">{error}</p>
|
|
) : hint ? (
|
|
<p className="mt-1 text-[12px] text-ink-500">{hint}</p>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|