- 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 library uses a global-first architecture — every school sees the platform-wide CatalogBook catalog out of the box. No setup, no onboarding wiring. Schools can hide individual books; borrowing is tracked via lazily-provisioned school-scoped Book records.
Architecture
GLOBAL (no schoolId)
CatalogBook (PUBLISHED + APPROVED) ← all schools see by default
OPT-OUT (schoolId-scoped)
SchoolBookSelection
isActive: false → hidden for this school
no record → visible (default)
LAZY PROVISIONING
Book (school-scoped, auto-created on first detail page visit)
copied from CatalogBook, used for borrow / return tracking only
BorrowRecord (school-scoped)
userId, bookId, borrowDate, dueDate, status (BORROWED / RETURNED / OVERDUE)
| Approach | Setup | Data for 100 schools × 90 books | Sharing |
|---|---|---|---|
| Per-school provisioning | Admin adds | 9,000 Book records | None |
| Onboarding wiring | Auto | 9,000 Book records | None |
| Global-first (current) | Zero | 90 CatalogBooks (shared) | Visible to all |
Benefits: zero setup, no duplication, school autonomy (can hide), lazy efficiency, community contributions enrich all schools.
User flows
Student browses + borrows
Student visits /library
→ content.tsx queries CatalogBook directly (global)
excludes books hidden by school (SchoolBookSelection.isActive = false)
green banner, then ONE spotlighted book, then the rows
categorizes into rows: Latest, Featured, Literature, Science
→ click book card → /library/books/[id]
loads CatalogBook by ID
finds or creates school-scoped Book (lazy)
tinted hero: cover, grade, title, author, rating, borrow card
then About, Information, video, "More by Author", "You Might Also Like"
→ click "Borrow Book"
borrowBook() creates BorrowRecord + decrements availableCopies
→ click "Return Book"
returnBook() updates status to RETURNED + increments availableCopies
The spotlighted book
Between the banner and the shelves, the home page gives one book the whole width: its cover photograph, its title, its author, and its own opening paragraph rather than a summary of it. The paragraph is not clamped — it is kept to one paragraph in the seed instead.
The title is matched on the raw English string, because the catalog is authored centrally in English, and it is the one the section's photograph is of. Change the book and the picture stops matching the words.
Its Arabic is hand-written, not machine-produced. A general translator returns
a literal reading of the title where the published Arabic edition has a settled
name, so both the title and the excerpt are pinned in
src/components/translation/canonical.ts, which every translation path
consults before its caches and before any provider.
Admin manages library
/library/catalog → browse global catalog, select / deselect, adjust copies + shelf
/library/admin → dashboard
/library/admin/books → manage table (add from catalog, edit details, delete)
Teacher contributes
/library/contribute → contributeBook() creates CatalogBook (PENDING + DRAFT)
sets contributedBy + contributedSchoolId
SaaS /catalog/approvals → approve → APPROVED + PUBLISHED (visible to all)
reject → REJECTED + reason
Routes
| Route | Purpose | Auth |
|---|---|---|
/library | Library home (banner + rows) | All roles |
/library/books | All books with search / filter | All roles |
/library/books/[id] | Book detail + borrow / return | All roles |
/library/catalog | Browse global catalog | ADMIN, DEVELOPER |
/library/admin | Admin dashboard | ADMIN, DEVELOPER |
/library/admin/books | Manage books table | ADMIN, DEVELOPER |
/library/admin/books/new | Add book from catalog | ADMIN, DEVELOPER |
/library/contribute | Submit book contribution | ADMIN, TEACHER, DEVELOPER |
/library/contributions | My contributions history | ADMIN, TEACHER, DEVELOPER |
/library/my-profile | Borrow history + stats | All roles |
The home banner
/library opens on the same green brand banner as /live. It is one object rendered
from two places, not two lookalikes: hero.tsx here and status-hero.tsx under the live
block share the ground, the geometry, the type and the pills. When one changes, change
the other.
- Ground
#00bc6d, the saas-marketing green, with#050505ink. Both are pinned literals. The banner does not follow the theme, because white on this green measures about 2.5:1 and is unreadable. - Headline is thmanyah sans at ~38px, light, with one phrase carried in bold. That
phrase travels as its own dictionary key,
library.hero.titleMark, so a translator moves the words without carrying markup through a right-to-left string. A template with no{mark}renders unmarked. - Its two rows are width-matched, and the tatweels inside the Arabic string are what matches them: 333px over 333px in Arabic, 320px over 312px in English, against the live banner's 331 over 333. They are not typos. Editing the headline means measuring the two line boxes in a browser again, and it is why the two languages are transcreated rather than translated word-for-word -- a literal mirror of either line wraps to three rows.
- Two pills, never more: explore the shelves, then the reader's own favourites.
- Art is
public/anthropic/category-04-alpha.svg, a transparent line drawing sitting straight on the green with no card of its own. It is derived fromcategory-04.svgby recolouring one shape to the mint#9FE5B1; regenerate it, never hand-edit it. Below themdbreakpoint the card stacks to one column and the art is dropped rather than shrunk.
Data flow
Library home query
// Get books hidden by this school
const hiddenSelections = await db.schoolBookSelection.findMany({
where: { schoolId, isActive: false },
select: { catalogBookId: true },
})
const hiddenBookIds = new Set(hiddenSelections.map((s) => s.catalogBookId))
// Query global catalog, excluding hidden
const catalogBooks = await db.catalogBook.findMany({
where: {
status: "PUBLISHED",
approvalStatus: "APPROVED",
visibility: { in: ["PUBLIC", "SCHOOL"] },
...(hiddenBookIds.size > 0
? { id: { notIn: Array.from(hiddenBookIds) } }
: {}),
},
orderBy: [{ rating: "desc" }, { createdAt: "desc" }],
})Lazy book provisioning
// Load CatalogBook (global)
const catalogBook = await db.catalogBook.findFirst({
where: { id: bookId, status: "PUBLISHED", approvalStatus: "APPROVED" },
})
// Find or create school Book (for borrowing)
let schoolBook = await db.book.findFirst({
where: { schoolId, catalogBookId: bookId },
})
if (!schoolBook) {
schoolBook = await db.book.create({
data: {
schoolId,
catalogBookId: catalogBook.id,
title: catalogBook.title,
author: catalogBook.author,
// … copies CatalogBook fields
totalCopies: 3,
availableCopies: 3,
},
})
}Browsing uses CatalogBook (zero school data). Borrowing uses school Book (created on demand). No bulk provisioning.
Hide mechanism
await db.schoolBookSelection.upsert({
where: { schoolId_catalogBookId: { schoolId, catalogBookId } },
create: { schoolId, catalogBookId, isActive: false },
update: { isActive: false },
})- No record → visible (default)
isActive: true→ explicitly selected (from catalog UI)isActive: false→ hidden for this school
RBAC
Authorization via checkLibraryPermission(role, action) in authorization.ts. Cross-school access always denied.
| Role | read | create | update | delete | borrow | return | manage |
|---|---|---|---|---|---|---|---|
| DEVELOPER | Y | Y | Y | Y | Y | Y | Y |
| ADMIN | Y | Y | Y | Y | Y | Y | Y |
| TEACHER | Y | — | — | — | Y | Y | — |
| STUDENT | Y | — | — | — | Y | Y | — |
| GUARDIAN | Y | — | — | — | Y | Y | — |
| STAFF | Y | — | — | — | — | — | — |
| ACCOUNTANT | Y | — | — | — | — | — | — |
| USER | — | — | — | — | — | — | — |
Server actions
Book CRUD (actions.ts)
| Action | Function | Notes |
|---|---|---|
| Create | createBook(params) | Requires catalogBookId — no standalone creation |
| Update | updateBook(params) | Partial updates, verifies school ownership |
| Delete | deleteBook(params) | Blocked when active borrows exist |
Borrow / return
| Action | Function | Notes |
|---|---|---|
| Borrow | borrowBook({ bookId, userId, schoolId }) | Transaction: create BorrowRecord + decrement copies |
| Return | returnBook({ borrowRecordId, schoolId }) | Transaction: update status + increment copies |
| Overdue | markOverdueBooks() | Batch: BORROWED past due date → OVERDUE |
Catalog selection (catalog/actions.ts)
| Action | Function | Notes |
|---|---|---|
| Select | selectCatalogBook(catalogBookId, copies) | Creates bridge + Book in transaction |
| Deselect | deselectCatalogBook(catalogBookId) | Removes bridge, nullifies Book.catalogBookId |
| Toggle | toggleBookSelection(selectionId) | Flips isActive (show / hide) |
| Update | updateBookSelection(selectionId, data) | Adjust copies, shelf location |
Validation schemas
| Schema | Key fields | Constraints |
|---|---|---|
bookSchema | title, author, genre, rating, coverUrl, coverColor | rating 0-5, hex coverColor, totalCopies ≥ 1 |
borrowBookSchema | bookId, userId, schoolId, dueDate | dueDate must be future |
returnBookSchema | borrowRecordId, schoolId | Both required |
updateBookSchema | bookId + partial book fields | At least one field required |
deleteBookSchema | bookId, schoolId | Both required |
Types
interface BookListItem {
id: string
title: string
author: string
genre: string
coverUrl: string
coverColor: string | null
rating: number
createdAt: Date
}
interface Book extends BookListItem {
description: string
totalCopies: number
availableCopies: number
summary: string
gradeLevel: string
schoolId: string
// optional: isbn, publisher, publicationYear, language, pageCount, videoUrl
}BookListItem works for both CatalogBook query results and Book records — used by BookList and BookCard.
Configuration
| Constant | Value | Purpose |
|---|---|---|
MAX_BORROW_DAYS | 14 | Default loan period |
MAX_BOOKS_PER_USER | 5 | Borrow limit per student |
BOOKS_PER_PAGE | 20 | Pagination size |
DEFAULT_COVER_COLOR | #000000 | Fallback for missing covers |
Tests
pnpm vitest run src/components/library/ --reporter=verbose # 160 tests
pnpm vitest run src/components/catalog/__tests__/setup.test.ts --reporter=verbose # 54 tests| Suite | Tests | Coverage |
|---|---|---|
__tests__/actions.test.ts | 39 | All 6 server actions (create, update, delete, borrow, return, overdue) |
__tests__/authorization.test.ts | 35 | 8 roles × 7 actions + cross-school + edge cases |
__tests__/validation.test.ts | 46 | All 5 Zod schemas |
catalog/__tests__/actions.test.ts | 33 | 4 catalog selection actions |
contribute/__tests__/actions.test.ts | 7 | Contribution submission |
Data layer (CatalogBook)
Books extend the universal catalog with CatalogBook. Like subjects, books live globally (no schoolId); schools reference them via SchoolBookSelection and a school-scoped Book provisioned lazily for borrow tracking.
CatalogBook carries the canonical metadata (title, author, genre, ISBN, cover, digital file, rating, usageCount) plus the contribution audit trail (contributedBy, contributedSchoolId, approvalStatus, approvedBy, rejectionReason). SchoolBookSelection carries per-school overrides (totalCopies, availableCopies, shelfLocation, customName, isActive). Book.catalogBookId is nullable so deselecting preserves BorrowRecord history.
Contribution flow (specific to CatalogBook)
Teacher / Admin → /library/contribute → contributeBook()
- auth + tenant context
- creates CatalogBook (PENDING + DRAFT)
- sets contributedBy + contributedSchoolId
SaaS → /catalog/approvals
- approveContent("CatalogBook", id) → APPROVED
- rejectContent("CatalogBook", id) → REJECTED + reason
→ visible to all schools (PUBLIC visibility) or just the originating school (SCHOOL visibility)
The contribution form lives at /library/contribute; the contributor's history is at /library/contributions for both ADMIN and TEACHER roles.
SaaS dashboard CRUD
Route /catalog/books. DEVELOPER only — global records, no schoolId. createCatalogBook, updateCatalogBook, deleteCatalogBook plus a detail view at /catalog/books/[bookId] showing the full metadata, cover, digital file info, contribution details, and the list of selecting schools.
Approval integration
CatalogBook is one of five content types in the unified approval system — alongside CatalogQuestion, CatalogMaterial, CatalogAssignment, and LessonVideo. All five share approveContent() and rejectContent() in approval-actions.ts, which switches on contentType. They use the same enums (ApprovalStatus, ContentVisibility, ContentStatus) and the same SaaS review surface at /catalog/approvals.
Seed
pnpm db:seed:single books90 entries across 8 genres (Arabic Literature, Islamic Studies, Sciences, Mathematics, Quran Sciences, History & Geography, Children's, English, Computer Science & Reference) — all APPROVED + PUBLISHED + PUBLIC, with picsum.photos placeholder covers. Idempotent.
See also
- Catalog — universal catalog architecture
- Multi-tenancy — tenant scoping
On This Page
ArchitectureUser flowsStudent browses + borrowsThe spotlighted bookAdmin manages libraryTeacher contributesRoutesThe home bannerData flowLibrary home queryLazy book provisioningHide mechanismRBACServer actionsBook CRUD (actions.ts)Borrow / returnCatalog selection (catalog/actions.ts)Validation schemasTypesConfigurationTestsData layer (CatalogBook)Contribution flow (specific to CatalogBook)SaaS dashboard CRUDApproval integrationSeedSee also