Onboarding documents
Upload your signed contract, ID and proof of address before your first day.
Drag files here or browse
PDF, JPG or PNG up to 10 MB each
Documents uploaded1 of 3
- employment_contract_signed.pdfUpload complete412 kB · Uploaded
- passport_scan.jpgUploading1,860 kB · 40%
- proof_of_address_october.pdfUpload failed12,400 kB · Larger than 10 MB. Compress it and try again.
file-upload.tsx
"use client";
import { useEffect, useState } from "react";
import { LuFileText, LuImage } from "react-icons/lu";
import { Dropzone, FileItem, ProgressBar } from "@nexera-ui/react";
interface Upload {
id: string;
name: string;
sizeKb: number;
isImage: boolean;
progress: number;
failed?: boolean;
}
const INITIAL: Upload[] = [
{ id: "u1", name: "employment_contract_signed.pdf", sizeKb: 412, isImage: false, progress: 100 },
{ id: "u2", name: "passport_scan.jpg", sizeKb: 1860, isImage: true, progress: 40 },
{
id: "u3",
name: "proof_of_address_october.pdf",
sizeKb: 12400,
isImage: false,
progress: 0,
failed: true,
},
];
const kb = new Intl.NumberFormat("en-GB", {
style: "unit",
unit: "kilobyte",
maximumFractionDigits: 0,
});
/** File upload: drop onboarding documents and watch each one upload, with retry for failures. */
export default function FileUpload() {
const [files, setFiles] = useState(INITIAL);
const [rejected, setRejected] = useState<string | null>(null);
const uploading = files.some((f) => !f.failed && f.progress < 100);
// Simulated upload: advances every 400 ms and stops once nothing is uploading. Replace with your upload progress events.
useEffect(() => {
if (!uploading) return;
const timer = setTimeout(() => {
setFiles((list) =>
list.map((f) =>
f.failed || f.progress >= 100 ? f : { ...f, progress: Math.min(100, f.progress + 15) },
),
);
}, 400);
return () => {
clearTimeout(timer);
};
}, [files, uploading]);
const done = files.filter((f) => f.progress >= 100).length;
const update = (id: string, patch: Partial<Upload>) => {
setFiles((list) => list.map((f) => (f.id === id ? { ...f, ...patch } : f)));
};
return (
<div className="@container bg-page px-4 py-8 @3xl:px-8">
<section
aria-labelledby="upload-title"
className="mx-auto grid max-w-2xl gap-5 rounded-xl border border-default bg-surface p-5 shadow-sm @3xl:p-8"
>
<header className="grid gap-1">
<h2 id="upload-title" className="text-heading-h3 text-primary">
Onboarding documents
</h2>
<p className="text-body-default text-secondary">
Upload your signed contract, ID and proof of address before your first day.
</p>
</header>
<Dropzone
title="Drag files here or browse"
description="PDF, JPG or PNG up to 10 MB each"
acceptedFileTypes={["application/pdf", "image/jpeg", "image/png"]}
allowsMultiple
isInvalid={rejected !== null}
errorMessage={rejected ? `${rejected} is not a PDF, JPG or PNG file.` : undefined}
onSelect={(selected) => {
setRejected(null);
setFiles((list) => [
...list,
...selected.map((file, i) => ({
id: `${file.name}-${list.length + i}`,
name: file.name,
sizeKb: Math.max(1, Math.round(file.size / 1024)),
isImage: file.type.startsWith("image/"),
progress: 0,
})),
]);
}}
onReject={(list) => {
setRejected(list.map((file) => file.name).join(", "));
}}
/>
<ProgressBar
label="Documents uploaded"
labelPosition="top"
value={files.length ? (done / files.length) * 100 : 0}
valueLabel={`${done} of ${files.length}`}
/>
<ul className="grid gap-3" aria-label="Files">
{files.map((f) => {
const status = f.failed ? "error" : f.progress >= 100 ? "complete" : "uploading";
return (
<li key={f.id}>
<FileItem
name={f.name}
icon={f.isImage ? <LuImage /> : <LuFileText />}
status={status}
progress={f.progress}
meta={
status === "error"
? `${kb.format(f.sizeKb)} · Larger than 10 MB. Compress it and try again.`
: status === "complete"
? `${kb.format(f.sizeKb)} · Uploaded`
: `${kb.format(f.sizeKb)} · ${f.progress}%`
}
onRemove={() => {
setFiles((list) => list.filter((x) => x.id !== f.id));
}}
onRetry={() => {
update(f.id, { failed: false, progress: 0 });
}}
/>
</li>
);
})}
</ul>
</section>
</div>
);
}
More forms and data entry blocks
- Contact formName, email, topic and message with validation.
- Add a personCreate a record in a modal.
- Multi-step formLong forms split into steps you can go back to.
- Address and phoneCountry, address lookup and phone with dialling code.
- Date range pickerChoose a start and end date on a calendar.
- Search with suggestionsType ahead and pick from matching results.
- Terms and consentScrollable terms with a required checkbox.
- Choose an optionPick one option from a short list in a dialog.
- Pick a colourBrand or label colour from swatches or a picker.