pramudya-yb commited on
Commit
2b62b0c
·
1 Parent(s): 7faa899

Rename project from Labas to Pram

Browse files

Rebrand the repository from “Labas” to “Pram”. Updates include docs and marketing copy (README, AGENTS.md, PRD.md, DESIGN.md, meta tags), replacement/addition of web assets (logo, favicon, PWA icons), and updating the sitemap/OG URLs. Internal workspace namespace and imports changed from @labas /* to @pram /* across apps and packages, plus corresponding package.json, tsconfig, and tsdown config updates. Server entry updated (imports, queue registration, sitemap baseUrl). This is a global rename/branding change; no functional logic changes intended.

This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. AGENTS.md +13 -13
  2. DESIGN.md +2 -2
  3. PRD.md +28 -87
  4. README.md +22 -30
  5. apps/server/.env.example +1 -1
  6. apps/server/package.json +5 -5
  7. apps/server/src/index.ts +8 -8
  8. apps/server/tsconfig.json +1 -1
  9. apps/server/tsdown.config.ts +1 -1
  10. apps/web/components.json +2 -2
  11. apps/web/index.html +13 -11
  12. apps/web/package.json +6 -6
  13. apps/web/public/{labas_icon.png → apple-touch-icon-180x180.png} +2 -2
  14. apps/web/public/favicon.ico +3 -0
  15. apps/web/public/logo.png +2 -2
  16. apps/web/public/maskable-icon-512x512.png +3 -0
  17. apps/web/public/pram_icon.png +3 -0
  18. apps/web/public/pwa-192x192.png +3 -0
  19. apps/web/public/pwa-512x512.png +3 -0
  20. apps/web/public/pwa-64x64.png +3 -0
  21. apps/web/public/robots.txt +2 -2
  22. apps/web/public/sitemap.xml +6 -6
  23. apps/web/pwa-assets.config.ts +1 -1
  24. apps/web/src/components/CommunityModal.tsx +3 -3
  25. apps/web/src/components/DonationModal.tsx +3 -3
  26. apps/web/src/components/ErrorFallback.tsx +1 -1
  27. apps/web/src/components/GettingStartedCard.tsx +2 -2
  28. apps/web/src/components/NotFoundPage.tsx +1 -1
  29. apps/web/src/components/PageGuide.tsx +1 -1
  30. apps/web/src/components/TourGuide.tsx +1 -2
  31. apps/web/src/components/TourGuideImpl.tsx +3 -4
  32. apps/web/src/components/TurnstileField.tsx +1 -1
  33. apps/web/src/components/admin/Pagination.tsx +1 -1
  34. apps/web/src/components/analytics/BreakdownCharts.tsx +1 -2
  35. apps/web/src/components/analytics/OverviewCards.tsx +1 -1
  36. apps/web/src/components/analytics/ScoreTrendChart.tsx +1 -1
  37. apps/web/src/components/analytics/TimeAnalyticsPanel.tsx +1 -1
  38. apps/web/src/components/analytics/WeaknessPanel.tsx +2 -2
  39. apps/web/src/components/attempt/OptionDisplay.tsx +1 -1
  40. apps/web/src/components/attempt/QuestionReviewCard.tsx +2 -2
  41. apps/web/src/components/bank/AdvancedFilters.tsx +1 -1
  42. apps/web/src/components/bank/AutoBundleModal.tsx +3 -3
  43. apps/web/src/components/bank/BundleSidebar.tsx +2 -2
  44. apps/web/src/components/bank/CalloutCard.tsx +1 -1
  45. apps/web/src/components/bank/CreatePackageModal.tsx +3 -3
  46. apps/web/src/components/bank/FilterBar.tsx +2 -3
  47. apps/web/src/components/bank/MobileFilterSheet.tsx +3 -3
  48. apps/web/src/components/bank/QuestionCard.tsx +1 -1
  49. apps/web/src/components/bank/QuestionDetailModal.tsx +3 -4
  50. apps/web/src/components/bank/SectionBrowser.tsx +2 -2
AGENTS.md CHANGED
@@ -1,4 +1,4 @@
1
- # AGENTS.md — Labas Project Guide
2
 
3
  > This file is for AI agents. Read this first before making any changes.
4
 
@@ -6,7 +6,7 @@
6
 
7
  ## 1. Project Identity
8
 
9
- - **Name:** `labas` — AI-powered multi-language test practice platform.
10
  - **Repo type:** Turborepo monorepo (Bun workspaces).
11
  - **Workspace packages:** `apps/*`, `packages/*`.
12
  - **Runtime & package manager:** **Bun** (`packageManager: "bun@1.3.11"`).
@@ -23,7 +23,7 @@
23
  | DB Engine | PostgreSQL | Managed via Drizzle. |
24
  | DB ORM | Drizzle ORM | **Not Prisma.** |
25
  | Auth | Better-Auth | Email/password + OAuth. |
26
- | UI | shadcn/ui (shared) | Lives in `packages/ui`. Imported as `@labas/ui/components/*`. |
27
  | AI | OpenAI-compatible API | User-managed API keys via Settings UI. |
28
  | Build | Turborepo + `tsdown` | Server compiled with `tsdown`. |
29
 
@@ -32,7 +32,7 @@
32
  ## 3. Architecture Overview
33
 
34
  ```
35
- labas/
36
  ├── apps/
37
  │ ├── web/ # Frontend (Vite, TanStack Router) — Port 5173
38
  │ └── server/ # Backend (Hono, tRPC) — Port 3000
@@ -46,7 +46,7 @@ labas/
46
  │ └── config/ # Shared TypeScript configs
47
  ```
48
 
49
- - **Internal imports** use the `@labas/*` workspace namespace.
50
  - Do **NOT** create cross-imports between `apps/*` directly; route through `packages/*`.
51
 
52
  ---
@@ -95,7 +95,7 @@ bun run build # Build all packages
95
  - ❌ **Do not hardcode API keys** in source code. AI keys are user-managed via the Settings UI.
96
  - ❌ **Do not add global CSS** in `apps/web` without checking `packages/ui/src/styles/globals.css` for existing design tokens and CSS variables.
97
  - ❌ **Do not skip heading levels**. After `<h1>`, the next heading must be `<h2>`, not `<h3>` (or lower). Never have two `<h1>` on one page.
98
- - ❌ **Do not create custom modal/dialog components**. Use shadcn `<Dialog>` from `@labas/ui/components/dialog` — it provides built-in focus trap, escape key, scroll lock, and ARIA.
99
  - ❌ **Do not use `any`** in frontend TypeScript. Enforced by eslint `@typescript-eslint/no-explicit-any: "error"` in `apps/web/eslint.config.mjs`.
100
 
101
  ---
@@ -106,7 +106,7 @@ bun run build # Build all packages
106
  - shadcn/ui primitives are shared via **`packages/ui`**.
107
  - Import components like this:
108
  ```tsx
109
- import { Button } from "@labas/ui/components/button";
110
  ```
111
  - To add new shadcn primitives, run from the **project root**:
112
  ```bash
@@ -233,7 +233,7 @@ export const Route = createFileRoute("/my-route")({
233
  - **Icon-only buttons**: Every `<button>` or `<Button>` with only an icon (no visible text) **must** have `aria-label`. The icon element (`<MaterialIcon>`, `<span>`) should NOT have `aria-hidden="true"` unless decorative.
234
  - **Heading hierarchy**: Never skip levels (`h1 → h2 → h3`, never `h1 → h3`). Exactly one `<h1>` per page. Heading level corresponds to semantic nesting, not visual size.
235
  - **Form labels**: Every `<Input>`, `<select>`, `<textarea>` needs an associated `<Label>` (with `htmlFor` + `id`) OR an `aria-label` attribute. Search inputs, OTP digits, and icon-only filter buttons are common offenders.
236
- - **Dialogs**: All modals use the shadcn `<Dialog>` component (from `@labas/ui/components/dialog`), which provides:
237
  - Focus trap inside dialog
238
  - Escape key to close
239
  - Scroll lock on body
@@ -286,7 +286,7 @@ export const Route = createFileRoute("/my-route")({
286
  - **Always use English** error messages in tRPC routers.
287
  - Use helper functions from `packages/api/src/lib/errors.ts`:
288
  ```ts
289
- import { throwNotFound, throwForbidden, throwBadRequest } from "@labas/api/lib/errors";
290
  assertOwnership(row, userId, "Question"); // throws NotFound or Forbidden
291
  ```
292
  - Never throw raw `new Error("...")` in tRPC routers.
@@ -294,7 +294,7 @@ export const Route = createFileRoute("/my-route")({
294
  ### Pagination
295
  - Use shared schema + helper for all list endpoints:
296
  ```ts
297
- import { paginationSchema, paginateDefaults } from "@labas/api/lib/pagination";
298
  .input(z.object({ search: z.string().optional(), ...paginationSchema?.shape }).optional())
299
  const { limit, offset } = paginateDefaults(input);
300
  ```
@@ -363,10 +363,10 @@ export const Route = createFileRoute("/my-route")({
363
 
364
  ### Integration DB (PGlite)
365
  - **PGlite** (`@electric-sql/pglite`) — PostgreSQL WASM in-memory.
366
- - Strategy: `mock.module("@labas/db")` intercept DB saat dynamic import untuk inject PGlite-based drizzle instance.
367
  - Setup helper: `packages/api/src/__tests__/test-setup.ts` — skema 14 tabel via raw SQL + seed data.
368
- - Schema diimport dari `../../../db/src/schema` (langsung, bukan via `@labas/db`) untuk hindari env validation side-effect.
369
- - Env vars di-mock via `mock.module("@labas/env/server")` sebelum dynamic import.
370
 
371
  ### tRPC Router Testing
372
  - Gunakan `router.createCaller({ session, auth })` untuk memanggil procedure.
 
1
+ # AGENTS.md — Pram Project Guide
2
 
3
  > This file is for AI agents. Read this first before making any changes.
4
 
 
6
 
7
  ## 1. Project Identity
8
 
9
+ - **Name:** `pram` — AI-powered multi-language test practice platform.
10
  - **Repo type:** Turborepo monorepo (Bun workspaces).
11
  - **Workspace packages:** `apps/*`, `packages/*`.
12
  - **Runtime & package manager:** **Bun** (`packageManager: "bun@1.3.11"`).
 
23
  | DB Engine | PostgreSQL | Managed via Drizzle. |
24
  | DB ORM | Drizzle ORM | **Not Prisma.** |
25
  | Auth | Better-Auth | Email/password + OAuth. |
26
+ | UI | shadcn/ui (shared) | Lives in `packages/ui`. Imported as `@pram/ui/components/*`. |
27
  | AI | OpenAI-compatible API | User-managed API keys via Settings UI. |
28
  | Build | Turborepo + `tsdown` | Server compiled with `tsdown`. |
29
 
 
32
  ## 3. Architecture Overview
33
 
34
  ```
35
+ pram/
36
  ├── apps/
37
  │ ├── web/ # Frontend (Vite, TanStack Router) — Port 5173
38
  │ └── server/ # Backend (Hono, tRPC) — Port 3000
 
46
  │ └── config/ # Shared TypeScript configs
47
  ```
48
 
49
+ - **Internal imports** use the `@pram/*` workspace namespace.
50
  - Do **NOT** create cross-imports between `apps/*` directly; route through `packages/*`.
51
 
52
  ---
 
95
  - ❌ **Do not hardcode API keys** in source code. AI keys are user-managed via the Settings UI.
96
  - ❌ **Do not add global CSS** in `apps/web` without checking `packages/ui/src/styles/globals.css` for existing design tokens and CSS variables.
97
  - ❌ **Do not skip heading levels**. After `<h1>`, the next heading must be `<h2>`, not `<h3>` (or lower). Never have two `<h1>` on one page.
98
+ - ❌ **Do not create custom modal/dialog components**. Use shadcn `<Dialog>` from `@pram/ui/components/dialog` — it provides built-in focus trap, escape key, scroll lock, and ARIA.
99
  - ❌ **Do not use `any`** in frontend TypeScript. Enforced by eslint `@typescript-eslint/no-explicit-any: "error"` in `apps/web/eslint.config.mjs`.
100
 
101
  ---
 
106
  - shadcn/ui primitives are shared via **`packages/ui`**.
107
  - Import components like this:
108
  ```tsx
109
+ import { Button } from "@pram/ui/components/button";
110
  ```
111
  - To add new shadcn primitives, run from the **project root**:
112
  ```bash
 
233
  - **Icon-only buttons**: Every `<button>` or `<Button>` with only an icon (no visible text) **must** have `aria-label`. The icon element (`<MaterialIcon>`, `<span>`) should NOT have `aria-hidden="true"` unless decorative.
234
  - **Heading hierarchy**: Never skip levels (`h1 → h2 → h3`, never `h1 → h3`). Exactly one `<h1>` per page. Heading level corresponds to semantic nesting, not visual size.
235
  - **Form labels**: Every `<Input>`, `<select>`, `<textarea>` needs an associated `<Label>` (with `htmlFor` + `id`) OR an `aria-label` attribute. Search inputs, OTP digits, and icon-only filter buttons are common offenders.
236
+ - **Dialogs**: All modals use the shadcn `<Dialog>` component (from `@pram/ui/components/dialog`), which provides:
237
  - Focus trap inside dialog
238
  - Escape key to close
239
  - Scroll lock on body
 
286
  - **Always use English** error messages in tRPC routers.
287
  - Use helper functions from `packages/api/src/lib/errors.ts`:
288
  ```ts
289
+ import { throwNotFound, throwForbidden, throwBadRequest } from "@pram/api/lib/errors";
290
  assertOwnership(row, userId, "Question"); // throws NotFound or Forbidden
291
  ```
292
  - Never throw raw `new Error("...")` in tRPC routers.
 
294
  ### Pagination
295
  - Use shared schema + helper for all list endpoints:
296
  ```ts
297
+ import { paginationSchema, paginateDefaults } from "@pram/api/lib/pagination";
298
  .input(z.object({ search: z.string().optional(), ...paginationSchema?.shape }).optional())
299
  const { limit, offset } = paginateDefaults(input);
300
  ```
 
363
 
364
  ### Integration DB (PGlite)
365
  - **PGlite** (`@electric-sql/pglite`) — PostgreSQL WASM in-memory.
366
+ - Strategy: `mock.module("@pram/db")` intercept DB saat dynamic import untuk inject PGlite-based drizzle instance.
367
  - Setup helper: `packages/api/src/__tests__/test-setup.ts` — skema 14 tabel via raw SQL + seed data.
368
+ - Schema diimport dari `../../../db/src/schema` (langsung, bukan via `@pram/db`) untuk hindari env validation side-effect.
369
+ - Env vars di-mock via `mock.module("@pram/env/server")` sebelum dynamic import.
370
 
371
  ### tRPC Router Testing
372
  - Gunakan `router.createCaller({ session, auth })` untuk memanggil procedure.
DESIGN.md CHANGED
@@ -1,4 +1,4 @@
1
- # Design System Inspired by Clay
2
 
3
  ## 1. Visual Theme & Atmosphere
4
 
@@ -327,7 +327,7 @@ All `packages/ui` primitives use semantic tokens; zero app-specific CSS vars in
327
  - `--primary` → `--matcha-600` (green/action)
328
  - `--secondary` → `--clay-black` (black/neutral)
329
  - Theme lives in `packages/ui/src/styles/globals.css`
330
- - App (`apps/web`) imports only `@import "@labas/ui/globals.css"` — no `index.css` overrides
331
 
332
  ### 4.4 Accessibility Decisions
333
 
 
1
+ # Design System Inspired by Clay
2
 
3
  ## 1. Visual Theme & Atmosphere
4
 
 
327
  - `--primary` → `--matcha-600` (green/action)
328
  - `--secondary` → `--clay-black` (black/neutral)
329
  - Theme lives in `packages/ui/src/styles/globals.css`
330
+ - App (`apps/web`) imports only `@import "@pram/ui/globals.css"` — no `index.css` overrides
331
 
332
  ### 4.4 Accessibility Decisions
333
 
PRD.md CHANGED
@@ -1,107 +1,48 @@
1
- # PRD: Labas — AI-Powered Multi-Language Test Practice Platform
2
 
3
  ## Problem Statement
4
 
5
- Pelajar Indonesia yang mempersiapkan ujian bahasa (IELTS, TOEFL, JLPT, HSK, Goethe) kesulitan menemukan soal latihan reading comprehension yang:
6
- - **Bervariasi** dan mencakup format soal spesifik tiap ujian (synonym, grammar-in-context, cloze, matching headings, author's view, dll.)
7
  - **Terjangkau** — platform existing biasanya berbayar dan tidak fleksibel
8
  - **Relevan** dengan kelemahan personal — tidak ada analisis per-section yang memberi rekomendasi perbaikan
9
  - **Terbuka** — tidak ada platform open-source "bring your own key" untuk generasi soal AI
10
 
11
  ## Solution
12
 
13
- **Labas** adalah platform open-source yang menyediakan:
14
- 1. **AI Question Generator** — generate soal reading comprehension pakai OpenAI-compatible API (BYOK). Dua mode: quick (single prompt) dan agentic (multi-step validation)
15
  2. **Question Bank** — kumpulan soal publik/privat yang bisa di-browse, di-rating, dan digunakan ulang
16
- 3. **Package Builder** — susun paket soal dari kombinasi soal existing + AI-generated, dengan visibility publik/privat
17
- 4. **Package Combiner** — gabung section dari berbagai paket tanpa perlu generate ulang
18
- 5. **Exam Interface** — antarmuka ujian side-by-side (teks + soal), timer, auto-grade
19
- 6. **Deep Analytics** — evaluasi per-section, identifikasi kelemahan, rekomendasi paket soal untuk perbaikan
20
 
21
- Target pengguna: pelajar Indonesia yang mempersiapkan IELTS, TOEFL, JLPT, HSK, dan ujian bahasa Jerman.
22
 
23
  ## User Stories
24
 
25
  ### AI Generation
26
- 1. Sebagai pelajar, saya ingin generate soal reading comprehension dengan AI menggunakan API key saya sendiri, agar saya bisa latihan tanpa batasan platform
27
- 2. Sebagai pelajar, saya ingin memilih jenis ujian (IELTS/TOEFL/JLPT/HSK/German), section (Reading/Writing), dan tingkat kesulitan sebelum generate, agar soal sesuai kebutuhan saya
28
- 3. Sebagai pelajar, saya ingin memilih format soal (multiple choice, true/false, matching, fill-in-blank, synonym, dll.) yang akan digenerate, agar latihan saya spesifik
29
- 4. Sebagai pelajar, saya ingin memilih topik bacaan (Science, Business, Arts, dll.) untuk soal yang digenerate, agar konteks relevan dengan minat saya
30
- 5. Sebagai pelajar, saya ingin melihat preview hasil generate sebelum menyimpan, agar saya bisa mengedit atau mere-generate soal yang kurang baik
31
- 6. Sebagai power user, saya ingin menggunakan mode "agentic" yang melakukan validasi multi-step (passage → validate → questions → self-answer → score), agar kualitas soal lebih terjamin
32
- 7. Sebagai pelajar casual, saya ingin menggunakan mode "quick" yang cepat dan hemat token, agar saya bisa langsung latihan
33
-
34
- ### Question Bank
35
- 8. Sebagai pelajar, saya ingin browse kumpulan soal publik berdasarkan bahasa, tingkat kesulitan, format, dan topik, agar saya bisa menemukan soal yang sesuai
36
- 9. Sebagai pelajar, saya ingin melihat detail soal (passage, pertanyaan, pilihan, jawaban) sebelum memutuskan menggunakannya
37
- 10. Sebagai pelajar, saya ingin memberi rating dan melihat rating soal/paket dari pengguna lain, agar saya tahu kualitasnya
38
- 11. Sebagai kreator, saya ingin mempublikasikan soal yang saya buat ke bank publik, agar orang lain bisa menggunakannya
39
- 12. Sebagai kreator, saya ingin menyimpan soal sebagai privat, agar hanya saya yang bisa mengaksesnya
40
- 13. Sebagai pelajar, saya ingin mencari soal berdasarkan keyword di teks bacaan atau pertanyaan
41
-
42
- ### Package Builder & Combiner
43
- 14. Sebagai pelajar, saya ingin membuat paket soal (bundle) dari beberapa soal yang saya pilih dari bank, agar saya bisa latihan dalam satu sesi terstruktur
44
- 15. Sebagai kreator, saya ingin memberi judul, deskripsi, dan visibility (publik/privat) pada paket yang saya buat
45
- 16. Sebagai pelajar, saya ingin mengkombinasikan section dari berbagai paket (misal: Reading dari Paket A, Writing dari Paket B) menjadi satu paket ujian baru, tanpa perlu generate ulang dengan AI
46
- 17. Sebagai pelajar, saya ingin melihat estimasi durasi dan jumlah soal sebelum memulai ujian
47
-
48
- ### Exam Interface
49
- 18. Sebagai pelajar, saya ingin melihat teks bacaan dan soal secara side-by-side saat mengerjakan, agar saya tidak perlu scroll bolak-balik
50
- 19. Sebagai pelajar, saya ingin timer yang menunjukkan sisa waktu pengerjaan, agar saya bisa mengatur pace
51
- 20. Sebagai pelajar, saya ingin navigasi antar soal (previous/next/jump), agar saya bisa mengerjakan dengan fleksibel
52
- 21. Sebagai pelajar, saya ingin menandai soal untuk direview nanti, agar saya bisa fokus dulu ke soal lain
53
- 22. Sebagai pelajar, saya ingin melihat progress (soal ke-X dari total) selama ujian
54
- 23. Sebagai pelajar, saya ingin jawaban saya tersimpan otomatis, agar tidak hilang jika koneksi terputus
55
- 24. Sebagai pelajar, saya ingin hasil ujian langsung terlihat setelah selesai (auto-grade untuk soal objektif)
56
-
57
- ### Analytics & Evaluation
58
- 25. Sebagai pelajar, saya ingin melihat skor per-section (Reading/Writing/Listening/Speaking) setelah ujian, agar saya tahu kekuatan dan kelemahan saya
59
- 26. Sebagai pelajar, saya ingin melihat breakdown akurasi per skill tag (grammar, vocabulary, inference, dll.), agar saya tahu area mana yang perlu diperbaiki
60
- 27. Sebagai pelajar, saya ingin melihat tren progres dari waktu ke waktu (line chart akurasi), agar saya tahu apakah saya improving
61
- 28. Sebagai pelajar, saya ingin melihat radar chart proficiency per skill, agar visualisasi kelemahan saya jelas
62
- 29. Sebagai pelajar, saya ingin mendapat rekomendasi paket soal yang fokus ke kelemahan saya, agar saya bisa memperbaiki area tersebut secara terarah
63
- 30. Sebagai pelajar, saya ingin melihat statistik: total soal dikerjakan, total waktu latihan, akurasi rata-rata
64
-
65
- ### Settings & Profile
66
- 31. Sebagai pengguna, saya ingin menyimpan API key saya (OpenAI-compatible) dengan aman (encrypted), agar saya tidak perlu input ulang setiap generate
67
- 32. Sebagai pengguna, saya ingin mengatur base URL dan model name untuk AI provider, agar saya bisa pakai provider alternatif (OpenRouter, Groq, local LLM)
68
- 33. Sebagai pengguna, saya ingin mengelola (tambah/edit/hapus) multiple API keys untuk provider berbeda
69
- 34. Sebagai pengguna, saya ingin mengedit profil (nama, avatar) saya
70
-
71
- ### Community
72
- 35. Sebagai pelajar, saya ingin melihat paket soal "trending" atau "staff pick", agar saya bisa menemukan soal berkualitas tinggi
73
- 36. Sebagai kreator, saya ingin melihat berapa kali paket soal saya digunakan orang lain
74
- 37. Sebagai pelajar, saya ingin membagikan link paket soal ke teman
75
-
76
- ## Exam Types Supported
77
-
78
- | Exam | Language | Levels | Focus Areas |
79
- |------|----------|--------|-------------|
80
- | IELTS Academic | English | Band 1-9 | Academic reading, formal writing |
81
- | TOEFL iBT | English | 0-120 | Academic English, university context |
82
- | JLPT | Japanese | N5-N1 | Kanji reading, grammar particles, text comprehension |
83
- | HSK | Chinese | HSK 1-6 | Character recognition, word choice, sentence structure |
84
- | Goethe / TestDaF | German | A1-C2 | Article/case, vocabulary in context, Lückentext |
85
 
86
  ## Question Formats
87
 
88
- | Format | IELTS | TOEFL | JLPT | HSK | German | Auto-Gradeable |
89
- |--------|-------|-------|------|-----|--------|:---:|
90
- | Multiple Choice | | ✓ | ✓ | ✓ | ✓ | ✓ |
91
- | True/False/Not Given | ✓ | ✓ | | | ✓ | ✓ |
92
- | Matching Headings | ✓ | ✓ | | | ✓ | ✓ |
93
- | Matching Information | ✓ | ✓ | | | ✓ | ✓ |
94
- | Fill in Blank | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
95
- | Synonym / Vocabulary | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
96
- | Grammar in Context | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
97
- | Sentence Completion | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
98
- | Summary Completion | ✓ | ✓ | | | | ✓ |
99
- | Cloze Test | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
100
- | Reference (pronoun) | | ✓ | ✓ | | | ✓ |
101
- | Author's View | ✓ | ✓ | ✓ | | | ✓ |
102
- | Kanji Reading | | | ✓ | | | ✓ |
103
- | Particle Choice | | | ✓ | | | ✓ |
104
- | Article / Case Choice | | | | | ✓ | ✓ |
105
 
106
  ## Implementation Decisions
107
 
@@ -328,4 +269,4 @@ A good test:
328
  - Timer di exam interface harus client-side dengan periodic sync ke server
329
  - PWA support sudah ada, perlu di-enhance untuk offline exam mode (cache soal)
330
  - Default theme: light, dark toggle tetap ada
331
- - Nama "Labas" dari "labs" + "bahasa"
 
1
+ # PRD: Pram — AI-Powered CPNS Test Practice Platform
2
 
3
  ## Problem Statement
4
 
5
+ Calon Pegawai Negeri Sipil (CPNS) di Indonesia kesulitan menemukan soal latihan yang:
6
+ - **Bervariasi** dan mencakup format soal spesifik (TWK, TIU, TKP)
7
  - **Terjangkau** — platform existing biasanya berbayar dan tidak fleksibel
8
  - **Relevan** dengan kelemahan personal — tidak ada analisis per-section yang memberi rekomendasi perbaikan
9
  - **Terbuka** — tidak ada platform open-source "bring your own key" untuk generasi soal AI
10
 
11
  ## Solution
12
 
13
+ **Pram** adalah platform open-source yang menyediakan:
14
+ 1. **AI Question Generator** — generate soal CPNS pakai OpenAI-compatible API (BYOK). Dua mode: quick (single prompt) dan agentic (multi-step validation)
15
  2. **Question Bank** — kumpulan soal publik/privat yang bisa di-browse, di-rating, dan digunakan ulang
16
+ 3. **Package Builder** — susun paket soal dari kombinasi soal existing + AI-generated
17
+ 4. **Exam Interface** — antarmuka ujian simulasi CAT (Computer Assisted Test)
18
+ 5. **Deep Analytics** — evaluasi per-section (TWK, TIU, TKP), identifikasi kelemahan, rekomendasi paket soal
 
19
 
20
+ Target pengguna: Calon Pegawai Negeri Sipil (CPNS) Indonesia.
21
 
22
  ## User Stories
23
 
24
  ### AI Generation
25
+ 1. Sebagai peserta, saya ingin generate soal CPNS dengan AI menggunakan API key saya sendiri
26
+ 2. Sebagai peserta, saya ingin memilih section (TWK, TIU, TKP) dan tingkat kesulitan sebelum generate
27
+ 3. Sebagai peserta, saya ingin memilih topik spesifik (Pancasila, Deret Angka, Pelayanan Publik, dll.)
28
+ 4. Sebagai power user, saya ingin menggunakan mode "agentic" untuk validasi kualitas soal yang lebih tinggi
29
+
30
+ ## Exam Sections Supported
31
+
32
+ | Section | Type | Focus Areas |
33
+ |---------|------|-------------|
34
+ | TWK | Tes Wawasan Kebangsaan | Nasionalisme, Integritas, Bela Negara, Pilar Negara, Bahasa Indonesia |
35
+ | TIU | Tes Intelegensia Umum | Analogi, Silogisme, Analitis, Deret Angka, Perbandingan, Soal Cerita, Figural |
36
+ | TKP | Tes Karakteristik Pribadi | Pelayanan Publik, Jejaring Kerja, Sosial Budaya, TIK, Profesionalisme, Anti Radikalisme |
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
 
38
  ## Question Formats
39
 
40
+ Semua soal CPNS menggunakan format **Multiple Choice** dengan 5 pilihan jawaban (A, B, C, D, E). Beberapa format tambahan untuk latihan:
41
+ - Multiple Choice (Standard)
42
+ - Benar / Salah (Latihan Konsep)
43
+ - Melengkapi Kalimat (Bahasa Indonesia)
44
+ - Analogi & Silogisme (TIU)
45
+ - Deret Angka (TIU)
 
 
 
 
 
 
 
 
 
 
 
46
 
47
  ## Implementation Decisions
48
 
 
269
  - Timer di exam interface harus client-side dengan periodic sync ke server
270
  - PWA support sudah ada, perlu di-enhance untuk offline exam mode (cache soal)
271
  - Default theme: light, dark toggle tetap ada
272
+ - Nama "Pram" dari "labs" + "bahasa"
README.md CHANGED
@@ -1,8 +1,8 @@
1
- <p align="center">
2
- <img src="./apps/web/public/logo.png" alt="Labas" width="200" />
3
  </p>
4
 
5
- <h1 align="center">Labas</h1>
6
 
7
  <p align="center">
8
  <strong>AI-powered multi-language test practice platform</strong><br />
@@ -20,38 +20,30 @@
20
 
21
  ---
22
 
23
- ## What is Labas?
24
 
25
- **Labas** is an open-source platform for practicing foreign-language proficiency exams. Users can generate reading (and writing) questions with AI, save them into question packages, take timed mock tests, and review results with explanations in **Bahasa Indonesia** (foreign script such as kanji, hanzi, hangul, or Arabic may appear when relevant).
26
 
27
  The project is a **Turborepo monorepo** managed with **Bun**. The web app talks to a **Hono + tRPC** backend backed by **PostgreSQL**, **Redis** (job queue), and **Better Auth**.
28
 
29
  ## Features
30
 
31
- - **AI question generation** — Quick mode (single-pass) and Agentic mode (multi-step pipeline with passage validation and quality checks)
32
- - **8 exam types** — IELTS, TOEFL, JLPT, HSK, Goethe, TOPIK, TOAFL, DELE
33
- - **20+ question formats** — Multiple choice, true/false/not given, fill-in-blank, kanji reading, matching, and more
34
  - **Question bank & packages** — Organize generated questions into shareable or private packages
35
  - **Mock tests & attempts** — Timed practice sessions with scoring and review
36
  - **Analytics & leaderboard** — Track performance over time
37
- - **User-managed AI keys** — Bring your own OpenAI-compatible API key via Settings (never hardcoded in server env)
38
- - **Credit system** — Token-based usage tracking for AI generation
39
- - **Admin panel** — User management, moderation, jobs, credits, and featured content
40
  - **PWA** — Installable progressive web app
41
- - **Accessible UI** — Shared shadcn/ui components with skip links, proper ARIA, and focus management
42
 
43
- ## Supported Exams
44
 
45
- | Exam | Language | Notes |
46
- |------|----------|-------|
47
- | IELTS Academic | English | Reading & Writing sections |
48
- | TOEFL iBT | English | Reading & Writing sections |
49
- | JLPT | Japanese | Kanji annotations supported (`漢字(かんじ)`) |
50
- | HSK | Chinese | |
51
- | Goethe-Zertifikat | German | |
52
- | TOPIK | Korean | Particles, honorifics, speech levels |
53
- | TOAFL | Arabic | RTL text support |
54
- | DELE | Spanish | Verb conjugation focus |
55
 
56
  ## Tech Stack
57
 
@@ -81,8 +73,8 @@ The project is a **Turborepo monorepo** managed with **Bun**. The web app talks
81
  ### 1. Clone and install
82
 
83
  ```bash
84
- git clone https://github.com/<your-org>/labas.git
85
- cd labas
86
  bun install
87
  ```
88
 
@@ -111,7 +103,7 @@ Edit **`apps/server/.env`**:
111
 
112
  ```env
113
  # PostgreSQL (matches docker-compose defaults)
114
- DATABASE_URL=postgresql://postgres:password@localhost:5432/labas
115
 
116
  # Redis (defaults to redis://localhost:6379 if omitted)
117
  REDIS_URL=redis://localhost:6379
@@ -186,7 +178,7 @@ AI logic lives in [`packages/ai/`](./packages/ai/).
186
  ## Project Structure
187
 
188
  ```
189
- labas/
190
  ├── apps/
191
  │ ├── web/ # Frontend (React + TanStack Router) — port 3001
192
  │ └── server/ # Backend entry (Hono + tRPC) — port 3000
@@ -202,7 +194,7 @@ labas/
202
  └── turbo.json
203
  ```
204
 
205
- Internal imports use the `@labas/*` workspace namespace. Apps should not import from each other directly — share code through `packages/*`.
206
 
207
  ## Available Scripts
208
 
@@ -257,7 +249,7 @@ npx shadcn@latest add accordion dialog -c packages/ui
257
  Import shared components:
258
 
259
  ```tsx
260
- import { Button } from "@labas/ui/components/button";
261
  ```
262
 
263
  Design tokens and global styles: [`packages/ui/src/styles/globals.css`](./packages/ui/src/styles/globals.css).
@@ -280,7 +272,7 @@ For AI-related changes, see the **AI Generation Context** section in `AGENTS.md`
280
 
281
  ## Support
282
 
283
- If you find Labas helpful and want to support its ongoing development, consider buying the creator a coffee! Your support helps cover API and hosting costs. ☕
284
 
285
  - **[Saweria](https://saweria.co/rogasper)** (Indonesian supporters)
286
  - **[Ko-fi](https://ko-fi.com/rogasper)** (International)
 
1
+ <p align="center">
2
+ <img src="./apps/web/public/logo.png" alt="Pram" width="200" />
3
  </p>
4
 
5
+ <h1 align="center">Pram</h1>
6
 
7
  <p align="center">
8
  <strong>AI-powered multi-language test practice platform</strong><br />
 
20
 
21
  ---
22
 
23
+ ## What is Pram?
24
 
25
+ **Pram** is an open-source platform for practicing CPNS (Seleksi Calon Pegawai Negeri Sipil) exams. Users can generate questions for TWK, TIU, and TKP with AI, save them into question packages, take timed mock tests, and review results with detailed explanations in **Bahasa Indonesia**.
26
 
27
  The project is a **Turborepo monorepo** managed with **Bun**. The web app talks to a **Hono + tRPC** backend backed by **PostgreSQL**, **Redis** (job queue), and **Better Auth**.
28
 
29
  ## Features
30
 
31
+ - **AI question generation** — Quick mode (single-pass) and Agentic mode (multi-step pipeline)
32
+ - **CPNS Focus** — TWK, TIU, and TKP sections
33
+ - **20+ question formats** — Multiple choice, true/false, fill-in-blank, and more
34
  - **Question bank & packages** — Organize generated questions into shareable or private packages
35
  - **Mock tests & attempts** — Timed practice sessions with scoring and review
36
  - **Analytics & leaderboard** — Track performance over time
37
+ - **User-managed AI keys** — Bring your own OpenAI-compatible API key
 
 
38
  - **PWA** — Installable progressive web app
 
39
 
40
+ ## Supported Exam Sections
41
 
42
+ | Section | Type | Notes |
43
+ |---------|------|-------|
44
+ | TWK | Tes Wawasan Kebangsaan | Nasionalisme, Integritas, Bela Negara, Pilar Negara, Bahasa Indonesia |
45
+ | TIU | Tes Intelegensia Umum | Verbal, Numerik, Figural |
46
+ | TKP | Tes Karakteristik Pribadi | Pelayanan Publik, Jejaring Kerja, Sosial Budaya, TIK, Profesionalisme, Anti Radikalisme |
 
 
 
 
 
47
 
48
  ## Tech Stack
49
 
 
73
  ### 1. Clone and install
74
 
75
  ```bash
76
+ git clone https://github.com/<your-org>/pram.git
77
+ cd pram
78
  bun install
79
  ```
80
 
 
103
 
104
  ```env
105
  # PostgreSQL (matches docker-compose defaults)
106
+ DATABASE_URL=postgresql://postgres:password@localhost:5432/pram
107
 
108
  # Redis (defaults to redis://localhost:6379 if omitted)
109
  REDIS_URL=redis://localhost:6379
 
178
  ## Project Structure
179
 
180
  ```
181
+ pram/
182
  ├── apps/
183
  │ ├── web/ # Frontend (React + TanStack Router) — port 3001
184
  │ └── server/ # Backend entry (Hono + tRPC) — port 3000
 
194
  └── turbo.json
195
  ```
196
 
197
+ Internal imports use the `@pram/*` workspace namespace. Apps should not import from each other directly — share code through `packages/*`.
198
 
199
  ## Available Scripts
200
 
 
249
  Import shared components:
250
 
251
  ```tsx
252
+ import { Button } from "@pram/ui/components/button";
253
  ```
254
 
255
  Design tokens and global styles: [`packages/ui/src/styles/globals.css`](./packages/ui/src/styles/globals.css).
 
272
 
273
  ## Support
274
 
275
+ If you find Pram helpful and want to support its ongoing development, consider buying the creator a coffee! Your support helps cover API and hosting costs. ☕
276
 
277
  - **[Saweria](https://saweria.co/rogasper)** (Indonesian supporters)
278
  - **[Ko-fi](https://ko-fi.com/rogasper)** (International)
apps/server/.env.example CHANGED
@@ -1,5 +1,5 @@
1
  # PostgreSQL connection string
2
- # Local dev (docker-compose): postgresql://postgres:password@localhost:5432/labas
3
  DATABASE_URL=
4
 
5
  # Redis — required for AI generation job queue
 
1
  # PostgreSQL connection string
2
+ # Local dev (docker-compose): postgresql://postgres:password@localhost:5432/pram
3
  DATABASE_URL=
4
 
5
  # Redis — required for AI generation job queue
apps/server/package.json CHANGED
@@ -12,10 +12,10 @@
12
  },
13
  "dependencies": {
14
  "@hono/trpc-server": "^0.4.0",
15
- "@labas/api": "workspace:*",
16
- "@labas/auth": "workspace:*",
17
- "@labas/db": "workspace:*",
18
- "@labas/env": "workspace:*",
19
  "@trpc/server": "catalog:",
20
  "better-auth": "catalog:",
21
  "dotenv": "catalog:",
@@ -24,7 +24,7 @@
24
  "zod": "catalog:"
25
  },
26
  "devDependencies": {
27
- "@labas/config": "workspace:*",
28
  "@types/bun": "catalog:",
29
  "@types/nodemailer": "^8.0.0",
30
  "tsdown": "^0.21.9",
 
12
  },
13
  "dependencies": {
14
  "@hono/trpc-server": "^0.4.0",
15
+ "@pram/api": "workspace:*",
16
+ "@pram/auth": "workspace:*",
17
+ "@pram/db": "workspace:*",
18
+ "@pram/env": "workspace:*",
19
  "@trpc/server": "catalog:",
20
  "better-auth": "catalog:",
21
  "dotenv": "catalog:",
 
24
  "zod": "catalog:"
25
  },
26
  "devDependencies": {
27
+ "@pram/config": "workspace:*",
28
  "@types/bun": "catalog:",
29
  "@types/nodemailer": "^8.0.0",
30
  "tsdown": "^0.21.9",
apps/server/src/index.ts CHANGED
@@ -1,14 +1,14 @@
1
  import { trpcServer } from "@hono/trpc-server";
2
- import { createContext } from "@labas/api/context";
3
- import { appRouter } from "@labas/api/routers/index";
4
- import { checkRateLimitAllowed } from "@labas/api/lib/rate-limit";
5
- import { auth } from "@labas/auth";
6
- import { env } from "@labas/env/server";
7
- import { withRequestId } from "@labas/api/logger";
8
  import { Hono } from "hono";
9
  import { cors } from "hono/cors";
10
 
11
- import "@labas/api/queue";
12
 
13
  const app = new Hono();
14
 
@@ -73,7 +73,7 @@ app.use(
73
  );
74
 
75
  app.get("/sitemap.xml", (c) => {
76
- const baseUrl = "https://labas.rogasper.com";
77
  const urls = [
78
  { loc: `${baseUrl}/`, changefreq: "weekly", priority: "1.0" },
79
  ];
 
1
  import { trpcServer } from "@hono/trpc-server";
2
+ import { createContext } from "@pram/api/context";
3
+ import { appRouter } from "@pram/api/routers/index";
4
+ import { checkRateLimitAllowed } from "@pram/api/lib/rate-limit";
5
+ import { auth } from "@pram/auth";
6
+ import { env } from "@pram/env/server";
7
+ import { withRequestId } from "@pram/api/logger";
8
  import { Hono } from "hono";
9
  import { cors } from "hono/cors";
10
 
11
+ import "@pram/api/queue";
12
 
13
  const app = new Hono();
14
 
 
73
  );
74
 
75
  app.get("/sitemap.xml", (c) => {
76
+ const baseUrl = "https://pram.rogasper.com";
77
  const urls = [
78
  { loc: `${baseUrl}/`, changefreq: "weekly", priority: "1.0" },
79
  ];
apps/server/tsconfig.json CHANGED
@@ -1,5 +1,5 @@
1
  {
2
- "extends": "@labas/config/tsconfig.base.json",
3
  "compilerOptions": {
4
  "composite": true,
5
  "outDir": "dist",
 
1
  {
2
+ "extends": "@pram/config/tsconfig.base.json",
3
  "compilerOptions": {
4
  "composite": true,
5
  "outDir": "dist",
apps/server/tsdown.config.ts CHANGED
@@ -5,5 +5,5 @@ export default defineConfig({
5
  format: "esm",
6
  outDir: "./dist",
7
  clean: true,
8
- noExternal: [/@labas\/.*/],
9
  });
 
