GEMATRI (Gemakan Mahir Transformasi Geometri) — Next.js learning app for geometric transformations using Batik motifs. Nusantara Rebel design: NeoBrutalism + Indonesian heritage.
Resources
12Install
npx skillscat add dhadya/geo-batik-mobile-learning Install via the SkillsCat registry.
GEMATRI Skill — Agent Instructions
Role
You are a full-stack Next.js engineer working on GEMATRI, an interactive
learning media for geometric transformations (translasi & refleksi) targeting
Indonesian SMP students. The product uses Batik motifs as geometric context.
1. Stack & Versions
| Layer | Version / Package |
|---|---|
| Next.js | 16.2.9 (App Router, Turbopack) |
| React | 19.2.4 |
| TypeScript | Strict mode, @/* path alias |
| Styling | Tailwind CSS v4 + shadcn v4 + tw-animate-css |
| Base UI | @base-ui/react ^1.6.0 (via RetroUI) |
| Icons | Material Symbols (via @/components/common/MaterialIcon) — no lucide-react |
| CVA | class-variance-authority for variants |
| Auth | BetterAuth |
| Database | Supabase (PostgreSQL) + Drizzle ORM |
| AI | Gemini API |
| API Layer | AppError codes + handleError() + requireAuth() |
| Services | Plain async functions under features/*/services/ |
| Data Layer | TanStack Query — hooks in features/*/hooks/, provider in app/providers.tsx |
2. Project Structure
app/ # Next.js App Router
├── (app)/ # App shell (header + nav)
│ ├── menu/ # Main menu — 3-card nav grid
│ ├── prasyarat/ # Prerequisite material
│ ├── lab/ # Lab Batik creative sandbox
│ ├── apersepsi/[slug]/ # Module intro (translasi | refleksi)
│ ├── modul/[slug]/ # Learning modules
│ │ ├── layout.tsx # Tab navigation + footer
│ │ ├── page.tsx # Redirects to first tab
│ │ ├── [tab]/page.tsx # Tab content (titik|garis|bangun|sumbu-x|...)
│ │ └── kuis/ # Quiz flow
│ │ ├── [nomor]/ # Per-question (1–10) with prev/next
│ │ └── hasil/ # Score + pembahasan
│ └── layout.tsx # App shell layout
├── (auth)/ # Auth pages (no app shell)
│ ├── login/
│ └── register/
├── (landing)/ # Landing page hero (no app shell)
├── api/auth/[...all]/ # BetterAuth API handler
├── layout.tsx # Root layout — font + globals
└── globals.css # Nusantara Rebel palette + utilities
features/ # Feature-based modular architecture
├── auth/ # Auth feature
│ ├── components/ # LoginForm, RegisterForm, AuthFormField
│ └── hooks/ # useLoginForm, useRegisterForm
├── menu/ # Menu page feature
│ ├── components/ # ModuleCard, LabCard, MenuHeader, ModuleGrid, BackLink
│ ├── data.ts # Menu module data
│ └── index.ts # Barrel exports
├── modules/ # Core learning engine
│ ├── components/ # ConclusionArea, AssessmentSection, etc.
│ ├── hooks/ # TanStack Query hooks (useSectionSubmission, useTabProgress, useEvaluateSection)
│ ├── data/ # Shared constants (moduleConfig.ts)
│ ├── lib/ # Client-side utils (shuffle.ts)
│ ├── services/ # Layer 2 — plain async service functions
│ └── index.ts # Barrel exports
├── quiz/ # Quiz feature
│ ├── components/ # QuizResult, QuizNavigation, etc.
│ ├── hooks/ # TanStack Query hooks (useSubmitQuiz, useQuizResult, useEvaluateQuiz)
│ ├── data/ # Question bank (translasi.ts, refleksi.ts)
│ └── index.ts # Barrel exports
└── prasyarat/ # Prerequisite material feature
├── components/ # InteractiveCanvas, GeoGebraCanvas, ControlPanel, ConceptCard, VideoEmbed
├── hooks/ # useGeoGebra, useToggleControls
├── data.ts # Prerequisite concept data
├── toggles.ts # Toggle config and accordion groups
├── types.ts # GGBApplet, GGBWindow, GeoGebraToggle types
└── index.ts # Barrel exports
components/ # Shared React components
├── retroui/ # NeoBrutalism primitives (Button, Card, Toggle, Accordion, Skeleton, Sonner, etc.)
├── batik/ # KawungStamp, BatikWatermark
├── common/ # AmbientCircles, MaterialIcon
└── layout/ # AuthLayout, LandingFooter, ProfileDropdown
lib/ # Utilities and clients
├── query/ # TanStack Query setup
│ └── client.ts # Singleton QueryClient factory
├── api/ # Layer 1 shared primitives
│ ├── errors.ts # AppError + typed codes + handleError()
│ └── auth-utils.ts # requireAuth() via BetterAuth
├── supabase/ # Supabase client (client, server, middleware)
├── auth.ts # BetterAuth server config
├── auth-client.ts # BetterAuth browser client
├── db.ts # Drizzle + getDb() lazy accessor
├── utils.ts # Utility functions
├── validate-redirect.ts # Redirect URL validation
└── validators.ts # Form validation
drizzle/ # Drizzle ORM schema
└── schema.ts # All DB tables
supabase/ # Database migrations & schema
├── migrations/
└── schema.sql
public/ # Static assets
├── icons/ # SVG icons (google.svg)
└── images/ # Module preview images
Root config files: AGENTS.md, CLAUDE.md, SKILL.md, DESIGN.md, StyleGuide.md, PRD.md, PRD_v2.md3. Routing Conventions
Route Groups
(auth)/— no layout wrapper; used for login/register(app)/— wrapped in(app)/layout.tsx(GEMATRI header + nav)(landing)/— no layout; full-brand hero page
Dynamic Segments
// Always use Promise-based params (Next.js 16)
export default async function Page(props: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await props.params;
}Module Tab Structure
| Slug | Tab values |
|---|---|
translasi |
titik, garis, bangun |
refleksi |
sumbu-x, sumbu-y, garis, bangun |
Tabs are defined in modul/[slug]/layout.tsx via the MODULE_TABS constant.
Quiz Flow
/modul/[slug]/kuis → Intro / first question
/modul/[slug]/kuis/1…10 → Per-question with prev/next
/modul/[slug]/kuis/hasil → Results + pembahasan4. Design System (Nusantara Rebel)
Colors — Always use Tailwind semantic tokens
| Token | Hex | Tailwind |
|---|---|---|
--background |
#fff8ef |
bg-background |
--foreground |
#1f1b12 |
text-foreground |
--primary |
#ffd93d |
bg-primary |
--primary-dark |
#705d00 |
text-primary-dark |
--secondary |
#006e29 |
bg-secondary |
--secondary-container |
#93f59c |
bg-secondary-container |
--tertiary |
#ae2f34 |
bg-tertiary |
--tertiary-container |
#ffd1ce |
bg-tertiary-container |
--border |
#000 |
border-border |
--muted |
#eae2d2 |
bg-muted |
--muted-foreground |
#4d4633 |
text-muted-foreground |
Component Usage
Do not use native HTML elements. Always use RetroUI components:
import { Button } from "@/components/retroui/Button"
import { MaterialIcon } from "@/components/common/MaterialIcon"
// Primary CTA
<Button variant="default" size="lg"
className="neubrutal-shadow hover-shift active-shift"
onClick={...}>
MASUK
<MaterialIcon className="size-10" name="arrow_forward" />
</Button>
// Outline
<Button variant="outline" size="md">
KEMBALI
</Button>
// Ghost icon
<Button variant="ghost" size="icon">
<MaterialIcon name="visibility" className="size-5" />
</Button>Card:
import { Card } from "@/components/retroui/Card";
<Card>
<Card.Header>
<Card.Title>JUDUL</Card.Title>
</Card.Header>
<Card.Content>{children}</Card.Content>
</Card>;Tabs:
import { Tabs } from "@/components/retroui/Tab";
<Tabs defaultValue="titik">
<Tabs.List>
<Tabs.Trigger value="titik">TITIK</Tabs.Trigger>
<Tabs.Trigger value="garis">GARIS</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="titik">{children}</Tabs.Content>
</Tabs>;Icons — priority Material Symbols, fallback lucide-react:
import { MaterialIcon } from "@/components/common/MaterialIcon";
// Preferred — Material Symbols
<MaterialIcon name="arrow_forward" />
<MaterialIcon name="visibility" />
<MaterialIcon name="check" />
<MaterialIcon name="close" />
<MaterialIcon name="menu" />Uppercase Rule
All labels, headings, and button text are font-black uppercase. Body text
is never uppercase.
Buttons and cards
RetroUI components already have rounded-none baked in — never add rounded-* classes.
Shadows
.neubrutal-shadow → 12px 12px 0 0 #000 (for CTAs)
.shadow-lg → 6px 6px 0 0 border (for cards)
.hover-shift → translate(-4,-4) + bigger shadow
.active-shift → translate(8,8) + no shadow5. Data Model
Subtopic Slugs
translasi: titik, garis, bangun
refleksi: sumbu-x, sumbu-y, garis, bangunKey Types
interface InquiryStep {
id: string; // "step1", "step2", ...
text: string;
completed?: boolean;
}
interface PageContent {
id: string; // matches subtopic slug
title: string;
batikConcept: string;
batikDescription: string;
interactiveTitle: string;
instructions: string[];
geogebraUrl?: string;
inquirySteps: InquiryStep[];
observations: string[];
matrixFormula: string; // LaTeX
matrixExplanation: string;
}Database Tables
| Table | Purpose |
|---|---|
user |
BetterAuth user account |
section_progress |
Per-section answer attempts + AI feedback + scores |
tab_progress |
Tab unlock/completion state per module |
quiz_results |
Quiz attempt results with per-question two-attempt |
6. API Conventions
See docs/GEMATRI_CONVENTIONS_REFERENCE.md.
Response Envelope
{ ok: true, data: T } | { ok: false, error: { code: string, message: string } }3-Layer Architecture
| Layer | Location | Rules |
|---|---|---|
| 1 — Route Handler | app/api/.../route.ts |
Thin: parse → Zod validate → call service → respond. catch → handleError() |
| 2 — Service | features/modules/services/*.ts |
Plain async. No Next.js imports. Business logic + AppError throws. getDb() lazily |
| 3 — Database | lib/db.ts + drizzle/schema.ts |
Lazy getDb(). Never at module level. Drizzle ORM |
Error Handling
import { appError, handleError } from "@/lib/api/errors";
// In service:
throw appError("TAB_LOCKED");
// In route handler:
catch (e) { return handleError(e); }Commit Messages
After every task, inspect git status, git diff, and git log --oneline -5 to understand what changed. Always propose the commit message in chat for approval — never commit without confirmation.
- Never use quotes (single or double) in commit messages — plain text only
- Never use emoji in commit messages
<type>(<scope>): <description>
- bullet points for bodyTypes: feat, fix, docs, refactor, test, chore
Scopes: api, web, ui, db, shared
See docs/CONVENTIONAL_COMMITS.md.
7. Responsive Design Guide
Apply these rules to every page and component. Always use a minimum of 2 breakpoints (md: and up).
Text sizing
Reduce one level under md vs md+:
text-xs md:text-smtext-sm md:text-basetext-base md:text-lgtext-lg md:text-xltext-xl md:text-2xltext-2xl md:text-3xl
Spacing (padding, margin, gap, size)
Use ~3/4 of md+ value under md, rounding to the nearest valid Tailwind size:
| md+ | under md |
|---|---|
p-8 |
p-6 |
p-6 |
p-4 |
p-4 |
p-3 |
p-3 |
p-2 |
gap-8 |
gap-6 |
gap-6 |
gap-4 |
size-12 |
size-9 |
size-10 |
size-8 |
Layout
- All pages and components:
max-w-384 mx-auto - Top page padding:
pt-6 md:pt-8 - Bottom page padding:
pb-16 md:pb-20 - Ensure no overflow or horizontal scrolling (
overflow-hiddenwhere needed) - Max
tracking-wide— never usetracking-widerortracking-widest
Font weight consistency
Check neighbouring elements in the same component — keep font weights consistent within the same type of heading or body text. Within a page, use the same weight for all h1, all h2, all labels, etc.
8. Code Documentation
Always add TSDoc/JSDoc comments on exported functions, components, interfaces, and types — describe the why (purpose, behavior), not the what (implementation). Use @param and @returns where non-obvious. Keep comments concise.
9. Testing & Quality
Commands
npm run build # Production build (must pass before commit)
npm run dev # Dev server
npm run lint # ESLint — fix all errors
npx tsc --noEmit # TypeScript checkPre-commit (Husky)
lint-stagedrunseslint --fixon.ts,.tsxfilesprettier --writeon.json,.md,.cssfiles
Pre-push
npm run lintnpx tsc --noEmitnpm run build
10. Common Pitfalls
- Plain
<button>or other native HTML — always use RetroUI components
(Button,Card,Input,Checkbox,Dialog,Radio,Select, etc.).
Even icon-only toggles must use<Button>. - Raw hex colors — always use Tailwind semantic classes
(bg-primary,text-foreground,border-border). - Rounded-* on RetroUI components — RetroUI already has
rounded-none
baked in. Never addrounded-*or!rounded-none. - Dynamic params — always use
params: Promise<...>+await props.params
(Next.js 16 pattern, not the old sync pattern). - `"use client" — add only when needed (hooks, state, event handlers).
Default to server components. - Material Symbols priority — use
@/components/common/MaterialIcon
exclusively. Never uselucide-reacticons. - Teacher role — does not exist. All users are students.
- Raw fetch in components — never use
fetch()oruseEffect+fetch in
components. All API calls go through TanStack Query hooks infeatures/*/hooks/. - File size over 300 lines — split into sub-components, hooks, or helpers.
One exported component/function per file. Extract helpers to separate files. - Numbered component names — never
Item7Renderer,Step3Form, etc.
Use descriptive names likeMatrixExplanationRenderer,BayanganTableRenderer. - Tailwind
!prefix — never use!size-5,bg-white!,justify-start!.
Use standard specificity and proper Tailwind utilities. - Emoji in app code — never use
🎉,✅, etc. Use MaterialIcon instead. - Inline constants — extract labels, icons, colors to
features/*/data/
files. Never define them in page or component files. - Dead code — files with zero imports must be deleted. Remove unused barrel
exports, deprecated hooks, and stale references.