├── .env.example ├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── feature_request.yml ├── .gitignore ├── .npmrc ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── apps └── www │ ├── .gitignore │ ├── env.ts │ ├── eslint.config.js │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── postcss.config.js │ ├── public │ ├── ads.txt │ └── images │ │ ├── bg.svg │ │ ├── cvsu-front.jpg │ │ ├── default-avatar.png │ │ ├── faq.svg │ │ └── logo.png │ ├── sentry.client.config.js │ ├── sentry.edge.config.js │ ├── sentry.server.config.js │ ├── src │ ├── app │ │ ├── (main) │ │ │ ├── (auth) │ │ │ │ ├── layout.tsx │ │ │ │ ├── register │ │ │ │ │ └── page.tsx │ │ │ │ └── sign-in │ │ │ │ │ └── page.tsx │ │ │ ├── (legal) │ │ │ │ ├── cookie │ │ │ │ │ └── page.tsx │ │ │ │ ├── disclaimer │ │ │ │ │ └── page.tsx │ │ │ │ ├── layout.tsx │ │ │ │ ├── privacy │ │ │ │ │ └── page.tsx │ │ │ │ └── terms │ │ │ │ │ └── page.tsx │ │ │ ├── [electionSlug] │ │ │ │ ├── (main) │ │ │ │ │ ├── layout.tsx │ │ │ │ │ ├── loading.tsx │ │ │ │ │ └── page.tsx │ │ │ │ ├── [candidateSlug] │ │ │ │ │ ├── loading.tsx │ │ │ │ │ └── page.tsx │ │ │ │ ├── realtime │ │ │ │ │ ├── loading.tsx │ │ │ │ │ └── page.tsx │ │ │ │ └── vote │ │ │ │ │ ├── loading.tsx │ │ │ │ │ └── page.tsx │ │ │ ├── account │ │ │ │ ├── layout.tsx │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ ├── contact │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── page.tsx │ │ │ └── pricing │ │ │ │ └── page.tsx │ │ ├── (private) │ │ │ └── dashboard │ │ │ │ ├── [electionDashboardSlug] │ │ │ │ ├── candidate │ │ │ │ │ ├── loading.tsx │ │ │ │ │ └── page.tsx │ │ │ │ ├── layout.tsx │ │ │ │ ├── loading.tsx │ │ │ │ ├── page.tsx │ │ │ │ ├── partylist │ │ │ │ │ ├── loading.tsx │ │ │ │ │ └── page.tsx │ │ │ │ ├── position │ │ │ │ │ ├── loading.tsx │ │ │ │ │ └── page.tsx │ │ │ │ ├── settings │ │ │ │ │ ├── loading.tsx │ │ │ │ │ └── page.tsx │ │ │ │ └── voter │ │ │ │ │ ├── loading.tsx │ │ │ │ │ └── page.tsx │ │ │ │ └── page.tsx │ │ ├── api │ │ │ ├── auth │ │ │ │ ├── callback │ │ │ │ │ └── route.ts │ │ │ │ └── magic-link │ │ │ │ │ └── route.ts │ │ │ ├── billing │ │ │ │ └── webhook │ │ │ │ │ └── route.ts │ │ │ ├── inngest │ │ │ │ └── route.ts │ │ │ ├── og │ │ │ │ └── route.tsx │ │ │ └── trpc │ │ │ │ └── [trpc] │ │ │ │ └── route.ts │ │ ├── apple-touch-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon.ico │ │ ├── global-error.tsx │ │ ├── layout.tsx │ │ ├── manifest.ts │ │ ├── not-found.tsx │ │ ├── opengraph-image.png │ │ ├── robots.txt │ │ └── twitter-image.png │ ├── components │ │ ├── ad-modal.tsx │ │ ├── boost-card.tsx │ │ ├── contact-form.tsx │ │ ├── dashboard-card.tsx │ │ ├── elections-left.tsx │ │ ├── footer.tsx │ │ ├── generated-result-row.tsx │ │ ├── header.tsx │ │ ├── key-features.tsx │ │ ├── layout │ │ │ ├── account.tsx │ │ │ ├── dashboard-election.tsx │ │ │ └── dashboard.tsx │ │ ├── modals │ │ │ ├── create-candidate.tsx │ │ │ ├── create-election.tsx │ │ │ ├── create-partylist.tsx │ │ │ ├── create-position.tsx │ │ │ ├── create-voter.tsx │ │ │ ├── dashboard-show-qr-code.tsx │ │ │ ├── delete-bulk-voter.tsx │ │ │ ├── delete-candidate.tsx │ │ │ ├── delete-partylist.tsx │ │ │ ├── delete-position.tsx │ │ │ ├── delete-voter.tsx │ │ │ ├── edit-candidate.tsx │ │ │ ├── edit-partylist.tsx │ │ │ ├── edit-position.tsx │ │ │ ├── edit-voter.tsx │ │ │ ├── election-boost.tsx │ │ │ ├── election-show-qr-code.tsx │ │ │ ├── message-commissioner.tsx │ │ │ ├── qr-code.tsx │ │ │ ├── update-voter-field.tsx │ │ │ └── upload-bulk-voter.tsx │ │ ├── my-elections.tsx │ │ ├── my-messages-election.tsx │ │ ├── pages │ │ │ ├── account.tsx │ │ │ ├── dashboard-candidate.tsx │ │ │ ├── dashboard-overview.tsx │ │ │ ├── dashboard-partylist.tsx │ │ │ ├── dashboard-position.tsx │ │ │ ├── dashboard-settings.tsx │ │ │ ├── dashboard-voter.tsx │ │ │ ├── election-candidate.tsx │ │ │ ├── election-page.tsx │ │ │ └── realtime.tsx │ │ ├── providers.tsx │ │ ├── public-elections.tsx │ │ ├── react-player.tsx │ │ ├── register-form.tsx │ │ ├── scroll-to-top.tsx │ │ ├── signin-form.tsx │ │ └── vote-form.tsx │ ├── config │ │ └── site.tsx │ ├── middleware.ts │ ├── pdf │ │ └── generate-result.tsx │ ├── store.ts │ ├── styles │ │ ├── Candidate.module.css │ │ ├── Dashboard.module.css │ │ ├── Election.module.css │ │ ├── Header.module.css │ │ ├── Home.module.css │ │ ├── NotFound.module.css │ │ ├── Partylist.module.css │ │ ├── Position.module.css │ │ └── Pricing.module.css │ ├── supabase │ │ ├── admin.ts │ │ ├── client.ts │ │ ├── middleware.ts │ │ └── server.ts │ ├── trpc │ │ ├── client.tsx │ │ ├── query-client.ts │ │ └── server.ts │ └── utils │ │ ├── api.ts │ │ ├── index.tsx │ │ └── toWords.ts │ └── tsconfig.json ├── package.json ├── packages ├── api │ ├── eslint.config.js │ ├── index.ts │ ├── package.json │ ├── src │ │ ├── root.ts │ │ ├── router │ │ │ ├── auth.ts │ │ │ ├── candidate.ts │ │ │ ├── election.ts │ │ │ ├── partylist.ts │ │ │ ├── payment.ts │ │ │ ├── position.ts │ │ │ ├── system.ts │ │ │ ├── user.ts │ │ │ └── voter.ts │ │ └── trpc.ts │ └── tsconfig.json ├── constants │ ├── index.ts │ ├── package.json │ └── tsconfig.json ├── email │ ├── emails │ │ ├── election-result.tsx │ │ ├── election-start.tsx │ │ └── vote-casted.tsx │ ├── eslint.config.js │ ├── index.ts │ ├── package.json │ └── tsconfig.json ├── inngest │ ├── env.ts │ ├── functions │ │ ├── election-end.ts │ │ └── election-start.ts │ ├── index.ts │ ├── package.json │ └── tsconfig.json └── payment │ ├── index.ts │ ├── package.json │ └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── supabase ├── .gitignore ├── config.toml ├── custom-types.ts ├── migrations │ ├── 20240330115109_remote_schema.sql │ ├── 20240330160754_init.sql │ ├── 20240331075949_handle_new_user.sql │ ├── 20240331082422_variant_id_int4.sql │ ├── 20240331131939_users_select_rls.sql │ ├── 20240401141151_storage_buckets.sql │ ├── 20240407085300_elections_no_of_voters.sql │ ├── 20240413022850_add_indexes.sql │ └── 20240601132906_elections_description.sql ├── schema.sql ├── seed.sql ├── seed.ts ├── templates │ └── magic-link.html └── types.ts ├── tooling ├── eslint │ ├── base.js │ ├── nextjs.js │ ├── package.json │ ├── react.js │ ├── tsconfig.json │ └── types.d.ts ├── prettier │ ├── index.js │ ├── package.json │ └── tsconfig.json └── tsconfig │ ├── base.json │ ├── internal-package.json │ └── package.json └── turbo.json /.env.example: -------------------------------------------------------------------------------- 1 | APP_URL='http://localhost:3000' 2 | 3 | NEXT_PUBLIC_SUPABASE_URL= 4 | NEXT_PUBLIC_SUPABASE_ANON_KEY= 5 | SUPABASE_SERVICE_ROLE_KEY= 6 | 7 | VERCEL_WEB_ANALYTICS_ID= 8 | 9 | EMAIL_FROM= 10 | 11 | AWS_SES_REGION= 12 | AWS_ACCESS_KEY_ID= 13 | AWS_SECRET_ACCESS_KEY= 14 | 15 | SMTP_USER= 16 | SMTP_PASSWORD= 17 | SMTP_HOST= 18 | SMTP_PORT= 19 | 20 | DISCORD_WEBHOOK_URL= 21 | 22 | LEMONSQUEEZY_API_KEY= 23 | 24 | LEMONSQUEEZY_STORE_ID= 25 | LEMONSQUEEZY_FREE_VARIANT_ID= 26 | LEMONSQUEEZY_BOOST_PRODUCT_ID= 27 | LEMONSQUEEZY_PLUS_VARIANT_ID= 28 | LEMONSQUEEZY_WEBHOOK_SECRET= 29 | 30 | INNGEST_SIGNING_KEY= 31 | INNGEST_EVENT_KEY= 32 | 33 | NEXT_PUBLIC_POSTHOG_KEY= 34 | NEXT_PUBLIC_POSTHOG_HOST= -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: bricesuazo 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: 🐞 Bug Report 2 | description: Create a bug report to help us improve 3 | title: "bug: " 4 | labels: ["🐞❔ unconfirmed bug"] 5 | body: 6 | - type: textarea 7 | attributes: 8 | label: Provide environment information 9 | description: | 10 | Run this command in your project root and paste the results in a code block: 11 | ```bash 12 | npx envinfo --system --binaries 13 | ``` 14 | validations: 15 | required: true 16 | - type: textarea 17 | attributes: 18 | label: Describe the bug 19 | description: A clear and concise description of the bug, as well as what you expected to happen when encountering it. 20 | validations: 21 | required: true 22 | - type: input 23 | attributes: 24 | label: Link to reproduction 25 | description: Please provide a link to a reproduction of the bug. Issues without a reproduction repo may be ignored. 26 | validations: 27 | required: true 28 | - type: textarea 29 | attributes: 30 | label: To reproduce 31 | description: Describe how to reproduce your bug. Steps, code snippets, reproduction repos etc. 32 | validations: 33 | required: true 34 | - type: textarea 35 | attributes: 36 | label: Additional information 37 | description: Add any other information related to the bug here, screenshots if applicable. 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | # This template is heavily inspired by the Next.js's template: 2 | # See here: https://github.com/vercel/next.js/blob/canary/.github/ISSUE_TEMPLATE/3.feature_request.yml 3 | 4 | name: 🛠 Feature Request 5 | description: Create a feature request for the core packages 6 | title: 'feat: ' 7 | labels: ['✨ enhancement'] 8 | body: 9 | - type: markdown 10 | attributes: 11 | value: | 12 | Thank you for taking the time to file a feature request. Please fill out this form as completely as possible. 13 | - type: textarea 14 | attributes: 15 | label: Describe the feature you'd like to request 16 | description: Please describe the feature as clear and concise as possible. Remember to add context as to why you believe this feature is needed. 17 | validations: 18 | required: true 19 | - type: textarea 20 | attributes: 21 | label: Describe the solution you'd like to see 22 | description: Please describe the solution you would like to see. Adding example usage is a good way to provide context. 23 | validations: 24 | required: true 25 | - type: textarea 26 | attributes: 27 | label: Additional information 28 | description: Add any other information related to the feature here. If your feature request is related to any issues or discussions, link them here. 29 | 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # testing 9 | coverage 10 | 11 | # next.js 12 | .next/ 13 | out/ 14 | build 15 | 16 | # misc 17 | .DS_Store 18 | *.pem 19 | 20 | # debug 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # local env files 26 | .env 27 | .env.local 28 | .env.production 29 | .env.development 30 | 31 | # turbo 32 | .turbo 33 | 34 | # vercel 35 | .vercel 36 | .env*.local 37 | 38 | .react-email 39 | 40 | dist 41 | .cache -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers = true -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "expo.vscode-expo-tools", 5 | "esbenp.prettier-vscode", 6 | "yoavbls.pretty-ts-errors", 7 | "bradlc.vscode-tailwindcss" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.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/www/", 10 | "skipFiles": ["/**"] 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "git.ignoreLimitWarning": true, 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll.eslint": "explicit" 5 | }, 6 | "editor.defaultFormatter": "esbenp.prettier-vscode", 7 | "editor.formatOnSave": true, 8 | "eslint.rules.customizations": [{ "rule": "*", "severity": "warn" }], 9 | "eslint.useFlatConfig": true, 10 | "eslint.workingDirectories": [ 11 | { "pattern": "apps/*/" }, 12 | { "pattern": "packages/*/" }, 13 | { "pattern": "tooling/*/" } 14 | ], 15 | "tailwindCSS.experimental.classRegex": [ 16 | ["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"], 17 | ["cx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"] 18 | ], 19 | "tailwindCSS.experimental.configFile": "./tooling/tailwind/base.ts", 20 | "typescript.enablePromptUseWorkspaceTsdk": true, 21 | "typescript.preferences.autoImportFileExcludePatterns": [ 22 | "next/router.d.ts", 23 | "next/dist/client/router.d.ts" 24 | ], 25 | "typescript.tsdk": "node_modules/typescript/lib" 26 | } 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/bricesuazo/eboto) 2 | 3 | # [eBoto](https://eboto.app) - Your One-Stop Online Voting Solution 4 | 5 | Empower your elections with eBoto, the versatile and web-based voting platform that offers secure online elections for any type of organization. 6 | 7 | - **Monorepo**: [TurboRepo](https://turbo.build/) 8 | - **Framework**: [Next.js](https://nextjs.org/) + [Typescript](https://www.typescriptlang.org/) 9 | - **Database, Storage, and Authentication**: [Supabase](https://supabase.com/) 10 | - **API**: [tRPC](https://trpc.io/) + [TanStack Query](https://tanstack.com/query/) 11 | - **Payment**: [Lemon Squeezy](https://www.lemonsqueezy.com/) 12 | - **Deployment**: [Vercel](https://vercel.com) 13 | - **Styling**: [Mantine](https://mantine.dev/) 14 | - **Email**: [React Email](https://react.email/) + [Amazon SES](https://aws.amazon.com/ses/) 15 | - **Data Validator**: [Zod](https://zod.dev/) 16 | - **Analytics**: [PostHog](https://posthog.com/) 17 | 18 | ## Running Locally 19 | 20 | ### For the [Web](/apps/www) Version 21 | 22 | ```bash 23 | git clone https://github.com/bricesuazo/eboto.git 24 | cd eboto 25 | pnpm install 26 | pnpm run dev 27 | ``` 28 | 29 | Create a `.env` file similar to [`.env.example`](.env.example). 30 | 31 | ### For the [Mobile](/apps/mobile) Version 32 | 33 | ```bash 34 | Coming soon... 35 | ``` 36 | 37 | ### For the [Local](/apps/local) Version 38 | 39 | ```bash 40 | Coming soon... 41 | ``` 42 | 43 | ## License 44 | 45 | This project is licensed under the GNU Affero General Public License v3.0 License - see the [LICENSE](LICENSE) file for details. 46 | -------------------------------------------------------------------------------- /apps/www/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /apps/www/env.ts: -------------------------------------------------------------------------------- 1 | import { createEnv } from "@t3-oss/env-nextjs"; 2 | import { vercel } from "@t3-oss/env-nextjs/presets-zod"; 3 | import { z } from "zod"; 4 | 5 | export const env = createEnv({ 6 | extends: [vercel()], 7 | shared: { 8 | NODE_ENV: z.enum(["development", "test", "production"]), 9 | PORT: z.coerce.number().default(3000), 10 | NEXT_PUBLIC_SUPABASE_URL: z.string().min(1).url(), 11 | NEXT_PUBLIC_SUPABASE_ANON_KEY: z.string().min(1), 12 | }, 13 | server: { 14 | APP_URL: z.string().url(), 15 | SUPABASE_SERVICE_ROLE_KEY: z.string().min(1), 16 | 17 | DISCORD_WEBHOOK_URL: z.string().min(1), 18 | 19 | INNGEST_SIGNING_KEY: z.string().min(1), 20 | INNGEST_EVENT_KEY: z.string().min(1), 21 | 22 | LEMONSQUEEZY_WEBHOOK_SECRET: z.string().min(1), 23 | LEMONSQUEEZY_STORE_ID: z.number().min(1), 24 | LEMONSQUEEZY_API_KEY: z.string().min(1), 25 | LEMONSQUEEZY_FREE_VARIANT_ID: z.number().min(1), 26 | LEMONSQUEEZY_BOOST_PRODUCT_ID: z.number().min(1), 27 | LEMONSQUEEZY_PLUS_VARIANT_ID: z.number().min(1), 28 | }, 29 | 30 | client: { 31 | NEXT_PUBLIC_SENTRY_DSN: z.string().min(1), 32 | NEXT_PUBLIC_POSTHOG_KEY: z.string().min(1), 33 | NEXT_PUBLIC_POSTHOG_HOST: z.string().min(1), 34 | }, 35 | 36 | runtimeEnv: { 37 | APP_URL: process.env.APP_URL, 38 | PORT: process.env.PORT, 39 | NODE_ENV: process.env.NODE_ENV, 40 | 41 | SUPABASE_SERVICE_ROLE_KEY: process.env.SUPABASE_SERVICE_ROLE_KEY, 42 | NEXT_PUBLIC_SUPABASE_URL: process.env.NEXT_PUBLIC_SUPABASE_URL, 43 | NEXT_PUBLIC_SUPABASE_ANON_KEY: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY, 44 | 45 | NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY, 46 | NEXT_PUBLIC_POSTHOG_HOST: process.env.NEXT_PUBLIC_POSTHOG_HOST, 47 | 48 | DISCORD_WEBHOOK_URL: process.env.DISCORD_WEBHOOK_URL, 49 | 50 | INNGEST_SIGNING_KEY: process.env.INNGEST_SIGNING_KEY, 51 | INNGEST_EVENT_KEY: process.env.INNGEST_EVENT_KEY, 52 | 53 | LEMONSQUEEZY_WEBHOOK_SECRET: process.env.LEMONSQUEEZY_WEBHOOK_SECRET, 54 | LEMONSQUEEZY_STORE_ID: parseInt(process.env.LEMONSQUEEZY_STORE_ID ?? "-1"), 55 | NEXT_PUBLIC_SENTRY_DSN: process.env.NEXT_PUBLIC_SENTRY_DSN, 56 | LEMONSQUEEZY_API_KEY: process.env.LEMONSQUEEZY_API_KEY, 57 | LEMONSQUEEZY_FREE_VARIANT_ID: parseInt( 58 | process.env.LEMONSQUEEZY_FREE_VARIANT_ID ?? "-1", 59 | ), 60 | LEMONSQUEEZY_BOOST_PRODUCT_ID: parseInt( 61 | process.env.LEMONSQUEEZY_BOOST_PRODUCT_ID ?? "-1", 62 | ), 63 | LEMONSQUEEZY_PLUS_VARIANT_ID: parseInt( 64 | process.env.LEMONSQUEEZY_PLUS_VARIANT_ID ?? "-1", 65 | ), 66 | }, 67 | }); 68 | -------------------------------------------------------------------------------- /apps/www/eslint.config.js: -------------------------------------------------------------------------------- 1 | import baseConfig, { restrictEnvAccess } from "@eboto/eslint-config/base"; 2 | import nextjsConfig from "@eboto/eslint-config/nextjs"; 3 | import reactConfig from "@eboto/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/www/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. 6 | -------------------------------------------------------------------------------- /apps/www/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | export default { 3 | reactStrictMode: true, 4 | transpilePackages: ["@eboto/api", "@eboto/email", "@eboto/inngest"], 5 | experimental: { 6 | optimizePackageImports: ["@mantine/core", "@mantine/hooks"], 7 | }, 8 | images: { 9 | // unoptimized: true, 10 | remotePatterns: [ 11 | { 12 | protocol: "https", 13 | hostname: "lh3.googleusercontent.com", 14 | }, 15 | { 16 | protocol: "http", 17 | hostname: "localhost", 18 | port: "54321", 19 | }, 20 | { 21 | protocol: "https", 22 | hostname: "ssczhefhijwasxhlzdez.supabase.co", 23 | }, 24 | ], 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /apps/www/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "www", 3 | "type": "module", 4 | "scripts": { 5 | "build": "pnpm with-env next build", 6 | "clean": "git clean -xdf .next .turbo node_modules", 7 | "dev": "pnpm with-env next dev --turbo", 8 | "lint": "eslint", 9 | "format": "prettier --check \"**/*.{js,cjs,mjs,ts,tsx,md,json}\"", 10 | "start": "pnpm with-env next start", 11 | "typecheck": "tsc --noEmit", 12 | "with-env": "dotenv -e ../../.env --" 13 | }, 14 | "dependencies": { 15 | "@alexandernanberg/react-pdf-renderer": "4.0.0-canary-5", 16 | "@ctrl/react-adsense": "1.8.0", 17 | "@eboto/api": "workspace:*", 18 | "@eboto/constants": "workspace:*", 19 | "@eboto/email": "workspace:*", 20 | "@eboto/inngest": "workspace:*", 21 | "@eboto/payment": "workspace:*", 22 | "@emotion/react": "catalog:", 23 | "@mantine/carousel": "catalog:", 24 | "@mantine/code-highlight": "catalog:", 25 | "@mantine/core": "catalog:", 26 | "@mantine/dates": "catalog:", 27 | "@mantine/dropzone": "catalog:", 28 | "@mantine/form": "catalog:", 29 | "@mantine/hooks": "catalog:", 30 | "@mantine/modals": "catalog:", 31 | "@mantine/notifications": "catalog:", 32 | "@mantine/nprogress": "catalog:", 33 | "@mantine/spotlight": "catalog:", 34 | "@mantine/tiptap": "catalog:", 35 | "@react-pdf/renderer": "4.2.3", 36 | "@sentry/nextjs": "9.3.0", 37 | "@supabase/ssr": "catalog:", 38 | "@supabase/supabase-js": "catalog:", 39 | "@t3-oss/env-nextjs": "catalog:", 40 | "@tabler/icons-react": "catalog:", 41 | "@tanstack/react-query": "catalog:", 42 | "@tanstack/react-query-devtools": "catalog:", 43 | "@tiptap/extension-link": "catalog:", 44 | "@tiptap/react": "catalog:", 45 | "@tiptap/starter-kit": "catalog:", 46 | "@trpc/client": "catalog:", 47 | "@trpc/react-query": "catalog:", 48 | "@trpc/server": "catalog:", 49 | "@vercel/analytics": "catalog:", 50 | "@vercel/speed-insights": "catalog:", 51 | "dayjs": "catalog:", 52 | "dotenv-cli": "catalog:", 53 | "embla-carousel-react": "catalog:", 54 | "inngest": "catalog:", 55 | "mantine-form-zod-resolver": "1.1.0", 56 | "mantine-react-table": "2.0.0-beta.0", 57 | "moment": "2.30.1", 58 | "next": "catalog:", 59 | "posthog-js": "catalog:", 60 | "qrcode.react": "4.2.0", 61 | "react": "catalog:", 62 | "react-canvas-confetti": "2.0.7", 63 | "react-dom": "catalog:", 64 | "react-player": "2.16.0", 65 | "react-wrap-balancer": "1.1.1", 66 | "read-excel-file": "5.8.6", 67 | "sharp": "0.33.5", 68 | "superjson": "catalog:", 69 | "to-words": "4.3.0", 70 | "uuid": "catalog:", 71 | "xlsx": "https://cdn.sheetjs.com/xlsx-0.19.3/xlsx-0.19.3.tgz", 72 | "zod": "catalog:", 73 | "zustand": "5.0.3" 74 | }, 75 | "devDependencies": { 76 | "@eboto/eslint-config": "workspace:*", 77 | "@eboto/prettier-config": "workspace:*", 78 | "@eboto/tsconfig": "workspace:*", 79 | "@types/node": "catalog:", 80 | "@types/react": "catalog:", 81 | "@types/react-dom": "catalog:", 82 | "@types/uuid": "catalog:", 83 | "encoding": "0.1.13", 84 | "postcss-preset-mantine": "1.17.0", 85 | "postcss-simple-vars": "7.0.1", 86 | "typescript": "catalog:" 87 | }, 88 | "prettier": "@eboto/prettier-config" 89 | } 90 | -------------------------------------------------------------------------------- /apps/www/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | "postcss-preset-mantine": {}, 4 | "postcss-simple-vars": { 5 | variables: { 6 | "mantine-breakpoint-xs": "36em", 7 | "mantine-breakpoint-sm": "48em", 8 | "mantine-breakpoint-md": "62em", 9 | "mantine-breakpoint-lg": "75em", 10 | "mantine-breakpoint-xl": "88em", 11 | }, 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /apps/www/public/ads.txt: -------------------------------------------------------------------------------- 1 | google.com, pub-8867310433048493, DIRECT, f08c47fec0942fa0 -------------------------------------------------------------------------------- /apps/www/public/images/bg.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/www/public/images/cvsu-front.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bricesuazo/eboto/ea6526b1c78954f21469f80513eb277a486dc47b/apps/www/public/images/cvsu-front.jpg -------------------------------------------------------------------------------- /apps/www/public/images/default-avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bricesuazo/eboto/ea6526b1c78954f21469f80513eb277a486dc47b/apps/www/public/images/default-avatar.png -------------------------------------------------------------------------------- /apps/www/public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bricesuazo/eboto/ea6526b1c78954f21469f80513eb277a486dc47b/apps/www/public/images/logo.png -------------------------------------------------------------------------------- /apps/www/sentry.client.config.js: -------------------------------------------------------------------------------- 1 | import * as Sentry from "@sentry/nextjs"; 2 | 3 | import { env } from "./env"; 4 | 5 | Sentry.init({ 6 | dsn: env.NEXT_PUBLIC_SENTRY_DSN, 7 | // Replay may only be enabled for the client-side 8 | integrations: [Sentry.replayIntegration()], 9 | 10 | // Set tracesSampleRate to 1.0 to capture 100% 11 | // of transactions for performance monitoring. 12 | // We recommend adjusting this value in production 13 | tracesSampleRate: 1.0, 14 | 15 | // Capture Replay for 10% of all sessions, 16 | // plus for 100% of sessions with an error 17 | replaysSessionSampleRate: 0.1, 18 | replaysOnErrorSampleRate: 1.0, 19 | 20 | // ... 21 | 22 | // Note: if you want to override the automatic release value, do not set a 23 | // `release` value here - use the environment variable `SENTRY_RELEASE`, so 24 | // that it will also get attached to your source maps 25 | }); 26 | -------------------------------------------------------------------------------- /apps/www/sentry.edge.config.js: -------------------------------------------------------------------------------- 1 | import * as Sentry from "@sentry/nextjs"; 2 | 3 | import { env } from "./env"; 4 | 5 | Sentry.init({ 6 | dsn: env.NEXT_PUBLIC_SENTRY_DSN, 7 | 8 | // Set tracesSampleRate to 1.0 to capture 100% 9 | // of transactions for performance monitoring. 10 | // We recommend adjusting this value in production 11 | tracesSampleRate: 1.0, 12 | 13 | // ... 14 | 15 | // Note: if you want to override the automatic release value, do not set a 16 | // `release` value here - use the environment variable `SENTRY_RELEASE`, so 17 | // that it will also get attached to your source maps 18 | }); 19 | -------------------------------------------------------------------------------- /apps/www/sentry.server.config.js: -------------------------------------------------------------------------------- 1 | import * as Sentry from "@sentry/nextjs"; 2 | 3 | import { env } from "./env"; 4 | 5 | Sentry.init({ 6 | dsn: env.NEXT_PUBLIC_SENTRY_DSN, 7 | 8 | // Set tracesSampleRate to 1.0 to capture 100% 9 | // of transactions for performance monitoring. 10 | // We recommend adjusting this value in production 11 | tracesSampleRate: 1.0, 12 | 13 | // ... 14 | 15 | // Note: if you want to override the automatic release value, do not set a 16 | // `release` value here - use the environment variable `SENTRY_RELEASE`, so 17 | // that it will also get attached to your source maps 18 | }); 19 | -------------------------------------------------------------------------------- /apps/www/src/app/(main)/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | import { Container } from "@mantine/core"; 3 | 4 | import { createClient } from "~/supabase/server"; 5 | 6 | export default async function AuthLayout(props: React.PropsWithChildren) { 7 | const supabase = await createClient(); 8 | const { 9 | data: { user }, 10 | } = await supabase.auth.getUser(); 11 | 12 | if (user) redirect("/dashboard"); 13 | 14 | return ( 15 | 16 | {props.children} 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /apps/www/src/app/(main)/(auth)/register/page.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import Link from "next/link"; 3 | import { Anchor, Text, Title } from "@mantine/core"; 4 | 5 | import RegisterForm from "~/components/register-form"; 6 | 7 | export const metadata: Metadata = { 8 | title: "Create an account", 9 | }; 10 | 11 | export default async function RegisterPage({ 12 | searchParams, 13 | }: { 14 | searchParams: Promise<{ 15 | next?: string; 16 | }>; 17 | }) { 18 | const { next } = await searchParams; 19 | 20 | return ( 21 | <> 22 | 23 | Create an account! 24 | 25 | 26 | 27 | Already have an account?{" "} 28 | 34 | Sign in 35 | 36 | 37 | 38 | 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /apps/www/src/app/(main)/(auth)/sign-in/page.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import Link from "next/link"; 3 | import { Anchor, Text, Title } from "@mantine/core"; 4 | 5 | import SigninForm from "~/components/signin-form"; 6 | 7 | export const metadata: Metadata = { 8 | title: "Sign in to your account", 9 | }; 10 | 11 | export default async function SignInPage({ 12 | searchParams, 13 | }: { 14 | searchParams: Promise<{ 15 | next?: string; 16 | }>; 17 | }) { 18 | const { next } = await searchParams; 19 | 20 | return ( 21 | <> 22 | 23 | Welcome back! 24 | 25 | 26 | 27 | Don't have an account yet?{" "} 28 | 34 | Create account 35 | 36 | 37 | 38 | 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /apps/www/src/app/(main)/(legal)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Container } from "@mantine/core"; 2 | 3 | export const dynamic = "force-static"; 4 | 5 | export default function LegalPagesLayout({ 6 | children, 7 | }: React.PropsWithChildren) { 8 | return {children}; 9 | } 10 | -------------------------------------------------------------------------------- /apps/www/src/app/(main)/[electionSlug]/(main)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { notFound, redirect } from "next/navigation"; 2 | 3 | import { isElectionOngoing } from "@eboto/constants"; 4 | 5 | import { createClient as createClientAdmin } from "~/supabase/admin"; 6 | import { createClient as createClientServer } from "~/supabase/server"; 7 | 8 | export default async function ElectionLayout( 9 | props: React.PropsWithChildren<{ params: Promise<{ electionSlug: string }> }>, 10 | ) { 11 | const { electionSlug } = await props.params; 12 | const supabaseServer = await createClientServer(); 13 | const { 14 | data: { user }, 15 | } = await supabaseServer.auth.getUser(); 16 | 17 | const supabaseAdmin = createClientAdmin(); 18 | const { data: election } = await supabaseAdmin 19 | .from("elections") 20 | .select() 21 | .eq("slug", electionSlug) 22 | .is("deleted_at", null) 23 | .single(); 24 | 25 | if (!election) notFound(); 26 | 27 | const isOngoing = isElectionOngoing({ election }); 28 | 29 | if (election.publicity === "PRIVATE") { 30 | if (!user) notFound(); 31 | 32 | const { data: commissioner } = await supabaseAdmin 33 | .from("commissioners") 34 | .select() 35 | .eq("election_id", election.id) 36 | .eq("user_id", user.id) 37 | .is("deleted_at", null) 38 | .single(); 39 | 40 | if (!commissioner) notFound(); 41 | } else if (election.publicity === "VOTER") { 42 | const next = `/sign-in?next=/${electionSlug}`; 43 | 44 | if (!user) redirect(next); 45 | 46 | const { data: voter } = await supabaseAdmin 47 | .from("voters") 48 | .select() 49 | .eq("election_id", election.id) 50 | .eq("email", user.email ?? "") 51 | .is("deleted_at", null) 52 | .single(); 53 | 54 | const { data: commissioner } = await supabaseAdmin 55 | .from("commissioners") 56 | .select() 57 | .eq("election_id", election.id) 58 | .eq("user_id", user.id) 59 | .is("deleted_at", null) 60 | .single(); 61 | 62 | if (!isOngoing && !voter && !commissioner) notFound(); 63 | 64 | if (!voter && !commissioner) redirect(next); 65 | } 66 | 67 | return <>{props.children}; 68 | } 69 | -------------------------------------------------------------------------------- /apps/www/src/app/(main)/[electionSlug]/(main)/loading.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from "react"; 2 | import { Container, Flex, Group, Skeleton, Stack } from "@mantine/core"; 3 | 4 | export default function ElectionPageLoading() { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {[...(Array(8) as number[])].map((_, i) => ( 23 | 24 | 25 | {[...(Array(2) as number[])].map((_, j) => ( 26 | 27 | 28 | 29 | 30 | ))} 31 | 32 | 33 | 34 | ))} 35 | 36 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /apps/www/src/app/(main)/[electionSlug]/(main)/page.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { notFound } from "next/navigation"; 3 | import { env } from "env"; 4 | import moment from "moment"; 5 | 6 | import ElectionPageClient from "~/components/pages/election-page"; 7 | import { createClient as createClientAdmin } from "~/supabase/admin"; 8 | import { createClient as createClientServer } from "~/supabase/server"; 9 | import { api } from "~/trpc/server"; 10 | 11 | export async function generateMetadata({ 12 | params, 13 | }: { 14 | params: Promise<{ electionSlug: string }>; 15 | }): Promise { 16 | const { electionSlug } = await params; 17 | 18 | const supabaseServer = await createClientServer(); 19 | const { 20 | data: { user }, 21 | } = await supabaseServer.auth.getUser(); 22 | 23 | const supabaseAdmin = createClientAdmin(); 24 | const { data: election } = await supabaseAdmin 25 | .from("elections") 26 | .select() 27 | .eq("slug", electionSlug) 28 | .is("deleted_at", null) 29 | .single(); 30 | 31 | if (!election) notFound(); 32 | 33 | if (election.publicity === "PRIVATE") { 34 | if (!user) notFound(); 35 | 36 | const { data: commissioners } = await supabaseAdmin 37 | .from("commissioners") 38 | .select() 39 | .eq("election_id", election.id) 40 | .eq("user_id", user.id) 41 | .is("deleted_at", null); 42 | 43 | if (commissioners?.length === 0) notFound(); 44 | } else if (election.publicity === "VOTER") { 45 | if (!user) notFound(); 46 | 47 | const { data: commissioners } = await supabaseAdmin 48 | .from("commissioners") 49 | .select() 50 | .eq("election_id", election.id) 51 | .eq("user_id", user.id) 52 | .is("deleted_at", null); 53 | 54 | const { data: voters } = await supabaseAdmin 55 | .from("voters") 56 | .select() 57 | .eq("election_id", election.id) 58 | .eq("email", user.email ?? "") 59 | .is("deleted_at", null); 60 | 61 | if (voters?.length === 0 && commissioners?.length === 0) notFound(); 62 | } 63 | 64 | let logo_url: string | null = null; 65 | 66 | if (election.logo_path) { 67 | const { data: url } = supabaseServer.storage 68 | .from("elections") 69 | .getPublicUrl(election.logo_path); 70 | 71 | logo_url = url.publicUrl; 72 | } 73 | 74 | return { 75 | title: election.name, 76 | description: `See details about ${election.name} | eBoto`, 77 | openGraph: { 78 | title: election.name, 79 | description: `See details about ${election.name} | eBoto`, 80 | images: [ 81 | { 82 | url: `${ 83 | env.NODE_ENV === "production" 84 | ? "https://eboto.app" 85 | : "http://localhost:3000" 86 | }/api/og?type=election&election_name=${encodeURIComponent( 87 | election.name, 88 | )}&election_logo=${encodeURIComponent( 89 | logo_url ?? "", 90 | )}&election_date=${encodeURIComponent( 91 | moment(election.start_date).format("MMMM D, YYYY") + 92 | " - " + 93 | moment(election.end_date).format("MMMM D, YYYY"), 94 | )}`, 95 | width: 1200, 96 | height: 630, 97 | alt: election.name, 98 | }, 99 | ], 100 | }, 101 | }; 102 | } 103 | 104 | export default async function ElectionPage({ 105 | params, 106 | }: { 107 | params: Promise<{ electionSlug: string }>; 108 | }) { 109 | const { electionSlug } = await params; 110 | 111 | const getElectionPage = await api.election.getElectionPage({ 112 | election_slug: electionSlug, 113 | }); 114 | 115 | return ( 116 | 125 | ); 126 | } 127 | -------------------------------------------------------------------------------- /apps/www/src/app/(main)/[electionSlug]/[candidateSlug]/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Center, Loader } from "@mantine/core"; 2 | 3 | export default function Loading() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /apps/www/src/app/(main)/[electionSlug]/[candidateSlug]/page.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { notFound } from "next/navigation"; 3 | import { env } from "env"; 4 | 5 | import { formatName } from "@eboto/constants"; 6 | 7 | import ElectionCandidate from "~/components/pages/election-candidate"; 8 | import { createClient as createClientAdmin } from "~/supabase/admin"; 9 | import { createClient as createClientServer } from "~/supabase/server"; 10 | import { api } from "~/trpc/server"; 11 | 12 | export async function generateMetadata({ 13 | params, 14 | }: { 15 | params: Promise<{ electionSlug: string; candidateSlug: string }>; 16 | }): Promise { 17 | const { electionSlug, candidateSlug } = await params; 18 | 19 | const supabaseServer = await createClientServer(); 20 | const { 21 | data: { user }, 22 | } = await supabaseServer.auth.getUser(); 23 | 24 | const supabaseAdmin = createClientAdmin(); 25 | const { data: election } = await supabaseAdmin 26 | .from("elections") 27 | .select() 28 | .eq("slug", electionSlug) 29 | .is("deleted_at", null) 30 | .single(); 31 | 32 | if (!election) notFound(); 33 | 34 | if (election.publicity === "PRIVATE") { 35 | if (!user) notFound(); 36 | 37 | const { data: commissioners } = await supabaseAdmin 38 | .from("commissioners") 39 | .select() 40 | .eq("election_id", election.id) 41 | .eq("user_id", user.id) 42 | .is("deleted_at", null); 43 | 44 | if (commissioners?.length === 0) notFound(); 45 | } else if (election.publicity === "VOTER") { 46 | if (!user) notFound(); 47 | 48 | const { data: commissioners } = await supabaseAdmin 49 | .from("commissioners") 50 | .select() 51 | .eq("election_id", election.id) 52 | .eq("user_id", user.id) 53 | .is("deleted_at", null); 54 | 55 | const { data: voters } = await supabaseAdmin 56 | .from("voters") 57 | .select() 58 | .eq("election_id", election.id) 59 | .eq("email", user.email ?? "") 60 | .is("deleted_at", null); 61 | 62 | if (commissioners?.length === 0 && voters?.length === 0) notFound(); 63 | } 64 | 65 | const { data: candidate } = await supabaseAdmin 66 | .from("candidates") 67 | .select("*, position: positions(name)") 68 | .eq("election_id", election.id) 69 | .eq("slug", candidateSlug) 70 | .is("deleted_at", null) 71 | .single(); 72 | 73 | if (!candidate?.position) return notFound(); 74 | 75 | let image_url: string | null = null; 76 | 77 | if (candidate.image_path) { 78 | const { data: image } = supabaseServer.storage 79 | .from("candidates") 80 | .getPublicUrl(candidate.image_path); 81 | 82 | image_url = image.publicUrl; 83 | } 84 | 85 | return { 86 | title: `${formatName(election.name_arrangement, candidate)} – ${ 87 | election.name 88 | }`, 89 | description: `See information about ${candidate.first_name} ${candidate.last_name} | eBoto`, 90 | openGraph: { 91 | title: election.name, 92 | description: `See information about ${candidate.first_name} ${candidate.last_name} | eBoto`, 93 | images: [ 94 | { 95 | url: `${ 96 | env.NODE_ENV === "production" 97 | ? "https://eboto.app" 98 | : "http://localhost:3000" 99 | }/api/og?type=candidate&candidate_name=${encodeURIComponent( 100 | candidate.first_name, 101 | )}${ 102 | (candidate.middle_name && 103 | `%20${encodeURIComponent(candidate.middle_name)}`) ?? 104 | "" 105 | }%20${encodeURIComponent( 106 | candidate.last_name, 107 | )}&candidate_position=${encodeURIComponent( 108 | candidate.position.name, 109 | )}&candidate_img=${encodeURIComponent(image_url ?? "")}`, 110 | width: 1200, 111 | height: 630, 112 | alt: election.name, 113 | }, 114 | ], 115 | }, 116 | }; 117 | } 118 | 119 | export default async function CandidatePage({ 120 | params, 121 | }: { 122 | params: Promise<{ electionSlug: string; candidateSlug: string }>; 123 | }) { 124 | const { electionSlug, candidateSlug } = await params; 125 | 126 | const data = await api.candidate.getPageData({ 127 | candidate_slug: candidateSlug, 128 | election_slug: electionSlug, 129 | }); 130 | 131 | return ( 132 | 141 | ); 142 | } 143 | -------------------------------------------------------------------------------- /apps/www/src/app/(main)/[electionSlug]/realtime/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Center, Loader } from "@mantine/core"; 2 | 3 | export default function Loading() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /apps/www/src/app/(main)/[electionSlug]/vote/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Center, Loader } from "@mantine/core"; 2 | 3 | export default function Loading() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /apps/www/src/app/(main)/account/layout.tsx: -------------------------------------------------------------------------------- 1 | import AccountPageLayoutClient from "~/components/layout/account"; 2 | 3 | export default function AccountPageLayout({ 4 | children, 5 | }: { 6 | children: React.ReactNode; 7 | }) { 8 | return {children}; 9 | } 10 | -------------------------------------------------------------------------------- /apps/www/src/app/(main)/account/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Center, Loader } from "@mantine/core"; 2 | 3 | export default function Loading() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /apps/www/src/app/(main)/account/page.tsx: -------------------------------------------------------------------------------- 1 | import { notFound } from "next/navigation"; 2 | 3 | import AccountPageClient from "~/components/pages/account"; 4 | import { createClient } from "~/supabase/server"; 5 | import { api } from "~/trpc/server"; 6 | 7 | export default async function AccountPage() { 8 | const supabase = await createClient(); 9 | const { 10 | data: { user }, 11 | } = await supabase.auth.getUser(); 12 | 13 | if (!user) notFound(); 14 | 15 | const getUserProtectedQuery = await api.auth.getUserProtected(); 16 | 17 | return ; 18 | } 19 | -------------------------------------------------------------------------------- /apps/www/src/app/(main)/contact/page.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Container, Stack, Text, Title } from "@mantine/core"; 2 | import Balancer from "react-wrap-balancer"; 3 | 4 | import ContactForm from "~/components/contact-form"; 5 | 6 | export const dynamic = "force-static"; 7 | 8 | export default function ContactPage() { 9 | return ( 10 | 11 | 12 | 13 | Contact Us 14 | 15 | 16 | We are happy to answer any questions you may have. Please reach 17 | out to us and we will respond as soon as we can. 18 | 19 | 20 | 21 | 22 | 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /apps/www/src/app/(main)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | AppShell, 3 | AppShellFooter, 4 | AppShellHeader, 5 | AppShellMain, 6 | } from "@mantine/core"; 7 | 8 | import Footer from "~/components/footer"; 9 | import Header from "~/components/header"; 10 | import { createClient } from "~/supabase/server"; 11 | 12 | export default async function RootLayout({ 13 | children, 14 | }: { 15 | children: React.ReactNode; 16 | }) { 17 | const supabase = await createClient(); 18 | const { 19 | data: { user }, 20 | } = await supabase.auth.getUser(); 21 | return ( 22 | 30 | 31 |
32 | 33 | 34 | {children} 35 | 36 | 37 |