5
  format: "esm",
6
  outDir: "./dist",
7
  clean: true,
8
+ noExternal: [/@pram\/.*/],
9
  });
apps/web/components.json CHANGED
@@ -13,8 +13,8 @@
13
  "iconLibrary": "lucide",
14
  "aliases": {
15
  "components": "@/components",
16
- "utils": "@labas/ui/lib/utils",
17
- "ui": "@labas/ui/components",
18
  "lib": "@/lib",
19
  "hooks": "@/hooks"
20
  },
 
13
  "iconLibrary": "lucide",
14
  "aliases": {
15
  "components": "@/components",
16
+ "utils": "@pram/ui/lib/utils",
17
+ "ui": "@pram/ui/components",
18
  "lib": "@/lib",
19
  "hooks": "@/hooks"
20
  },
apps/web/index.html CHANGED
@@ -5,29 +5,31 @@
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
  <meta name="description" content="Generate AI-powered practice questions for JLPT, TOPIK, TOAFL, and more. Practice smarter with adaptive test preparation." />
7
  <meta name="theme-color" content="#0c0c0c" />
8
- <meta property="og:title" content="Labas — AI-Powered Exam Practice Platform" />
9
  <meta property="og:description" content="Generate AI-powered practice questions for JLPT, TOPIK, TOAFL, and more. Practice smarter with adaptive test preparation." />
