├── .nvmrc ├── .yarnrc.yml ├── apps ├── processor │ ├── .gitignore │ ├── README.md │ ├── tsconfig.json │ ├── tsup.config.ts │ ├── eslint.config.js │ ├── index.ts │ ├── src │ │ ├── api.ts │ │ ├── db.ts │ │ ├── env.ts │ │ ├── logger.ts │ │ └── utils.ts │ ├── package.json │ └── Dockerfile ├── web │ ├── src │ │ ├── app │ │ │ ├── api │ │ │ │ ├── auth │ │ │ │ │ └── [...nextauth] │ │ │ │ │ │ └── route.ts │ │ │ │ ├── uploads │ │ │ │ │ └── [filename] │ │ │ │ │ │ └── route.ts │ │ │ │ └── trpc │ │ │ │ │ └── [trpc] │ │ │ │ │ └── route.ts │ │ │ ├── (noauth) │ │ │ │ ├── (logreg) │ │ │ │ │ ├── login │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── register │ │ │ │ │ │ └── page.tsx │ │ │ │ │ └── layout.tsx │ │ │ │ ├── page.tsx │ │ │ │ └── layout.tsx │ │ │ ├── fonts │ │ │ │ └── NotoSansSundanese-Regular.ttf │ │ │ ├── (auth) │ │ │ │ └── admin │ │ │ │ │ ├── (adminRole) │ │ │ │ │ ├── layout.tsx │ │ │ │ │ ├── kandidat │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── statistik │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── page.tsx │ │ │ │ │ └── pengaturan │ │ │ │ │ │ └── page.tsx │ │ │ │ │ └── partisipan │ │ │ │ │ └── page.tsx │ │ │ ├── globals.css │ │ │ └── _components │ │ │ │ └── nav │ │ │ │ └── nav-items.tsx │ │ ├── trpc │ │ │ ├── server.ts │ │ │ └── react.tsx │ │ └── env.ts │ ├── public │ │ ├── sora.png │ │ └── favicon.ico │ ├── postcss.config.cjs │ ├── eslint.config.js │ ├── tsconfig.json │ ├── tailwind.config.ts │ ├── next.config.js │ ├── README.md │ ├── package.json │ └── Dockerfile └── clients │ ├── attendance │ ├── src │ │ ├── vite-env.d.ts │ │ ├── utils │ │ │ ├── api.tsx │ │ │ └── atom.ts │ │ ├── main.tsx │ │ ├── routes │ │ │ └── main-page.tsx │ │ ├── env.ts │ │ ├── index.css │ │ ├── components │ │ │ └── scanner │ │ │ │ └── main-scanner.tsx │ │ └── App.tsx │ ├── public │ │ └── favicon.ico │ ├── .env.example │ ├── postcss.config.cjs │ ├── tsconfig.json │ ├── .gitignore │ ├── tsconfig.node.json │ ├── index.html │ ├── vite.config.ts │ ├── eslint.config.js │ ├── tailwind.config.ts │ ├── tsconfig.app.json │ ├── README.md │ ├── Dockerfile │ └── package.json │ └── chooser │ ├── src │ ├── vite-env.d.ts │ ├── utils │ │ ├── api.tsx │ │ └── atom.ts │ ├── main.tsx │ ├── components │ │ ├── universal-loading.tsx │ │ ├── scanner │ │ │ ├── main-scanner.tsx │ │ │ └── index.tsx │ │ └── universal-error.tsx │ ├── routes │ │ └── main-page.tsx │ ├── env.ts │ ├── index.css │ ├── App.tsx │ └── context │ │ └── hardware-websocket.tsx │ ├── public │ └── favicon.ico │ ├── postcss.config.cjs │ ├── tsconfig.json │ ├── .env.example │ ├── .gitignore │ ├── tsconfig.node.json │ ├── index.html │ ├── vite.config.ts │ ├── eslint.config.js │ ├── tailwind.config.ts │ ├── tsconfig.app.json │ ├── README.md │ ├── Dockerfile │ └── package.json ├── packages ├── id-generator │ ├── src │ │ ├── index.ts │ │ └── generator.ts │ ├── eslint.config.js │ ├── tsconfig.json │ ├── .gitignore │ └── package.json ├── config │ ├── package.json │ ├── eslint │ │ ├── package.json │ │ └── index.js │ └── schema │ │ ├── package.json │ │ ├── admin.settings.schema.ts │ │ ├── admin.participant.schema.ts │ │ ├── admin.candidate.schema.ts │ │ └── auth.schema.ts ├── db │ ├── src │ │ ├── index.ts │ │ ├── schema │ │ │ ├── _table.ts │ │ │ └── main.ts │ │ ├── config.ts │ │ └── client.ts │ ├── eslint.config.js │ ├── tsconfig.json │ └── package.json ├── validators │ ├── src │ │ ├── index.ts │ │ ├── admin.ts │ │ ├── settings.ts │ │ ├── auth.ts │ │ ├── participant.ts │ │ └── candidate.ts │ ├── eslint.config.js │ ├── tsconfig.json │ └── package.json ├── ui │ ├── src │ │ ├── index.ts │ │ ├── skeleton.tsx │ │ ├── label.tsx │ │ ├── input.tsx │ │ ├── separator.tsx │ │ ├── client-not-found.tsx │ │ ├── toast.tsx │ │ ├── tooltip.tsx │ │ ├── switch.tsx │ │ ├── theme.tsx │ │ ├── avatar.tsx │ │ ├── resizable.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ └── table.tsx │ ├── eslint.config.js │ ├── tsconfig.json │ ├── tailwind.config.ts │ ├── components.json │ └── package.json ├── api │ ├── eslint.config.js │ ├── tsconfig.json │ ├── src │ │ ├── root.ts │ │ ├── index.ts │ │ └── router │ │ │ ├── settings.ts │ │ │ ├── auth.ts │ │ │ └── statistic.ts │ └── package.json ├── auth │ ├── tsconfig.json │ ├── eslint.config.js │ ├── src │ │ ├── index.ts │ │ ├── index.rsc.ts │ │ └── config.ts │ ├── env.ts │ └── package.json ├── settings │ ├── eslint.config.js │ ├── tsconfig.json │ ├── package.json │ └── src │ │ ├── index.ts │ │ └── SettingsManager.ts └── db-migrate │ ├── eslint.config.js │ ├── tsconfig.json │ ├── migrations │ ├── meta │ │ └── _journal.json │ └── 0000_confused_forgotten_one.sql │ ├── src │ └── index.ts │ └── package.json ├── vercel.json ├── assets ├── tutorial │ ├── 001-ping-host.png │ ├── 011-print-pdf.png │ ├── 009-export-json.png │ ├── 010-simpan-file.png │ ├── 008-selesai-upload.jpg │ ├── 003-ke-halaman-login.png │ ├── 004-halaman-beranda.png │ ├── 012-prompt-save-pdf.png │ ├── 005-ke-halaman-peserta.jpg │ ├── 007-contoh-upload-csv.png │ ├── 002-halaman-daftar-admin.png │ └── 006-halaman-tambah-peserta.png └── samples │ ├── Kandidat Nama OS │ ├── Manjaro 5.png │ ├── Ubuntu 1.png │ ├── Arch Linux 3.png │ ├── Linux Mint 4.png │ └── MX Linux 2.png │ ├── Kandidat Nama Orang │ ├── 3_Tole.png │ ├── 4_Budi.png │ ├── 5_Agus.png │ ├── 1_Entonk.png │ └── 2_Ujang.png │ └── Paslon Nama OS │ ├── Ubuntu Mint.png │ ├── Manjaro Arch.png │ └── Pop OS - MX Linux.png ├── tooling ├── typescript │ ├── package.json │ ├── internal-package.json │ └── base.json ├── tailwind │ ├── eslint.config.js │ ├── native.ts │ ├── tsconfig.json │ ├── package.json │ ├── web.ts │ └── base.ts ├── eslint │ ├── tsconfig.json │ ├── nextjs.js │ ├── react.js │ ├── package.json │ ├── types.d.ts │ └── base.js └── prettier │ ├── tsconfig.json │ ├── package.json │ └── index.js ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── start-web.sh ├── turbo └── generators │ ├── templates │ ├── eslint.config.js.hbs │ ├── tsconfig.json.hbs │ └── package.json.hbs │ └── config.ts ├── .gitignore ├── .env.example ├── .github └── workflows │ └── ci.yml ├── turbo.json ├── package.json └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 20.12 -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /apps/processor/.gitignore: -------------------------------------------------------------------------------- 1 | processor.log 2 | -------------------------------------------------------------------------------- /apps/processor/README.md: -------------------------------------------------------------------------------- 1 | ## Vote Processor 2 | -------------------------------------------------------------------------------- /packages/id-generator/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./generator"; 2 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "github": { 3 | "silent": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /apps/web/src/app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | export { GET, POST } from "@sora-vp/auth"; 2 | -------------------------------------------------------------------------------- /apps/web/public/sora.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sora-vp/baseline/HEAD/apps/web/public/sora.png -------------------------------------------------------------------------------- /apps/web/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /apps/web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sora-vp/baseline/HEAD/apps/web/public/favicon.ico -------------------------------------------------------------------------------- /packages/config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "config", 3 | "version": "0.0.0", 4 | "private": true 5 | } 6 | -------------------------------------------------------------------------------- /packages/db/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "drizzle-orm/sql"; 2 | export { alias } from "drizzle-orm/mysql-core"; 3 | -------------------------------------------------------------------------------- /assets/tutorial/001-ping-host.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sora-vp/baseline/HEAD/assets/tutorial/001-ping-host.png -------------------------------------------------------------------------------- /assets/tutorial/011-print-pdf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sora-vp/baseline/HEAD/assets/tutorial/011-print-pdf.png -------------------------------------------------------------------------------- /assets/tutorial/009-export-json.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sora-vp/baseline/HEAD/assets/tutorial/009-export-json.png -------------------------------------------------------------------------------- /assets/tutorial/010-simpan-file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sora-vp/baseline/HEAD/assets/tutorial/010-simpan-file.png -------------------------------------------------------------------------------- /apps/clients/attendance/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare const APP_VERSION: string; 4 | -------------------------------------------------------------------------------- /apps/clients/chooser/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare const APP_VERSION: string; 4 | -------------------------------------------------------------------------------- /assets/tutorial/008-selesai-upload.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sora-vp/baseline/HEAD/assets/tutorial/008-selesai-upload.jpg -------------------------------------------------------------------------------- /apps/clients/chooser/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sora-vp/baseline/HEAD/apps/clients/chooser/public/favicon.ico -------------------------------------------------------------------------------- /apps/web/src/app/(noauth)/(logreg)/login/page.tsx: -------------------------------------------------------------------------------- 1 | export { LoginComponent as default } from "~/app/_components/auth/login-page"; 2 | -------------------------------------------------------------------------------- /assets/tutorial/003-ke-halaman-login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sora-vp/baseline/HEAD/assets/tutorial/003-ke-halaman-login.png -------------------------------------------------------------------------------- /assets/tutorial/004-halaman-beranda.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sora-vp/baseline/HEAD/assets/tutorial/004-halaman-beranda.png -------------------------------------------------------------------------------- /assets/tutorial/012-prompt-save-pdf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sora-vp/baseline/HEAD/assets/tutorial/012-prompt-save-pdf.png -------------------------------------------------------------------------------- /apps/clients/attendance/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sora-vp/baseline/HEAD/apps/clients/attendance/public/favicon.ico -------------------------------------------------------------------------------- /assets/tutorial/005-ke-halaman-peserta.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sora-vp/baseline/HEAD/assets/tutorial/005-ke-halaman-peserta.jpg -------------------------------------------------------------------------------- /assets/tutorial/007-contoh-upload-csv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sora-vp/baseline/HEAD/assets/tutorial/007-contoh-upload-csv.png -------------------------------------------------------------------------------- /assets/samples/Kandidat Nama OS/Manjaro 5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sora-vp/baseline/HEAD/assets/samples/Kandidat Nama OS/Manjaro 5.png -------------------------------------------------------------------------------- /assets/samples/Kandidat Nama OS/Ubuntu 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sora-vp/baseline/HEAD/assets/samples/Kandidat Nama OS/Ubuntu 1.png -------------------------------------------------------------------------------- /assets/samples/Kandidat Nama Orang/3_Tole.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sora-vp/baseline/HEAD/assets/samples/Kandidat Nama Orang/3_Tole.png -------------------------------------------------------------------------------- /assets/samples/Kandidat Nama Orang/4_Budi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sora-vp/baseline/HEAD/assets/samples/Kandidat Nama Orang/4_Budi.png -------------------------------------------------------------------------------- /assets/samples/Kandidat Nama Orang/5_Agus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sora-vp/baseline/HEAD/assets/samples/Kandidat Nama Orang/5_Agus.png -------------------------------------------------------------------------------- /assets/samples/Paslon Nama OS/Ubuntu Mint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sora-vp/baseline/HEAD/assets/samples/Paslon Nama OS/Ubuntu Mint.png -------------------------------------------------------------------------------- /assets/tutorial/002-halaman-daftar-admin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sora-vp/baseline/HEAD/assets/tutorial/002-halaman-daftar-admin.png -------------------------------------------------------------------------------- /apps/clients/attendance/.env.example: -------------------------------------------------------------------------------- 1 | # This is useful for development or manual deployment 2 | VITE_TRPC_URL="http://maybeyourlocalip/api/trpc" 3 | -------------------------------------------------------------------------------- /apps/clients/attendance/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | module.exports = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /apps/clients/chooser/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | module.exports = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /apps/processor/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sora-vp/tsconfig/base.json", 3 | "include": ["src"], 4 | "exclude": ["node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /assets/samples/Kandidat Nama OS/Arch Linux 3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sora-vp/baseline/HEAD/assets/samples/Kandidat Nama OS/Arch Linux 3.png -------------------------------------------------------------------------------- /assets/samples/Kandidat Nama OS/Linux Mint 4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sora-vp/baseline/HEAD/assets/samples/Kandidat Nama OS/Linux Mint 4.png -------------------------------------------------------------------------------- /assets/samples/Kandidat Nama OS/MX Linux 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sora-vp/baseline/HEAD/assets/samples/Kandidat Nama OS/MX Linux 2.png -------------------------------------------------------------------------------- /assets/samples/Kandidat Nama Orang/1_Entonk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sora-vp/baseline/HEAD/assets/samples/Kandidat Nama Orang/1_Entonk.png -------------------------------------------------------------------------------- /assets/samples/Kandidat Nama Orang/2_Ujang.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sora-vp/baseline/HEAD/assets/samples/Kandidat Nama Orang/2_Ujang.png -------------------------------------------------------------------------------- /assets/samples/Paslon Nama OS/Manjaro Arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sora-vp/baseline/HEAD/assets/samples/Paslon Nama OS/Manjaro Arch.png -------------------------------------------------------------------------------- /assets/tutorial/006-halaman-tambah-peserta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sora-vp/baseline/HEAD/assets/tutorial/006-halaman-tambah-peserta.png -------------------------------------------------------------------------------- /apps/web/src/app/(noauth)/(logreg)/register/page.tsx: -------------------------------------------------------------------------------- 1 | export { RegistrationComponent as default } from "~/app/_components/auth/registration-page"; 2 | -------------------------------------------------------------------------------- /apps/web/src/app/fonts/NotoSansSundanese-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sora-vp/baseline/HEAD/apps/web/src/app/fonts/NotoSansSundanese-Regular.ttf -------------------------------------------------------------------------------- /assets/samples/Paslon Nama OS/Pop OS - MX Linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sora-vp/baseline/HEAD/assets/samples/Paslon Nama OS/Pop OS - MX Linux.png -------------------------------------------------------------------------------- /tooling/typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sora-vp/tsconfig", 3 | "private": true, 4 | "version": "0.1.0", 5 | "files": [ 6 | "*.json" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /packages/validators/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./auth"; 2 | export * from "./admin"; 3 | export * from "./settings"; 4 | export * from "./participant"; 5 | export * from "./candidate"; 6 | -------------------------------------------------------------------------------- /apps/clients/attendance/src/utils/api.tsx: -------------------------------------------------------------------------------- 1 | import type { AppRouter } from "@sora-vp/api"; 2 | import { createTRPCReact } from "@trpc/react-query"; 3 | 4 | export const api = createTRPCReact(); 5 | -------------------------------------------------------------------------------- /apps/clients/chooser/src/utils/api.tsx: -------------------------------------------------------------------------------- 1 | import type { AppRouter } from "@sora-vp/api"; 2 | import { createTRPCReact } from "@trpc/react-query"; 3 | 4 | export const api = createTRPCReact(); 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "esbenp.prettier-vscode", 5 | "yoavbls.pretty-ts-errors", 6 | "bradlc.vscode-tailwindcss" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /start-web.sh: -------------------------------------------------------------------------------- 1 | # Migrate 2 | echo "Memulai proses migrasi sora..." 3 | sh "/db-migrate/db-migrate-release-command.sh" 4 | 5 | # Mulai aplikasi web 6 | echo "Memulai web server..." 7 | node apps/web/server.js 8 | -------------------------------------------------------------------------------- /packages/ui/src/index.ts: -------------------------------------------------------------------------------- 1 | import { cx } from "class-variance-authority"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | const cn = (...inputs: Parameters) => twMerge(cx(inputs)); 5 | 6 | export { cn }; 7 | -------------------------------------------------------------------------------- /apps/clients/attendance/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.app.json" 6 | }, 7 | { 8 | "path": "./tsconfig.node.json" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /apps/clients/chooser/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.app.json" 6 | }, 7 | { 8 | "path": "./tsconfig.node.json" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /tooling/tailwind/eslint.config.js: -------------------------------------------------------------------------------- 1 | // FIXME: This kinda stinks... 2 | /// 3 | 4 | import baseConfig from "@sora-vp/eslint-config/base"; 5 | 6 | export default [...baseConfig]; 7 | -------------------------------------------------------------------------------- /tooling/tailwind/native.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | import base from "./base"; 4 | 5 | export default { 6 | content: base.content, 7 | presets: [base], 8 | theme: {}, 9 | } satisfies Config; 10 | -------------------------------------------------------------------------------- /apps/clients/chooser/.env.example: -------------------------------------------------------------------------------- 1 | # This is for uploads endpoint 2 | VITE_IMAGE_RETRIEVER="http://maybeyourlocalip/api/uploads" 3 | 4 | # This is useful for development or manual deployment 5 | VITE_TRPC_URL="http://maybeyourlocalip/api/trpc" 6 | -------------------------------------------------------------------------------- /tooling/eslint/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sora-vp/tsconfig/base.json", 3 | "compilerOptions": { 4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" 5 | }, 6 | "include": ["."], 7 | "exclude": ["node_modules"] 8 | } 9 | -------------------------------------------------------------------------------- /apps/processor/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig({ 4 | clean: true, 5 | entry: ["src/index.ts"], 6 | noExternal: ["@sora-vp/db", "@sora-vp/id-generator"], 7 | format: ["esm"], 8 | }); 9 | -------------------------------------------------------------------------------- /packages/api/eslint.config.js: -------------------------------------------------------------------------------- 1 | import baseConfig from "@sora-vp/eslint-config/base"; 2 | 3 | /** @type {import('typescript-eslint').Config} */ 4 | export default [ 5 | { 6 | ignores: ["dist/**"], 7 | }, 8 | ...baseConfig, 9 | ]; 10 | -------------------------------------------------------------------------------- /packages/db/eslint.config.js: -------------------------------------------------------------------------------- 1 | import baseConfig from "@sora-vp/eslint-config/base"; 2 | 3 | /** @type {import('typescript-eslint').Config} */ 4 | export default [ 5 | { 6 | ignores: ["dist/**"], 7 | }, 8 | ...baseConfig, 9 | ]; 10 | -------------------------------------------------------------------------------- /tooling/prettier/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sora-vp/tsconfig/base.json", 3 | "compilerOptions": { 4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" 5 | }, 6 | "include": ["."], 7 | "exclude": ["node_modules"] 8 | } 9 | -------------------------------------------------------------------------------- /tooling/tailwind/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sora-vp/tsconfig/base.json", 3 | "compilerOptions": { 4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" 5 | }, 6 | "include": ["."], 7 | "exclude": ["node_modules"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/auth/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sora-vp/tsconfig/base.json", 3 | "compilerOptions": { 4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" 5 | }, 6 | "include": ["src", "*.ts"], 7 | "exclude": ["node_modules"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/settings/eslint.config.js: -------------------------------------------------------------------------------- 1 | import baseConfig from "@sora-vp/eslint-config/base"; 2 | 3 | /** @type {import('typescript-eslint').Config} */ 4 | export default [ 5 | { 6 | ignores: ["dist/**"], 7 | }, 8 | ...baseConfig, 9 | ]; 10 | -------------------------------------------------------------------------------- /packages/db-migrate/eslint.config.js: -------------------------------------------------------------------------------- 1 | import baseConfig from "@sora-vp/eslint-config/base"; 2 | 3 | /** @type {import('typescript-eslint').Config} */ 4 | export default [ 5 | { 6 | ignores: ["dist/**"], 7 | }, 8 | ...baseConfig, 9 | ]; 10 | -------------------------------------------------------------------------------- /packages/id-generator/eslint.config.js: -------------------------------------------------------------------------------- 1 | import baseConfig from "@sora-vp/eslint-config/base"; 2 | 3 | /** @type {import('typescript-eslint').Config} */ 4 | export default [ 5 | { 6 | ignores: ["dist/**"], 7 | }, 8 | ...baseConfig, 9 | ]; 10 | -------------------------------------------------------------------------------- /packages/validators/eslint.config.js: -------------------------------------------------------------------------------- 1 | import baseConfig from "@sora-vp/eslint-config/base"; 2 | 3 | /** @type {import('typescript-eslint').Config} */ 4 | export default [ 5 | { 6 | ignores: ["dist/**"], 7 | }, 8 | ...baseConfig, 9 | ]; 10 | -------------------------------------------------------------------------------- /turbo/generators/templates/eslint.config.js.hbs: -------------------------------------------------------------------------------- 1 | import baseConfig from "@sora-vp/eslint-config/base"; 2 | 3 | /** @type {import('typescript-eslint').Config} */ 4 | export default [ 5 | { 6 | ignores: [], 7 | }, 8 | ...baseConfig, 9 | ]; 10 | -------------------------------------------------------------------------------- /turbo/generators/templates/tsconfig.json.hbs: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sora-vp/tsconfig/base.json", 3 | "compilerOptions": { 4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" 5 | }, 6 | "include": ["*.ts", "src"], 7 | "exclude": ["node_modules"] 8 | } 9 | -------------------------------------------------------------------------------- /apps/processor/eslint.config.js: -------------------------------------------------------------------------------- 1 | import baseConfig from "@sora-vp/eslint-config/base"; 2 | 3 | /** @type {import('typescript-eslint').Config} */ 4 | export default [ 5 | { 6 | ignores: ["dist/**", "index.ts", "tsup.config.ts"], 7 | }, 8 | ...baseConfig, 9 | ]; 10 | -------------------------------------------------------------------------------- /packages/api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sora-vp/tsconfig/internal-package.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" 6 | }, 7 | "include": ["src"], 8 | "exclude": ["node_modules"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/auth/eslint.config.js: -------------------------------------------------------------------------------- 1 | import baseConfig, { restrictEnvAccess } from "@sora-vp/eslint-config/base"; 2 | 3 | /** @type {import('typescript-eslint').Config} */ 4 | export default [ 5 | { 6 | ignores: [], 7 | }, 8 | ...baseConfig, 9 | ...restrictEnvAccess, 10 | ]; 11 | -------------------------------------------------------------------------------- /packages/db/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sora-vp/tsconfig/internal-package.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" 6 | }, 7 | "include": ["src"], 8 | "exclude": ["node_modules"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/db-migrate/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sora-vp/tsconfig/internal-package.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" 6 | }, 7 | "include": ["src"], 8 | "exclude": ["node_modules"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/settings/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sora-vp/tsconfig/internal-package.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" 6 | }, 7 | "include": ["src"], 8 | "exclude": ["node_modules"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/id-generator/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sora-vp/tsconfig/internal-package.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" 6 | }, 7 | "include": ["src"], 8 | "exclude": ["node_modules"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/validators/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sora-vp/tsconfig/internal-package.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" 6 | }, 7 | "include": ["*.ts", "src"], 8 | "exclude": ["node_modules"] 9 | } 10 | -------------------------------------------------------------------------------- /apps/processor/index.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { fileURLToPath } from "url"; 3 | 4 | import { consumeMessagesFromQueue } from "./src/index"; 5 | 6 | const __filename = fileURLToPath(import.meta.url); 7 | const __dirname = path.dirname(__filename); 8 | 9 | void consumeMessagesFromQueue(__dirname); 10 | -------------------------------------------------------------------------------- /packages/db-migrate/migrations/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "5", 3 | "dialect": "mysql", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "5", 8 | "when": 1719041544918, 9 | "tag": "0000_confused_forgotten_one", 10 | "breakpoints": true 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /packages/ui/eslint.config.js: -------------------------------------------------------------------------------- 1 | import baseConfig from "@sora-vp/eslint-config/base"; 2 | import reactConfig from "@sora-vp/eslint-config/react"; 3 | 4 | /** @type {import('typescript-eslint').Config} */ 5 | export default [ 6 | { 7 | ignores: [], 8 | }, 9 | ...baseConfig, 10 | ...reactConfig, 11 | ]; 12 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Next.js", 6 | "type": "node-terminal", 7 | "request": "launch", 8 | "command": "pnpm dev", 9 | "cwd": "${workspaceFolder}/apps/nextjs/", 10 | "skipFiles": ["/**"] 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /apps/clients/attendance/src/utils/atom.ts: -------------------------------------------------------------------------------- 1 | import { atomWithStorage } from "jotai/utils"; 2 | 3 | /** 4 | * Atom yang digunakan untuk menentukan lama waktu tampil notifikasi berhasil 5 | * setelah partisipan menunjukan gambar QR dan terbaca oleh server. 6 | */ 7 | export const successTimeoutAtom = atomWithStorage("successTimeout", 5_000); 8 | -------------------------------------------------------------------------------- /packages/auth/src/index.ts: -------------------------------------------------------------------------------- 1 | import NextAuth from "next-auth"; 2 | 3 | import { authConfig } from "./config"; 4 | 5 | export type { Session } from "next-auth"; 6 | 7 | const { 8 | handlers: { GET, POST }, 9 | auth, 10 | signIn, 11 | signOut, 12 | } = NextAuth(authConfig); 13 | 14 | export { GET, POST, auth, signIn, signOut }; 15 | -------------------------------------------------------------------------------- /packages/ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sora-vp/tsconfig/internal-package.json", 3 | "compilerOptions": { 4 | "lib": ["dom", "dom.iterable", "ES2022"], 5 | "jsx": "preserve", 6 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" 7 | }, 8 | "include": ["*.ts", "src"], 9 | "exclude": ["node_modules"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/ui/src/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@sora-vp/ui"; 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ); 13 | } 14 | 15 | export { Skeleton }; 16 | -------------------------------------------------------------------------------- /tooling/typescript/internal-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "./base.json", 4 | "compilerOptions": { 5 | /** Emit types for internal packages to speed up editor performance. */ 6 | "declaration": true, 7 | "declarationMap": true, 8 | "emitDeclarationOnly": true, 9 | "noEmit": false 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /apps/clients/attendance/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /apps/clients/chooser/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /packages/ui/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is not used for any compilation purpose, it is only used 3 | * for Tailwind Intellisense & Autocompletion in the source files 4 | */ 5 | import type { Config } from "tailwindcss"; 6 | 7 | import baseConfig from "@sora-vp/tailwind-config/web"; 8 | 9 | export default { 10 | content: ["./src/**/*.tsx"], 11 | presets: [baseConfig], 12 | } satisfies Config; 13 | -------------------------------------------------------------------------------- /apps/clients/attendance/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 5 | "skipLibCheck": true, 6 | "module": "ESNext", 7 | "moduleResolution": "bundler", 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "noEmit": true 11 | }, 12 | "include": ["vite.config.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /apps/clients/chooser/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 5 | "skipLibCheck": true, 6 | "module": "ESNext", 7 | "moduleResolution": "bundler", 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "noEmit": true 11 | }, 12 | "include": ["vite.config.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /packages/db/src/schema/_table.ts: -------------------------------------------------------------------------------- 1 | import { mysqlTableCreator } from "drizzle-orm/mysql-core"; 2 | 3 | /** 4 | * This is an example of how to use the multi-project schema feature of Drizzle ORM. 5 | * Use the same database instance for multiple projects. 6 | * 7 | * @see https://orm.drizzle.team/docs/goodies#multi-project-schema 8 | */ 9 | export const mySqlTable = mysqlTableCreator((name) => `sora_${name}`); 10 | -------------------------------------------------------------------------------- /apps/clients/chooser/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Chooser Client 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /apps/clients/attendance/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Attendance Client 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/ui/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "./tailwind.config.ts", 8 | "css": "unused.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "utils": "@sora-vp/ui", 14 | "components": "src/", 15 | "ui": "src/" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /apps/web/src/app/(auth)/admin/(adminRole)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | 3 | import { auth } from "@sora-vp/auth"; 4 | 5 | export default async function RootLayout(props: { children: React.ReactNode }) { 6 | const isLoggedIn = await auth(); 7 | 8 | if (!isLoggedIn) redirect("/login"); 9 | 10 | if (isLoggedIn.user.role !== "admin") redirect("/admin/partisipan"); 11 | 12 | return <>{props.children}; 13 | } 14 | -------------------------------------------------------------------------------- /apps/web/eslint.config.js: -------------------------------------------------------------------------------- 1 | import baseConfig, { restrictEnvAccess } from "@sora-vp/eslint-config/base"; 2 | import nextjsConfig from "@sora-vp/eslint-config/nextjs"; 3 | import reactConfig from "@sora-vp/eslint-config/react"; 4 | 5 | /** @type {import('typescript-eslint').Config} */ 6 | export default [ 7 | { 8 | ignores: [".next/**"], 9 | }, 10 | ...baseConfig, 11 | ...reactConfig, 12 | ...nextjsConfig, 13 | ...restrictEnvAccess, 14 | ]; 15 | -------------------------------------------------------------------------------- /apps/clients/chooser/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from "@vitejs/plugin-react-swc"; 2 | import { defineConfig } from "vite"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | define: { 8 | // eslint-disable-next-line no-restricted-properties 9 | APP_VERSION: JSON.stringify(process.env.npm_package_version), 10 | }, 11 | resolve: { 12 | alias: [{ find: "@", replacement: "/src" }], 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /apps/clients/attendance/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from "@vitejs/plugin-react-swc"; 2 | import { defineConfig } from "vite"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | define: { 8 | // eslint-disable-next-line no-restricted-properties 9 | APP_VERSION: JSON.stringify(process.env.npm_package_version), 10 | }, 11 | resolve: { 12 | alias: [{ find: "@", replacement: "/src" }], 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /packages/id-generator/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .idea 17 | .DS_Store 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw? 23 | 24 | release 25 | .vscode/.debug.env 26 | package-lock.json 27 | pnpm-lock.yaml 28 | yarn.lock 29 | dist-electron 30 | dist/ 31 | -------------------------------------------------------------------------------- /apps/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sora-vp/tsconfig/base.json", 3 | "compilerOptions": { 4 | "lib": ["es2022", "dom", "dom.iterable"], 5 | "jsx": "preserve", 6 | "baseUrl": ".", 7 | "paths": { 8 | "~/*": ["./src/*"] 9 | }, 10 | "plugins": [{ "name": "next" }], 11 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json", 12 | "module": "esnext" 13 | }, 14 | "include": [".", ".next/types/**/*.ts"], 15 | "exclude": ["node_modules"] 16 | } 17 | -------------------------------------------------------------------------------- /packages/auth/env.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-restricted-properties */ 2 | import { createEnv } from "@t3-oss/env-nextjs"; 3 | import { z } from "zod"; 4 | 5 | export const env = createEnv({ 6 | server: { 7 | AUTH_SECRET: 8 | process.env.NODE_ENV === "production" 9 | ? z.string().min(1) 10 | : z.string().min(1).optional(), 11 | }, 12 | client: {}, 13 | experimental__runtimeEnv: {}, 14 | skipValidation: !!process.env.CI || !!process.env.SKIP_ENV_VALIDATION, 15 | }); 16 | -------------------------------------------------------------------------------- /packages/id-generator/src/generator.ts: -------------------------------------------------------------------------------- 1 | import { customAlphabet } from "nanoid"; 2 | 3 | const customToken = "1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ"; 4 | const arrayValidator = customToken.split(""); 5 | 6 | export const nanoid = customAlphabet(customToken, 15); 7 | export const randomFileName = customAlphabet("1234567890abcdef", 10); 8 | 9 | export const validateId = (id: string) => 10 | id 11 | .split("") 12 | .map((char) => arrayValidator.includes(char)) 13 | 14 | // Every item is true 15 | .every((item) => item); 16 | -------------------------------------------------------------------------------- /apps/processor/src/api.ts: -------------------------------------------------------------------------------- 1 | import type { AppRouter } from "@sora-vp/api"; 2 | import type { inferRouterOutputs } from "@trpc/server"; 3 | import { createTRPCProxyClient, httpLink } from "@trpc/client"; 4 | import superjson from "superjson"; 5 | 6 | import { env } from "./env"; 7 | 8 | export const api = createTRPCProxyClient({ 9 | links: [ 10 | httpLink({ 11 | url: env.PROCESSOR_API_URL, 12 | transformer: superjson, 13 | }), 14 | ], 15 | }); 16 | 17 | export type RouterOutputs = inferRouterOutputs; 18 | -------------------------------------------------------------------------------- /tooling/eslint/nextjs.js: -------------------------------------------------------------------------------- 1 | import nextPlugin from "@next/eslint-plugin-next"; 2 | 3 | /** @type {Awaited} */ 4 | export default [ 5 | { 6 | files: ["**/*.ts", "**/*.tsx"], 7 | plugins: { 8 | "@next/next": nextPlugin, 9 | }, 10 | rules: { 11 | ...nextPlugin.configs.recommended.rules, 12 | ...nextPlugin.configs["core-web-vitals"].rules, 13 | // TypeError: context.getAncestors is not a function 14 | "@next/next/no-duplicate-head": "off", 15 | }, 16 | }, 17 | ]; 18 | -------------------------------------------------------------------------------- /apps/web/src/app/(auth)/admin/partisipan/page.tsx: -------------------------------------------------------------------------------- 1 | import { DataTable } from "~/app/_components/participant/data-table"; 2 | 3 | export default function ParticipantPage() { 4 | return ( 5 |
6 |
7 |

