diff --git a/AGENTS.md b/AGENTS.md index 3d29d42e2453c3dc25f04cfae0a1ae7b9e65b2ca..8ae925148ea753781296a05116fa04c699113ac8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,4 +1,4 @@ -# AGENTS.md — Labas Project Guide +# AGENTS.md — Pram Project Guide > This file is for AI agents. Read this first before making any changes. @@ -6,7 +6,7 @@ ## 1. Project Identity -- **Name:** `labas` — AI-powered multi-language test practice platform. +- **Name:** `pram` — AI-powered multi-language test practice platform. - **Repo type:** Turborepo monorepo (Bun workspaces). - **Workspace packages:** `apps/*`, `packages/*`. - **Runtime & package manager:** **Bun** (`packageManager: "bun@1.3.11"`). @@ -23,7 +23,7 @@ | DB Engine | PostgreSQL | Managed via Drizzle. | | DB ORM | Drizzle ORM | **Not Prisma.** | | Auth | Better-Auth | Email/password + OAuth. | -| UI | shadcn/ui (shared) | Lives in `packages/ui`. Imported as `@labas/ui/components/*`. | +| UI | shadcn/ui (shared) | Lives in `packages/ui`. Imported as `@pram/ui/components/*`. | | AI | OpenAI-compatible API | User-managed API keys via Settings UI. | | Build | Turborepo + `tsdown` | Server compiled with `tsdown`. | @@ -32,7 +32,7 @@ ## 3. Architecture Overview ``` -labas/ +pram/ ├── apps/ │ ├── web/ # Frontend (Vite, TanStack Router) — Port 5173 │ └── server/ # Backend (Hono, tRPC) — Port 3000 @@ -46,7 +46,7 @@ labas/ │ └── config/ # Shared TypeScript configs ``` -- **Internal imports** use the `@labas/*` workspace namespace. +- **Internal imports** use the `@pram/*` workspace namespace. - Do **NOT** create cross-imports between `apps/*` directly; route through `packages/*`. --- @@ -95,7 +95,7 @@ bun run build # Build all packages - ❌ **Do not hardcode API keys** in source code. AI keys are user-managed via the Settings UI. - ❌ **Do not add global CSS** in `apps/web` without checking `packages/ui/src/styles/globals.css` for existing design tokens and CSS variables. - ❌ **Do not skip heading levels**. After `
- +
+
AI-powered multi-language test practice platform @@ -20,38 +20,30 @@ --- -## What is Labas? +## What is Pram? -**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). +**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**. 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**. ## Features -- **AI question generation** — Quick mode (single-pass) and Agentic mode (multi-step pipeline with passage validation and quality checks) -- **8 exam types** — IELTS, TOEFL, JLPT, HSK, Goethe, TOPIK, TOAFL, DELE -- **20+ question formats** — Multiple choice, true/false/not given, fill-in-blank, kanji reading, matching, and more +- **AI question generation** — Quick mode (single-pass) and Agentic mode (multi-step pipeline) +- **CPNS Focus** — TWK, TIU, and TKP sections +- **20+ question formats** — Multiple choice, true/false, fill-in-blank, and more - **Question bank & packages** — Organize generated questions into shareable or private packages - **Mock tests & attempts** — Timed practice sessions with scoring and review - **Analytics & leaderboard** — Track performance over time -- **User-managed AI keys** — Bring your own OpenAI-compatible API key via Settings (never hardcoded in server env) -- **Credit system** — Token-based usage tracking for AI generation -- **Admin panel** — User management, moderation, jobs, credits, and featured content +- **User-managed AI keys** — Bring your own OpenAI-compatible API key - **PWA** — Installable progressive web app -- **Accessible UI** — Shared shadcn/ui components with skip links, proper ARIA, and focus management -## Supported Exams +## Supported Exam Sections -| Exam | Language | Notes | -|------|----------|-------| -| IELTS Academic | English | Reading & Writing sections | -| TOEFL iBT | English | Reading & Writing sections | -| JLPT | Japanese | Kanji annotations supported (`漢字(かんじ)`) | -| HSK | Chinese | | -| Goethe-Zertifikat | German | | -| TOPIK | Korean | Particles, honorifics, speech levels | -| TOAFL | Arabic | RTL text support | -| DELE | Spanish | Verb conjugation focus | +| Section | Type | Notes | +|---------|------|-------| +| TWK | Tes Wawasan Kebangsaan | Nasionalisme, Integritas, Bela Negara, Pilar Negara, Bahasa Indonesia | +| TIU | Tes Intelegensia Umum | Verbal, Numerik, Figural | +| TKP | Tes Karakteristik Pribadi | Pelayanan Publik, Jejaring Kerja, Sosial Budaya, TIK, Profesionalisme, Anti Radikalisme | ## Tech Stack @@ -81,8 +73,8 @@ The project is a **Turborepo monorepo** managed with **Bun**. The web app talks ### 1. Clone and install ```bash -git clone https://github.com//labas.git -cd labas +git clone https://github.com//pram.git +cd pram bun install ``` @@ -111,7 +103,7 @@ Edit **`apps/server/.env`**: ```env # PostgreSQL (matches docker-compose defaults) -DATABASE_URL=postgresql://postgres:password@localhost:5432/labas +DATABASE_URL=postgresql://postgres:password@localhost:5432/pram # Redis (defaults to redis://localhost:6379 if omitted) REDIS_URL=redis://localhost:6379 @@ -186,7 +178,7 @@ AI logic lives in [`packages/ai/`](./packages/ai/). ## Project Structure ``` -labas/ +pram/ ├── apps/ │ ├── web/ # Frontend (React + TanStack Router) — port 3001 │ └── server/ # Backend entry (Hono + tRPC) — port 3000 @@ -202,7 +194,7 @@ labas/ └── turbo.json ``` -Internal imports use the `@labas/*` workspace namespace. Apps should not import from each other directly — share code through `packages/*`. +Internal imports use the `@pram/*` workspace namespace. Apps should not import from each other directly — share code through `packages/*`. ## Available Scripts @@ -257,7 +249,7 @@ npx shadcn@latest add accordion dialog -c packages/ui Import shared components: ```tsx -import { Button } from "@labas/ui/components/button"; +import { Button } from "@pram/ui/components/button"; ``` 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` ## Support -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. ☕ +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. ☕ - **[Saweria](https://saweria.co/rogasper)** (Indonesian supporters) - **[Ko-fi](https://ko-fi.com/rogasper)** (International) diff --git a/apps/server/.env.example b/apps/server/.env.example index 9f04a6ab7474d24184e984cd8a9914559fe0a844..3c53d1abe8668042261e6224a937be9493978f52 100644 --- a/apps/server/.env.example +++ b/apps/server/.env.example @@ -1,5 +1,5 @@ # PostgreSQL connection string -# Local dev (docker-compose): postgresql://postgres:password@localhost:5432/labas +# Local dev (docker-compose): postgresql://postgres:password@localhost:5432/pram DATABASE_URL= # Redis — required for AI generation job queue diff --git a/apps/server/package.json b/apps/server/package.json index 93e02d9a9380b9be4670790cf00d55a393762a7e..f51bb68d8d421b35a63ce2612f5f7e7152e938e6 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -12,10 +12,10 @@ }, "dependencies": { "@hono/trpc-server": "^0.4.0", - "@labas/api": "workspace:*", - "@labas/auth": "workspace:*", - "@labas/db": "workspace:*", - "@labas/env": "workspace:*", + "@pram/api": "workspace:*", + "@pram/auth": "workspace:*", + "@pram/db": "workspace:*", + "@pram/env": "workspace:*", "@trpc/server": "catalog:", "better-auth": "catalog:", "dotenv": "catalog:", @@ -24,7 +24,7 @@ "zod": "catalog:" }, "devDependencies": { - "@labas/config": "workspace:*", + "@pram/config": "workspace:*", "@types/bun": "catalog:", "@types/nodemailer": "^8.0.0", "tsdown": "^0.21.9", diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index 3b1570841b99d66f7af47bf4109906c8bc404ae9..742257e7cdb8b0a405832f66c16eb90c30335124 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -1,14 +1,14 @@ import { trpcServer } from "@hono/trpc-server"; -import { createContext } from "@labas/api/context"; -import { appRouter } from "@labas/api/routers/index"; -import { checkRateLimitAllowed } from "@labas/api/lib/rate-limit"; -import { auth } from "@labas/auth"; -import { env } from "@labas/env/server"; -import { withRequestId } from "@labas/api/logger"; +import { createContext } from "@pram/api/context"; +import { appRouter } from "@pram/api/routers/index"; +import { checkRateLimitAllowed } from "@pram/api/lib/rate-limit"; +import { auth } from "@pram/auth"; +import { env } from "@pram/env/server"; +import { withRequestId } from "@pram/api/logger"; import { Hono } from "hono"; import { cors } from "hono/cors"; -import "@labas/api/queue"; +import "@pram/api/queue"; const app = new Hono(); @@ -73,7 +73,7 @@ app.use( ); app.get("/sitemap.xml", (c) => { - const baseUrl = "https://labas.rogasper.com"; + const baseUrl = "https://pram.rogasper.com"; const urls = [ { loc: `${baseUrl}/`, changefreq: "weekly", priority: "1.0" }, ]; diff --git a/apps/server/tsconfig.json b/apps/server/tsconfig.json index ba3e7df6bd2fbbc221e4e2c80bb72f1d0267c6c8..b8b255db917557c5c57a7bdec4f0513ecf46af69 100644 --- a/apps/server/tsconfig.json +++ b/apps/server/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "@labas/config/tsconfig.base.json", + "extends": "@pram/config/tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "dist", diff --git a/apps/server/tsdown.config.ts b/apps/server/tsdown.config.ts index 8502d383208cfef85968325a0aece7aba55f02a9..2399646c9874fbc312153199da22dc8264429bab 100644 --- a/apps/server/tsdown.config.ts +++ b/apps/server/tsdown.config.ts @@ -5,5 +5,5 @@ export default defineConfig({ format: "esm", outDir: "./dist", clean: true, - noExternal: [/@labas\/.*/], + noExternal: [/@pram\/.*/], }); diff --git a/apps/web/components.json b/apps/web/components.json index 81dfaeed5df47952628a71e177f3adfaee248426..3a297551c42ebfe17d78c3a092baa2953cd52eed 100644 --- a/apps/web/components.json +++ b/apps/web/components.json @@ -13,8 +13,8 @@ "iconLibrary": "lucide", "aliases": { "components": "@/components", - "utils": "@labas/ui/lib/utils", - "ui": "@labas/ui/components", + "utils": "@pram/ui/lib/utils", + "ui": "@pram/ui/components", "lib": "@/lib", "hooks": "@/hooks" }, diff --git a/apps/web/index.html b/apps/web/index.html index 4c5178aac0067de51825a154d78f7e554001375a..a2c834a45efa55401a31d84ca068029bc47e197f 100644 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -5,29 +5,31 @@ - + - - + + - - + + - + - + - - + + - - Labas — AI-Powered Exam Practice Platform + + + + Pram — AI-Powered Exam Practice Platform diff --git a/apps/web/package.json b/apps/web/package.json index 5d14933b1be2b294f1e06f8f5b8342a7fcd5f138..810c7d2df8fec44441c425e7a70042a440775171 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -17,11 +17,11 @@ }, "dependencies": { "@hookform/resolvers": "^5.2.2", - "@labas/ai": "workspace:*", - "@labas/api": "workspace:*", - "@labas/auth": "workspace:*", - "@labas/env": "workspace:*", - "@labas/ui": "workspace:*", + "@pram/ai": "workspace:*", + "@pram/api": "workspace:*", + "@pram/auth": "workspace:*", + "@pram/env": "workspace:*", + "@pram/ui": "workspace:*", "@marsidev/react-turnstile": "^1.5.2", "@tailwindcss/vite": "^4.2.2", "@tanstack/react-form": "^1.28.0", @@ -45,7 +45,7 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", - "@labas/config": "workspace:*", + "@pram/config": "workspace:*", "@tanstack/react-query-devtools": "^5.91.1", "@tanstack/react-router-devtools": "^1.166.13", "@tanstack/router-plugin": "^1.167.22", diff --git a/apps/web/public/apple-touch-icon-180x180.png b/apps/web/public/apple-touch-icon-180x180.png new file mode 100644 index 0000000000000000000000000000000000000000..5be0184c9436208b470fb600780273d37de9f6c1 --- /dev/null +++ b/apps/web/public/apple-touch-icon-180x180.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7ebaddcdba5a02aa882e48d19c94f4517d6a74715f630b3f981a9e1ca3a5c873 +size 2464 diff --git a/apps/web/public/favicon.ico b/apps/web/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..5629bb07ff87ca6d1573ee2cdb03a5742442e167 --- /dev/null +++ b/apps/web/public/favicon.ico @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e394e2946280f200160275e9af3cd9a98f2b6d016f16b69d30037f524ff3bf15 +size 1125 diff --git a/apps/web/public/labas_icon.png b/apps/web/public/labas_icon.png deleted file mode 100644 index 4f4ca7d5db7b8f625dc002401aeac6354acf9783..0000000000000000000000000000000000000000 --- a/apps/web/public/labas_icon.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e39f5b556433db51ae13ac44126c0f37b1c51b566b711ba4b954d834e99d4362 -size 250035 diff --git a/apps/web/public/logo.png b/apps/web/public/logo.png index 4f4ca7d5db7b8f625dc002401aeac6354acf9783..d5dc572be4e2e5bf0c76d73a7d3b3a8fa48b3f73 100644 --- a/apps/web/public/logo.png +++ b/apps/web/public/logo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e39f5b556433db51ae13ac44126c0f37b1c51b566b711ba4b954d834e99d4362 -size 250035 +oid sha256:53415d59f2c001f6e68689937ec9cd57729379fb37e314d5503834749622e681 +size 429116 diff --git a/apps/web/public/maskable-icon-512x512.png b/apps/web/public/maskable-icon-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..3e47bd3b666b24fef0711ac26b750a28944c3c5f --- /dev/null +++ b/apps/web/public/maskable-icon-512x512.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:90812f0875abfe9f88f2318c9dfffcf776c1c590863ab463c7a763f6d9f4a289 +size 9032 diff --git a/apps/web/public/pram_icon.png b/apps/web/public/pram_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..d5dc572be4e2e5bf0c76d73a7d3b3a8fa48b3f73 --- /dev/null +++ b/apps/web/public/pram_icon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:53415d59f2c001f6e68689937ec9cd57729379fb37e314d5503834749622e681 +size 429116 diff --git a/apps/web/public/pwa-192x192.png b/apps/web/public/pwa-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..9e8ce9a81df5e925a5530d1bd60b7b5b2b51d8f4 --- /dev/null +++ b/apps/web/public/pwa-192x192.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:67e6da6d38061996b08cca1de901e2751b697af4fcf29053081b9b5d30d0c276 +size 4519 diff --git a/apps/web/public/pwa-512x512.png b/apps/web/public/pwa-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..971c4f912ef80aac2ba15fb059bb148ca4116e00 --- /dev/null +++ b/apps/web/public/pwa-512x512.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:99a12b83cd72a5c00aa0963cdbe8fa6ddfc8e30a58c422952f84b53c327240b1 +size 20584 diff --git a/apps/web/public/pwa-64x64.png b/apps/web/public/pwa-64x64.png new file mode 100644 index 0000000000000000000000000000000000000000..88445018896dbefa6bd1a81e39e9c5f5a084a86a --- /dev/null +++ b/apps/web/public/pwa-64x64.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b6086f8c10632c5829b96f4f02e2d7dd485d229c38522b11e03a4691e54fb87c +size 1061 diff --git a/apps/web/public/robots.txt b/apps/web/public/robots.txt index 2d00362f8f9dac26b1f2621fdaab86bb01dcd687..9fe3a43d1a40fa64aef621c2c218e081b7629a4e 100644 --- a/apps/web/public/robots.txt +++ b/apps/web/public/robots.txt @@ -1,3 +1,3 @@ -User-agent: * +User-agent: * Allow: / -Sitemap: https://labas.rogasper.com/sitemap.xml +Sitemap: https://pram.rogasper.com/sitemap.xml diff --git a/apps/web/public/sitemap.xml b/apps/web/public/sitemap.xml index 72f2c20cd7e17b621f6a290f53a500f919b8f732..fefcaba9713872bd70365f3960cccb8d5d3706a2 100644 --- a/apps/web/public/sitemap.xml +++ b/apps/web/public/sitemap.xml @@ -1,27 +1,27 @@ - + - https://labas.rogasper.com/ + https://pram.rogasper.com/ weekly 1.0 - https://labas.rogasper.com/login + https://pram.rogasper.com/login monthly 0.5 - https://labas.rogasper.com/bank + https://pram.rogasper.com/bank weekly 0.8 - https://labas.rogasper.com/packages + https://pram.rogasper.com/packages weekly 0.8 - https://labas.rogasper.com/leaderboard + https://pram.rogasper.com/leaderboard daily 0.6 diff --git a/apps/web/pwa-assets.config.ts b/apps/web/pwa-assets.config.ts index 4a1f2032e17baf7e32b236f8b0d0cdd6ce5d9daa..1c90e5f9bf040c0611b8c6ae36f48a30f754595a 100644 --- a/apps/web/pwa-assets.config.ts +++ b/apps/web/pwa-assets.config.ts @@ -5,5 +5,5 @@ export default defineConfig({ preset: "2023", }, preset, - images: ["public/labas_icon.png"], + images: ["public/pram_icon.png"], }); diff --git a/apps/web/src/components/CommunityModal.tsx b/apps/web/src/components/CommunityModal.tsx index 6ece22ed44fd9845d29f67e755c8ee9c810e6cdf..f692c98f990f334c1530fb27245fd1a89732cee0 100644 --- a/apps/web/src/components/CommunityModal.tsx +++ b/apps/web/src/components/CommunityModal.tsx @@ -1,4 +1,4 @@ -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@labas/ui/components/dialog"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@pram/ui/components/dialog"; import { MaterialIcon } from "@/components/ui/MaterialIcon"; import { CommunityChannelList } from "@/components/CommunityChannelList"; @@ -17,10 +17,10 @@ export function CommunityModal({ - Gabung Komunitas Labas + Gabung Komunitas Pram - Belajar bahasa, lapor bug, atau ikut ngembangin Labas — pilih channel yang paling pas. + Belajar bahasa, lapor bug, atau ikut ngembangin Pram — pilih channel yang paling pas. diff --git a/apps/web/src/components/DonationModal.tsx b/apps/web/src/components/DonationModal.tsx index 5b0fa2ea81b91068b0052704ad5e19a07f60abef..80dcd015a0c5ee934896c5ace991f81a5fb473ac 100644 --- a/apps/web/src/components/DonationModal.tsx +++ b/apps/web/src/components/DonationModal.tsx @@ -1,4 +1,4 @@ -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@labas/ui/components/dialog"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@pram/ui/components/dialog"; import { MaterialIcon } from "@/components/ui/MaterialIcon"; export function DonationModal({ @@ -18,12 +18,12 @@ export function DonationModal({ - Dukung Labas! + Dukung Pram! Selamat, kamu baru saja menyelesaikan {triggerAction === "exam" ? "latihan soal" : "generate soal AI"}! 🎉 - Jika Labas membantumu, dukung kami untuk menutupi biaya server & mengembangkan fitur baru dengan mentraktir kopi. ☕ + Jika Pram membantumu, dukung kami untuk menutupi biaya server & mengembangkan fitur baru dengan mentraktir kopi. ☕ diff --git a/apps/web/src/components/ErrorFallback.tsx b/apps/web/src/components/ErrorFallback.tsx index 8737cde0ddd05a3f7b54f4455893118ba6319678..cc21125e4fe69a27cc76d12ccaacbe73f9ae5d5b 100644 --- a/apps/web/src/components/ErrorFallback.tsx +++ b/apps/web/src/components/ErrorFallback.tsx @@ -1,4 +1,4 @@ -import { Button } from "@labas/ui/components/button"; +import { Button } from "@pram/ui/components/button"; export function ErrorFallback({ error, reset }: { error: Error; reset?: () => void }) { return ( diff --git a/apps/web/src/components/GettingStartedCard.tsx b/apps/web/src/components/GettingStartedCard.tsx index 750432e5fc5834e45ec21de31329ea67c3b8fb30..6337f2f7d8eb16a79d7308f81a0e5ef14bdc5a17 100644 --- a/apps/web/src/components/GettingStartedCard.tsx +++ b/apps/web/src/components/GettingStartedCard.tsx @@ -1,9 +1,9 @@ import { useEffect, useState } from "react"; import { Link } from "@tanstack/react-router"; -import { Card, CardContent } from "@labas/ui/components/card"; +import { Card, CardContent } from "@pram/ui/components/card"; import { MaterialIcon } from "@/components/ui/MaterialIcon"; -const LS_KEY = "labas-getting-started-dismissed"; +const LS_KEY = "pram-getting-started-dismissed"; const steps = [ { diff --git a/apps/web/src/components/NotFoundPage.tsx b/apps/web/src/components/NotFoundPage.tsx index 54b934914e190661c540b676b90f7aa547a4be92..be698b818e8da1d7ba632f91ca3e3cfff73357fd 100644 --- a/apps/web/src/components/NotFoundPage.tsx +++ b/apps/web/src/components/NotFoundPage.tsx @@ -1,5 +1,5 @@ import { Link } from "@tanstack/react-router"; -import { Button } from "@labas/ui/components/button"; +import { Button } from "@pram/ui/components/button"; export function NotFoundPage() { return ( diff --git a/apps/web/src/components/PageGuide.tsx b/apps/web/src/components/PageGuide.tsx index b113b914d1f3d68f6a4aafd36a498e4ebfbe27e8..ec08f2b6f056ebb9919da1166a0bfefff7f39936 100644 --- a/apps/web/src/components/PageGuide.tsx +++ b/apps/web/src/components/PageGuide.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { Card, CardContent } from "@labas/ui/components/card"; +import { Card } from "@pram/ui/components/card"; import { MaterialIcon } from "@/components/ui/MaterialIcon"; interface GuideItem { diff --git a/apps/web/src/components/TourGuide.tsx b/apps/web/src/components/TourGuide.tsx index d033248b8510a86e8ccc34bd2c7a008d7a013c67..8bb8b32f98f269a4dfb7db12c2ad906298d839ea 100644 --- a/apps/web/src/components/TourGuide.tsx +++ b/apps/web/src/components/TourGuide.tsx @@ -7,8 +7,7 @@ const PageTourImpl = lazy(() => import("./TourGuideImpl").then((m) => ({ default export type { Step }; -const LS_GLOBAL = "labas-tour-completed"; -const TRIGGER_GLOBAL = "labas-tour-trigger"; +const TRIGGER_GLOBAL = "pram-tour-trigger"; export function TourGuide() { return ( diff --git a/apps/web/src/components/TourGuideImpl.tsx b/apps/web/src/components/TourGuideImpl.tsx index b1e137eaa85a32d2ca925bcba80426621c6a82ad..807608e2a57789b754ca0b395a0669fa330009d1 100644 --- a/apps/web/src/components/TourGuideImpl.tsx +++ b/apps/web/src/components/TourGuideImpl.tsx @@ -1,6 +1,5 @@ import { useState, useEffect, type CSSProperties } from "react"; import { Joyride, type Step, type EventData } from "react-joyride"; -import { MaterialIcon } from "@/components/ui/MaterialIcon"; const joyrideOptions = { primaryColor: "var(--matcha-600)", @@ -24,11 +23,11 @@ export type { Step, EventData }; // ── Global site tour ── -const LS_GLOBAL = "labas-tour-completed"; -const TRIGGER_GLOBAL = "labas-tour-trigger"; +const LS_GLOBAL = "pram-tour-completed"; +const TRIGGER_GLOBAL = "pram-tour-trigger"; const globalSteps: Step[] = [ - { target: "body", placement: "center", title: "Selamat Datang di Labas!", content: "Platform latihan ujian bahasa berbasis AI. Yuk, kita lihat fitur-fitur utamanya!", hideOverlay: true }, + { target: "body", placement: "center", title: "Selamat Datang di Pram!", content: "Platform latihan ujian bahasa berbasis AI. Yuk, kita lihat fitur-fitur utamanya!", hideOverlay: true }, { target: "[data-tour='dashboard-stats']", title: "Ringkasan Aktivitas", content: "Pantau jumlah latihan, waktu belajar, soal terjawab, dan akurasi kamu di sini.", spotlightPadding: 8 }, { 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 }, { target: "[data-tour='nav-bank']", title: "Bank Soal — Buat Paket", content: "Atur soal-soal kamu jadi paket latihan dari bank soal.", spotlightPadding: 4 }, diff --git a/apps/web/src/components/TurnstileField.tsx b/apps/web/src/components/TurnstileField.tsx index 3e8249bb0e10a5077ee0210153cb6de7c43685e1..726e101993166229a5837a7041c8ed92e7de5bb3 100644 --- a/apps/web/src/components/TurnstileField.tsx +++ b/apps/web/src/components/TurnstileField.tsx @@ -1,6 +1,6 @@ import { Turnstile, type TurnstileInstance, type TurnstileProps } from "@marsidev/react-turnstile"; import { forwardRef } from "react"; -import { env } from "@labas/env/web"; +import { env } from "@pram/env/web"; export const TURNSTILE_SITE_KEY = env.VITE_CLOUDFLARE_TURNSTILE_SITE_KEY; diff --git a/apps/web/src/components/admin/Pagination.tsx b/apps/web/src/components/admin/Pagination.tsx index b6b404166af8496e711f3cf3ac1329a27e6bed11..9b25c1f21f0ec864ff9212fbc1eee68dd0906907 100644 --- a/apps/web/src/components/admin/Pagination.tsx +++ b/apps/web/src/components/admin/Pagination.tsx @@ -1,4 +1,4 @@ -import { Button } from "@labas/ui/components/button"; +import { Button } from "@pram/ui/components/button"; interface PaginationProps { page: number; diff --git a/apps/web/src/components/analytics/BreakdownCharts.tsx b/apps/web/src/components/analytics/BreakdownCharts.tsx index a29674b3a80220b9d3d2d672c8bec3fe0413cd84..b27fa10c5fef11a3a3fdc11af50bef6d50029769 100644 --- a/apps/web/src/components/analytics/BreakdownCharts.tsx +++ b/apps/web/src/components/analytics/BreakdownCharts.tsx @@ -8,10 +8,9 @@ import { ResponsiveContainer, Cell, } from "recharts"; -import { Card, CardContent, CardHeader, CardTitle } from "@labas/ui/components/card"; +import { Card, CardContent, CardHeader, CardTitle } from "@pram/ui/components/card"; import { formatLabel } from "@/lib/format"; import { formatTime } from "@/lib/time"; -import { MaterialIcon } from "@/components/ui/MaterialIcon"; export interface ExamTypeItem { examTypeId: string; diff --git a/apps/web/src/components/analytics/OverviewCards.tsx b/apps/web/src/components/analytics/OverviewCards.tsx index 51ec49451a8222eea9ef11ece2b7d922ef791e45..1ba758c0b4a9d3d8332968856a0e0c528b95b81e 100644 --- a/apps/web/src/components/analytics/OverviewCards.tsx +++ b/apps/web/src/components/analytics/OverviewCards.tsx @@ -1,5 +1,5 @@ import { MaterialIcon } from "@/components/ui/MaterialIcon"; -import { Card, CardContent } from "@labas/ui/components/card"; +import { Card, CardContent } from "@pram/ui/components/card"; import { formatTime } from "@/lib/time"; interface OverviewCardsProps { diff --git a/apps/web/src/components/analytics/ScoreTrendChart.tsx b/apps/web/src/components/analytics/ScoreTrendChart.tsx index 430a554feeda11321fc383605db0cf337bb16ebf..677d3a9bfa575e3c840c2772e2e4f71479884daa 100644 --- a/apps/web/src/components/analytics/ScoreTrendChart.tsx +++ b/apps/web/src/components/analytics/ScoreTrendChart.tsx @@ -7,7 +7,7 @@ import { Tooltip, ResponsiveContainer, } from "recharts"; -import { Card, CardContent, CardHeader, CardTitle } from "@labas/ui/components/card"; +import { Card, CardContent, CardHeader, CardTitle } from "@pram/ui/components/card"; interface TrendPoint { date: string; diff --git a/apps/web/src/components/analytics/TimeAnalyticsPanel.tsx b/apps/web/src/components/analytics/TimeAnalyticsPanel.tsx index 76bfb7d0d5325ab36c4352ea58492cfd041dadb7..44fcad201484afa93f6d1299b77e78af30033a58 100644 --- a/apps/web/src/components/analytics/TimeAnalyticsPanel.tsx +++ b/apps/web/src/components/analytics/TimeAnalyticsPanel.tsx @@ -7,7 +7,7 @@ import { Tooltip, ResponsiveContainer, } from "recharts"; -import { Card, CardContent, CardHeader, CardTitle } from "@labas/ui/components/card"; +import { Card, CardContent, CardHeader, CardTitle } from "@pram/ui/components/card"; import { formatTime } from "@/lib/time"; import { formatLabel } from "@/lib/format"; diff --git a/apps/web/src/components/analytics/WeaknessPanel.tsx b/apps/web/src/components/analytics/WeaknessPanel.tsx index 4ceb8ca76e2c22d9236e5bd3278a7388119ec85d..7193c5d8d8bd2784f7895762f4dc1fbc1110ddbd 100644 --- a/apps/web/src/components/analytics/WeaknessPanel.tsx +++ b/apps/web/src/components/analytics/WeaknessPanel.tsx @@ -1,4 +1,4 @@ -import { Card, CardContent, CardHeader, CardTitle } from "@labas/ui/components/card"; +import { Card, CardContent, CardHeader, CardTitle } from "@pram/ui/components/card"; import { MaterialIcon } from "@/components/ui/MaterialIcon"; import { formatLabel } from "@/lib/format"; @@ -57,7 +57,7 @@ export function WeaknessPanel({ weaknesses, recommendations }: WeaknessPanelProp {hasData ? ( <> - {weaknesses.map((w, i) => ( + {weaknesses.map((w, _i) => ( { - localStorage.setItem("labas-bank-private-callout-dismissed", "true"); + localStorage.setItem("pram-bank-private-callout-dismissed", "true"); setBulkMode((prev) => prev); }; diff --git a/apps/web/src/components/generate/GlobalGenerationProgress.tsx b/apps/web/src/components/generate/GlobalGenerationProgress.tsx index af391c4e55b31e2a1522629c9d1cfa77ad54762c..8f27747de0b060ee4f7a5f4a863f2852d201ea70 100644 --- a/apps/web/src/components/generate/GlobalGenerationProgress.tsx +++ b/apps/web/src/components/generate/GlobalGenerationProgress.tsx @@ -1,7 +1,6 @@ import { useState, useRef, useEffect } from "react"; import { useRouterState, Link } from "@tanstack/react-router"; import { useMutation } from "@tanstack/react-query"; -import { Button } from "@labas/ui/components/button"; import { MaterialIcon } from "@/components/ui/MaterialIcon"; import { useGenerationJobs, type ActiveJob } from "@/hooks/use-generation-jobs"; import { isTerminal } from "@/hooks/use-job-shared"; diff --git a/apps/web/src/components/generate/ResultSection.tsx b/apps/web/src/components/generate/ResultSection.tsx index b6271135f0ea9dfe2073dfafb29ab09134bd1835..8c4da1885c3c8935bf272e4de86efab277eb490e 100644 --- a/apps/web/src/components/generate/ResultSection.tsx +++ b/apps/web/src/components/generate/ResultSection.tsx @@ -1,8 +1,8 @@ import { Link } from "@tanstack/react-router"; -import { Button } from "@labas/ui/components/button"; -import { Card, CardContent, CardHeader } from "@labas/ui/components/card"; +import { Button } from "@pram/ui/components/button"; +import { Card, CardContent, CardHeader } from "@pram/ui/components/card"; import { MaterialIcon } from "@/components/ui/MaterialIcon"; -import type { GenerationResult } from "@labas/ai"; +import type { GenerationResult } from "@pram/ai"; interface ResultSectionProps { result: GenerationResult; diff --git a/apps/web/src/components/generate/TestBlueprintCard.tsx b/apps/web/src/components/generate/TestBlueprintCard.tsx index 7cf44ca6b78ee941092312f34fdfab1a7bc70ee6..b7d87197c9d6fdcb01dec4bcff7f7fe074860a1e 100644 --- a/apps/web/src/components/generate/TestBlueprintCard.tsx +++ b/apps/web/src/components/generate/TestBlueprintCard.tsx @@ -1,5 +1,5 @@ -import { Button } from "@labas/ui/components/button"; -import { Card, CardContent, CardHeader, CardTitle } from "@labas/ui/components/card"; +import { Button } from "@pram/ui/components/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@pram/ui/components/card"; import { MaterialIcon } from "@/components/ui/MaterialIcon"; import { EXAM_TYPES, SECTIONS } from "@/lib/generate-constants"; diff --git a/apps/web/src/components/header.tsx b/apps/web/src/components/header.tsx index 66b42bc3919b32643b75b3aab8316a561a007a0f..be38daddd65a17dadb779f28ee413a117127e3d5 100644 --- a/apps/web/src/components/header.tsx +++ b/apps/web/src/components/header.tsx @@ -1,4 +1,3 @@ -import { Link } from "@tanstack/react-router"; import { ModeToggle } from "./mode-toggle"; import UserMenu from "./user-menu"; @@ -6,7 +5,7 @@ export default function Header() { return ( - Labas + Pram diff --git a/apps/web/src/components/leaderboard/MyRankCard.tsx b/apps/web/src/components/leaderboard/MyRankCard.tsx index 223177c5d7aeda6d7a70454f3cc8d3db17806243..251981db84ed3e6f9f62b24fa7f2c1c7bc74c1ef 100644 --- a/apps/web/src/components/leaderboard/MyRankCard.tsx +++ b/apps/web/src/components/leaderboard/MyRankCard.tsx @@ -1,4 +1,4 @@ -import { Card, CardContent } from "@labas/ui/components/card"; +import { Card, CardContent } from "@pram/ui/components/card"; import { MaterialIcon } from "@/components/ui/MaterialIcon"; interface MyRankData { diff --git a/apps/web/src/components/leaderboard/RankingPodium.tsx b/apps/web/src/components/leaderboard/RankingPodium.tsx index 36d899180dba6926d7af82a61990191b3032131f..f5b440104261203ab0e168906b764ab4c80f7c14 100644 --- a/apps/web/src/components/leaderboard/RankingPodium.tsx +++ b/apps/web/src/components/leaderboard/RankingPodium.tsx @@ -1,5 +1,4 @@ -import { Card, CardContent } from "@labas/ui/components/card"; -import { MaterialIcon } from "@/components/ui/MaterialIcon"; +import { Card, CardContent } from "@pram/ui/components/card"; import { Avatar } from "@/components/ui/Avatar"; interface PodiumUser { diff --git a/apps/web/src/components/leaderboard/RankingRow.tsx b/apps/web/src/components/leaderboard/RankingRow.tsx index 41821bada5c36d6c711aaffd5c2fb8a20e264d95..2d0695b2ed81651801c8a9f0341e7f1436ca834d 100644 --- a/apps/web/src/components/leaderboard/RankingRow.tsx +++ b/apps/web/src/components/leaderboard/RankingRow.tsx @@ -1,5 +1,4 @@ -import { Card, CardContent } from "@labas/ui/components/card"; -import { MaterialIcon } from "@/components/ui/MaterialIcon"; +import { Card, CardContent } from "@pram/ui/components/card"; import { Avatar } from "@/components/ui/Avatar"; interface RankingEntry { diff --git a/apps/web/src/components/mode-toggle.tsx b/apps/web/src/components/mode-toggle.tsx index 9ae5e2a2103fd0d26402bacb68f05c78297886df..43126b16e39faf0d1735fb0080aa9dfff5e7b59c 100644 --- a/apps/web/src/components/mode-toggle.tsx +++ b/apps/web/src/components/mode-toggle.tsx @@ -1,10 +1,10 @@ -import { Button } from "@labas/ui/components/button"; +import { Button } from "@pram/ui/components/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, -} from "@labas/ui/components/dropdown-menu"; +} from "@pram/ui/components/dropdown-menu"; import { Moon, Sun } from "lucide-react"; import { useTheme } from "@/components/theme-provider"; diff --git a/apps/web/src/components/package/EditPackageModal.tsx b/apps/web/src/components/package/EditPackageModal.tsx index 670d26d978cd319f4abe501adeca35997fa4b9d3..ee088a2dfbdfc4f45d3c79377190c1b85427f4d7 100644 --- a/apps/web/src/components/package/EditPackageModal.tsx +++ b/apps/web/src/components/package/EditPackageModal.tsx @@ -1,12 +1,12 @@ import { useState } from "react"; -import { Input } from "@labas/ui/components/input"; -import { Button } from "@labas/ui/components/button"; +import { Input } from "@pram/ui/components/input"; +import { Button } from "@pram/ui/components/button"; import { Dialog, DialogContent, DialogHeader, DialogTitle, -} from "@labas/ui/components/dialog"; +} from "@pram/ui/components/dialog"; export function EditPackageModal({ initialTitle, diff --git a/apps/web/src/components/packages/PackageCard.tsx b/apps/web/src/components/packages/PackageCard.tsx index 52831e1d35fdc9f20d110a45cd513920c0fab6af..fe55d0c676dd071155e436b7597ed956396a9810 100644 --- a/apps/web/src/components/packages/PackageCard.tsx +++ b/apps/web/src/components/packages/PackageCard.tsx @@ -1,6 +1,6 @@ import { Link, useNavigate } from "@tanstack/react-router"; -import { Button } from "@labas/ui/components/button"; -import { Card, CardContent } from "@labas/ui/components/card"; +import { Button } from "@pram/ui/components/button"; +import { Card, CardContent } from "@pram/ui/components/card"; import { MaterialIcon } from "@/components/ui/MaterialIcon"; import { toast } from "sonner"; diff --git a/apps/web/src/components/routes/BankPage.tsx b/apps/web/src/components/routes/BankPage.tsx index 883c541a2bf2d16118eb3425ee8c396b301d97c7..dfd56be385e6dfd2cd173b3efbdd28cf11804d39 100644 --- a/apps/web/src/components/routes/BankPage.tsx +++ b/apps/web/src/components/routes/BankPage.tsx @@ -23,7 +23,7 @@ import type { Step } from "react-joyride"; import { createFileRoute, redirect } from "@tanstack/react-router"; import z from "zod"; -const FILTER_ADVANCED_KEY = "labas-bank-filter-advanced"; +const FILTER_ADVANCED_KEY = "pram-bank-filter-advanced"; export const Route = createFileRoute("/bank")({ validateSearch: z.object({ @@ -588,7 +588,7 @@ export function BankComponent() { } // ── Bank page tour ── -const BANK_TOUR_KEY = "labas-page-tour-bank"; +const BANK_TOUR_KEY = "pram-page-tour-bank"; const bankPageSteps: Step[] = [ { target: "[data-tour='bank-filters']", diff --git a/apps/web/src/components/routes/GeneratePage.tsx b/apps/web/src/components/routes/GeneratePage.tsx index 83fd2441515b4afba63cc723e2d08abc93199b98..9d87be2219d05f1a9627e7d0edd054697ebadd9a 100644 --- a/apps/web/src/components/routes/GeneratePage.tsx +++ b/apps/web/src/components/routes/GeneratePage.tsx @@ -1,19 +1,18 @@ import { useState, useEffect, useRef } from "react"; import { useMutation, useQuery } from "@tanstack/react-query"; import { Link } from "@tanstack/react-router"; -import { authClient } from "@/lib/auth-client"; import { trpc } from "@/utils/trpc"; import { useApiKeys } from "@/hooks/use-api-key"; -import { useGenerationJobs, type CompletedResult } from "@/hooks/use-generation-jobs"; +import { useGenerationJobs } from "@/hooks/use-generation-jobs"; import { trackUmamiEvent, AnalyticsEvent } from "@/lib/umami"; -import { Button } from "@labas/ui/components/button"; +import { Button } from "@pram/ui/components/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, -} from "@labas/ui/components/select"; +} from "@pram/ui/components/select"; import { MaterialIcon } from "@/components/ui/MaterialIcon"; import { TestBlueprintCard } from "@/components/generate/TestBlueprintCard"; import { ResultSection } from "@/components/generate/ResultSection"; @@ -58,7 +57,7 @@ export function RouteComponent() { const { activeCount, completedResults, - isGenerating, + isGenerating: _isGenerating, error, addJob, resetAll, @@ -764,7 +763,7 @@ export function RouteComponent() { } // ── Generate page tour ── -const GENERATE_TOUR_KEY = "labas-page-tour-generate"; +const GENERATE_TOUR_KEY = "pram-page-tour-generate"; const generatePageSteps: Step[] = [ { target: "[data-tour='generate-exam-type']", diff --git a/apps/web/src/components/routes/HistoryPage.tsx b/apps/web/src/components/routes/HistoryPage.tsx index 320adeeb35913c198d0e41b7f475b31d2324aa40..0e7402e9bf1cba39972cdccb66144e8f4cefb916 100644 --- a/apps/web/src/components/routes/HistoryPage.tsx +++ b/apps/web/src/components/routes/HistoryPage.tsx @@ -5,9 +5,9 @@ import { z } from "zod"; import { authClient } from "@/lib/auth-client"; import { trpc } from "@/utils/trpc"; import { trackUmamiEvent, AnalyticsEvent } from "@/lib/umami"; -import { Button } from "@labas/ui/components/button"; -import { Card, CardContent } from "@labas/ui/components/card"; -import { Input } from "@labas/ui/components/input"; +import { Button } from "@pram/ui/components/button"; +import { Card, CardContent } from "@pram/ui/components/card"; +import { Input } from "@pram/ui/components/input"; import { Select, SelectContent, @@ -15,7 +15,7 @@ import { SelectItem, SelectTrigger, SelectValue, -} from "@labas/ui/components/select"; +} from "@pram/ui/components/select"; import { MaterialIcon } from "@/components/ui/MaterialIcon"; import { EXAM_TYPES } from "@/lib/exam-constants"; diff --git a/apps/web/src/components/routes/LandingPage.tsx b/apps/web/src/components/routes/LandingPage.tsx index 0d3b6391bff83486024f5521eaea467667ac237c..e1b6f206af4329d13f9a9f773c5dd19adbbe1130 100644 --- a/apps/web/src/components/routes/LandingPage.tsx +++ b/apps/web/src/components/routes/LandingPage.tsx @@ -1,6 +1,6 @@ import { Link } from "@tanstack/react-router"; -import { Button } from "@labas/ui/components/button"; -import { Card, CardContent } from "@labas/ui/components/card"; +import { Button } from "@pram/ui/components/button"; +import { Card, CardContent } from "@pram/ui/components/card"; import { MaterialIcon } from "@/components/ui/MaterialIcon"; import { useState } from "react"; import { CommunityModal } from "@/components/CommunityModal"; @@ -46,8 +46,8 @@ export function LandingPage() { {/* Navbar */} - - Labas + + Pram Support - + @@ -106,7 +106,7 @@ export function LandingPage() { Mulai Latihan Sekarang - + @@ -122,7 +122,7 @@ export function LandingPage() { {/* Variatif Floating Elements */} @@ -323,7 +323,7 @@ export function LandingPage() { - Fitur Unggulan Labas + Fitur Unggulan Pram Desain yang elegan, namun kokoh untuk mempercepat kesiapan Anda menghadapi ujian. @@ -434,15 +434,15 @@ export function LandingPage() { Pertanyaan Umum - Temukan jawaban cepat untuk pertanyaan seputar Labas. + Temukan jawaban cepat untuk pertanyaan seputar Pram. @@ -496,7 +496,7 @@ export function LandingPage() { @rogasper - + GitHub @@ -523,7 +523,7 @@ export function LandingPage() { - © {new Date().getFullYear()} Labas. Didesain dengan penuh kehangatan. + © {new Date().getFullYear()} Pram. Didesain dengan penuh kehangatan. diff --git a/apps/web/src/components/routes/PackagesPage.tsx b/apps/web/src/components/routes/PackagesPage.tsx index 2ddd8207855ce517d8e7bf218a820b0af2da732c..39b16983d7d91ebc87c3f9c265c8bae836835273 100644 --- a/apps/web/src/components/routes/PackagesPage.tsx +++ b/apps/web/src/components/routes/PackagesPage.tsx @@ -1,17 +1,17 @@ -import { useState, useEffect, useCallback } from "react"; +import { useState, useEffect } from "react"; import { useQuery, useMutation } from "@tanstack/react-query"; import { createFileRoute, redirect, Link } from "@tanstack/react-router"; import { z } from "zod"; import { authClient } from "@/lib/auth-client"; import { trpc, queryClient } from "@/utils/trpc"; -import { Input } from "@labas/ui/components/input"; -import { Button } from "@labas/ui/components/button"; -import { Card } from "@labas/ui/components/card"; +import { Input } from "@pram/ui/components/input"; +import { Button } from "@pram/ui/components/button"; +import { Card } from "@pram/ui/components/card"; import { Tabs, TabsList, TabsTrigger, -} from "@labas/ui/components/tabs"; +} from "@pram/ui/components/tabs"; import { Select, SelectContent, @@ -19,7 +19,7 @@ import { SelectItem, SelectTrigger, SelectValue, -} from "@labas/ui/components/select"; +} from "@pram/ui/components/select"; import { MaterialIcon } from "@/components/ui/MaterialIcon"; import { GettingStartedCard } from "@/components/GettingStartedCard"; import { CalloutCard } from "@/components/bank/CalloutCard"; @@ -166,11 +166,11 @@ export function PackagesComponent() { // ── Private callout state ── const [calloutDismissed, setCalloutDismissed] = useState( - typeof window !== "undefined" && localStorage.getItem("labas-packages-private-callout-dismissed") === "true", + typeof window !== "undefined" && localStorage.getItem("pram-packages-private-callout-dismissed") === "true", ); const handleDismissCallout = () => { - localStorage.setItem("labas-packages-private-callout-dismissed", "true"); + localStorage.setItem("pram-packages-private-callout-dismissed", "true"); setCalloutDismissed(true); }; @@ -438,7 +438,7 @@ export function PackagesComponent() { } // ── Packages page tour ── -const PACKAGES_TOUR_KEY = "labas-page-tour-packages"; +const PACKAGES_TOUR_KEY = "pram-page-tour-packages"; const packagesPageSteps: Step[] = [ { target: "[data-tour='packages-header']", diff --git a/apps/web/src/components/routes/SettingsPage.tsx b/apps/web/src/components/routes/SettingsPage.tsx index 53c72b1480aabe07855c4a9263ed86e115f4beef..2d77bb28b54bc104da650e5f3926f450050ce2d7 100644 --- a/apps/web/src/components/routes/SettingsPage.tsx +++ b/apps/web/src/components/routes/SettingsPage.tsx @@ -9,8 +9,8 @@ import { TabsList, TabsTrigger, TabsContent, -} from "@labas/ui/components/tabs"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@labas/ui/components/card"; +} from "@pram/ui/components/tabs"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@pram/ui/components/card"; import { MaterialIcon } from "@/components/ui/MaterialIcon"; import { TokenUsageChart } from "@/components/settings/TokenUsageChart"; import { ApiKeyList } from "@/components/settings/ApiKeyList"; diff --git a/apps/web/src/components/settings/AccountSettings.tsx b/apps/web/src/components/settings/AccountSettings.tsx index 57f767e9326bd08e5221f95129c0c89fdad9d4bc..a0c52bb6052939005e91a4f8fe5bff0d2f5e3c43 100644 --- a/apps/web/src/components/settings/AccountSettings.tsx +++ b/apps/web/src/components/settings/AccountSettings.tsx @@ -1,8 +1,8 @@ import { useState } from "react"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@labas/ui/components/card"; -import { Input } from "@labas/ui/components/input"; -import { Label } from "@labas/ui/components/label"; -import { Button } from "@labas/ui/components/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@pram/ui/components/card"; +import { Input } from "@pram/ui/components/input"; +import { Label } from "@pram/ui/components/label"; +import { Button } from "@pram/ui/components/button"; import { toast } from "sonner"; import { getErrorMessage } from "@/lib/error-utils"; import { authClient } from "@/lib/auth-client"; diff --git a/apps/web/src/components/settings/ApiKeyForm.tsx b/apps/web/src/components/settings/ApiKeyForm.tsx index 4d5fca8a6a85c0c39dea2c86d17dfc9782259fdc..629191e84dd1f8c523f6c6afd59fcf5c33dd33e0 100644 --- a/apps/web/src/components/settings/ApiKeyForm.tsx +++ b/apps/web/src/components/settings/ApiKeyForm.tsx @@ -1,8 +1,8 @@ import { type ApiKeyConfig } from "@/hooks/use-api-key"; -import { Button } from "@labas/ui/components/button"; -import { Input } from "@labas/ui/components/input"; -import { Label } from "@labas/ui/components/label"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@labas/ui/components/card"; +import { Button } from "@pram/ui/components/button"; +import { Input } from "@pram/ui/components/input"; +import { Label } from "@pram/ui/components/label"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@pram/ui/components/card"; import { Select, SelectContent, @@ -10,7 +10,7 @@ import { SelectItem, SelectTrigger, SelectValue, -} from "@labas/ui/components/select"; +} from "@pram/ui/components/select"; type FormState = Omit & { apiKey: string }; diff --git a/apps/web/src/components/settings/ApiKeyList.tsx b/apps/web/src/components/settings/ApiKeyList.tsx index 00eb0d3730b2c7ebdb29b36dd2d65e76ad16f0be..dbe384352a3d43eb0b72734fa19bef1204aad7d1 100644 --- a/apps/web/src/components/settings/ApiKeyList.tsx +++ b/apps/web/src/components/settings/ApiKeyList.tsx @@ -1,6 +1,6 @@ import { type ApiKeyConfig } from "@/hooks/use-api-key"; -import { Button } from "@labas/ui/components/button"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@labas/ui/components/card"; +import { Button } from "@pram/ui/components/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@pram/ui/components/card"; import { MaterialIcon } from "@/components/ui/MaterialIcon"; interface ProviderOption { diff --git a/apps/web/src/components/settings/CommunityCard.tsx b/apps/web/src/components/settings/CommunityCard.tsx index 3afcd5df6f6580924e9f3c4b87e653618404f33d..ed1fefff50d97fe6a28da72437e744f448d91eef 100644 --- a/apps/web/src/components/settings/CommunityCard.tsx +++ b/apps/web/src/components/settings/CommunityCard.tsx @@ -1,4 +1,4 @@ -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@labas/ui/components/card"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@pram/ui/components/card"; import { CommunityChannelList } from "@/components/CommunityChannelList"; export function CommunityCard() { diff --git a/apps/web/src/components/settings/SecurityInfo.tsx b/apps/web/src/components/settings/SecurityInfo.tsx index f04a4fe25f301626698f9967103525f11871d426..91b357999a037f9aa3afb2c5a90f72bc6d52ed61 100644 --- a/apps/web/src/components/settings/SecurityInfo.tsx +++ b/apps/web/src/components/settings/SecurityInfo.tsx @@ -1,4 +1,4 @@ -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@labas/ui/components/card"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@pram/ui/components/card"; import { MaterialIcon } from "@/components/ui/MaterialIcon"; export function SecurityInfo() { diff --git a/apps/web/src/components/settings/TipsCard.tsx b/apps/web/src/components/settings/TipsCard.tsx index 3b7f752d073af4025b95be4ee735d8c66db60f11..e82b4ff4cee326464309cbc6742f8d4331de7fbb 100644 --- a/apps/web/src/components/settings/TipsCard.tsx +++ b/apps/web/src/components/settings/TipsCard.tsx @@ -1,4 +1,4 @@ -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@labas/ui/components/card"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@pram/ui/components/card"; import { MaterialIcon } from "@/components/ui/MaterialIcon"; export function TipsCard() { diff --git a/apps/web/src/components/settings/TokenUsageChart.tsx b/apps/web/src/components/settings/TokenUsageChart.tsx index f1b0fd3da7cf61a6966f3b46f95ff184e7241af1..20a0b97d1195669c90fd8af70131e65b11c8d8d8 100644 --- a/apps/web/src/components/settings/TokenUsageChart.tsx +++ b/apps/web/src/components/settings/TokenUsageChart.tsx @@ -11,12 +11,12 @@ import { } from "recharts"; import { useQuery } from "@tanstack/react-query"; import { trpc } from "@/utils/trpc"; -import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@labas/ui/components/card"; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@pram/ui/components/card"; import { Tabs, TabsList, TabsTrigger, -} from "@labas/ui/components/tabs"; +} from "@pram/ui/components/tabs"; import { MaterialIcon } from "@/components/ui/MaterialIcon"; type Period = "daily" | "weekly" | "monthly"; diff --git a/apps/web/src/components/sidebar.tsx b/apps/web/src/components/sidebar.tsx index 0efcb3f45fcb2a93a92ed0a3e2f6d3d371a64ee0..f16f2adb2478e7e6d58218a55ab171af75354833 100644 --- a/apps/web/src/components/sidebar.tsx +++ b/apps/web/src/components/sidebar.tsx @@ -114,14 +114,9 @@ export function Sidebar() { }`} > - + + pram + {!collapsed && ( Exam Prep Portal diff --git a/apps/web/src/components/sign-in-form.tsx b/apps/web/src/components/sign-in-form.tsx index 50458962b6fe7230a612b94852c07a7f162eff84..3dd11adf6bb54e84dfc8cdca57a4f82ff033765a 100644 --- a/apps/web/src/components/sign-in-form.tsx +++ b/apps/web/src/components/sign-in-form.tsx @@ -1,7 +1,7 @@ -import { Button } from "@labas/ui/components/button"; -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@labas/ui/components/card"; -import { Input } from "@labas/ui/components/input"; -import { Label } from "@labas/ui/components/label"; +import { Button } from "@pram/ui/components/button"; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@pram/ui/components/card"; +import { Input } from "@pram/ui/components/input"; +import { Label } from "@pram/ui/components/label"; import { useForm } from "@tanstack/react-form"; import { Link, useNavigate } from "@tanstack/react-router"; import { toast } from "sonner"; diff --git a/apps/web/src/components/sign-up-form.tsx b/apps/web/src/components/sign-up-form.tsx index daa14994adb4df7f1875f402f5fcae16c447b738..a7e0422467b723e9bd6b706ebc4b8d005055b623 100644 --- a/apps/web/src/components/sign-up-form.tsx +++ b/apps/web/src/components/sign-up-form.tsx @@ -1,7 +1,7 @@ -import { Button } from "@labas/ui/components/button"; -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@labas/ui/components/card"; -import { Input } from "@labas/ui/components/input"; -import { Label } from "@labas/ui/components/label"; +import { Button } from "@pram/ui/components/button"; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@pram/ui/components/card"; +import { Input } from "@pram/ui/components/input"; +import { Label } from "@pram/ui/components/label"; import { useForm } from "@tanstack/react-form"; import { useMutation } from "@tanstack/react-query"; import { useNavigate } from "@tanstack/react-router"; diff --git a/apps/web/src/components/test/AccentKeyboard.tsx b/apps/web/src/components/test/AccentKeyboard.tsx index 642c8598c3d0a5e04f94482327680ec9e78c9343..a4a79da225060b0944cbc85456ebc886a584bd65 100644 --- a/apps/web/src/components/test/AccentKeyboard.tsx +++ b/apps/web/src/components/test/AccentKeyboard.tsx @@ -1,4 +1,4 @@ -import { useCallback } from "react"; +import { useMemo } from "react"; const ACCENT_CHARS: Record = { DELE: ["á", "é", "í", "ó", "ú", "ñ", "¿", "¡"], diff --git a/apps/web/src/components/test/AttemptTestView.tsx b/apps/web/src/components/test/AttemptTestView.tsx index 96ee2394fc4abcd48b7581a60552e9b79c08896d..9d16e2bd73e9f5cc4b9845919628ed71fa2d1548 100644 --- a/apps/web/src/components/test/AttemptTestView.tsx +++ b/apps/web/src/components/test/AttemptTestView.tsx @@ -1,8 +1,7 @@ import { useState, useEffect, useRef, useCallback, memo } from "react"; import { useQuery } from "@tanstack/react-query"; -import { Button } from "@labas/ui/components/button"; +import { Button } from "@pram/ui/components/button"; import { MaterialIcon } from "@/components/ui/MaterialIcon"; -import { formatTime } from "@/lib/time"; import { trpc } from "@/utils/trpc"; import { QuestionInput } from "./QuestionInput"; import { AccentKeyboard } from "./AccentKeyboard"; diff --git a/apps/web/src/components/test/QuestionInput.tsx b/apps/web/src/components/test/QuestionInput.tsx index 83a70eaac98e99252da85d8dedb7c960f8458e74..93e5d318eb07b3773fac83bec2adb2b98577a560 100644 --- a/apps/web/src/components/test/QuestionInput.tsx +++ b/apps/web/src/components/test/QuestionInput.tsx @@ -1,5 +1,5 @@ -import { useState, useEffect, useRef, useCallback, useMemo } from "react"; -import { Input } from "@labas/ui/components/input"; +import { useState, useEffect, useRef, useCallback } from "react"; +import { Input } from "@pram/ui/components/input"; import { MCQ_FORMATS } from "@/lib/question-formats"; import type { Question } from "@/lib/types"; diff --git a/apps/web/src/components/test/attempt/AbandonDialog.tsx b/apps/web/src/components/test/attempt/AbandonDialog.tsx index f3f35afd40e69f6f403cfec45ece1bc9ca57089f..fc2b9d056c1f3539162bc0b40cf8bf73f8d7b93b 100644 --- a/apps/web/src/components/test/attempt/AbandonDialog.tsx +++ b/apps/web/src/components/test/attempt/AbandonDialog.tsx @@ -1,11 +1,11 @@ -import { Button } from "@labas/ui/components/button"; +import { Button } from "@pram/ui/components/button"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, -} from "@labas/ui/components/dialog"; +} from "@pram/ui/components/dialog"; import { MaterialIcon } from "@/components/ui/MaterialIcon"; interface AbandonDialogProps { diff --git a/apps/web/src/components/test/attempt/AttemptHeader.tsx b/apps/web/src/components/test/attempt/AttemptHeader.tsx index 6ad97fa7bb64767db2cf021076b80bd76bb3bb41..8d2e7d354b5e04b04967a89aaed57da5829556b6 100644 --- a/apps/web/src/components/test/attempt/AttemptHeader.tsx +++ b/apps/web/src/components/test/attempt/AttemptHeader.tsx @@ -1,4 +1,4 @@ -import { Button } from "@labas/ui/components/button"; +import { Button } from "@pram/ui/components/button"; import { MaterialIcon } from "@/components/ui/MaterialIcon"; import { formatTime } from "@/lib/time"; diff --git a/apps/web/src/components/test/attempt/FinishDialog.tsx b/apps/web/src/components/test/attempt/FinishDialog.tsx index 5a444879a7b39a5254754b2e7c8e4d1650b5d030..326bd9481f4c13339fd7031c3d6c184ea8af8415 100644 --- a/apps/web/src/components/test/attempt/FinishDialog.tsx +++ b/apps/web/src/components/test/attempt/FinishDialog.tsx @@ -1,11 +1,11 @@ -import { Button } from "@labas/ui/components/button"; +import { Button } from "@pram/ui/components/button"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, -} from "@labas/ui/components/dialog"; +} from "@pram/ui/components/dialog"; import { MaterialIcon } from "@/components/ui/MaterialIcon"; interface FinishDialogProps { diff --git a/apps/web/src/components/ui/Avatar.tsx b/apps/web/src/components/ui/Avatar.tsx index ed8f209c8fba664aaee63adc375c68787cbdb5e8..419902f69b18515b8fad42e2d35c3c0e0aec2005 100644 --- a/apps/web/src/components/ui/Avatar.tsx +++ b/apps/web/src/components/ui/Avatar.tsx @@ -1,4 +1,4 @@ -import { cn } from "@labas/ui/lib/utils"; +import { cn } from "@pram/ui/lib/utils"; const sizeClasses = { xs: { container: "h-6 w-6", text: "text-xs" }, diff --git a/apps/web/src/components/ui/PasswordInput.tsx b/apps/web/src/components/ui/PasswordInput.tsx index 2584d64efd2803718951a7f6ace99b0e7d4c468f..9d2c9016a739e68f5d87ea61867ee47b79eeb0f4 100644 --- a/apps/web/src/components/ui/PasswordInput.tsx +++ b/apps/web/src/components/ui/PasswordInput.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; -import { Input } from "@labas/ui/components/input"; -import { Button } from "@labas/ui/components/button"; +import { Input } from "@pram/ui/components/input"; +import { Button } from "@pram/ui/components/button"; import { MaterialIcon } from "@/components/ui/MaterialIcon"; type PasswordInputProps = { diff --git a/apps/web/src/components/user-menu.tsx b/apps/web/src/components/user-menu.tsx index e3ca0a650875a5c7be6ff69616e015205eb03d76..998ce805d867d1ebd1682185c1563fec6fc99f56 100644 --- a/apps/web/src/components/user-menu.tsx +++ b/apps/web/src/components/user-menu.tsx @@ -1,4 +1,4 @@ -import { Button } from "@labas/ui/components/button"; +import { Button } from "@pram/ui/components/button"; import { DropdownMenu, DropdownMenuContent, @@ -7,8 +7,8 @@ import { DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, -} from "@labas/ui/components/dropdown-menu"; -import { Skeleton } from "@labas/ui/components/skeleton"; +} from "@pram/ui/components/dropdown-menu"; +import { Skeleton } from "@pram/ui/components/skeleton"; import { Link, useNavigate } from "@tanstack/react-router"; import { authClient } from "@/lib/auth-client"; diff --git a/apps/web/src/hooks/use-api-key.ts b/apps/web/src/hooks/use-api-key.ts index bf210f35e06bf961a92ee3a02ff962c7e3629174..0a67b2005189cd60035078b4f6023000434621ac 100644 --- a/apps/web/src/hooks/use-api-key.ts +++ b/apps/web/src/hooks/use-api-key.ts @@ -2,7 +2,7 @@ import { useState, useEffect, useCallback } from "react"; import { encryptText, decryptText } from "@/lib/crypto"; import { trackUmamiEvent, AnalyticsEvent } from "@/lib/umami"; -const STORAGE_KEY = "labas_api_keys_v2"; +const STORAGE_KEY = "pram_api_keys_v2"; export interface ApiKeyConfig { id: string; @@ -102,7 +102,7 @@ export function useApiKeys() { // Backward-compatible hook for places that only need the first (default) key export function useApiKey() { - const { configs, isLoading, addConfig, updateConfig, removeConfig } = + const { configs, isLoading, addConfig, removeConfig } = useApiKeys(); const storedKey = configs[0] ?? null; diff --git a/apps/web/src/hooks/use-generation-jobs.ts b/apps/web/src/hooks/use-generation-jobs.ts index f721a61d517f9f46f1bbe9b329f7d7b2ed8912f6..52312773d3d61fb5e73cc139d9eb6736595a47fc 100644 --- a/apps/web/src/hooks/use-generation-jobs.ts +++ b/apps/web/src/hooks/use-generation-jobs.ts @@ -5,7 +5,7 @@ import { authClient } from "@/lib/auth-client"; import { useJobState } from "./use-job-state"; import { usePollingTransport } from "./use-polling-transport"; import { MAX_PARALLEL, ACTIVE_STATUSES, isTerminal } from "./use-job-shared"; -import type { GenerationResult } from "@labas/ai"; +import type { GenerationResult } from "@pram/ai"; export type { ActiveJob, CompletedResult } from "./use-job-shared"; diff --git a/apps/web/src/hooks/use-job-shared.ts b/apps/web/src/hooks/use-job-shared.ts index 81fbb11149b1b2efccfea205a716687019f55358..2b61df48d32354091843aa3908017b573d10e6e2 100644 --- a/apps/web/src/hooks/use-job-shared.ts +++ b/apps/web/src/hooks/use-job-shared.ts @@ -1,8 +1,8 @@ /** Shared types and constants for job management hooks. */ -export const STORAGE_KEY = "labas_active_jobs"; -export const RESULTS_KEY = "labas_completed_results"; -export const CLEARED_JOBS_KEY = "labas_cleared_jobs"; +export const STORAGE_KEY = "pram_active_jobs"; +export const RESULTS_KEY = "pram_completed_results"; +export const CLEARED_JOBS_KEY = "pram_cleared_jobs"; export const MAX_PARALLEL = 3; export interface ActiveJob { @@ -21,7 +21,7 @@ export interface ActiveJob { export interface CompletedResult { jobId: string; - result: import("@labas/ai").GenerationResult; + result: import("@pram/ai").GenerationResult; generatedPackageId: string | null; examType?: string; section?: string; diff --git a/apps/web/src/hooks/use-job-state.ts b/apps/web/src/hooks/use-job-state.ts index fcb8563f40794950576ea63ecda02027d6e82495..92cec40b4695f63846b7ccf94dda81e942343d83 100644 --- a/apps/web/src/hooks/use-job-state.ts +++ b/apps/web/src/hooks/use-job-state.ts @@ -52,13 +52,13 @@ function writeClearedJobIds(ids: string[]) { function broadcastJobs(ids: string[]) { window.dispatchEvent( - new CustomEvent("labas:jobs-change", { detail: { ids } }), + new CustomEvent("pram:jobs-change", { detail: { ids } }), ); } function broadcastResults(results: CompletedResult[]) { window.dispatchEvent( - new CustomEvent("labas:results-change", { detail: { results } }), + new CustomEvent("pram:results-change", { detail: { results } }), ); } @@ -222,8 +222,8 @@ export function useJobState() { const detail = (e as CustomEvent).detail as { ids: string[] }; dispatch({ type: "syncIds", ids: detail.ids }); }; - window.addEventListener("labas:jobs-change", handler); - return () => window.removeEventListener("labas:jobs-change", handler); + window.addEventListener("pram:jobs-change", handler); + return () => window.removeEventListener("pram:jobs-change", handler); }, []); /* Sync completed results across hook instances (GeneratePage + GlobalProgress) */ @@ -233,8 +233,8 @@ export function useJobState() { skipPersistRef.current = true; dispatch({ type: "syncResults", results: detail.results }); }; - window.addEventListener("labas:results-change", handler); - return () => window.removeEventListener("labas:results-change", handler); + window.addEventListener("pram:results-change", handler); + return () => window.removeEventListener("pram:results-change", handler); }, []); const addJob = useCallback((id: string) => dispatch({ type: "addJob", id }), []); diff --git a/apps/web/src/hooks/use-sidebar.ts b/apps/web/src/hooks/use-sidebar.ts index 782ce330738cba1763ae0398c707d5cd287d51b8..01a5e783c07b0cd49a9d45fbcbca912730d1beb9 100644 --- a/apps/web/src/hooks/use-sidebar.ts +++ b/apps/web/src/hooks/use-sidebar.ts @@ -1,6 +1,6 @@ import { useSyncExternalStore } from "react"; -const STORAGE_KEY = "labas_sidebar_collapsed"; +const STORAGE_KEY = "pram_sidebar_collapsed"; function getSnapshot(): boolean { if (typeof window === "undefined") return false; diff --git a/apps/web/src/hooks/use-test-session.ts b/apps/web/src/hooks/use-test-session.ts index c1a0edfbc2471b3e67cff9051a543d35ec809013..9baa12f08914e0d082b42ffd81601a0be1653e50 100644 --- a/apps/web/src/hooks/use-test-session.ts +++ b/apps/web/src/hooks/use-test-session.ts @@ -5,15 +5,15 @@ import { trpc } from "@/utils/trpc"; import { getErrorMessage } from "@/lib/error-utils"; function getTimerKey(attemptId: string | null) { - return attemptId ? `labas_attempt_timer_${attemptId}` : null; + return attemptId ? `pram_attempt_timer_${attemptId}` : null; } function getMarkedKey(attemptId: string | null) { - return attemptId ? `labas_attempt_marked_${attemptId}` : null; + return attemptId ? `pram_attempt_marked_${attemptId}` : null; } function getSectionKey(attemptId: string | null) { - return attemptId ? `labas_attempt_section_${attemptId}` : null; + return attemptId ? `pram_attempt_section_${attemptId}` : null; } function loadElapsedTime(attemptId: string | null): number { diff --git a/apps/web/src/index.css b/apps/web/src/index.css index 3ce68a841b45168012eacbe08445b73547f3bbf8..f2533e9f5881cc43516f3b61568a862715c62be3 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -1 +1 @@ -@import "@labas/ui/globals.css"; +@import "@pram/ui/globals.css"; diff --git a/apps/web/src/lib/__tests__/difficulty-mapping.test.ts b/apps/web/src/lib/__tests__/difficulty-mapping.test.ts index 2e52f5d606f926c8c19c8eee6cc83f29e0d21952..dd6a8917e6b8860a2a7bcfd75eb3427f239dfd9b 100644 --- a/apps/web/src/lib/__tests__/difficulty-mapping.test.ts +++ b/apps/web/src/lib/__tests__/difficulty-mapping.test.ts @@ -8,7 +8,7 @@ describe("DIFFICULTY_LABELS", () => { }); it("each exam has exactly 5 difficulty levels", () => { - for (const [exam, labels] of Object.entries(DIFFICULTY_LABELS)) { + for (const [_exam, labels] of Object.entries(DIFFICULTY_LABELS)) { expect(labels).toHaveLength(5); } }); diff --git a/apps/web/src/lib/auth-client.ts b/apps/web/src/lib/auth-client.ts index e771d5c8a949fc022f137a01699c582f54fa85af..5513c56c27b3d4162b0cf41a35f9727d71ed3924 100644 --- a/apps/web/src/lib/auth-client.ts +++ b/apps/web/src/lib/auth-client.ts @@ -1,4 +1,4 @@ -import { env } from "@labas/env/web"; +import { env } from "@pram/env/web"; import { createAuthClient } from "better-auth/react"; export const authClient = createAuthClient({ diff --git a/apps/web/src/lib/community-links.ts b/apps/web/src/lib/community-links.ts index 251299027a42c9903580b5e23edbcb0b355c29c5..3ffba1602ffe8cf78c191060521b31dec985ea84 100644 --- a/apps/web/src/lib/community-links.ts +++ b/apps/web/src/lib/community-links.ts @@ -33,10 +33,10 @@ export const COMMUNITY_PLATFORM_META: Record< /** Satu sumber data — tambah Discord cukup isi `href` di entri bawah. */ export const COMMUNITY_CHANNELS: CommunityChannel[] = [ { - id: "labas-whatsapp", + id: "pram-whatsapp", platform: "whatsapp", - name: "Labas — Open Source", - description: "Laporkan bug, usulkan fitur, atau diskusi pengembangan Labas.", + name: "Pram — Open Source", + description: "Laporkan bug, usulkan fitur, atau diskusi pengembangan Pram.", href: "https://chat.whatsapp.com/JT6bDIXMQ5x6SUQCA1aDWB", icon: "groups", }, @@ -50,16 +50,16 @@ export const COMMUNITY_CHANNELS: CommunityChannel[] = [ }, // Aktifkan saat server Discord siap — cukup isi href: { - id: "labas-discord", + id: "pram-discord", platform: "discord", - name: "Labas Discord", + name: "Pram Discord", description: "Diskusi async, bug report terstruktur, dan pengumuman fitur baru.", href: null, icon: "chat", }, ]; -export const COMMUNITY_PROMPT_STORAGE_KEY = "labas_community_prompt_seen"; +export const COMMUNITY_PROMPT_STORAGE_KEY = "pram_community_prompt_seen"; const PLATFORM_ORDER: CommunityPlatform[] = ["whatsapp", "discord"]; diff --git a/apps/web/src/lib/crypto.ts b/apps/web/src/lib/crypto.ts index 7035a0f847877564e6a59586d2985caa38eaeff1..90a9037301a87f4a44cb3cd5dfd3fc2e4c6fbc31 100644 --- a/apps/web/src/lib/crypto.ts +++ b/apps/web/src/lib/crypto.ts @@ -1,6 +1,6 @@ -const DB_NAME = "labas-crypto"; +const DB_NAME = "pram-crypto"; const STORE_NAME = "keys"; -const KEY_NAME = "labas-master-key"; +const KEY_NAME = "pram-master-key"; function openDB(): Promise { return new Promise((resolve, reject) => { diff --git a/apps/web/src/lib/generate-constants.ts b/apps/web/src/lib/generate-constants.ts index 42a8ac9280e507225ae19519769ebf3ba162737a..3200260591b04cfed68eae58f73013d8db0e90a9 100644 --- a/apps/web/src/lib/generate-constants.ts +++ b/apps/web/src/lib/generate-constants.ts @@ -1,45 +1,156 @@ export const EXAM_TYPES = [ - { id: "IELTS", name: "IELTS Academic", code: "gb" }, - { id: "TOEFL", name: "TOEFL iBT", code: "us" }, - { id: "JLPT", name: "JLPT", code: "jp" }, - { id: "HSK", name: "HSK", code: "cn" }, - { id: "GOETHE", name: "Goethe-Zertifikat", code: "de" }, - { id: "TOPIK", name: "TOPIK", code: "kr" }, - { id: "TOAFL", name: "TOAFL", code: "sa" }, - { id: "DELE", name: "DELE", code: "es" }, + { id: "CPNS", name: "Ujian CPNS (SKD)", code: "id" }, + { id: "SKB", name: "Ujian SKB (Teknis)", code: "id" }, + { id: "SKB_PENATA_PERTANAHAN", name: "SKB Penata Pertanahan (BPN/ATR)", code: "id" }, ]; export const SECTIONS = [ - { id: "READING", name: "Reading", icon: "menu_book" }, - { id: "WRITING", name: "Writing", icon: "edit_note" }, + { id: "TWK", name: "TWK", icon: "history_edu" }, + { id: "TIU", name: "TIU", icon: "psychology" }, + { id: "TKP", name: "TKP", icon: "groups" }, + { id: "Teknis", name: "Teknis", icon: "engineering" }, + { id: "Pertanahan_Dasar", name: "Pertanahan & Agraria", icon: "landscape" }, + { id: "Pengukuran_Kadastral", name: "Pengukuran & Pemetaan Kadastral", icon: "map" }, + { id: "Pendaftaran_Tanah", name: "Pendaftaran Tanah & Sertipikat", icon: "assignment" }, + { id: "Pengadaan_Tanah", name: "Pengadaan Tanah & Ganti Rugi", icon: "real_estate_agent" }, + { id: "Pengendalian_Pertanahan", name: "Pengendalian & Penertiban Tanah", icon: "policy" }, + { id: "Penilaian_Tanah", name: "Penilaian Tanah & Properti", icon: "price_check" }, ]; export const FORMATS = [ - { id: "multiple_choice", name: "Multiple Choice", allowedExams: ["IELTS", "TOEFL", "JLPT", "HSK", "GOETHE", "TOPIK", "TOAFL", "DELE"] }, - { id: "true_false_not_given", name: "True / False / Not Given", allowedExams: ["IELTS", "TOEFL", "JLPT", "HSK", "GOETHE", "TOPIK", "TOAFL", "DELE"] }, - { id: "fill_blank", name: "Fill in Blank", allowedExams: ["IELTS", "TOEFL", "JLPT", "HSK", "GOETHE", "TOPIK", "TOAFL", "DELE"] }, - { id: "synonym", name: "Synonym / Vocabulary", allowedExams: ["IELTS", "TOEFL", "JLPT", "HSK", "GOETHE", "TOPIK", "TOAFL", "DELE"] }, - { id: "grammar_in_context", name: "Grammar in Context", allowedExams: ["IELTS", "TOEFL", "JLPT", "HSK", "GOETHE", "TOPIK", "TOAFL", "DELE"] }, - { id: "sentence_completion", name: "Sentence Completion", allowedExams: ["IELTS", "TOEFL", "JLPT", "HSK", "GOETHE", "TOPIK", "TOAFL", "DELE"] }, - { id: "cloze", name: "Cloze Test", allowedExams: ["IELTS", "TOEFL", "JLPT", "HSK", "GOETHE", "TOPIK", "TOAFL", "DELE"] }, - { id: "reference", name: "Reference (Pronoun)", allowedExams: ["IELTS", "TOEFL", "JLPT", "HSK", "GOETHE", "TOPIK", "TOAFL", "DELE"] }, - { id: "author_view", name: "Author's View", allowedExams: ["IELTS", "TOEFL", "JLPT", "HSK", "GOETHE", "TOPIK", "TOAFL", "DELE"] }, - { id: "matching_headings", name: "Matching Headings", allowedExams: ["IELTS", "TOEFL", "JLPT", "HSK", "GOETHE", "TOPIK", "TOAFL", "DELE"] }, - { id: "matching_information", name: "Matching Information", allowedExams: ["IELTS", "TOEFL", "JLPT", "HSK", "GOETHE", "TOPIK", "TOAFL", "DELE"] }, - { id: "summary_completion", name: "Summary Completion", allowedExams: ["IELTS", "TOEFL", "JLPT", "HSK", "GOETHE", "TOPIK", "TOAFL", "DELE"] }, - { id: "matching_pairs", name: "Matching Pairs", allowedExams: ["IELTS", "TOEFL", "GOETHE", "DELE", "TOPIK"] }, - { id: "error_recognition", name: "Error Recognition", allowedExams: ["TOEFL", "HSK", "TOPIK", "TOAFL"] }, - { id: "text_insertion", name: "Text Insertion", allowedExams: ["TOEFL", "IELTS"] }, - { id: "kanji_reading", name: "Kanji Reading", allowedExams: ["JLPT"] }, - { id: "particle_choice", name: "Particle / Conjunction Choice", allowedExams: ["JLPT", "TOPIK"] }, - { id: "article_case", name: "Article / Gender / Agreement", allowedExams: ["GOETHE", "TOAFL", "DELE"] }, - { id: "character_reading", name: "Character Reading", allowedExams: ["HSK", "TOPIK"] }, - { id: "sentence_arrangement", name: "Sentence Arrangement", allowedExams: ["HSK", "TOPIK", "DELE", "TOAFL"] }, + { id: "multiple_choice", name: "Multiple Choice", allowedExams: ["CPNS", "SKB"] }, + { id: "true_false_not_given", name: "Benar / Salah", allowedExams: ["CPNS", "SKB"] }, + { id: "fill_blank", name: "Isian Singkat", allowedExams: ["CPNS", "SKB"] }, + { id: "synonym", name: "Sinonim / Kosakata", allowedExams: ["CPNS", "SKB"] }, + { id: "grammar_in_context", name: "Tata Bahasa", allowedExams: ["CPNS", "SKB"] }, + { id: "sentence_completion", name: "Melengkapi Kalimat", allowedExams: ["CPNS", "SKB"] }, + { id: "cloze", name: "Cloze Test", allowedExams: ["CPNS", "SKB"] }, + { id: "reference", name: "Rujukan Kata", allowedExams: ["CPNS", "SKB"] }, + { id: "author_view", name: "Pandangan Penulis", allowedExams: ["CPNS", "SKB"] }, + { id: "matching_headings", name: "Mencocokkan Judul", allowedExams: ["CPNS", "SKB"] }, + { id: "matching_information", name: "Mencocokkan Informasi", allowedExams: ["CPNS", "SKB"] }, + { id: "summary_completion", name: "Melengkapi Ringkasan", allowedExams: ["CPNS", "SKB"] }, + { id: "matching_pairs", name: "Mencocokkan Pasangan", allowedExams: ["CPNS", "SKB"] }, + { id: "error_recognition", name: "Analisis Kesalahan", allowedExams: ["CPNS", "SKB"] }, + { id: "sentence_arrangement", name: "Menyusun Kalimat", allowedExams: ["CPNS", "SKB"] }, ]; export const TOPICS = [ - "Science & Tech", "Business", "Sociology", "Arts", "History", "Environment", "Health", "Education", - "Daily Interaction", "Social Etiquette", "Workplace", + // SKD - TWK + "Nasionalisme", "Integritas", "Bela Negara", "Pilar Negara", "Bahasa Indonesia", + // SKD - TIU + "Analogi", "Silogisme", "Analitis", "Deret Angka", "Perbandingan Kuantitatif", "Soal Cerita", "Figural", + // SKD - TKP + "Pelayanan Publik", "Jejaring Kerja", "Sosial Budaya", "TIK", "Profesionalisme", "Anti Radikalisme", + // SKB Teknis Umum + "Manajemen Pemerintahan", "Hukum Administrasi Negara", "Kebijakan Publik", + + // ── SKB Penata Pertanahan ───────────────────────────────────────────── + // 1. Hukum Agraria & Regulasi Pertanahan + "Undang-Undang Pokok Agraria (UUPA No. 5 Tahun 1960)", + "Hak Milik, Hak Guna Usaha, Hak Guna Bangunan, Hak Pakai", + "Hak atas Tanah Adat dan Konversi Hak Lama", + "Peraturan Pemerintah No. 24 Tahun 1997 tentang Pendaftaran Tanah", + "Peraturan Pemerintah No. 18 Tahun 2021 tentang Hak Pengelolaan", + "Peraturan Pemerintah No. 20 Tahun 2021 tentang Penertiban Kawasan dan Tanah Terlantar", + "Peraturan Menteri ATR/BPN tentang Pendaftaran Tanah", + "Kepala Badan Pertanahan Nasional dan Struktur Organisasi BPN", + "Reforma Agraria dan Redistribusi Tanah", + "Tanah Negara, Tanah Adat, dan Tanah Ulayat", + "Pengaturan Pemilikan dan Penguasaan Tanah", + "Larangan Pemilikan Tanah secara Absentee", + + // 2. Pendaftaran Tanah & Sertipikat + "Sistem Pendaftaran Tanah Stelsel Negatif Bertendensi Positif", + "Proses Pensertipikatan Tanah: Sporadis dan Sistematis", + "Pendaftaran Tanah Sistematis Lengkap (PTSL)", + "Buku Tanah dan Surat Ukur sebagai Produk Pendaftaran", + "Sertipikat Hak Atas Tanah sebagai Tanda Bukti Hak", + "Pemeliharaan Data Pendaftaran Tanah (Pembaruan Data)", + "Balik Nama, Pemisahan, Pemecahan, dan Penggabungan Sertipikat", + "Sertipikat Tanah Elektronik", + "Pendaftaran Peralihan Hak Karena Jual Beli, Hibah, dan Waris", + "Roya Hak Tanggungan dan Pencatatan Hak Tanggungan", + "Pengumuman dan Penerbitan Sertipikat pada PTSL", + "Kegiatan Pengumpulan Data Fisik dan Yuridis", + + // 3. Pengukuran & Pemetaan Kadastral + "Dasar-Dasar Ilmu Ukur Tanah (Survei & Pemetaan)", + "Pengukuran Batas Bidang Tanah dan Penentuan Batas", + "Peta Bidang Tanah dan Gambar Situasi", + "Peta Dasar Pendaftaran dan Peta Pendaftaran", + "Sistem Koordinat Nasional (TM3)", + "Penggunaan GPS/GNSS untuk Pengukuran Kadastral", + "Drone dan Fotogrametri dalam Pemetaan Pertanahan", + "Kadaster dan Sistem Informasi Pertanahan", + "KKP (Komputerisasi Kegiatan Pertanahan) dan BHUMI", + "Peta Tematik Pertanahan", + "Peta Zona Nilai Tanah (ZNT)", + "Identifikasi dan Delineasi Bidang Tanah", + "Pengukuran Keliling dan Luas Bidang Tanah", + "Rekonstruksi Batas Bidang Tanah", + "Survei Kadastral Berbasis Partisipasi Masyarakat", + + // 4. Pengadaan Tanah & Ganti Rugi + "Undang-Undang No. 2 Tahun 2012 tentang Pengadaan Tanah untuk Kepentingan Umum", + "Peraturan Presiden No. 71 Tahun 2012 dan Perubahannya", + "Tahapan Pengadaan Tanah: Perencanaan, Persiapan, Pelaksanaan, Penyerahan", + "Penetapan Lokasi Pembangunan", + "Inventarisasi dan Identifikasi Bidang Tanah yang Terkena Pengadaan", + "Penilaian Ganti Kerugian oleh Penilai Publik", + "Musyawarah Penetapan Bentuk Ganti Kerugian", + "Penitipan Ganti Kerugian di Pengadilan (Konsignasi)", + "Pengadaan Tanah untuk Proyek Strategis Nasional (PSN)", + "Hak Pemegang Hak Atas Tanah dalam Pengadaan Tanah", + "Ganti Rugi: Uang, Tanah Pengganti, Pemukiman Kembali, dan Kepemilikan Saham", + "Pengadaan Tanah Skala Kecil (di bawah 5 hektar)", + + // 5. Pengendalian & Penertiban Tanah + "Tanah Terlantar dan Proses Penertiban", + "Penetapan Tanah Terlantar sesuai PP No. 20 Tahun 2021", + "Redistribusi Tanah Terlantar sebagai Tanah Cadangan Umum Negara", + "Penertiban Penguasaan Tanah Absentee", + "Monitoring dan Evaluasi Penggunaan Tanah", + "Pengendalian Alih Fungsi Lahan Pertanian", + "Pemantauan Pemanfaatan Ruang dan Kesesuaian Tata Ruang", + "Tata Cara Penyelesaian Sengketa Pertanahan", + "Mediasi dan Pengaduan Sengketa Pertanahan di BPN", + "Sengketa, Konflik, dan Perkara Pertanahan", + "Pemberdayaan Tanah Masyarakat", + + // 6. Penilaian Tanah & Properti + "Zona Nilai Tanah (ZNT) dan Nilai Indikasi Rata-Rata (NIR)", + "Metode Penilaian Tanah: Perbandingan Data Pasar, Biaya, Pendapatan", + "Peta ZNT sebagai Basis NJOP dan Pajak", + "Penilai Publik dan Penilai Internal BPN", + "Nilai Pasar dan Nilai Ganti Rugi", + "Faktor-Faktor yang Mempengaruhi Nilai Tanah", + "Penilaian Tanah untuk PTSL dan Pendaftaran", + + // 7. Tata Ruang & Rencana Penggunaan Tanah + "Undang-Undang No. 26 Tahun 2007 tentang Penataan Ruang", + "RTRW (Rencana Tata Ruang Wilayah) Nasional, Provinsi, Kabupaten/Kota", + "Kesesuaian Tata Ruang dalam Penerbitan Izin dan Hak Atas Tanah", + "Rencana Detail Tata Ruang (RDTR)", + "Izin Peruntukan Penggunaan Tanah (IPPT)", + "Pertanian Pangan Berkelanjutan dan Perlindungan Lahan Sawah", + "Kawasan Lindung, Kawasan Budidaya, dan Kawasan Strategis", + + // 8. Teknologi Informasi Pertanahan + "Aplikasi KKP (Komputerisasi Kegiatan Pertanahan)", + "Layanan BHUMI dan Informasi Publik Pertanahan", + "Sistem Informasi Geografis (SIG) dalam Pertanahan", + "Electronic Land Registration System", + "Data Spasial dan Non-Spasial Pertanahan", + "Integrasi Data Pertanahan dengan OSS (Online Single Submission)", + + // 9. Etika & Pelayanan Publik Pertanahan + "Standar Pelayanan Pertanahan di Kantor Pertanahan", + "Penanganan Pengaduan Pelayanan Pertanahan", + "Maklumat Pelayanan dan SOP BPN", + "Integritas dan Anti-Korupsi dalam Pelayanan Pertanahan", + "Jabatan Fungsional Penata Pertanahan: Tugas dan Kewenangan", + "Angka Kredit dan Jenjang Jabatan Penata Pertanahan", ]; export const DIFFICULTIES = ["Beginner", "Elementary", "Intermediate", "Advanced", "Expert"]; diff --git a/apps/web/src/lib/site-seo.ts b/apps/web/src/lib/site-seo.ts index 064abd2fe88ccc0a43a9034c3b6dbaf36f2ad816..11f5e89a9463d471e15ba08c4644b6fdcb952241 100644 --- a/apps/web/src/lib/site-seo.ts +++ b/apps/web/src/lib/site-seo.ts @@ -1,5 +1,5 @@ -export const SITE_URL = "https://labas.rogasper.com"; -export const SITE_NAME = "Labas"; +export const SITE_URL = "https://pram.rogasper.com"; +export const SITE_NAME = "Pram"; /** Match rogasper.com: separate OG (PNG) + Twitter (JPEG) assets for crawler compatibility. */ export const OG_IMAGE_PATH = "/opengraph-image.png"; @@ -12,9 +12,9 @@ export const TWITTER_IMAGE_URL = `${SITE_URL}${TWITTER_IMAGE_PATH}`; export const OG_IMAGE_WIDTH = 1200; export const OG_IMAGE_HEIGHT = 630; export const OG_IMAGE_ALT = - "Labas — platform latihan ujian bahasa dengan AI Generator dan simulasi tes"; + "Pram — platform latihan ujian bahasa dengan AI Generator dan simulasi tes"; -export const DEFAULT_SITE_TITLE = "Labas — AI-Powered Exam Practice Platform"; +export const DEFAULT_SITE_TITLE = "Pram — AI-Powered Exam Practice Platform"; export const DEFAULT_SITE_DESCRIPTION = "Generate AI-powered practice questions for JLPT, TOPIK, TOAFL, and more. Practice smarter with adaptive test preparation."; diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx index d195e897d4496780c8efcb0ac366d23df434e76b..031bacf0668e6ccb6a30d6c3b73c73273db34733 100644 --- a/apps/web/src/routes/__root.tsx +++ b/apps/web/src/routes/__root.tsx @@ -1,4 +1,4 @@ -import { Toaster } from "@labas/ui/components/sonner"; +import { Toaster } from "@pram/ui/components/sonner"; import type { QueryClient } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { HeadContent, Outlet, createRootRouteWithContext, useRouterState } from "@tanstack/react-router"; @@ -56,7 +56,7 @@ export const Route = createRootRouteWithContext()({ { name: "robots", content: "index, follow" }, ], links: [ - { rel: "icon", href: "/labas_icon.png" }, + { rel: "icon", href: "/pram_icon.png" }, { rel: "canonical", href: `${SITE_URL}/` }, { rel: "preconnect", href: "https://fonts.googleapis.com" }, { rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" as const }, @@ -71,7 +71,7 @@ export const Route = createRootRouteWithContext()({ children: JSON.stringify({ "@context": "https://schema.org", "@type": "WebSite", - name: "Labas", + name: "Pram", url: SITE_URL, description: DEFAULT_SITE_DESCRIPTION, potentialAction: { @@ -98,7 +98,7 @@ function RootComponent() { defaultTheme="light" enableSystem disableTransitionOnChange - storageKey="labas-theme" + storageKey="pram-theme" > {shell === "fullscreen" ? ( diff --git a/apps/web/src/routes/admin.credits.tsx b/apps/web/src/routes/admin.credits.tsx index e10474357c9ee24caca6b4cf2cc0c310a8990728..0c829a1813c3bee4981641c5be73f278cc9c0f10 100644 --- a/apps/web/src/routes/admin.credits.tsx +++ b/apps/web/src/routes/admin.credits.tsx @@ -2,13 +2,12 @@ import { useState } from "react"; import { createFileRoute } from "@tanstack/react-router"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { trpc } from "@/utils/trpc"; -import { Input } from "@labas/ui/components/input"; -import { Button } from "@labas/ui/components/button"; +import { Input } from "@pram/ui/components/input"; +import { Button } from "@pram/ui/components/button"; import { toast } from "sonner"; import { getErrorMessage } from "@/lib/error-utils"; import { useDebouncedValue } from "@/hooks/use-debounced-value"; import { DataTable } from "@/components/admin/DataTable"; -import type { ColumnDef } from "@/components/admin/DataTable"; export const Route = createFileRoute("/admin/credits")({ component: AdminCredits, diff --git a/apps/web/src/routes/admin.featured.tsx b/apps/web/src/routes/admin.featured.tsx index 608e1204864904ff047f20ba0d56ee17f64b5129..3d15eaaa0e9ea0a947ecdd0a34d0152623ec1e14 100644 --- a/apps/web/src/routes/admin.featured.tsx +++ b/apps/web/src/routes/admin.featured.tsx @@ -2,9 +2,9 @@ import { useState } from "react"; import { createFileRoute } from "@tanstack/react-router"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { trpc } from "@/utils/trpc"; -import { Input } from "@labas/ui/components/input"; -import { Button } from "@labas/ui/components/button"; -import { Tabs, TabsList, TabsTrigger, TabsContent } from "@labas/ui/components/tabs"; +import { Input } from "@pram/ui/components/input"; +import { Button } from "@pram/ui/components/button"; +import { Tabs, TabsList, TabsTrigger, TabsContent } from "@pram/ui/components/tabs"; import { toast } from "sonner"; import { getErrorMessage } from "@/lib/error-utils"; import { useDebouncedValue } from "@/hooks/use-debounced-value"; diff --git a/apps/web/src/routes/admin.index.tsx b/apps/web/src/routes/admin.index.tsx index bf4ee0268878b86fa7d8d5e7dc1ed42f19e8f096..fef04394d3f9eb01f7207d09abbbe76440b745c2 100644 --- a/apps/web/src/routes/admin.index.tsx +++ b/apps/web/src/routes/admin.index.tsx @@ -2,8 +2,8 @@ import { useState } from "react"; import { createFileRoute } from "@tanstack/react-router"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { trpc } from "@/utils/trpc"; -import { Input } from "@labas/ui/components/input"; -import { Button } from "@labas/ui/components/button"; +import { Input } from "@pram/ui/components/input"; +import { Button } from "@pram/ui/components/button"; import { toast } from "sonner"; import { getErrorMessage } from "@/lib/error-utils"; diff --git a/apps/web/src/routes/admin.jobs.tsx b/apps/web/src/routes/admin.jobs.tsx index 676e64ac9dce222346c70423e3dd865e9f1a63a3..96b7abf67059fd4772231ada286f52854554fb13 100644 --- a/apps/web/src/routes/admin.jobs.tsx +++ b/apps/web/src/routes/admin.jobs.tsx @@ -2,7 +2,7 @@ import { useState } from "react"; import { createFileRoute } from "@tanstack/react-router"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { trpc } from "@/utils/trpc"; -import { Button } from "@labas/ui/components/button"; +import { Button } from "@pram/ui/components/button"; import { toast } from "sonner"; import { getErrorMessage } from "@/lib/error-utils"; import { DataTable } from "@/components/admin/DataTable"; diff --git a/apps/web/src/routes/admin.moderation.tsx b/apps/web/src/routes/admin.moderation.tsx index ddd08ee137c00a776c96d5eb5e987c15ff161e88..c5521abbef99d735031ac4fcb7dc00bc72789fdc 100644 --- a/apps/web/src/routes/admin.moderation.tsx +++ b/apps/web/src/routes/admin.moderation.tsx @@ -2,7 +2,7 @@ import { useState } from "react"; import { createFileRoute } from "@tanstack/react-router"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { trpc } from "@/utils/trpc"; -import { Button } from "@labas/ui/components/button"; +import { Button } from "@pram/ui/components/button"; import { toast } from "sonner"; import { getErrorMessage } from "@/lib/error-utils"; import { DataTable } from "@/components/admin/DataTable"; diff --git a/apps/web/src/routes/admin.users.tsx b/apps/web/src/routes/admin.users.tsx index 9c98917e234bd09d926b40746759e02564a66d5e..87f637992292591e9a3ef0831af51f6a0ebac0b2 100644 --- a/apps/web/src/routes/admin.users.tsx +++ b/apps/web/src/routes/admin.users.tsx @@ -2,8 +2,8 @@ import { useState } from "react"; import { createFileRoute } from "@tanstack/react-router"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { trpc } from "@/utils/trpc"; -import { Input } from "@labas/ui/components/input"; -import { Button } from "@labas/ui/components/button"; +import { Input } from "@pram/ui/components/input"; +import { Button } from "@pram/ui/components/button"; import { toast } from "sonner"; import { getErrorMessage } from "@/lib/error-utils"; import { useDebouncedValue } from "@/hooks/use-debounced-value"; diff --git a/apps/web/src/routes/attempt.$id.tsx b/apps/web/src/routes/attempt.$id.tsx index 3c172506dce0573aac7418f1b28d41e3c49c9cfe..5ee5da657b93cd0c3a94b5c8eb7215d65a095f75 100644 --- a/apps/web/src/routes/attempt.$id.tsx +++ b/apps/web/src/routes/attempt.$id.tsx @@ -3,8 +3,8 @@ import { useQuery, useMutation } from "@tanstack/react-query"; import { createFileRoute, redirect, Link } from "@tanstack/react-router"; import { authClient } from "@/lib/auth-client"; import { trpc } from "@/utils/trpc"; -import { Button } from "@labas/ui/components/button"; -import { Card, CardContent } from "@labas/ui/components/card"; +import { Button } from "@pram/ui/components/button"; +import { Card, CardContent } from "@pram/ui/components/card"; import { MaterialIcon } from "@/components/ui/MaterialIcon"; import { formatTime } from "@/lib/time"; import type { Question } from "@/lib/types"; @@ -23,13 +23,6 @@ interface AnswerItem { timeSpentSec?: number; } -interface QuestionAnswer { - q: Question; - ans: AnswerItem | null; - secIdx: number; - qIdx: number; -} - export const Route = createFileRoute("/attempt/$id")({ component: AttemptResultComponent, beforeLoad: async () => { diff --git a/apps/web/src/routes/dashboard.tsx b/apps/web/src/routes/dashboard.tsx index 10782826e3b3436c3c2cb1350c3cd19909a5e8cb..857765036bec003200ffa8886bb0e23b22a51b9a 100644 --- a/apps/web/src/routes/dashboard.tsx +++ b/apps/web/src/routes/dashboard.tsx @@ -4,8 +4,8 @@ import { createFileRoute, redirect, Link } from "@tanstack/react-router"; import { authClient } from "@/lib/auth-client"; import { routeShell } from "@/lib/route-shell"; import { trpc } from "@/utils/trpc"; -import { Button } from "@labas/ui/components/button"; -import { Card, CardContent } from "@labas/ui/components/card"; +import { Button } from "@pram/ui/components/button"; +import { Card, CardContent } from "@pram/ui/components/card"; import { MaterialIcon } from "@/components/ui/MaterialIcon"; import { TourGuide } from "@/components/TourGuide"; import { DonationModal } from "@/components/DonationModal"; @@ -118,7 +118,7 @@ function HomeComponent() { - Selamat Datang di Labas + Selamat Datang di Pram Platform latihan ujian bahasa berbasis AI. Ikuti 3 langkah mudah untuk memulai: diff --git a/apps/web/src/routes/forgot-password.tsx b/apps/web/src/routes/forgot-password.tsx index 9a5622e0fe2e1a232bb997cbdc6e776f8ba23673..336be3bf8d33f88b59c9f4d696554304febb6ffb 100644 --- a/apps/web/src/routes/forgot-password.tsx +++ b/apps/web/src/routes/forgot-password.tsx @@ -1,10 +1,10 @@ import { createFileRoute, Link, useNavigate } from "@tanstack/react-router"; import { useState, useEffect, useRef } from "react"; import { useMutation } from "@tanstack/react-query"; -import { Button } from "@labas/ui/components/button"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@labas/ui/components/card"; -import { Input } from "@labas/ui/components/input"; -import { Label } from "@labas/ui/components/label"; +import { Button } from "@pram/ui/components/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@pram/ui/components/card"; +import { Input } from "@pram/ui/components/input"; +import { Label } from "@pram/ui/components/label"; import { toast } from "sonner"; import { getErrorMessage } from "@/lib/error-utils"; import { trpc } from "@/utils/trpc"; @@ -242,7 +242,7 @@ function RouteComponent() { if (!email) { return ( - + ); @@ -250,7 +250,7 @@ function RouteComponent() { return ( - + ); diff --git a/apps/web/src/routes/jobs.tsx b/apps/web/src/routes/jobs.tsx index 452d44b54190ccb0aae5887d0f8a5a299ffc119e..90c25c04927f9888c19bb0ca4fe25805555175d6 100644 --- a/apps/web/src/routes/jobs.tsx +++ b/apps/web/src/routes/jobs.tsx @@ -4,10 +4,10 @@ import { createFileRoute, redirect, Link } from "@tanstack/react-router"; import { z } from "zod"; import { authClient } from "@/lib/auth-client"; import { trpc } from "@/utils/trpc"; -import { Card, CardContent, CardHeader, CardTitle } from "@labas/ui/components/card"; -import { Button } from "@labas/ui/components/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@pram/ui/components/card"; +import { Button } from "@pram/ui/components/button"; import { MaterialIcon } from "@/components/ui/MaterialIcon"; -import type { GenerationResult } from "@labas/ai"; +import type { GenerationResult } from "@pram/ai"; interface LogEntry { step: string; diff --git a/apps/web/src/routes/leaderboard.tsx b/apps/web/src/routes/leaderboard.tsx index bc65d0a4e42611418e0fc906b529df25a26cc064..004b74d5141df231f1e08a87c8099c7856a61d30 100644 --- a/apps/web/src/routes/leaderboard.tsx +++ b/apps/web/src/routes/leaderboard.tsx @@ -3,8 +3,8 @@ import { createFileRoute, redirect, Link } from "@tanstack/react-router"; import { useState } from "react"; import { authClient } from "@/lib/auth-client"; import { trpc } from "@/utils/trpc"; -import { Button } from "@labas/ui/components/button"; -import { Card, CardContent } from "@labas/ui/components/card"; +import { Button } from "@pram/ui/components/button"; +import { Card, CardContent } from "@pram/ui/components/card"; import { Select, SelectContent, @@ -12,7 +12,7 @@ import { SelectItem, SelectTrigger, SelectValue, -} from "@labas/ui/components/select"; +} from "@pram/ui/components/select"; import { MaterialIcon } from "@/components/ui/MaterialIcon"; import { EXAM_TYPES } from "@/lib/exam-constants"; import { RankingPodium } from "@/components/leaderboard/RankingPodium"; diff --git a/apps/web/src/routes/login.tsx b/apps/web/src/routes/login.tsx index 359cf2571d16696b1a6c8df824a2f300c396c6d4..fca1a61aca2e9fd199de53f79ee57ab5d960201b 100644 --- a/apps/web/src/routes/login.tsx +++ b/apps/web/src/routes/login.tsx @@ -16,7 +16,7 @@ function RouteComponent() { return ( - + pram {showSignIn ? ( setShowSignIn(false)} /> ) : ( diff --git a/apps/web/src/routes/me.tsx b/apps/web/src/routes/me.tsx index 96fba9ce8527f55bc9f3cbf8d09263c5a07682f8..33fc84e23bd5562de4581cea43d64a166dfe9f00 100644 --- a/apps/web/src/routes/me.tsx +++ b/apps/web/src/routes/me.tsx @@ -3,9 +3,9 @@ import { useQuery, useMutation } from "@tanstack/react-query"; import { createFileRoute, redirect, Link } from "@tanstack/react-router"; import { authClient } from "@/lib/auth-client"; import { trpc } from "@/utils/trpc"; -import { Button } from "@labas/ui/components/button"; -import { Input } from "@labas/ui/components/input"; -import { Card, CardContent } from "@labas/ui/components/card"; +import { Button } from "@pram/ui/components/button"; +import { Input } from "@pram/ui/components/input"; +import { Card, CardContent } from "@pram/ui/components/card"; import { MaterialIcon } from "@/components/ui/MaterialIcon"; import { Avatar } from "@/components/ui/Avatar"; diff --git a/apps/web/src/routes/package.$id.attempt.$attemptId.tsx b/apps/web/src/routes/package.$id.attempt.$attemptId.tsx index 431cbe0a6dcae4e04337349f53d975553b479190..17a90146ddea595593ebf26302b5b52c27afa22e 100644 --- a/apps/web/src/routes/package.$id.attempt.$attemptId.tsx +++ b/apps/web/src/routes/package.$id.attempt.$attemptId.tsx @@ -5,7 +5,7 @@ import { authClient } from "@/lib/auth-client"; import { trpc } from "@/utils/trpc"; import { useTestSession } from "@/hooks/use-test-session"; import { trackUmamiEvent, AnalyticsEvent } from "@/lib/umami"; -import { Button } from "@labas/ui/components/button"; +import { Button } from "@pram/ui/components/button"; import { MaterialIcon } from "@/components/ui/MaterialIcon"; import { AttemptTestView } from "@/components/test/AttemptTestView"; import { routeShell } from "@/lib/route-shell"; diff --git a/apps/web/src/routes/package.$id.index.tsx b/apps/web/src/routes/package.$id.index.tsx index 95880db2a7e7ac7ded21474e6a0273dd652429f2..3391a180a2ee524735954682cd765987000078d4 100644 --- a/apps/web/src/routes/package.$id.index.tsx +++ b/apps/web/src/routes/package.$id.index.tsx @@ -3,8 +3,8 @@ import { useQuery, useMutation } from "@tanstack/react-query"; import { createFileRoute, redirect, Link } from "@tanstack/react-router"; import { authClient } from "@/lib/auth-client"; import { trpc } from "@/utils/trpc"; -import { Button } from "@labas/ui/components/button"; -import { Card, CardContent } from "@labas/ui/components/card"; +import { Button } from "@pram/ui/components/button"; +import { Card, CardContent } from "@pram/ui/components/card"; import { MaterialIcon } from "@/components/ui/MaterialIcon"; import { formatLabel } from "@/lib/format"; import { EditPackageModal } from "@/components/package/EditPackageModal"; diff --git a/apps/web/src/routes/package.$id.take.tsx b/apps/web/src/routes/package.$id.take.tsx index cf61d9f5a8ef420998c57cdca3a492b9a25a2ea9..0067dfd00b0acbdb735c6c5f12830031ddb262f3 100644 --- a/apps/web/src/routes/package.$id.take.tsx +++ b/apps/web/src/routes/package.$id.take.tsx @@ -5,8 +5,8 @@ import { authClient } from "@/lib/auth-client"; import { trpc } from "@/utils/trpc"; import { useTestSession } from "@/hooks/use-test-session"; import { trackUmamiEvent, AnalyticsEvent } from "@/lib/umami"; -import { Button } from "@labas/ui/components/button"; -import { Card, CardContent } from "@labas/ui/components/card"; +import { Button } from "@pram/ui/components/button"; +import { Card, CardContent } from "@pram/ui/components/card"; import { MaterialIcon } from "@/components/ui/MaterialIcon"; import { AttemptTestView } from "@/components/test/AttemptTestView"; import { routeShell } from "@/lib/route-shell"; diff --git a/apps/web/src/routes/profile.$userId.tsx b/apps/web/src/routes/profile.$userId.tsx index b17dcb5c8d12fa150689c4a467f87c4b66f5dd6c..2dd375c409b70975844f5d1a071e7ca985925a11 100644 --- a/apps/web/src/routes/profile.$userId.tsx +++ b/apps/web/src/routes/profile.$userId.tsx @@ -1,7 +1,7 @@ import { useQuery } from "@tanstack/react-query"; import { createFileRoute, Link } from "@tanstack/react-router"; import { trpc } from "@/utils/trpc"; -import { Card, CardContent } from "@labas/ui/components/card"; +import { Card, CardContent } from "@pram/ui/components/card"; import { MaterialIcon } from "@/components/ui/MaterialIcon"; import { Avatar } from "@/components/ui/Avatar"; diff --git a/apps/web/src/routes/setup-avatar.tsx b/apps/web/src/routes/setup-avatar.tsx index 570d398dc6d5300c992d4aaa3f0b00f828d2ee49..e210a59ca08a663609e5395b4ad0b7ffa3ec00c2 100644 --- a/apps/web/src/routes/setup-avatar.tsx +++ b/apps/web/src/routes/setup-avatar.tsx @@ -7,10 +7,10 @@ import { } from "@tanstack/react-router"; import { authClient } from "@/lib/auth-client"; import { trpc } from "@/utils/trpc"; -import { Button } from "@labas/ui/components/button"; -import { Card, CardContent } from "@labas/ui/components/card"; +import { Button } from "@pram/ui/components/button"; +import { Card, CardContent } from "@pram/ui/components/card"; import { MaterialIcon } from "@/components/ui/MaterialIcon"; -import { cn } from "@labas/ui/lib/utils"; +import { cn } from "@pram/ui/lib/utils"; import { buildDiceBearLoreleiUrl, randomSeed, diff --git a/apps/web/src/routes/verify-email.tsx b/apps/web/src/routes/verify-email.tsx index 61df3bf6871293fa7fc19a94b389fcf5026a9df1..b19383e1175bc3214c5efa923e29032947c448ae 100644 --- a/apps/web/src/routes/verify-email.tsx +++ b/apps/web/src/routes/verify-email.tsx @@ -1,9 +1,9 @@ import { createFileRoute, Link, useNavigate, redirect } from "@tanstack/react-router"; import { useState, useEffect, useRef } from "react"; import { useMutation } from "@tanstack/react-query"; -import { Button } from "@labas/ui/components/button"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@labas/ui/components/card"; -import { Input } from "@labas/ui/components/input"; +import { Button } from "@pram/ui/components/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@pram/ui/components/card"; +import { Input } from "@pram/ui/components/input"; import { toast } from "sonner"; import type { TurnstileInstance } from "@marsidev/react-turnstile"; import { getErrorMessage } from "@/lib/error-utils"; diff --git a/apps/web/src/utils/trpc.ts b/apps/web/src/utils/trpc.ts index 3094d73f5dc27c3ad8ffd1283d64aff03cbf3fae..ecd4c5b8901dc7d833e157f6cef122b90e7958c3 100644 --- a/apps/web/src/utils/trpc.ts +++ b/apps/web/src/utils/trpc.ts @@ -1,5 +1,5 @@ -import type { AppRouter } from "@labas/api/routers/index"; -import { env } from "@labas/env/web"; +import type { AppRouter } from "@pram/api/routers/index"; +import { env } from "@pram/env/web"; import { QueryCache, QueryClient } from "@tanstack/react-query"; import { createTRPCClient, httpBatchLink, TRPCClientError } from "@trpc/client"; import { createTRPCOptionsProxy } from "@trpc/tanstack-react-query"; diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 377719442b29c0fd5ae51765b3da83eb11884796..ef489c1f50f45cf43cc6e67d782bbe82f7691a56 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -11,9 +11,9 @@ "rootDirs": ["."], "paths": { "@/*": ["./src/*"], - "@labas/ui/*": ["../../packages/ui/src/*"], - "@labas/ai": ["../../packages/ai/src/index.ts"], - "@labas/ai/*": ["../../packages/ai/src/*.ts"] + "@pram/ui/*": ["../../packages/ui/src/*"], + "@pram/ai": ["../../packages/ai/src/index.ts"], + "@pram/ai/*": ["../../packages/ai/src/*.ts"] } } } diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 9762118a09919f234e2ae6c5257e8c264f8f6b47..4dfa304be6f1036282ad89290fdebd9f5671e740 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -43,8 +43,8 @@ export default defineConfig({ VitePWA({ registerType: "autoUpdate", manifest: { - name: "labas", - short_name: "labas", + name: "pram", + short_name: "pram", description: "AI-powered multi-language test practice platform", theme_color: "#0c0c0c", }, diff --git a/bts.jsonc b/bts.jsonc index a936a7cd678ef1a7b92cc73d2c80faed430e955f..cc158ea17abbf915cfc48ea313f92c499f297807 100644 --- a/bts.jsonc +++ b/bts.jsonc @@ -13,7 +13,7 @@ "$schema": "https://r2.better-t-stack.dev/schema.json", "version": "3.27.5", "createdAt": "2026-04-25T04:42:01.769Z", - "reproducibleCommand": "bun create better-t-stack@latest labas --frontend tanstack-router --backend hono --runtime bun --database postgres --orm drizzle --api trpc --auth better-auth --payments none --addons pwa skills turborepo --examples none --db-setup docker --web-deploy none --server-deploy none --git --package-manager bun --install", + "reproducibleCommand": "bun create better-t-stack@latest pram --frontend tanstack-router --backend hono --runtime bun --database postgres --orm drizzle --api trpc --auth better-auth --payments none --addons pwa skills turborepo --examples none --db-setup docker --web-deploy none --server-deploy none --git --package-manager bun --install", "database": "postgres", "orm": "drizzle", "backend": "hono", diff --git a/bun.lock b/bun.lock index a4fa2f3242e4a380b3c0b161483f1bcd17c73cf3..6754c0c4e82b553368bb57d5c5822957eff3dbea 100644 --- a/bun.lock +++ b/bun.lock @@ -3,15 +3,15 @@ "configVersion": 1, "workspaces": { "": { - "name": "labas", + "name": "pram", "dependencies": { - "@labas/env": "workspace:*", + "@pram/env": "workspace:*", "dotenv": "catalog:", "react-joyride": "^3.1.0", "zod": "catalog:", }, "devDependencies": { - "@labas/config": "workspace:*", + "@pram/config": "workspace:*", "@types/bun": "catalog:", "turbo": "^2.8.12", "typescript": "catalog:", @@ -21,10 +21,10 @@ "name": "server", "dependencies": { "@hono/trpc-server": "^0.4.0", - "@labas/api": "workspace:*", - "@labas/auth": "workspace:*", - "@labas/db": "workspace:*", - "@labas/env": "workspace:*", + "@pram/api": "workspace:*", + "@pram/auth": "workspace:*", + "@pram/db": "workspace:*", + "@pram/env": "workspace:*", "@trpc/server": "catalog:", "better-auth": "catalog:", "dotenv": "catalog:", @@ -33,7 +33,7 @@ "zod": "catalog:", }, "devDependencies": { - "@labas/config": "workspace:*", + "@pram/config": "workspace:*", "@types/bun": "catalog:", "@types/nodemailer": "^8.0.0", "tsdown": "^0.21.9", @@ -45,12 +45,12 @@ "version": "0.0.0", "dependencies": { "@hookform/resolvers": "^5.2.2", - "@labas/ai": "workspace:*", - "@labas/api": "workspace:*", - "@labas/auth": "workspace:*", - "@labas/env": "workspace:*", - "@labas/ui": "workspace:*", "@marsidev/react-turnstile": "^1.5.2", + "@pram/ai": "workspace:*", + "@pram/api": "workspace:*", + "@pram/auth": "workspace:*", + "@pram/env": "workspace:*", + "@pram/ui": "workspace:*", "@tailwindcss/vite": "^4.2.2", "@tanstack/react-form": "^1.28.0", "@tanstack/react-query": "^5.90.12", @@ -73,7 +73,7 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", - "@labas/config": "workspace:*", + "@pram/config": "workspace:*", "@tanstack/react-query-devtools": "^5.91.1", "@tanstack/react-router-devtools": "^1.166.13", "@tanstack/router-plugin": "^1.167.22", @@ -92,23 +92,23 @@ }, }, "packages/ai": { - "name": "@labas/ai", + "name": "@pram/ai", "dependencies": { "zod": "catalog:", "zod-to-json-schema": "^3.25.2", }, "devDependencies": { - "@labas/config": "workspace:*", + "@pram/config": "workspace:*", "typescript": "catalog:", }, }, "packages/api": { - "name": "@labas/api", + "name": "@pram/api", "dependencies": { - "@labas/ai": "workspace:*", - "@labas/auth": "workspace:*", - "@labas/db": "workspace:*", - "@labas/env": "workspace:*", + "@pram/ai": "workspace:*", + "@pram/auth": "workspace:*", + "@pram/db": "workspace:*", + "@pram/env": "workspace:*", "@trpc/client": "catalog:", "@trpc/server": "catalog:", "bcryptjs": "^3.0.3", @@ -122,7 +122,7 @@ }, "devDependencies": { "@electric-sql/pglite": "^0.4.5", - "@labas/config": "workspace:*", + "@pram/config": "workspace:*", "@types/bcryptjs": "^3.0.0", "@types/nodemailer": "^8.0.0", "hono": "catalog:", @@ -130,41 +130,41 @@ }, }, "packages/auth": { - "name": "@labas/auth", + "name": "@pram/auth", "dependencies": { - "@labas/db": "workspace:*", - "@labas/env": "workspace:*", + "@pram/db": "workspace:*", + "@pram/env": "workspace:*", "better-auth": "catalog:", "dotenv": "catalog:", "zod": "catalog:", }, "devDependencies": { - "@labas/config": "workspace:*", + "@pram/config": "workspace:*", "typescript": "catalog:", }, }, "packages/config": { - "name": "@labas/config", + "name": "@pram/config", "version": "0.0.0", }, "packages/db": { - "name": "@labas/db", + "name": "@pram/db", "dependencies": { - "@labas/env": "workspace:*", + "@pram/env": "workspace:*", "dotenv": "catalog:", "drizzle-orm": "^0.45.1", "pg": "^8.17.1", "zod": "catalog:", }, "devDependencies": { - "@labas/config": "workspace:*", + "@pram/config": "workspace:*", "@types/pg": "^8.16.0", "drizzle-kit": "^0.31.8", "typescript": "catalog:", }, }, "packages/env": { - "name": "@labas/env", + "name": "@pram/env", "version": "0.0.0", "dependencies": { "@t3-oss/env-core": "^0.13.1", @@ -172,13 +172,13 @@ "zod": "catalog:", }, "devDependencies": { - "@labas/config": "workspace:*", + "@pram/config": "workspace:*", "@types/bun": "catalog:", "typescript": "catalog:", }, }, "packages/ui": { - "name": "@labas/ui", + "name": "@pram/ui", "version": "0.0.0", "dependencies": { "@base-ui/react": "^1.0.0", @@ -194,7 +194,7 @@ "tw-animate-css": "^1.3.4", }, "devDependencies": { - "@labas/config": "workspace:*", + "@pram/config": "workspace:*", "@types/react": "^19.2.10", "@types/react-dom": "catalog:", "tailwindcss": "^4.1.18", @@ -617,20 +617,6 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - "@labas/ai": ["@labas/ai@workspace:packages/ai"], - - "@labas/api": ["@labas/api@workspace:packages/api"], - - "@labas/auth": ["@labas/auth@workspace:packages/auth"], - - "@labas/config": ["@labas/config@workspace:packages/config"], - - "@labas/db": ["@labas/db@workspace:packages/db"], - - "@labas/env": ["@labas/env@workspace:packages/env"], - - "@labas/ui": ["@labas/ui@workspace:packages/ui"], - "@marsidev/react-turnstile": ["@marsidev/react-turnstile@1.5.2", "", { "peerDependencies": { "react": "^17.0.2 || ^18.0.0 || ^19.0", "react-dom": "^17.0.2 || ^18.0.0 || ^19.0" } }, "sha512-+3aBPxp86JzSC0ZmgyonoGoUEENcUkH3LGahXSpkV87ArvD2DzRCmPgh0FyQk6PQRmJwQJDAfwNavFsxUxMQWA=="], "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="], @@ -673,6 +659,20 @@ "@oxc-project/types": ["@oxc-project/types@0.127.0", "", {}, "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ=="], + "@pram/ai": ["@pram/ai@workspace:packages/ai"], + + "@pram/api": ["@pram/api@workspace:packages/api"], + + "@pram/auth": ["@pram/auth@workspace:packages/auth"], + + "@pram/config": ["@pram/config@workspace:packages/config"], + + "@pram/db": ["@pram/db@workspace:packages/db"], + + "@pram/env": ["@pram/env@workspace:packages/env"], + + "@pram/ui": ["@pram/ui@workspace:packages/ui"], + "@quansync/fs": ["@quansync/fs@1.0.0", "", { "dependencies": { "quansync": "^1.0.0" } }, "sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ=="], "@reduxjs/toolkit": ["@reduxjs/toolkit@2.11.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ=="], diff --git a/deploy/COOLIFY.md b/deploy/COOLIFY.md index 0e1dfbb1eb0e14f349bb5922cccc56f8e8d4f389..257d753488e712d9a95204056679ba5c1c268942 100644 --- a/deploy/COOLIFY.md +++ b/deploy/COOLIFY.md @@ -1,6 +1,6 @@ -# Deploying Labas on Coolify +# Deploying Pram on Coolify -Labas is a Turborepo monorepo with separate **web** (Vite SPA) and **server** (Hono API) apps. Deploy them as **two Coolify applications** from the same Git repository, plus **PostgreSQL** and **Redis**. +Pram is a Turborepo monorepo with separate **web** (Vite SPA) and **server** (Hono API) apps. Deploy them as **two Coolify applications** from the same Git repository, plus **PostgreSQL** and **Redis**. ``` User → app.example.com (Web) → api.example.com (Server) → PostgreSQL + Redis @@ -23,13 +23,13 @@ Both applications use the **repository root** as Base Directory. They differ onl 1. In Coolify, create a **PostgreSQL** database service. 2. Note the **internal hostname**, port, username, and password. -3. Create a database named `labas` (or adjust `DATABASE_URL` accordingly). +3. Create a database named `pram` (or adjust `DATABASE_URL` accordingly). 4. Keep the service in the same Coolify project/network as the server app. Example internal connection (hostname varies by Coolify): ```env -DATABASE_URL=postgresql://postgres:YOUR_PASSWORD@YOUR_POSTGRES_HOST:5432/labas +DATABASE_URL=postgresql://postgres:YOUR_PASSWORD@YOUR_POSTGRES_HOST:5432/pram ``` --- @@ -81,7 +81,7 @@ Set these on the **server** application. All are validated at startup in `packag ```env NODE_ENV=production -DATABASE_URL=postgresql://postgres:YOUR_PASSWORD@YOUR_POSTGRES_HOST:5432/labas +DATABASE_URL=postgresql://postgres:YOUR_PASSWORD@YOUR_POSTGRES_HOST:5432/pram REDIS_URL=redis://YOUR_REDIS_HOST:6379 BETTER_AUTH_SECRET=YOUR_RANDOM_SECRET_MIN_32_CHARS @@ -94,7 +94,7 @@ SMTP_HOST=smtp.example.com SMTP_PORT=587 SMTP_USER=your-smtp-user SMTP_PASS=your-smtp-password -SMTP_FROM=Labas +SMTP_FROM=Pram ``` Generate secrets: @@ -139,7 +139,7 @@ To get the connection string: 3. Paste into the **server app** env: ```env -DATABASE_URL=postgresql://postgres:YOUR_PASSWORD@YOUR_POSTGRES_HOST:5432/labas +DATABASE_URL=postgresql://postgres:YOUR_PASSWORD@YOUR_POSTGRES_HOST:5432/pram ``` Verify in the server app **Terminal**: @@ -243,4 +243,4 @@ Rebuild the web app whenever the API URL changes. | [`deploy/server.nixpacks.toml`](./server.nixpacks.toml) | API server | | [`deploy/web.nixpacks.toml`](./web.nixpacks.toml) | Web app (Option B) | -Both run `bun install` from the monorepo root so workspace packages (`@labas/*`) resolve correctly. +Both run `bun install` from the monorepo root so workspace packages (`@pram/*`) resolve correctly. diff --git a/deploy/server.nixpacks.toml b/deploy/server.nixpacks.toml index 5d374360518c1d171605b92a12654fa343c491c5..74a5bc2601052cdd6c4cb3ce7b3988eb285fc9e5 100644 --- a/deploy/server.nixpacks.toml +++ b/deploy/server.nixpacks.toml @@ -1,4 +1,4 @@ -# Nixpacks config for the Labas API server (Hono + tRPC). +# Nixpacks config for the Pram API server (Hono + tRPC). # Point Coolify to this file: NIXPACKS_CONFIG_FILE=deploy/server.nixpacks.toml [phases.setup] diff --git a/deploy/web.nixpacks.toml b/deploy/web.nixpacks.toml index 9f1e98747b9cc17d4be5f7bcc78aa1264daa7c2e..9308ef220705d22cf73d80c55e465af4dc31c6d7 100644 --- a/deploy/web.nixpacks.toml +++ b/deploy/web.nixpacks.toml @@ -1,4 +1,4 @@ -# Nixpacks config for the Labas web app (Vite SPA). +# Nixpacks config for the Pram web app (Vite SPA). # Point Coolify to this file: NIXPACKS_CONFIG_FILE=deploy/web.nixpacks.toml # # Requires VITE_SERVER_URL at build time (enable "Available at Buildtime" in Coolify). diff --git a/fix-mojibake.cjs b/fix-mojibake.cjs new file mode 100644 index 0000000000000000000000000000000000000000..cc1b5ad712975748e6801545e8c4d1293fca8873 --- /dev/null +++ b/fix-mojibake.cjs @@ -0,0 +1,41 @@ +const fs = require('fs'); +const path = require('path'); + +const replacements = { + '─': '─', + '│': '│', + '├': '├', + 'â””': '└', +}; + +function processDirectory(dir) { + const files = fs.readdirSync(dir); + for (const file of files) { + if (file === 'node_modules' || file === '.git' || file === 'dist' || file === 'build') continue; + + const fullPath = path.join(dir, file); + const stat = fs.statSync(fullPath); + + if (stat.isDirectory()) { + processDirectory(fullPath); + } else if (stat.isFile() && (fullPath.endsWith('.ts') || fullPath.endsWith('.tsx') || fullPath.endsWith('.html') || fullPath.endsWith('.md') || fullPath.endsWith('.js') || fullPath.endsWith('.json') || fullPath.endsWith('.env.example'))) { + let content = fs.readFileSync(fullPath, 'utf8'); + let modified = false; + + for (const [bad, good] of Object.entries(replacements)) { + if (content.includes(bad)) { + content = content.split(bad).join(good); + modified = true; + } + } + + if (modified) { + fs.writeFileSync(fullPath, content, 'utf8'); + console.log(`Fixed: ${fullPath}`); + } + } + } +} + +processDirectory(__dirname); +console.log('Done!'); diff --git a/package.json b/package.json index 87e1600074fa60044cc5fafec0ffd369d6ecf635..ee68e6df6fcdd076b1d896f88714767480c5b51e 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "labas", + "name": "pram", "version": "0.1.0", "private": true, "workspaces": { @@ -22,31 +22,31 @@ }, "type": "module", "scripts": { - "dev": "turbo dev", - "build": "turbo build", - "test": "turbo test", - "check-types": "turbo check-types", + "dev": "turbo dev --filter=!.", + "build": "turbo build --filter=!.", + "test": "turbo test --filter=!.", + "check-types": "turbo check-types --filter=!.", "compress:og": "bun scripts/compress-og-image.ts", "dev:web": "turbo -F web dev", "dev:server": "turbo -F server dev", - "db:push": "turbo -F @labas/db db:push", - "db:studio": "turbo -F @labas/db db:studio", - "db:generate": "turbo -F @labas/db db:generate", - "db:migrate": "turbo -F @labas/db db:migrate", - "db:seed": "turbo -F @labas/db db:seed", - "db:start": "turbo -F @labas/db db:start", - "db:watch": "turbo -F @labas/db db:watch", - "db:stop": "turbo -F @labas/db db:stop", - "db:down": "turbo -F @labas/db db:down" + "db:push": "turbo -F @pram/db db:push", + "db:studio": "turbo -F @pram/db db:studio", + "db:generate": "turbo -F @pram/db db:generate", + "db:migrate": "turbo -F @pram/db db:migrate", + "db:seed": "turbo -F @pram/db db:seed", + "db:start": "turbo -F @pram/db db:start", + "db:watch": "turbo -F @pram/db db:watch", + "db:stop": "turbo -F @pram/db db:stop", + "db:down": "turbo -F @pram/db db:down" }, "dependencies": { - "@labas/env": "workspace:*", + "@pram/env": "workspace:*", "dotenv": "catalog:", "react-joyride": "^3.1.0", "zod": "catalog:" }, "devDependencies": { - "@labas/config": "workspace:*", + "@pram/config": "workspace:*", "@types/bun": "catalog:", "turbo": "^2.8.12", "typescript": "catalog:" diff --git a/packages/ai/LICENSE b/packages/ai/LICENSE index 1fc95992a2e8ad86439ad2a53a3ad137e2c52dc2..566070fb89d97a3937c0d03275e52536067e9e44 100644 --- a/packages/ai/LICENSE +++ b/packages/ai/LICENSE @@ -1,6 +1,6 @@ -MIT License +MIT License -Copyright (c) 2026 Labas Project Authors +Copyright (c) 2026 Pram Project Authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/packages/ai/package.json b/packages/ai/package.json index 8fb73c47803b123733c8a7aaf0888a6760a3962f..4792b1c7568d3e83942ce27a05728c091267bdc9 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,5 +1,5 @@ { - "name": "@labas/ai", + "name": "@pram/ai", "license": "MIT", "type": "module", "exports": { @@ -18,7 +18,7 @@ "test": "bun test" }, "devDependencies": { - "@labas/config": "workspace:*", + "@pram/config": "workspace:*", "typescript": "catalog:" } } diff --git a/packages/ai/src/agentic.ts b/packages/ai/src/agentic.ts index 37999b6daaefeff85213b3384557a845f7d0359f..fbb4a3799f15499e9798d4ba46d3124c5f8982e4 100644 --- a/packages/ai/src/agentic.ts +++ b/packages/ai/src/agentic.ts @@ -183,7 +183,7 @@ Rules: - Each question must be directly answerable from the passage - Use "passageText" field with the relevant excerpt from the passage (or full passage if needed) - Questions should test real comprehension, not surface recall -- For multiple choice: always provide 4 options (A, B, C, D) with one clearly correct answer +- For multiple choice: always provide 5 options (A, B, C, D, E) with one clearly correct answer - Options must be plausible distractors ${OPTION_QUALITY_RULES} ${buildContentLanguageRules(input.examType)} diff --git a/packages/ai/src/language-rules.ts b/packages/ai/src/language-rules.ts index e0dde078d590dad86384e85ad24238c53da58364..9e764163981680ba536c77f97fff6546728ea709 100644 --- a/packages/ai/src/language-rules.ts +++ b/packages/ai/src/language-rules.ts @@ -4,105 +4,49 @@ interface QuestionLanguageFields { } /** Target language for exam content (passage, question, options). */ -export function getTargetLanguage(examType: string): string { - if (examType === "JLPT") return "Japanese"; - if (examType === "HSK") return "Chinese"; - if (examType === "GOETHE") return "German"; - if (examType === "TOPIK") return "Korean"; - if (examType === "TOAFL") return "Arabic"; - if (examType === "DELE") return "Spanish"; - return "English"; +export function getTargetLanguage(_examType: string): string { + return "Bahasa Indonesia"; } -const EXAMS_REQUIRING_NON_LATIN_SCRIPT = new Set(["JLPT", "HSK", "TOPIK"]); - const INDONESIAN_PROSE_MARKERS = - /\b(yang|dari|pada|Apakah|Menurut|paragraf|bacaan|teks|judul|penulis|artikel|soal|pilihan|jawaban|berikut|manakah|bagian|kalimat|isinya|topik|gagasan|benar|karena|merujuk|sesuai|artinya|makna|kata|terdapat|disebutkan|dijelaskan|adalah|jadi|oleh|opsi|pilihan|jawabannya|penjelasan)\b/i; + /\b(yang|dari|pada|Apakah|Menurut|paragraf|bacaan|teks|judul|penulis|artikel|soal|pilihan|jawaban|berikut|manakah|bagian|kalimat|isinya|topik|gagasan|benar|karena|merujuk|sesuai|artinya|makna|kata|terdapat|disebutkan|dijelaskan|adalah|jadi|oleh|opsi|pilihan|jawabannya|penjelasan|Pancasila|UUD|NKRI|Bhinneka|Nasionalisme|Integritas|Negara|TIU|TWK|TKP)\b/i; -/** Strip exam-script characters to analyze Indonesian prose separately. */ +/** Strip foreign characters (heuristics) if needed, but for CPNS we mostly deal with Latin script. */ export function stripExamScriptForAnalysis(text: string): string { - return text - .replace(/[\u4E00-\u9FFF\u3400-\u4DBF\u3040-\u309F\u30A0-\u30FF\uAC00-\uD7AF\u0600-\u06FF]+/g, " ") - .replace(/\s+/g, " ") - .trim(); + return text.trim(); } -/** Latin prose looks like Bahasa Indonesia (not English-only or empty). */ +/** Latin prose looks like Bahasa Indonesia. */ export function hasIndonesianProse(text: string): boolean { const t = text.trim(); if (!t) return false; if (INDONESIAN_PROSE_MARKERS.test(t)) return true; - if (/^(The|This|That|According|Because|Therefore|Option|Answer|It is|In the)\b/i.test(t)) { - return false; - } return t.length >= 15; } -export function hasCJKScript(text: string): boolean { - return /[\u4E00-\u9FFF\u3400-\u4DBF\u3040-\u309F\u30A0-\u30FF\uAC00-\uD7AF]/.test(text); -} - -export function hasArabicScript(text: string): boolean { - return /[\u0600-\u06FF]/.test(text); -} - -function hasTargetScript(text: string, examType: string): boolean { - if (examType === "TOAFL") return hasArabicScript(text); - if (EXAMS_REQUIRING_NON_LATIN_SCRIPT.has(examType)) return hasCJKScript(text); - return true; -} - -/** Heuristic: Latin text that looks like Indonesian exam instructions, not English. */ -export function isLikelyIndonesianContent(text: string): boolean { - const t = text.trim(); - if (!t) return false; - if (hasCJKScript(t) || hasArabicScript(t)) return false; - return INDONESIAN_PROSE_MARKERS.test(t); -} - -export function buildContentLanguageRules(examType: string): string { - const target = getTargetLanguage(examType); +export function buildContentLanguageRules(_examType: string): string { return `- LANGUAGE RULE (critical): - * passageText, questionText, and every option "text" MUST be written in ${target} — the authentic exam language. - * NEVER write questionText or options in Bahasa Indonesia for ${examType}. - * explanation: tulis dalam Bahasa Indonesia; boleh sisipkan istilah/kanji/kata ${target} bila perlu (mis. 「環境」 atau kutipan singkat dari teks).`; + * passageText, questionText, and every option "text" MUST be written in Bahasa Indonesia — the authentic exam language for CPNS. + * explanation: tulis dalam Bahasa Indonesia yang mendalam dan mudah dimengerti.`; } -export function buildExplanationLanguageRule(examType?: string): string { - const target = examType ? getTargetLanguage(examType) : "bahasa ujian"; - return `- explanation — WAJIB ditulis dalam Bahasa Indonesia sebagai penjelasan utama. Boleh menyertakan istilah, kanji, atau kutipan singkat dalam ${target} jika relevan (mis. arti kanji, padanan kata). Jangan tulis seluruh explanation hanya dalam ${target} atau Inggris.`; +export function buildExplanationLanguageRule(_examType?: string): string { + return `- explanation — WAJIB ditulis dalam Bahasa Indonesia sebagai penjelasan utama.`; } export function getQuestionLanguageErrors( q: QuestionLanguageFields, - examType: string, + _examType: string, ): string[] { const errors: string[] = []; - const target = getTargetLanguage(examType); - - if (isLikelyIndonesianContent(q.questionText)) { - errors.push(`questionText must be ${target}, not Bahasa Indonesia`); - } - - if (EXAMS_REQUIRING_NON_LATIN_SCRIPT.has(examType) && !hasTargetScript(q.questionText, examType)) { - errors.push(`questionText must be written in ${target}`); - } - - if (examType === "IELTS" || examType === "TOEFL") { - if (isLikelyIndonesianContent(q.questionText)) { - errors.push("questionText must be English for IELTS/TOEFL"); - } + if (!hasIndonesianProse(q.questionText)) { + errors.push("questionText must be in Bahasa Indonesia"); } - for (const opt of q.options ?? []) { - if (isLikelyIndonesianContent(opt.text)) { - errors.push(`option ${opt.key} must be ${target}, not Bahasa Indonesia`); - } - if (EXAMS_REQUIRING_NON_LATIN_SCRIPT.has(examType) && !hasTargetScript(opt.text, examType)) { - errors.push(`option ${opt.key} must be written in ${target}`); + if (!hasIndonesianProse(opt.text) && opt.text.length > 5) { + errors.push(`option ${opt.key} must be in Bahasa Indonesia`); } } - return errors; } diff --git a/packages/ai/src/prompts.ts b/packages/ai/src/prompts.ts index 5fa91be76e793caffdfc90d934705e9b7aefbf23..ddcc90308f9cf83fb5aee5af3fd3b93854226ca8 100644 --- a/packages/ai/src/prompts.ts +++ b/packages/ai/src/prompts.ts @@ -10,40 +10,46 @@ export function buildQuickModePrompt(input: GenerationInput): string { const questionSchemaJson = getQuestionJsonSchemaDescription(); - return `You are an expert exam question writer for ${examType} ${section.toLowerCase()} section. + return `You are an expert exam question writer for CPNS (Seleksi Calon Pegawai Negeri Sipil) specifically for the ${section} section. -Generate ${questionCount} authentic, high-quality reading comprehension questions. +Generate ${questionCount} authentic, high-quality questions for CPNS. -EXAM: ${examType} +EXAM: CPNS SECTION: ${section} DIFFICULTY: ${difficulty}/5 TOPICS: ${topics.join(", ")} FORMATS TO GENERATE: ${formats.join(", ")} +CONTEXT FOR SECTIONS: +- TWK (Tes Wawasan Kebangsaan): Fokus pada Pancasila, UUD 1945, NKRI, Bhinneka Tunggal Ika, Nasionalisme, Integritas, Bela Negara, Pilar Negara, dan Bahasa Indonesia. +- TIU (Tes Intelegensia Umum): Fokus pada Verbal (Analogi, Silogisme, Analitis), Numerik (Deret Angka, Perbandingan Kuantitatif, Soal Cerita), dan Figural. +- TKP (Tes Karakteristik Pribadi): Fokus pada Pelayanan Publik, Jejaring Kerja, Sosial Budaya, TIK, Profesionalisme, dan Anti Radikalisme. +- Teknis (SKB Teknis Umum): Soal teknis sesuai jabatan yang dilamar. +- Pertanahan_Dasar / Pertanahan & Agraria: Hukum Agraria (UUPA No. 5/1960), jenis-jenis hak atas tanah (HM, HGU, HGB, HP, HPL), konversi hak, tanah negara, tanah adat, reforma agraria, regulasi BPN/ATR. +- Pengukuran_Kadastral / Pengukuran & Pemetaan Kadastral: Ilmu ukur tanah, pengukuran batas bidang, peta kadastral, sistem koordinat TM3, GPS/GNSS, drone, fotogrametri, Kadaster, KKP, BHUMI, Peta ZNT. +- Pendaftaran_Tanah / Pendaftaran Tanah & Sertipikat: PP No. 24/1997, PTSL, buku tanah, surat ukur, sertipikat, pemeliharaan data, balik nama, pemecahan/penggabungan, sertipikat elektronik, hak tanggungan. +- Pengadaan_Tanah / Pengadaan Tanah & Ganti Rugi: UU No. 2/2012, Perpres No. 71/2012, tahapan pengadaan, penetapan lokasi, inventarisasi, penilaian ganti rugi, musyawarah, konsignasi, PSN. +- Pengendalian_Pertanahan / Pengendalian & Penertiban Tanah: Tanah terlantar (PP No. 20/2021), penertiban absentee, monitoring penggunaan tanah, alih fungsi lahan, sengketa-konflik-perkara pertanahan, mediasi. +- Penilaian_Tanah / Penilaian Tanah & Properti: Zona Nilai Tanah (ZNT), NIR, metode penilaian tanah (perbandingan pasar/biaya/pendapatan), penilai publik, NJOP, faktor nilai tanah. + INSTRUCTIONS: ${buildContentLanguageRules(examType)} ${buildExplanationLanguageRule(examType)} -- For Korean (TOPIK): Focus on particles, honorifics (speech levels), and functional grammar. -- For Arabic (TOAFL): Support RTL text. Focus on I'rab (case endings/vowel changes) and grammar. -- For Spanish (DELE): Focus on verb conjugation by subject and agreement. -- Passage length should be appropriate for the exam type and difficulty. +- Passage length should be appropriate for the difficulty. - Each question must have: - * a reading passage (passageText) - * a clear question prompt (questionText) — in exam language, NOT Bahasa Indonesia + * a reading passage or context (passageText) — Wajib Bahasa Indonesia + * a clear question prompt (questionText) — Wajib Bahasa Indonesia * a correct answer (correctAnswer) - * an explanation (explanation) — Bahasa Indonesia, boleh sisipkan istilah/kanji/kata ujian bila perlu + * a detailed explanation (explanation) — Wajib Bahasa Indonesia * difficulty level (${difficulty}) * relevant skill tags (skillTags) -- For JLPT/TOPIK kanji/hanja: Include reading annotations in format: 漢字(かんじ) for words that have readings. -- Questions should test real comprehension, not just surface-level recall. -- For multiple choice: always provide 4 options labeled A, B, C, D. +- For multiple choice: always provide 5 options labeled A, B, C, D, E (standard for CPNS). - Options must be plausible distractors — one clearly correct answer. ${OPTION_QUALITY_RULES} - For matching_pairs: Provide options as an array of {key, text} where key is the left item identifier and text is the left item. correctAnswer should be a serialized mapping like "A:1,B:2,C:3" matching each left key to its right pair. -- For error_recognition: options are error segments (A, B, C, D) and correctAnswer is the key of the segment containing an error. -- For text_insertion: options are position markers (A, B, C, D) within the passage where a sentence could be inserted. correctAnswer is the best position key. +- For error_recognition: options are error segments (A, B, C, D, E) and correctAnswer is the key of the segment containing an error. +- For text_insertion: options are position markers (A, B, C, D, E) within the passage where a sentence could be inserted. - For sentence_arrangement: Provide options as shuffled fragments in random order. correctAnswer is the correct order as comma-separated keys (e.g. "D,A,C,B"). -- For matching_information: options are information items with key and text. correctAnswer is the correct match as serialized mapping. OUTPUT FORMAT: Return ONLY a valid JSON object with this exact structure (no markdown code blocks, no extra text): @@ -55,6 +61,6 @@ ${questionSchemaJson.split("\n").map((l) => " " + l).join("\n")} ] } -You may reuse the same passage for multiple questions, or generate a new passage per question — whichever is more natural for the exam type. +You may reuse the same passage for multiple questions, or generate a new passage per question. `; } diff --git a/packages/ai/src/schemas.ts b/packages/ai/src/schemas.ts index 70c2feebb648668977b3ba3e769793749c15c332..159ea276dd997defc973dacd9c16348cf24dd643 100644 --- a/packages/ai/src/schemas.ts +++ b/packages/ai/src/schemas.ts @@ -2,8 +2,8 @@ import { z } from "zod"; // ── Shared Schemas ───────────────────────────────────────── -export const examTypeSchema = z.enum(["IELTS", "TOEFL", "JLPT", "HSK", "GOETHE", "TOPIK", "TOAFL", "DELE"]); -export const sectionTypeSchema = z.enum(["READING", "WRITING", "LISTENING", "SPEAKING"]); +export const examTypeSchema = z.enum(["CPNS", "SKB"]); +export const sectionTypeSchema = z.enum(["TWK", "TIU", "TKP", "Teknis"]); export const questionFormatSchema = z.enum([ "multiple_choice", diff --git a/packages/api/LICENSE b/packages/api/LICENSE index 1fc95992a2e8ad86439ad2a53a3ad137e2c52dc2..566070fb89d97a3937c0d03275e52536067e9e44 100644 --- a/packages/api/LICENSE +++ b/packages/api/LICENSE @@ -1,6 +1,6 @@ -MIT License +MIT License -Copyright (c) 2026 Labas Project Authors +Copyright (c) 2026 Pram Project Authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/packages/api/package.json b/packages/api/package.json index 934a2929fb325771d7c54c8c6517e565adfce7e2..ad716c07347a25a6054e66022b9595d3e5146edd 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,5 +1,5 @@ { - "name": "@labas/api", + "name": "@pram/api", "license": "MIT", "type": "module", "exports": { @@ -14,10 +14,10 @@ "test": "bun test" }, "dependencies": { - "@labas/ai": "workspace:*", - "@labas/auth": "workspace:*", - "@labas/db": "workspace:*", - "@labas/env": "workspace:*", + "@pram/ai": "workspace:*", + "@pram/auth": "workspace:*", + "@pram/db": "workspace:*", + "@pram/env": "workspace:*", "@trpc/client": "catalog:", "@trpc/server": "catalog:", "bcryptjs": "^3.0.3", @@ -31,7 +31,7 @@ }, "devDependencies": { "@electric-sql/pglite": "^0.4.5", - "@labas/config": "workspace:*", + "@pram/config": "workspace:*", "@types/bcryptjs": "^3.0.0", "@types/nodemailer": "^8.0.0", "hono": "catalog:", diff --git a/packages/api/src/__tests__/admin.integration.test.ts b/packages/api/src/__tests__/admin.integration.test.ts index b1f775d836a6a5d191c6465525a6c3bed7b625da..a5aaf84c2b075077418c7c64703002e3e074531f 100644 --- a/packages/api/src/__tests__/admin.integration.test.ts +++ b/packages/api/src/__tests__/admin.integration.test.ts @@ -21,7 +21,7 @@ const MOCK_ENV = { DEFAULT_SIGNUP_CREDIT_TOKENS: "50000", }; -mock.module("@labas/env/server", () => ({ +mock.module("@pram/env/server", () => ({ env: MOCK_ENV, })); @@ -40,7 +40,7 @@ describe("Admin Integration", () => { const pg = await getTestPGlite(); db = drizzle(pg, { schema }); - mock.module("@labas/db", () => ({ + mock.module("@pram/db", () => ({ db, ...schema, })); diff --git a/packages/api/src/__tests__/attempt.integration.test.ts b/packages/api/src/__tests__/attempt.integration.test.ts index 96dad3cb48ee0450040b4d570ce57a4372d8b51f..fbcab3e15035e94119bae445cb47a92327a64aba 100644 --- a/packages/api/src/__tests__/attempt.integration.test.ts +++ b/packages/api/src/__tests__/attempt.integration.test.ts @@ -3,7 +3,7 @@ import { drizzle } from "drizzle-orm/pglite"; import * as schema from "../../../db/src/schema"; import { closeTestPGlite, getTestPGlite } from "./test-setup"; -mock.module("@labas/env/server", () => ({ +mock.module("@pram/env/server", () => ({ env: { DATABASE_URL: "postgres://localhost:5432/test", BETTER_AUTH_SECRET: "a".repeat(32), @@ -41,7 +41,7 @@ describe("attempt router", () => { const pg = await getTestPGlite(); testDb = drizzle(pg, { schema }); - mock.module("@labas/db", () => ({ + mock.module("@pram/db", () => ({ __esModule: true, ...schema, db: testDb, diff --git a/packages/api/src/__tests__/credit.test.ts b/packages/api/src/__tests__/credit.test.ts index d822b0f66fab9af36af6c0f0cdd205a62445309a..3fc2c8e684ed66f2c954987c89de77f0be45b809 100644 --- a/packages/api/src/__tests__/credit.test.ts +++ b/packages/api/src/__tests__/credit.test.ts @@ -24,7 +24,7 @@ const MOCK_ENV = { FREE_CREDITS_MAX_POOL: 1000000, }; -mock.module("@labas/env/server", () => ({ +mock.module("@pram/env/server", () => ({ env: MOCK_ENV, })); @@ -44,7 +44,7 @@ describe("Credit Utility", () => { const pg = await getTestPGlite(); db = drizzle(pg, { schema }); - mock.module("@labas/db", () => ({ + mock.module("@pram/db", () => ({ db, ...schema, })); diff --git a/packages/api/src/__tests__/package.integration.test.ts b/packages/api/src/__tests__/package.integration.test.ts index 4a31e55d6ed72bf1e1c76035e3ee53fce232d702..87f9dde017b41e39e9d251cf6b0086c999b11ba1 100644 --- a/packages/api/src/__tests__/package.integration.test.ts +++ b/packages/api/src/__tests__/package.integration.test.ts @@ -4,7 +4,7 @@ import { drizzle } from "drizzle-orm/pglite"; import * as schema from "../../../db/src/schema"; import { closeTestPGlite, getTestPGlite } from "./test-setup"; -mock.module("@labas/env/server", () => ({ +mock.module("@pram/env/server", () => ({ env: { DATABASE_URL: "postgres://localhost:5432/test", BETTER_AUTH_SECRET: "a".repeat(32), @@ -33,7 +33,7 @@ describe("package router", () => { const pg = await getTestPGlite(); testDb = drizzle(pg, { schema }); - mock.module("@labas/db", () => ({ + mock.module("@pram/db", () => ({ __esModule: true, ...schema, db: testDb, diff --git a/packages/api/src/__tests__/question.integration.test.ts b/packages/api/src/__tests__/question.integration.test.ts index acaae0022d593e1ae78dcdbc928ecbb07c9d90aa..af69473e036dea1bdcdbf9924e9012c9df59a838 100644 --- a/packages/api/src/__tests__/question.integration.test.ts +++ b/packages/api/src/__tests__/question.integration.test.ts @@ -3,7 +3,7 @@ import { drizzle } from "drizzle-orm/pglite"; import * as schema from "../../../db/src/schema"; import { closeTestPGlite, getTestPGlite, createTestUserData } from "./test-setup"; -mock.module("@labas/env/server", () => ({ +mock.module("@pram/env/server", () => ({ env: { DATABASE_URL: "postgres://localhost:5432/test", BETTER_AUTH_SECRET: "a".repeat(32), @@ -26,7 +26,7 @@ describe("question router", () => { const pg = await getTestPGlite(); const testDb = drizzle(pg, { schema }); - mock.module("@labas/db", () => ({ + mock.module("@pram/db", () => ({ __esModule: true, ...schema, db: testDb, diff --git a/packages/api/src/__tests__/test-db.ts b/packages/api/src/__tests__/test-db.ts index c7f74e782f7b9503b0fdd2c178f5ecb376e057f2..d92448ab6087535c1bed4849d7a5fa192750906e 100644 --- a/packages/api/src/__tests__/test-db.ts +++ b/packages/api/src/__tests__/test-db.ts @@ -1,6 +1,6 @@ import { PGlite } from "@electric-sql/pglite"; import { drizzle } from "drizzle-orm/pglite"; -import * as schema from "@labas/db"; +import * as schema from "@pram/db"; let _client: PGlite | null = null; diff --git a/packages/api/src/__tests__/verification.integration.test.ts b/packages/api/src/__tests__/verification.integration.test.ts index a4d337c8d0804b9868a3d6f5db820ee426cf11da..1e9679affe358340f2e1c37eafac1d61000a527d 100644 --- a/packages/api/src/__tests__/verification.integration.test.ts +++ b/packages/api/src/__tests__/verification.integration.test.ts @@ -6,7 +6,7 @@ import { getTestPGlite, closeTestPGlite } from "./test-setup"; // ── Mocks (must be declared before dynamic imports) ────────────────────────── -mock.module("@labas/env/server", () => ({ +mock.module("@pram/env/server", () => ({ env: { DATABASE_URL: "postgres://localhost:5432/test", BETTER_AUTH_SECRET: "a".repeat(32), @@ -57,7 +57,7 @@ describe("verification router", () => { const pg = await getTestPGlite(); db = drizzle(pg, { schema }); - mock.module("@labas/db", () => ({ + mock.module("@pram/db", () => ({ __esModule: true, ...schema, db, diff --git a/packages/api/src/context.ts b/packages/api/src/context.ts index 1f14d63a8f9cf66708406ff01a626b4becc9fa19..bf8126eb55f8dcd6eb7670e0871440c1785e2902 100644 --- a/packages/api/src/context.ts +++ b/packages/api/src/context.ts @@ -1,4 +1,4 @@ -import { auth } from "@labas/auth"; +import { auth } from "@pram/auth"; import type { Context as HonoContext } from "hono"; import { getClientIp } from "./lib/client-ip"; diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 8e63ab021072b7da63977a5480643a59f755b0e7..305290ebf2e4f73cb235bd24e6e6433dca134219 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -2,8 +2,8 @@ import { initTRPC, TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; import { logger } from "./logger"; import type { Context } from "./context"; -import { db } from "@labas/db"; -import { user } from "@labas/db"; +import { db } from "@pram/db"; +import { user } from "@pram/db"; export const t = initTRPC.context().create(); diff --git a/packages/api/src/lib/credit.ts b/packages/api/src/lib/credit.ts index 9eb7e0447fa89f0e077cd56545dc3221b6b8273d..0681744769550891daff2261b4b31fbea8b1c5ac 100644 --- a/packages/api/src/lib/credit.ts +++ b/packages/api/src/lib/credit.ts @@ -1,8 +1,8 @@ import { eq, sql, and, desc, sum, gte } from "drizzle-orm"; -import { db } from "@labas/db"; -import { userCredit, creditTransaction, user, platformConfig } from "@labas/db"; +import { db } from "@pram/db"; +import { userCredit, creditTransaction, user, platformConfig } from "@pram/db"; import { TRPCError } from "@trpc/server"; -import { env } from "@labas/env/server"; +import { env } from "@pram/env/server"; const COOLDOWN_DAYS = 7; diff --git a/packages/api/src/lib/email.ts b/packages/api/src/lib/email.ts index a60d73dace7465b233ee981ff6ee717b374fada9..dd096ec9ea37d88268a022c2dd0451104b452218 100644 --- a/packages/api/src/lib/email.ts +++ b/packages/api/src/lib/email.ts @@ -1,20 +1,31 @@ -import { env } from "@labas/env/server"; +import { env } from "@pram/env/server"; import nodemailer from "nodemailer"; +const smtpConfigured = + env.SMTP_HOST && env.SMTP_USER && env.SMTP_PASS && env.SMTP_FROM; + +if (!smtpConfigured) { + console.warn( + "[email] SMTP not configured — email features (verification, password reset) will be disabled. Set SMTP_HOST, SMTP_USER, SMTP_PASS, SMTP_FROM in apps/server/.env to enable." + ); +} + const smtpPort = Number(env.SMTP_PORT) || 587; -const transporter = nodemailer.createTransport({ - host: env.SMTP_HOST, - port: smtpPort, - secure: smtpPort === 465, - auth: { - user: env.SMTP_USER, - pass: env.SMTP_PASS, - }, -}); +const transporter = smtpConfigured + ? nodemailer.createTransport({ + host: env.SMTP_HOST, + port: smtpPort, + secure: smtpPort === 465, + auth: { + user: env.SMTP_USER, + pass: env.SMTP_PASS, + }, + }) + : null; const subjects: Record = { - "email-verification": "Verifikasi Email Labas", - "forget-password": "Reset Password Labas", + "email-verification": "Verifikasi Email Pram", + "forget-password": "Reset Password Pram", }; const messages: Record = { @@ -29,7 +40,12 @@ type SendOtpEmailProps = { }; export async function sendOtpEmail({ to, otp, type }: SendOtpEmailProps) { - const subject = subjects[type] || "Kode Verifikasi Labas"; + if (!transporter) { + console.warn(`[email] Skipping OTP email to ${to} — SMTP not configured.`); + return; + } + + const subject = subjects[type] || "Kode Verifikasi Pram"; const message = messages[type] || "Kode verifikasi Anda:"; await transporter.sendMail({ @@ -38,7 +54,7 @@ export async function sendOtpEmail({ to, otp, type }: SendOtpEmailProps) { subject, html: ` - Labas + Pram ${message} ${otp} diff --git a/packages/api/src/lib/generation-outcome.ts b/packages/api/src/lib/generation-outcome.ts index 6ac04566da9185981668e18eaa37564e4d401b40..9e419e718f5e5c44b4fd1019413eedeedfecc522 100644 --- a/packages/api/src/lib/generation-outcome.ts +++ b/packages/api/src/lib/generation-outcome.ts @@ -43,7 +43,7 @@ export function formatGenerationErrorMessage(input: { lower.includes("") || /<\/?think>/.test(lower) ) { - return `Model reasoning tidak kompatibel dengan parser Labas.${hint} Coba non-reasoning model, atau update server ke versi terbaru.`; + return `Model reasoning tidak kompatibel dengan parser Pram.${hint} Coba non-reasoning model, atau update server ke versi terbaru.`; } if ( diff --git a/packages/api/src/lib/rate-limit.ts b/packages/api/src/lib/rate-limit.ts index 8c72b627047e5d586a89d0d02184deb86568ced5..b5b65e67e343587a55562c311b2e9e8b890abc1e 100644 --- a/packages/api/src/lib/rate-limit.ts +++ b/packages/api/src/lib/rate-limit.ts @@ -1,7 +1,7 @@ import IORedis from "ioredis"; -import { env } from "@labas/env/server"; +import { env } from "@pram/env/server"; import { TRPCError } from "@trpc/server"; -import { logger } from "@labas/api/logger"; +import { logger } from "@pram/api/logger"; let redis: IORedis | null = null; diff --git a/packages/api/src/lib/turnstile.ts b/packages/api/src/lib/turnstile.ts index 1a98b690bc2ee9e398ef7b051423a7a378238106..2bb718c10346b5323111f67fd98b98d014e723b6 100644 --- a/packages/api/src/lib/turnstile.ts +++ b/packages/api/src/lib/turnstile.ts @@ -1,6 +1,6 @@ import { TRPCError } from "@trpc/server"; -import { env } from "@labas/env/server"; -import { logger } from "@labas/api/logger"; +import { env } from "@pram/env/server"; +import { logger } from "@pram/api/logger"; /** * Validates a Cloudflare Turnstile token server-side. diff --git a/packages/api/src/logger.ts b/packages/api/src/logger.ts index 92261877522e37f33a831982bbb0d0024acc169e..65d81209384e8b839690efc66f537edc64a7a120 100644 --- a/packages/api/src/logger.ts +++ b/packages/api/src/logger.ts @@ -22,14 +22,14 @@ function createTransports() { if (isProduction) { transports.push( new winston.transports.File({ - filename: "logs/labas-error.log", + filename: "logs/pram-error.log", level: "error", format: combine(timestamp(), errors({ stack: true }), json()), maxsize: 10 * 1024 * 1024, maxFiles: 14, }), new winston.transports.File({ - filename: "logs/labas-combined.log", + filename: "logs/pram-combined.log", format: combine(timestamp(), errors({ stack: true }), json()), maxsize: 10 * 1024 * 1024, maxFiles: 7, @@ -50,7 +50,7 @@ function createTransports() { export const logger = winston.createLogger({ level: process.env.LOG_LEVEL ?? "info", - defaultMeta: { service: "labas-api" }, + defaultMeta: { service: "pram-api" }, transports: createTransports(), }); diff --git a/packages/api/src/queue.ts b/packages/api/src/queue.ts index 693f410b70715de1ee5ffd067d59823eb22fa69c..3d6f059e8bb609117d7ed9c9332dfd06a327af10 100644 --- a/packages/api/src/queue.ts +++ b/packages/api/src/queue.ts @@ -1,9 +1,9 @@ import { Queue, Worker, type Job } from "bullmq"; import IORedis from "ioredis"; -import { env } from "@labas/env/server"; +import { env } from "@pram/env/server"; async function log(level: "debug" | "warn", message: string, meta?: Record) { - const mod = await import("@labas/api/logger"); + const mod = await import("@pram/api/logger"); (mod.logger as any)[level](message, meta); } import { @@ -15,15 +15,15 @@ import { type AgenticProgress, type GenerationInput, type GenerationResult, -} from "@labas/ai"; -import { db } from "@labas/db"; +} from "@pram/ai"; +import { db } from "@pram/db"; import { generationJob, question, testPackage, packageSection, sectionQuestion, -} from "@labas/db"; +} from "@pram/db"; import { and, eq, notInArray } from "drizzle-orm"; import { encryptApiKey, decryptApiKey } from "./lib/encryption"; import { @@ -489,7 +489,7 @@ async function completeJobWithResult(params: { .where(eq(generationJob.id, params.jobId)) .limit(1); if (jobRow) { - const { deductCredit } = await import("@labas/api/lib/credit"); + const { deductCredit } = await import("@pram/api/lib/credit"); await deductCredit(jobRow.userId, params.totalTokens).catch(() => {}); } } diff --git a/packages/api/src/routers/admin.ts b/packages/api/src/routers/admin.ts index 6d83b832741f436237bed31160b044e6494a33da..4b3d8650bfeafaff750f48d2f3f19f1b35b84f4b 100644 --- a/packages/api/src/routers/admin.ts +++ b/packages/api/src/routers/admin.ts @@ -1,12 +1,12 @@ import { z } from "zod"; import { eq, desc, sql, count, ilike, or } from "drizzle-orm"; import { adminProcedure, protectedProcedure, router } from "../index"; -import * as schema from "@labas/db"; -import { db } from "@labas/db"; +import * as schema from "@pram/db"; +import { db } from "@pram/db"; import { paginationSchema, paginateDefaults } from "../lib/pagination"; import { throwNotFound, throwForbidden, throwBadRequest } from "../lib/errors"; import { getUserCredit, getLastRefillAt, getPoolUsage, getConfig, setConfig, autoRefillIfEligible } from "../lib/credit"; -import { env } from "@labas/env/server"; +import { env } from "@pram/env/server"; function audit(adminUserId: string, action: string, targetUserId: string | null, details?: Record) { return db.insert(schema.adminAuditLog).values({ diff --git a/packages/api/src/routers/ai.ts b/packages/api/src/routers/ai.ts index 822579c14f58b57d2b7eabef515a3c8e862c0498..9f193d5ac5e254faaf0a1ffc2171f9352f714301 100644 --- a/packages/api/src/routers/ai.ts +++ b/packages/api/src/routers/ai.ts @@ -1,16 +1,16 @@ import { z } from "zod"; import { eq, desc, and, gte, sql } from "drizzle-orm"; import { router, protectedProcedure } from "../index"; -import { type GenerationInput, generationInputSchema } from "@labas/ai/schemas"; +import { type GenerationInput, generationInputSchema } from "@pram/ai/schemas"; import { cancelGenerationJob, enqueueGeneration } from "../queue"; -import { db } from "@labas/db"; -import { question, generationJob } from "@labas/db"; +import { db } from "@pram/db"; +import { question, generationJob } from "@pram/db"; import { paginationSchema, paginateDefaults } from "../lib/pagination"; import { throwNotFound, throwForbidden, throwBadRequest } from "../lib/errors"; import { checkDailyBudget } from "../lib/rate-limit"; import { decryptApiKey } from "../lib/encryption"; import { isUserSuspended, getUserCredit, autoRefillIfEligible } from "../lib/credit"; -import { env } from "@labas/env/server"; +import { env } from "@pram/env/server"; const DAILY_TOKEN_BUDGET = 500_000; diff --git a/packages/api/src/routers/attempt.ts b/packages/api/src/routers/attempt.ts index 763f7c253e55335ea32a59cc8bb233ca6caa3e5a..05edc79542f495317fb12e39451e8a496ab595ee 100644 --- a/packages/api/src/routers/attempt.ts +++ b/packages/api/src/routers/attempt.ts @@ -1,7 +1,7 @@ import { z } from "zod"; import { eq, and, desc, sql, inArray, ilike } from "drizzle-orm"; import { router, protectedProcedure } from "../index"; -import { db } from "@labas/db"; +import { db } from "@pram/db"; import { testAttempt, sectionResult, @@ -11,7 +11,7 @@ import { sectionQuestion, question, examType, -} from "@labas/db"; +} from "@pram/db"; import { paginationSchema, paginateDefaults } from "../lib/pagination"; import { assertOwnership } from "../lib/ownership"; import { throwNotFound, throwForbidden, throwBadRequest } from "../lib/errors"; diff --git a/packages/api/src/routers/combo.ts b/packages/api/src/routers/combo.ts index fdccbdd8b1f17e243633a7fe156c51b0eb9cf683..f943c873c219ad4f211f1efdee14d5f1341822c9 100644 --- a/packages/api/src/routers/combo.ts +++ b/packages/api/src/routers/combo.ts @@ -1,7 +1,7 @@ import { z } from "zod"; import { eq, and, desc, sql, inArray } from "drizzle-orm"; import { router, protectedProcedure, publicProcedure } from "../index"; -import { db } from "@labas/db"; +import { db } from "@pram/db"; import { comboPackage, comboSection, @@ -12,7 +12,7 @@ import { examType, sectionType, user, -} from "@labas/db"; +} from "@pram/db"; import { paginationSchema, paginateDefaults } from "../lib/pagination"; import { assertOwnership } from "../lib/ownership"; import { buildVisibilityCondition } from "../lib/visibility"; diff --git a/packages/api/src/routers/feedback.ts b/packages/api/src/routers/feedback.ts index 79d96c7d4824d0653e8dee294fc5caf87a7342fd..9e0d5466b98f676ef14dc22ac40aa391a2bf4fcc 100644 --- a/packages/api/src/routers/feedback.ts +++ b/packages/api/src/routers/feedback.ts @@ -1,8 +1,8 @@ import { z } from "zod"; import { eq, and, sql } from "drizzle-orm"; import { router, protectedProcedure, publicProcedure } from "../index"; -import { db } from "@labas/db"; -import { questionFeedback } from "@labas/db"; +import { db } from "@pram/db"; +import { questionFeedback } from "@pram/db"; import { checkRateLimit } from "../lib/rate-limit"; export const feedbackRouter = router({ diff --git a/packages/api/src/routers/leaderboard.ts b/packages/api/src/routers/leaderboard.ts index 058d47d0ac1e7d8f81f345fd2f3ae843f82bb65c..f41c261b2273691776620655ad16b39221d04b6d 100644 --- a/packages/api/src/routers/leaderboard.ts +++ b/packages/api/src/routers/leaderboard.ts @@ -1,8 +1,8 @@ import { z } from "zod"; import { eq, and, sql, gte, desc } from "drizzle-orm"; import { router, protectedProcedure, publicProcedure } from "../index"; -import { db } from "@labas/db"; -import { testAttempt, testPackage, user } from "@labas/db"; +import { db } from "@pram/db"; +import { testAttempt, testPackage, user } from "@pram/db"; import { paginationSchema, paginateDefaults } from "../lib/pagination"; const periodSchema = z.enum(["today", "week", "month", "all"]); diff --git a/packages/api/src/routers/package.ts b/packages/api/src/routers/package.ts index 6b99ad0f8c12e956a57cb388dd0235bb4c3e04df..fe047ae2f0a665e06bff952f226152f3bf55afbc 100644 --- a/packages/api/src/routers/package.ts +++ b/packages/api/src/routers/package.ts @@ -1,7 +1,7 @@ import { z } from "zod"; import { eq, and, desc, sql, inArray } from "drizzle-orm"; import { router, protectedProcedure, publicProcedure } from "../index"; -import { db } from "@labas/db"; +import { db } from "@pram/db"; import { testPackage, packageSection, @@ -10,7 +10,7 @@ import { examType, sectionType, user, -} from "@labas/db"; +} from "@pram/db"; import { paginationSchema, paginateDefaults } from "../lib/pagination"; import { assertOwnership } from "../lib/ownership"; import { buildVisibilityCondition } from "../lib/visibility"; diff --git a/packages/api/src/routers/profile.ts b/packages/api/src/routers/profile.ts index be2527cd48d6f56d3f69433cf0aa2cf7cca626cc..b8ed328b2ac009c3ed3cee150e19e84128edc3d5 100644 --- a/packages/api/src/routers/profile.ts +++ b/packages/api/src/routers/profile.ts @@ -1,8 +1,8 @@ import { z } from "zod"; import { eq, and, desc, sql } from "drizzle-orm"; import { router, protectedProcedure, publicProcedure } from "../index"; -import { db } from "@labas/db"; -import { testPackage, question, examType, user } from "@labas/db"; +import { db } from "@pram/db"; +import { testPackage, question, examType, user } from "@pram/db"; import { throwBadRequest } from "../lib/errors"; export const profileRouter = router({ diff --git a/packages/api/src/routers/question.ts b/packages/api/src/routers/question.ts index f9eb1634a3954b0ce63b2413e881a08914e53e37..b6e027d8b8d60cd1badca3e569c653c0567a1656 100644 --- a/packages/api/src/routers/question.ts +++ b/packages/api/src/routers/question.ts @@ -1,8 +1,8 @@ import { z } from "zod"; import { eq, and, desc, sql, like, or, inArray } from "drizzle-orm"; import { router, protectedProcedure, publicProcedure } from "../index"; -import { db } from "@labas/db"; -import { question, examType, sectionType, user } from "@labas/db"; +import { db } from "@pram/db"; +import { question, examType, sectionType, user } from "@pram/db"; import { paginationSchema, paginateDefaults } from "../lib/pagination"; import { buildVisibilityCondition } from "../lib/visibility"; import { assertOwnership } from "../lib/ownership"; diff --git a/packages/api/src/routers/rating.ts b/packages/api/src/routers/rating.ts index 990dcf40f5b2daaa7e3019a6bb07acd132d2b303..3367618cb62bd519d7d0e8cf80cb94a267f833e4 100644 --- a/packages/api/src/routers/rating.ts +++ b/packages/api/src/routers/rating.ts @@ -1,8 +1,8 @@ import { z } from "zod"; import { eq, and, avg } from "drizzle-orm"; import { router, protectedProcedure, publicProcedure } from "../index"; -import { db } from "@labas/db"; -import { questionRating, question, packageRating, testPackage } from "@labas/db"; +import { db } from "@pram/db"; +import { questionRating, question, packageRating, testPackage } from "@pram/db"; import { checkRateLimit } from "../lib/rate-limit"; export const ratingRouter = router({ diff --git a/packages/api/src/routers/stats.ts b/packages/api/src/routers/stats.ts index 4c70cd662c0e226015aad6d3d9a8b587e55139fe..c9d32c873711a8e309207cf78c75e3496ca6bab1 100644 --- a/packages/api/src/routers/stats.ts +++ b/packages/api/src/routers/stats.ts @@ -1,7 +1,7 @@ import { z } from "zod"; import { eq, and, sql, gte } from "drizzle-orm"; import { router, protectedProcedure } from "../index"; -import { db } from "@labas/db"; +import { db } from "@pram/db"; import { testAttempt, sectionResult, @@ -10,7 +10,7 @@ import { question, examType, sectionType, -} from "@labas/db"; +} from "@pram/db"; export const statsRouter = router({ overview: protectedProcedure.query(async ({ ctx }) => { diff --git a/packages/api/src/routers/verification.ts b/packages/api/src/routers/verification.ts index 048ba9a11dab29ffe7aeb1b1d6496ce2224e2e03..a8e796d4896e91444c4e915975f2f5bf790359ac 100644 --- a/packages/api/src/routers/verification.ts +++ b/packages/api/src/routers/verification.ts @@ -1,5 +1,5 @@ -import { db } from "@labas/db"; -import { verification, user, account } from "@labas/db/schema/auth"; +import { db } from "@pram/db"; +import { verification, user, account } from "@pram/db/schema/auth"; import { TRPCError } from "@trpc/server"; import bcrypt from "bcryptjs"; import crypto from "node:crypto"; diff --git a/packages/api/tsconfig.json b/packages/api/tsconfig.json index 8a5e6cfc1939eb5df5ecd8c025a5cb51f457eeb5..028a9503327b544323159b510aee6ef49581936b 100644 --- a/packages/api/tsconfig.json +++ b/packages/api/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "@labas/config/tsconfig.base.json", + "extends": "@pram/config/tsconfig.base.json", "compilerOptions": { "declaration": true, "declarationMap": true, diff --git a/packages/auth/LICENSE b/packages/auth/LICENSE index 1fc95992a2e8ad86439ad2a53a3ad137e2c52dc2..566070fb89d97a3937c0d03275e52536067e9e44 100644 --- a/packages/auth/LICENSE +++ b/packages/auth/LICENSE @@ -1,6 +1,6 @@ -MIT License +MIT License -Copyright (c) 2026 Labas Project Authors +Copyright (c) 2026 Pram Project Authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/packages/auth/package.json b/packages/auth/package.json index 94b1b72fb565e0e3b15a5d72ef3b1b7eb4192766..6dba2dcdb713bc1dc8395d74226d45f0581ed333 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -1,5 +1,5 @@ { - "name": "@labas/auth", + "name": "@pram/auth", "license": "MIT", "type": "module", "exports": { @@ -12,14 +12,14 @@ }, "scripts": {}, "dependencies": { - "@labas/db": "workspace:*", - "@labas/env": "workspace:*", + "@pram/db": "workspace:*", + "@pram/env": "workspace:*", "better-auth": "catalog:", "dotenv": "catalog:", "zod": "catalog:" }, "devDependencies": { - "@labas/config": "workspace:*", + "@pram/config": "workspace:*", "typescript": "catalog:" } } diff --git a/packages/auth/src/index.ts b/packages/auth/src/index.ts index bd0e8ef2ec1ceb88c40f6209bd3b682ff65102dc..138381fcf06f0b22cf5be86422ae32fb6f45b401 100644 --- a/packages/auth/src/index.ts +++ b/packages/auth/src/index.ts @@ -1,6 +1,6 @@ -import { createDb } from "@labas/db"; -import * as schema from "@labas/db/schema/auth"; -import { env } from "@labas/env/server"; +import { createDb } from "@pram/db"; +import * as schema from "@pram/db/schema/auth"; +import { env } from "@pram/env/server"; import { betterAuth } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; diff --git a/packages/auth/tsconfig.json b/packages/auth/tsconfig.json index 8a5e6cfc1939eb5df5ecd8c025a5cb51f457eeb5..028a9503327b544323159b510aee6ef49581936b 100644 --- a/packages/auth/tsconfig.json +++ b/packages/auth/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "@labas/config/tsconfig.base.json", + "extends": "@pram/config/tsconfig.base.json", "compilerOptions": { "declaration": true, "declarationMap": true, diff --git a/packages/config/LICENSE b/packages/config/LICENSE index 1fc95992a2e8ad86439ad2a53a3ad137e2c52dc2..566070fb89d97a3937c0d03275e52536067e9e44 100644 --- a/packages/config/LICENSE +++ b/packages/config/LICENSE @@ -1,6 +1,6 @@ -MIT License +MIT License -Copyright (c) 2026 Labas Project Authors +Copyright (c) 2026 Pram Project Authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/packages/config/package.json b/packages/config/package.json index dad702995a7816f7e73570220c40a5c340b2a8d3..beb396e18b7a97a22a4e9a80c07741a6629a2475 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -1,5 +1,5 @@ { - "name": "@labas/config", + "name": "@pram/config", "version": "0.0.0", "private": true, "license": "MIT" diff --git a/packages/db/LICENSE b/packages/db/LICENSE index 1fc95992a2e8ad86439ad2a53a3ad137e2c52dc2..566070fb89d97a3937c0d03275e52536067e9e44 100644 --- a/packages/db/LICENSE +++ b/packages/db/LICENSE @@ -1,6 +1,6 @@ -MIT License +MIT License -Copyright (c) 2026 Labas Project Authors +Copyright (c) 2026 Pram Project Authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/packages/db/docker-compose.yml b/packages/db/docker-compose.yml index 56a7417beb5f3fb303ed7e8d41c8d1b0dde1e759..df47c7da2a28b19489ff876a4f8814339df579bd 100644 --- a/packages/db/docker-compose.yml +++ b/packages/db/docker-compose.yml @@ -1,17 +1,17 @@ -name: labas +name: pram services: postgres: image: postgres - container_name: labas-postgres + container_name: pram-postgres environment: - POSTGRES_DB: labas + POSTGRES_DB: pram POSTGRES_USER: postgres POSTGRES_PASSWORD: password ports: - "5432:5432" volumes: - - labas_postgres_data:/var/lib/postgresql + - pram_postgres_data:/var/lib/postgresql healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 10s @@ -21,11 +21,11 @@ services: redis: image: redis:7-alpine - container_name: labas-redis + container_name: pram-redis ports: - "6379:6379" volumes: - - labas_redis_data:/data + - pram_redis_data:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s @@ -34,5 +34,5 @@ services: restart: unless-stopped volumes: - labas_postgres_data: - labas_redis_data: + pram_postgres_data: + pram_redis_data: diff --git a/packages/db/package.json b/packages/db/package.json index def4cc588cf9952f8fa377748ebae03da51d27a9..1cd96638ceabcd47596d859ecaabfbf653225615 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -1,5 +1,5 @@ { - "name": "@labas/db", + "name": "@pram/db", "license": "MIT", "type": "module", "exports": { @@ -22,14 +22,14 @@ "db:seed": "bun run src/seed.ts" }, "dependencies": { - "@labas/env": "workspace:*", + "@pram/env": "workspace:*", "dotenv": "catalog:", "drizzle-orm": "^0.45.1", "pg": "^8.17.1", "zod": "catalog:" }, "devDependencies": { - "@labas/config": "workspace:*", + "@pram/config": "workspace:*", "@types/pg": "^8.16.0", "drizzle-kit": "^0.31.8", "typescript": "catalog:" diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts index 231370ad5fbfa84a720ec9f71dd14550cef7fc96..29efd50b15820a7d04fb97d7b62b81aaf70e2ae8 100644 --- a/packages/db/src/index.ts +++ b/packages/db/src/index.ts @@ -1,4 +1,4 @@ -import { env } from "@labas/env/server"; +import { env } from "@pram/env/server"; import { drizzle } from "drizzle-orm/node-postgres"; import * as schema from "./schema"; diff --git a/packages/db/src/seed.ts b/packages/db/src/seed.ts index ec8f12e3048b49e2abf740f1a1281ebf17eee793..fbc2409c2ebab1a87b9d72d79a52b7872aeb72d1 100644 --- a/packages/db/src/seed.ts +++ b/packages/db/src/seed.ts @@ -6,21 +6,13 @@ import { examType, sectionType } from "./schema"; const db = drizzle(loadDatabaseUrl()); const examTypes = [ - { id: "IELTS", name: "IELTS Academic", language: "English", description: "International English Language Testing System" }, - { id: "TOEFL", name: "TOEFL iBT", language: "English", description: "Test of English as a Foreign Language" }, - { id: "JLPT", name: "JLPT", language: "Japanese", description: "Japanese-Language Proficiency Test" }, - { id: "HSK", name: "HSK", language: "Chinese", description: "Hanyu Shuiping Kaoshi" }, - { id: "GOETHE", name: "Goethe-Zertifikat", language: "German", description: "Goethe-Zertifikat German proficiency test" }, - { id: "TOPIK", name: "TOPIK", language: "Korean", description: "Test of Proficiency in Korean" }, - { id: "TOAFL", name: "TOAFL", language: "Arabic", description: "Test of Arabic as a Foreign Language" }, - { id: "DELE", name: "DELE", language: "Spanish", description: "Diplomas de Español como Lengua Extranjera" }, + { id: "CPNS", name: "Ujian CPNS", language: "Bahasa Indonesia", description: "Seleksi Calon Pegawai Negeri Sipil" }, ]; const sectionTypes = [ - { id: "READING", name: "Reading" }, - { id: "WRITING", name: "Writing" }, - { id: "LISTENING", name: "Listening" }, - { id: "SPEAKING", name: "Speaking" }, + { id: "TWK", name: "Tes Wawasan Kebangsaan" }, + { id: "TIU", name: "Tes Intelegensia Umum" }, + { id: "TKP", name: "Tes Karakteristik Pribadi" }, ]; async function seed() { diff --git a/packages/db/tsconfig.json b/packages/db/tsconfig.json index 8a5e6cfc1939eb5df5ecd8c025a5cb51f457eeb5..028a9503327b544323159b510aee6ef49581936b 100644 --- a/packages/db/tsconfig.json +++ b/packages/db/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "@labas/config/tsconfig.base.json", + "extends": "@pram/config/tsconfig.base.json", "compilerOptions": { "declaration": true, "declarationMap": true, diff --git a/packages/env/LICENSE b/packages/env/LICENSE index 1fc95992a2e8ad86439ad2a53a3ad137e2c52dc2..566070fb89d97a3937c0d03275e52536067e9e44 100644 --- a/packages/env/LICENSE +++ b/packages/env/LICENSE @@ -1,6 +1,6 @@ -MIT License +MIT License -Copyright (c) 2026 Labas Project Authors +Copyright (c) 2026 Pram Project Authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/packages/env/package.json b/packages/env/package.json index 2c4110c3a0dbd86667a787c29e4284a6dfa81234..d215eb8bd3a70367bb47143ca02fe9e3cf5f8b00 100644 --- a/packages/env/package.json +++ b/packages/env/package.json @@ -1,5 +1,5 @@ { - "name": "@labas/env", + "name": "@pram/env", "version": "0.0.0", "private": true, "license": "MIT", @@ -14,7 +14,7 @@ "zod": "catalog:" }, "devDependencies": { - "@labas/config": "workspace:*", + "@pram/config": "workspace:*", "@types/bun": "catalog:", "typescript": "catalog:" } diff --git a/packages/env/src/server.ts b/packages/env/src/server.ts index 5520b83061a099c44e3c7a4d462ed31bdc07df6d..ecf217f566ae5922bf1341292406bb8b5e1ae14d 100644 --- a/packages/env/src/server.ts +++ b/packages/env/src/server.ts @@ -11,11 +11,11 @@ export const env = createEnv({ CORS_ORIGIN: z.url(), API_KEY_ENCRYPTION_KEY: z.string().min(32), NODE_ENV: z.enum(["development", "production", "test"]).default("development"), - SMTP_HOST: z.string().min(1), + SMTP_HOST: z.string().min(1).optional(), SMTP_PORT: z.coerce.number().positive().default(587), - SMTP_USER: z.string().min(1), - SMTP_PASS: z.string().min(1), - SMTP_FROM: z.string().min(1), + SMTP_USER: z.string().min(1).optional(), + SMTP_PASS: z.string().min(1).optional(), + SMTP_FROM: z.string().min(1).optional(), PLATFORM_AI_API_KEY: z.string().optional(), PLATFORM_AI_BASE_URL: z.string().optional(), PLATFORM_AI_MODEL: z.string().optional(), diff --git a/packages/env/tsconfig.json b/packages/env/tsconfig.json index 0006edc512a4a24193e25e1d0b9957fc15c25884..1f0817dc7fc219c6d57c99e824f295d2ed3a7ecf 100644 --- a/packages/env/tsconfig.json +++ b/packages/env/tsconfig.json @@ -1,3 +1,3 @@ { - "extends": "@labas/config/tsconfig.base.json" + "extends": "@pram/config/tsconfig.base.json" } diff --git a/packages/ui/LICENSE b/packages/ui/LICENSE index 1fc95992a2e8ad86439ad2a53a3ad137e2c52dc2..566070fb89d97a3937c0d03275e52536067e9e44 100644 --- a/packages/ui/LICENSE +++ b/packages/ui/LICENSE @@ -1,6 +1,6 @@ -MIT License +MIT License -Copyright (c) 2026 Labas Project Authors +Copyright (c) 2026 Pram Project Authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/packages/ui/components.json b/packages/ui/components.json index 0a10405b7632fd990a742c0242b38f9bef85e220..3f3c9d16466dfbc61a1e93be8c93d0df88e2dced 100644 --- a/packages/ui/components.json +++ b/packages/ui/components.json @@ -12,11 +12,11 @@ }, "iconLibrary": "lucide", "aliases": { - "components": "@labas/ui/components", - "utils": "@labas/ui/lib/utils", - "hooks": "@labas/ui/hooks", - "lib": "@labas/ui/lib", - "ui": "@labas/ui/components" + "components": "@pram/ui/components", + "utils": "@pram/ui/lib/utils", + "hooks": "@pram/ui/hooks", + "lib": "@pram/ui/lib", + "ui": "@pram/ui/components" }, "menuColor": "default", "menuAccent": "subtle", diff --git a/packages/ui/package.json b/packages/ui/package.json index 2c819c2188fb12bd04a38d6eac972acf0f54778b..a7c90c7a66b4b63c90af0e11ac75655fa4fcba76 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,5 +1,5 @@ { - "name": "@labas/ui", + "name": "@pram/ui", "version": "0.0.0", "private": true, "license": "MIT", @@ -28,7 +28,7 @@ "tw-animate-css": "^1.3.4" }, "devDependencies": { - "@labas/config": "workspace:*", + "@pram/config": "workspace:*", "@types/react": "^19.2.10", "@types/react-dom": "catalog:", "tailwindcss": "^4.1.18", diff --git a/packages/ui/src/components/button.tsx b/packages/ui/src/components/button.tsx index d46dd1f73297767363bb6bae39ac65ff0b4a8ad7..295a2879683290d257e51681e154dcdd529790c0 100644 --- a/packages/ui/src/components/button.tsx +++ b/packages/ui/src/components/button.tsx @@ -1,7 +1,7 @@ import { Button as ButtonPrimitive } from "@base-ui/react/button" import { cva, type VariantProps } from "class-variance-authority" -import { cn } from "@labas/ui/lib/utils" +import { cn } from "@pram/ui/lib/utils" const buttonVariants = cva( "group/button inline-flex shrink-0 items-center justify-center rounded-none border border-transparent bg-clip-padding text-xs font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", diff --git a/packages/ui/src/components/card.tsx b/packages/ui/src/components/card.tsx index 003db8150aaacd3ed4558e1b06585d512755ee1f..7c2c41e6f3f12dd07eb56e13dfe39da2579c3272 100644 --- a/packages/ui/src/components/card.tsx +++ b/packages/ui/src/components/card.tsx @@ -1,4 +1,4 @@ -import { cn } from "@labas/ui/lib/utils"; +import { cn } from "@pram/ui/lib/utils"; import * as React from "react"; function Card({ diff --git a/packages/ui/src/components/checkbox.tsx b/packages/ui/src/components/checkbox.tsx index 2c8a577898f65cbcd6a773cbcd4205c4c027eabe..a4a2eb5d417bc0c214dd929e7a01e4c4f7243de3 100644 --- a/packages/ui/src/components/checkbox.tsx +++ b/packages/ui/src/components/checkbox.tsx @@ -1,7 +1,7 @@ "use client"; import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox"; -import { cn } from "@labas/ui/lib/utils"; +import { cn } from "@pram/ui/lib/utils"; import { CheckIcon } from "lucide-react"; function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) { diff --git a/packages/ui/src/components/dialog.tsx b/packages/ui/src/components/dialog.tsx index da1c632bbb224ec6d3e31bf0b3acd634ab234c9c..ccbf7b9b891b3a8ced2ae5a77ef9e6c727d1d773 100644 --- a/packages/ui/src/components/dialog.tsx +++ b/packages/ui/src/components/dialog.tsx @@ -1,8 +1,8 @@ import * as React from "react" import { Dialog as DialogPrimitive } from "@base-ui/react/dialog" -import { cn } from "@labas/ui/lib/utils" -import { Button } from "@labas/ui/components/button" +import { cn } from "@pram/ui/lib/utils" +import { Button } from "@pram/ui/components/button" import { XIcon } from "lucide-react" function Dialog({ ...props }: DialogPrimitive.Root.Props) { diff --git a/packages/ui/src/components/dropdown-menu.tsx b/packages/ui/src/components/dropdown-menu.tsx index 53ad3b20342f34d884829036bcf769b73bb4ab7b..592f84c56e193e222fd663c9f9541cf336e32506 100644 --- a/packages/ui/src/components/dropdown-menu.tsx +++ b/packages/ui/src/components/dropdown-menu.tsx @@ -1,7 +1,7 @@ "use client"; import { Menu as MenuPrimitive } from "@base-ui/react/menu"; -import { cn } from "@labas/ui/lib/utils"; +import { cn } from "@pram/ui/lib/utils"; import { ChevronRightIcon, CheckIcon } from "lucide-react"; import * as React from "react"; diff --git a/packages/ui/src/components/input.tsx b/packages/ui/src/components/input.tsx index 9f9b701cdab2e008f29b25214b7339115cd27651..a258305bb81019b5c94768c7c0472cea1e824d25 100644 --- a/packages/ui/src/components/input.tsx +++ b/packages/ui/src/components/input.tsx @@ -1,5 +1,5 @@ import { Input as InputPrimitive } from "@base-ui/react/input"; -import { cn } from "@labas/ui/lib/utils"; +import { cn } from "@pram/ui/lib/utils"; import * as React from "react"; function Input({ className, type, ...props }: React.ComponentProps<"input">) { diff --git a/packages/ui/src/components/label.tsx b/packages/ui/src/components/label.tsx index 4bcc1c3b484d41cc7adf0b256ca46f8811a1ed9d..6d3a230d93b9b3b78ae06d9a1b5b8f11ff4b2ffe 100644 --- a/packages/ui/src/components/label.tsx +++ b/packages/ui/src/components/label.tsx @@ -1,4 +1,4 @@ -import { cn } from "@labas/ui/lib/utils"; +import { cn } from "@pram/ui/lib/utils"; import * as React from "react"; function Label({ className, ...props }: React.ComponentProps<"label">) { diff --git a/packages/ui/src/components/select.tsx b/packages/ui/src/components/select.tsx index 6b5384a5a561a000a9462aa42a1f989345e3f24a..b451766012ff8a9bb660b1aefe65aa247c949d70 100644 --- a/packages/ui/src/components/select.tsx +++ b/packages/ui/src/components/select.tsx @@ -1,7 +1,7 @@ import * as React from "react" import { Select as SelectPrimitive } from "@base-ui/react/select" -import { cn } from "@labas/ui/lib/utils" +import { cn } from "@pram/ui/lib/utils" import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react" const Select = SelectPrimitive.Root diff --git a/packages/ui/src/components/sheet.tsx b/packages/ui/src/components/sheet.tsx index 3fe3d2b93e11818f07e6bdfbbdfa7c304b1d8c57..4d333709ddd8419ff57d94fa75f0957e8431801b 100644 --- a/packages/ui/src/components/sheet.tsx +++ b/packages/ui/src/components/sheet.tsx @@ -1,8 +1,8 @@ import * as React from "react" import { Dialog as SheetPrimitive } from "@base-ui/react/dialog" -import { cn } from "@labas/ui/lib/utils" -import { Button } from "@labas/ui/components/button" +import { cn } from "@pram/ui/lib/utils" +import { Button } from "@pram/ui/components/button" import { XIcon } from "lucide-react" function Sheet({ ...props }: SheetPrimitive.Root.Props) { diff --git a/packages/ui/src/components/skeleton.tsx b/packages/ui/src/components/skeleton.tsx index 391daf79cffda5723eb194f6d40b34a867a599d7..58062975be31184696eb3cc446c218f06a19609a 100644 --- a/packages/ui/src/components/skeleton.tsx +++ b/packages/ui/src/components/skeleton.tsx @@ -1,4 +1,4 @@ -import { cn } from "@labas/ui/lib/utils"; +import { cn } from "@pram/ui/lib/utils"; function Skeleton({ className, ...props }: React.ComponentProps<"div">) { return ( diff --git a/packages/ui/src/components/tabs.tsx b/packages/ui/src/components/tabs.tsx index 3af00069baf56ed14283bcaaf4dacb930d6e2eb5..753f6cc533c29b7941be272f1aa35cf7218d8d14 100644 --- a/packages/ui/src/components/tabs.tsx +++ b/packages/ui/src/components/tabs.tsx @@ -1,7 +1,7 @@ import { Tabs as TabsPrimitive } from "@base-ui/react/tabs" import { cva, type VariantProps } from "class-variance-authority" -import { cn } from "@labas/ui/lib/utils" +import { cn } from "@pram/ui/lib/utils" function Tabs({ className, diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json index 9711f3caf4aa9da441b11be12d657d618ef816d9..06c6a7755308cd842f003dbc4ac2ae56c2f8d2e2 100644 --- a/packages/ui/tsconfig.json +++ b/packages/ui/tsconfig.json @@ -1,10 +1,10 @@ { - "extends": "@labas/config/tsconfig.base.json", + "extends": "@pram/config/tsconfig.base.json", "compilerOptions": { "jsx": "react-jsx", "lib": ["ESNext", "DOM", "DOM.Iterable"], "paths": { - "@labas/ui/*": ["./src/*"] + "@pram/ui/*": ["./src/*"] } }, "include": ["src/**/*.ts", "src/**/*.tsx"], diff --git a/scripts/compress-og-image.ts b/scripts/compress-og-image.ts index 4cc75dfc032530829daca71dd6be3daae8db2837..bab1de975e4eb70f0a71e5817fa1d6b6d18299a2 100644 --- a/scripts/compress-og-image.ts +++ b/scripts/compress-og-image.ts @@ -1,5 +1,5 @@ /** - * Build social preview images for Labas (Open Graph + Twitter). + * Build social preview images for Pram (Open Graph + Twitter). * Requires Bun >= 1.3.14 (Bun.Image API). * * Outputs truecolor PNG + JPEG — palette PNG breaks some Twitter/Threads crawlers. diff --git a/tsconfig.json b/tsconfig.json index 0006edc512a4a24193e25e1d0b9957fc15c25884..1f0817dc7fc219c6d57c99e824f295d2ed3a7ecf 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,3 +1,3 @@ { - "extends": "@labas/config/tsconfig.base.json" + "extends": "@pram/config/tsconfig.base.json" }
Desain yang elegan, namun kokoh untuk mempercepat kesiapan Anda menghadapi ujian. @@ -434,15 +434,15 @@ export function LandingPage() { Pertanyaan Umum
- Temukan jawaban cepat untuk pertanyaan seputar Labas. + Temukan jawaban cepat untuk pertanyaan seputar Pram.
- © {new Date().getFullYear()} Labas. Didesain dengan penuh kehangatan. + © {new Date().getFullYear()} Pram. Didesain dengan penuh kehangatan.
Exam Prep Portal diff --git a/apps/web/src/components/sign-in-form.tsx b/apps/web/src/components/sign-in-form.tsx index 50458962b6fe7230a612b94852c07a7f162eff84..3dd11adf6bb54e84dfc8cdca57a4f82ff033765a 100644 --- a/apps/web/src/components/sign-in-form.tsx +++ b/apps/web/src/components/sign-in-form.tsx @@ -1,7 +1,7 @@ -import { Button } from "@labas/ui/components/button"; -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@labas/ui/components/card"; -import { Input } from "@labas/ui/components/input"; -import { Label } from "@labas/ui/components/label"; +import { Button } from "@pram/ui/components/button"; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@pram/ui/components/card"; +import { Input } from "@pram/ui/components/input"; +import { Label } from "@pram/ui/components/label"; import { useForm } from "@tanstack/react-form"; import { Link, useNavigate } from "@tanstack/react-router"; import { toast } from "sonner"; diff --git a/apps/web/src/components/sign-up-form.tsx b/apps/web/src/components/sign-up-form.tsx index daa14994adb4df7f1875f402f5fcae16c447b738..a7e0422467b723e9bd6b706ebc4b8d005055b623 100644 --- a/apps/web/src/components/sign-up-form.tsx +++ b/apps/web/src/components/sign-up-form.tsx @@ -1,7 +1,7 @@ -import { Button } from "@labas/ui/components/button"; -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@labas/ui/components/card"; -import { Input } from "@labas/ui/components/input"; -import { Label } from "@labas/ui/components/label"; +import { Button } from "@pram/ui/components/button"; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@pram/ui/components/card"; +import { Input } from "@pram/ui/components/input"; +import { Label } from "@pram/ui/components/label"; import { useForm } from "@tanstack/react-form"; import { useMutation } from "@tanstack/react-query"; import { useNavigate } from "@tanstack/react-router"; diff --git a/apps/web/src/components/test/AccentKeyboard.tsx b/apps/web/src/components/test/AccentKeyboard.tsx index 642c8598c3d0a5e04f94482327680ec9e78c9343..a4a79da225060b0944cbc85456ebc886a584bd65 100644 --- a/apps/web/src/components/test/AccentKeyboard.tsx +++ b/apps/web/src/components/test/AccentKeyboard.tsx @@ -1,4 +1,4 @@ -import { useCallback } from "react"; +import { useMemo } from "react"; const ACCENT_CHARS: Record = { DELE: ["á", "é", "í", "ó", "ú", "ñ", "¿", "¡"], diff --git a/apps/web/src/components/test/AttemptTestView.tsx b/apps/web/src/components/test/AttemptTestView.tsx index 96ee2394fc4abcd48b7581a60552e9b79c08896d..9d16e2bd73e9f5cc4b9845919628ed71fa2d1548 100644 --- a/apps/web/src/components/test/AttemptTestView.tsx +++ b/apps/web/src/components/test/AttemptTestView.tsx @@ -1,8 +1,7 @@ import { useState, useEffect, useRef, useCallback, memo } from "react"; import { useQuery } from "@tanstack/react-query"; -import { Button } from "@labas/ui/components/button"; +import { Button } from "@pram/ui/components/button"; import { MaterialIcon } from "@/components/ui/MaterialIcon"; -import { formatTime } from "@/lib/time"; import { trpc } from "@/utils/trpc"; import { QuestionInput } from "./QuestionInput"; import { AccentKeyboard } from "./AccentKeyboard"; diff --git a/apps/web/src/components/test/QuestionInput.tsx b/apps/web/src/components/test/QuestionInput.tsx index 83a70eaac98e99252da85d8dedb7c960f8458e74..93e5d318eb07b3773fac83bec2adb2b98577a560 100644 --- a/apps/web/src/components/test/QuestionInput.tsx +++ b/apps/web/src/components/test/QuestionInput.tsx @@ -1,5 +1,5 @@ -import { useState, useEffect, useRef, useCallback, useMemo } from "react"; -import { Input } from "@labas/ui/components/input"; +import { useState, useEffect, useRef, useCallback } from "react"; +import { Input } from "@pram/ui/components/input"; import { MCQ_FORMATS } from "@/lib/question-formats"; import type { Question } from "@/lib/types"; diff --git a/apps/web/src/components/test/attempt/AbandonDialog.tsx b/apps/web/src/components/test/attempt/AbandonDialog.tsx index f3f35afd40e69f6f403cfec45ece1bc9ca57089f..fc2b9d056c1f3539162bc0b40cf8bf73f8d7b93b 100644 --- a/apps/web/src/components/test/attempt/AbandonDialog.tsx +++ b/apps/web/src/components/test/attempt/AbandonDialog.tsx @@ -1,11 +1,11 @@ -import { Button } from "@labas/ui/components/button"; +import { Button } from "@pram/ui/components/button"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, -} from "@labas/ui/components/dialog"; +} from "@pram/ui/components/dialog"; import { MaterialIcon } from "@/components/ui/MaterialIcon"; interface AbandonDialogProps { diff --git a/apps/web/src/components/test/attempt/AttemptHeader.tsx b/apps/web/src/components/test/attempt/AttemptHeader.tsx index 6ad97fa7bb64767db2cf021076b80bd76bb3bb41..8d2e7d354b5e04b04967a89aaed57da5829556b6 100644 --- a/apps/web/src/components/test/attempt/AttemptHeader.tsx +++ b/apps/web/src/components/test/attempt/AttemptHeader.tsx @@ -1,4 +1,4 @@ -import { Button } from "@labas/ui/components/button"; +import { Button } from "@pram/ui/components/button"; import { MaterialIcon } from "@/components/ui/MaterialIcon"; import { formatTime } from "@/lib/time"; diff --git a/apps/web/src/components/test/attempt/FinishDialog.tsx b/apps/web/src/components/test/attempt/FinishDialog.tsx index 5a444879a7b39a5254754b2e7c8e4d1650b5d030..326bd9481f4c13339fd7031c3d6c184ea8af8415 100644 --- a/apps/web/src/components/test/attempt/FinishDialog.tsx +++ b/apps/web/src/components/test/attempt/FinishDialog.tsx @@ -1,11 +1,11 @@ -import { Button } from "@labas/ui/components/button"; +import { Button } from "@pram/ui/components/button"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, -} from "@labas/ui/components/dialog"; +} from "@pram/ui/components/dialog"; import { MaterialIcon } from "@/components/ui/MaterialIcon"; interface FinishDialogProps { diff --git a/apps/web/src/components/ui/Avatar.tsx b/apps/web/src/components/ui/Avatar.tsx index ed8f209c8fba664aaee63adc375c68787cbdb5e8..419902f69b18515b8fad42e2d35c3c0e0aec2005 100644 --- a/apps/web/src/components/ui/Avatar.tsx +++ b/apps/web/src/components/ui/Avatar.tsx @@ -1,4 +1,4 @@ -import { cn } from "@labas/ui/lib/utils"; +import { cn } from "@pram/ui/lib/utils"; const sizeClasses = { xs: { container: "h-6 w-6", text: "text-xs" }, diff --git a/apps/web/src/components/ui/PasswordInput.tsx b/apps/web/src/components/ui/PasswordInput.tsx index 2584d64efd2803718951a7f6ace99b0e7d4c468f..9d2c9016a739e68f5d87ea61867ee47b79eeb0f4 100644 --- a/apps/web/src/components/ui/PasswordInput.tsx +++ b/apps/web/src/components/ui/PasswordInput.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; -import { Input } from "@labas/ui/components/input"; -import { Button } from "@labas/ui/components/button"; +import { Input } from "@pram/ui/components/input"; +import { Button } from "@pram/ui/components/button"; import { MaterialIcon } from "@/components/ui/MaterialIcon"; type PasswordInputProps = { diff --git a/apps/web/src/components/user-menu.tsx b/apps/web/src/components/user-menu.tsx index e3ca0a650875a5c7be6ff69616e015205eb03d76..998ce805d867d1ebd1682185c1563fec6fc99f56 100644 --- a/apps/web/src/components/user-menu.tsx +++ b/apps/web/src/components/user-menu.tsx @@ -1,4 +1,4 @@ -import { Button } from "@labas/ui/components/button"; +import { Button } from "@pram/ui/components/button"; import { DropdownMenu, DropdownMenuContent, @@ -7,8 +7,8 @@ import { DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, -} from "@labas/ui/components/dropdown-menu"; -import { Skeleton } from "@labas/ui/components/skeleton"; +} from "@pram/ui/components/dropdown-menu"; +import { Skeleton } from "@pram/ui/components/skeleton"; import { Link, useNavigate } from "@tanstack/react-router"; import { authClient } from "@/lib/auth-client"; diff --git a/apps/web/src/hooks/use-api-key.ts b/apps/web/src/hooks/use-api-key.ts index bf210f35e06bf961a92ee3a02ff962c7e3629174..0a67b2005189cd60035078b4f6023000434621ac 100644 --- a/apps/web/src/hooks/use-api-key.ts +++ b/apps/web/src/hooks/use-api-key.ts @@ -2,7 +2,7 @@ import { useState, useEffect, useCallback } from "react"; import { encryptText, decryptText } from "@/lib/crypto"; import { trackUmamiEvent, AnalyticsEvent } from "@/lib/umami"; -const STORAGE_KEY = "labas_api_keys_v2"; +const STORAGE_KEY = "pram_api_keys_v2"; export interface ApiKeyConfig { id: string; @@ -102,7 +102,7 @@ export function useApiKeys() { // Backward-compatible hook for places that only need the first (default) key export function useApiKey() { - const { configs, isLoading, addConfig, updateConfig, removeConfig } = + const { configs, isLoading, addConfig, removeConfig } = useApiKeys(); const storedKey = configs[0] ?? null; diff --git a/apps/web/src/hooks/use-generation-jobs.ts b/apps/web/src/hooks/use-generation-jobs.ts index f721a61d517f9f46f1bbe9b329f7d7b2ed8912f6..52312773d3d61fb5e73cc139d9eb6736595a47fc 100644 --- a/apps/web/src/hooks/use-generation-jobs.ts +++ b/apps/web/src/hooks/use-generation-jobs.ts @@ -5,7 +5,7 @@ import { authClient } from "@/lib/auth-client"; import { useJobState } from "./use-job-state"; import { usePollingTransport } from "./use-polling-transport"; import { MAX_PARALLEL, ACTIVE_STATUSES, isTerminal } from "./use-job-shared"; -import type { GenerationResult } from "@labas/ai"; +import type { GenerationResult } from "@pram/ai"; export type { ActiveJob, CompletedResult } from "./use-job-shared"; diff --git a/apps/web/src/hooks/use-job-shared.ts b/apps/web/src/hooks/use-job-shared.ts index 81fbb11149b1b2efccfea205a716687019f55358..2b61df48d32354091843aa3908017b573d10e6e2 100644 --- a/apps/web/src/hooks/use-job-shared.ts +++ b/apps/web/src/hooks/use-job-shared.ts @@ -1,8 +1,8 @@ /** Shared types and constants for job management hooks. */ -export const STORAGE_KEY = "labas_active_jobs"; -export const RESULTS_KEY = "labas_completed_results"; -export const CLEARED_JOBS_KEY = "labas_cleared_jobs"; +export const STORAGE_KEY = "pram_active_jobs"; +export const RESULTS_KEY = "pram_completed_results"; +export const CLEARED_JOBS_KEY = "pram_cleared_jobs"; export const MAX_PARALLEL = 3; export interface ActiveJob { @@ -21,7 +21,7 @@ export interface ActiveJob { export interface CompletedResult { jobId: string; - result: import("@labas/ai").GenerationResult; + result: import("@pram/ai").GenerationResult; generatedPackageId: string | null; examType?: string; section?: string; diff --git a/apps/web/src/hooks/use-job-state.ts b/apps/web/src/hooks/use-job-state.ts index fcb8563f40794950576ea63ecda02027d6e82495..92cec40b4695f63846b7ccf94dda81e942343d83 100644 --- a/apps/web/src/hooks/use-job-state.ts +++ b/apps/web/src/hooks/use-job-state.ts @@ -52,13 +52,13 @@ function writeClearedJobIds(ids: string[]) { function broadcastJobs(ids: string[]) { window.dispatchEvent( - new CustomEvent("labas:jobs-change", { detail: { ids } }), + new CustomEvent("pram:jobs-change", { detail: { ids } }), ); } function broadcastResults(results: CompletedResult[]) { window.dispatchEvent( - new CustomEvent("labas:results-change", { detail: { results } }), + new CustomEvent("pram:results-change", { detail: { results } }), ); } @@ -222,8 +222,8 @@ export function useJobState() { const detail = (e as CustomEvent).detail as { ids: string[] }; dispatch({ type: "syncIds", ids: detail.ids }); }; - window.addEventListener("labas:jobs-change", handler); - return () => window.removeEventListener("labas:jobs-change", handler); + window.addEventListener("pram:jobs-change", handler); + return () => window.removeEventListener("pram:jobs-change", handler); }, []); /* Sync completed results across hook instances (GeneratePage + GlobalProgress) */ @@ -233,8 +233,8 @@ export function useJobState() { skipPersistRef.current = true; dispatch({ type: "syncResults", results: detail.results }); }; - window.addEventListener("labas:results-change", handler); - return () => window.removeEventListener("labas:results-change", handler); + window.addEventListener("pram:results-change", handler); + return () => window.removeEventListener("pram:results-change", handler); }, []); const addJob = useCallback((id: string) => dispatch({ type: "addJob", id }), []); diff --git a/apps/web/src/hooks/use-sidebar.ts b/apps/web/src/hooks/use-sidebar.ts index 782ce330738cba1763ae0398c707d5cd287d51b8..01a5e783c07b0cd49a9d45fbcbca912730d1beb9 100644 --- a/apps/web/src/hooks/use-sidebar.ts +++ b/apps/web/src/hooks/use-sidebar.ts @@ -1,6 +1,6 @@ import { useSyncExternalStore } from "react"; -const STORAGE_KEY = "labas_sidebar_collapsed"; +const STORAGE_KEY = "pram_sidebar_collapsed"; function getSnapshot(): boolean { if (typeof window === "undefined") return false; diff --git a/apps/web/src/hooks/use-test-session.ts b/apps/web/src/hooks/use-test-session.ts index c1a0edfbc2471b3e67cff9051a543d35ec809013..9baa12f08914e0d082b42ffd81601a0be1653e50 100644 --- a/apps/web/src/hooks/use-test-session.ts +++ b/apps/web/src/hooks/use-test-session.ts @@ -5,15 +5,15 @@ import { trpc } from "@/utils/trpc"; import { getErrorMessage } from "@/lib/error-utils"; function getTimerKey(attemptId: string | null) { - return attemptId ? `labas_attempt_timer_${attemptId}` : null; + return attemptId ? `pram_attempt_timer_${attemptId}` : null; } function getMarkedKey(attemptId: string | null) { - return attemptId ? `labas_attempt_marked_${attemptId}` : null; + return attemptId ? `pram_attempt_marked_${attemptId}` : null; } function getSectionKey(attemptId: string | null) { - return attemptId ? `labas_attempt_section_${attemptId}` : null; + return attemptId ? `pram_attempt_section_${attemptId}` : null; } function loadElapsedTime(attemptId: string | null): number { diff --git a/apps/web/src/index.css b/apps/web/src/index.css index 3ce68a841b45168012eacbe08445b73547f3bbf8..f2533e9f5881cc43516f3b61568a862715c62be3 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -1 +1 @@ -@import "@labas/ui/globals.css"; +@import "@pram/ui/globals.css"; diff --git a/apps/web/src/lib/__tests__/difficulty-mapping.test.ts b/apps/web/src/lib/__tests__/difficulty-mapping.test.ts index 2e52f5d606f926c8c19c8eee6cc83f29e0d21952..dd6a8917e6b8860a2a7bcfd75eb3427f239dfd9b 100644 --- a/apps/web/src/lib/__tests__/difficulty-mapping.test.ts +++ b/apps/web/src/lib/__tests__/difficulty-mapping.test.ts @@ -8,7 +8,7 @@ describe("DIFFICULTY_LABELS", () => { }); it("each exam has exactly 5 difficulty levels", () => { - for (const [exam, labels] of Object.entries(DIFFICULTY_LABELS)) { + for (const [_exam, labels] of Object.entries(DIFFICULTY_LABELS)) { expect(labels).toHaveLength(5); } }); diff --git a/apps/web/src/lib/auth-client.ts b/apps/web/src/lib/auth-client.ts index e771d5c8a949fc022f137a01699c582f54fa85af..5513c56c27b3d4162b0cf41a35f9727d71ed3924 100644 --- a/apps/web/src/lib/auth-client.ts +++ b/apps/web/src/lib/auth-client.ts @@ -1,4 +1,4 @@ -import { env } from "@labas/env/web"; +import { env } from "@pram/env/web"; import { createAuthClient } from "better-auth/react"; export const authClient = createAuthClient({ diff --git a/apps/web/src/lib/community-links.ts b/apps/web/src/lib/community-links.ts index 251299027a42c9903580b5e23edbcb0b355c29c5..3ffba1602ffe8cf78c191060521b31dec985ea84 100644 --- a/apps/web/src/lib/community-links.ts +++ b/apps/web/src/lib/community-links.ts @@ -33,10 +33,10 @@ export const COMMUNITY_PLATFORM_META: Record< /** Satu sumber data — tambah Discord cukup isi `href` di entri bawah. */ export const COMMUNITY_CHANNELS: CommunityChannel[] = [ { - id: "labas-whatsapp", + id: "pram-whatsapp", platform: "whatsapp", - name: "Labas — Open Source", - description: "Laporkan bug, usulkan fitur, atau diskusi pengembangan Labas.", + name: "Pram — Open Source", + description: "Laporkan bug, usulkan fitur, atau diskusi pengembangan Pram.", href: "https://chat.whatsapp.com/JT6bDIXMQ5x6SUQCA1aDWB", icon: "groups", }, @@ -50,16 +50,16 @@ export const COMMUNITY_CHANNELS: CommunityChannel[] = [ }, // Aktifkan saat server Discord siap — cukup isi href: { - id: "labas-discord", + id: "pram-discord", platform: "discord", - name: "Labas Discord", + name: "Pram Discord", description: "Diskusi async, bug report terstruktur, dan pengumuman fitur baru.", href: null, icon: "chat", }, ]; -export const COMMUNITY_PROMPT_STORAGE_KEY = "labas_community_prompt_seen"; +export const COMMUNITY_PROMPT_STORAGE_KEY = "pram_community_prompt_seen"; const PLATFORM_ORDER: CommunityPlatform[] = ["whatsapp", "discord"]; diff --git a/apps/web/src/lib/crypto.ts b/apps/web/src/lib/crypto.ts index 7035a0f847877564e6a59586d2985caa38eaeff1..90a9037301a87f4a44cb3cd5dfd3fc2e4c6fbc31 100644 --- a/apps/web/src/lib/crypto.ts +++ b/apps/web/src/lib/crypto.ts @@ -1,6 +1,6 @@ -const DB_NAME = "labas-crypto"; +const DB_NAME = "pram-crypto"; const STORE_NAME = "keys"; -const KEY_NAME = "labas-master-key"; +const KEY_NAME = "pram-master-key"; function openDB(): Promise { return new Promise((resolve, reject) => { diff --git a/apps/web/src/lib/generate-constants.ts b/apps/web/src/lib/generate-constants.ts index 42a8ac9280e507225ae19519769ebf3ba162737a..3200260591b04cfed68eae58f73013d8db0e90a9 100644 --- a/apps/web/src/lib/generate-constants.ts +++ b/apps/web/src/lib/generate-constants.ts @@ -1,45 +1,156 @@ export const EXAM_TYPES = [ - { id: "IELTS", name: "IELTS Academic", code: "gb" }, - { id: "TOEFL", name: "TOEFL iBT", code: "us" }, - { id: "JLPT", name: "JLPT", code: "jp" }, - { id: "HSK", name: "HSK", code: "cn" }, - { id: "GOETHE", name: "Goethe-Zertifikat", code: "de" }, - { id: "TOPIK", name: "TOPIK", code: "kr" }, - { id: "TOAFL", name: "TOAFL", code: "sa" }, - { id: "DELE", name: "DELE", code: "es" }, + { id: "CPNS", name: "Ujian CPNS (SKD)", code: "id" }, + { id: "SKB", name: "Ujian SKB (Teknis)", code: "id" }, + { id: "SKB_PENATA_PERTANAHAN", name: "SKB Penata Pertanahan (BPN/ATR)", code: "id" }, ]; export const SECTIONS = [ - { id: "READING", name: "Reading", icon: "menu_book" }, - { id: "WRITING", name: "Writing", icon: "edit_note" }, + { id: "TWK", name: "TWK", icon: "history_edu" }, + { id: "TIU", name: "TIU", icon: "psychology" }, + { id: "TKP", name: "TKP", icon: "groups" }, + { id: "Teknis", name: "Teknis", icon: "engineering" }, + { id: "Pertanahan_Dasar", name: "Pertanahan & Agraria", icon: "landscape" }, + { id: "Pengukuran_Kadastral", name: "Pengukuran & Pemetaan Kadastral", icon: "map" }, + { id: "Pendaftaran_Tanah", name: "Pendaftaran Tanah & Sertipikat", icon: "assignment" }, + { id: "Pengadaan_Tanah", name: "Pengadaan Tanah & Ganti Rugi", icon: "real_estate_agent" }, + { id: "Pengendalian_Pertanahan", name: "Pengendalian & Penertiban Tanah", icon: "policy" }, + { id: "Penilaian_Tanah", name: "Penilaian Tanah & Properti", icon: "price_check" }, ]; export const FORMATS = [ - { id: "multiple_choice", name: "Multiple Choice", allowedExams: ["IELTS", "TOEFL", "JLPT", "HSK", "GOETHE", "TOPIK", "TOAFL", "DELE"] }, - { id: "true_false_not_given", name: "True / False / Not Given", allowedExams: ["IELTS", "TOEFL", "JLPT", "HSK", "GOETHE", "TOPIK", "TOAFL", "DELE"] }, - { id: "fill_blank", name: "Fill in Blank", allowedExams: ["IELTS", "TOEFL", "JLPT", "HSK", "GOETHE", "TOPIK", "TOAFL", "DELE"] }, - { id: "synonym", name: "Synonym / Vocabulary", allowedExams: ["IELTS", "TOEFL", "JLPT", "HSK", "GOETHE", "TOPIK", "TOAFL", "DELE"] }, - { id: "grammar_in_context", name: "Grammar in Context", allowedExams: ["IELTS", "TOEFL", "JLPT", "HSK", "GOETHE", "TOPIK", "TOAFL", "DELE"] }, - { id: "sentence_completion", name: "Sentence Completion", allowedExams: ["IELTS", "TOEFL", "JLPT", "HSK", "GOETHE", "TOPIK", "TOAFL", "DELE"] }, - { id: "cloze", name: "Cloze Test", allowedExams: ["IELTS", "TOEFL", "JLPT", "HSK", "GOETHE", "TOPIK", "TOAFL", "DELE"] }, - { id: "reference", name: "Reference (Pronoun)", allowedExams: ["IELTS", "TOEFL", "JLPT", "HSK", "GOETHE", "TOPIK", "TOAFL", "DELE"] }, - { id: "author_view", name: "Author's View", allowedExams: ["IELTS", "TOEFL", "JLPT", "HSK", "GOETHE", "TOPIK", "TOAFL", "DELE"] }, - { id: "matching_headings", name: "Matching Headings", allowedExams: ["IELTS", "TOEFL", "JLPT", "HSK", "GOETHE", "TOPIK", "TOAFL", "DELE"] }, - { id: "matching_information", name: "Matching Information", allowedExams: ["IELTS", "TOEFL", "JLPT", "HSK", "GOETHE", "TOPIK", "TOAFL", "DELE"] }, - { id: "summary_completion", name: "Summary Completion", allowedExams: ["IELTS", "TOEFL", "JLPT", "HSK", "GOETHE", "TOPIK", "TOAFL", "DELE"] }, - { id: "matching_pairs", name: "Matching Pairs", allowedExams: ["IELTS", "TOEFL", "GOETHE", "DELE", "TOPIK"] }, - { id: "error_recognition", name: "Error Recognition", allowedExams: ["TOEFL", "HSK", "TOPIK", "TOAFL"] }, - { id: "text_insertion", name: "Text Insertion", allowedExams: ["TOEFL", "IELTS"] }, - { id: "kanji_reading", name: "Kanji Reading", allowedExams: ["JLPT"] }, - { id: "particle_choice", name: "Particle / Conjunction Choice", allowedExams: ["JLPT", "TOPIK"] }, - { id: "article_case", name: "Article / Gender / Agreement", allowedExams: ["GOETHE", "TOAFL", "DELE"] }, - { id: "character_reading", name: "Character Reading", allowedExams: ["HSK", "TOPIK"] }, - { id: "sentence_arrangement", name: "Sentence Arrangement", allowedExams: ["HSK", "TOPIK", "DELE", "TOAFL"] }, + { id: "multiple_choice", name: "Multiple Choice", allowedExams: ["CPNS", "SKB"] }, + { id: "true_false_not_given", name: "Benar / Salah", allowedExams: ["CPNS", "SKB"] }, + { id: "fill_blank", name: "Isian Singkat", allowedExams: ["CPNS", "SKB"] }, + { id: "synonym", name: "Sinonim / Kosakata", allowedExams: ["CPNS", "SKB"] }, + { id: "grammar_in_context", name: "Tata Bahasa", allowedExams: ["CPNS", "SKB"] }, + { id: "sentence_completion", name: "Melengkapi Kalimat", allowedExams: ["CPNS", "SKB"] }, + { id: "cloze", name: "Cloze Test", allowedExams: ["CPNS", "SKB"] }, + { id: "reference", name: "Rujukan Kata", allowedExams: ["CPNS", "SKB"] }, + { id: "author_view", name: "Pandangan Penulis", allowedExams: ["CPNS", "SKB"] }, + { id: "matching_headings", name: "Mencocokkan Judul", allowedExams: ["CPNS", "SKB"] }, + { id: "matching_information", name: "Mencocokkan Informasi", allowedExams: ["CPNS", "SKB"] }, + { id: "summary_completion", name: "Melengkapi Ringkasan", allowedExams: ["CPNS", "SKB"] }, + { id: "matching_pairs", name: "Mencocokkan Pasangan", allowedExams: ["CPNS", "SKB"] }, + { id: "error_recognition", name: "Analisis Kesalahan", allowedExams: ["CPNS", "SKB"] }, + { id: "sentence_arrangement", name: "Menyusun Kalimat", allowedExams: ["CPNS", "SKB"] }, ]; export const TOPICS = [ - "Science & Tech", "Business", "Sociology", "Arts", "History", "Environment", "Health", "Education", - "Daily Interaction", "Social Etiquette", "Workplace", + // SKD - TWK + "Nasionalisme", "Integritas", "Bela Negara", "Pilar Negara", "Bahasa Indonesia", + // SKD - TIU + "Analogi", "Silogisme", "Analitis", "Deret Angka", "Perbandingan Kuantitatif", "Soal Cerita", "Figural", + // SKD - TKP + "Pelayanan Publik", "Jejaring Kerja", "Sosial Budaya", "TIK", "Profesionalisme", "Anti Radikalisme", + // SKB Teknis Umum + "Manajemen Pemerintahan", "Hukum Administrasi Negara", "Kebijakan Publik", + + // ── SKB Penata Pertanahan ───────────────────────────────────────────── + // 1. Hukum Agraria & Regulasi Pertanahan + "Undang-Undang Pokok Agraria (UUPA No. 5 Tahun 1960)", + "Hak Milik, Hak Guna Usaha, Hak Guna Bangunan, Hak Pakai", + "Hak atas Tanah Adat dan Konversi Hak Lama", + "Peraturan Pemerintah No. 24 Tahun 1997 tentang Pendaftaran Tanah", + "Peraturan Pemerintah No. 18 Tahun 2021 tentang Hak Pengelolaan", + "Peraturan Pemerintah No. 20 Tahun 2021 tentang Penertiban Kawasan dan Tanah Terlantar", + "Peraturan Menteri ATR/BPN tentang Pendaftaran Tanah", + "Kepala Badan Pertanahan Nasional dan Struktur Organisasi BPN", + "Reforma Agraria dan Redistribusi Tanah", + "Tanah Negara, Tanah Adat, dan Tanah Ulayat", + "Pengaturan Pemilikan dan Penguasaan Tanah", + "Larangan Pemilikan Tanah secara Absentee", + + // 2. Pendaftaran Tanah & Sertipikat + "Sistem Pendaftaran Tanah Stelsel Negatif Bertendensi Positif", + "Proses Pensertipikatan Tanah: Sporadis dan Sistematis", + "Pendaftaran Tanah Sistematis Lengkap (PTSL)", + "Buku Tanah dan Surat Ukur sebagai Produk Pendaftaran", + "Sertipikat Hak Atas Tanah sebagai Tanda Bukti Hak", + "Pemeliharaan Data Pendaftaran Tanah (Pembaruan Data)", + "Balik Nama, Pemisahan, Pemecahan, dan Penggabungan Sertipikat", + "Sertipikat Tanah Elektronik", + "Pendaftaran Peralihan Hak Karena Jual Beli, Hibah, dan Waris", + "Roya Hak Tanggungan dan Pencatatan Hak Tanggungan", + "Pengumuman dan Penerbitan Sertipikat pada PTSL", + "Kegiatan Pengumpulan Data Fisik dan Yuridis", + + // 3. Pengukuran & Pemetaan Kadastral + "Dasar-Dasar Ilmu Ukur Tanah (Survei & Pemetaan)", + "Pengukuran Batas Bidang Tanah dan Penentuan Batas", + "Peta Bidang Tanah dan Gambar Situasi", + "Peta Dasar Pendaftaran dan Peta Pendaftaran", + "Sistem Koordinat Nasional (TM3)", + "Penggunaan GPS/GNSS untuk Pengukuran Kadastral", + "Drone dan Fotogrametri dalam Pemetaan Pertanahan", + "Kadaster dan Sistem Informasi Pertanahan", + "KKP (Komputerisasi Kegiatan Pertanahan) dan BHUMI", + "Peta Tematik Pertanahan", + "Peta Zona Nilai Tanah (ZNT)", + "Identifikasi dan Delineasi Bidang Tanah", + "Pengukuran Keliling dan Luas Bidang Tanah", + "Rekonstruksi Batas Bidang Tanah", + "Survei Kadastral Berbasis Partisipasi Masyarakat", + + // 4. Pengadaan Tanah & Ganti Rugi + "Undang-Undang No. 2 Tahun 2012 tentang Pengadaan Tanah untuk Kepentingan Umum", + "Peraturan Presiden No. 71 Tahun 2012 dan Perubahannya", + "Tahapan Pengadaan Tanah: Perencanaan, Persiapan, Pelaksanaan, Penyerahan", + "Penetapan Lokasi Pembangunan", + "Inventarisasi dan Identifikasi Bidang Tanah yang Terkena Pengadaan", + "Penilaian Ganti Kerugian oleh Penilai Publik", + "Musyawarah Penetapan Bentuk Ganti Kerugian", + "Penitipan Ganti Kerugian di Pengadilan (Konsignasi)", + "Pengadaan Tanah untuk Proyek Strategis Nasional (PSN)", + "Hak Pemegang Hak Atas Tanah dalam Pengadaan Tanah", + "Ganti Rugi: Uang, Tanah Pengganti, Pemukiman Kembali, dan Kepemilikan Saham", + "Pengadaan Tanah Skala Kecil (di bawah 5 hektar)", + + // 5. Pengendalian & Penertiban Tanah + "Tanah Terlantar dan Proses Penertiban", + "Penetapan Tanah Terlantar sesuai PP No. 20 Tahun 2021", + "Redistribusi Tanah Terlantar sebagai Tanah Cadangan Umum Negara", + "Penertiban Penguasaan Tanah Absentee", + "Monitoring dan Evaluasi Penggunaan Tanah", + "Pengendalian Alih Fungsi Lahan Pertanian", + "Pemantauan Pemanfaatan Ruang dan Kesesuaian Tata Ruang", + "Tata Cara Penyelesaian Sengketa Pertanahan", + "Mediasi dan Pengaduan Sengketa Pertanahan di BPN", + "Sengketa, Konflik, dan Perkara Pertanahan", + "Pemberdayaan Tanah Masyarakat", + + // 6. Penilaian Tanah & Properti + "Zona Nilai Tanah (ZNT) dan Nilai Indikasi Rata-Rata (NIR)", + "Metode Penilaian Tanah: Perbandingan Data Pasar, Biaya, Pendapatan", + "Peta ZNT sebagai Basis NJOP dan Pajak", + "Penilai Publik dan Penilai Internal BPN", + "Nilai Pasar dan Nilai Ganti Rugi", + "Faktor-Faktor yang Mempengaruhi Nilai Tanah", + "Penilaian Tanah untuk PTSL dan Pendaftaran", + + // 7. Tata Ruang & Rencana Penggunaan Tanah + "Undang-Undang No. 26 Tahun 2007 tentang Penataan Ruang", + "RTRW (Rencana Tata Ruang Wilayah) Nasional, Provinsi, Kabupaten/Kota", + "Kesesuaian Tata Ruang dalam Penerbitan Izin dan Hak Atas Tanah", + "Rencana Detail Tata Ruang (RDTR)", + "Izin Peruntukan Penggunaan Tanah (IPPT)", + "Pertanian Pangan Berkelanjutan dan Perlindungan Lahan Sawah", + "Kawasan Lindung, Kawasan Budidaya, dan Kawasan Strategis", + + // 8. Teknologi Informasi Pertanahan + "Aplikasi KKP (Komputerisasi Kegiatan Pertanahan)", + "Layanan BHUMI dan Informasi Publik Pertanahan", + "Sistem Informasi Geografis (SIG) dalam Pertanahan", + "Electronic Land Registration System", + "Data Spasial dan Non-Spasial Pertanahan", + "Integrasi Data Pertanahan dengan OSS (Online Single Submission)", + + // 9. Etika & Pelayanan Publik Pertanahan + "Standar Pelayanan Pertanahan di Kantor Pertanahan", + "Penanganan Pengaduan Pelayanan Pertanahan", + "Maklumat Pelayanan dan SOP BPN", + "Integritas dan Anti-Korupsi dalam Pelayanan Pertanahan", + "Jabatan Fungsional Penata Pertanahan: Tugas dan Kewenangan", + "Angka Kredit dan Jenjang Jabatan Penata Pertanahan", ]; export const DIFFICULTIES = ["Beginner", "Elementary", "Intermediate", "Advanced", "Expert"]; diff --git a/apps/web/src/lib/site-seo.ts b/apps/web/src/lib/site-seo.ts index 064abd2fe88ccc0a43a9034c3b6dbaf36f2ad816..11f5e89a9463d471e15ba08c4644b6fdcb952241 100644 --- a/apps/web/src/lib/site-seo.ts +++ b/apps/web/src/lib/site-seo.ts @@ -1,5 +1,5 @@ -export const SITE_URL = "https://labas.rogasper.com"; -export const SITE_NAME = "Labas"; +export const SITE_URL = "https://pram.rogasper.com"; +export const SITE_NAME = "Pram"; /** Match rogasper.com: separate OG (PNG) + Twitter (JPEG) assets for crawler compatibility. */ export const OG_IMAGE_PATH = "/opengraph-image.png"; @@ -12,9 +12,9 @@ export const TWITTER_IMAGE_URL = `${SITE_URL}${TWITTER_IMAGE_PATH}`; export const OG_IMAGE_WIDTH = 1200; export const OG_IMAGE_HEIGHT = 630; export const OG_IMAGE_ALT = - "Labas — platform latihan ujian bahasa dengan AI Generator dan simulasi tes"; + "Pram — platform latihan ujian bahasa dengan AI Generator dan simulasi tes"; -export const DEFAULT_SITE_TITLE = "Labas — AI-Powered Exam Practice Platform"; +export const DEFAULT_SITE_TITLE = "Pram — AI-Powered Exam Practice Platform"; export const DEFAULT_SITE_DESCRIPTION = "Generate AI-powered practice questions for JLPT, TOPIK, TOAFL, and more. Practice smarter with adaptive test preparation."; diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx index d195e897d4496780c8efcb0ac366d23df434e76b..031bacf0668e6ccb6a30d6c3b73c73273db34733 100644 --- a/apps/web/src/routes/__root.tsx +++ b/apps/web/src/routes/__root.tsx @@ -1,4 +1,4 @@ -import { Toaster } from "@labas/ui/components/sonner"; +import { Toaster } from "@pram/ui/components/sonner"; import type { QueryClient } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { HeadContent, Outlet, createRootRouteWithContext, useRouterState } from "@tanstack/react-router"; @@ -56,7 +56,7 @@ export const Route = createRootRouteWithContext()({ { name: "robots", content: "index, follow" }, ], links: [ - { rel: "icon", href: "/labas_icon.png" }, + { rel: "icon", href: "/pram_icon.png" }, { rel: "canonical", href: `${SITE_URL}/` }, { rel: "preconnect", href: "https://fonts.googleapis.com" }, { rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" as const }, @@ -71,7 +71,7 @@ export const Route = createRootRouteWithContext()({ children: JSON.stringify({ "@context": "https://schema.org", "@type": "WebSite", - name: "Labas", + name: "Pram", url: SITE_URL, description: DEFAULT_SITE_DESCRIPTION, potentialAction: { @@ -98,7 +98,7 @@ function RootComponent() { defaultTheme="light" enableSystem disableTransitionOnChange - storageKey="labas-theme" + storageKey="pram-theme" > {shell === "fullscreen" ? ( diff --git a/apps/web/src/routes/admin.credits.tsx b/apps/web/src/routes/admin.credits.tsx index e10474357c9ee24caca6b4cf2cc0c310a8990728..0c829a1813c3bee4981641c5be73f278cc9c0f10 100644 --- a/apps/web/src/routes/admin.credits.tsx +++ b/apps/web/src/routes/admin.credits.tsx @@ -2,13 +2,12 @@ import { useState } from "react"; import { createFileRoute } from "@tanstack/react-router"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { trpc } from "@/utils/trpc"; -import { Input } from "@labas/ui/components/input"; -import { Button } from "@labas/ui/components/button"; +import { Input } from "@pram/ui/components/input"; +import { Button } from "@pram/ui/components/button"; import { toast } from "sonner"; import { getErrorMessage } from "@/lib/error-utils"; import { useDebouncedValue } from "@/hooks/use-debounced-value"; import { DataTable } from "@/components/admin/DataTable"; -import type { ColumnDef } from "@/components/admin/DataTable"; export const Route = createFileRoute("/admin/credits")({ component: AdminCredits, diff --git a/apps/web/src/routes/admin.featured.tsx b/apps/web/src/routes/admin.featured.tsx index 608e1204864904ff047f20ba0d56ee17f64b5129..3d15eaaa0e9ea0a947ecdd0a34d0152623ec1e14 100644 --- a/apps/web/src/routes/admin.featured.tsx +++ b/apps/web/src/routes/admin.featured.tsx @@ -2,9 +2,9 @@ import { useState } from "react"; import { createFileRoute } from "@tanstack/react-router"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { trpc } from "@/utils/trpc"; -import { Input } from "@labas/ui/components/input"; -import { Button } from "@labas/ui/components/button"; -import { Tabs, TabsList, TabsTrigger, TabsContent } from "@labas/ui/components/tabs"; +import { Input } from "@pram/ui/components/input"; +import { Button } from "@pram/ui/components/button"; +import { Tabs, TabsList, TabsTrigger, TabsContent } from "@pram/ui/components/tabs"; import { toast } from "sonner"; import { getErrorMessage } from "@/lib/error-utils"; import { useDebouncedValue } from "@/hooks/use-debounced-value"; diff --git a/apps/web/src/routes/admin.index.tsx b/apps/web/src/routes/admin.index.tsx index bf4ee0268878b86fa7d8d5e7dc1ed42f19e8f096..fef04394d3f9eb01f7207d09abbbe76440b745c2 100644 --- a/apps/web/src/routes/admin.index.tsx +++ b/apps/web/src/routes/admin.index.tsx @@ -2,8 +2,8 @@ import { useState } from "react"; import { createFileRoute } from "@tanstack/react-router"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { trpc } from "@/utils/trpc"; -import { Input } from "@labas/ui/components/input"; -import { Button } from "@labas/ui/components/button"; +import { Input } from "@pram/ui/components/input"; +import { Button } from "@pram/ui/components/button"; import { toast } from "sonner"; import { getErrorMessage } from "@/lib/error-utils"; diff --git a/apps/web/src/routes/admin.jobs.tsx b/apps/web/src/routes/admin.jobs.tsx index 676e64ac9dce222346c70423e3dd865e9f1a63a3..96b7abf67059fd4772231ada286f52854554fb13 100644 --- a/apps/web/src/routes/admin.jobs.tsx +++ b/apps/web/src/routes/admin.jobs.tsx @@ -2,7 +2,7 @@ import { useState } from "react"; import { createFileRoute } from "@tanstack/react-router"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { trpc } from "@/utils/trpc"; -import { Button } from "@labas/ui/components/button"; +import { Button } from "@pram/ui/components/button"; import { toast } from "sonner"; import { getErrorMessage } from "@/lib/error-utils"; import { DataTable } from "@/components/admin/DataTable"; diff --git a/apps/web/src/routes/admin.moderation.tsx b/apps/web/src/routes/admin.moderation.tsx index ddd08ee137c00a776c96d5eb5e987c15ff161e88..c5521abbef99d735031ac4fcb7dc00bc72789fdc 100644 --- a/apps/web/src/routes/admin.moderation.tsx +++ b/apps/web/src/routes/admin.moderation.tsx @@ -2,7 +2,7 @@ import { useState } from "react"; import { createFileRoute } from "@tanstack/react-router"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { trpc } from "@/utils/trpc"; -import { Button } from "@labas/ui/components/button"; +import { Button } from "@pram/ui/components/button"; import { toast } from "sonner"; import { getErrorMessage } from "@/lib/error-utils"; import { DataTable } from "@/components/admin/DataTable"; diff --git a/apps/web/src/routes/admin.users.tsx b/apps/web/src/routes/admin.users.tsx index 9c98917e234bd09d926b40746759e02564a66d5e..87f637992292591e9a3ef0831af51f6a0ebac0b2 100644 --- a/apps/web/src/routes/admin.users.tsx +++ b/apps/web/src/routes/admin.users.tsx @@ -2,8 +2,8 @@ import { useState } from "react"; import { createFileRoute } from "@tanstack/react-router"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { trpc } from "@/utils/trpc"; -import { Input } from "@labas/ui/components/input"; -import { Button } from "@labas/ui/components/button"; +import { Input } from "@pram/ui/components/input"; +import { Button } from "@pram/ui/components/button"; import { toast } from "sonner"; import { getErrorMessage } from "@/lib/error-utils"; import { useDebouncedValue } from "@/hooks/use-debounced-value"; diff --git a/apps/web/src/routes/attempt.$id.tsx b/apps/web/src/routes/attempt.$id.tsx index 3c172506dce0573aac7418f1b28d41e3c49c9cfe..5ee5da657b93cd0c3a94b5c8eb7215d65a095f75 100644 --- a/apps/web/src/routes/attempt.$id.tsx +++ b/apps/web/src/routes/attempt.$id.tsx @@ -3,8 +3,8 @@ import { useQuery, useMutation } from "@tanstack/react-query"; import { createFileRoute, redirect, Link } from "@tanstack/react-router"; import { authClient } from "@/lib/auth-client"; import { trpc } from "@/utils/trpc"; -import { Button } from "@labas/ui/components/button"; -import { Card, CardContent } from "@labas/ui/components/card"; +import { Button } from "@pram/ui/components/button"; +import { Card, CardContent } from "@pram/ui/components/card"; import { MaterialIcon } from "@/components/ui/MaterialIcon"; import { formatTime } from "@/lib/time"; import type { Question } from "@/lib/types"; @@ -23,13 +23,6 @@ interface AnswerItem { timeSpentSec?: number; } -interface QuestionAnswer { - q: Question; - ans: AnswerItem | null; - secIdx: number; - qIdx: number; -} - export const Route = createFileRoute("/attempt/$id")({ component: AttemptResultComponent, beforeLoad: async () => { diff --git a/apps/web/src/routes/dashboard.tsx b/apps/web/src/routes/dashboard.tsx index 10782826e3b3436c3c2cb1350c3cd19909a5e8cb..857765036bec003200ffa8886bb0e23b22a51b9a 100644 --- a/apps/web/src/routes/dashboard.tsx +++ b/apps/web/src/routes/dashboard.tsx @@ -4,8 +4,8 @@ import { createFileRoute, redirect, Link } from "@tanstack/react-router"; import { authClient } from "@/lib/auth-client"; import { routeShell } from "@/lib/route-shell"; import { trpc } from "@/utils/trpc"; -import { Button } from "@labas/ui/components/button"; -import { Card, CardContent } from "@labas/ui/components/card"; +import { Button } from "@pram/ui/components/button"; +import { Card, CardContent } from "@pram/ui/components/card"; import { MaterialIcon } from "@/components/ui/MaterialIcon"; import { TourGuide } from "@/components/TourGuide"; import { DonationModal } from "@/components/DonationModal"; @@ -118,7 +118,7 @@ function HomeComponent() { - Selamat Datang di Labas + Selamat Datang di Pram Platform latihan ujian bahasa berbasis AI. Ikuti 3 langkah mudah untuk memulai: diff --git a/apps/web/src/routes/forgot-password.tsx b/apps/web/src/routes/forgot-password.tsx index 9a5622e0fe2e1a232bb997cbdc6e776f8ba23673..336be3bf8d33f88b59c9f4d696554304febb6ffb 100644 --- a/apps/web/src/routes/forgot-password.tsx +++ b/apps/web/src/routes/forgot-password.tsx @@ -1,10 +1,10 @@ import { createFileRoute, Link, useNavigate } from "@tanstack/react-router"; import { useState, useEffect, useRef } from "react"; import { useMutation } from "@tanstack/react-query"; -import { Button } from "@labas/ui/components/button"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@labas/ui/components/card"; -import { Input } from "@labas/ui/components/input"; -import { Label } from "@labas/ui/components/label"; +import { Button } from "@pram/ui/components/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@pram/ui/components/card"; +import { Input } from "@pram/ui/components/input"; +import { Label } from "@pram/ui/components/label"; import { toast } from "sonner"; import { getErrorMessage } from "@/lib/error-utils"; import { trpc } from "@/utils/trpc"; @@ -242,7 +242,7 @@ function RouteComponent() { if (!email) { return ( - + ); @@ -250,7 +250,7 @@ function RouteComponent() { return ( - + ); diff --git a/apps/web/src/routes/jobs.tsx b/apps/web/src/routes/jobs.tsx index 452d44b54190ccb0aae5887d0f8a5a299ffc119e..90c25c04927f9888c19bb0ca4fe25805555175d6 100644 --- a/apps/web/src/routes/jobs.tsx +++ b/apps/web/src/routes/jobs.tsx @@ -4,10 +4,10 @@ import { createFileRoute, redirect, Link } from "@tanstack/react-router"; import { z } from "zod"; import { authClient } from "@/lib/auth-client"; import { trpc } from "@/utils/trpc"; -import { Card, CardContent, CardHeader, CardTitle } from "@labas/ui/components/card"; -import { Button } from "@labas/ui/components/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@pram/ui/components/card"; +import { Button } from "@pram/ui/components/button"; import { MaterialIcon } from "@/components/ui/MaterialIcon"; -import type { GenerationResult } from "@labas/ai"; +import type { GenerationResult } from "@pram/ai"; interface LogEntry { step: string; diff --git a/apps/web/src/routes/leaderboard.tsx b/apps/web/src/routes/leaderboard.tsx index bc65d0a4e42611418e0fc906b529df25a26cc064..004b74d5141df231f1e08a87c8099c7856a61d30 100644 --- a/apps/web/src/routes/leaderboard.tsx +++ b/apps/web/src/routes/leaderboard.tsx @@ -3,8 +3,8 @@ import { createFileRoute, redirect, Link } from "@tanstack/react-router"; import { useState } from "react"; import { authClient } from "@/lib/auth-client"; import { trpc } from "@/utils/trpc"; -import { Button } from "@labas/ui/components/button"; -import { Card, CardContent } from "@labas/ui/components/card"; +import { Button } from "@pram/ui/components/button"; +import { Card, CardContent } from "@pram/ui/components/card"; import { Select, SelectContent, @@ -12,7 +12,7 @@ import { SelectItem, SelectTrigger, SelectValue, -} from "@labas/ui/components/select"; +} from "@pram/ui/components/select"; import { MaterialIcon } from "@/components/ui/MaterialIcon"; import { EXAM_TYPES } from "@/lib/exam-constants"; import { RankingPodium } from "@/components/leaderboard/RankingPodium"; diff --git a/apps/web/src/routes/login.tsx b/apps/web/src/routes/login.tsx index 359cf2571d16696b1a6c8df824a2f300c396c6d4..fca1a61aca2e9fd199de53f79ee57ab5d960201b 100644 --- a/apps/web/src/routes/login.tsx +++ b/apps/web/src/routes/login.tsx @@ -16,7 +16,7 @@ function RouteComponent() { return ( - + pram {showSignIn ? ( setShowSignIn(false)} /> ) : ( diff --git a/apps/web/src/routes/me.tsx b/apps/web/src/routes/me.tsx index 96fba9ce8527f55bc9f3cbf8d09263c5a07682f8..33fc84e23bd5562de4581cea43d64a166dfe9f00 100644 --- a/apps/web/src/routes/me.tsx +++ b/apps/web/src/routes/me.tsx @@ -3,9 +3,9 @@ import { useQuery, useMutation } from "@tanstack/react-query"; import { createFileRoute, redirect, Link } from "@tanstack/react-router"; import { authClient } from "@/lib/auth-client"; import { trpc } from "@/utils/trpc"; -import { Button } from "@labas/ui/components/button"; -import { Input } from "@labas/ui/components/input"; -import { Card, CardContent } from "@labas/ui/components/card"; +import { Button } from "@pram/ui/components/button"; +import { Input } from "@pram/ui/components/input"; +import { Card, CardContent } from "@pram/ui/components/card"; import { MaterialIcon } from "@/components/ui/MaterialIcon"; import { Avatar } from "@/components/ui/Avatar"; diff --git a/apps/web/src/routes/package.$id.attempt.$attemptId.tsx b/apps/web/src/routes/package.$id.attempt.$attemptId.tsx index 431cbe0a6dcae4e04337349f53d975553b479190..17a90146ddea595593ebf26302b5b52c27afa22e 100644 --- a/apps/web/src/routes/package.$id.attempt.$attemptId.tsx +++ b/apps/web/src/routes/package.$id.attempt.$attemptId.tsx @@ -5,7 +5,7 @@ import { authClient } from "@/lib/auth-client"; import { trpc } from "@/utils/trpc"; import { useTestSession } from "@/hooks/use-test-session"; import { trackUmamiEvent, AnalyticsEvent } from "@/lib/umami"; -import { Button } from "@labas/ui/components/button"; +import { Button } from "@pram/ui/components/button"; import { MaterialIcon } from "@/components/ui/MaterialIcon"; import { AttemptTestView } from "@/components/test/AttemptTestView"; import { routeShell } from "@/lib/route-shell"; diff --git a/apps/web/src/routes/package.$id.index.tsx b/apps/web/src/routes/package.$id.index.tsx index 95880db2a7e7ac7ded21474e6a0273dd652429f2..3391a180a2ee524735954682cd765987000078d4 100644 --- a/apps/web/src/routes/package.$id.index.tsx +++ b/apps/web/src/routes/package.$id.index.tsx @@ -3,8 +3,8 @@ import { useQuery, useMutation } from "@tanstack/react-query"; import { createFileRoute, redirect, Link } from "@tanstack/react-router"; import { authClient } from "@/lib/auth-client"; import { trpc } from "@/utils/trpc"; -import { Button } from "@labas/ui/components/button"; -import { Card, CardContent } from "@labas/ui/components/card"; +import { Button } from "@pram/ui/components/button"; +import { Card, CardContent } from "@pram/ui/components/card"; import { MaterialIcon } from "@/components/ui/MaterialIcon"; import { formatLabel } from "@/lib/format"; import { EditPackageModal } from "@/components/package/EditPackageModal"; diff --git a/apps/web/src/routes/package.$id.take.tsx b/apps/web/src/routes/package.$id.take.tsx index cf61d9f5a8ef420998c57cdca3a492b9a25a2ea9..0067dfd00b0acbdb735c6c5f12830031ddb262f3 100644 --- a/apps/web/src/routes/package.$id.take.tsx +++ b/apps/web/src/routes/package.$id.take.tsx @@ -5,8 +5,8 @@ import { authClient } from "@/lib/auth-client"; import { trpc } from "@/utils/trpc"; import { useTestSession } from "@/hooks/use-test-session"; import { trackUmamiEvent, AnalyticsEvent } from "@/lib/umami"; -import { Button } from "@labas/ui/components/button"; -import { Card, CardContent } from "@labas/ui/components/card"; +import { Button } from "@pram/ui/components/button"; +import { Card, CardContent } from "@pram/ui/components/card"; import { MaterialIcon } from "@/components/ui/MaterialIcon"; import { AttemptTestView } from "@/components/test/AttemptTestView"; import { routeShell } from "@/lib/route-shell"; diff --git a/apps/web/src/routes/profile.$userId.tsx b/apps/web/src/routes/profile.$userId.tsx index b17dcb5c8d12fa150689c4a467f87c4b66f5dd6c..2dd375c409b70975844f5d1a071e7ca985925a11 100644 --- a/apps/web/src/routes/profile.$userId.tsx +++ b/apps/web/src/routes/profile.$userId.tsx @@ -1,7 +1,7 @@ import { useQuery } from "@tanstack/react-query"; import { createFileRoute, Link } from "@tanstack/react-router"; import { trpc } from "@/utils/trpc"; -import { Card, CardContent } from "@labas/ui/components/card"; +import { Card, CardContent } from "@pram/ui/components/card"; import { MaterialIcon } from "@/components/ui/MaterialIcon"; import { Avatar } from "@/components/ui/Avatar"; diff --git a/apps/web/src/routes/setup-avatar.tsx b/apps/web/src/routes/setup-avatar.tsx index 570d398dc6d5300c992d4aaa3f0b00f828d2ee49..e210a59ca08a663609e5395b4ad0b7ffa3ec00c2 100644 --- a/apps/web/src/routes/setup-avatar.tsx +++ b/apps/web/src/routes/setup-avatar.tsx @@ -7,10 +7,10 @@ import { } from "@tanstack/react-router"; import { authClient } from "@/lib/auth-client"; import { trpc } from "@/utils/trpc"; -import { Button } from "@labas/ui/components/button"; -import { Card, CardContent } from "@labas/ui/components/card"; +import { Button } from "@pram/ui/components/button"; +import { Card, CardContent } from "@pram/ui/components/card"; import { MaterialIcon } from "@/components/ui/MaterialIcon"; -import { cn } from "@labas/ui/lib/utils"; +import { cn } from "@pram/ui/lib/utils"; import { buildDiceBearLoreleiUrl, randomSeed, diff --git a/apps/web/src/routes/verify-email.tsx b/apps/web/src/routes/verify-email.tsx index 61df3bf6871293fa7fc19a94b389fcf5026a9df1..b19383e1175bc3214c5efa923e29032947c448ae 100644 --- a/apps/web/src/routes/verify-email.tsx +++ b/apps/web/src/routes/verify-email.tsx @@ -1,9 +1,9 @@ import { createFileRoute, Link, useNavigate, redirect } from "@tanstack/react-router"; import { useState, useEffect, useRef } from "react"; import { useMutation } from "@tanstack/react-query"; -import { Button } from "@labas/ui/components/button"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@labas/ui/components/card"; -import { Input } from "@labas/ui/components/input"; +import { Button } from "@pram/ui/components/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@pram/ui/components/card"; +import { Input } from "@pram/ui/components/input"; import { toast } from "sonner"; import type { TurnstileInstance } from "@marsidev/react-turnstile"; import { getErrorMessage } from "@/lib/error-utils"; diff --git a/apps/web/src/utils/trpc.ts b/apps/web/src/utils/trpc.ts index 3094d73f5dc27c3ad8ffd1283d64aff03cbf3fae..ecd4c5b8901dc7d833e157f6cef122b90e7958c3 100644 --- a/apps/web/src/utils/trpc.ts +++ b/apps/web/src/utils/trpc.ts @@ -1,5 +1,5 @@ -import type { AppRouter } from "@labas/api/routers/index"; -import { env } from "@labas/env/web"; +import type { AppRouter } from "@pram/api/routers/index"; +import { env } from "@pram/env/web"; import { QueryCache, QueryClient } from "@tanstack/react-query"; import { createTRPCClient, httpBatchLink, TRPCClientError } from "@trpc/client"; import { createTRPCOptionsProxy } from "@trpc/tanstack-react-query"; diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 377719442b29c0fd5ae51765b3da83eb11884796..ef489c1f50f45cf43cc6e67d782bbe82f7691a56 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -11,9 +11,9 @@ "rootDirs": ["."], "paths": { "@/*": ["./src/*"], - "@labas/ui/*": ["../../packages/ui/src/*"], - "@labas/ai": ["../../packages/ai/src/index.ts"], - "@labas/ai/*": ["../../packages/ai/src/*.ts"] + "@pram/ui/*": ["../../packages/ui/src/*"], + "@pram/ai": ["../../packages/ai/src/index.ts"], + "@pram/ai/*": ["../../packages/ai/src/*.ts"] } } } diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 9762118a09919f234e2ae6c5257e8c264f8f6b47..4dfa304be6f1036282ad89290fdebd9f5671e740 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -43,8 +43,8 @@ export default defineConfig({ VitePWA({ registerType: "autoUpdate", manifest: { - name: "labas", - short_name: "labas", + name: "pram", + short_name: "pram", description: "AI-powered multi-language test practice platform", theme_color: "#0c0c0c", }, diff --git a/bts.jsonc b/bts.jsonc index a936a7cd678ef1a7b92cc73d2c80faed430e955f..cc158ea17abbf915cfc48ea313f92c499f297807 100644 --- a/bts.jsonc +++ b/bts.jsonc @@ -13,7 +13,7 @@ "$schema": "https://r2.better-t-stack.dev/schema.json", "version": "3.27.5", "createdAt": "2026-04-25T04:42:01.769Z", - "reproducibleCommand": "bun create better-t-stack@latest labas --frontend tanstack-router --backend hono --runtime bun --database postgres --orm drizzle --api trpc --auth better-auth --payments none --addons pwa skills turborepo --examples none --db-setup docker --web-deploy none --server-deploy none --git --package-manager bun --install", + "reproducibleCommand": "bun create better-t-stack@latest pram --frontend tanstack-router --backend hono --runtime bun --database postgres --orm drizzle --api trpc --auth better-auth --payments none --addons pwa skills turborepo --examples none --db-setup docker --web-deploy none --server-deploy none --git --package-manager bun --install", "database": "postgres", "orm": "drizzle", "backend": "hono", diff --git a/bun.lock b/bun.lock index a4fa2f3242e4a380b3c0b161483f1bcd17c73cf3..6754c0c4e82b553368bb57d5c5822957eff3dbea 100644 --- a/bun.lock +++ b/bun.lock @@ -3,15 +3,15 @@ "configVersion": 1, "workspaces": { "": { - "name": "labas", + "name": "pram", "dependencies": { - "@labas/env": "workspace:*", + "@pram/env": "workspace:*", "dotenv": "catalog:", "react-joyride": "^3.1.0", "zod": "catalog:", }, "devDependencies": { - "@labas/config": "workspace:*", + "@pram/config": "workspace:*", "@types/bun": "catalog:", "turbo": "^2.8.12", "typescript": "catalog:", @@ -21,10 +21,10 @@ "name": "server", "dependencies": { "@hono/trpc-server": "^0.4.0", - "@labas/api": "workspace:*", - "@labas/auth": "workspace:*", - "@labas/db": "workspace:*", - "@labas/env": "workspace:*", + "@pram/api": "workspace:*", + "@pram/auth": "workspace:*", + "@pram/db": "workspace:*", + "@pram/env": "workspace:*", "@trpc/server": "catalog:", "better-auth": "catalog:", "dotenv": "catalog:", @@ -33,7 +33,7 @@ "zod": "catalog:", }, "devDependencies": { - "@labas/config": "workspace:*", + "@pram/config": "workspace:*", "@types/bun": "catalog:", "@types/nodemailer": "^8.0.0", "tsdown": "^0.21.9", @@ -45,12 +45,12 @@ "version": "0.0.0", "dependencies": { "@hookform/resolvers": "^5.2.2", - "@labas/ai": "workspace:*", - "@labas/api": "workspace:*", - "@labas/auth": "workspace:*", - "@labas/env": "workspace:*", - "@labas/ui": "workspace:*", "@marsidev/react-turnstile": "^1.5.2", + "@pram/ai": "workspace:*", + "@pram/api": "workspace:*", + "@pram/auth": "workspace:*", + "@pram/env": "workspace:*", + "@pram/ui": "workspace:*", "@tailwindcss/vite": "^4.2.2", "@tanstack/react-form": "^1.28.0", "@tanstack/react-query": "^5.90.12", @@ -73,7 +73,7 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", - "@labas/config": "workspace:*", + "@pram/config": "workspace:*", "@tanstack/react-query-devtools": "^5.91.1", "@tanstack/react-router-devtools": "^1.166.13", "@tanstack/router-plugin": "^1.167.22", @@ -92,23 +92,23 @@ }, }, "packages/ai": { - "name": "@labas/ai", + "name": "@pram/ai", "dependencies": { "zod": "catalog:", "zod-to-json-schema": "^3.25.2", }, "devDependencies": { - "@labas/config": "workspace:*", + "@pram/config": "workspace:*", "typescript": "catalog:", }, }, "packages/api": { - "name": "@labas/api", + "name": "@pram/api", "dependencies": { - "@labas/ai": "workspace:*", - "@labas/auth": "workspace:*", - "@labas/db": "workspace:*", - "@labas/env": "workspace:*", + "@pram/ai": "workspace:*", + "@pram/auth": "workspace:*", + "@pram/db": "workspace:*", + "@pram/env": "workspace:*", "@trpc/client": "catalog:", "@trpc/server": "catalog:", "bcryptjs": "^3.0.3", @@ -122,7 +122,7 @@ }, "devDependencies": { "@electric-sql/pglite": "^0.4.5", - "@labas/config": "workspace:*", + "@pram/config": "workspace:*", "@types/bcryptjs": "^3.0.0", "@types/nodemailer": "^8.0.0", "hono": "catalog:", @@ -130,41 +130,41 @@ }, }, "packages/auth": { - "name": "@labas/auth", + "name": "@pram/auth", "dependencies": { - "@labas/db": "workspace:*", - "@labas/env": "workspace:*", + "@pram/db": "workspace:*", + "@pram/env": "workspace:*", "better-auth": "catalog:", "dotenv": "catalog:", "zod": "catalog:", }, "devDependencies": { - "@labas/config": "workspace:*", + "@pram/config": "workspace:*", "typescript": "catalog:", }, }, "packages/config": { - "name": "@labas/config", + "name": "@pram/config", "version": "0.0.0", }, "packages/db": { - "name": "@labas/db", + "name": "@pram/db", "dependencies": { - "@labas/env": "workspace:*", + "@pram/env": "workspace:*", "dotenv": "catalog:", "drizzle-orm": "^0.45.1", "pg": "^8.17.1", "zod": "catalog:", }, "devDependencies": { - "@labas/config": "workspace:*", + "@pram/config": "workspace:*", "@types/pg": "^8.16.0", "drizzle-kit": "^0.31.8", "typescript": "catalog:", }, }, "packages/env": { - "name": "@labas/env", + "name": "@pram/env", "version": "0.0.0", "dependencies": { "@t3-oss/env-core": "^0.13.1", @@ -172,13 +172,13 @@ "zod": "catalog:", }, "devDependencies": { - "@labas/config": "workspace:*", + "@pram/config": "workspace:*", "@types/bun": "catalog:", "typescript": "catalog:", }, }, "packages/ui": { - "name": "@labas/ui", + "name": "@pram/ui", "version": "0.0.0", "dependencies": { "@base-ui/react": "^1.0.0", @@ -194,7 +194,7 @@ "tw-animate-css": "^1.3.4", }, "devDependencies": { - "@labas/config": "workspace:*", + "@pram/config": "workspace:*", "@types/react": "^19.2.10", "@types/react-dom": "catalog:", "tailwindcss": "^4.1.18", @@ -617,20 +617,6 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - "@labas/ai": ["@labas/ai@workspace:packages/ai"], - - "@labas/api": ["@labas/api@workspace:packages/api"], - - "@labas/auth": ["@labas/auth@workspace:packages/auth"], - - "@labas/config": ["@labas/config@workspace:packages/config"], - - "@labas/db": ["@labas/db@workspace:packages/db"], - - "@labas/env": ["@labas/env@workspace:packages/env"], - - "@labas/ui": ["@labas/ui@workspace:packages/ui"], - "@marsidev/react-turnstile": ["@marsidev/react-turnstile@1.5.2", "", { "peerDependencies": { "react": "^17.0.2 || ^18.0.0 || ^19.0", "react-dom": "^17.0.2 || ^18.0.0 || ^19.0" } }, "sha512-+3aBPxp86JzSC0ZmgyonoGoUEENcUkH3LGahXSpkV87ArvD2DzRCmPgh0FyQk6PQRmJwQJDAfwNavFsxUxMQWA=="], "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="], @@ -673,6 +659,20 @@ "@oxc-project/types": ["@oxc-project/types@0.127.0", "", {}, "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ=="], + "@pram/ai": ["@pram/ai@workspace:packages/ai"], + + "@pram/api": ["@pram/api@workspace:packages/api"], + + "@pram/auth": ["@pram/auth@workspace:packages/auth"], + + "@pram/config": ["@pram/config@workspace:packages/config"], + + "@pram/db": ["@pram/db@workspace:packages/db"], + + "@pram/env": ["@pram/env@workspace:packages/env"], + + "@pram/ui": ["@pram/ui@workspace:packages/ui"], + "@quansync/fs": ["@quansync/fs@1.0.0", "", { "dependencies": { "quansync": "^1.0.0" } }, "sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ=="], "@reduxjs/toolkit": ["@reduxjs/toolkit@2.11.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ=="], diff --git a/deploy/COOLIFY.md b/deploy/COOLIFY.md index 0e1dfbb1eb0e14f349bb5922cccc56f8e8d4f389..257d753488e712d9a95204056679ba5c1c268942 100644 --- a/deploy/COOLIFY.md +++ b/deploy/COOLIFY.md @@ -1,6 +1,6 @@ -# Deploying Labas on Coolify +# Deploying Pram on Coolify -Labas is a Turborepo monorepo with separate **web** (Vite SPA) and **server** (Hono API) apps. Deploy them as **two Coolify applications** from the same Git repository, plus **PostgreSQL** and **Redis**. +Pram is a Turborepo monorepo with separate **web** (Vite SPA) and **server** (Hono API) apps. Deploy them as **two Coolify applications** from the same Git repository, plus **PostgreSQL** and **Redis**. ``` User → app.example.com (Web) → api.example.com (Server) → PostgreSQL + Redis @@ -23,13 +23,13 @@ Both applications use the **repository root** as Base Directory. They differ onl 1. In Coolify, create a **PostgreSQL** database service. 2. Note the **internal hostname**, port, username, and password. -3. Create a database named `labas` (or adjust `DATABASE_URL` accordingly). +3. Create a database named `pram` (or adjust `DATABASE_URL` accordingly). 4. Keep the service in the same Coolify project/network as the server app. Example internal connection (hostname varies by Coolify): ```env -DATABASE_URL=postgresql://postgres:YOUR_PASSWORD@YOUR_POSTGRES_HOST:5432/labas +DATABASE_URL=postgresql://postgres:YOUR_PASSWORD@YOUR_POSTGRES_HOST:5432/pram ``` --- @@ -81,7 +81,7 @@ Set these on the **server** application. All are validated at startup in `packag ```env NODE_ENV=production -DATABASE_URL=postgresql://postgres:YOUR_PASSWORD@YOUR_POSTGRES_HOST:5432/labas +DATABASE_URL=postgresql://postgres:YOUR_PASSWORD@YOUR_POSTGRES_HOST:5432/pram REDIS_URL=redis://YOUR_REDIS_HOST:6379 BETTER_AUTH_SECRET=YOUR_RANDOM_SECRET_MIN_32_CHARS @@ -94,7 +94,7 @@ SMTP_HOST=smtp.example.com SMTP_PORT=587 SMTP_USER=your-smtp-user SMTP_PASS=your-smtp-password -SMTP_FROM=Labas +SMTP_FROM=Pram ``` Generate secrets: @@ -139,7 +139,7 @@ To get the connection string: 3. Paste into the **server app** env: ```env -DATABASE_URL=postgresql://postgres:YOUR_PASSWORD@YOUR_POSTGRES_HOST:5432/labas +DATABASE_URL=postgresql://postgres:YOUR_PASSWORD@YOUR_POSTGRES_HOST:5432/pram ``` Verify in the server app **Terminal**: @@ -243,4 +243,4 @@ Rebuild the web app whenever the API URL changes. | [`deploy/server.nixpacks.toml`](./server.nixpacks.toml) | API server | | [`deploy/web.nixpacks.toml`](./web.nixpacks.toml) | Web app (Option B) | -Both run `bun install` from the monorepo root so workspace packages (`@labas/*`) resolve correctly. +Both run `bun install` from the monorepo root so workspace packages (`@pram/*`) resolve correctly. diff --git a/deploy/server.nixpacks.toml b/deploy/server.nixpacks.toml index 5d374360518c1d171605b92a12654fa343c491c5..74a5bc2601052cdd6c4cb3ce7b3988eb285fc9e5 100644 --- a/deploy/server.nixpacks.toml +++ b/deploy/server.nixpacks.toml @@ -1,4 +1,4 @@ -# Nixpacks config for the Labas API server (Hono + tRPC). +# Nixpacks config for the Pram API server (Hono + tRPC). # Point Coolify to this file: NIXPACKS_CONFIG_FILE=deploy/server.nixpacks.toml [phases.setup] diff --git a/deploy/web.nixpacks.toml b/deploy/web.nixpacks.toml index 9f1e98747b9cc17d4be5f7bcc78aa1264daa7c2e..9308ef220705d22cf73d80c55e465af4dc31c6d7 100644 --- a/deploy/web.nixpacks.toml +++ b/deploy/web.nixpacks.toml @@ -1,4 +1,4 @@ -# Nixpacks config for the Labas web app (Vite SPA). +# Nixpacks config for the Pram web app (Vite SPA). # Point Coolify to this file: NIXPACKS_CONFIG_FILE=deploy/web.nixpacks.toml # # Requires VITE_SERVER_URL at build time (enable "Available at Buildtime" in Coolify). diff --git a/fix-mojibake.cjs b/fix-mojibake.cjs new file mode 100644 index 0000000000000000000000000000000000000000..cc1b5ad712975748e6801545e8c4d1293fca8873 --- /dev/null +++ b/fix-mojibake.cjs @@ -0,0 +1,41 @@ +const fs = require('fs'); +const path = require('path'); + +const replacements = { + '─': '─', + '│': '│', + '├': '├', + 'â””': '└', +}; + +function processDirectory(dir) { + const files = fs.readdirSync(dir); + for (const file of files) { + if (file === 'node_modules' || file === '.git' || file === 'dist' || file === 'build') continue; + + const fullPath = path.join(dir, file); + const stat = fs.statSync(fullPath); + + if (stat.isDirectory()) { + processDirectory(fullPath); + } else if (stat.isFile() && (fullPath.endsWith('.ts') || fullPath.endsWith('.tsx') || fullPath.endsWith('.html') || fullPath.endsWith('.md') || fullPath.endsWith('.js') || fullPath.endsWith('.json') || fullPath.endsWith('.env.example'))) { + let content = fs.readFileSync(fullPath, 'utf8'); + let modified = false; + + for (const [bad, good] of Object.entries(replacements)) { + if (content.includes(bad)) { + content = content.split(bad).join(good); + modified = true; + } + } + + if (modified) { + fs.writeFileSync(fullPath, content, 'utf8'); + console.log(`Fixed: ${fullPath}`); + } + } + } +} + +processDirectory(__dirname); +console.log('Done!'); diff --git a/package.json b/package.json index 87e1600074fa60044cc5fafec0ffd369d6ecf635..ee68e6df6fcdd076b1d896f88714767480c5b51e 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "labas", + "name": "pram", "version": "0.1.0", "private": true, "workspaces": { @@ -22,31 +22,31 @@ }, "type": "module", "scripts": { - "dev": "turbo dev", - "build": "turbo build", - "test": "turbo test", - "check-types": "turbo check-types", + "dev": "turbo dev --filter=!.", + "build": "turbo build --filter=!.", + "test": "turbo test --filter=!.", + "check-types": "turbo check-types --filter=!.", "compress:og": "bun scripts/compress-og-image.ts", "dev:web": "turbo -F web dev", "dev:server": "turbo -F server dev", - "db:push": "turbo -F @labas/db db:push", - "db:studio": "turbo -F @labas/db db:studio", - "db:generate": "turbo -F @labas/db db:generate", - "db:migrate": "turbo -F @labas/db db:migrate", - "db:seed": "turbo -F @labas/db db:seed", - "db:start": "turbo -F @labas/db db:start", - "db:watch": "turbo -F @labas/db db:watch", - "db:stop": "turbo -F @labas/db db:stop", - "db:down": "turbo -F @labas/db db:down" + "db:push": "turbo -F @pram/db db:push", + "db:studio": "turbo -F @pram/db db:studio", + "db:generate": "turbo -F @pram/db db:generate", + "db:migrate": "turbo -F @pram/db db:migrate", + "db:seed": "turbo -F @pram/db db:seed", + "db:start": "turbo -F @pram/db db:start", + "db:watch": "turbo -F @pram/db db:watch", + "db:stop": "turbo -F @pram/db db:stop", + "db:down": "turbo -F @pram/db db:down" }, "dependencies": { - "@labas/env": "workspace:*", + "@pram/env": "workspace:*", "dotenv": "catalog:", "react-joyride": "^3.1.0", "zod": "catalog:" }, "devDependencies": { - "@labas/config": "workspace:*", + "@pram/config": "workspace:*", "@types/bun": "catalog:", "turbo": "^2.8.12", "typescript": "catalog:" diff --git a/packages/ai/LICENSE b/packages/ai/LICENSE index 1fc95992a2e8ad86439ad2a53a3ad137e2c52dc2..566070fb89d97a3937c0d03275e52536067e9e44 100644 --- a/packages/ai/LICENSE +++ b/packages/ai/LICENSE @@ -1,6 +1,6 @@ -MIT License +MIT License -Copyright (c) 2026 Labas Project Authors +Copyright (c) 2026 Pram Project Authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/packages/ai/package.json b/packages/ai/package.json index 8fb73c47803b123733c8a7aaf0888a6760a3962f..4792b1c7568d3e83942ce27a05728c091267bdc9 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,5 +1,5 @@ { - "name": "@labas/ai", + "name": "@pram/ai", "license": "MIT", "type": "module", "exports": { @@ -18,7 +18,7 @@ "test": "bun test" }, "devDependencies": { - "@labas/config": "workspace:*", + "@pram/config": "workspace:*", "typescript": "catalog:" } } diff --git a/packages/ai/src/agentic.ts b/packages/ai/src/agentic.ts index 37999b6daaefeff85213b3384557a845f7d0359f..fbb4a3799f15499e9798d4ba46d3124c5f8982e4 100644 --- a/packages/ai/src/agentic.ts +++ b/packages/ai/src/agentic.ts @@ -183,7 +183,7 @@ Rules: - Each question must be directly answerable from the passage - Use "passageText" field with the relevant excerpt from the passage (or full passage if needed) - Questions should test real comprehension, not surface recall -- For multiple choice: always provide 4 options (A, B, C, D) with one clearly correct answer +- For multiple choice: always provide 5 options (A, B, C, D, E) with one clearly correct answer - Options must be plausible distractors ${OPTION_QUALITY_RULES} ${buildContentLanguageRules(input.examType)} diff --git a/packages/ai/src/language-rules.ts b/packages/ai/src/language-rules.ts index e0dde078d590dad86384e85ad24238c53da58364..9e764163981680ba536c77f97fff6546728ea709 100644 --- a/packages/ai/src/language-rules.ts +++ b/packages/ai/src/language-rules.ts @@ -4,105 +4,49 @@ interface QuestionLanguageFields { } /** Target language for exam content (passage, question, options). */ -export function getTargetLanguage(examType: string): string { - if (examType === "JLPT") return "Japanese"; - if (examType === "HSK") return "Chinese"; - if (examType === "GOETHE") return "German"; - if (examType === "TOPIK") return "Korean"; - if (examType === "TOAFL") return "Arabic"; - if (examType === "DELE") return "Spanish"; - return "English"; +export function getTargetLanguage(_examType: string): string { + return "Bahasa Indonesia"; } -const EXAMS_REQUIRING_NON_LATIN_SCRIPT = new Set(["JLPT", "HSK", "TOPIK"]); - const INDONESIAN_PROSE_MARKERS = - /\b(yang|dari|pada|Apakah|Menurut|paragraf|bacaan|teks|judul|penulis|artikel|soal|pilihan|jawaban|berikut|manakah|bagian|kalimat|isinya|topik|gagasan|benar|karena|merujuk|sesuai|artinya|makna|kata|terdapat|disebutkan|dijelaskan|adalah|jadi|oleh|opsi|pilihan|jawabannya|penjelasan)\b/i; + /\b(yang|dari|pada|Apakah|Menurut|paragraf|bacaan|teks|judul|penulis|artikel|soal|pilihan|jawaban|berikut|manakah|bagian|kalimat|isinya|topik|gagasan|benar|karena|merujuk|sesuai|artinya|makna|kata|terdapat|disebutkan|dijelaskan|adalah|jadi|oleh|opsi|pilihan|jawabannya|penjelasan|Pancasila|UUD|NKRI|Bhinneka|Nasionalisme|Integritas|Negara|TIU|TWK|TKP)\b/i; -/** Strip exam-script characters to analyze Indonesian prose separately. */ +/** Strip foreign characters (heuristics) if needed, but for CPNS we mostly deal with Latin script. */ export function stripExamScriptForAnalysis(text: string): string { - return text - .replace(/[\u4E00-\u9FFF\u3400-\u4DBF\u3040-\u309F\u30A0-\u30FF\uAC00-\uD7AF\u0600-\u06FF]+/g, " ") - .replace(/\s+/g, " ") - .trim(); + return text.trim(); } -/** Latin prose looks like Bahasa Indonesia (not English-only or empty). */ +/** Latin prose looks like Bahasa Indonesia. */ export function hasIndonesianProse(text: string): boolean { const t = text.trim(); if (!t) return false; if (INDONESIAN_PROSE_MARKERS.test(t)) return true; - if (/^(The|This|That|According|Because|Therefore|Option|Answer|It is|In the)\b/i.test(t)) { - return false; - } return t.length >= 15; } -export function hasCJKScript(text: string): boolean { - return /[\u4E00-\u9FFF\u3400-\u4DBF\u3040-\u309F\u30A0-\u30FF\uAC00-\uD7AF]/.test(text); -} - -export function hasArabicScript(text: string): boolean { - return /[\u0600-\u06FF]/.test(text); -} - -function hasTargetScript(text: string, examType: string): boolean { - if (examType === "TOAFL") return hasArabicScript(text); - if (EXAMS_REQUIRING_NON_LATIN_SCRIPT.has(examType)) return hasCJKScript(text); - return true; -} - -/** Heuristic: Latin text that looks like Indonesian exam instructions, not English. */ -export function isLikelyIndonesianContent(text: string): boolean { - const t = text.trim(); - if (!t) return false; - if (hasCJKScript(t) || hasArabicScript(t)) return false; - return INDONESIAN_PROSE_MARKERS.test(t); -} - -export function buildContentLanguageRules(examType: string): string { - const target = getTargetLanguage(examType); +export function buildContentLanguageRules(_examType: string): string { return `- LANGUAGE RULE (critical): - * passageText, questionText, and every option "text" MUST be written in ${target} — the authentic exam language. - * NEVER write questionText or options in Bahasa Indonesia for ${examType}. - * explanation: tulis dalam Bahasa Indonesia; boleh sisipkan istilah/kanji/kata ${target} bila perlu (mis. 「環境」 atau kutipan singkat dari teks).`; + * passageText, questionText, and every option "text" MUST be written in Bahasa Indonesia — the authentic exam language for CPNS. + * explanation: tulis dalam Bahasa Indonesia yang mendalam dan mudah dimengerti.`; } -export function buildExplanationLanguageRule(examType?: string): string { - const target = examType ? getTargetLanguage(examType) : "bahasa ujian"; - return `- explanation — WAJIB ditulis dalam Bahasa Indonesia sebagai penjelasan utama. Boleh menyertakan istilah, kanji, atau kutipan singkat dalam ${target} jika relevan (mis. arti kanji, padanan kata). Jangan tulis seluruh explanation hanya dalam ${target} atau Inggris.`; +export function buildExplanationLanguageRule(_examType?: string): string { + return `- explanation — WAJIB ditulis dalam Bahasa Indonesia sebagai penjelasan utama.`; } export function getQuestionLanguageErrors( q: QuestionLanguageFields, - examType: string, + _examType: string, ): string[] { const errors: string[] = []; - const target = getTargetLanguage(examType); - - if (isLikelyIndonesianContent(q.questionText)) { - errors.push(`questionText must be ${target}, not Bahasa Indonesia`); - } - - if (EXAMS_REQUIRING_NON_LATIN_SCRIPT.has(examType) && !hasTargetScript(q.questionText, examType)) { - errors.push(`questionText must be written in ${target}`); - } - - if (examType === "IELTS" || examType === "TOEFL") { - if (isLikelyIndonesianContent(q.questionText)) { - errors.push("questionText must be English for IELTS/TOEFL"); - } + if (!hasIndonesianProse(q.questionText)) { + errors.push("questionText must be in Bahasa Indonesia"); } - for (const opt of q.options ?? []) { - if (isLikelyIndonesianContent(opt.text)) { - errors.push(`option ${opt.key} must be ${target}, not Bahasa Indonesia`); - } - if (EXAMS_REQUIRING_NON_LATIN_SCRIPT.has(examType) && !hasTargetScript(opt.text, examType)) { - errors.push(`option ${opt.key} must be written in ${target}`); + if (!hasIndonesianProse(opt.text) && opt.text.length > 5) { + errors.push(`option ${opt.key} must be in Bahasa Indonesia`); } } - return errors; } diff --git a/packages/ai/src/prompts.ts b/packages/ai/src/prompts.ts index 5fa91be76e793caffdfc90d934705e9b7aefbf23..ddcc90308f9cf83fb5aee5af3fd3b93854226ca8 100644 --- a/packages/ai/src/prompts.ts +++ b/packages/ai/src/prompts.ts @@ -10,40 +10,46 @@ export function buildQuickModePrompt(input: GenerationInput): string { const questionSchemaJson = getQuestionJsonSchemaDescription(); - return `You are an expert exam question writer for ${examType} ${section.toLowerCase()} section. + return `You are an expert exam question writer for CPNS (Seleksi Calon Pegawai Negeri Sipil) specifically for the ${section} section. -Generate ${questionCount} authentic, high-quality reading comprehension questions. +Generate ${questionCount} authentic, high-quality questions for CPNS. -EXAM: ${examType} +EXAM: CPNS SECTION: ${section} DIFFICULTY: ${difficulty}/5 TOPICS: ${topics.join(", ")} FORMATS TO GENERATE: ${formats.join(", ")} +CONTEXT FOR SECTIONS: +- TWK (Tes Wawasan Kebangsaan): Fokus pada Pancasila, UUD 1945, NKRI, Bhinneka Tunggal Ika, Nasionalisme, Integritas, Bela Negara, Pilar Negara, dan Bahasa Indonesia. +- TIU (Tes Intelegensia Umum): Fokus pada Verbal (Analogi, Silogisme, Analitis), Numerik (Deret Angka, Perbandingan Kuantitatif, Soal Cerita), dan Figural. +- TKP (Tes Karakteristik Pribadi): Fokus pada Pelayanan Publik, Jejaring Kerja, Sosial Budaya, TIK, Profesionalisme, dan Anti Radikalisme. +- Teknis (SKB Teknis Umum): Soal teknis sesuai jabatan yang dilamar. +- Pertanahan_Dasar / Pertanahan & Agraria: Hukum Agraria (UUPA No. 5/1960), jenis-jenis hak atas tanah (HM, HGU, HGB, HP, HPL), konversi hak, tanah negara, tanah adat, reforma agraria, regulasi BPN/ATR. +- Pengukuran_Kadastral / Pengukuran & Pemetaan Kadastral: Ilmu ukur tanah, pengukuran batas bidang, peta kadastral, sistem koordinat TM3, GPS/GNSS, drone, fotogrametri, Kadaster, KKP, BHUMI, Peta ZNT. +- Pendaftaran_Tanah / Pendaftaran Tanah & Sertipikat: PP No. 24/1997, PTSL, buku tanah, surat ukur, sertipikat, pemeliharaan data, balik nama, pemecahan/penggabungan, sertipikat elektronik, hak tanggungan. +- Pengadaan_Tanah / Pengadaan Tanah & Ganti Rugi: UU No. 2/2012, Perpres No. 71/2012, tahapan pengadaan, penetapan lokasi, inventarisasi, penilaian ganti rugi, musyawarah, konsignasi, PSN. +- Pengendalian_Pertanahan / Pengendalian & Penertiban Tanah: Tanah terlantar (PP No. 20/2021), penertiban absentee, monitoring penggunaan tanah, alih fungsi lahan, sengketa-konflik-perkara pertanahan, mediasi. +- Penilaian_Tanah / Penilaian Tanah & Properti: Zona Nilai Tanah (ZNT), NIR, metode penilaian tanah (perbandingan pasar/biaya/pendapatan), penilai publik, NJOP, faktor nilai tanah. + INSTRUCTIONS: ${buildContentLanguageRules(examType)} ${buildExplanationLanguageRule(examType)} -- For Korean (TOPIK): Focus on particles, honorifics (speech levels), and functional grammar. -- For Arabic (TOAFL): Support RTL text. Focus on I'rab (case endings/vowel changes) and grammar. -- For Spanish (DELE): Focus on verb conjugation by subject and agreement. -- Passage length should be appropriate for the exam type and difficulty. +- Passage length should be appropriate for the difficulty. - Each question must have: - * a reading passage (passageText) - * a clear question prompt (questionText) — in exam language, NOT Bahasa Indonesia + * a reading passage or context (passageText) — Wajib Bahasa Indonesia + * a clear question prompt (questionText) — Wajib Bahasa Indonesia * a correct answer (correctAnswer) - * an explanation (explanation) — Bahasa Indonesia, boleh sisipkan istilah/kanji/kata ujian bila perlu + * a detailed explanation (explanation) — Wajib Bahasa Indonesia * difficulty level (${difficulty}) * relevant skill tags (skillTags) -- For JLPT/TOPIK kanji/hanja: Include reading annotations in format: 漢字(かんじ) for words that have readings. -- Questions should test real comprehension, not just surface-level recall. -- For multiple choice: always provide 4 options labeled A, B, C, D. +- For multiple choice: always provide 5 options labeled A, B, C, D, E (standard for CPNS). - Options must be plausible distractors — one clearly correct answer. ${OPTION_QUALITY_RULES} - For matching_pairs: Provide options as an array of {key, text} where key is the left item identifier and text is the left item. correctAnswer should be a serialized mapping like "A:1,B:2,C:3" matching each left key to its right pair. -- For error_recognition: options are error segments (A, B, C, D) and correctAnswer is the key of the segment containing an error. -- For text_insertion: options are position markers (A, B, C, D) within the passage where a sentence could be inserted. correctAnswer is the best position key. +- For error_recognition: options are error segments (A, B, C, D, E) and correctAnswer is the key of the segment containing an error. +- For text_insertion: options are position markers (A, B, C, D, E) within the passage where a sentence could be inserted. - For sentence_arrangement: Provide options as shuffled fragments in random order. correctAnswer is the correct order as comma-separated keys (e.g. "D,A,C,B"). -- For matching_information: options are information items with key and text. correctAnswer is the correct match as serialized mapping. OUTPUT FORMAT: Return ONLY a valid JSON object with this exact structure (no markdown code blocks, no extra text): @@ -55,6 +61,6 @@ ${questionSchemaJson.split("\n").map((l) => " " + l).join("\n")} ] } -You may reuse the same passage for multiple questions, or generate a new passage per question — whichever is more natural for the exam type. +You may reuse the same passage for multiple questions, or generate a new passage per question. `; } diff --git a/packages/ai/src/schemas.ts b/packages/ai/src/schemas.ts index 70c2feebb648668977b3ba3e769793749c15c332..159ea276dd997defc973dacd9c16348cf24dd643 100644 --- a/packages/ai/src/schemas.ts +++ b/packages/ai/src/schemas.ts @@ -2,8 +2,8 @@ import { z } from "zod"; // ── Shared Schemas ───────────────────────────────────────── -export const examTypeSchema = z.enum(["IELTS", "TOEFL", "JLPT", "HSK", "GOETHE", "TOPIK", "TOAFL", "DELE"]); -export const sectionTypeSchema = z.enum(["READING", "WRITING", "LISTENING", "SPEAKING"]); +export const examTypeSchema = z.enum(["CPNS", "SKB"]); +export const sectionTypeSchema = z.enum(["TWK", "TIU", "TKP", "Teknis"]); export const questionFormatSchema = z.enum([ "multiple_choice", diff --git a/packages/api/LICENSE b/packages/api/LICENSE index 1fc95992a2e8ad86439ad2a53a3ad137e2c52dc2..566070fb89d97a3937c0d03275e52536067e9e44 100644 --- a/packages/api/LICENSE +++ b/packages/api/LICENSE @@ -1,6 +1,6 @@ -MIT License +MIT License -Copyright (c) 2026 Labas Project Authors +Copyright (c) 2026 Pram Project Authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/packages/api/package.json b/packages/api/package.json index 934a2929fb325771d7c54c8c6517e565adfce7e2..ad716c07347a25a6054e66022b9595d3e5146edd 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,5 +1,5 @@ { - "name": "@labas/api", + "name": "@pram/api", "license": "MIT", "type": "module", "exports": { @@ -14,10 +14,10 @@ "test": "bun test" }, "dependencies": { - "@labas/ai": "workspace:*", - "@labas/auth": "workspace:*", - "@labas/db": "workspace:*", - "@labas/env": "workspace:*", + "@pram/ai": "workspace:*", + "@pram/auth": "workspace:*", + "@pram/db": "workspace:*", + "@pram/env": "workspace:*", "@trpc/client": "catalog:", "@trpc/server": "catalog:", "bcryptjs": "^3.0.3", @@ -31,7 +31,7 @@ }, "devDependencies": { "@electric-sql/pglite": "^0.4.5", - "@labas/config": "workspace:*", + "@pram/config": "workspace:*", "@types/bcryptjs": "^3.0.0", "@types/nodemailer": "^8.0.0", "hono": "catalog:", diff --git a/packages/api/src/__tests__/admin.integration.test.ts b/packages/api/src/__tests__/admin.integration.test.ts index b1f775d836a6a5d191c6465525a6c3bed7b625da..a5aaf84c2b075077418c7c64703002e3e074531f 100644 --- a/packages/api/src/__tests__/admin.integration.test.ts +++ b/packages/api/src/__tests__/admin.integration.test.ts @@ -21,7 +21,7 @@ const MOCK_ENV = { DEFAULT_SIGNUP_CREDIT_TOKENS: "50000", }; -mock.module("@labas/env/server", () => ({ +mock.module("@pram/env/server", () => ({ env: MOCK_ENV, })); @@ -40,7 +40,7 @@ describe("Admin Integration", () => { const pg = await getTestPGlite(); db = drizzle(pg, { schema }); - mock.module("@labas/db", () => ({ + mock.module("@pram/db", () => ({ db, ...schema, })); diff --git a/packages/api/src/__tests__/attempt.integration.test.ts b/packages/api/src/__tests__/attempt.integration.test.ts index 96dad3cb48ee0450040b4d570ce57a4372d8b51f..fbcab3e15035e94119bae445cb47a92327a64aba 100644 --- a/packages/api/src/__tests__/attempt.integration.test.ts +++ b/packages/api/src/__tests__/attempt.integration.test.ts @@ -3,7 +3,7 @@ import { drizzle } from "drizzle-orm/pglite"; import * as schema from "../../../db/src/schema"; import { closeTestPGlite, getTestPGlite } from "./test-setup"; -mock.module("@labas/env/server", () => ({ +mock.module("@pram/env/server", () => ({ env: { DATABASE_URL: "postgres://localhost:5432/test", BETTER_AUTH_SECRET: "a".repeat(32), @@ -41,7 +41,7 @@ describe("attempt router", () => { const pg = await getTestPGlite(); testDb = drizzle(pg, { schema }); - mock.module("@labas/db", () => ({ + mock.module("@pram/db", () => ({ __esModule: true, ...schema, db: testDb, diff --git a/packages/api/src/__tests__/credit.test.ts b/packages/api/src/__tests__/credit.test.ts index d822b0f66fab9af36af6c0f0cdd205a62445309a..3fc2c8e684ed66f2c954987c89de77f0be45b809 100644 --- a/packages/api/src/__tests__/credit.test.ts +++ b/packages/api/src/__tests__/credit.test.ts @@ -24,7 +24,7 @@ const MOCK_ENV = { FREE_CREDITS_MAX_POOL: 1000000, }; -mock.module("@labas/env/server", () => ({ +mock.module("@pram/env/server", () => ({ env: MOCK_ENV, })); @@ -44,7 +44,7 @@ describe("Credit Utility", () => { const pg = await getTestPGlite(); db = drizzle(pg, { schema }); - mock.module("@labas/db", () => ({ + mock.module("@pram/db", () => ({ db, ...schema, })); diff --git a/packages/api/src/__tests__/package.integration.test.ts b/packages/api/src/__tests__/package.integration.test.ts index 4a31e55d6ed72bf1e1c76035e3ee53fce232d702..87f9dde017b41e39e9d251cf6b0086c999b11ba1 100644 --- a/packages/api/src/__tests__/package.integration.test.ts +++ b/packages/api/src/__tests__/package.integration.test.ts @@ -4,7 +4,7 @@ import { drizzle } from "drizzle-orm/pglite"; import * as schema from "../../../db/src/schema"; import { closeTestPGlite, getTestPGlite } from "./test-setup"; -mock.module("@labas/env/server", () => ({ +mock.module("@pram/env/server", () => ({ env: { DATABASE_URL: "postgres://localhost:5432/test", BETTER_AUTH_SECRET: "a".repeat(32), @@ -33,7 +33,7 @@ describe("package router", () => { const pg = await getTestPGlite(); testDb = drizzle(pg, { schema }); - mock.module("@labas/db", () => ({ + mock.module("@pram/db", () => ({ __esModule: true, ...schema, db: testDb, diff --git a/packages/api/src/__tests__/question.integration.test.ts b/packages/api/src/__tests__/question.integration.test.ts index acaae0022d593e1ae78dcdbc928ecbb07c9d90aa..af69473e036dea1bdcdbf9924e9012c9df59a838 100644 --- a/packages/api/src/__tests__/question.integration.test.ts +++ b/packages/api/src/__tests__/question.integration.test.ts @@ -3,7 +3,7 @@ import { drizzle } from "drizzle-orm/pglite"; import * as schema from "../../../db/src/schema"; import { closeTestPGlite, getTestPGlite, createTestUserData } from "./test-setup"; -mock.module("@labas/env/server", () => ({ +mock.module("@pram/env/server", () => ({ env: { DATABASE_URL: "postgres://localhost:5432/test", BETTER_AUTH_SECRET: "a".repeat(32), @@ -26,7 +26,7 @@ describe("question router", () => { const pg = await getTestPGlite(); const testDb = drizzle(pg, { schema }); - mock.module("@labas/db", () => ({ + mock.module("@pram/db", () => ({ __esModule: true, ...schema, db: testDb, diff --git a/packages/api/src/__tests__/test-db.ts b/packages/api/src/__tests__/test-db.ts index c7f74e782f7b9503b0fdd2c178f5ecb376e057f2..d92448ab6087535c1bed4849d7a5fa192750906e 100644 --- a/packages/api/src/__tests__/test-db.ts +++ b/packages/api/src/__tests__/test-db.ts @@ -1,6 +1,6 @@ import { PGlite } from "@electric-sql/pglite"; import { drizzle } from "drizzle-orm/pglite"; -import * as schema from "@labas/db"; +import * as schema from "@pram/db"; let _client: PGlite | null = null; diff --git a/packages/api/src/__tests__/verification.integration.test.ts b/packages/api/src/__tests__/verification.integration.test.ts index a4d337c8d0804b9868a3d6f5db820ee426cf11da..1e9679affe358340f2e1c37eafac1d61000a527d 100644 --- a/packages/api/src/__tests__/verification.integration.test.ts +++ b/packages/api/src/__tests__/verification.integration.test.ts @@ -6,7 +6,7 @@ import { getTestPGlite, closeTestPGlite } from "./test-setup"; // ── Mocks (must be declared before dynamic imports) ────────────────────────── -mock.module("@labas/env/server", () => ({ +mock.module("@pram/env/server", () => ({ env: { DATABASE_URL: "postgres://localhost:5432/test", BETTER_AUTH_SECRET: "a".repeat(32), @@ -57,7 +57,7 @@ describe("verification router", () => { const pg = await getTestPGlite(); db = drizzle(pg, { schema }); - mock.module("@labas/db", () => ({ + mock.module("@pram/db", () => ({ __esModule: true, ...schema, db, diff --git a/packages/api/src/context.ts b/packages/api/src/context.ts index 1f14d63a8f9cf66708406ff01a626b4becc9fa19..bf8126eb55f8dcd6eb7670e0871440c1785e2902 100644 --- a/packages/api/src/context.ts +++ b/packages/api/src/context.ts @@ -1,4 +1,4 @@ -import { auth } from "@labas/auth"; +import { auth } from "@pram/auth"; import type { Context as HonoContext } from "hono"; import { getClientIp } from "./lib/client-ip"; diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 8e63ab021072b7da63977a5480643a59f755b0e7..305290ebf2e4f73cb235bd24e6e6433dca134219 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -2,8 +2,8 @@ import { initTRPC, TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; import { logger } from "./logger"; import type { Context } from "./context"; -import { db } from "@labas/db"; -import { user } from "@labas/db"; +import { db } from "@pram/db"; +import { user } from "@pram/db"; export const t = initTRPC.context().create(); diff --git a/packages/api/src/lib/credit.ts b/packages/api/src/lib/credit.ts index 9eb7e0447fa89f0e077cd56545dc3221b6b8273d..0681744769550891daff2261b4b31fbea8b1c5ac 100644 --- a/packages/api/src/lib/credit.ts +++ b/packages/api/src/lib/credit.ts @@ -1,8 +1,8 @@ import { eq, sql, and, desc, sum, gte } from "drizzle-orm"; -import { db } from "@labas/db"; -import { userCredit, creditTransaction, user, platformConfig } from "@labas/db"; +import { db } from "@pram/db"; +import { userCredit, creditTransaction, user, platformConfig } from "@pram/db"; import { TRPCError } from "@trpc/server"; -import { env } from "@labas/env/server"; +import { env } from "@pram/env/server"; const COOLDOWN_DAYS = 7; diff --git a/packages/api/src/lib/email.ts b/packages/api/src/lib/email.ts index a60d73dace7465b233ee981ff6ee717b374fada9..dd096ec9ea37d88268a022c2dd0451104b452218 100644 --- a/packages/api/src/lib/email.ts +++ b/packages/api/src/lib/email.ts @@ -1,20 +1,31 @@ -import { env } from "@labas/env/server"; +import { env } from "@pram/env/server"; import nodemailer from "nodemailer"; +const smtpConfigured = + env.SMTP_HOST && env.SMTP_USER && env.SMTP_PASS && env.SMTP_FROM; + +if (!smtpConfigured) { + console.warn( + "[email] SMTP not configured — email features (verification, password reset) will be disabled. Set SMTP_HOST, SMTP_USER, SMTP_PASS, SMTP_FROM in apps/server/.env to enable." + ); +} + const smtpPort = Number(env.SMTP_PORT) || 587; -const transporter = nodemailer.createTransport({ - host: env.SMTP_HOST, - port: smtpPort, - secure: smtpPort === 465, - auth: { - user: env.SMTP_USER, - pass: env.SMTP_PASS, - }, -}); +const transporter = smtpConfigured + ? nodemailer.createTransport({ + host: env.SMTP_HOST, + port: smtpPort, + secure: smtpPort === 465, + auth: { + user: env.SMTP_USER, + pass: env.SMTP_PASS, + }, + }) + : null; const subjects: Record = { - "email-verification": "Verifikasi Email Labas", - "forget-password": "Reset Password Labas", + "email-verification": "Verifikasi Email Pram", + "forget-password": "Reset Password Pram", }; const messages: Record = { @@ -29,7 +40,12 @@ type SendOtpEmailProps = { }; export async function sendOtpEmail({ to, otp, type }: SendOtpEmailProps) { - const subject = subjects[type] || "Kode Verifikasi Labas"; + if (!transporter) { + console.warn(`[email] Skipping OTP email to ${to} — SMTP not configured.`); + return; + } + + const subject = subjects[type] || "Kode Verifikasi Pram"; const message = messages[type] || "Kode verifikasi Anda:"; await transporter.sendMail({ @@ -38,7 +54,7 @@ export async function sendOtpEmail({ to, otp, type }: SendOtpEmailProps) { subject, html: ` - Labas + Pram ${message} ${otp} diff --git a/packages/api/src/lib/generation-outcome.ts b/packages/api/src/lib/generation-outcome.ts index 6ac04566da9185981668e18eaa37564e4d401b40..9e419e718f5e5c44b4fd1019413eedeedfecc522 100644 --- a/packages/api/src/lib/generation-outcome.ts +++ b/packages/api/src/lib/generation-outcome.ts @@ -43,7 +43,7 @@ export function formatGenerationErrorMessage(input: { lower.includes("") || /<\/?think>/.test(lower) ) { - return `Model reasoning tidak kompatibel dengan parser Labas.${hint} Coba non-reasoning model, atau update server ke versi terbaru.`; + return `Model reasoning tidak kompatibel dengan parser Pram.${hint} Coba non-reasoning model, atau update server ke versi terbaru.`; } if ( diff --git a/packages/api/src/lib/rate-limit.ts b/packages/api/src/lib/rate-limit.ts index 8c72b627047e5d586a89d0d02184deb86568ced5..b5b65e67e343587a55562c311b2e9e8b890abc1e 100644 --- a/packages/api/src/lib/rate-limit.ts +++ b/packages/api/src/lib/rate-limit.ts @@ -1,7 +1,7 @@ import IORedis from "ioredis"; -import { env } from "@labas/env/server"; +import { env } from "@pram/env/server"; import { TRPCError } from "@trpc/server"; -import { logger } from "@labas/api/logger"; +import { logger } from "@pram/api/logger"; let redis: IORedis | null = null; diff --git a/packages/api/src/lib/turnstile.ts b/packages/api/src/lib/turnstile.ts index 1a98b690bc2ee9e398ef7b051423a7a378238106..2bb718c10346b5323111f67fd98b98d014e723b6 100644 --- a/packages/api/src/lib/turnstile.ts +++ b/packages/api/src/lib/turnstile.ts @@ -1,6 +1,6 @@ import { TRPCError } from "@trpc/server"; -import { env } from "@labas/env/server"; -import { logger } from "@labas/api/logger"; +import { env } from "@pram/env/server"; +import { logger } from "@pram/api/logger"; /** * Validates a Cloudflare Turnstile token server-side. diff --git a/packages/api/src/logger.ts b/packages/api/src/logger.ts index 92261877522e37f33a831982bbb0d0024acc169e..65d81209384e8b839690efc66f537edc64a7a120 100644 --- a/packages/api/src/logger.ts +++ b/packages/api/src/logger.ts @@ -22,14 +22,14 @@ function createTransports() { if (isProduction) { transports.push( new winston.transports.File({ - filename: "logs/labas-error.log", + filename: "logs/pram-error.log", level: "error", format: combine(timestamp(), errors({ stack: true }), json()), maxsize: 10 * 1024 * 1024, maxFiles: 14, }), new winston.transports.File({ - filename: "logs/labas-combined.log", + filename: "logs/pram-combined.log", format: combine(timestamp(), errors({ stack: true }), json()), maxsize: 10 * 1024 * 1024, maxFiles: 7, @@ -50,7 +50,7 @@ function createTransports() { export const logger = winston.createLogger({ level: process.env.LOG_LEVEL ?? "info", - defaultMeta: { service: "labas-api" }, + defaultMeta: { service: "pram-api" }, transports: createTransports(), }); diff --git a/packages/api/src/queue.ts b/packages/api/src/queue.ts index 693f410b70715de1ee5ffd067d59823eb22fa69c..3d6f059e8bb609117d7ed9c9332dfd06a327af10 100644 --- a/packages/api/src/queue.ts +++ b/packages/api/src/queue.ts @@ -1,9 +1,9 @@ import { Queue, Worker, type Job } from "bullmq"; import IORedis from "ioredis"; -import { env } from "@labas/env/server"; +import { env } from "@pram/env/server"; async function log(level: "debug" | "warn", message: string, meta?: Record) { - const mod = await import("@labas/api/logger"); + const mod = await import("@pram/api/logger"); (mod.logger as any)[level](message, meta); } import { @@ -15,15 +15,15 @@ import { type AgenticProgress, type GenerationInput, type GenerationResult, -} from "@labas/ai"; -import { db } from "@labas/db"; +} from "@pram/ai"; +import { db } from "@pram/db"; import { generationJob, question, testPackage, packageSection, sectionQuestion, -} from "@labas/db"; +} from "@pram/db"; import { and, eq, notInArray } from "drizzle-orm"; import { encryptApiKey, decryptApiKey } from "./lib/encryption"; import { @@ -489,7 +489,7 @@ async function completeJobWithResult(params: { .where(eq(generationJob.id, params.jobId)) .limit(1); if (jobRow) { - const { deductCredit } = await import("@labas/api/lib/credit"); + const { deductCredit } = await import("@pram/api/lib/credit"); await deductCredit(jobRow.userId, params.totalTokens).catch(() => {}); } } diff --git a/packages/api/src/routers/admin.ts b/packages/api/src/routers/admin.ts index 6d83b832741f436237bed31160b044e6494a33da..4b3d8650bfeafaff750f48d2f3f19f1b35b84f4b 100644 --- a/packages/api/src/routers/admin.ts +++ b/packages/api/src/routers/admin.ts @@ -1,12 +1,12 @@ import { z } from "zod"; import { eq, desc, sql, count, ilike, or } from "drizzle-orm"; import { adminProcedure, protectedProcedure, router } from "../index"; -import * as schema from "@labas/db"; -import { db } from "@labas/db"; +import * as schema from "@pram/db"; +import { db } from "@pram/db"; import { paginationSchema, paginateDefaults } from "../lib/pagination"; import { throwNotFound, throwForbidden, throwBadRequest } from "../lib/errors"; import { getUserCredit, getLastRefillAt, getPoolUsage, getConfig, setConfig, autoRefillIfEligible } from "../lib/credit"; -import { env } from "@labas/env/server"; +import { env } from "@pram/env/server"; function audit(adminUserId: string, action: string, targetUserId: string | null, details?: Record) { return db.insert(schema.adminAuditLog).values({ diff --git a/packages/api/src/routers/ai.ts b/packages/api/src/routers/ai.ts index 822579c14f58b57d2b7eabef515a3c8e862c0498..9f193d5ac5e254faaf0a1ffc2171f9352f714301 100644 --- a/packages/api/src/routers/ai.ts +++ b/packages/api/src/routers/ai.ts @@ -1,16 +1,16 @@ import { z } from "zod"; import { eq, desc, and, gte, sql } from "drizzle-orm"; import { router, protectedProcedure } from "../index"; -import { type GenerationInput, generationInputSchema } from "@labas/ai/schemas"; +import { type GenerationInput, generationInputSchema } from "@pram/ai/schemas"; import { cancelGenerationJob, enqueueGeneration } from "../queue"; -import { db } from "@labas/db"; -import { question, generationJob } from "@labas/db"; +import { db } from "@pram/db"; +import { question, generationJob } from "@pram/db"; import { paginationSchema, paginateDefaults } from "../lib/pagination"; import { throwNotFound, throwForbidden, throwBadRequest } from "../lib/errors"; import { checkDailyBudget } from "../lib/rate-limit"; import { decryptApiKey } from "../lib/encryption"; import { isUserSuspended, getUserCredit, autoRefillIfEligible } from "../lib/credit"; -import { env } from "@labas/env/server"; +import { env } from "@pram/env/server"; const DAILY_TOKEN_BUDGET = 500_000; diff --git a/packages/api/src/routers/attempt.ts b/packages/api/src/routers/attempt.ts index 763f7c253e55335ea32a59cc8bb233ca6caa3e5a..05edc79542f495317fb12e39451e8a496ab595ee 100644 --- a/packages/api/src/routers/attempt.ts +++ b/packages/api/src/routers/attempt.ts @@ -1,7 +1,7 @@ import { z } from "zod"; import { eq, and, desc, sql, inArray, ilike } from "drizzle-orm"; import { router, protectedProcedure } from "../index"; -import { db } from "@labas/db"; +import { db } from "@pram/db"; import { testAttempt, sectionResult, @@ -11,7 +11,7 @@ import { sectionQuestion, question, examType, -} from "@labas/db"; +} from "@pram/db"; import { paginationSchema, paginateDefaults } from "../lib/pagination"; import { assertOwnership } from "../lib/ownership"; import { throwNotFound, throwForbidden, throwBadRequest } from "../lib/errors"; diff --git a/packages/api/src/routers/combo.ts b/packages/api/src/routers/combo.ts index fdccbdd8b1f17e243633a7fe156c51b0eb9cf683..f943c873c219ad4f211f1efdee14d5f1341822c9 100644 --- a/packages/api/src/routers/combo.ts +++ b/packages/api/src/routers/combo.ts @@ -1,7 +1,7 @@ import { z } from "zod"; import { eq, and, desc, sql, inArray } from "drizzle-orm"; import { router, protectedProcedure, publicProcedure } from "../index"; -import { db } from "@labas/db"; +import { db } from "@pram/db"; import { comboPackage, comboSection, @@ -12,7 +12,7 @@ import { examType, sectionType, user, -} from "@labas/db"; +} from "@pram/db"; import { paginationSchema, paginateDefaults } from "../lib/pagination"; import { assertOwnership } from "../lib/ownership"; import { buildVisibilityCondition } from "../lib/visibility"; diff --git a/packages/api/src/routers/feedback.ts b/packages/api/src/routers/feedback.ts index 79d96c7d4824d0653e8dee294fc5caf87a7342fd..9e0d5466b98f676ef14dc22ac40aa391a2bf4fcc 100644 --- a/packages/api/src/routers/feedback.ts +++ b/packages/api/src/routers/feedback.ts @@ -1,8 +1,8 @@ import { z } from "zod"; import { eq, and, sql } from "drizzle-orm"; import { router, protectedProcedure, publicProcedure } from "../index"; -import { db } from "@labas/db"; -import { questionFeedback } from "@labas/db"; +import { db } from "@pram/db"; +import { questionFeedback } from "@pram/db"; import { checkRateLimit } from "../lib/rate-limit"; export const feedbackRouter = router({ diff --git a/packages/api/src/routers/leaderboard.ts b/packages/api/src/routers/leaderboard.ts index 058d47d0ac1e7d8f81f345fd2f3ae843f82bb65c..f41c261b2273691776620655ad16b39221d04b6d 100644 --- a/packages/api/src/routers/leaderboard.ts +++ b/packages/api/src/routers/leaderboard.ts @@ -1,8 +1,8 @@ import { z } from "zod"; import { eq, and, sql, gte, desc } from "drizzle-orm"; import { router, protectedProcedure, publicProcedure } from "../index"; -import { db } from "@labas/db"; -import { testAttempt, testPackage, user } from "@labas/db"; +import { db } from "@pram/db"; +import { testAttempt, testPackage, user } from "@pram/db"; import { paginationSchema, paginateDefaults } from "../lib/pagination"; const periodSchema = z.enum(["today", "week", "month", "all"]); diff --git a/packages/api/src/routers/package.ts b/packages/api/src/routers/package.ts index 6b99ad0f8c12e956a57cb388dd0235bb4c3e04df..fe047ae2f0a665e06bff952f226152f3bf55afbc 100644 --- a/packages/api/src/routers/package.ts +++ b/packages/api/src/routers/package.ts @@ -1,7 +1,7 @@ import { z } from "zod"; import { eq, and, desc, sql, inArray } from "drizzle-orm"; import { router, protectedProcedure, publicProcedure } from "../index"; -import { db } from "@labas/db"; +import { db } from "@pram/db"; import { testPackage, packageSection, @@ -10,7 +10,7 @@ import { examType, sectionType, user, -} from "@labas/db"; +} from "@pram/db"; import { paginationSchema, paginateDefaults } from "../lib/pagination"; import { assertOwnership } from "../lib/ownership"; import { buildVisibilityCondition } from "../lib/visibility"; diff --git a/packages/api/src/routers/profile.ts b/packages/api/src/routers/profile.ts index be2527cd48d6f56d3f69433cf0aa2cf7cca626cc..b8ed328b2ac009c3ed3cee150e19e84128edc3d5 100644 --- a/packages/api/src/routers/profile.ts +++ b/packages/api/src/routers/profile.ts @@ -1,8 +1,8 @@ import { z } from "zod"; import { eq, and, desc, sql } from "drizzle-orm"; import { router, protectedProcedure, publicProcedure } from "../index"; -import { db } from "@labas/db"; -import { testPackage, question, examType, user } from "@labas/db"; +import { db } from "@pram/db"; +import { testPackage, question, examType, user } from "@pram/db"; import { throwBadRequest } from "../lib/errors"; export const profileRouter = router({ diff --git a/packages/api/src/routers/question.ts b/packages/api/src/routers/question.ts index f9eb1634a3954b0ce63b2413e881a08914e53e37..b6e027d8b8d60cd1badca3e569c653c0567a1656 100644 --- a/packages/api/src/routers/question.ts +++ b/packages/api/src/routers/question.ts @@ -1,8 +1,8 @@ import { z } from "zod"; import { eq, and, desc, sql, like, or, inArray } from "drizzle-orm"; import { router, protectedProcedure, publicProcedure } from "../index"; -import { db } from "@labas/db"; -import { question, examType, sectionType, user } from "@labas/db"; +import { db } from "@pram/db"; +import { question, examType, sectionType, user } from "@pram/db"; import { paginationSchema, paginateDefaults } from "../lib/pagination"; import { buildVisibilityCondition } from "../lib/visibility"; import { assertOwnership } from "../lib/ownership"; diff --git a/packages/api/src/routers/rating.ts b/packages/api/src/routers/rating.ts index 990dcf40f5b2daaa7e3019a6bb07acd132d2b303..3367618cb62bd519d7d0e8cf80cb94a267f833e4 100644 --- a/packages/api/src/routers/rating.ts +++ b/packages/api/src/routers/rating.ts @@ -1,8 +1,8 @@ import { z } from "zod"; import { eq, and, avg } from "drizzle-orm"; import { router, protectedProcedure, publicProcedure } from "../index"; -import { db } from "@labas/db"; -import { questionRating, question, packageRating, testPackage } from "@labas/db"; +import { db } from "@pram/db"; +import { questionRating, question, packageRating, testPackage } from "@pram/db"; import { checkRateLimit } from "../lib/rate-limit"; export const ratingRouter = router({ diff --git a/packages/api/src/routers/stats.ts b/packages/api/src/routers/stats.ts index 4c70cd662c0e226015aad6d3d9a8b587e55139fe..c9d32c873711a8e309207cf78c75e3496ca6bab1 100644 --- a/packages/api/src/routers/stats.ts +++ b/packages/api/src/routers/stats.ts @@ -1,7 +1,7 @@ import { z } from "zod"; import { eq, and, sql, gte } from "drizzle-orm"; import { router, protectedProcedure } from "../index"; -import { db } from "@labas/db"; +import { db } from "@pram/db"; import { testAttempt, sectionResult, @@ -10,7 +10,7 @@ import { question, examType, sectionType, -} from "@labas/db"; +} from "@pram/db"; export const statsRouter = router({ overview: protectedProcedure.query(async ({ ctx }) => { diff --git a/packages/api/src/routers/verification.ts b/packages/api/src/routers/verification.ts index 048ba9a11dab29ffe7aeb1b1d6496ce2224e2e03..a8e796d4896e91444c4e915975f2f5bf790359ac 100644 --- a/packages/api/src/routers/verification.ts +++ b/packages/api/src/routers/verification.ts @@ -1,5 +1,5 @@ -import { db } from "@labas/db"; -import { verification, user, account } from "@labas/db/schema/auth"; +import { db } from "@pram/db"; +import { verification, user, account } from "@pram/db/schema/auth"; import { TRPCError } from "@trpc/server"; import bcrypt from "bcryptjs"; import crypto from "node:crypto"; diff --git a/packages/api/tsconfig.json b/packages/api/tsconfig.json index 8a5e6cfc1939eb5df5ecd8c025a5cb51f457eeb5..028a9503327b544323159b510aee6ef49581936b 100644 --- a/packages/api/tsconfig.json +++ b/packages/api/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "@labas/config/tsconfig.base.json", + "extends": "@pram/config/tsconfig.base.json", "compilerOptions": { "declaration": true, "declarationMap": true, diff --git a/packages/auth/LICENSE b/packages/auth/LICENSE index 1fc95992a2e8ad86439ad2a53a3ad137e2c52dc2..566070fb89d97a3937c0d03275e52536067e9e44 100644 --- a/packages/auth/LICENSE +++ b/packages/auth/LICENSE @@ -1,6 +1,6 @@ -MIT License +MIT License -Copyright (c) 2026 Labas Project Authors +Copyright (c) 2026 Pram Project Authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/packages/auth/package.json b/packages/auth/package.json index 94b1b72fb565e0e3b15a5d72ef3b1b7eb4192766..6dba2dcdb713bc1dc8395d74226d45f0581ed333 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -1,5 +1,5 @@ { - "name": "@labas/auth", + "name": "@pram/auth", "license": "MIT", "type": "module", "exports": { @@ -12,14 +12,14 @@ }, "scripts": {}, "dependencies": { - "@labas/db": "workspace:*", - "@labas/env": "workspace:*", + "@pram/db": "workspace:*", + "@pram/env": "workspace:*", "better-auth": "catalog:", "dotenv": "catalog:", "zod": "catalog:" }, "devDependencies": { - "@labas/config": "workspace:*", + "@pram/config": "workspace:*", "typescript": "catalog:" } } diff --git a/packages/auth/src/index.ts b/packages/auth/src/index.ts index bd0e8ef2ec1ceb88c40f6209bd3b682ff65102dc..138381fcf06f0b22cf5be86422ae32fb6f45b401 100644 --- a/packages/auth/src/index.ts +++ b/packages/auth/src/index.ts @@ -1,6 +1,6 @@ -import { createDb } from "@labas/db"; -import * as schema from "@labas/db/schema/auth"; -import { env } from "@labas/env/server"; +import { createDb } from "@pram/db"; +import * as schema from "@pram/db/schema/auth"; +import { env } from "@pram/env/server"; import { betterAuth } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; diff --git a/packages/auth/tsconfig.json b/packages/auth/tsconfig.json index 8a5e6cfc1939eb5df5ecd8c025a5cb51f457eeb5..028a9503327b544323159b510aee6ef49581936b 100644 --- a/packages/auth/tsconfig.json +++ b/packages/auth/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "@labas/config/tsconfig.base.json", + "extends": "@pram/config/tsconfig.base.json", "compilerOptions": { "declaration": true, "declarationMap": true, diff --git a/packages/config/LICENSE b/packages/config/LICENSE index 1fc95992a2e8ad86439ad2a53a3ad137e2c52dc2..566070fb89d97a3937c0d03275e52536067e9e44 100644 --- a/packages/config/LICENSE +++ b/packages/config/LICENSE @@ -1,6 +1,6 @@ -MIT License +MIT License -Copyright (c) 2026 Labas Project Authors +Copyright (c) 2026 Pram Project Authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/packages/config/package.json b/packages/config/package.json index dad702995a7816f7e73570220c40a5c340b2a8d3..beb396e18b7a97a22a4e9a80c07741a6629a2475 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -1,5 +1,5 @@ { - "name": "@labas/config", + "name": "@pram/config", "version": "0.0.0", "private": true, "license": "MIT" diff --git a/packages/db/LICENSE b/packages/db/LICENSE index 1fc95992a2e8ad86439ad2a53a3ad137e2c52dc2..566070fb89d97a3937c0d03275e52536067e9e44 100644 --- a/packages/db/LICENSE +++ b/packages/db/LICENSE @@ -1,6 +1,6 @@ -MIT License +MIT License -Copyright (c) 2026 Labas Project Authors +Copyright (c) 2026 Pram Project Authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/packages/db/docker-compose.yml b/packages/db/docker-compose.yml index 56a7417beb5f3fb303ed7e8d41c8d1b0dde1e759..df47c7da2a28b19489ff876a4f8814339df579bd 100644 --- a/packages/db/docker-compose.yml +++ b/packages/db/docker-compose.yml @@ -1,17 +1,17 @@ -name: labas +name: pram services: postgres: image: postgres - container_name: labas-postgres + container_name: pram-postgres environment: - POSTGRES_DB: labas + POSTGRES_DB: pram POSTGRES_USER: postgres POSTGRES_PASSWORD: password ports: - "5432:5432" volumes: - - labas_postgres_data:/var/lib/postgresql + - pram_postgres_data:/var/lib/postgresql healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 10s @@ -21,11 +21,11 @@ services: redis: image: redis:7-alpine - container_name: labas-redis + container_name: pram-redis ports: - "6379:6379" volumes: - - labas_redis_data:/data + - pram_redis_data:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s @@ -34,5 +34,5 @@ services: restart: unless-stopped volumes: - labas_postgres_data: - labas_redis_data: + pram_postgres_data: + pram_redis_data: diff --git a/packages/db/package.json b/packages/db/package.json index def4cc588cf9952f8fa377748ebae03da51d27a9..1cd96638ceabcd47596d859ecaabfbf653225615 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -1,5 +1,5 @@ { - "name": "@labas/db", + "name": "@pram/db", "license": "MIT", "type": "module", "exports": { @@ -22,14 +22,14 @@ "db:seed": "bun run src/seed.ts" }, "dependencies": { - "@labas/env": "workspace:*", + "@pram/env": "workspace:*", "dotenv": "catalog:", "drizzle-orm": "^0.45.1", "pg": "^8.17.1", "zod": "catalog:" }, "devDependencies": { - "@labas/config": "workspace:*", + "@pram/config": "workspace:*", "@types/pg": "^8.16.0", "drizzle-kit": "^0.31.8", "typescript": "catalog:" diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts index 231370ad5fbfa84a720ec9f71dd14550cef7fc96..29efd50b15820a7d04fb97d7b62b81aaf70e2ae8 100644 --- a/packages/db/src/index.ts +++ b/packages/db/src/index.ts @@ -1,4 +1,4 @@ -import { env } from "@labas/env/server"; +import { env } from "@pram/env/server"; import { drizzle } from "drizzle-orm/node-postgres"; import * as schema from "./schema"; diff --git a/packages/db/src/seed.ts b/packages/db/src/seed.ts index ec8f12e3048b49e2abf740f1a1281ebf17eee793..fbc2409c2ebab1a87b9d72d79a52b7872aeb72d1 100644 --- a/packages/db/src/seed.ts +++ b/packages/db/src/seed.ts @@ -6,21 +6,13 @@ import { examType, sectionType } from "./schema"; const db = drizzle(loadDatabaseUrl()); const examTypes = [ - { id: "IELTS", name: "IELTS Academic", language: "English", description: "International English Language Testing System" }, - { id: "TOEFL", name: "TOEFL iBT", language: "English", description: "Test of English as a Foreign Language" }, - { id: "JLPT", name: "JLPT", language: "Japanese", description: "Japanese-Language Proficiency Test" }, - { id: "HSK", name: "HSK", language: "Chinese", description: "Hanyu Shuiping Kaoshi" }, - { id: "GOETHE", name: "Goethe-Zertifikat", language: "German", description: "Goethe-Zertifikat German proficiency test" }, - { id: "TOPIK", name: "TOPIK", language: "Korean", description: "Test of Proficiency in Korean" }, - { id: "TOAFL", name: "TOAFL", language: "Arabic", description: "Test of Arabic as a Foreign Language" }, - { id: "DELE", name: "DELE", language: "Spanish", description: "Diplomas de Español como Lengua Extranjera" }, + { id: "CPNS", name: "Ujian CPNS", language: "Bahasa Indonesia", description: "Seleksi Calon Pegawai Negeri Sipil" }, ]; const sectionTypes = [ - { id: "READING", name: "Reading" }, - { id: "WRITING", name: "Writing" }, - { id: "LISTENING", name: "Listening" }, - { id: "SPEAKING", name: "Speaking" }, + { id: "TWK", name: "Tes Wawasan Kebangsaan" }, + { id: "TIU", name: "Tes Intelegensia Umum" }, + { id: "TKP", name: "Tes Karakteristik Pribadi" }, ]; async function seed() { diff --git a/packages/db/tsconfig.json b/packages/db/tsconfig.json index 8a5e6cfc1939eb5df5ecd8c025a5cb51f457eeb5..028a9503327b544323159b510aee6ef49581936b 100644 --- a/packages/db/tsconfig.json +++ b/packages/db/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "@labas/config/tsconfig.base.json", + "extends": "@pram/config/tsconfig.base.json", "compilerOptions": { "declaration": true, "declarationMap": true, diff --git a/packages/env/LICENSE b/packages/env/LICENSE index 1fc95992a2e8ad86439ad2a53a3ad137e2c52dc2..566070fb89d97a3937c0d03275e52536067e9e44 100644 --- a/packages/env/LICENSE +++ b/packages/env/LICENSE @@ -1,6 +1,6 @@ -MIT License +MIT License -Copyright (c) 2026 Labas Project Authors +Copyright (c) 2026 Pram Project Authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/packages/env/package.json b/packages/env/package.json index 2c4110c3a0dbd86667a787c29e4284a6dfa81234..d215eb8bd3a70367bb47143ca02fe9e3cf5f8b00 100644 --- a/packages/env/package.json +++ b/packages/env/package.json @@ -1,5 +1,5 @@ { - "name": "@labas/env", + "name": "@pram/env", "version": "0.0.0", "private": true, "license": "MIT", @@ -14,7 +14,7 @@ "zod": "catalog:" }, "devDependencies": { - "@labas/config": "workspace:*", + "@pram/config": "workspace:*", "@types/bun": "catalog:", "typescript": "catalog:" } diff --git a/packages/env/src/server.ts b/packages/env/src/server.ts index 5520b83061a099c44e3c7a4d462ed31bdc07df6d..ecf217f566ae5922bf1341292406bb8b5e1ae14d 100644 --- a/packages/env/src/server.ts +++ b/packages/env/src/server.ts @@ -11,11 +11,11 @@ export const env = createEnv({ CORS_ORIGIN: z.url(), API_KEY_ENCRYPTION_KEY: z.string().min(32), NODE_ENV: z.enum(["development", "production", "test"]).default("development"), - SMTP_HOST: z.string().min(1), + SMTP_HOST: z.string().min(1).optional(), SMTP_PORT: z.coerce.number().positive().default(587), - SMTP_USER: z.string().min(1), - SMTP_PASS: z.string().min(1), - SMTP_FROM: z.string().min(1), + SMTP_USER: z.string().min(1).optional(), + SMTP_PASS: z.string().min(1).optional(), + SMTP_FROM: z.string().min(1).optional(), PLATFORM_AI_API_KEY: z.string().optional(), PLATFORM_AI_BASE_URL: z.string().optional(), PLATFORM_AI_MODEL: z.string().optional(), diff --git a/packages/env/tsconfig.json b/packages/env/tsconfig.json index 0006edc512a4a24193e25e1d0b9957fc15c25884..1f0817dc7fc219c6d57c99e824f295d2ed3a7ecf 100644 --- a/packages/env/tsconfig.json +++ b/packages/env/tsconfig.json @@ -1,3 +1,3 @@ { - "extends": "@labas/config/tsconfig.base.json" + "extends": "@pram/config/tsconfig.base.json" } diff --git a/packages/ui/LICENSE b/packages/ui/LICENSE index 1fc95992a2e8ad86439ad2a53a3ad137e2c52dc2..566070fb89d97a3937c0d03275e52536067e9e44 100644 --- a/packages/ui/LICENSE +++ b/packages/ui/LICENSE @@ -1,6 +1,6 @@ -MIT License +MIT License -Copyright (c) 2026 Labas Project Authors +Copyright (c) 2026 Pram Project Authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/packages/ui/components.json b/packages/ui/components.json index 0a10405b7632fd990a742c0242b38f9bef85e220..3f3c9d16466dfbc61a1e93be8c93d0df88e2dced 100644 --- a/packages/ui/components.json +++ b/packages/ui/components.json @@ -12,11 +12,11 @@ }, "iconLibrary": "lucide", "aliases": { - "components": "@labas/ui/components", - "utils": "@labas/ui/lib/utils", - "hooks": "@labas/ui/hooks", - "lib": "@labas/ui/lib", - "ui": "@labas/ui/components" + "components": "@pram/ui/components", + "utils": "@pram/ui/lib/utils", + "hooks": "@pram/ui/hooks", + "lib": "@pram/ui/lib", + "ui": "@pram/ui/components" }, "menuColor": "default", "menuAccent": "subtle", diff --git a/packages/ui/package.json b/packages/ui/package.json index 2c819c2188fb12bd04a38d6eac972acf0f54778b..a7c90c7a66b4b63c90af0e11ac75655fa4fcba76 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,5 +1,5 @@ { - "name": "@labas/ui", + "name": "@pram/ui", "version": "0.0.0", "private": true, "license": "MIT", @@ -28,7 +28,7 @@ "tw-animate-css": "^1.3.4" }, "devDependencies": { - "@labas/config": "workspace:*", + "@pram/config": "workspace:*", "@types/react": "^19.2.10", "@types/react-dom": "catalog:", "tailwindcss": "^4.1.18", diff --git a/packages/ui/src/components/button.tsx b/packages/ui/src/components/button.tsx index d46dd1f73297767363bb6bae39ac65ff0b4a8ad7..295a2879683290d257e51681e154dcdd529790c0 100644 --- a/packages/ui/src/components/button.tsx +++ b/packages/ui/src/components/button.tsx @@ -1,7 +1,7 @@ import { Button as ButtonPrimitive } from "@base-ui/react/button" import { cva, type VariantProps } from "class-variance-authority" -import { cn } from "@labas/ui/lib/utils" +import { cn } from "@pram/ui/lib/utils" const buttonVariants = cva( "group/button inline-flex shrink-0 items-center justify-center rounded-none border border-transparent bg-clip-padding text-xs font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", diff --git a/packages/ui/src/components/card.tsx b/packages/ui/src/components/card.tsx index 003db8150aaacd3ed4558e1b06585d512755ee1f..7c2c41e6f3f12dd07eb56e13dfe39da2579c3272 100644 --- a/packages/ui/src/components/card.tsx +++ b/packages/ui/src/components/card.tsx @@ -1,4 +1,4 @@ -import { cn } from "@labas/ui/lib/utils"; +import { cn } from "@pram/ui/lib/utils"; import * as React from "react"; function Card({ diff --git a/packages/ui/src/components/checkbox.tsx b/packages/ui/src/components/checkbox.tsx index 2c8a577898f65cbcd6a773cbcd4205c4c027eabe..a4a2eb5d417bc0c214dd929e7a01e4c4f7243de3 100644 --- a/packages/ui/src/components/checkbox.tsx +++ b/packages/ui/src/components/checkbox.tsx @@ -1,7 +1,7 @@ "use client"; import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox"; -import { cn } from "@labas/ui/lib/utils"; +import { cn } from "@pram/ui/lib/utils"; import { CheckIcon } from "lucide-react"; function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) { diff --git a/packages/ui/src/components/dialog.tsx b/packages/ui/src/components/dialog.tsx index da1c632bbb224ec6d3e31bf0b3acd634ab234c9c..ccbf7b9b891b3a8ced2ae5a77ef9e6c727d1d773 100644 --- a/packages/ui/src/components/dialog.tsx +++ b/packages/ui/src/components/dialog.tsx @@ -1,8 +1,8 @@ import * as React from "react" import { Dialog as DialogPrimitive } from "@base-ui/react/dialog" -import { cn } from "@labas/ui/lib/utils" -import { Button } from "@labas/ui/components/button" +import { cn } from "@pram/ui/lib/utils" +import { Button } from "@pram/ui/components/button" import { XIcon } from "lucide-react" function Dialog({ ...props }: DialogPrimitive.Root.Props) { diff --git a/packages/ui/src/components/dropdown-menu.tsx b/packages/ui/src/components/dropdown-menu.tsx index 53ad3b20342f34d884829036bcf769b73bb4ab7b..592f84c56e193e222fd663c9f9541cf336e32506 100644 --- a/packages/ui/src/components/dropdown-menu.tsx +++ b/packages/ui/src/components/dropdown-menu.tsx @@ -1,7 +1,7 @@ "use client"; import { Menu as MenuPrimitive } from "@base-ui/react/menu"; -import { cn } from "@labas/ui/lib/utils"; +import { cn } from "@pram/ui/lib/utils"; import { ChevronRightIcon, CheckIcon } from "lucide-react"; import * as React from "react"; diff --git a/packages/ui/src/components/input.tsx b/packages/ui/src/components/input.tsx index 9f9b701cdab2e008f29b25214b7339115cd27651..a258305bb81019b5c94768c7c0472cea1e824d25 100644 --- a/packages/ui/src/components/input.tsx +++ b/packages/ui/src/components/input.tsx @@ -1,5 +1,5 @@ import { Input as InputPrimitive } from "@base-ui/react/input"; -import { cn } from "@labas/ui/lib/utils"; +import { cn } from "@pram/ui/lib/utils"; import * as React from "react"; function Input({ className, type, ...props }: React.ComponentProps<"input">) { diff --git a/packages/ui/src/components/label.tsx b/packages/ui/src/components/label.tsx index 4bcc1c3b484d41cc7adf0b256ca46f8811a1ed9d..6d3a230d93b9b3b78ae06d9a1b5b8f11ff4b2ffe 100644 --- a/packages/ui/src/components/label.tsx +++ b/packages/ui/src/components/label.tsx @@ -1,4 +1,4 @@ -import { cn } from "@labas/ui/lib/utils"; +import { cn } from "@pram/ui/lib/utils"; import * as React from "react"; function Label({ className, ...props }: React.ComponentProps<"label">) { diff --git a/packages/ui/src/components/select.tsx b/packages/ui/src/components/select.tsx index 6b5384a5a561a000a9462aa42a1f989345e3f24a..b451766012ff8a9bb660b1aefe65aa247c949d70 100644 --- a/packages/ui/src/components/select.tsx +++ b/packages/ui/src/components/select.tsx @@ -1,7 +1,7 @@ import * as React from "react" import { Select as SelectPrimitive } from "@base-ui/react/select" -import { cn } from "@labas/ui/lib/utils" +import { cn } from "@pram/ui/lib/utils" import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react" const Select = SelectPrimitive.Root diff --git a/packages/ui/src/components/sheet.tsx b/packages/ui/src/components/sheet.tsx index 3fe3d2b93e11818f07e6bdfbbdfa7c304b1d8c57..4d333709ddd8419ff57d94fa75f0957e8431801b 100644 --- a/packages/ui/src/components/sheet.tsx +++ b/packages/ui/src/components/sheet.tsx @@ -1,8 +1,8 @@ import * as React from "react" import { Dialog as SheetPrimitive } from "@base-ui/react/dialog" -import { cn } from "@labas/ui/lib/utils" -import { Button } from "@labas/ui/components/button" +import { cn } from "@pram/ui/lib/utils" +import { Button } from "@pram/ui/components/button" import { XIcon } from "lucide-react" function Sheet({ ...props }: SheetPrimitive.Root.Props) { diff --git a/packages/ui/src/components/skeleton.tsx b/packages/ui/src/components/skeleton.tsx index 391daf79cffda5723eb194f6d40b34a867a599d7..58062975be31184696eb3cc446c218f06a19609a 100644 --- a/packages/ui/src/components/skeleton.tsx +++ b/packages/ui/src/components/skeleton.tsx @@ -1,4 +1,4 @@ -import { cn } from "@labas/ui/lib/utils"; +import { cn } from "@pram/ui/lib/utils"; function Skeleton({ className, ...props }: React.ComponentProps<"div">) { return ( diff --git a/packages/ui/src/components/tabs.tsx b/packages/ui/src/components/tabs.tsx index 3af00069baf56ed14283bcaaf4dacb930d6e2eb5..753f6cc533c29b7941be272f1aa35cf7218d8d14 100644 --- a/packages/ui/src/components/tabs.tsx +++ b/packages/ui/src/components/tabs.tsx @@ -1,7 +1,7 @@ import { Tabs as TabsPrimitive } from "@base-ui/react/tabs" import { cva, type VariantProps } from "class-variance-authority" -import { cn } from "@labas/ui/lib/utils" +import { cn } from "@pram/ui/lib/utils" function Tabs({ className, diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json index 9711f3caf4aa9da441b11be12d657d618ef816d9..06c6a7755308cd842f003dbc4ac2ae56c2f8d2e2 100644 --- a/packages/ui/tsconfig.json +++ b/packages/ui/tsconfig.json @@ -1,10 +1,10 @@ { - "extends": "@labas/config/tsconfig.base.json", + "extends": "@pram/config/tsconfig.base.json", "compilerOptions": { "jsx": "react-jsx", "lib": ["ESNext", "DOM", "DOM.Iterable"], "paths": { - "@labas/ui/*": ["./src/*"] + "@pram/ui/*": ["./src/*"] } }, "include": ["src/**/*.ts", "src/**/*.tsx"], diff --git a/scripts/compress-og-image.ts b/scripts/compress-og-image.ts index 4cc75dfc032530829daca71dd6be3daae8db2837..bab1de975e4eb70f0a71e5817fa1d6b6d18299a2 100644 --- a/scripts/compress-og-image.ts +++ b/scripts/compress-og-image.ts @@ -1,5 +1,5 @@ /** - * Build social preview images for Labas (Open Graph + Twitter). + * Build social preview images for Pram (Open Graph + Twitter). * Requires Bun >= 1.3.14 (Bun.Image API). * * Outputs truecolor PNG + JPEG — palette PNG breaks some Twitter/Threads crawlers. diff --git a/tsconfig.json b/tsconfig.json index 0006edc512a4a24193e25e1d0b9957fc15c25884..1f0817dc7fc219c6d57c99e824f295d2ed3a7ecf 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,3 +1,3 @@ { - "extends": "@labas/config/tsconfig.base.json" + "extends": "@pram/config/tsconfig.base.json" }
Platform latihan ujian bahasa berbasis AI. Ikuti 3 langkah mudah untuk memulai: diff --git a/apps/web/src/routes/forgot-password.tsx b/apps/web/src/routes/forgot-password.tsx index 9a5622e0fe2e1a232bb997cbdc6e776f8ba23673..336be3bf8d33f88b59c9f4d696554304febb6ffb 100644 --- a/apps/web/src/routes/forgot-password.tsx +++ b/apps/web/src/routes/forgot-password.tsx @@ -1,10 +1,10 @@ import { createFileRoute, Link, useNavigate } from "@tanstack/react-router"; import { useState, useEffect, useRef } from "react"; import { useMutation } from "@tanstack/react-query"; -import { Button } from "@labas/ui/components/button"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@labas/ui/components/card"; -import { Input } from "@labas/ui/components/input"; -import { Label } from "@labas/ui/components/label"; +import { Button } from "@pram/ui/components/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@pram/ui/components/card"; +import { Input } from "@pram/ui/components/input"; +import { Label } from "@pram/ui/components/label"; import { toast } from "sonner"; import { getErrorMessage } from "@/lib/error-utils"; import { trpc } from "@/utils/trpc"; @@ -242,7 +242,7 @@ function RouteComponent() { if (!email) { return (
${message}