10
- <meta property="og:url" content="https://labas.rogasper.com/" />
11
- <meta property="og:site_name" content="Labas" />
12
  <meta property="og:locale" content="id_ID" />
13
  <meta property="og:type" content="website" />
14
- <meta property="og:image" content="https://labas.rogasper.com/opengraph-image.png" />
15
- <meta property="og:image:secure_url" content="https://labas.rogasper.com/opengraph-image.png" />
16
  <meta property="og:image:type" content="image/png" />
17
  <meta property="og:image:width" content="1200" />
18
  <meta property="og:image:height" content="630" />
19
- <meta property="og:image:alt" content="Labas — platform latihan ujian bahasa dengan AI Generator dan simulasi tes" />
20
  <meta name="twitter:card" content="summary_large_image" />
21
- <meta name="twitter:title" content="Labas — AI-Powered Exam Practice Platform" />
22
  <meta name="twitter:description" content="Generate AI-powered practice questions for JLPT, TOPIK, TOAFL, and more. Practice smarter with adaptive test preparation." />
23
- <meta name="twitter:image" content="https://labas.rogasper.com/twitter-image.jpg" />
24
- <meta name="twitter:image:alt" content="Labas — platform latihan ujian bahasa dengan AI Generator dan simulasi tes" />
25
  <meta name="twitter:image:type" content="image/jpeg" />