Partisipan

8 |

9 | Kelola partisipan yang tercantum sebagai daftar pemilih tetap. 10 |

11 |
12 | 13 | 14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /apps/processor/src/db.ts: -------------------------------------------------------------------------------- 1 | import { drizzle } from "drizzle-orm/mysql2"; 2 | import mysql from "mysql2/promise"; 3 | 4 | import * as schema from "@sora-vp/db/schema"; 5 | 6 | import { env } from "./env"; 7 | 8 | const connectionStr = new URL(`mysql://${env.DB_HOST}/${env.DB_NAME}`); 9 | connectionStr.username = env.DB_USERNAME; 10 | connectionStr.password = env.DB_PASSWORD; 11 | 12 | const poolConnection = mysql.createPool(connectionStr.toString()); 13 | 14 | export const db = drizzle(poolConnection, { 15 | schema, 16 | mode: "default", 17 | }); 18 | 19 | export { schema }; 20 | -------------------------------------------------------------------------------- /apps/processor/src/env.ts: -------------------------------------------------------------------------------- 1 | import { createEnv } from "@t3-oss/env-core"; 2 | import { z } from "zod"; 3 | 4 | export const env = createEnv({ 5 | clientPrefix: "", 6 | client: {}, 7 | 8 | server: { 9 | DB_HOST: z.string(), 10 | DB_NAME: z.string(), 11 | DB_PASSWORD: z.string(), 12 | DB_USERNAME: z.string(), 13 | AMQP_URL: z.string().url(), 14 | PROCESSOR_API_URL: z.string().url(), 15 | }, 16 | 17 | runtimeEnv: process.env, 18 | emptyStringAsUndefined: true, 19 | skipValidation: !!process.env.CI || !!process.env.SKIP_ENV_VALIDATION, 20 | }); 21 | -------------------------------------------------------------------------------- /packages/validators/src/admin.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | const id = z.number().min(1).int(); 4 | const role = z.enum(["admin", "comittee"], { 5 | required_error: "Dimohon untuk memilih tingkatan pengguna", 6 | }); 7 | 8 | const ServerAcceptObjectIdNumber = z.object({ 9 | id, 10 | }); 11 | 12 | const ServerAcceptIdAndRole = z.object({ 13 | id, 14 | role, 15 | }); 16 | 17 | const RoleFormSchema = z.object({ 18 | role, 19 | }); 20 | 21 | export const admin = { 22 | ServerAcceptObjectIdNumber, 23 | ServerAcceptIdAndRole, 24 | RoleFormSchema, 25 | } as const; 26 | -------------------------------------------------------------------------------- /packages/config/eslint/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sora/eslint-config", 3 | "version": "0.1.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "@types/eslint": "^8.37.0", 8 | "@typescript-eslint/eslint-plugin": "^5.59.2", 9 | "@typescript-eslint/parser": "^5.59.2", 10 | "eslint-config-next": "^13.4.2", 11 | "eslint-config-prettier": "^8.8.0", 12 | "eslint-config-turbo": "^1.9.8", 13 | "eslint-plugin-react": "7.32.2" 14 | }, 15 | "devDependencies": { 16 | "eslint": "^8.40.0", 17 | "typescript": "^5.2.2" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /apps/web/src/app/(auth)/admin/(adminRole)/kandidat/page.tsx: -------------------------------------------------------------------------------- 1 | import { DataTable } from "~/app/_components/candidate/data-table"; 2 | 3 | export default function CandidatePage() { 4 | return ( 5 |
6 |
7 |

Kandidat

8 |

9 | Kelola siapa saja yang menjadi kandidat untuk dipilih pada halaman 10 | ini. 11 |

12 |
13 | 14 | 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /tooling/eslint/react.js: -------------------------------------------------------------------------------- 1 | import reactPlugin from "eslint-plugin-react"; 2 | import hooksPlugin from "eslint-plugin-react-hooks"; 3 | 4 | /** @type {Awaited} */ 5 | export default [ 6 | { 7 | files: ["**/*.ts", "**/*.tsx"], 8 | plugins: { 9 | react: reactPlugin, 10 | "react-hooks": hooksPlugin, 11 | }, 12 | rules: { 13 | ...reactPlugin.configs["jsx-runtime"].rules, 14 | ...hooksPlugin.configs.recommended.rules, 15 | }, 16 | languageOptions: { 17 | globals: { 18 | React: "writable", 19 | }, 20 | }, 21 | }, 22 | ]; 23 | -------------------------------------------------------------------------------- /apps/clients/chooser/eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import eslint from "@eslint/js"; 4 | import reactRefresh from "eslint-plugin-react-refresh"; 5 | import tseslint from "typescript-eslint"; 6 | 7 | import { restrictEnvAccess } from "@sora-vp/eslint-config/base"; 8 | import reactConfig from "@sora-vp/eslint-config/react"; 9 | 10 | export default tseslint.config( 11 | { 12 | ignores: ["dist/**"], 13 | plugins: { 14 | "react-refresh": reactRefresh, 15 | }, 16 | }, 17 | eslint.configs.recommended, 18 | ...tseslint.configs.recommended, 19 | ...reactConfig, 20 | ...restrictEnvAccess, 21 | ); 22 | -------------------------------------------------------------------------------- /packages/auth/src/index.rsc.ts: -------------------------------------------------------------------------------- 1 | import { cache } from "react"; 2 | import NextAuth from "next-auth"; 3 | 4 | import { authConfig } from "./config"; 5 | 6 | export type { Session } from "next-auth"; 7 | 8 | const { 9 | handlers: { GET, POST }, 10 | auth: defaultAuth, 11 | signIn, 12 | signOut, 13 | } = NextAuth(authConfig); 14 | 15 | /** 16 | * This is the main way to get session data for your RSCs. 17 | * This will de-duplicate all calls to next-auth's default `auth()` function and only call it once per request 18 | */ 19 | const auth = cache(defaultAuth); 20 | 21 | export { GET, POST, auth, signIn, signOut }; 22 | -------------------------------------------------------------------------------- /apps/clients/attendance/eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import eslint from "@eslint/js"; 4 | import reactRefresh from "eslint-plugin-react-refresh"; 5 | import tseslint from "typescript-eslint"; 6 | 7 | import { restrictEnvAccess } from "@sora-vp/eslint-config/base"; 8 | import reactConfig from "@sora-vp/eslint-config/react"; 9 | 10 | export default tseslint.config( 11 | { 12 | ignores: ["dist/**"], 13 | plugins: { 14 | "react-refresh": reactRefresh, 15 | }, 16 | }, 17 | eslint.configs.recommended, 18 | ...tseslint.configs.recommended, 19 | ...reactConfig, 20 | ...restrictEnvAccess, 21 | ); 22 | -------------------------------------------------------------------------------- /apps/clients/attendance/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | 4 | import App from "./App"; 5 | 6 | import "non.geist"; 7 | import "non.geist/mono"; 8 | import "@fontsource-variable/noto-sans-sundanese"; 9 | import "./index.css"; 10 | 11 | import { AnimatePresence } from "motion/react"; 12 | 13 | import { Toaster } from "@sora-vp/ui/toast"; 14 | 15 | ReactDOM.createRoot(document.getElementById("root")!).render( 16 | 17 | 18 | 19 | 20 | 21 | , 22 | ); 23 | -------------------------------------------------------------------------------- /apps/clients/chooser/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | 4 | import App from "./App"; 5 | 6 | import "non.geist"; 7 | import "non.geist/mono"; 8 | import "@fontsource-variable/noto-sans-sundanese"; 9 | import "./index.css"; 10 | 11 | import { AnimatePresence } from "motion/react"; 12 | 13 | import { Toaster } from "@sora-vp/ui/toast"; 14 | 15 | ReactDOM.createRoot(document.getElementById("root")!).render( 16 | 17 | 18 | 19 | 20 | 21 | , 22 | ); 23 | -------------------------------------------------------------------------------- /apps/web/src/app/(auth)/admin/(adminRole)/statistik/page.tsx: -------------------------------------------------------------------------------- 1 | import Essential from "~/app/_components/statistic/essential"; 2 | import { Graphic } from "~/app/_components/statistic/graphic"; 3 | 4 | export default function StatisticPage() { 5 | return ( 6 |
7 |
8 | 9 |
10 |
11 | 12 |
13 | 14 | {/* for scrollable purpose */} 15 |
16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /packages/config/schema/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sora/schema-config", 3 | "version": "0.1.0", 4 | "files": [ 5 | "src/**" 6 | ], 7 | "license": "MIT", 8 | "dependencies": { 9 | "@sora/id-generator": "^0.1.0", 10 | "@types/eslint": "^8.37.0", 11 | "@typescript-eslint/eslint-plugin": "^5.59.2", 12 | "@typescript-eslint/parser": "^5.59.2", 13 | "eslint-config-next": "^13.4.2", 14 | "eslint-config-prettier": "^8.8.0", 15 | "eslint-config-turbo": "^1.9.8", 16 | "eslint-plugin-react": "7.32.2", 17 | "zod": "^3.22.2" 18 | }, 19 | "devDependencies": { 20 | "eslint": "^8.40.0", 21 | "typescript": "^5.2.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /apps/web/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | import { fontFamily } from "tailwindcss/defaultTheme"; 3 | 4 | import baseConfig from "@sora-vp/tailwind-config/web"; 5 | 6 | export default { 7 | // We need to append the path to the UI package to the content array so that 8 | // those classes are included correctly. 9 | content: [...baseConfig.content, "../../packages/ui/**/*.{ts,tsx}"], 10 | presets: [baseConfig], 11 | theme: { 12 | extend: { 13 | fontFamily: { 14 | sans: ["var(--font-geist-sans)", ...fontFamily.sans], 15 | mono: ["var(--font-geist-mono)", ...fontFamily.mono], 16 | }, 17 | }, 18 | }, 19 | } satisfies Config; 20 | -------------------------------------------------------------------------------- /apps/web/src/trpc/server.ts: -------------------------------------------------------------------------------- 1 | import { cache } from "react"; 2 | import { headers } from "next/headers"; 3 | 4 | import { createCaller, createTRPCContext } from "@sora-vp/api"; 5 | import { auth } from "@sora-vp/auth"; 6 | 7 | /** 8 | * This wraps the `createTRPCContext` helper and provides the required context for the tRPC API when 9 | * handling a tRPC call from a React Server Component. 10 | */ 11 | const createContext = cache(async () => { 12 | const heads = new Headers(await headers()); 13 | heads.set("x-trpc-source", "rsc"); 14 | 15 | return createTRPCContext({ 16 | session: await auth(), 17 | headers: heads, 18 | }); 19 | }); 20 | 21 | export const api = createCaller(createContext); 22 | -------------------------------------------------------------------------------- /tooling/prettier/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sora-vp/prettier-config", 3 | "private": true, 4 | "version": "0.1.0", 5 | "type": "module", 6 | "exports": { 7 | ".": "./index.js" 8 | }, 9 | "scripts": { 10 | "clean": "rm -rf .turbo node_modules", 11 | "format": "prettier --check . --ignore-path ../../.gitignore", 12 | "typecheck": "tsc --noEmit" 13 | }, 14 | "dependencies": { 15 | "@ianvs/prettier-plugin-sort-imports": "^4.4.1", 16 | "prettier": "^3.4.2", 17 | "prettier-plugin-tailwindcss": "^0.6.10" 18 | }, 19 | "devDependencies": { 20 | "@sora-vp/tsconfig": "*", 21 | "typescript": "^5.6.3" 22 | }, 23 | "prettier": "@sora-vp/prettier-config" 24 | } 25 | -------------------------------------------------------------------------------- /apps/processor/src/logger.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import pino from "pino"; 3 | 4 | export const initLogger = (destinationDirectoryPath: string) => 5 | pino({ 6 | transport: { 7 | targets: [ 8 | { 9 | target: "pino-pretty", 10 | level: "debug", 11 | options: { 12 | colorize: true, 13 | ignore: "pid,hostname", 14 | translateTime: "SYS:standard", 15 | }, 16 | }, 17 | { 18 | target: "pino/file", 19 | level: "debug", 20 | options: { 21 | destination: path.join(destinationDirectoryPath, "processor.log"), 22 | }, 23 | }, 24 | ], 25 | }, 26 | }); 27 | -------------------------------------------------------------------------------- /apps/processor/src/utils.ts: -------------------------------------------------------------------------------- 1 | import type { RouterOutputs } from "./api"; 2 | 3 | type TSettings = RouterOutputs["clientConsumer"]["settings"]; 4 | 5 | export const canVoteNow = (settings: TSettings) => { 6 | const waktuMulai = settings.startTime ? settings.startTime.getTime() : null; 7 | const waktuSelesai = settings.endTime ? settings.endTime.getTime() : null; 8 | 9 | const currentTime = new Date().getTime(); 10 | 11 | const canVote = 12 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 13 | waktuMulai! <= currentTime && 14 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 15 | waktuSelesai! >= currentTime && 16 | settings.canVote; 17 | 18 | return canVote; 19 | }; 20 | -------------------------------------------------------------------------------- /turbo/generators/templates/package.json.hbs: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sora-vp/{{ name }}", 3 | "private": true, 4 | "version": "0.1.0", 5 | "type": "module", 6 | "exports": { 7 | ".": "./src/index.ts" 8 | }, 9 | "license": "MIT", 10 | "scripts": { 11 | "clean": "rm -rf .turbo node_modules", 12 | "format": "prettier --check . --ignore-path ../../.gitignore", 13 | "lint": "eslint", 14 | "typecheck": "tsc --noEmit" 15 | }, 16 | "devDependencies": { 17 | "@sora-vp/eslint-config": "*", 18 | "@sora-vp/prettier-config": "*", 19 | "@sora-vp/tsconfig": "*", 20 | "eslint": "^9.0.0", 21 | "prettier": "^3.2.5", 22 | "typescript": "^5.4.3" 23 | }, 24 | "prettier": "@sora-vp/prettier-config" 25 | } 26 | -------------------------------------------------------------------------------- /apps/clients/chooser/src/utils/atom.ts: -------------------------------------------------------------------------------- 1 | import { atomWithStorage } from "jotai/utils"; 2 | 3 | /** 4 | * Atom yang digunakan untuk menentukan lama waktu tampil notifikasi berhasil 5 | * setelah partisipan memilih kandidat dan berhasil di proses oleh server. 6 | */ 7 | export const successTimeoutAtom = atomWithStorage("successTimeout", 12_000); 8 | 9 | /** 10 | * Dua atom di bawah ini adalah atom yang akan mengatur apakah perangkat 11 | * ini terdapat modul tombol yang memerlukan koneksi websocket supaya 12 | * modul tombol dapat mengirimkan perintah dari sw2s. 13 | */ 14 | 15 | export const enableWSConnectionAtom = atomWithStorage( 16 | "enableWSConnection", 17 | false, 18 | ); 19 | 20 | export const defaultWSPortAtom = atomWithStorage("defaultWSPort", 3000); 21 | -------------------------------------------------------------------------------- /apps/clients/chooser/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | import { fontFamily } from "tailwindcss/defaultTheme"; 3 | 4 | import baseConfig from "@sora-vp/tailwind-config/web"; 5 | 6 | export default { 7 | // We need to append the path to the UI package to the content array so that 8 | // those classes are included correctly. 9 | content: [...baseConfig.content, "../../../packages/ui/**/*.{ts,tsx}"], 10 | presets: [baseConfig], 11 | theme: { 12 | extend: { 13 | fontFamily: { 14 | sans: ["Geist Variable", ...fontFamily.sans], 15 | mono: ["Geist Mono Variable", ...fontFamily.mono], 16 | sundanese: ["'Noto Sans Sundanese Variable'", "sans-serif"], 17 | }, 18 | }, 19 | }, 20 | } satisfies Config; 21 | -------------------------------------------------------------------------------- /apps/clients/attendance/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | import { fontFamily } from "tailwindcss/defaultTheme"; 3 | 4 | import baseConfig from "@sora-vp/tailwind-config/web"; 5 | 6 | export default { 7 | // We need to append the path to the UI package to the content array so that 8 | // those classes are included correctly. 9 | content: [...baseConfig.content, "../../../packages/ui/**/*.{ts,tsx}"], 10 | presets: [baseConfig], 11 | theme: { 12 | extend: { 13 | fontFamily: { 14 | sans: ["Geist Variable", ...fontFamily.sans], 15 | mono: ["Geist Mono Variable", ...fontFamily.mono], 16 | sundanese: ["'Noto Sans Sundanese Variable'", "sans-serif"], 17 | }, 18 | }, 19 | }, 20 | } satisfies Config; 21 | -------------------------------------------------------------------------------- /apps/web/src/app/(noauth)/page.tsx: -------------------------------------------------------------------------------- 1 | import localFont from "next/font/local"; 2 | 3 | import { cn } from "@sora-vp/ui"; 4 | 5 | const sundaneseFont = localFont({ 6 | src: "../fonts/NotoSansSundanese-Regular.ttf", 7 | }); 8 | 9 | export default function HomePage() { 10 | return ( 11 |
12 |

18 | ᮞᮧᮛ 19 |

20 |

21 | Sebuah aplikasi pemilihan yang membantu proses demokrasi. 22 |

23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /packages/config/schema/admin.settings.schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const PengaturanPerilakuValidationSchema = z.object({ 4 | canVote: z.boolean(), 5 | canAttend: z.boolean(), 6 | }); 7 | 8 | export type PengaturanPerilakuFormValues = z.infer< 9 | typeof PengaturanPerilakuValidationSchema 10 | >; 11 | 12 | export const ServerPengaturanWaktuValidationSchema = z 13 | .object({ 14 | startTime: z.date({ 15 | required_error: "Diperlukan kapan waktu mulai pemilihan!", 16 | }), 17 | endTime: z.date({ 18 | required_error: "Diperlukan kapan waktu selesai pemilihan!", 19 | }), 20 | }) 21 | .refine((data) => data.startTime < data.endTime, { 22 | path: ["endTime"], 23 | message: "Waktu selesai tidak boleh kurang dari waktu mulai!", 24 | }); 25 | -------------------------------------------------------------------------------- /packages/api/src/root.ts: -------------------------------------------------------------------------------- 1 | import { adminRouter } from "./router/admin"; 2 | import { authRouter } from "./router/auth"; 3 | import { candidateRouter } from "./router/candidate"; 4 | import { clientRouter } from "./router/client"; 5 | import { participantRouter } from "./router/participant"; 6 | import { settingsRouter } from "./router/settings"; 7 | import { statisticRouter } from "./router/statistic"; 8 | import { createTRPCRouter } from "./trpc"; 9 | 10 | export const appRouter = createTRPCRouter({ 11 | auth: authRouter, 12 | admin: adminRouter, 13 | candidate: candidateRouter, 14 | statistic: statisticRouter, 15 | settings: settingsRouter, 16 | participant: participantRouter, 17 | clientConsumer: clientRouter, 18 | }); 19 | 20 | // export type definition of API 21 | export type AppRouter = typeof appRouter; 22 | -------------------------------------------------------------------------------- /apps/web/next.config.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from "url"; 2 | import createJiti from "jiti"; 3 | 4 | // Import env files to validate at build time. Use jiti so we can load .ts files in here. 5 | createJiti(fileURLToPath(import.meta.url))("./src/env"); 6 | 7 | /** @type {import("next").NextConfig} */ 8 | const config = { 9 | reactStrictMode: true, 10 | 11 | /** Enables hot reloading for local packages without a build step */ 12 | transpilePackages: [ 13 | "@sora-vp/api", 14 | "@sora-vp/auth", 15 | "@sora-vp/db", 16 | "@sora-vp/ui", 17 | "@sora-vp/validators", 18 | ], 19 | 20 | output: "standalone", 21 | 22 | /** We already do linting and typechecking as separate tasks in CI */ 23 | eslint: { ignoreDuringBuilds: true }, 24 | typescript: { ignoreBuildErrors: true }, 25 | }; 26 | 27 | export default config; 28 | -------------------------------------------------------------------------------- /apps/web/src/app/api/uploads/[filename]/route.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import mime from "mime-types"; 4 | 5 | const ROOT_PATH = path.join(path.resolve(), "public/uploads"); 6 | 7 | export function GET( 8 | request: Request, 9 | { params }: { params: { filename: string } }, 10 | ) { 11 | const filename = params.filename; 12 | 13 | const safeSuffix = path.normalize(filename).replace(/^(\.\.(\/|\\|$))+/, ""); 14 | const filePath = path.join(ROOT_PATH, safeSuffix); 15 | 16 | if (!fs.existsSync(filePath)) 17 | return Response.json({ message: "Image not found!" }, { status: 404 }); 18 | 19 | const image = fs.readFileSync(filePath); 20 | const contentType = mime.lookup(filePath); 21 | 22 | return new Response(image, { 23 | headers: { "Content-Type": contentType as string }, 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # yarn 4 | .yarn/ 5 | 6 | # dependencies 7 | node_modules 8 | .pnp 9 | .pnp.js 10 | 11 | # testing 12 | coverage 13 | 14 | # next.js 15 | .next/ 16 | out/ 17 | next-env.d.ts 18 | 19 | # nitro 20 | .nitro/ 21 | .output/ 22 | 23 | # expo 24 | .expo/ 25 | expo-env.d.ts 26 | apps/expo/.gitignore 27 | apps/expo/ios 28 | apps/expo/android 29 | 30 | # production 31 | build 32 | 33 | # misc 34 | .DS_Store 35 | *.pem 36 | 37 | # debug 38 | npm-debug.log* 39 | yarn-debug.log* 40 | yarn-error.log* 41 | .pnpm-debug.log* 42 | 43 | # local env files 44 | .env 45 | .env*.local 46 | 47 | # vercel 48 | .vercel 49 | 50 | # typescript 51 | *.tsbuildinfo 52 | dist/ 53 | 54 | # turbo 55 | .turbo 56 | 57 | # web app 58 | apps/web/public/uploads 59 | 60 | # docker 61 | ./db 62 | -------------------------------------------------------------------------------- /packages/ui/src/label.tsx: -------------------------------------------------------------------------------- 1 | import type { VariantProps } from "class-variance-authority"; 2 | import * as React from "react"; 3 | import * as LabelPrimitive from "@radix-ui/react-label"; 4 | import { cva } from "class-variance-authority"; 5 | 6 | import { cn } from "@sora-vp/ui"; 7 | 8 | const labelVariants = cva( 9 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", 10 | ); 11 | 12 | const Label = React.forwardRef< 13 | React.ElementRef, 14 | React.ComponentPropsWithoutRef & 15 | VariantProps 16 | >(({ className, ...props }, ref) => ( 17 | 22 | )); 23 | Label.displayName = LabelPrimitive.Root.displayName; 24 | 25 | export { Label }; 26 | -------------------------------------------------------------------------------- /tooling/typescript/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | /** Base Options */ 5 | "esModuleInterop": true, 6 | "skipLibCheck": true, 7 | "target": "ES2022", 8 | "lib": ["ES2022"], 9 | "allowJs": true, 10 | "resolveJsonModule": true, 11 | "moduleDetection": "force", 12 | "isolatedModules": true, 13 | 14 | /** Keep TSC performant in monorepos */ 15 | "incremental": true, 16 | "disableSourceOfProjectReferenceRedirect": true, 17 | 18 | /** Strictness */ 19 | "strict": true, 20 | "noUncheckedIndexedAccess": true, 21 | "checkJs": true, 22 | 23 | /** Transpile using Bundler (not tsc) */ 24 | "module": "Preserve", 25 | "moduleResolution": "Bundler", 26 | "noEmit": true 27 | }, 28 | "exclude": ["node_modules", "build", "dist", ".next", ".expo"] 29 | } 30 | -------------------------------------------------------------------------------- /packages/ui/src/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@sora-vp/ui"; 4 | 5 | type InputProps = React.InputHTMLAttributes; 6 | 7 | const Input = React.forwardRef( 8 | ({ className, type, ...props }, ref) => { 9 | return ( 10 | 19 | ); 20 | }, 21 | ); 22 | Input.displayName = "Input"; 23 | 24 | export { Input }; 25 | -------------------------------------------------------------------------------- /tooling/tailwind/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sora-vp/tailwind-config", 3 | "version": "0.1.0", 4 | "private": true, 5 | "type": "module", 6 | "exports": { 7 | "./native": "./native.ts", 8 | "./web": "./web.ts" 9 | }, 10 | "license": "MIT", 11 | "scripts": { 12 | "clean": "rm -rf .turbo node_modules", 13 | "format": "prettier --check . --ignore-path ../../.gitignore", 14 | "lint": "eslint", 15 | "typecheck": "tsc --noEmit" 16 | }, 17 | "dependencies": { 18 | "postcss": "^8.5.1", 19 | "tailwindcss": "^3.4.17", 20 | "tailwindcss-animate": "^1.0.7" 21 | }, 22 | "devDependencies": { 23 | "@sora-vp/eslint-config": "*", 24 | "@sora-vp/prettier-config": "*", 25 | "@sora-vp/tsconfig": "*", 26 | "eslint": "^9.12.0", 27 | "prettier": "^3.4.2", 28 | "typescript": "^5.6.3" 29 | }, 30 | "prettier": "@sora-vp/prettier-config" 31 | } 32 | -------------------------------------------------------------------------------- /packages/config/eslint/index.js: -------------------------------------------------------------------------------- 1 | /** @type {import("eslint").Linter.Config} */ 2 | const config = { 3 | extends: [ 4 | "next", 5 | "turbo", 6 | "plugin:@typescript-eslint/recommended", 7 | "prettier", 8 | ], 9 | rules: { 10 | "@next/next/no-html-link-for-pages": "off", 11 | "@typescript-eslint/restrict-template-expressions": "off", 12 | "@typescript-eslint/no-unused-vars": [ 13 | "error", 14 | { 15 | argsIgnorePattern: "^_", 16 | varsIgnorePattern: "^_", 17 | caughtErrorsIgnorePattern: "^_", 18 | }, 19 | ], 20 | "@typescript-eslint/consistent-type-imports": [ 21 | "error", 22 | { prefer: "type-imports", fixStyle: "inline-type-imports" }, 23 | ], 24 | }, 25 | ignorePatterns: ["**/*.config.js", "**/*.config.cjs", "packages/config/**"], 26 | reportUnusedDisableDirectives: true, 27 | }; 28 | 29 | module.exports = config; 30 | -------------------------------------------------------------------------------- /apps/clients/chooser/src/components/universal-loading.tsx: -------------------------------------------------------------------------------- 1 | import { Loader } from "lucide-react"; 2 | import { motion } from "motion/react"; 3 | 4 | export function UniversalLoading(props: { 5 | title: string; 6 | description: string; 7 | }) { 8 | return ( 9 | 15 | 20 | 21 |
22 |

23 | {props.title} 24 |

25 |

{props.description}

26 |
27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /packages/ui/src/separator.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as SeparatorPrimitive from "@radix-ui/react-separator"; 5 | 6 | import { cn } from "@sora-vp/ui"; 7 | 8 | const Separator = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >( 12 | ( 13 | { className, orientation = "horizontal", decorative = true, ...props }, 14 | ref, 15 | ) => ( 16 | 27 | ), 28 | ); 29 | Separator.displayName = SeparatorPrimitive.Root.displayName; 30 | 31 | export { Separator }; 32 | -------------------------------------------------------------------------------- /apps/clients/attendance/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 5 | "target": "ES2020", 6 | "useDefineForClassFields": true, 7 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 8 | "module": "ESNext", 9 | "skipLibCheck": true, 10 | 11 | /* Bundler mode */ 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "moduleDetection": "force", 17 | "noEmit": true, 18 | "jsx": "react-jsx", 19 | 20 | /* Linting */ 21 | "strict": true, 22 | "noUnusedLocals": true, 23 | "noUnusedParameters": true, 24 | "noFallthroughCasesInSwitch": true, 25 | 26 | /* Absolute Import */ 27 | "baseUrl": "./", 28 | "paths": { 29 | "@/*": ["src/*"] 30 | } 31 | }, 32 | "include": ["src"] 33 | } 34 | -------------------------------------------------------------------------------- /apps/clients/chooser/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 5 | "target": "ES2020", 6 | "useDefineForClassFields": true, 7 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 8 | "module": "ESNext", 9 | "skipLibCheck": true, 10 | 11 | /* Bundler mode */ 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "moduleDetection": "force", 17 | "noEmit": true, 18 | "jsx": "react-jsx", 19 | 20 | /* Linting */ 21 | "strict": true, 22 | "noUnusedLocals": true, 23 | "noUnusedParameters": true, 24 | "noFallthroughCasesInSwitch": true, 25 | 26 | /* Absolute Import */ 27 | "baseUrl": "./", 28 | "paths": { 29 | "@/*": ["src/*"] 30 | } 31 | }, 32 | "include": ["src"] 33 | } 34 | -------------------------------------------------------------------------------- /packages/db/src/config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "drizzle-kit"; 2 | import { createEnv } from "@t3-oss/env-core"; 3 | import * as z from "zod"; 4 | 5 | const env = createEnv({ 6 | server: { 7 | DB_HOST: z.string(), 8 | DB_NAME: z.string(), 9 | DB_USERNAME: z.string(), 10 | DB_PASSWORD: z.string(), 11 | }, 12 | runtimeEnv: process.env, 13 | emptyStringAsUndefined: true, 14 | }); 15 | 16 | // Push requires SSL so use URL instead of username/password 17 | export const connectionStr = new URL(`mysql://${env.DB_HOST}/${env.DB_NAME}`); 18 | connectionStr.username = env.DB_USERNAME; 19 | connectionStr.password = env.DB_PASSWORD; 20 | // connectionStr.searchParams.set("ssl", '{"rejectUnauthorized":true}'); 21 | 22 | export default { 23 | schema: "./src/schema", 24 | dialect: "mysql", 25 | out: "../db-migrate/migrations", 26 | dbCredentials: { url: connectionStr.href }, 27 | tablesFilter: ["sora_*"], 28 | } satisfies Config; 29 | -------------------------------------------------------------------------------- /apps/clients/chooser/src/routes/main-page.tsx: -------------------------------------------------------------------------------- 1 | import { ScannerComponent } from "@/components/scanner"; 2 | import { useServerSetting } from "@/context/server-setting"; 3 | import { motion } from "motion/react"; 4 | 5 | export default function MainPage() { 6 | const { canVote } = useServerSetting(); 7 | 8 | if (!canVote) 9 | return ( 10 |
11 | 20 | Belum Bisa Memilih! 21 | 22 |
23 | ); 24 | 25 | return ; 26 | } 27 | -------------------------------------------------------------------------------- /apps/clients/attendance/src/routes/main-page.tsx: -------------------------------------------------------------------------------- 1 | import { ScannerComponent } from "@/components/scanner"; 2 | import { useServerSetting } from "@/context/server-setting"; 3 | import { motion } from "motion/react"; 4 | 5 | export default function MainPage() { 6 | const { canAttend } = useServerSetting(); 7 | 8 | if (!canAttend) 9 | return ( 10 |
11 | 20 | Belum Bisa Absen! 21 | 22 |
23 | ); 24 | 25 | return ; 26 | } 27 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll.eslint": "explicit" 4 | }, 5 | "editor.defaultFormatter": "esbenp.prettier-vscode", 6 | "editor.formatOnSave": true, 7 | "eslint.rules.customizations": [{ "rule": "*", "severity": "warn" }], 8 | "eslint.experimental.useFlatConfig": true, 9 | "eslint.workingDirectories": [ 10 | { "pattern": "apps/*/" }, 11 | { "pattern": "packages/*/" }, 12 | { "pattern": "tooling/*/" } 13 | ], 14 | "tailwindCSS.experimental.classRegex": [ 15 | ["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"], 16 | ["cx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"] 17 | ], 18 | "tailwindCSS.experimental.configFile": "./tooling/tailwind/web.ts", 19 | "typescript.enablePromptUseWorkspaceTsdk": true, 20 | "typescript.preferences.autoImportFileExcludePatterns": [ 21 | "next/router.d.ts", 22 | "next/dist/client/router.d.ts" 23 | ], 24 | "typescript.tsdk": "node_modules/typescript/lib" 25 | } 26 | -------------------------------------------------------------------------------- /packages/settings/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sora-vp/settings", 3 | "private": true, 4 | "version": "0.1.0", 5 | "type": "module", 6 | "exports": { 7 | ".": { 8 | "types": "./dist/index.d.ts", 9 | "default": "./src/index.ts" 10 | }, 11 | "./manager": { 12 | "types": "./dist/SettingsManager.d.ts", 13 | "default": "./src/SettingsManager.ts" 14 | } 15 | }, 16 | "license": "MIT", 17 | "scripts": { 18 | "build": "tsc", 19 | "dev": "tsc --watch", 20 | "clean": "rm -rf .turbo node_modules", 21 | "format": "prettier --check . --ignore-path ../../.gitignore", 22 | "lint": "eslint", 23 | "typecheck": "tsc --noEmit --emitDeclarationOnly false" 24 | }, 25 | "devDependencies": { 26 | "@sora-vp/eslint-config": "*", 27 | "@sora-vp/prettier-config": "*", 28 | "@sora-vp/tsconfig": "*", 29 | "eslint": "^9.12.0", 30 | "prettier": "^3.4.2", 31 | "typescript": "^5.6.3" 32 | }, 33 | "prettier": "@sora-vp/prettier-config" 34 | } 35 | -------------------------------------------------------------------------------- /packages/validators/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sora-vp/validators", 3 | "private": true, 4 | "version": "0.1.0", 5 | "type": "module", 6 | "exports": { 7 | ".": { 8 | "types": "./dist/index.d.ts", 9 | "default": "./src/index.ts" 10 | } 11 | }, 12 | "license": "MIT", 13 | "scripts": { 14 | "build": "tsc", 15 | "dev": "tsc --watch", 16 | "clean": "rm -rf .turbo node_modules", 17 | "format": "prettier --check . --ignore-path ../../.gitignore", 18 | "lint": "eslint", 19 | "typecheck": "tsc --noEmit --emitDeclarationOnly false" 20 | }, 21 | "dependencies": { 22 | "@sora-vp/id-generator": "*", 23 | "js-base64": "^3.7.7", 24 | "zod": "^3.24.1" 25 | }, 26 | "devDependencies": { 27 | "@sora-vp/eslint-config": "*", 28 | "@sora-vp/prettier-config": "*", 29 | "@sora-vp/tsconfig": "*", 30 | "eslint": "^9.12.0", 31 | "prettier": "^3.4.2", 32 | "typescript": "^5.6.3" 33 | }, 34 | "prettier": "@sora-vp/prettier-config" 35 | } 36 | -------------------------------------------------------------------------------- /packages/ui/src/client-not-found.tsx: -------------------------------------------------------------------------------- 1 | import { House } from "lucide-react"; 2 | import { NavLink } from "react-router-dom"; 3 | 4 | import { Button } from "./button"; 5 | import { Separator } from "./separator"; 6 | 7 | export function ClientNotFound() { 8 | return ( 9 |
10 |
11 |

12 | 404 13 |

14 | 15 |

16 | Halaman tidak ditemukan. 17 |

18 |
19 | 20 | 21 | {() => ( 22 | 26 | )} 27 | 28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /packages/id-generator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sora-vp/id-generator", 3 | "version": "0.1.0", 4 | "private": true, 5 | "type": "module", 6 | "exports": { 7 | ".": { 8 | "types": "./dist/index.d.ts", 9 | "default": "./src/index.ts" 10 | } 11 | }, 12 | "author": "Ezra Khairan Permana", 13 | "license": "GPL-3.0", 14 | "scripts": { 15 | "build": "tsc", 16 | "dev": "tsc --watch", 17 | "clean": "rm -rf .turbo node_modules", 18 | "format": "prettier --check . --ignore-path ../../.gitignore", 19 | "lint": "eslint", 20 | "typecheck": "tsc --noEmit --emitDeclarationOnly false" 21 | }, 22 | "dependencies": { 23 | "nanoid": "^5.1.0" 24 | }, 25 | "devDependencies": { 26 | "@sora-vp/eslint-config": "*", 27 | "@sora-vp/prettier-config": "*", 28 | "@sora-vp/tsconfig": "*", 29 | "@types/eslint": "^8.56.12", 30 | "eslint": "^9.12.0", 31 | "prettier": "^3.4.2", 32 | "typescript": "^5.6.3" 33 | }, 34 | "prettier": "@sora-vp/prettier-config" 35 | } 36 | -------------------------------------------------------------------------------- /packages/ui/src/toast.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useTheme } from "next-themes"; 4 | import { Toaster as Sonner, toast } from "sonner"; 5 | 6 | type ToasterProps = React.ComponentProps; 7 | 8 | const Toaster = ({ ...props }: ToasterProps) => { 9 | const { theme = "system" } = useTheme(); 10 | 11 | return ( 12 | 28 | ); 29 | }; 30 | 31 | export { Toaster, toast }; 32 | -------------------------------------------------------------------------------- /packages/api/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server"; 2 | 3 | import type { AppRouter } from "./root"; 4 | import { appRouter } from "./root"; 5 | import { createCallerFactory, createTRPCContext } from "./trpc"; 6 | 7 | /** 8 | * Create a server-side caller for the tRPC API 9 | * @example 10 | * const trpc = createCaller(createContext); 11 | * const res = await trpc.post.all(); 12 | * ^? Post[] 13 | */ 14 | const createCaller = createCallerFactory(appRouter); 15 | 16 | /** 17 | * Inference helpers for input types 18 | * @example 19 | * type PostByIdInput = RouterInputs['post']['byId'] 20 | * ^? { id: number } 21 | **/ 22 | type RouterInputs = inferRouterInputs; 23 | 24 | /** 25 | * Inference helpers for output types 26 | * @example 27 | * type AllPostsOutput = RouterOutputs['post']['all'] 28 | * ^? Post[] 29 | **/ 30 | type RouterOutputs = inferRouterOutputs; 31 | 32 | export { createTRPCContext, appRouter, createCaller }; 33 | export type { AppRouter, RouterInputs, RouterOutputs }; 34 | -------------------------------------------------------------------------------- /tooling/eslint/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sora-vp/eslint-config", 3 | "private": true, 4 | "version": "0.3.0", 5 | "type": "module", 6 | "exports": { 7 | "./base": "./base.js", 8 | "./nextjs": "./nextjs.js", 9 | "./react": "./react.js" 10 | }, 11 | "scripts": { 12 | "clean": "rm -rf .turbo node_modules", 13 | "format": "prettier --check . --ignore-path ../../.gitignore", 14 | "typecheck": "tsc --noEmit" 15 | }, 16 | "dependencies": { 17 | "@eslint/compat": "^1.2.3", 18 | "@next/eslint-plugin-next": "^14.2.15", 19 | "eslint-plugin-import": "^2.31.0", 20 | "eslint-plugin-jsx-a11y": "^6.10.2", 21 | "eslint-plugin-react": "^7.37.2", 22 | "eslint-plugin-react-hooks": "^5.0.0", 23 | "eslint-plugin-turbo": "^2.3.3", 24 | "typescript-eslint": "^8.32.0" 25 | }, 26 | "devDependencies": { 27 | "@sora-vp/prettier-config": "*", 28 | "@sora-vp/tsconfig": "*", 29 | "eslint": "^9.12.0", 30 | "prettier": "^3.4.2", 31 | "typescript": "^5.6.3" 32 | }, 33 | "prettier": "@sora-vp/prettier-config" 34 | } 35 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Since .env is gitignored, you can use .env.example to build a new `.env` file when you clone the repo. 2 | # Keep this file up-to-date when you add new variables to \`.env\`. 3 | 4 | # This file will be committed to version control, so make sure not to have any secrets in it. 5 | # If you are cloning this repo, create a copy of this file named `.env` and populate it with your secrets. 6 | 7 | # The database URL is used to connect to your PlanetScale database. 8 | DB_HOST='localhost' 9 | DB_NAME='sora' 10 | DB_USERNAME='' 11 | DB_PASSWORD='' 12 | 13 | # You can generate the secret via 'openssl rand -base64 32' on Unix 14 | # or using node js itself to generate the random secret 15 | # node -e 'console.log(require("crypto").randomBytes(50).toString("base64"));' 16 | # @see https://next-auth.js.org/configuration/options#secret 17 | AUTH_SECRET='supersecret' 18 | 19 | # RabbitMQ 20 | # This env variable will connect to rabbitmq instance 21 | AMQP_URL="amqp://localhost" 22 | 23 | # API Endpoint 24 | # This env variable will tell the vote processor where is the trpc endpoint 25 | PROCESSOR_API_URL="http://localhost:3000/api/trpc" 26 | -------------------------------------------------------------------------------- /packages/auth/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sora-vp/auth", 3 | "version": "0.1.0", 4 | "private": true, 5 | "type": "module", 6 | "exports": { 7 | ".": { 8 | "react-server": "./src/index.rsc.ts", 9 | "default": "./src/index.ts" 10 | }, 11 | "./env": "./env.ts" 12 | }, 13 | "license": "MIT", 14 | "scripts": { 15 | "clean": "rm -rf .turbo node_modules", 16 | "format": "prettier --check . --ignore-path ../../.gitignore", 17 | "lint": "eslint", 18 | "typecheck": "tsc --noEmit" 19 | }, 20 | "dependencies": { 21 | "@sora-vp/db": "*", 22 | "@t3-oss/env-nextjs": "^0.11.1", 23 | "bcrypt": "^5.1.1", 24 | "next": "15.3.2", 25 | "next-auth": "5.0.0-beta.25", 26 | "react": "19.1.0", 27 | "react-dom": "19.1.0", 28 | "zod": "^3.24.1" 29 | }, 30 | "devDependencies": { 31 | "@sora-vp/eslint-config": "*", 32 | "@sora-vp/prettier-config": "*", 33 | "@sora-vp/tsconfig": "*", 34 | "@types/bcrypt": "^5.0.2", 35 | "eslint": "^9.12.0", 36 | "prettier": "^3.4.2", 37 | "typescript": "^5.6.3" 38 | }, 39 | "prettier": "@sora-vp/prettier-config" 40 | } 41 | -------------------------------------------------------------------------------- /packages/db-migrate/src/index.ts: -------------------------------------------------------------------------------- 1 | import { drizzle } from "drizzle-orm/mysql2"; 2 | import { migrate } from "drizzle-orm/mysql2/migrator"; 3 | import mysql from "mysql2/promise"; 4 | 5 | const sourceDir = new URL("../migrations", import.meta.url); 6 | console.log("launched..."); 7 | 8 | (async () => { 9 | const connectionStr = new URL( 10 | `mysql://${process.env.DB_HOST}/${process.env.DB_NAME}`, 11 | ); 12 | 13 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 14 | connectionStr.username = process.env.DB_USERNAME!; 15 | 16 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 17 | connectionStr.password = process.env.DB_PASSWORD!; 18 | 19 | const sql = await mysql.createConnection(connectionStr.href); 20 | const db = drizzle(sql); 21 | 22 | console.log("migrating database..."); 23 | await migrate(db, { migrationsFolder: sourceDir.pathname }); 24 | console.log("migrations successful."); 25 | process.exit(0); 26 | })().catch((e) => { 27 | // Deal with the fact the chain failed 28 | console.error("migration failed"); 29 | console.error(e); 30 | process.exit(1); 31 | }); 32 | -------------------------------------------------------------------------------- /packages/validators/src/settings.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | const canLogin = z.boolean(); 4 | 5 | const SharedCanLogin = z.object({ 6 | canLogin, 7 | }); 8 | 9 | const SharedBehaviour = z.object({ 10 | canVote: z.boolean(), 11 | canAttend: z.boolean(), 12 | }); 13 | 14 | const startTimeError = "Diperlukan kapan waktu mulai pemilihan!"; 15 | const endTimeError = "Diperlukan kapan waktu selesai pemilihan!"; 16 | 17 | const SharedDuration = z 18 | .object({ 19 | startTime: z.date({ 20 | errorMap: (issue, { defaultError }) => ({ 21 | message: issue.code === "invalid_type" ? startTimeError : defaultError, 22 | }), 23 | }), 24 | endTime: z.date({ 25 | errorMap: (issue, { defaultError }) => ({ 26 | message: issue.code === "invalid_type" ? endTimeError : defaultError, 27 | }), 28 | }), 29 | }) 30 | .refine((data) => data.startTime < data.endTime, { 31 | path: ["endTime"], 32 | message: "Waktu selesai tidak boleh kurang dari waktu mulai!", 33 | }); 34 | 35 | export const settings = { 36 | SharedCanLogin, 37 | SharedBehaviour, 38 | SharedDuration, 39 | } as const; 40 | -------------------------------------------------------------------------------- /tooling/tailwind/web.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | import animate from "tailwindcss-animate"; 3 | 4 | import base from "./base"; 5 | 6 | export default { 7 | content: base.content, 8 | presets: [base], 9 | theme: { 10 | container: { 11 | center: true, 12 | padding: "2rem", 13 | screens: { 14 | "2xl": "1400px", 15 | }, 16 | }, 17 | extend: { 18 | borderRadius: { 19 | lg: "var(--radius)", 20 | md: "calc(var(--radius) - 2px)", 21 | sm: "calc(var(--radius) - 4px)", 22 | }, 23 | keyframes: { 24 | "accordion-down": { 25 | from: { height: "0" }, 26 | to: { height: "var(--radix-accordion-content-height)" }, 27 | }, 28 | "accordion-up": { 29 | from: { height: "var(--radix-accordion-content-height)" }, 30 | to: { height: "0" }, 31 | }, 32 | }, 33 | animation: { 34 | "accordion-down": "accordion-down 0.2s ease-out", 35 | "accordion-up": "accordion-up 0.2s ease-out", 36 | }, 37 | }, 38 | }, 39 | plugins: [animate], 40 | } satisfies Config; 41 | -------------------------------------------------------------------------------- /apps/web/src/app/(noauth)/(logreg)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | 3 | import { auth } from "@sora-vp/auth"; 4 | import settings from "@sora-vp/settings"; 5 | import { Toaster } from "@sora-vp/ui/toast"; 6 | 7 | import { TRPCReactProvider } from "~/trpc/react"; 8 | 9 | export default async function LogRegLayout(props: { 10 | children: React.ReactNode; 11 | }) { 12 | const alreadyLoggedIn = await auth(); 13 | 14 | if (alreadyLoggedIn) redirect("/admin"); 15 | 16 | const { canLogin } = settings.getSettings(); 17 | 18 | if (!canLogin) 19 | return ( 20 |
21 |

