├── .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 | [](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