26
  <meta name="twitter:image:width" content="1200" />
27
  <meta name="twitter:image:height" content="630" />
28
  <meta name="robots" content="index, follow" />
29
- <link rel="canonical" href="https://labas.rogasper.com/" />
30
- <title>Labas AI-Powered Exam Practice Platform</title>
 
 
31
  </head>
32
 
33
  <body>
 
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
  <meta name="description" content="Generate AI-powered practice questions for JLPT, TOPIK, TOAFL, and more. Practice smarter with adaptive test preparation." />
7
  <meta name="theme-color" content="#0c0c0c" />
8
+ <meta property="og:title" content="Pram — AI-Powered Exam Practice Platform" />
9
  <meta property="og:description" content="Generate AI-powered practice questions for JLPT, TOPIK, TOAFL, and more. Practice smarter with adaptive test preparation." />
10
+ <meta property="og:url" content="https://pram.rogasper.com/" />
11
+ <meta property="og:site_name" content="Pram" />
12
  <meta property="og:locale" content="id_ID" />
13
  <meta property="og:type" content="website" />
14
+ <meta property="og:image" content="https://pram.rogasper.com/opengraph-image.png" />
15
+ <meta property="og:image:secure_url" content="https://pram.rogasper.com/opengraph-image.png" />
16
  <meta property="og:image:type" content="image/png" />
