pramudya-yb commited on
Commit ·
2b62b0c
1
Parent(s): 7faa899
Rename project from Labas to Pram
Browse filesRebrand 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
- AGENTS.md +13 -13
- DESIGN.md +2 -2
- PRD.md +28 -87
- README.md +22 -30
- apps/server/.env.example +1 -1
- apps/server/package.json +5 -5
- apps/server/src/index.ts +8 -8
- apps/server/tsconfig.json +1 -1
- apps/server/tsdown.config.ts +1 -1
- apps/web/components.json +2 -2
- apps/web/index.html +13 -11
- apps/web/package.json +6 -6
- apps/web/public/{labas_icon.png → apple-touch-icon-180x180.png} +2 -2
- apps/web/public/favicon.ico +3 -0
- apps/web/public/logo.png +2 -2
- apps/web/public/maskable-icon-512x512.png +3 -0
- apps/web/public/pram_icon.png +3 -0
- apps/web/public/pwa-192x192.png +3 -0
- apps/web/public/pwa-512x512.png +3 -0
- apps/web/public/pwa-64x64.png +3 -0
- apps/web/public/robots.txt +2 -2
- apps/web/public/sitemap.xml +6 -6
- apps/web/pwa-assets.config.ts +1 -1
- apps/web/src/components/CommunityModal.tsx +3 -3
- apps/web/src/components/DonationModal.tsx +3 -3
- apps/web/src/components/ErrorFallback.tsx +1 -1
- apps/web/src/components/GettingStartedCard.tsx +2 -2
- apps/web/src/components/NotFoundPage.tsx +1 -1
- apps/web/src/components/PageGuide.tsx +1 -1
- apps/web/src/components/TourGuide.tsx +1 -2
- apps/web/src/components/TourGuideImpl.tsx +3 -4
- apps/web/src/components/TurnstileField.tsx +1 -1
- apps/web/src/components/admin/Pagination.tsx +1 -1
- apps/web/src/components/analytics/BreakdownCharts.tsx +1 -2
- apps/web/src/components/analytics/OverviewCards.tsx +1 -1
- apps/web/src/components/analytics/ScoreTrendChart.tsx +1 -1
- apps/web/src/components/analytics/TimeAnalyticsPanel.tsx +1 -1
- apps/web/src/components/analytics/WeaknessPanel.tsx +2 -2
- apps/web/src/components/attempt/OptionDisplay.tsx +1 -1
- apps/web/src/components/attempt/QuestionReviewCard.tsx +2 -2
- apps/web/src/components/bank/AdvancedFilters.tsx +1 -1
- apps/web/src/components/bank/AutoBundleModal.tsx +3 -3
- apps/web/src/components/bank/BundleSidebar.tsx +2 -2
- apps/web/src/components/bank/CalloutCard.tsx +1 -1
- apps/web/src/components/bank/CreatePackageModal.tsx +3 -3
- apps/web/src/components/bank/FilterBar.tsx +2 -3
- apps/web/src/components/bank/MobileFilterSheet.tsx +3 -3
- apps/web/src/components/bank/QuestionCard.tsx +1 -1
- apps/web/src/components/bank/QuestionDetailModal.tsx +3 -4
- apps/web/src/components/bank/SectionBrowser.tsx +2 -2
AGENTS.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
# AGENTS.md —
|
| 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:** `
|
| 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 `@
|
| 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 |
-
|
| 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 `@
|
| 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 `@
|
| 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 "@
|
| 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 `@
|
| 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 "@
|
| 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 "@
|
| 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("@
|
| 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 `@
|
| 369 |
-
- Env vars di-mock via `mock.module("@
|
| 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 "@
|
| 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:
|
| 2 |
|
| 3 |
## Problem Statement
|
| 4 |
|
| 5 |
-
|
| 6 |
-
- **Bervariasi** dan mencakup format soal spesifik
|
| 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 |
-
**
|
| 14 |
-
1. **AI Question Generator** — generate soal
|
| 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. **
|
| 18 |
-
5. **
|
| 19 |
-
6. **Deep Analytics** — evaluasi per-section, identifikasi kelemahan, rekomendasi paket soal untuk perbaikan
|
| 20 |
|
| 21 |
-
Target pengguna:
|
| 22 |
|
| 23 |
## User Stories
|
| 24 |
|
| 25 |
### AI Generation
|
| 26 |
-
1. Sebagai
|
| 27 |
-
2. Sebagai
|
| 28 |
-
3. Sebagai
|
| 29 |
-
4. Sebagai
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 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 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 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 "
|
|
|
|
| 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="
|
| 3 |
</p>
|
| 4 |
|
| 5 |
-
<h1 align="center">
|
| 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
|
| 24 |
|
| 25 |
-
**
|
| 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 |
-
- **
|
| 33 |
-
- **20+ question formats** — Multiple choice, true/false
|
| 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 |
-
- **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
|
| 44 |
|
| 45 |
-
|
|
| 46 |
-
|------
|
| 47 |
-
|
|
| 48 |
-
|
|
| 49 |
-
|
|
| 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>/
|
| 85 |
-
cd
|
| 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/
|
| 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 |
-
|
| 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 `@
|
| 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 "@
|
| 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
|
| 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/
|
| 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 |
-
"@
|
| 16 |
-
"@
|
| 17 |
-
"@
|
| 18 |
-
"@
|
| 19 |
"@trpc/server": "catalog:",
|
| 20 |
"better-auth": "catalog:",
|
| 21 |
"dotenv": "catalog:",
|
|
@@ -24,7 +24,7 @@
|
|
| 24 |
"zod": "catalog:"
|
| 25 |
},
|
| 26 |
"devDependencies": {
|
| 27 |
-
"@
|
| 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 "@
|
| 3 |
-
import { appRouter } from "@
|
| 4 |
-
import { checkRateLimitAllowed } from "@
|
| 5 |
-
import { auth } from "@
|
| 6 |
-
import { env } from "@
|
| 7 |
-
import { withRequestId } from "@
|
| 8 |
import { Hono } from "hono";
|
| 9 |
import { cors } from "hono/cors";
|
| 10 |
|
| 11 |
-
import "@
|
| 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://
|
| 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": "@
|
| 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: [/@
|
| 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": "@
|
| 17 |
-
"ui": "@
|
| 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="
|
| 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://
|
| 11 |
-
<meta property="og:site_name" content="
|
| 12 |
<meta property="og:locale" content="id_ID" />
|
| 13 |
<meta property="og:type" content="website" />
|
| 14 |
-
<meta property="og:image" content="https://
|
| 15 |
-
<meta property="og:image:secure_url" content="https://
|
| 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="
|
| 20 |
<meta name="twitter:card" content="summary_large_image" />
|
| 21 |
-
<meta name="twitter:title" content="
|
| 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://
|
| 24 |
-
<meta name="twitter:image:alt" content="
|
| 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://
|
| 30 |
-
<
|
|
|
|
|
|
|
| 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 |
-
"@
|
| 21 |
-
"@
|
| 22 |
-
"@
|
| 23 |
-
"@
|
| 24 |
-
"@
|
| 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 |
-
"@
|
| 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
|
apps/web/public/logo.png
CHANGED
|
Git LFS Details
|
|
Git LFS Details
|
apps/web/public/maskable-icon-512x512.png
ADDED
|
|
Git LFS Details
|
apps/web/public/pram_icon.png
ADDED
|
|
Git LFS Details
|
apps/web/public/pwa-192x192.png
ADDED
|
Git LFS Details
|
apps/web/public/pwa-512x512.png
ADDED
|
Git LFS Details
|
apps/web/public/pwa-64x64.png
ADDED
|
Git LFS Details
|
apps/web/public/robots.txt
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
-
User-agent: *
|
| 2 |
Allow: /
|
| 3 |
-
Sitemap: https://
|
|
|
|
| 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://
|
| 5 |
<changefreq>weekly</changefreq>
|
| 6 |
<priority>1.0</priority>
|
| 7 |
</url>
|
| 8 |
<url>
|
| 9 |
-
<loc>https://
|
| 10 |
<changefreq>monthly</changefreq>
|
| 11 |
<priority>0.5</priority>
|
| 12 |
</url>
|
| 13 |
<url>
|
| 14 |
-
<loc>https://
|
| 15 |
<changefreq>weekly</changefreq>
|
| 16 |
<priority>0.8</priority>
|
| 17 |
</url>
|
| 18 |
<url>
|
| 19 |
-
<loc>https://
|
| 20 |
<changefreq>weekly</changefreq>
|
| 21 |
<priority>0.8</priority>
|
| 22 |
</url>
|
| 23 |
<url>
|
| 24 |
-
<loc>https://
|
| 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/
|
| 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 "@
|
| 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
|
| 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
|
| 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 "@
|
| 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
|
| 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
|
| 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 "@
|
| 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 "@
|
| 4 |
import { MaterialIcon } from "@/components/ui/MaterialIcon";
|
| 5 |
|
| 6 |
-
const LS_KEY = "
|
| 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 "@
|
| 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
|
| 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
|
| 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 = "
|
| 28 |
-
const TRIGGER_GLOBAL = "
|
| 29 |
|
| 30 |
const globalSteps: Step[] = [
|
| 31 |
-
{ target: "body", placement: "center", title: "Selamat Datang di
|
| 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 "@
|
| 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 "@
|
| 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 "@
|
| 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 "@
|
| 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 "@
|
| 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 "@
|
| 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 "@
|
| 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,
|
| 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
|
| 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 "@
|
| 5 |
-
import { Input } from "@
|
| 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 "@
|
| 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 "@
|
| 3 |
-
import { Button } from "@
|
| 4 |
import {
|
| 5 |
Dialog,
|
| 6 |
DialogContent,
|
| 7 |
DialogHeader,
|
| 8 |
DialogTitle,
|
| 9 |
-
} from "@
|
| 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 "@
|
| 2 |
-
import { Input } from "@
|
| 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 "@
|
| 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 "@
|
| 3 |
-
import { Button } from "@
|
| 4 |
import {
|
| 5 |
Dialog,
|
| 6 |
DialogContent,
|
| 7 |
DialogHeader,
|
| 8 |
DialogTitle,
|
| 9 |
-
} from "@
|
| 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 "@
|
| 4 |
-
import { Input } from "@
|
| 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 "@
|
| 8 |
import {
|
| 9 |
Select,
|
| 10 |
SelectContent,
|
|
@@ -12,8 +12,8 @@ import {
|
|
| 12 |
SelectItem,
|
| 13 |
SelectTrigger,
|
| 14 |
SelectValue,
|
| 15 |
-
} from "@
|
| 16 |
-
import { Button } from "@
|
| 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 "@
|
| 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 "@
|
| 3 |
-
import { Card, CardContent } from "@
|
| 4 |
import {
|
| 5 |
Dialog,
|
| 6 |
DialogContent,
|
| 7 |
DialogHeader,
|
| 8 |
-
|
| 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 "@
|
| 2 |
-
import { Button } from "@
|
| 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 |
|