22 | Akses masuk ditolak. 23 |

24 |
25 | ); 26 | 27 | return ( 28 | 29 |
30 | {props.children} 31 |
32 | 33 | 34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /packages/db-migrate/migrations/0000_confused_forgotten_one.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `sora_candidate` ( 2 | `id` int AUTO_INCREMENT NOT NULL, 3 | `name` text NOT NULL, 4 | `counter` int NOT NULL DEFAULT 0, 5 | `image` varchar(100) NOT NULL, 6 | CONSTRAINT `sora_candidate_id` PRIMARY KEY(`id`) 7 | ); 8 | --> statement-breakpoint 9 | CREATE TABLE `sora_participant` ( 10 | `id` int AUTO_INCREMENT NOT NULL, 11 | `name` text NOT NULL, 12 | `sub_part` varchar(50) NOT NULL, 13 | `qr_id` varchar(30), 14 | `already_attended` boolean NOT NULL DEFAULT false, 15 | `attended_at` timestamp, 16 | `already_choosing` boolean NOT NULL DEFAULT false, 17 | `choosing_at` timestamp, 18 | CONSTRAINT `sora_participant_id` PRIMARY KEY(`id`), 19 | CONSTRAINT `qr_id_unique_index` UNIQUE(`qr_id`) 20 | ); 21 | --> statement-breakpoint 22 | CREATE TABLE `sora_user` ( 23 | `id` int AUTO_INCREMENT NOT NULL, 24 | `name` text NOT NULL, 25 | `email` varchar(255) NOT NULL, 26 | `password` varchar(255) NOT NULL, 27 | `verified_at` timestamp, 28 | `role` enum('admin','comittee'), 29 | CONSTRAINT `sora_user_id` PRIMARY KEY(`id`), 30 | CONSTRAINT `email_unique_index` UNIQUE(`email`) 31 | ); 32 | -------------------------------------------------------------------------------- /tooling/prettier/index.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from "url"; 2 | 3 | /** @typedef {import("prettier").Config} PrettierConfig */ 4 | /** @typedef {import("prettier-plugin-tailwindcss").PluginOptions} TailwindConfig */ 5 | /** @typedef {import("@ianvs/prettier-plugin-sort-imports").PluginConfig} SortImportsConfig */ 6 | 7 | /** @type { PrettierConfig | SortImportsConfig | TailwindConfig } */ 8 | const config = { 9 | plugins: [ 10 | "@ianvs/prettier-plugin-sort-imports", 11 | "prettier-plugin-tailwindcss", 12 | ], 13 | tailwindConfig: fileURLToPath( 14 | new URL("../../tooling/tailwind/web.ts", import.meta.url), 15 | ), 16 | tailwindFunctions: ["cn", "cva"], 17 | importOrder: [ 18 | "", 19 | "^(react/(.*)$)|^(react$)|^(react-native(.*)$)", 20 | "^(next/(.*)$)|^(next$)", 21 | "^(expo(.*)$)|^(expo$)", 22 | "", 23 | "", 24 | "^@acme", 25 | "^@sora-vp/(.*)$", 26 | "", 27 | "^[.|..|~]", 28 | "^~/", 29 | "^[../]", 30 | "^[./]", 31 | ], 32 | importOrderParserPlugins: ["typescript", "jsx", "decorators-legacy"], 33 | importOrderTypeScriptVersion: "4.4.0", 34 | }; 35 | 36 | export default config; 37 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | branches: ["*"] 6 | push: 7 | branches: ["main"] 8 | merge_group: 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} 13 | 14 | jobs: 15 | lint: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - name: Monorepo install 21 | uses: ./.github/actions/yarn-nm-install 22 | 23 | - name: Copy env 24 | shell: bash 25 | run: cp .env.example .env 26 | 27 | - name: Lint 28 | run: yarn pre-build && yarn lint && yarn lint:ws 29 | 30 | format: 31 | runs-on: ubuntu-latest 32 | steps: 33 | - uses: actions/checkout@v4 34 | 35 | - name: Monorepo install 36 | uses: ./.github/actions/yarn-nm-install 37 | 38 | - name: Format 39 | run: yarn format 40 | 41 | typecheck: 42 | runs-on: ubuntu-latest 43 | steps: 44 | - uses: actions/checkout@v4 45 | 46 | - name: Monorepo install 47 | uses: ./.github/actions/yarn-nm-install 48 | 49 | - name: Typecheck 50 | run: yarn pre-build && yarn typecheck 51 | -------------------------------------------------------------------------------- /apps/web/src/app/(auth)/admin/(adminRole)/page.tsx: -------------------------------------------------------------------------------- 1 | import { AllRegisteredUser } from "~/app/_components/admin/all-registered-user/index"; 2 | import { PendingUser } from "~/app/_components/admin/pending-user/index"; 3 | import { ToggleCanLogin } from "~/app/_components/admin/toggle-can-login"; 4 | 5 | export default function AdminPage() { 6 | return ( 7 |
8 |
9 |

Beranda Admin

10 |

11 | Kelola semua pengguna dan perilaku pengguna pada halaman ini. 12 |

13 |
14 | 15 | 16 | 17 |
18 |

19 | Seluruh Pengguna 20 |

21 | 22 | 23 |
24 | 25 |
26 |

27 | Menunggu Persetujuan 28 |

29 | 30 | 31 |
32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /packages/settings/src/index.ts: -------------------------------------------------------------------------------- 1 | import { settings } from "./SettingsManager"; 2 | 3 | export { settings as default } from "./SettingsManager"; 4 | 5 | const getTimePermission = () => { 6 | const currentSettings = settings.getSettings(); 7 | const currentTime = new Date().getTime(); 8 | 9 | const currentTimeIsBiggerThanStart = 10 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 11 | currentSettings.startTime && currentSettings.startTime 12 | ? currentTime >= currentSettings.startTime.getTime() 13 | : false; 14 | 15 | const currentTimeIsSmallerThanEnd = 16 | currentSettings.startTime && currentSettings.endTime 17 | ? currentTime <= currentSettings.endTime.getTime() 18 | : false; 19 | 20 | return { 21 | isPermittedByTime: 22 | currentTimeIsBiggerThanStart && currentTimeIsSmallerThanEnd, 23 | settings: currentSettings, 24 | }; 25 | }; 26 | 27 | export const canVoteNow = () => { 28 | const { isPermittedByTime, settings } = getTimePermission(); 29 | 30 | return isPermittedByTime && settings.canVote; 31 | }; 32 | 33 | export const canAttendNow = () => { 34 | const { isPermittedByTime, settings } = getTimePermission(); 35 | 36 | return isPermittedByTime && settings.canAttend; 37 | }; 38 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turborepo.org/schema.json", 3 | "globalDependencies": ["**/.env"], 4 | "globalEnv": [ 5 | "DB_HOST", 6 | "DB_NAME", 7 | "DB_USERNAME", 8 | "DB_PASSWORD", 9 | "AUTH_SECRET", 10 | "AMQP_URL", 11 | "PROCESSOR_API_URL", 12 | "CI", 13 | "SKIP_ENV_VALIDATION" 14 | ], 15 | "tasks": { 16 | "topo": { 17 | "dependsOn": ["^topo"] 18 | }, 19 | "build": { 20 | "dependsOn": ["^build"], 21 | "outputs": [ 22 | ".next/**", 23 | "!.next/cache/**", 24 | "next-env.d.ts", 25 | ".expo/**", 26 | ".output/**", 27 | ".vercel/output/**" 28 | ] 29 | }, 30 | "dev": { 31 | "persistent": true, 32 | "cache": false 33 | }, 34 | "format": { 35 | "outputs": ["node_modules/.cache/.prettiercache"], 36 | "outputLogs": "new-only" 37 | }, 38 | "lint": { 39 | "dependsOn": ["^topo"], 40 | "outputs": ["node_modules/.cache/.eslintcache"] 41 | }, 42 | "typecheck": { 43 | "dependsOn": ["^topo"], 44 | "outputs": ["node_modules/.cache/tsbuildinfo.json"] 45 | }, 46 | "clean": { 47 | "cache": false 48 | }, 49 | "//#clean": { 50 | "cache": false 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/ui/src/tooltip.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"; 5 | 6 | import { cn } from "@sora-vp/ui"; 7 | 8 | const TooltipProvider = TooltipPrimitive.Provider; 9 | 10 | const Tooltip = TooltipPrimitive.Root; 11 | 12 | const TooltipTrigger = TooltipPrimitive.Trigger; 13 | 14 | const TooltipContent = React.forwardRef< 15 | React.ElementRef, 16 | React.ComponentPropsWithoutRef 17 | >(({ className, sideOffset = 4, ...props }, ref) => ( 18 | 27 | )); 28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName; 29 | 30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; 31 | -------------------------------------------------------------------------------- /apps/web/src/app/(noauth)/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata, Viewport } from "next"; 2 | import { GeistMono } from "geist/font/mono"; 3 | import { GeistSans } from "geist/font/sans"; 4 | 5 | import { cn } from "@sora-vp/ui"; 6 | import { ThemeProvider, ThemeToggle } from "@sora-vp/ui/theme"; 7 | 8 | import "~/app/globals.css"; 9 | 10 | export const metadata: Metadata = { 11 | title: "sora", 12 | }; 13 | 14 | export const viewport: Viewport = { 15 | themeColor: [ 16 | { media: "(prefers-color-scheme: light)", color: "white" }, 17 | { media: "(prefers-color-scheme: dark)", color: "black" }, 18 | ], 19 | }; 20 | 21 | export default function RootLayout(props: { children: React.ReactNode }) { 22 | return ( 23 | 24 | 31 | 32 | {props.children} 33 |
34 | 35 |
36 |
37 | 38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /packages/ui/src/switch.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as SwitchPrimitives from "@radix-ui/react-switch"; 5 | 6 | import { cn } from "@sora-vp/ui"; 7 | 8 | const Switch = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | 25 | 26 | )); 27 | Switch.displayName = SwitchPrimitives.Root.displayName; 28 | 29 | export { Switch }; 30 | -------------------------------------------------------------------------------- /packages/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sora-vp/api", 3 | "version": "0.1.0", 4 | "private": true, 5 | "type": "module", 6 | "exports": { 7 | ".": { 8 | "types": "./dist/index.d.ts", 9 | "default": "./src/index.ts" 10 | } 11 | }, 12 | "license": "MIT", 13 | "scripts": { 14 | "type-build": "tsc", 15 | "dev": "tsc --watch", 16 | "clean": "rm -rf .turbo node_modules", 17 | "format": "prettier --check . --ignore-path ../../.gitignore", 18 | "lint": "eslint", 19 | "typecheck": "tsc --noEmit --emitDeclarationOnly false" 20 | }, 21 | "dependencies": { 22 | "@sora-vp/auth": "*", 23 | "@sora-vp/db": "*", 24 | "@sora-vp/settings": "*", 25 | "@sora-vp/validators": "*", 26 | "@trpc/server": "^11.1.0", 27 | "amqplib": "^0.10.5", 28 | "bcrypt": "^5.1.1", 29 | "mime-types": "^2.1.35", 30 | "superjson": "2.2.2", 31 | "zod": "^3.24.1" 32 | }, 33 | "devDependencies": { 34 | "@sora-vp/eslint-config": "*", 35 | "@sora-vp/prettier-config": "*", 36 | "@sora-vp/tsconfig": "*", 37 | "@types/amqplib": "^0.10.6", 38 | "@types/bcrypt": "^5.0.2", 39 | "@types/mime-types": "^2.1.4", 40 | "eslint": "^9.12.0", 41 | "prettier": "^3.4.2", 42 | "typescript": "^5.6.3" 43 | }, 44 | "prettier": "@sora-vp/prettier-config" 45 | } 46 | -------------------------------------------------------------------------------- /apps/web/src/app/(auth)/admin/(adminRole)/pengaturan/page.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Card, 3 | CardContent, 4 | CardDescription, 5 | CardHeader, 6 | CardTitle, 7 | } from "@sora-vp/ui/card"; 8 | 9 | import { Behaviour } from "~/app/_components/settings/behaviour"; 10 | import { Duration } from "~/app/_components/settings/duration"; 11 | 12 | export default function SettingsPage() { 13 | return ( 14 |
15 |
16 | 17 | 18 | Perilaku Pemilihan 19 | 20 | Atur perilaku pemilihan dengan mengubah switch di bawah. 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | Waktu Pemilihan 31 | 32 | Tetapkan kapan lama durasi pemilihan ini berlangsung. 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 |
41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /apps/web/src/app/api/trpc/[trpc]/route.ts: -------------------------------------------------------------------------------- 1 | import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; 2 | 3 | import { appRouter, createTRPCContext } from "@sora-vp/api"; 4 | import { auth } from "@sora-vp/auth"; 5 | 6 | /** 7 | * Configure basic CORS headers 8 | * You should extend this to match your needs 9 | */ 10 | const setCorsHeaders = (res: Response) => { 11 | res.headers.set("Access-Control-Allow-Origin", "*"); 12 | res.headers.set("Access-Control-Request-Method", "*"); 13 | res.headers.set("Access-Control-Allow-Methods", "OPTIONS, GET, POST"); 14 | res.headers.set("Access-Control-Allow-Headers", "*"); 15 | }; 16 | 17 | export const OPTIONS = () => { 18 | const response = new Response(null, { 19 | status: 204, 20 | }); 21 | setCorsHeaders(response); 22 | return response; 23 | }; 24 | 25 | const handler = auth(async (req) => { 26 | const response = await fetchRequestHandler({ 27 | endpoint: "/api/trpc", 28 | router: appRouter, 29 | req, 30 | createContext: () => 31 | createTRPCContext({ 32 | session: req.auth, 33 | headers: req.headers, 34 | }), 35 | onError({ error, path }) { 36 | console.error(`>>> tRPC Error on '${path}'`, error); 37 | }, 38 | }); 39 | 40 | setCorsHeaders(response); 41 | return response; 42 | }); 43 | 44 | export { handler as GET, handler as POST }; 45 | -------------------------------------------------------------------------------- /packages/db-migrate/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sora-vp/db-migrate", 3 | "version": "0.1.0", 4 | "private": true, 5 | "type": "module", 6 | "files": [ 7 | "dist", 8 | "migrations/**" 9 | ], 10 | "bin": "./dist/index.js", 11 | "scripts": { 12 | "build": "tsup src/index.ts --format esm", 13 | "migrate": "yarn with-env tsx migrate/index.mts", 14 | "studio": "yarn with-env drizzle-kit studio --config src/config.ts", 15 | "dev": "tsc --watch", 16 | "clean": "rm -rf .turbo node_modules", 17 | "format": "prettier --check . --ignore-path ../../.gitignore", 18 | "lint": "eslint", 19 | "push": "yarn with-env drizzle-kit push:mysql --config src/config.ts", 20 | "typecheck": "tsc --noEmit --emitDeclarationOnly false", 21 | "with-env": "dotenv -e ../../.env --" 22 | }, 23 | "dependencies": { 24 | "@t3-oss/env-core": "^0.11.1", 25 | "drizzle-orm": "^0.38.4", 26 | "mysql2": "^3.12.0", 27 | "zod": "^3.24.1" 28 | }, 29 | "devDependencies": { 30 | "@sora-vp/eslint-config": "*", 31 | "@sora-vp/prettier-config": "*", 32 | "@sora-vp/tsconfig": "*", 33 | "dotenv-cli": "^7.4.4", 34 | "drizzle-kit": "^0.30.2", 35 | "eslint": "^9.12.0", 36 | "prettier": "^3.4.2", 37 | "tsup": "^8.3.5", 38 | "typescript": "^5.6.3" 39 | }, 40 | "prettier": "@sora-vp/prettier-config" 41 | } 42 | -------------------------------------------------------------------------------- /apps/clients/attendance/src/env.ts: -------------------------------------------------------------------------------- 1 | import { createEnv } from "@t3-oss/env-core"; 2 | import { z } from "zod"; 3 | 4 | export const env = createEnv({ 5 | /** 6 | * The prefix that client-side variables must have. This is enforced both at 7 | * a type-level and at runtime. 8 | */ 9 | clientPrefix: "VITE_", 10 | 11 | client: { 12 | VITE_IS_DOCKER: z.optional(z.coerce.boolean()), 13 | VITE_TRPC_URL: z.optional(z.string().url()), 14 | }, 15 | 16 | /** 17 | * What object holds the environment variables at runtime. This is usually 18 | * `process.env` or `import.meta.env`. 19 | */ 20 | runtimeEnv: import.meta.env, 21 | 22 | /** 23 | * By default, this library will feed the environment variables directly to 24 | * the Zod validator. 25 | * 26 | * This means that if you have an empty string for a value that is supposed 27 | * to be a number (e.g. `PORT=` in a ".env" file), Zod will incorrectly flag 28 | * it as a type mismatch violation. Additionally, if you have an empty string 29 | * for a value that is supposed to be a string with a default value (e.g. 30 | * `DOMAIN=` in an ".env" file), the default value will never be applied. 31 | * 32 | * In order to solve these issues, we recommend that all new projects 33 | * explicitly specify this option as true. 34 | */ 35 | emptyStringAsUndefined: true, 36 | }); 37 | -------------------------------------------------------------------------------- /packages/validators/src/auth.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | const validNameRegex = 4 | /^(?![ -.&,_'":?!])(?!.*[- &_'":]$)(?!.*[-.#@&,:?!]{2})[a-zA-Z- .,']+$/; 5 | 6 | const email = z 7 | .string() 8 | .min(1, { message: "Bidang email harus di isi!" }) 9 | .email({ message: "Bidang email harus berupa email yang valid!" }); 10 | const password = z 11 | .string() 12 | .min(1, { message: "Kata sandi harus di isi!" }) 13 | .min(6, { message: "Kata sandi memiliki panjang setidaknya 6 karakter!" }); 14 | const name = z 15 | .string() 16 | .min(3, { message: "Bidang nama harus di isi!" }) 17 | .regex(validNameRegex, { 18 | message: "Bidang nama harus berupa nama yang valid!", 19 | }); 20 | 21 | const LoginFormSchema = z.object({ 22 | email, 23 | password, 24 | }); 25 | 26 | const ServerRegisterSchema = z.object({ 27 | email, 28 | password, 29 | name, 30 | }); 31 | 32 | const RegisterFormSchema = ServerRegisterSchema.merge( 33 | z.object({ 34 | passConfirm: z.string().min(6, { 35 | message: "Konfirmasi kata sandi diperlukan setidaknya 6 karakter!", 36 | }), 37 | }), 38 | ).refine((data) => data.password === data.passConfirm, { 39 | message: "Konfirmasi kata sandi tidak sama!", 40 | path: ["passConfirm"], 41 | }); 42 | 43 | export const auth = { 44 | LoginFormSchema, 45 | RegisterFormSchema, 46 | ServerRegisterSchema, 47 | } as const; 48 | -------------------------------------------------------------------------------- /packages/api/src/router/settings.ts: -------------------------------------------------------------------------------- 1 | import type { TRPCRouterRecord } from "@trpc/server"; 2 | 3 | import settings from "@sora-vp/settings"; 4 | import { settings as settingsSchema } from "@sora-vp/validators"; 5 | 6 | import { adminProcedure } from "../trpc"; 7 | 8 | export const settingsRouter = { 9 | getSettings: adminProcedure.query(() => { 10 | const currentSettings = settings.getSettings(); 11 | 12 | return currentSettings; 13 | }), 14 | 15 | getCanLoginStatus: adminProcedure.query(() => { 16 | const { canLogin } = settings.getSettings(); 17 | 18 | return { canLogin }; 19 | }), 20 | 21 | updateCanLogin: adminProcedure 22 | .input(settingsSchema.SharedCanLogin) 23 | .mutation(({ input }) => settings.updateSettings.canLogin(input.canLogin)), 24 | 25 | changeVotingBehaviour: adminProcedure 26 | .input(settingsSchema.SharedBehaviour) 27 | .mutation(({ input }) => { 28 | settings.updateSettings.canVote(input.canVote); 29 | settings.updateSettings.canAttend(input.canAttend); 30 | 31 | return { success: true }; 32 | }), 33 | 34 | changeVotingTime: adminProcedure 35 | .input(settingsSchema.SharedDuration) 36 | .mutation(({ input }) => { 37 | settings.updateSettings.startTime(input.startTime); 38 | settings.updateSettings.endTime(input.endTime); 39 | 40 | return { success: true }; 41 | }), 42 | } satisfies TRPCRouterRecord; 43 | -------------------------------------------------------------------------------- /apps/clients/chooser/src/env.ts: -------------------------------------------------------------------------------- 1 | import { createEnv } from "@t3-oss/env-core"; 2 | import { z } from "zod"; 3 | 4 | export const env = createEnv({ 5 | /** 6 | * The prefix that client-side variables must have. This is enforced both at 7 | * a type-level and at runtime. 8 | */ 9 | clientPrefix: "VITE_", 10 | 11 | client: { 12 | VITE_IS_DOCKER: z.optional(z.coerce.boolean()), 13 | VITE_TRPC_URL: z.optional(z.string().url()), 14 | VITE_IMAGE_RETRIEVER: z.optional(z.string().url()), 15 | }, 16 | 17 | /** 18 | * What object holds the environment variables at runtime. This is usually 19 | * `process.env` or `import.meta.env`. 20 | */ 21 | runtimeEnv: import.meta.env, 22 | 23 | /** 24 | * By default, this library will feed the environment variables directly to 25 | * the Zod validator. 26 | * 27 | * This means that if you have an empty string for a value that is supposed 28 | * to be a number (e.g. `PORT=` in a ".env" file), Zod will incorrectly flag 29 | * it as a type mismatch violation. Additionally, if you have an empty string 30 | * for a value that is supposed to be a string with a default value (e.g. 31 | * `DOMAIN=` in an ".env" file), the default value will never be applied. 32 | * 33 | * In order to solve these issues, we recommend that all new projects 34 | * explicitly specify this option as true. 35 | */ 36 | emptyStringAsUndefined: true, 37 | }); 38 | -------------------------------------------------------------------------------- /apps/clients/chooser/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: 13 | 14 | - Configure the top-level `parserOptions` property like this: 15 | 16 | ```js 17 | export default { 18 | // other rules... 19 | parserOptions: { 20 | ecmaVersion: "latest", 21 | sourceType: "module", 22 | project: ["./tsconfig.json", "./tsconfig.node.json"], 23 | tsconfigRootDir: __dirname, 24 | }, 25 | }; 26 | ``` 27 | 28 | - Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` 29 | - Optionally add `plugin:@typescript-eslint/stylistic-type-checked` 30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list 31 | -------------------------------------------------------------------------------- /apps/clients/attendance/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: 13 | 14 | - Configure the top-level `parserOptions` property like this: 15 | 16 | ```js 17 | export default { 18 | // other rules... 19 | parserOptions: { 20 | ecmaVersion: "latest", 21 | sourceType: "module", 22 | project: ["./tsconfig.json", "./tsconfig.node.json"], 23 | tsconfigRootDir: __dirname, 24 | }, 25 | }; 26 | ``` 27 | 28 | - Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` 29 | - Optionally add `plugin:@typescript-eslint/stylistic-type-checked` 30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list 31 | -------------------------------------------------------------------------------- /packages/ui/src/theme.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { MoonIcon, SunIcon } from "@radix-ui/react-icons"; 5 | import { ThemeProvider, useTheme } from "next-themes"; 6 | 7 | import { Button } from "./button"; 8 | import { 9 | DropdownMenu, 10 | DropdownMenuContent, 11 | DropdownMenuItem, 12 | DropdownMenuTrigger, 13 | } from "./dropdown-menu"; 14 | 15 | function ThemeToggle() { 16 | const { setTheme } = useTheme(); 17 | 18 | return ( 19 | 20 | 21 | 26 | 27 | 28 | setTheme("light")}> 29 | Terang 30 | 31 | setTheme("dark")}> 32 | Gelap 33 | 34 | setTheme("system")}> 35 | Sistem 36 | 37 | 38 | 39 | ); 40 | } 41 | 42 | export { ThemeProvider, ThemeToggle }; 43 | -------------------------------------------------------------------------------- /apps/web/README.md: -------------------------------------------------------------------------------- 1 | # Create T3 App 2 | 3 | This is a [T3 Stack](https://create.t3.gg/) project bootstrapped with `create-t3-app`. 4 | 5 | ## What's next? How do I make an app with this? 6 | 7 | We try to keep this project as simple as possible, so you can start with just the scaffolding we set up for you, and add additional things later when they become necessary. 8 | 9 | If you are not familiar with the different technologies used in this project, please refer to the respective docs. If you still are in the wind, please join our [Discord](https://t3.gg/discord) and ask for help. 10 | 11 | - [Next.js](https://nextjs.org) 12 | - [NextAuth.js](https://next-auth.js.org) 13 | - [Drizzle](https://orm.drizzle.team) 14 | - [Tailwind CSS](https://tailwindcss.com) 15 | - [tRPC](https://trpc.io) 16 | 17 | ## Learn More 18 | 19 | To learn more about the [T3 Stack](https://create.t3.gg/), take a look at the following resources: 20 | 21 | - [Documentation](https://create.t3.gg/) 22 | - [Learn the T3 Stack](https://create.t3.gg/en/faq#what-learning-resources-are-currently-available) — Check out these awesome tutorials 23 | 24 | You can check out the [create-t3-app GitHub repository](https://github.com/t3-oss/create-t3-app) — your feedback and contributions are welcome! 25 | 26 | ## How do I deploy this? 27 | 28 | Follow our deployment guides for [Vercel](https://create.t3.gg/en/deployment/vercel) and [Docker](https://create.t3.gg/en/deployment/docker) for more information. 29 | -------------------------------------------------------------------------------- /apps/processor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sora-vp/processor", 3 | "version": "3.1.1", 4 | "private": true, 5 | "type": "module", 6 | "main": "dist/index.js", 7 | "files": [ 8 | "dist" 9 | ], 10 | "scripts": { 11 | "build": "tsup", 12 | "clean": "rm -rf .turbo node_modules", 13 | "dev": "yarn with-env tsx watch ./index.ts", 14 | "lint": "eslint", 15 | "format": "prettier --check . --ignore-path ../../.gitignore", 16 | "typecheck": "tsc --noEmit", 17 | "with-env": "dotenv -e ../../.env --" 18 | }, 19 | "dependencies": { 20 | "@t3-oss/env-core": "^0.11.1", 21 | "@trpc/client": "^11.1.0", 22 | "@trpc/server": "^11.1.0", 23 | "amqplib": "^0.10.5", 24 | "drizzle-orm": "^0.38.4", 25 | "mysql2": "^3.12.0", 26 | "pino": "^9.6.0", 27 | "pino-pretty": "^13.0.0", 28 | "superjson": "2.2.2", 29 | "zod": "^3.24.1" 30 | }, 31 | "devDependencies": { 32 | "@sora-vp/api": "*", 33 | "@sora-vp/db": "*", 34 | "@sora-vp/eslint-config": "*", 35 | "@sora-vp/id-generator": "*", 36 | "@sora-vp/prettier-config": "*", 37 | "@sora-vp/tailwind-config": "*", 38 | "@sora-vp/tsconfig": "*", 39 | "@types/amqplib": "^0.10.6", 40 | "@types/node": "^20.17.14", 41 | "dotenv-cli": "^7.4.4", 42 | "eslint": "^9.12.0", 43 | "prettier": "^3.4.2", 44 | "tsup": "^8.3.5", 45 | "tsx": "^4.19.2", 46 | "typescript": "^5.6.3" 47 | }, 48 | "prettier": "@sora-vp/prettier-config" 49 | } 50 | -------------------------------------------------------------------------------- /apps/clients/chooser/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20 AS base 2 | 3 | # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. 4 | RUN <, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )); 21 | Avatar.displayName = AvatarPrimitive.Root.displayName; 22 | 23 | const AvatarImage = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )); 33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName; 34 | 35 | const AvatarFallback = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | )); 48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; 49 | 50 | export { Avatar, AvatarImage, AvatarFallback }; 51 | -------------------------------------------------------------------------------- /packages/api/src/router/auth.ts: -------------------------------------------------------------------------------- 1 | import type { TRPCRouterRecord } from "@trpc/server"; 2 | import { TRPCError } from "@trpc/server"; 3 | import bcrypt from "bcrypt"; 4 | 5 | import { countUserTable, preparedGetUserByEmail } from "@sora-vp/db/client"; 6 | import * as schema from "@sora-vp/db/schema"; 7 | import { auth as authValidator } from "@sora-vp/validators"; 8 | 9 | import { publicProcedure } from "../trpc"; 10 | 11 | export const authRouter = { 12 | register: publicProcedure 13 | .input(authValidator.ServerRegisterSchema) 14 | .mutation(async ({ ctx, input }) => { 15 | if (ctx.session) 16 | throw new TRPCError({ 17 | code: "UNAUTHORIZED", 18 | message: "Tidak bisa membuat pengguna baru karena anda sudah login!", 19 | }); 20 | 21 | const isUserExist = await preparedGetUserByEmail.execute({ 22 | email: input.email, 23 | }); 24 | 25 | if (isUserExist) 26 | throw new TRPCError({ 27 | code: "BAD_REQUEST", 28 | message: "Pengguna dengan email yang sama sudah terdaftar!", 29 | }); 30 | 31 | const salt = bcrypt.genSaltSync(10); 32 | const hash = bcrypt.hashSync(input.password, salt); 33 | 34 | const userTable = await countUserTable.execute(); 35 | const availUser = userTable.at(0); 36 | const autoAdmin = availUser && availUser.count < 1; 37 | 38 | await ctx.db.insert(schema.users).values({ 39 | ...input, 40 | password: hash, 41 | verifiedAt: autoAdmin ? new Date() : null, 42 | role: autoAdmin ? "admin" : null, 43 | }); 44 | 45 | return { 46 | success: true, 47 | }; 48 | }), 49 | } satisfies TRPCRouterRecord; 50 | -------------------------------------------------------------------------------- /packages/db/src/schema/main.ts: -------------------------------------------------------------------------------- 1 | import { uniqueIndex } from "drizzle-orm/mysql-core"; 2 | 3 | import { nanoid } from "@sora-vp/id-generator"; 4 | 5 | import { mySqlTable } from "./_table"; 6 | 7 | export const users = mySqlTable( 8 | "user", 9 | (t) => ({ 10 | id: t.int("id").autoincrement().primaryKey(), 11 | name: t.text("name").notNull(), 12 | email: t.varchar("email", { length: 255 }).notNull(), 13 | password: t.varchar("password", { length: 255 }).notNull(), 14 | verifiedAt: t.timestamp("verified_at", { mode: "date" }), 15 | role: t.mysqlEnum("role", ["admin", "comittee"]), 16 | }), 17 | (users) => [uniqueIndex("email_unique_index").on(users.email)], 18 | ); 19 | 20 | export const candidates = mySqlTable("candidate", (t) => ({ 21 | id: t.int("id").autoincrement().primaryKey(), 22 | name: t.text("name").notNull(), 23 | counter: t.int("counter").notNull().default(0), 24 | image: t.varchar("image", { length: 100 }).notNull(), 25 | })); 26 | 27 | export const participants = mySqlTable( 28 | "participant", 29 | (t) => ({ 30 | id: t.int("id").autoincrement().primaryKey(), 31 | name: t.text("name").notNull(), 32 | subpart: t.varchar("sub_part", { length: 50 }).notNull(), 33 | qrId: t.varchar("qr_id", { length: 30 }).$defaultFn(() => nanoid()), 34 | 35 | // CRITICAL FEATURE, for presence functionality 36 | alreadyAttended: t.boolean("already_attended").default(false).notNull(), 37 | attendedAt: t.timestamp("attended_at", { mode: "date" }), 38 | 39 | // VERY VERY CRITICAL, for vote functionality 40 | alreadyChoosing: t.boolean("already_choosing").default(false).notNull(), 41 | choosingAt: t.timestamp("choosing_at", { mode: "date" }), 42 | }), 43 | (participants) => [uniqueIndex("qr_id_unique_index").on(participants.qrId)], 44 | ); 45 | -------------------------------------------------------------------------------- /tooling/eslint/types.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Since the ecosystem hasn't fully migrated to ESLint's new FlatConfig system yet, 3 | * we "need" to type some of the plugins manually :( 4 | */ 5 | 6 | declare module "@eslint/js" { 7 | // Why the hell doesn't eslint themselves export their types? 8 | import type { Linter } from "eslint"; 9 | 10 | export const configs: { 11 | readonly recommended: { readonly rules: Readonly }; 12 | readonly all: { readonly rules: Readonly }; 13 | }; 14 | } 15 | 16 | declare module "eslint-plugin-import" { 17 | import type { Linter, Rule } from "eslint"; 18 | 19 | export const configs: { 20 | recommended: { rules: Linter.RulesRecord }; 21 | }; 22 | export const rules: Record; 23 | } 24 | 25 | declare module "eslint-plugin-react" { 26 | import type { Linter, Rule } from "eslint"; 27 | 28 | export const configs: { 29 | recommended: { rules: Linter.RulesRecord }; 30 | all: { rules: Linter.RulesRecord }; 31 | "jsx-runtime": { rules: Linter.RulesRecord }; 32 | }; 33 | export const rules: Record; 34 | } 35 | 36 | declare module "eslint-plugin-react-hooks" { 37 | import type { Linter, Rule } from "eslint"; 38 | 39 | export const configs: { 40 | recommended: { 41 | rules: { 42 | "rules-of-hooks": Linter.RuleEntry; 43 | "exhaustive-deps": Linter.RuleEntry; 44 | }; 45 | }; 46 | }; 47 | export const rules: Record; 48 | } 49 | 50 | declare module "@next/eslint-plugin-next" { 51 | import type { Linter, Rule } from "eslint"; 52 | 53 | export const configs: { 54 | recommended: { rules: Linter.RulesRecord }; 55 | "core-web-vitals": { rules: Linter.RulesRecord }; 56 | }; 57 | export const rules: Record; 58 | } 59 | -------------------------------------------------------------------------------- /packages/validators/src/participant.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | import { validateId } from "@sora-vp/id-generator"; 4 | 5 | const baseNameSchema = z 6 | .string() 7 | .min(1, { message: "Diperlukan nama peserta!" }) 8 | .regex(/^[a-zA-Z0-9.,'\s`-]+$/, { 9 | message: 10 | "Hanya diperbolehkan menulis alfabet, angka, koma, petik satu, dan titik!", 11 | }); 12 | const baseSubpartSchema = z 13 | .string() 14 | .min(1, { message: "Diperlukan bagian darimana peserta ini!" }) 15 | .regex(/^[a-zA-Z0-9-_]+$/, { 16 | message: "Hanya diperbolehkan menulis alfabet, angka, dan garis bawah!", 17 | }); 18 | 19 | const SharedAddParticipant = z.object({ 20 | name: baseNameSchema, 21 | subpart: baseSubpartSchema, 22 | }); 23 | 24 | const SharedUploadManyParticipant = z.array( 25 | z.object({ Nama: baseNameSchema, "Bagian Dari": baseSubpartSchema }), 26 | ); 27 | 28 | const UploadParticipantSchema = z.object({ 29 | csv: z 30 | .any() 31 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access 32 | .refine((files) => files?.length == 1, "Diperlukan file csv!") 33 | .refine( 34 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access 35 | (files) => files?.[0]?.type === "text/csv", 36 | "Hanya format file csv yang diterima!", 37 | ), 38 | }); 39 | 40 | const ParticipantAttendSchema = z.string().refine(validateId); 41 | 42 | const ServerDeleteParticipant = z.object({ qrId: ParticipantAttendSchema }); 43 | 44 | const ServerUpdateParticipant = z.object({ 45 | name: baseNameSchema, 46 | subpart: baseSubpartSchema, 47 | qrId: ParticipantAttendSchema, 48 | }); 49 | 50 | export const participant = { 51 | SharedAddParticipant, 52 | SharedUploadManyParticipant, 53 | UploadParticipantSchema, 54 | ServerDeleteParticipant, 55 | ParticipantAttendSchema, 56 | ServerUpdateParticipant, 57 | } as const; 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sora-baseline", 3 | "private": true, 4 | "engines": { 5 | "node": ">=20.12.0" 6 | }, 7 | "scripts": { 8 | "pre-build": "turbo build --filter @sora-vp/db --filter @sora-vp/settings --filter @sora-vp/id-generator", 9 | "build": "turbo build", 10 | "clean": "git clean -xdf node_modules", 11 | "clean:workspaces": "turbo clean", 12 | "db:push": "yarn workspace @sora-vp/db push", 13 | "db:studio": "yarn workspace @sora-vp/db studio", 14 | "db:generate": "yarn workspace @sora-vp/db generate", 15 | "dev": "turbo dev --parallel", 16 | "dev:web": "turbo run dev --filter @sora-vp/web", 17 | "dev:attendance": "turbo run dev --filter @sora-vp/attendance", 18 | "dev:chooser": "turbo run dev --filter @sora-vp/chooser", 19 | "dev:processor": "turbo run dev --filter @sora-vp/processor", 20 | "format": "turbo format --continue -- --cache --cache-location node_modules/.cache/.prettiercache", 21 | "format:fix": "turbo format --continue -- --write --cache --cache-location node_modules/.cache/.prettiercache", 22 | "lint": "turbo lint --continue -- --cache --cache-location node_modules/.cache/.eslintcache", 23 | "lint:fix": "turbo lint --continue -- --fix --cache --cache-location node_modules/.cache/.eslintcache", 24 | "lint:ws": "yarn dlx sherif@latest", 25 | "postinstall": "yarn lint:ws", 26 | "typecheck": "turbo typecheck", 27 | "ui-add": "yarn workspace @sora-vp/ui ui-add" 28 | }, 29 | "devDependencies": { 30 | "@sora-vp/prettier-config": "*", 31 | "@turbo/gen": "^2.5.3", 32 | "prettier": "^3.4.2", 33 | "turbo": "^2.5.3", 34 | "typescript": "^5.6.3" 35 | }, 36 | "prettier": "@sora-vp/prettier-config", 37 | "packageManager": "yarn@4.9.1", 38 | "workspaces": [ 39 | "apps/clients/*", 40 | "apps/processor", 41 | "apps/web", 42 | "packages/*", 43 | "tooling/*" 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /packages/ui/src/resizable.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { DragHandleDots2Icon } from "@radix-ui/react-icons"; 4 | import * as ResizablePrimitive from "react-resizable-panels"; 5 | 6 | import { cn } from "@sora-vp/ui"; 7 | 8 | const ResizablePanelGroup = ({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) => ( 12 | 19 | ); 20 | 21 | const ResizablePanel = ResizablePrimitive.Panel; 22 | 23 | const ResizableHandle = ({ 24 | withHandle, 25 | className, 26 | ...props 27 | }: React.ComponentProps & { 28 | withHandle?: boolean; 29 | }) => ( 30 | div]:rotate-90", 33 | className, 34 | )} 35 | {...props} 36 | > 37 | {withHandle && ( 38 |
39 | 40 |
41 | )} 42 |
43 | ); 44 | 45 | export { ResizablePanelGroup, ResizablePanel, ResizableHandle }; 46 | -------------------------------------------------------------------------------- /packages/ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sora-vp/ui", 3 | "private": true, 4 | "version": "0.1.0", 5 | "type": "module", 6 | "exports": { 7 | ".": "./src/index.ts", 8 | "./*": "./src/*.tsx" 9 | }, 10 | "license": "MIT", 11 | "scripts": { 12 | "add": "yarn dlx shadcn-ui add", 13 | "clean": "rm -rf .turbo node_modules", 14 | "format": "prettier --check . --ignore-path ../../.gitignore", 15 | "lint": "eslint", 16 | "typecheck": "tsc --noEmit --emitDeclarationOnly false", 17 | "ui-add": "yarn dlx shadcn-ui add && prettier src --write --list-different" 18 | }, 19 | "dependencies": { 20 | "@hookform/resolvers": "^3.10.0", 21 | "@radix-ui/react-alert-dialog": "^1.1.13", 22 | "@radix-ui/react-avatar": "^1.1.2", 23 | "@radix-ui/react-dialog": "^1.1.4", 24 | "@radix-ui/react-dropdown-menu": "^2.1.4", 25 | "@radix-ui/react-icons": "^1.3.2", 26 | "@radix-ui/react-label": "^2.1.1", 27 | "@radix-ui/react-select": "^2.1.4", 28 | "@radix-ui/react-separator": "^1.1.1", 29 | "@radix-ui/react-slot": "^1.1.1", 30 | "@radix-ui/react-switch": "^1.1.2", 31 | "@radix-ui/react-tooltip": "^1.1.6", 32 | "class-variance-authority": "^0.7.1", 33 | "next-themes": "^0.4.4", 34 | "react-hook-form": "^7.54.2", 35 | "react-resizable-panels": "^2.1.7", 36 | "sonner": "^1.7.2", 37 | "tailwind-merge": "^2.6.0", 38 | "tailwindcss-animate": "^1.0.7" 39 | }, 40 | "devDependencies": { 41 | "@sora-vp/eslint-config": "*", 42 | "@sora-vp/prettier-config": "*", 43 | "@sora-vp/tailwind-config": "*", 44 | "@sora-vp/tsconfig": "*", 45 | "@types/react": "19.1.3", 46 | "eslint": "^9.12.0", 47 | "prettier": "^3.4.2", 48 | "react": "19.1.0", 49 | "tailwindcss": "^3.4.17", 50 | "typescript": "^5.6.3", 51 | "zod": "^3.24.1" 52 | }, 53 | "peerDependencies": { 54 | "react": "18.3.1", 55 | "zod": "^3.23.6" 56 | }, 57 | "prettier": "@sora-vp/prettier-config" 58 | } 59 | -------------------------------------------------------------------------------- /apps/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sora-vp/web", 3 | "version": "3.1.1", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "build": "yarn with-env next build", 8 | "clean": "git clean -xdf .next .turbo node_modules", 9 | "dev": "yarn with-env next dev --turbopack", 10 | "format": "prettier --check . --ignore-path ../../.gitignore", 11 | "lint": "eslint", 12 | "start": "yarn with-env next start", 13 | "typecheck": "tsc --noEmit", 14 | "with-env": "dotenv -e ../../.env --" 15 | }, 16 | "dependencies": { 17 | "@sora-vp/api": "*", 18 | "@sora-vp/auth": "*", 19 | "@sora-vp/db": "*", 20 | "@sora-vp/ui": "*", 21 | "@sora-vp/validators": "*", 22 | "@t3-oss/env-nextjs": "^0.11.1", 23 | "@tanstack/react-query": "^5.69.0", 24 | "@tanstack/react-table": "^8.20.6", 25 | "@trpc/client": "^11.1.0", 26 | "@trpc/react-query": "^11.1.0", 27 | "@trpc/server": "^11.1.0", 28 | "csv-parse": "^5.6.0", 29 | "date-fns": "^3.6.0", 30 | "exceljs": "^4.4.0", 31 | "geist": "^1.3.1", 32 | "js-base64": "^3.7.7", 33 | "lucide-react": "^0.473.0", 34 | "mime-types": "^2.1.35", 35 | "next": "15.3.2", 36 | "qrcode": "^1.5.4", 37 | "react": "19.1.0", 38 | "react-dom": "19.1.0", 39 | "recharts": "^2.15.3", 40 | "superjson": "2.2.2", 41 | "zod": "^3.24.1" 42 | }, 43 | "devDependencies": { 44 | "@sora-vp/eslint-config": "*", 45 | "@sora-vp/prettier-config": "*", 46 | "@sora-vp/tailwind-config": "*", 47 | "@sora-vp/tsconfig": "*", 48 | "@types/mime-types": "^2.1.4", 49 | "@types/node": "^20.17.14", 50 | "@types/qrcode": "^1.5.5", 51 | "@types/react": "19.1.3", 52 | "@types/react-dom": "19.1.3", 53 | "dotenv-cli": "^7.4.4", 54 | "eslint": "^9.12.0", 55 | "jiti": "^1.21.7", 56 | "prettier": "^3.4.2", 57 | "tailwindcss": "^3.4.17", 58 | "typescript": "^5.6.3" 59 | }, 60 | "prettier": "@sora-vp/prettier-config" 61 | } 62 | -------------------------------------------------------------------------------- /packages/config/schema/admin.participant.schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | import { validateId } from "@sora/id-generator"; 4 | 5 | const baseNameSchema = z 6 | .string() 7 | .min(1, { message: "Diperlukan nama peserta!" }) 8 | .regex(/^[a-zA-Z0-9.,'\s`-]+$/, { 9 | message: 10 | "Hanya diperbolehkan menulis alfabet, angka, koma, petik satu, dan titik!", 11 | }); 12 | const baseSubpartSchema = z 13 | .string() 14 | .min(1, { message: "Diperlukan bagian darimana peserta ini!" }) 15 | .regex(/^[a-zA-Z0-9-_]+$/, { 16 | message: "Hanya diperbolehkan menulis alfabet, angka, dan garis bawah!", 17 | }); 18 | 19 | export const TambahPesertaValidationSchema = z.object({ 20 | name: baseNameSchema, 21 | subpart: baseSubpartSchema, 22 | }); 23 | 24 | export const TambahPesertaManyValidationSchema = z.array( 25 | z.object({ Nama: baseNameSchema, "Bagian Dari": baseSubpartSchema }), 26 | ); 27 | 28 | export const UploadPartisipanValidationSchema = z.object({ 29 | csv: z 30 | .any() 31 | .refine((files) => files?.length == 1, "Diperlukan file csv!") 32 | .refine( 33 | (files) => files?.[0]?.type === "text/csv", 34 | "Hanya format file csv yang diterima!", 35 | ), 36 | }); 37 | 38 | export type TUploadFormValues = { csv: FileList }; 39 | 40 | export const DeletePesertaValidationSchema = z.object({ id: z.number() }); 41 | 42 | export type TambahFormValues = z.infer; 43 | 44 | export const PaginatedParticipantValidationSchema = z.object({ 45 | pageSize: z.number().min(10), 46 | pageIndex: z.number().min(0), 47 | }); 48 | 49 | export const ParticipantBySubpartValidationSchema = z.object({ 50 | subpart: z.string(), 51 | }); 52 | 53 | export const ParticipantAttendValidationSchema = z.string().refine(validateId); 54 | 55 | export const UpdateParticipantValidationSchema = z.object({ 56 | name: baseNameSchema, 57 | subpart: baseSubpartSchema, 58 | qrId: ParticipantAttendValidationSchema, 59 | }); 60 | -------------------------------------------------------------------------------- /apps/clients/attendance/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sora-vp/client-attendance", 3 | "private": true, 4 | "version": "3.1.1", 5 | "type": "module", 6 | "scripts": { 7 | "build": "yarn with-env vite build", 8 | "clean": "git clean -xdf dist .turbo node_modules", 9 | "dev": "yarn with-env vite", 10 | "format": "prettier --check . --ignore-path ../../.gitignore", 11 | "lint": "eslint", 12 | "preview": "vite preview", 13 | "typecheck": "tsc --noEmit", 14 | "with-env": "dotenv -e ../../.env --" 15 | }, 16 | "dependencies": { 17 | "@sora-vp/api": "*", 18 | "@sora-vp/ui": "*", 19 | "@sora-vp/validators": "*", 20 | "@t3-oss/env-core": "^0.11.1", 21 | "@tanstack/react-query": "^5.69.0", 22 | "@trpc/client": "^11.1.0", 23 | "@trpc/react-query": "^11.1.0", 24 | "@trpc/server": "^11.1.0", 25 | "jotai": "^2.11.1", 26 | "lucide-react": "^0.473.0", 27 | "motion": "^12.0.1", 28 | "non.geist": "^1.0.4", 29 | "qr-scanner": "^1.4.2", 30 | "react": "19.1.0", 31 | "react-dom": "19.1.0", 32 | "react-router-dom": "^7.1.3", 33 | "superjson": "2.2.2", 34 | "zod": "^3.24.1" 35 | }, 36 | "devDependencies": { 37 | "@eslint/js": "^9.18.0", 38 | "@fontsource-variable/noto-sans-sundanese": "^5.1.1", 39 | "@sora-vp/eslint-config": "*", 40 | "@sora-vp/prettier-config": "*", 41 | "@sora-vp/tailwind-config": "*", 42 | "@types/eslint__js": "^8.42.3", 43 | "@types/react": "19.1.3", 44 | "@types/react-dom": "19.1.3", 45 | "@typescript-eslint/eslint-plugin": "^8.32.0", 46 | "@typescript-eslint/parser": "^8.32.0", 47 | "@vitejs/plugin-react-swc": "^3.7.2", 48 | "autoprefixer": "^10.4.20", 49 | "dotenv-cli": "^7.4.4", 50 | "eslint": "^9.12.0", 51 | "eslint-plugin-react-hooks": "^5.0.0", 52 | "eslint-plugin-react-refresh": "^0.4.18", 53 | "postcss": "^8.5.1", 54 | "prettier": "^3.4.2", 55 | "tailwindcss": "^3.4.17", 56 | "typescript": "^5.6.3", 57 | "typescript-eslint": "^8.32.0", 58 | "vite": "^6.3.5" 59 | }, 60 | "prettier": "@sora-vp/prettier-config" 61 | } 62 | -------------------------------------------------------------------------------- /packages/ui/src/button.tsx: -------------------------------------------------------------------------------- 1 | import type { VariantProps } from "class-variance-authority"; 2 | import * as React from "react"; 3 | import { Slot } from "@radix-ui/react-slot"; 4 | import { cva } from "class-variance-authority"; 5 | 6 | import { cn } from "@sora-vp/ui"; 7 | 8 | const buttonVariants = cva( 9 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", 10 | { 11 | variants: { 12 | variant: { 13 | default: 14 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 15 | destructive: 16 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 17 | outline: 18 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 19 | secondary: 20 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 21 | ghost: "hover:bg-accent hover:text-accent-foreground", 22 | link: "text-primary underline-offset-4 hover:underline", 23 | }, 24 | size: { 25 | default: "h-9 px-4 py-2", 26 | sm: "h-8 rounded-md px-3 text-xs", 27 | lg: "h-10 rounded-md px-8", 28 | icon: "h-9 w-9", 29 | }, 30 | }, 31 | defaultVariants: { 32 | variant: "default", 33 | size: "default", 34 | }, 35 | }, 36 | ); 37 | 38 | export interface ButtonProps 39 | extends React.ButtonHTMLAttributes, 40 | VariantProps { 41 | asChild?: boolean; 42 | } 43 | 44 | const Button = React.forwardRef( 45 | ({ className, variant, size, asChild = false, ...props }, ref) => { 46 | const Comp = asChild ? Slot : "button"; 47 | return ( 48 | 53 | ); 54 | }, 55 | ); 56 | Button.displayName = "Button"; 57 | 58 | export { Button, buttonVariants }; 59 | -------------------------------------------------------------------------------- /apps/clients/chooser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sora-vp/client-chooser", 3 | "private": true, 4 | "version": "3.1.1", 5 | "type": "module", 6 | "scripts": { 7 | "build": "yarn with-env vite build", 8 | "clean": "git clean -xdf dist .turbo node_modules", 9 | "dev": "yarn with-env vite", 10 | "format": "prettier --check . --ignore-path ../../.gitignore", 11 | "lint": "eslint", 12 | "preview": "vite preview", 13 | "typecheck": "tsc --noEmit", 14 | "with-env": "dotenv -e ../../.env --" 15 | }, 16 | "dependencies": { 17 | "@sora-vp/api": "*", 18 | "@sora-vp/ui": "*", 19 | "@sora-vp/validators": "*", 20 | "@t3-oss/env-core": "^0.11.1", 21 | "@tanstack/react-query": "^5.69.0", 22 | "@trpc/client": "^11.1.0", 23 | "@trpc/react-query": "^11.1.0", 24 | "@trpc/server": "^11.1.0", 25 | "jotai": "^2.11.1", 26 | "lucide-react": "^0.473.0", 27 | "motion": "^12.0.1", 28 | "non.geist": "^1.0.4", 29 | "qr-scanner": "^1.4.2", 30 | "react": "19.1.0", 31 | "react-dom": "19.1.0", 32 | "react-router-dom": "^7.1.3", 33 | "react-use-websocket": "^4.11.1", 34 | "superjson": "2.2.2", 35 | "zod": "^3.24.1" 36 | }, 37 | "devDependencies": { 38 | "@eslint/js": "^9.18.0", 39 | "@fontsource-variable/noto-sans-sundanese": "^5.1.1", 40 | "@sora-vp/eslint-config": "*", 41 | "@sora-vp/prettier-config": "*", 42 | "@sora-vp/tailwind-config": "*", 43 | "@types/eslint__js": "^8.42.3", 44 | "@types/react": "19.1.3", 45 | "@types/react-dom": "19.1.3", 46 | "@typescript-eslint/eslint-plugin": "^8.32.0", 47 | "@typescript-eslint/parser": "^8.32.0", 48 | "@vitejs/plugin-react-swc": "^3.7.2", 49 | "autoprefixer": "^10.4.20", 50 | "dotenv-cli": "^7.4.4", 51 | "eslint": "^9.12.0", 52 | "eslint-plugin-react-hooks": "^5.0.0", 53 | "eslint-plugin-react-refresh": "^0.4.18", 54 | "postcss": "^8.5.1", 55 | "prettier": "^3.4.2", 56 | "tailwindcss": "^3.4.17", 57 | "typescript": "^5.6.3", 58 | "typescript-eslint": "^8.32.0", 59 | "vite": "^6.3.5" 60 | }, 61 | "prettier": "@sora-vp/prettier-config" 62 | } 63 | -------------------------------------------------------------------------------- /packages/api/src/router/statistic.ts: -------------------------------------------------------------------------------- 1 | import type { TRPCRouterRecord } from "@trpc/server"; 2 | import { TRPCError } from "@trpc/server"; 3 | 4 | import { 5 | preparedGetAllParticipants, 6 | preparedGetAttendedAndVoted, 7 | preparedGetCandidateCountsOnly, 8 | preparedGetGraphicalData, 9 | } from "@sora-vp/db/client"; 10 | 11 | import { adminProcedure } from "../trpc"; 12 | 13 | export const statisticRouter = { 14 | graphicalDataQuery: adminProcedure.query(() => 15 | preparedGetGraphicalData.execute(), 16 | ), 17 | 18 | essentialInfoQuery: adminProcedure.query(async () => { 19 | const candidates = await preparedGetCandidateCountsOnly.execute(); 20 | 21 | if (candidates.length < 1) 22 | return { 23 | isMatch: null, 24 | participants: null, 25 | candidates: null, 26 | }; 27 | 28 | const participantCounter = await preparedGetAttendedAndVoted.execute(); 29 | 30 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 31 | const extractCount = participantCounter.at(0)!; 32 | const participantsAccumulation = extractCount.count; 33 | 34 | const candidatesAccumulation = candidates 35 | .map((d) => d.counter) 36 | .reduce((curr, acc) => curr + acc); 37 | 38 | return { 39 | isMatch: participantsAccumulation === candidatesAccumulation, 40 | participants: participantsAccumulation, 41 | candidates: candidatesAccumulation, 42 | }; 43 | }), 44 | 45 | dataReportMutation: adminProcedure.mutation(async () => { 46 | const candidates = await preparedGetGraphicalData.execute(); 47 | const participants = await preparedGetAllParticipants.execute(); 48 | 49 | if (candidates.length < 0) 50 | throw new TRPCError({ 51 | code: "NOT_FOUND", 52 | message: "Tidak ada data kandidat!", 53 | }); 54 | 55 | if (participants.length < 0) 56 | throw new TRPCError({ 57 | code: "NOT_FOUND", 58 | message: "Tidak ada data peserta pemilihan!", 59 | }); 60 | 61 | return { 62 | participants, 63 | candidates: candidates.map((c) => [c.name, c.counter]), 64 | }; 65 | }), 66 | } satisfies TRPCRouterRecord; 67 | -------------------------------------------------------------------------------- /packages/ui/src/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@sora-vp/ui"; 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )); 18 | Card.displayName = "Card"; 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )); 30 | CardHeader.displayName = "CardHeader"; 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

41 | )); 42 | CardTitle.displayName = "CardTitle"; 43 | 44 | const CardDescription = React.forwardRef< 45 | HTMLParagraphElement, 46 | React.HTMLAttributes 47 | >(({ className, ...props }, ref) => ( 48 |

53 | )); 54 | CardDescription.displayName = "CardDescription"; 55 | 56 | const CardContent = React.forwardRef< 57 | HTMLDivElement, 58 | React.HTMLAttributes 59 | >(({ className, ...props }, ref) => ( 60 |

61 | )); 62 | CardContent.displayName = "CardContent"; 63 | 64 | const CardFooter = React.forwardRef< 65 | HTMLDivElement, 66 | React.HTMLAttributes 67 | >(({ className, ...props }, ref) => ( 68 |
73 | )); 74 | CardFooter.displayName = "CardFooter"; 75 | 76 | export { 77 | Card, 78 | CardHeader, 79 | CardFooter, 80 | CardTitle, 81 | CardDescription, 82 | CardContent, 83 | }; 84 | -------------------------------------------------------------------------------- /apps/clients/chooser/src/components/scanner/main-scanner.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react"; 2 | import { motion } from "motion/react"; 3 | import QrScanner from "qr-scanner"; 4 | 5 | import { validateId } from "@sora-vp/id-generator"; 6 | import { toast } from "@sora-vp/ui/toast"; 7 | 8 | export function MainScanner({ 9 | setInvalidQr, 10 | mutateData, 11 | }: { 12 | setInvalidQr: (invalid: boolean) => void; 13 | mutateData: (qrId: string) => void; 14 | }) { 15 | const videoRef = useRef(null!); 16 | 17 | useEffect(() => { 18 | const qrScanner = new QrScanner( 19 | videoRef.current, 20 | ({ data }) => { 21 | if (data || data !== "") { 22 | qrScanner.stop(); 23 | 24 | const isValidQr = validateId(data); 25 | 26 | if (!isValidQr) return setInvalidQr(true); 27 | 28 | mutateData(data); 29 | } 30 | }, 31 | { 32 | highlightCodeOutline: true, 33 | highlightScanRegion: true, 34 | onDecodeError: (error) => { 35 | if (error instanceof Error) 36 | toast.error("Terjadi kegagalan dalam memindai gambar QR.", { 37 | description: `Error: ${error.message}`, 38 | }); 39 | }, 40 | }, 41 | ); 42 | 43 | qrScanner.start(); 44 | 45 | return () => { 46 | qrScanner.destroy(); 47 | }; 48 | 49 | // eslint-disable-next-line react-hooks/exhaustive-deps 50 | }, []); 51 | 52 | return ( 53 |
54 | 63 | Mohon arahkan QR ke kotak kuning 64 | 65 | 74 |
75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /apps/clients/attendance/src/components/scanner/main-scanner.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react"; 2 | import { motion } from "motion/react"; 3 | import QrScanner from "qr-scanner"; 4 | 5 | import { validateId } from "@sora-vp/id-generator"; 6 | import { toast } from "@sora-vp/ui/toast"; 7 | 8 | export function MainScanner({ 9 | setInvalidQr, 10 | mutateData, 11 | }: { 12 | setInvalidQr: (invalid: boolean) => void; 13 | mutateData: (qrId: string) => void; 14 | }) { 15 | const videoRef = useRef(null!); 16 | 17 | useEffect(() => { 18 | const qrScanner = new QrScanner( 19 | videoRef.current, 20 | ({ data }) => { 21 | if (data || data !== "") { 22 | qrScanner.stop(); 23 | 24 | const isValidQr = validateId(data); 25 | 26 | if (!isValidQr) return setInvalidQr(true); 27 | 28 | mutateData(data); 29 | } 30 | }, 31 | { 32 | highlightCodeOutline: true, 33 | highlightScanRegion: true, 34 | onDecodeError: (error) => { 35 | if (error instanceof Error) 36 | toast.error("Terjadi kegagalan dalam memindai gambar QR.", { 37 | description: `Error: ${error.message}`, 38 | }); 39 | }, 40 | }, 41 | ); 42 | 43 | qrScanner.start(); 44 | 45 | return () => { 46 | qrScanner.destroy(); 47 | }; 48 | 49 | // eslint-disable-next-line react-hooks/exhaustive-deps 50 | }, []); 51 | 52 | return ( 53 |
54 | 63 | Mohon arahkan QR ke kotak kuning 64 | 65 | 74 |
75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /packages/config/schema/admin.candidate.schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | import { validateId } from "@sora/id-generator"; 4 | 5 | const TwoMegs = 2_000_000; 6 | const ACCEPTED_IMAGE_TYPES = [ 7 | "image/jpeg", 8 | "image/jpg", 9 | "image/png", 10 | "image/webp", 11 | ]; 12 | 13 | const baseAddAndEditForm = z.object({ 14 | kandidat: z.string().min(1, { message: "Diperlukan nama kandidat!" }), 15 | }); 16 | 17 | export const TambahKandidatValidationSchema = baseAddAndEditForm.merge( 18 | z.object({ 19 | image: z 20 | .any() 21 | .refine((files) => files?.length == 1, "Diperlukan gambar kandidat!") 22 | .refine( 23 | (files) => files?.[0]?.size <= TwoMegs, 24 | `Ukuran maksimal gambar adalah 2MB!`, 25 | ) 26 | .refine( 27 | (files) => ACCEPTED_IMAGE_TYPES.includes(files?.[0]?.type), 28 | "Hanya format gambar .jpg, .jpeg, .png dan .webp yang diterima!", 29 | ), 30 | }), 31 | ); 32 | 33 | export type TambahFormValues = { 34 | kandidat: string; 35 | image: File; 36 | }; 37 | 38 | export const adminDeleteCandidateValidationSchema = z.object({ 39 | id: z.number(), 40 | }); 41 | 42 | export const upvoteValidationSchema = 43 | adminDeleteCandidateValidationSchema.merge( 44 | z.object({ 45 | qrId: z.string().refine(validateId), 46 | }), 47 | ); 48 | 49 | export const adminGetSpecificCandidateValidationSchema = z.object({ 50 | id: z.number(), 51 | }); 52 | 53 | export const EditKandidatValidationSchema = baseAddAndEditForm.merge( 54 | z.object({ 55 | image: z 56 | .any() 57 | .refine( 58 | (files) => (files === undefined ? true : files?.length === 1), 59 | "Diperlukan gambar kandidat!", 60 | ) 61 | .refine( 62 | (files) => (files === undefined ? true : files?.[0]?.size <= TwoMegs), 63 | `Ukuran maksimal gambar adalah 2MB!`, 64 | ) 65 | .refine( 66 | (files) => 67 | files === undefined 68 | ? true 69 | : ACCEPTED_IMAGE_TYPES.includes(files?.[0]?.type), 70 | "Hanya format gambar .jpg, .jpeg, .png dan .webp yang diterima!", 71 | ), 72 | }), 73 | ); 74 | 75 | export type TEditKandidatValidationSchema = { 76 | kandidat: string; 77 | image: File; 78 | }; 79 | -------------------------------------------------------------------------------- /apps/clients/attendance/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { ServerSettingProvider } from "@/context/server-setting"; 3 | import MainPage from "@/routes/main-page"; 4 | import { api } from "@/utils/api"; 5 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 6 | import { httpBatchLink } from "@trpc/client"; 7 | import { createBrowserRouter, RouterProvider } from "react-router-dom"; 8 | import superjson from "superjson"; 9 | 10 | import { ClientNotFound } from "@sora-vp/ui/client-not-found"; 11 | 12 | import { env } from "./env"; 13 | 14 | const router = createBrowserRouter([ 15 | { 16 | path: "*", 17 | element: , 18 | }, 19 | { 20 | path: "/", 21 | element: , 22 | }, 23 | { 24 | path: "settings", 25 | lazy: async () => { 26 | const { SettingsPage } = await import("@/routes/setting-page"); 27 | 28 | return { Component: SettingsPage }; 29 | }, 30 | }, 31 | ]); 32 | 33 | const getBaseUrl = () => { 34 | if (!env.VITE_IS_DOCKER && env.VITE_TRPC_URL) return env.VITE_TRPC_URL; 35 | 36 | if (import.meta.env.DEV && !env.VITE_IS_DOCKER && !env.VITE_TRPC_URL) 37 | return "http://localhost:3000/api/trpc"; 38 | 39 | return "/api/trpc"; 40 | }; 41 | 42 | export default function App() { 43 | const [queryClient] = useState(() => new QueryClient()); 44 | const [trpcClient] = useState(() => 45 | api.createClient({ 46 | links: [ 47 | httpBatchLink({ 48 | url: getBaseUrl(), 49 | transformer: superjson, 50 | }), 51 | ], 52 | }), 53 | ); 54 | 55 | useEffect(() => { 56 | const onKeyUp = (e: KeyboardEvent) => { 57 | if (e.ctrlKey && e.key === ",") { 58 | e.preventDefault(); 59 | 60 | router.navigate("/settings"); 61 | } 62 | }; 63 | 64 | window.addEventListener("keyup", onKeyUp); 65 | 66 | return () => { 67 | window.removeEventListener("keyup", onKeyUp); 68 | }; 69 | }, []); 70 | 71 | return ( 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | ); 80 | } 81 | -------------------------------------------------------------------------------- /apps/clients/chooser/src/components/universal-error.tsx: -------------------------------------------------------------------------------- 1 | import { RotateCcw } from "lucide-react"; 2 | import { motion } from "motion/react"; 3 | 4 | import { Button } from "@sora-vp/ui/button"; 5 | 6 | export function UniversalError(props: { 7 | title: string; 8 | description: string; 9 | errorMessage?: string; 10 | }) { 11 | return ( 12 |
13 |
14 | 23 | {props.title} 24 | 25 | 35 | {props.description} 36 | 37 |
38 | 39 | {props.errorMessage ? ( 40 | 50 |

Pesan Error:

51 |
52 |             {props.errorMessage}
53 |           
54 |
55 | ) : null} 56 | 57 | 71 |
72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /apps/web/src/app/_components/nav/nav-items.tsx: -------------------------------------------------------------------------------- 1 | import type { LucideIcon } from "lucide-react"; 2 | import Link from "next/link"; 3 | 4 | import { cn } from "@sora-vp/ui"; 5 | import { buttonVariants } from "@sora-vp/ui/button"; 6 | import { Tooltip, TooltipContent, TooltipTrigger } from "@sora-vp/ui/tooltip"; 7 | 8 | interface NavProps { 9 | isCollapsed: boolean; 10 | links: { 11 | title: string; 12 | icon: LucideIcon; 13 | href: string; 14 | }[]; 15 | } 16 | 17 | export function NavItems({ links, isCollapsed }: NavProps) { 18 | return ( 19 |
23 | 62 |
63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /apps/web/src/trpc/react.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import type { AppRouter } from "@sora-vp/api"; 4 | import { useState } from "react"; 5 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 6 | import { loggerLink, unstable_httpBatchStreamLink } from "@trpc/client"; 7 | import { createTRPCReact } from "@trpc/react-query"; 8 | import SuperJSON from "superjson"; 9 | 10 | import { env } from "~/env"; 11 | 12 | const createQueryClient = () => 13 | new QueryClient({ 14 | defaultOptions: { 15 | queries: { 16 | // With SSR, we usually want to set some default staleTime 17 | // above 0 to avoid refetching immediately on the client 18 | staleTime: 30 * 1000, 19 | }, 20 | }, 21 | }); 22 | 23 | let clientQueryClientSingleton: QueryClient | undefined = undefined; 24 | const getQueryClient = () => { 25 | if (typeof window === "undefined") { 26 | // Server: always make a new query client 27 | return createQueryClient(); 28 | } else { 29 | // Browser: use singleton pattern to keep the same query client 30 | return (clientQueryClientSingleton ??= createQueryClient()); 31 | } 32 | }; 33 | 34 | export const api = createTRPCReact(); 35 | 36 | export function TRPCReactProvider(props: { children: React.ReactNode }) { 37 | const queryClient = getQueryClient(); 38 | 39 | const [trpcClient] = useState(() => 40 | api.createClient({ 41 | links: [ 42 | loggerLink({ 43 | enabled: (op) => 44 | env.NODE_ENV === "development" || 45 | (op.direction === "down" && op.result instanceof Error), 46 | }), 47 | unstable_httpBatchStreamLink({ 48 | transformer: SuperJSON, 49 | url: getBaseUrl() + "/api/trpc", 50 | headers() { 51 | const headers = new Headers(); 52 | headers.set("x-trpc-source", "nextjs-react"); 53 | return headers; 54 | }, 55 | }), 56 | ], 57 | }), 58 | ); 59 | 60 | return ( 61 | 62 | 63 | {props.children} 64 | 65 | 66 | ); 67 | } 68 | 69 | const getBaseUrl = () => { 70 | if (typeof window !== "undefined") return window.location.origin; 71 | if (env.VERCEL_URL) return `https://${env.VERCEL_URL}`; 72 | // eslint-disable-next-line no-restricted-properties, turbo/no-undeclared-env-vars 73 | return `http://localhost:${process.env.PORT ?? 3000}`; 74 | }; 75 | -------------------------------------------------------------------------------- /packages/config/schema/auth.schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | const validNameRegex = 4 | /^(?![ -.&,_'":?!])(?!.*[- &_'":]$)(?!.*[-.#@&,:?!]{2})[a-zA-Z- .,']+$/; 5 | 6 | const email = z 7 | .string() 8 | .min(1, { message: "Bidang email harus di isi!" }) 9 | .email({ message: "Bidang email harus berupa email yang valid!" }); 10 | const password = z 11 | .string() 12 | .nonempty({ message: "Kata sandi harus di isi!" }) 13 | .min(6, { message: "Kata sandi memiliki panjang setidaknya 6 karakter!" }); 14 | const name = z 15 | .string() 16 | .min(1, { message: "Bidang nama harus di isi!" }) 17 | .regex(validNameRegex, { 18 | message: "Bidang nama harus berupa nama yang valid!", 19 | }); 20 | 21 | export const LoginSchemaValidator = z.object({ 22 | email, 23 | password, 24 | }); 25 | 26 | export const ServerRegisterSchemaValidator = z.object({ 27 | email, 28 | password, 29 | name, 30 | }); 31 | 32 | export const ClientRegisterSchemaValidator = 33 | ServerRegisterSchemaValidator.merge( 34 | z.object({ 35 | passConfirm: z.string().min(6, { 36 | message: "Konfirmasi kata sandi diperlukan setidaknya 6 karakter!", 37 | }), 38 | }), 39 | ).refine((data) => data.password === data.passConfirm, { 40 | message: "Konfirmasi kata sandi tidak sama!", 41 | path: ["passConfirm"], 42 | }); 43 | 44 | export const ServerChangePasswordSchemaValidator = z.object({ 45 | lama: password, 46 | baru: z 47 | .string() 48 | .nonempty({ message: "Kata sandi baru harus di isi!" }) 49 | .min(6, { 50 | message: "Kata sandi baru memiliki panjang setidaknya 6 karakter!", 51 | }), 52 | }); 53 | 54 | export const ClientChangePasswordSchemaValidator = 55 | ServerChangePasswordSchemaValidator.merge( 56 | z.object({ 57 | konfirmasi: z.string().min(1, { 58 | message: "Konfirmasi kata sandi diperlukan setidaknya 6 karakter!", 59 | }), 60 | }), 61 | ).refine((data) => data.baru === data.konfirmasi, { 62 | message: "Konfirmasi kata sandi tidak sama!", 63 | path: ["konfirmasi"], 64 | }); 65 | 66 | export const ChangeNameSchemaValidator = z.object({ name }); 67 | 68 | export type LoginType = z.infer; 69 | export type ServerRegisterType = z.infer; 70 | export type ClientRegisterType = z.infer; 71 | export type ClientChangePasswordType = z.infer< 72 | typeof ClientChangePasswordSchemaValidator 73 | >; 74 | export type ChangeNameType = z.infer; 75 | -------------------------------------------------------------------------------- /packages/settings/src/SettingsManager.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from "events"; 2 | 3 | export interface AppSettings { 4 | startTime: Date | null; 5 | endTime: Date | null; 6 | canVote: boolean | null; 7 | canAttend: boolean | null; 8 | canLogin: boolean | null; 9 | } 10 | 11 | export interface ReturnedValues { 12 | startTime: Date | null; 13 | endTime: Date | null; 14 | canVote: boolean; 15 | canAttend: boolean; 16 | canLogin: boolean; 17 | } 18 | 19 | export interface UpdateEventMap { 20 | update: ReturnedValues; 21 | } 22 | 23 | export type ExtractValues = T extends unknown ? T[keyof T] : never; 24 | 25 | export class SettingsManager extends EventEmitter { 26 | private settingsMap: Map>; 27 | 28 | constructor() { 29 | super(); 30 | this.settingsMap = new Map>(); 31 | } 32 | 33 | getSettings(): ReturnedValues { 34 | type DateOrUndef = Date | undefined; 35 | type BoolOrUndef = boolean | undefined; 36 | 37 | const startTime = this.settingsMap.get("startTime") as DateOrUndef; 38 | const endTime = this.settingsMap.get("endTime") as DateOrUndef; 39 | 40 | const canVote = this.settingsMap.get("canVote") as BoolOrUndef; 41 | const canAttend = this.settingsMap.get("canAttend") as BoolOrUndef; 42 | const canLogin = this.settingsMap.get("canLogin") as BoolOrUndef; 43 | 44 | return { 45 | startTime: startTime ?? null, 46 | endTime: endTime ?? null, 47 | canVote: canVote ?? false, 48 | canAttend: canAttend ?? false, 49 | canLogin: canLogin ?? true, 50 | }; 51 | } 52 | 53 | private updateBuilder( 54 | key: keyof AppSettings, 55 | value: ExtractValues, 56 | ): void { 57 | this.settingsMap.set(key, value); 58 | this.emit("update", this.getSettings()); 59 | } 60 | 61 | updateSettings = { 62 | startTime: (time: Date) => this.updateBuilder("startTime", time), 63 | endTime: (time: Date) => this.updateBuilder("endTime", time), 64 | canVote: (votable: boolean) => this.updateBuilder("canVote", votable), 65 | canAttend: (attendable: boolean) => 66 | this.updateBuilder("canAttend", attendable), 67 | canLogin: (status: boolean) => this.updateBuilder("canLogin", status), 68 | } as const; 69 | 70 | on( 71 | event: K, 72 | listener: (payload: UpdateEventMap[K]) => void, 73 | ): this { 74 | return super.on(event, listener); 75 | } 76 | } 77 | 78 | export const settings = new SettingsManager(); 79 | -------------------------------------------------------------------------------- /apps/processor/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20 AS base 2 | 3 | # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. 4 | RUN < package.json 43 | echo "nodeLinker: node-modules" > .yarnrc.yml 44 | yarn set version stable 45 | yarn add ./out.tgz 46 | EOF 47 | 48 | # Debugging purpose 49 | # RUN < { 25 | const unsubHardware = subscribe((message) => { 26 | if (message.startsWith("SORA-KEYBIND-")) { 27 | const actualCommand = message.replace("SORA-KEYBIND-", ""); 28 | 29 | switch (actualCommand) { 30 | case "RELOAD": { 31 | if (isQrInvalid || participantAttended.isError) location.reload(); 32 | 33 | break; 34 | } 35 | } 36 | } 37 | }); 38 | 39 | return () => { 40 | unsubHardware(); 41 | }; 42 | 43 | // eslint-disable-next-line react-hooks/exhaustive-deps 44 | }, [isQrInvalid, participantAttended.isError]); 45 | 46 | const setIsQrValid = useCallback( 47 | (invalid: boolean) => setInvalidQr(invalid), 48 | [], 49 | ); 50 | const mutateData = useCallback( 51 | (qrId: string) => participantAttended.mutate(qrId), 52 | [participantAttended], 53 | ); 54 | 55 | if (qrId) return ; 56 | 57 | if (participantAttended.isPending) 58 | return ( 59 | 63 | ); 64 | 65 | if (isQrInvalid || participantAttended.isError) 66 | return ( 67 | 75 | ); 76 | 77 | return ; 78 | } 79 | -------------------------------------------------------------------------------- /apps/clients/chooser/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { ParticipantProvider } from "@/context/participant-context"; 3 | import { ServerSettingProvider } from "@/context/server-setting"; 4 | import MainPage from "@/routes/main-page"; 5 | import VotePage from "@/routes/vote-page"; 6 | import { api } from "@/utils/api"; 7 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 8 | import { httpBatchLink } from "@trpc/client"; 9 | import { createBrowserRouter, RouterProvider } from "react-router-dom"; 10 | import superjson from "superjson"; 11 | 12 | import { ClientNotFound } from "@sora-vp/ui/client-not-found"; 13 | 14 | import { HardwareWebsocketProvider } from "./context/hardware-websocket"; 15 | import { env } from "./env"; 16 | 17 | const router = createBrowserRouter([ 18 | { 19 | path: "*", 20 | element: , 21 | }, 22 | { 23 | path: "/", 24 | element: , 25 | }, 26 | { 27 | path: "vote", 28 | element: , 29 | }, 30 | { 31 | path: "settings", 32 | lazy: async () => { 33 | const { SettingsPage } = await import("@/routes/setting-page"); 34 | 35 | return { Component: SettingsPage }; 36 | }, 37 | }, 38 | ]); 39 | 40 | const getBaseUrl = () => { 41 | if (!env.VITE_IS_DOCKER && env.VITE_TRPC_URL) return env.VITE_TRPC_URL; 42 | 43 | if (import.meta.env.DEV && !env.VITE_IS_DOCKER && !env.VITE_TRPC_URL) 44 | return "http://localhost:3000/api/trpc"; 45 | 46 | return "/api/trpc"; 47 | }; 48 | 49 | export default function App() { 50 | const [queryClient] = useState(() => new QueryClient()); 51 | const [trpcClient] = useState(() => 52 | api.createClient({ 53 | links: [ 54 | httpBatchLink({ 55 | url: getBaseUrl(), 56 | transformer: superjson, 57 | }), 58 | ], 59 | }), 60 | ); 61 | 62 | useEffect(() => { 63 | const onKeyUp = (e: KeyboardEvent) => { 64 | if (e.ctrlKey && e.key === ",") { 65 | e.preventDefault(); 66 | 67 | router.navigate("/settings"); 68 | } 69 | }; 70 | 71 | window.addEventListener("keyup", onKeyUp); 72 | 73 | return () => { 74 | window.removeEventListener("keyup", onKeyUp); 75 | }; 76 | }, []); 77 | 78 | return ( 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | ); 91 | } 92 | -------------------------------------------------------------------------------- /tooling/eslint/base.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import * as path from "node:path"; 4 | import { includeIgnoreFile } from "@eslint/compat"; 5 | import eslint from "@eslint/js"; 6 | import importPlugin from "eslint-plugin-import"; 7 | import turboPlugin from "eslint-plugin-turbo"; 8 | import tseslint from "typescript-eslint"; 9 | 10 | /** 11 | * All packages that leverage t3-env should use this rule 12 | */ 13 | export const restrictEnvAccess = tseslint.config( 14 | { ignores: ["**/env.ts"] }, 15 | { 16 | files: ["**/*.js", "**/*.ts", "**/*.tsx"], 17 | rules: { 18 | "no-restricted-properties": [ 19 | "error", 20 | { 21 | object: "process", 22 | property: "env", 23 | message: 24 | "Use `import { env } from '~/env'` instead to ensure validated types.", 25 | }, 26 | ], 27 | "no-restricted-imports": [ 28 | "error", 29 | { 30 | name: "process", 31 | importNames: ["env"], 32 | message: 33 | "Use `import { env } from '~/env'` instead to ensure validated types.", 34 | }, 35 | ], 36 | }, 37 | }, 38 | ); 39 | 40 | export default tseslint.config( 41 | // Ignore files not tracked by VCS and any config files 42 | includeIgnoreFile(path.join(import.meta.dirname, "../../.gitignore")), 43 | { ignores: ["**/*.config.*"] }, 44 | { 45 | files: ["**/*.js", "**/*.ts", "**/*.tsx"], 46 | plugins: { 47 | import: importPlugin, 48 | turbo: turboPlugin, 49 | }, 50 | extends: [ 51 | eslint.configs.recommended, 52 | ...tseslint.configs.recommended, 53 | ...tseslint.configs.recommendedTypeChecked, 54 | ...tseslint.configs.stylisticTypeChecked, 55 | ], 56 | rules: { 57 | ...turboPlugin.configs.recommended.rules, 58 | "@typescript-eslint/no-unused-vars": [ 59 | "error", 60 | { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }, 61 | ], 62 | "@typescript-eslint/consistent-type-imports": [ 63 | "warn", 64 | { prefer: "type-imports", fixStyle: "separate-type-imports" }, 65 | ], 66 | "@typescript-eslint/no-misused-promises": [ 67 | 2, 68 | { checksVoidReturn: { attributes: false } }, 69 | ], 70 | "@typescript-eslint/no-unnecessary-condition": [ 71 | "error", 72 | { 73 | allowConstantLoopConditions: true, 74 | }, 75 | ], 76 | "@typescript-eslint/no-non-null-assertion": "error", 77 | "import/consistent-type-specifier-style": ["error", "prefer-top-level"], 78 | }, 79 | }, 80 | { 81 | linterOptions: { reportUnusedDisableDirectives: true }, 82 | languageOptions: { parserOptions: { projectService: true } }, 83 | }, 84 | ); 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

SORA

4 | 5 | [![Lint, TS, Prettier Check](https://github.com/reacto11mecha/sora/actions/workflows/ci.yml/badge.svg)](https://github.com/reacto11mecha/sora/actions/workflows/ci.yml) 6 |

7 | 8 | ᮞᮧᮛ (Sora) yang berarti suara adalah aplikasi yang dapat menyuarakan aspirasi masyarakat untuk memilih kandidat pemimpin yang baru. 9 | 10 | Project ini adalah hasil rebuild [NVA13](https://github.com/sekilas13/nva13) yang awalnya dibuat dengan [Node.js](https://nodejs.org/en/) dengan templating engine [EJS](https://ejs.co/) digantikan dengan [Next.js](https://nextjs.org/) yang lebih modular. Tujuan utama dari aplikasi ini untuk mengurangi biaya karena penggunaan kertas dan juga waktu penghitungan yang manual. 11 | 12 | Penjelasan penggunaan yang *production ready*, anda dapat mengunjungi web dokumentasi [sora baseline berikut ini](https://sora.rmecha.my.id/panduan/baseline/web-admin/prasyarat/). Penasaran dengan asal usul projek ini? Anda dapat mengunjungi https://sora.rmecha.my.id/perkenalan/ untuk mendapatkan penjelasan lengkapnya. 13 | 14 | ## Local Development 15 | 16 | Langkah pertama, fork atau clone terlebih dahulu. 17 | 18 | ```sh 19 | # HTTPS 20 | git clone https://github.com/reacto11mecha/sora.git 21 | 22 | # SSH 23 | git clone git@github.com:reacto11mecha/sora.git 24 | ``` 25 | 26 | Kedua, menginstall seluruh package yang dibutuhkan. 27 | 28 | ```sh 29 | yarn 30 | ``` 31 | 32 | Ketiga, menyalin file `env.example` menjadi `.env` dan isikan sesuai field yang telah dijelaskan sebelumnya di [Buat file](#buat-file). 33 | 34 | Setelah menginstall dependensi yang diperlukan, jalankan database MySQL bersamaan dengan RabbitMQ. Karena ada empat hal yang bisa di develop, maka script development ada empat. Berikut ini penjelasannya. 35 | 36 | - Develop sisi web 37 | 38 | ``` 39 | yarn dev:web 40 | ``` 41 | 42 | - Develop vote processor (RabbitMQ Consumer) 43 | 44 | ``` 45 | yarn dev:processor 46 | ``` 47 | 48 | - Develop sisi web presensi (attendance) 49 | 50 | ``` 51 | yarn dev:attendance 52 | ``` 53 | 54 | - Develop sisi web pemilih (chooser) 55 | 56 | ``` 57 | yarn dev:chooser 58 | ``` 59 | 60 | ## Ucapan Terimakasih 61 | 62 | Saya sebelumnya berterima kasih kepada tim [t3-oss](https://github.com/t3-oss) yang sudah membuat [`create-t3-app`](https://github.com/t3-oss/create-t3-app) dan [`create-t3-turbo`](https://github.com/t3-oss/create-t3-turbo) karena project ini menggunakan template mereka yang sudah membantu saya dalam pembuatan project ini. 63 | 64 | Saya juga berterima kasih terhadap MPK (Majelis Permusyawaratan Kelas) SMA Negeri 12 Kota Bekasi yang mau dan percaya untuk menggunakan aplikasi ini. Banyak kritik dan saran dari pihak guru dan murid-murid yang akhirnya terciptalah versi kedua dari project ini. 65 | 66 | ## Disclaimer 67 | 68 | Penegasan, **saya tidak bertanggung jawab atas hal-hal yang tidak anda inginkan, gunakan dengan bijak dan tepat!** 69 | 70 | ## Lisensi 71 | 72 | Semua kode yang ada di repositori ini bernaung dibawah [GPLv3](LICENSE). 73 | -------------------------------------------------------------------------------- /packages/db/src/client.ts: -------------------------------------------------------------------------------- 1 | import { and, count, eq, gt, sql } from "drizzle-orm"; 2 | import { drizzle } from "drizzle-orm/mysql2"; 3 | import mysql from "mysql2/promise"; 4 | 5 | import { connectionStr } from "./config"; 6 | import * as schema from "./schema/main"; 7 | 8 | const poolConnection = mysql.createPool(connectionStr.toString()); 9 | 10 | export const db = drizzle(poolConnection, { 11 | schema, 12 | mode: "default", 13 | }); 14 | 15 | // Prepared statement stuff 16 | export const preparedGetUserByEmail = db.query.users 17 | .findFirst({ 18 | where: eq(schema.users.email, sql.placeholder("email")), 19 | }) 20 | .prepare(); 21 | 22 | export const countUserTable = db 23 | .select({ count: sql`count(*)`.mapWith(Number) }) 24 | .from(schema.users) 25 | .prepare(); 26 | 27 | export const preparedGetAllParticipants = db.query.participants 28 | .findMany({ 29 | columns: { 30 | id: false, 31 | }, 32 | }) 33 | .prepare(); 34 | 35 | export const preparedGetExcelParticipants = db.query.participants 36 | .findMany({ 37 | columns: { 38 | name: true, 39 | qrId: true, 40 | subpart: true, 41 | }, 42 | }) 43 | .prepare(); 44 | 45 | export const preparedAdminGetCandidates = db.query.candidates 46 | .findMany() 47 | .prepare(); 48 | 49 | export const preparedGetCandidateCountsOnly = db.query.candidates 50 | .findMany({ 51 | columns: { 52 | counter: true, 53 | }, 54 | where: gt(schema.candidates.counter, 0), 55 | }) 56 | .prepare(); 57 | 58 | export const preparedGetAttendedAndVoted = db 59 | .select({ count: count() }) 60 | .from(schema.participants) 61 | .where( 62 | and( 63 | eq(schema.participants.alreadyAttended, true), 64 | eq(schema.participants.alreadyChoosing, true), 65 | ), 66 | ) 67 | .prepare(); 68 | 69 | export const preparedGetGraphicalData = db.query.candidates 70 | .findMany({ 71 | columns: { 72 | name: true, 73 | counter: true, 74 | }, 75 | }) 76 | .prepare(); 77 | 78 | export const preparedGetParticipantAttended = db.query.participants 79 | .findFirst({ 80 | where: eq(schema.participants.qrId, sql.placeholder("qrId")), 81 | 82 | columns: { 83 | alreadyAttended: true, 84 | alreadyChoosing: true, 85 | }, 86 | }) 87 | .prepare(); 88 | 89 | export const preparedGetParticipantStatus = db.query.participants 90 | .findFirst({ 91 | where: eq(schema.participants.qrId, sql.placeholder("qrId")), 92 | 93 | columns: { 94 | name: true, 95 | subpart: true, 96 | alreadyAttended: true, 97 | alreadyChoosing: true, 98 | }, 99 | }) 100 | .prepare(); 101 | 102 | export const preparedGetCandidates = db.query.candidates 103 | .findMany({ 104 | columns: { 105 | counter: false, 106 | }, 107 | }) 108 | .prepare(); 109 | // 110 | // export const preparedGetParticipantsStatistic = db.query.participants 111 | // .findMany({ 112 | // columns: { 113 | // id: false, 114 | // }, 115 | // }) 116 | // .prepare(); 117 | -------------------------------------------------------------------------------- /packages/validators/src/candidate.ts: -------------------------------------------------------------------------------- 1 | import { Base64 } from "js-base64"; 2 | import { z } from "zod"; 3 | 4 | import { validateId } from "@sora-vp/id-generator"; 5 | 6 | const TwoMegs = 2_000_000; 7 | const ACCEPTED_IMAGE_TYPES = [ 8 | "image/jpeg", 9 | "image/jpg", 10 | "image/png", 11 | "image/webp", 12 | ]; 13 | 14 | const id = z.number().min(1); 15 | const baseAddAndEditForm = z.object({ 16 | name: z.string().min(1, { message: "Diperlukan nama kandidat!" }), 17 | }); 18 | 19 | const AddNewCandidateSchema = baseAddAndEditForm.merge( 20 | z.object({ 21 | image: z 22 | .any() 23 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access 24 | .refine((files) => files?.length == 1, "Diperlukan gambar kandidat!") 25 | .refine( 26 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access 27 | (files) => files?.[0]?.size <= TwoMegs, 28 | `Ukuran maksimal gambar adalah 2MB!`, 29 | ) 30 | .refine( 31 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access 32 | (files) => ACCEPTED_IMAGE_TYPES.includes(files?.[0]?.type), 33 | "Hanya format gambar .jpg, .jpeg, .png dan .webp yang diterima!", 34 | ), 35 | }), 36 | ); 37 | 38 | const ServerAddNewCandidate = baseAddAndEditForm.merge( 39 | z.object({ 40 | image: z.string().refine(Base64.isValid), 41 | type: z.string(), 42 | }), 43 | ); 44 | 45 | const UpdateCandidateSchema = baseAddAndEditForm.merge( 46 | z.object({ 47 | image: z 48 | .any() 49 | .refine( 50 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access 51 | (files) => (files.length === 0 ? true : files?.length === 1), 52 | "Diperlukan gambar kandidat!", 53 | ) 54 | .refine( 55 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access 56 | (files) => (files.length === 0 ? true : files?.[0]?.size <= TwoMegs), 57 | `Ukuran maksimal gambar adalah 2MB!`, 58 | ) 59 | .refine( 60 | (files) => 61 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access 62 | files.length === 0 63 | ? true 64 | : // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument 65 | ACCEPTED_IMAGE_TYPES.includes(files?.[0]?.type), 66 | "Hanya format gambar .jpg, .jpeg, .png dan .webp yang diterima!", 67 | ), 68 | }), 69 | ); 70 | 71 | const ServerUpdateCandidate = baseAddAndEditForm.merge( 72 | z.object({ 73 | id, 74 | image: z.optional(z.string().refine(Base64.isValid)), 75 | type: z.optional(z.string()), 76 | }), 77 | ); 78 | 79 | const ServerDeleteCandidate = z.object({ id }); 80 | 81 | const ServerUpvoteCandidate = ServerDeleteCandidate.merge( 82 | z.object({ 83 | qrId: z.string().refine(validateId), 84 | }), 85 | ); 86 | 87 | export const candidate = { 88 | AddNewCandidateSchema, 89 | ServerAddNewCandidate, 90 | UpdateCandidateSchema, 91 | ServerUpdateCandidate, 92 | ServerDeleteCandidate, 93 | ServerUpvoteCandidate, 94 | } as const; 95 | -------------------------------------------------------------------------------- /apps/web/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20 AS base 2 | # LABEL fly_launch_runtime="Next.js" 3 | ENV NEXT_TELEMETRY_DISABLED=1 4 | # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. 5 | 6 | RUN < package.json 51 | echo "nodeLinker: node-modules" > .yarnrc.yml 52 | yarn set version stable 53 | yarn add ./out.tgz 54 | EOF 55 | 56 | # This is the fly.io release_command that performs a migration 57 | COPY < void; 15 | 16 | export interface IHardwareWebsocket { 17 | wsEnabled: boolean; 18 | subscribe(callbacK: THardwareWebsocketCallback): () => void; 19 | } 20 | 21 | export const HardwareWebsocketContext = createContext( 22 | {} as IHardwareWebsocket, 23 | ); 24 | 25 | export const HardwareWebsocketProvider = ({ 26 | children, 27 | }: { 28 | children: React.ReactNode; 29 | }) => { 30 | const currentSubscriberIdRef = useRef(0); 31 | const subscribersRef = useRef>( 32 | new Map(), 33 | ); 34 | 35 | const wsPortNumber = useAtomValue(defaultWSPortAtom); 36 | const wsEnabled = useAtomValue(enableWSConnectionAtom); 37 | 38 | const { lastMessage, readyState } = useWebSocket( 39 | wsEnabled ? `ws://127.0.0.1:${wsPortNumber}/ws` : null, 40 | { 41 | share: true, 42 | shouldReconnect: () => true, 43 | retryOnError: true, 44 | reconnectInterval: 5000, 45 | reconnectAttempts: Infinity, 46 | onError(event) { 47 | console.log(event); 48 | }, 49 | }, 50 | ); 51 | 52 | const subscribe = useCallback((callback: THardwareWebsocketCallback) => { 53 | const id = currentSubscriberIdRef.current; 54 | subscribersRef.current.set(id, callback); 55 | currentSubscriberIdRef.current++; 56 | 57 | return () => { 58 | subscribersRef.current.delete(id); 59 | }; 60 | }, []); 61 | 62 | useEffect(() => { 63 | if (lastMessage) { 64 | Array.from(subscribersRef.current).forEach(([, callback]) => { 65 | callback(lastMessage.data); 66 | }); 67 | } 68 | }, [lastMessage]); 69 | 70 | useEffect(() => { 71 | if (wsEnabled) { 72 | switch (readyState) { 73 | case ReadyState.CONNECTING: { 74 | toast.info("Sedang menghubungkan dengan modul tombol..."); 75 | 76 | break; 77 | } 78 | 79 | case ReadyState.CLOSED: { 80 | toast.error("Koneksi ditutup oleh modul tombol"); 81 | 82 | break; 83 | } 84 | 85 | case ReadyState.CLOSING: { 86 | toast.info("Menutup koneksi tombol..."); 87 | 88 | break; 89 | } 90 | 91 | case ReadyState.OPEN: { 92 | toast.success("Berhasil terhubung ke modul tombol!"); 93 | 94 | break; 95 | } 96 | } 97 | } 98 | }, [readyState, wsEnabled]); 99 | 100 | return ( 101 | 107 | {children} 108 | 109 | ); 110 | }; 111 | 112 | export const useHardwareWebsocket = () => 113 | useContext(HardwareWebsocketContext) as IHardwareWebsocket; 114 | -------------------------------------------------------------------------------- /packages/ui/src/table.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@sora-vp/ui"; 4 | 5 | const Table = React.forwardRef< 6 | HTMLTableElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
10 | 15 | 16 | )); 17 | Table.displayName = "Table"; 18 | 19 | const TableHeader = React.forwardRef< 20 | HTMLTableSectionElement, 21 | React.HTMLAttributes 22 | >(({ className, ...props }, ref) => ( 23 | 24 | )); 25 | TableHeader.displayName = "TableHeader"; 26 | 27 | const TableBody = React.forwardRef< 28 | HTMLTableSectionElement, 29 | React.HTMLAttributes 30 | >(({ className, ...props }, ref) => ( 31 | 36 | )); 37 | TableBody.displayName = "TableBody"; 38 | 39 | const TableFooter = React.forwardRef< 40 | HTMLTableSectionElement, 41 | React.HTMLAttributes 42 | >(({ className, ...props }, ref) => ( 43 | tr]:last:border-b-0", 47 | className, 48 | )} 49 | {...props} 50 | /> 51 | )); 52 | TableFooter.displayName = "TableFooter"; 53 | 54 | const TableRow = React.forwardRef< 55 | HTMLTableRowElement, 56 | React.HTMLAttributes 57 | >(({ className, ...props }, ref) => ( 58 | 66 | )); 67 | TableRow.displayName = "TableRow"; 68 | 69 | const TableHead = React.forwardRef< 70 | HTMLTableCellElement, 71 | React.ThHTMLAttributes 72 | >(({ className, ...props }, ref) => ( 73 |
[role=checkbox]]:translate-y-[2px]", 77 | className, 78 | )} 79 | {...props} 80 | /> 81 | )); 82 | TableHead.displayName = "TableHead"; 83 | 84 | const TableCell = React.forwardRef< 85 | HTMLTableCellElement, 86 | React.TdHTMLAttributes 87 | >(({ className, ...props }, ref) => ( 88 | [role=checkbox]]:translate-y-[2px]", 92 | className, 93 | )} 94 | {...props} 95 | /> 96 | )); 97 | TableCell.displayName = "TableCell"; 98 | 99 | const TableCaption = React.forwardRef< 100 | HTMLTableCaptionElement, 101 | React.HTMLAttributes 102 | >(({ className, ...props }, ref) => ( 103 |
108 | )); 109 | TableCaption.displayName = "TableCaption"; 110 | 111 | export { 112 | Table, 113 | TableHeader, 114 | TableBody, 115 | TableFooter, 116 | TableHead, 117 | TableRow, 118 | TableCell, 119 | TableCaption, 120 | }; 121 | -------------------------------------------------------------------------------- /turbo/generators/config.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from "node:child_process"; 2 | import path from "node:path"; 3 | import type { PlopTypes } from "@turbo/gen"; 4 | 5 | interface PackageJson { 6 | name: string; 7 | scripts: Record; 8 | dependencies: Record; 9 | devDependencies: Record; 10 | } 11 | 12 | const baseDir = path.join(__dirname, "../.."); 13 | 14 | export default function generator(plop: PlopTypes.NodePlopAPI): void { 15 | plop.setGenerator("init", { 16 | description: "Generate a new package for the Sora Baseline Monorepo", 17 | prompts: [ 18 | { 19 | type: "input", 20 | name: "name", 21 | message: 22 | "What is the name of the package? (You can skip the `@sora-vp/` prefix)", 23 | }, 24 | { 25 | type: "input", 26 | name: "deps", 27 | message: 28 | "Enter a space separated list of dependencies you would like to install", 29 | }, 30 | ], 31 | actions: [ 32 | (answers) => { 33 | if ("name" in answers && typeof answers.name === "string") { 34 | if (answers.name.startsWith("@sora-vp/")) { 35 | answers.name = answers.name.replace("@sora-vp/", ""); 36 | } 37 | } 38 | return "Config sanitized"; 39 | }, 40 | { 41 | type: "add", 42 | path: "packages/{{ name }}/eslint.config.js", 43 | templateFile: "templates/eslint.config.js.hbs", 44 | }, 45 | { 46 | type: "add", 47 | path: "packages/{{ name }}/package.json", 48 | templateFile: "templates/package.json.hbs", 49 | }, 50 | { 51 | type: "add", 52 | path: "packages/{{ name }}/tsconfig.json", 53 | templateFile: "templates/tsconfig.json.hbs", 54 | }, 55 | { 56 | type: "add", 57 | path: "packages/{{ name }}/src/index.ts", 58 | template: "export const name = '{{ name }}';", 59 | }, 60 | { 61 | type: "modify", 62 | path: "packages/{{ name }}/package.json", 63 | async transform(content, answers) { 64 | if ("deps" in answers && typeof answers.deps === "string") { 65 | const pkg = JSON.parse(content) as PackageJson; 66 | for (const dep of answers.deps.split(" ").filter(Boolean)) { 67 | const version = await fetch( 68 | `https://registry.npmjs.org/-/package/${dep}/dist-tags`, 69 | ) 70 | .then((res) => res.json()) 71 | .then((json) => json.latest); 72 | if (!pkg.dependencies) pkg.dependencies = {}; 73 | pkg.dependencies[dep] = `^${version}`; 74 | } 75 | return JSON.stringify(pkg, null, 2); 76 | } 77 | return content; 78 | }, 79 | }, 80 | async (answers) => { 81 | /** 82 | * Install deps and format everything 83 | */ 84 | if ("name" in answers && typeof answers.name === "string") { 85 | // execSync("pnpm dlx sherif@latest --fix", { 86 | // stdio: "inherit", 87 | // }); 88 | execSync("yarn", { stdio: "inherit", cwd: baseDir }); 89 | execSync( 90 | `yarn prettier --write packages/${answers.name}/** --list-different`, 91 | { 92 | cwd: baseDir, 93 | }, 94 | ); 95 | return "Package scaffolded"; 96 | } 97 | return "Package not scaffolded"; 98 | }, 99 | ], 100 | }); 101 | } 102 | --------------------------------------------------------------------------------