17
  <meta property="og:image:width" content="1200" />
18
  <meta property="og:image:height" content="630" />
19
+ <meta property="og:image:alt" content="Pram — platform latihan ujian bahasa dengan AI Generator dan simulasi tes" />
20
  <meta name="twitter:card" content="summary_large_image" />
21
+ <meta name="twitter:title" content="Pram — AI-Powered Exam Practice Platform" />
22
  <meta name="twitter:description" content="Generate AI-powered practice questions for JLPT, TOPIK, TOAFL, and more. Practice smarter with adaptive test preparation." />
23
+ <meta name="twitter:image" content="https://pram.rogasper.com/twitter-image.jpg" />
24
+ <meta name="twitter:image:alt" content="Pram — platform latihan ujian bahasa dengan AI Generator dan simulasi tes" />
25
  <meta name="twitter:image:type" content="image/jpeg" />
26
  <meta name="twitter:image:width" content="1200" />
27
  <meta name="twitter:image:height" content="630" />
28
  <meta name="robots" content="index, follow" />
29
+ <link rel="canonical" href="https://pram.rogasper.com/" />
30
+ <link rel="icon" href="/favicon.ico" sizes="48x48" />
31
+ <link rel="apple-touch-icon" href="/apple-touch-icon-180x180.png" />
32
+ <title>Pram — AI-Powered Exam Practice Platform</title>
33
  </head>
34
 
35
  <body>
apps/web/package.json CHANGED
@@ -17,11 +17,11 @@
17
  },
18
  "dependencies": {
19
  "@hookform/resolvers": "^5.2.2",
20
- "@labas/ai": "workspace:*",
21
- "@labas/api": "workspace:*",
22
- "@labas/auth": "workspace:*",
23
- "@labas/env": "workspace:*",
24
- "@labas/ui": "workspace:*",
25
  "@marsidev/react-turnstile": "^1.5.2",
26
  "@tailwindcss/vite": "^4.2.2",
27
  "@tanstack/react-form": "^1.28.0",
@@ -45,7 +45,7 @@
45
  },
46
  "devDependencies": {
47
  "@eslint/js": "^10.0.1",
48
- "@labas/config": "workspace:*",
49
  "@tanstack/react-query-devtools": "^5.91.1",
50
  "@tanstack/react-router-devtools": "^1.166.13",
51
  "@tanstack/router-plugin": "^1.167.22",
 
17
  },
18
  "dependencies": {
19
  "@hookform/resolvers": "^5.2.2",
20
+ "@pram/ai": "workspace:*",
21
+ "@pram/api": "workspace:*",
22
+ "@pram/auth": "workspace:*",
23
+ "@pram/env": "workspace:*",
24
+ "@pram/ui": "workspace:*",
25
  "@marsidev/react-turnstile": "^1.5.2",
26
  "@tailwindcss/vite": "^4.2.2",
27
  "@tanstack/react-form": "^1.28.0",
 
45
  },
