- Get Started
- Localhost
- Architecture
- Structure
- Pattern
- Page
- Layout
- Content
- Types
- Config
- Actions
- Queries
- Authorization
- Validation
- Form
- Table
- Detail
- Card
- Util
- Hooks
- List Params
- Views
- README.md
- ISSUE.md
- Technology Stack
- Database
- File
- CDN Assets
- Entry Points
- Dashboard
- Authentication
- Credentials
- OAuth
- Flow Diagrams
- Multi-Tenancy
- Offline
- Onboarding
- Onboarding Videos
- Add Values
- Admission
- Application
- Attendance
- Compliance
- Profile
- Exams
- Exam Wizard
- Timetable
- Classrooms
- Notifications
- Conference
- LMS (Lumos)
- Finance
- Fee Management
- Invoice
- Wallet
- Salary
- Payroll
- Timesheet
- Expenses
- Budget
- Receipt
- Accounts
- Banking
- Reports
- Dashboard
- Permissions
- Messages
- Integration Flow
- Provision
- AI Document Processing
- Document Intelligence
- Internationalization
- Translation
- Translation Guide
- Icons
- Docs Factory
- Inspiration
- Listings
- Teachers
- Students
- Catalog
- Library
- Contributing
- Code of conduct
- GitHub Workflow
- Database Seeds
- Database Safety
- Test Accounts
- Bulk import
- Playwright
- Prettier
- Block Rebound
- Transportation
The @/components/file module consolidates every file-related concern into one place. Six sub-modules, one import surface.
| Sub-module | Purpose |
|---|---|
| Upload | Cloud-based file upload with drag-and-drop and progress tracking |
| Browser | File manager UI with grid / list views |
| Export | Generate PDF, Excel, CSV, JSON from data |
| Import | Parse CSV / Excel with a validation wizard |
| Browser-native print and print-to-PDF | |
| Generate | Templated documents — invoice, receipt, certificate, report card, ID card, transcript |
Quick start
import {
Uploader,
ExportButton,
Importer,
PrintButton,
FileBrowser,
useUpload,
useExport,
} from "@/components/file"
// PDF templates pull @react-pdf/renderer — import them from their own module
import { useGenerate } from "@/components/file/generate/use-generate"
<Uploader folder="documents" category="document" onUpload={(urls) => console.log(urls)} />
<ExportButton
data={students}
config={{ columns: STUDENT_EXPORT_COLUMNS, filename: "students" }}
formats={["csv", "excel", "pdf"]}
/>
<Importer columns={STUDENT_IMPORT_COLUMNS} onImport={handleImport} />
<PrintButton contentRef={printRef}>Print Report</PrintButton>
<FileBrowser rootFolder="/school/documents" onSelect={handleSelect} />Upload
The Uploader component renders a drag-and-drop dropzone backed by S3 signed URLs.
<Uploader
folder="avatars"
category="image"
accept={{ "image/*": [".jpg", ".png", ".webp"] }}
maxSize={5 * 1024 * 1024}
maxFiles={1}
onUpload={(urls) => setAvatarUrl(urls[0])}
onError={(error) => toast.error(error)}
/>Presets
Ready-to-use configurations live alongside the Uploader: AvatarUploader, DocumentUploader, ImageUploader, VideoUploader. Each presets accept, maxSize, and category for the common shapes. Read source for exact props.
useUpload hook
The hook exposes the same upload pipeline imperatively (no UI), useful for custom triggers.
const { upload, progress, isUploading, error } = useUpload({
folder: "documents",
category: "document",
})
await upload(file) // returns the public URLServer actions
Two actions back the module:
| Action | Purpose |
|---|---|
getSignedUrl() | Mint a presigned PUT URL for the client to upload to S3. |
confirmUpload() | Persist the resulting FileObject row keyed by schoolId. |
Both validate auth() + getTenantContext() and write schoolId into every record.
Export
ExportButton opens a format picker (CSV, Excel, PDF, JSON) and triggers download.
<ExportButton
data={students}
config={{
columns: STUDENT_EXPORT_COLUMNS,
filename: "students",
title: "Student Roster",
}}
formats={["csv", "excel", "pdf"]}
/>For inline use without a picker, SimpleExportButton accepts a single format.
useExport hook
const { exportData, isExporting } = useExport()
await exportData({
format: "pdf",
data: students,
config: { columns: STUDENT_EXPORT_COLUMNS, filename: "students" },
})Columns
Per-feature column definitions live alongside the feature (e.g. students/columns-export.ts). They build on createColumn(key, label, options) helpers from @/components/file/export/columns. Common options: format, width, align, accessor.
Import
Importer renders a four-step wizard: upload → preview → map columns → validate.
<Importer
columns={STUDENT_IMPORT_COLUMNS}
template={{ filename: "students-template.xlsx", sheets: ["Students"] }}
onImport={async (rows) => {
const result = await bulkImportStudents(rows)
toast.success(`Imported ${result.created} of ${rows.length}`)
}}
/>useImport hook
const { parse, validate, isImporting } = useImport()
const rows = await parse(file)
const issues = await validate(rows, columns)Validation
validateRow(row, columns) checks every column's required, type, and custom validate(value, row). validateBatch(rows, columns) returns a per-row issue list and a totals summary.
PrintButton opens the browser print dialog scoped to a content ref.
const printRef = useRef<HTMLDivElement>(null)
<PrintButton contentRef={printRef} title="Student Report">
Print
</PrintButton>
<div ref={printRef}>{/* printable content */}</div>usePrint hook
const { print } = usePrint()
print(printRef.current, { title: "Student Report" })Generate
Pre-built templates render PDFs via @react-pdf/renderer. All return a Blob and a download trigger.
| Template | Helper | Use |
|---|---|---|
| Invoice | useGenerate.invoice() | Fee invoices, receipts |
| Receipt | useGenerate.receipt() | Payment confirmation |
| Certificate | useGenerate.certificate() | Course or achievement certificates |
| Report card | useGenerate.reportCard() | Term and annual reports |
| ID card | useGenerate.idCard() | Student / staff badges |
| Transcript | useGenerate.transcript() | Academic transcripts |
import { useGenerate } from "@/components/file/generate/use-generate"
const { invoice } = useGenerate()
const blob = await invoice({ schoolId, items, customer, total })generate is not re-exported from @/components/file: see What the barrel exports.
Each template accepts a typed payload and renders both a downloadable PDF and an inline preview component (InvoicePreview, etc.).
Browser
FileBrowser renders a tree-aware file manager rooted at any S3 prefix.
<FileBrowser
rootFolder={`/${schoolId}/documents`}
onSelect={(file) => setSelected(file)}
view="grid"
/>useBrowser hook
const { files, currentFolder, navigate, search } = useBrowser({
rootFolder: `/${schoolId}/documents`,
})Storage and limits
| Constraint | Default |
|---|---|
| Provider | AWS S3 + CloudFront CDN |
| Default size cap | 10 MB per file (per category override) |
| Image cap | 5 MB |
| Document cap | 25 MB |
| Video cap | 100 MB |
| Allowed types | Per-category whitelist; rejected uploads return error code |
Multi-tenant safety
Every persisted file row carries schoolId. Signed URLs include a schoolId claim so cross-tenant URL forging is impossible. FileBrowser always rooted at a schoolId-scoped prefix.
// correct — schoolId scope on every query
await db.fileObject.findMany({ where: { schoolId, folder } })i18n
UI text comes from the dictionary.file slice. Helpers (getDictionary server-side, useI18nMessages on the client) provide the full set of labels for the picker, the wizard, error toasts, and progress states.
Errors
Server actions return error codes; the client maps them via useI18nMessages(dictionary).error.
| Code | Meaning |
|---|---|
FILE_TOO_LARGE | Exceeds the per-category size cap |
INVALID_FILE_TYPE | MIME type not in the category whitelist |
UPLOAD_FAILED | S3 returned non-2xx |
MISSING_TENANT | No schoolId on the session |
UNAUTHORIZED | auth() check failed |
INVALID_FOLDER | Folder path outside the schoolId-scoped tree |
TypeScript
All public exports are fully typed. Common types:
import type {
ExportConfig,
FileObject,
GenerateConfig,
ImportConfig,
PrintConfig,
UploadConfig,
} from "@/components/file"What the barrel exports
@/components/file is imported by client components (ExportButton, FileUploader, …), and a re-export is bundled whether or not the importer uses it. Until 2026-09-19 the barrel re-exported the whole block, and 44–49 routes — students, attendance, grades, classrooms, exams, finance, teachers — shipped @react-pdf/renderer (462 KB gzip), the Node crypto polyfill (137 KB), xlsx (135 KB) and the AWS SDK (45 KB) in their initial JavaScript to draw a table with an Export button. Removing those re-exports took 870 KB gzip off each of those routes.
So the barrel carries components, hooks, types and light helpers. These are imported from their own module:
| What | Import from | Why it stays out of the barrel |
|---|---|---|
Storage providers (getProvider, S3, R2) | @/components/file/providers · …/providers/factory | AWS / R2 SDKs, server-only |
| Rate limiting | @/components/file/rate-limit | Upstash client, server-only |
CDN signing (generateSignedUrl, …) | @/components/file/cdn | node:crypto, server-only |
Deduplication (generateFileHash, …) | @/components/file/deduplication | node:crypto, server-only |
PDF templates, useGenerate | @/components/file/generate/* | @react-pdf/renderer |
exportToExcel, exportToPdf, PDFPreview | @/components/file/export/excel-generator · …/export/pdf-generator | xlsx, @react-pdf/renderer |
Their types are still exported from the barrel. useExport and the spreadsheet parser load xlsx / @react-pdf/renderer with await import() when an export or a parse is actually asked for — follow the same pattern for anything heavy that serves a click rather than a render.
pnpm perf:check fails the build gate if one of these libraries re-enters a tracked route's initial JavaScript (performance guide).
Migration from the old layout
Older code imported from @/components/upload, @/components/export, @/components/import, etc. Migrate components and hooks to the barrel (heavy and server-only modules keep their own path — see above):
// before
import { ExportButton } from "@/components/export"
// after
import { ExportButton, Uploader } from "@/components/file"
import { Uploader } from "@/components/upload"The old module paths still re-export for compatibility but are deprecated. Open issues for any direct imports still in the wild.