46
  "devDependencies": {
47
  "@eslint/js": "^10.0.1",
48
+ "@pram/config": "workspace:*",
49
  "@tanstack/react-query-devtools": "^5.91.1",
50
  "@tanstack/react-router-devtools": "^1.166.13",
51
  "@tanstack/router-plugin": "^1.167.22",
apps/web/public/{labas_icon.png → apple-touch-icon-180x180.png} RENAMED
File without changes
apps/web/public/favicon.ico ADDED

Git LFS Details

  • SHA256: e394e2946280f200160275e9af3cd9a98f2b6d016f16b69d30037f524ff3bf15
  • Pointer size: 129 Bytes
  • Size of remote file: 1.13 kB
apps/web/public/logo.png CHANGED

Git LFS Details

  • SHA256: e39f5b556433db51ae13ac44126c0f37b1c51b566b711ba4b954d834e99d4362
  • Pointer size: 131 Bytes
  • Size of remote file: 250 kB

Git LFS Details

  • SHA256: 53415d59f2c001f6e68689937ec9cd57729379fb37e314d5503834749622e681
  • Pointer size: 131 Bytes
  • Size of remote file: 429 kB
apps/web/public/maskable-icon-512x512.png ADDED

Git LFS Details

  • SHA256: 90812f0875abfe9f88f2318c9dfffcf776c1c590863ab463c7a763f6d9f4a289
  • Pointer size: 129 Bytes
  • Size of remote file: 9.03 kB
apps/web/public/pram_icon.png ADDED

Git LFS Details

  • SHA256: 53415d59f2c001f6e68689937ec9cd57729379fb37e314d5503834749622e681
  • Pointer size: 131 Bytes
  • Size of remote file: 429 kB
apps/web/public/pwa-192x192.png ADDED

Git LFS Details

  • SHA256: 67e6da6d38061996b08cca1de901e2751b697af4fcf29053081b9b5d30d0c276
  • Pointer size: 129 Bytes
  • Size of remote file: 4.52 kB
apps/web/public/pwa-512x512.png ADDED

Git LFS Details

  • SHA256: 99a12b83cd72a5c00aa0963cdbe8fa6ddfc8e30a58c422952f84b53c327240b1
  • Pointer size: 130 Bytes
  • Size of remote file: 20.6 kB
apps/web/public/pwa-64x64.png ADDED

Git LFS Details

  • SHA256: b6086f8c10632c5829b96f4f02e2d7dd485d229c38522b11e03a4691e54fb87c
  • Pointer size: 129 Bytes
  • Size of remote file: 1.06 kB
apps/web/public/robots.txt CHANGED
@@ -1,3 +1,3 @@
1
- User-agent: *
2
  Allow: /
3
- Sitemap: https://labas.rogasper.com/sitemap.xml
 
1
+ User-agent: *
2
  Allow: /
3
+ Sitemap: https://pram.rogasper.com/sitemap.xml
apps/web/public/sitemap.xml CHANGED
@@ -1,27 +1,27 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
  <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
3
  <url>
4
- <loc>https://labas.rogasper.com/</loc>
5
  <changefreq>weekly</changefreq>
6
  <priority>1.0</priority>
7
  </url>
8
  <url>
9
- <loc>https://labas.rogasper.com/login</loc>
10
  <changefreq>monthly</changefreq>
11
  <priority>0.5</priority>
12
  </url>
13
  <url>
14
- <loc>https://labas.rogasper.com/bank</loc>
15
  <changefreq>weekly</changefreq>
16
  <priority>0.8</priority>
17
  </url>
18
  <url>
19
- <loc>https://labas.rogasper.com/packages</loc>
20
  <changefreq>weekly</changefreq>
21
  <priority>0.8</priority>
22
  </url>
23
  <url>
24
- <loc>https://labas.rogasper.com/leaderboard</loc>
25
  <changefreq>daily</changefreq>
26
  <priority>0.6</priority>
27
  </url>
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
  <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
3
  <url>
4
+ <loc>https://pram.rogasper.com/</loc>
5
  <changefreq>weekly</changefreq>
6
  <priority>1.0</priority>
7
  </url>
8
  <url>
9
+ <loc>https://pram.rogasper.com/login</loc>
10
  <changefreq>monthly</changefreq>
11
  <priority>0.5</priority>
12
  </url>
13
  <url>
14
+ <loc>https://pram.rogasper.com/bank</loc>
15
  <changefreq>weekly</changefreq>
16
  <priority>0.8</priority>
17
  </url>
18
  <url>
19
+ <loc>https://pram.rogasper.com/packages</loc>
20
  <changefreq>weekly</changefreq>
21
  <priority>0.8</priority>
22
  </url>
23
  <url>
24
+ <loc>https://pram.rogasper.com/leaderboard</loc>
25
  <changefreq>daily</changefreq>
26
  <priority>0.6</priority>
27
  </url>
apps/web/pwa-assets.config.ts CHANGED
@@ -5,5 +5,5 @@ export default defineConfig({
5
  preset: "2023",
6
  },
7
  preset,
8
- images: ["public/labas_icon.png"],
9
  });
 
5
  preset: "2023",
6
  },
7
  preset,
8
+ images: ["public/pram_icon.png"],
9
  });
apps/web/src/components/CommunityModal.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@labas/ui/components/dialog";
2
  import { MaterialIcon } from "@/components/ui/MaterialIcon";
3
  import { CommunityChannelList } from "@/components/CommunityChannelList";
4
 
@@ -17,10 +17,10 @@ export function CommunityModal({
17
  <MaterialIcon name="forum" className="text-4xl text-[var(--matcha-800)]" />
18
  </div>
19
  <DialogTitle className="text-center text-2xl font-headline font-extrabold text-[var(--clay-black)]">
20
- Gabung Komunitas Labas
21
  </DialogTitle>
22
  <DialogDescription className="text-center text-base text-[var(--warm-charcoal)] pt-2 leading-relaxed">
23
- Belajar bahasa, lapor bug, atau ikut ngembangin Labas — pilih channel yang paling pas.
24
  </DialogDescription>
25
  </DialogHeader>
26
  <div className="mt-6">
 
1
+ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@pram/ui/components/dialog";
2
  import { MaterialIcon } from "@/components/ui/MaterialIcon";
3
  import { CommunityChannelList } from "@/components/CommunityChannelList";
4
 
 
17
  <MaterialIcon name="forum" className="text-4xl text-[var(--matcha-800)]" />
18
  </div>
19
  <DialogTitle className="text-center text-2xl font-headline font-extrabold text-[var(--clay-black)]">
20
+ Gabung Komunitas Pram
21
  </DialogTitle>
22
  <DialogDescription className="text-center text-base text-[var(--warm-charcoal)] pt-2 leading-relaxed">
23
+ Belajar bahasa, lapor bug, atau ikut ngembangin Pram — pilih channel yang paling pas.
24
  </DialogDescription>
25
  </DialogHeader>
26
  <div className="mt-6">
apps/web/src/components/DonationModal.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@labas/ui/components/dialog";
2
  import { MaterialIcon } from "@/components/ui/MaterialIcon";
3
 
4
  export function DonationModal({
@@ -18,12 +18,12 @@ export function DonationModal({
18
  <MaterialIcon name="favorite" className="text-4xl text-[var(--pomegranate-400)]" />
19
  </div>
20
  <DialogTitle className="text-center text-2xl font-headline font-extrabold text-[var(--clay-black)]">
21
- Dukung Labas!
22
  </DialogTitle>
23
  <DialogDescription className="text-center text-base text-[var(--warm-charcoal)] pt-2 leading-relaxed">
24
  Selamat, kamu baru saja menyelesaikan {triggerAction === "exam" ? "latihan soal" : "generate soal AI"}! 🎉
25
  <br/><br/>
26
- Jika Labas membantumu, dukung kami untuk menutupi biaya server & mengembangkan fitur baru dengan mentraktir kopi. ☕
27
  </DialogDescription>
28
  </DialogHeader>
29
  <div className="flex flex-col gap-3 mt-6">
 
1
+ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@pram/ui/components/dialog";
2
  import { MaterialIcon } from "@/components/ui/MaterialIcon";
3
 
4
  export function DonationModal({
 
18
  <MaterialIcon name="favorite" className="text-4xl text-[var(--pomegranate-400)]" />
19
  </div>
20
  <DialogTitle className="text-center text-2xl font-headline font-extrabold text-[var(--clay-black)]">
21
+ Dukung Pram!
22
  </DialogTitle>
23
  <DialogDescription className="text-center text-base text-[var(--warm-charcoal)] pt-2 leading-relaxed">
24
  Selamat, kamu baru saja menyelesaikan {triggerAction === "exam" ? "latihan soal" : "generate soal AI"}! 🎉
25
  <br/><br/>
26
+ Jika Pram membantumu, dukung kami untuk menutupi biaya server & mengembangkan fitur baru dengan mentraktir kopi. ☕
27
  </DialogDescription>
28
  </DialogHeader>
29
  <div className="flex flex-col gap-3 mt-6">
apps/web/src/components/ErrorFallback.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import { Button } from "@labas/ui/components/button";
2
 
3
  export function ErrorFallback({ error, reset }: { error: Error; reset?: () => void }) {
4
  return (
 
1
+ import { Button } from "@pram/ui/components/button";
2
 
3
  export function ErrorFallback({ error, reset }: { error: Error; reset?: () => void }) {
4
  return (
apps/web/src/components/GettingStartedCard.tsx CHANGED
@@ -1,9 +1,9 @@
1
  import { useEffect, useState } from "react";
2
  import { Link } from "@tanstack/react-router";
3
- import { Card, CardContent } from "@labas/ui/components/card";
4
  import { MaterialIcon } from "@/components/ui/MaterialIcon";
5
 
6
- const LS_KEY = "labas-getting-started-dismissed";
7
 
8
  const steps = [
9
  {
 
1
  import { useEffect, useState } from "react";
2
  import { Link } from "@tanstack/react-router";
3
+ import { Card, CardContent } from "@pram/ui/components/card";
4
  import { MaterialIcon } from "@/components/ui/MaterialIcon";
5
 
6
+ const LS_KEY = "pram-getting-started-dismissed";
7
 
8
  const steps = [
9
  {
apps/web/src/components/NotFoundPage.tsx CHANGED
@@ -1,5 +1,5 @@
1
  import { Link } from "@tanstack/react-router";
2
- import { Button } from "@labas/ui/components/button";
3
 
4
  export function NotFoundPage() {
5
  return (
 
1
  import { Link } from "@tanstack/react-router";
2
+ import { Button } from "@pram/ui/components/button";
3
 
4
  export function NotFoundPage() {
5
  return (
apps/web/src/components/PageGuide.tsx CHANGED
@@ -1,5 +1,5 @@
1
  import { useState } from "react";
2
- import { Card, CardContent } from "@labas/ui/components/card";
3
  import { MaterialIcon } from "@/components/ui/MaterialIcon";
4
 
5
  interface GuideItem {
 
1
  import { useState } from "react";
2
+ import { Card } from "@pram/ui/components/card";
3
  import { MaterialIcon } from "@/components/ui/MaterialIcon";
4
 
5
  interface GuideItem {
apps/web/src/components/TourGuide.tsx CHANGED
@@ -7,8 +7,7 @@ const PageTourImpl = lazy(() => import("./TourGuideImpl").then((m) => ({ default
7
 
8
  export type { Step };
9
 
10
- const LS_GLOBAL = "labas-tour-completed";
11
- const TRIGGER_GLOBAL = "labas-tour-trigger";
12
 
13
  export function TourGuide() {
14
  return (
 
7
 
8
  export type { Step };
9
 
10
+ const TRIGGER_GLOBAL = "pram-tour-trigger";
 
11
 
12
  export function TourGuide() {
13
  return (
apps/web/src/components/TourGuideImpl.tsx CHANGED
@@ -1,6 +1,5 @@
1
  import { useState, useEffect, type CSSProperties } from "react";
2
  import { Joyride, type Step, type EventData } from "react-joyride";
3
- import { MaterialIcon } from "@/components/ui/MaterialIcon";
4
 
5
  const joyrideOptions = {
6
  primaryColor: "var(--matcha-600)",
@@ -24,11 +23,11 @@ export type { Step, EventData };
24
 
25
  // ── Global site tour ──
26
 
27
- const LS_GLOBAL = "labas-tour-completed";
28
- const TRIGGER_GLOBAL = "labas-tour-trigger";
29
 
30
  const globalSteps: Step[] = [
31
- { target: "body", placement: "center", title: "Selamat Datang di Labas!", content: "Platform latihan ujian bahasa berbasis AI. Yuk, kita lihat fitur-fitur utamanya!", hideOverlay: true },
32
  { target: "[data-tour='dashboard-stats']", title: "Ringkasan Aktivitas", content: "Pantau jumlah latihan, waktu belajar, soal terjawab, dan akurasi kamu di sini.", spotlightPadding: 8 },
33
  { target: "[data-tour='nav-generate']", title: "AI Lab — Generate Soal", content: "Buat soal latihan sendiri pakai AI. Pilih jenis ujian, section, format, dan topik.", spotlightPadding: 4 },
34
  { target: "[data-tour='nav-bank']", title: "Bank Soal — Buat Paket", content: "Atur soal-soal kamu jadi paket latihan dari bank soal.", spotlightPadding: 4 },
 
1
  import { useState, useEffect, type CSSProperties } from "react";
2
  import { Joyride, type Step, type EventData } from "react-joyride";
 
3
 
4
  const joyrideOptions = {
5
  primaryColor: "var(--matcha-600)",
 
23
 
24
  // ── Global site tour ──
25
 
26
+ const LS_GLOBAL = "pram-tour-completed";
27
+ const TRIGGER_GLOBAL = "pram-tour-trigger";
28
 
29
  const globalSteps: Step[] = [
30
+ { target: "body", placement: "center", title: "Selamat Datang di Pram!", content: "Platform latihan ujian bahasa berbasis AI. Yuk, kita lihat fitur-fitur utamanya!", hideOverlay: true },
31
  { target: "[data-tour='dashboard-stats']", title: "Ringkasan Aktivitas", content: "Pantau jumlah latihan, waktu belajar, soal terjawab, dan akurasi kamu di sini.", spotlightPadding: 8 },
32
  { target: "[data-tour='nav-generate']", title: "AI Lab — Generate Soal", content: "Buat soal latihan sendiri pakai AI. Pilih jenis ujian, section, format, dan topik.", spotlightPadding: 4 },
33
  { target: "[data-tour='nav-bank']", title: "Bank Soal — Buat Paket", content: "Atur soal-soal kamu jadi paket latihan dari bank soal.", spotlightPadding: 4 },
apps/web/src/components/TurnstileField.tsx CHANGED
@@ -1,6 +1,6 @@
1
  import { Turnstile, type TurnstileInstance, type TurnstileProps } from "@marsidev/react-turnstile";
2
  import { forwardRef } from "react";
3
- import { env } from "@labas/env/web";
4
 
5
  export const TURNSTILE_SITE_KEY = env.VITE_CLOUDFLARE_TURNSTILE_SITE_KEY;
6
 
 
1
  import { Turnstile, type TurnstileInstance, type TurnstileProps } from "@marsidev/react-turnstile";
2
  import { forwardRef } from "react";
3
+ import { env } from "@pram/env/web";
4
 
5
  export const TURNSTILE_SITE_KEY = env.VITE_CLOUDFLARE_TURNSTILE_SITE_KEY;
6
 
apps/web/src/components/admin/Pagination.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import { Button } from "@labas/ui/components/button";
2
 
3
  interface PaginationProps {
4
  page: number;
 
1
+ import { Button } from "@pram/ui/components/button";
2
 
3
  interface PaginationProps {
4
  page: number;
apps/web/src/components/analytics/BreakdownCharts.tsx CHANGED
@@ -8,10 +8,9 @@ import {
8
  ResponsiveContainer,
9
  Cell,
10
  } from "recharts";
11
- import { Card, CardContent, CardHeader, CardTitle } from "@labas/ui/components/card";
12
  import { formatLabel } from "@/lib/format";
13
  import { formatTime } from "@/lib/time";
14
- import { MaterialIcon } from "@/components/ui/MaterialIcon";
15
 
16
  export interface ExamTypeItem {
17
  examTypeId: string;
 
8
  ResponsiveContainer,
9
  Cell,
10
  } from "recharts";
11
+ import { Card, CardContent, CardHeader, CardTitle } from "@pram/ui/components/card";
12
  import { formatLabel } from "@/lib/format";
13
  import { formatTime } from "@/lib/time";
 
14
 
15
  export interface ExamTypeItem {
16
  examTypeId: string;
apps/web/src/components/analytics/OverviewCards.tsx CHANGED
@@ -1,5 +1,5 @@
1
  import { MaterialIcon } from "@/components/ui/MaterialIcon";
2
- import { Card, CardContent } from "@labas/ui/components/card";
3
  import { formatTime } from "@/lib/time";
4
 
5
  interface OverviewCardsProps {
 
1
  import { MaterialIcon } from "@/components/ui/MaterialIcon";
2
+ import { Card, CardContent } from "@pram/ui/components/card";
3
  import { formatTime } from "@/lib/time";
4
 
5
  interface OverviewCardsProps {
apps/web/src/components/analytics/ScoreTrendChart.tsx CHANGED
@@ -7,7 +7,7 @@ import {
7
  Tooltip,
8
  ResponsiveContainer,
9
  } from "recharts";
10
- import { Card, CardContent, CardHeader, CardTitle } from "@labas/ui/components/card";
11
 
12
  interface TrendPoint {
13
  date: string;
 
7
  Tooltip,
8
  ResponsiveContainer,
9
  } from "recharts";
10
+ import { Card, CardContent, CardHeader, CardTitle } from "@pram/ui/components/card";
11
 
12
  interface TrendPoint {
13
  date: string;
apps/web/src/components/analytics/TimeAnalyticsPanel.tsx CHANGED
@@ -7,7 +7,7 @@ import {
7
  Tooltip,
8
  ResponsiveContainer,
9
  } from "recharts";
10
- import { Card, CardContent, CardHeader, CardTitle } from "@labas/ui/components/card";
11
  import { formatTime } from "@/lib/time";
12
  import { formatLabel } from "@/lib/format";
13
 
 
7
  Tooltip,
8
  ResponsiveContainer,
9
  } from "recharts";
10
+ import { Card, CardContent, CardHeader, CardTitle } from "@pram/ui/components/card";
11
  import { formatTime } from "@/lib/time";
12
  import { formatLabel } from "@/lib/format";
13
 
apps/web/src/components/analytics/WeaknessPanel.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import { Card, CardContent, CardHeader, CardTitle } from "@labas/ui/components/card";
2
  import { MaterialIcon } from "@/components/ui/MaterialIcon";
3
  import { formatLabel } from "@/lib/format";
4
 
@@ -57,7 +57,7 @@ export function WeaknessPanel({ weaknesses, recommendations }: WeaknessPanelProp
57
  {hasData ? (
58
  <>
59
  <div className="space-y-3">
60
- {weaknesses.map((w, i) => (
61
  <div
62
  key={`${w.type}-${w.name}`}
63
  className="flex items-center gap-4 p-3 rounded-[var(--radius-md)] bg-[var(--warm-cream)] border-2 border-[var(--oat-border)]"
 
1
+ import { Card, CardContent, CardHeader, CardTitle } from "@pram/ui/components/card";
2
  import { MaterialIcon } from "@/components/ui/MaterialIcon";
3
  import { formatLabel } from "@/lib/format";
4
 
 
57
  {hasData ? (
58
  <>
59
  <div className="space-y-3">
60
+ {weaknesses.map((w, _i) => (
61
  <div
62
  key={`${w.type}-${w.name}`}
63
  className="flex items-center gap-4 p-3 rounded-[var(--radius-md)] bg-[var(--warm-cream)] border-2 border-[var(--oat-border)]"
apps/web/src/components/attempt/OptionDisplay.tsx CHANGED
@@ -1,6 +1,6 @@
1
  import { MaterialIcon } from "@/components/ui/MaterialIcon";
2
 
3
- import { MCQ_FORMATS, McqFormat } from "@/lib/question-formats";
4
 
5
  interface McqOptionItem {
6
  key: string;
 
1
  import { MaterialIcon } from "@/components/ui/MaterialIcon";
2
 
3
+ import { MCQ_FORMATS } from "@/lib/question-formats";
4
 
5
  interface McqOptionItem {
6
  key: string;
apps/web/src/components/attempt/QuestionReviewCard.tsx CHANGED
@@ -1,8 +1,8 @@
1
  import { useState } from "react";
2
  import { useQuery, useMutation } from "@tanstack/react-query";
3
  import { trpc } from "@/utils/trpc";
4
- import { Button } from "@labas/ui/components/button";
5
- import { Input } from "@labas/ui/components/input";
6
  import { MaterialIcon } from "@/components/ui/MaterialIcon";
7
  import { formatTime } from "@/lib/time";
8
  import type { Question } from "@/lib/types";
 
1
  import { useState } from "react";
2
  import { useQuery, useMutation } from "@tanstack/react-query";
3
  import { trpc } from "@/utils/trpc";
4
+ import { Button } from "@pram/ui/components/button";
5
+ import { Input } from "@pram/ui/components/input";
6
  import { MaterialIcon } from "@/components/ui/MaterialIcon";
7
  import { formatTime } from "@/lib/time";
8
  import type { Question } from "@/lib/types";
apps/web/src/components/bank/AdvancedFilters.tsx CHANGED
@@ -5,7 +5,7 @@ import {
5
  SelectItem,
6
  SelectTrigger,
7
  SelectValue,
8
- } from "@labas/ui/components/select";
9
  import { SECTIONS, FORMATS, DIFFICULTIES } from "@/lib/exam-constants";
10
  import { formatLabel } from "@/lib/format";
11
 
 
5
  SelectItem,
6
  SelectTrigger,
7
  SelectValue,
8
+ } from "@pram/ui/components/select";
9
  import { SECTIONS, FORMATS, DIFFICULTIES } from "@/lib/exam-constants";
10
  import { formatLabel } from "@/lib/format";
11
 
apps/web/src/components/bank/AutoBundleModal.tsx CHANGED
@@ -1,12 +1,12 @@
1
  import { useState } from "react";
2
- import { Input } from "@labas/ui/components/input";
3
- import { Button } from "@labas/ui/components/button";
4
  import {
5
  Dialog,
6
  DialogContent,
7
  DialogHeader,
8
  DialogTitle,
9
- } from "@labas/ui/components/dialog";
10
  import { MaterialIcon } from "@/components/ui/MaterialIcon";
11
 
12
  export function AutoBundleModal({
 
1
  import { useState } from "react";
2
+ import { Input } from "@pram/ui/components/input";
3
+ import { Button } from "@pram/ui/components/button";
4
  import {
5
  Dialog,
6
  DialogContent,
7
  DialogHeader,
8
  DialogTitle,
9
+ } from "@pram/ui/components/dialog";
10
  import { MaterialIcon } from "@/components/ui/MaterialIcon";
11
 
12
  export function AutoBundleModal({
apps/web/src/components/bank/BundleSidebar.tsx CHANGED
@@ -1,5 +1,5 @@
1
- import { Button } from "@labas/ui/components/button";
2
- import { Input } from "@labas/ui/components/input";
3
  import { MaterialIcon } from "@/components/ui/MaterialIcon";
4
  import { formatLabel } from "@/lib/format";
5
 
 
1
+ import { Button } from "@pram/ui/components/button";
2
+ import { Input } from "@pram/ui/components/input";
3
  import { MaterialIcon } from "@/components/ui/MaterialIcon";
4
  import { formatLabel } from "@/lib/format";
5
 
apps/web/src/components/bank/CalloutCard.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import { Button } from "@labas/ui/components/button";
2
  import { MaterialIcon } from "@/components/ui/MaterialIcon";
3
 
4
  interface CalloutCardProps {
 
1
+ import { Button } from "@pram/ui/components/button";
2
  import { MaterialIcon } from "@/components/ui/MaterialIcon";
3
 
4
  interface CalloutCardProps {
apps/web/src/components/bank/CreatePackageModal.tsx CHANGED
@@ -1,12 +1,12 @@
1
  import { useState } from "react";
2
- import { Input } from "@labas/ui/components/input";
3
- import { Button } from "@labas/ui/components/button";
4
  import {
5
  Dialog,
6
  DialogContent,
7
  DialogHeader,
8
  DialogTitle,
9
- } from "@labas/ui/components/dialog";
10
  import { MaterialIcon } from "@/components/ui/MaterialIcon";
11
 
12
  export function CreatePackageModal({
 
1
  import { useState } from "react";
2
+ import { Input } from "@pram/ui/components/input";
3
+ import { Button } from "@pram/ui/components/button";
4
  import {
5
  Dialog,
6
  DialogContent,
7
  DialogHeader,
8
  DialogTitle,
9
+ } from "@pram/ui/components/dialog";
10
  import { MaterialIcon } from "@/components/ui/MaterialIcon";
11
 
12
  export function CreatePackageModal({
apps/web/src/components/bank/FilterBar.tsx CHANGED
@@ -1,7 +1,6 @@
1
- import { useState } from "react";
2
  import { SlidersHorizontal } from "lucide-react";
3
- import { Button } from "@labas/ui/components/button";
4
- import { Input } from "@labas/ui/components/input";
5
  import { MaterialIcon } from "@/components/ui/MaterialIcon";
6
  import { EXAM_TYPES } from "@/lib/exam-constants";
7
 
 
 
1
  import { SlidersHorizontal } from "lucide-react";
2
+ import { Button } from "@pram/ui/components/button";
3
+ import { Input } from "@pram/ui/components/input";
4
  import { MaterialIcon } from "@/components/ui/MaterialIcon";
5
  import { EXAM_TYPES } from "@/lib/exam-constants";
6
 
apps/web/src/components/bank/MobileFilterSheet.tsx CHANGED
@@ -4,7 +4,7 @@ import {
4
  SheetHeader,
5
  SheetTitle,
6
  SheetDescription,
7
- } from "@labas/ui/components/sheet";
8
  import {
9
  Select,
10
  SelectContent,
@@ -12,8 +12,8 @@ import {
12
  SelectItem,
13
  SelectTrigger,
14
  SelectValue,
15
- } from "@labas/ui/components/select";
16
- import { Button } from "@labas/ui/components/button";
17
  import { MaterialIcon } from "@/components/ui/MaterialIcon";
18
  import { SECTIONS, FORMATS, DIFFICULTIES } from "@/lib/exam-constants";
19
  import { formatLabel } from "@/lib/format";
 
4
  SheetHeader,
5
  SheetTitle,
6
  SheetDescription,
7
+ } from "@pram/ui/components/sheet";
8
  import {
9
  Select,
10
  SelectContent,
 
12
  SelectItem,
13
  SelectTrigger,
14
  SelectValue,
15
+ } from "@pram/ui/components/select";
16
+ import { Button } from "@pram/ui/components/button";
17
  import { MaterialIcon } from "@/components/ui/MaterialIcon";
18
  import { SECTIONS, FORMATS, DIFFICULTIES } from "@/lib/exam-constants";
19
  import { formatLabel } from "@/lib/format";
apps/web/src/components/bank/QuestionCard.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import { Button } from "@labas/ui/components/button";
2
  import { MaterialIcon } from "@/components/ui/MaterialIcon";
3
  import { formatLabel } from "@/lib/format";
4
  import type { Question } from "@/lib/types";
 
1
+ import { Button } from "@pram/ui/components/button";
2
  import { MaterialIcon } from "@/components/ui/MaterialIcon";
3
  import { formatLabel } from "@/lib/format";
4
  import type { Question } from "@/lib/types";
apps/web/src/components/bank/QuestionDetailModal.tsx CHANGED
@@ -1,12 +1,11 @@
1
  import { useQuery, useMutation } from "@tanstack/react-query";
2
- import { Button } from "@labas/ui/components/button";
3
- import { Card, CardContent } from "@labas/ui/components/card";
4
  import {
5
  Dialog,
6
  DialogContent,
7
  DialogHeader,
8
- DialogTitle,
9
- } from "@labas/ui/components/dialog";
10
  import { authClient } from "@/lib/auth-client";
11
  import { trpc } from "@/utils/trpc";
12
  import { formatLabel } from "@/lib/format";
 
1
  import { useQuery, useMutation } from "@tanstack/react-query";
2
+ import { Button } from "@pram/ui/components/button";
3
+ import { Card, CardContent } from "@pram/ui/components/card";
4
  import {
5
  Dialog,
6
  DialogContent,
7
  DialogHeader,
8
+ } from "@pram/ui/components/dialog";
 
9
  import { authClient } from "@/lib/auth-client";
10
  import { trpc } from "@/utils/trpc";
11
  import { formatLabel } from "@/lib/format";
apps/web/src/components/bank/SectionBrowser.tsx CHANGED
@@ -1,5 +1,5 @@
1
- import { Card, CardContent } from "@labas/ui/components/card";
2
- import { Button } from "@labas/ui/components/button";
3
  import { MaterialIcon } from "@/components/ui/MaterialIcon";
4
  import type { PackageSection } from "@/lib/types";
5
 
 
1
+ import { Card, CardContent } from "@pram/ui/components/card";
2
+ import { Button } from "@pram/ui/components/button";
3
  import { MaterialIcon } from "@/components/ui/MaterialIcon";
4
  import type { PackageSection } from "@/lib/types";
5