├── types.d.ts ├── cypress ├── .gitignore ├── fixtures │ └── example.json ├── tsconfig.json ├── e2e │ └── app.cy.ts └── support │ ├── e2e.ts │ └── commands.ts ├── docs ├── pages │ ├── _meta.json │ ├── _app.mdx │ ├── index.mdx │ └── setup.mdx ├── postcss.config.js ├── theme.config.jsx ├── next.config.js ├── package.json └── .gitignore ├── pail.png ├── public ├── bucket.png ├── sponsors │ ├── sagent.png │ ├── catalyst.png │ ├── diodes.webp │ ├── fyrehost.png │ ├── merrill.png │ ├── asteralabs.png │ └── digitalocean.png ├── correct.svg ├── incorrect.svg ├── ctftime.svg ├── loading.svg └── discord.svg ├── .prettierrc.json ├── postcss.config.js ├── .vscode └── settings.json ├── src ├── lib │ ├── ClientUtils.ts │ ├── Logger.ts │ ├── prismadb.ts │ ├── Utils.ts │ ├── Rankings.ts │ └── Middleware.ts ├── app │ ├── loading.tsx │ ├── layout.tsx │ ├── api │ │ ├── user │ │ │ └── route.ts │ │ ├── auth │ │ │ └── [...nextauth] │ │ │ │ └── route.ts │ │ ├── team │ │ │ └── route.ts │ │ ├── settings │ │ │ ├── [key] │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── teams │ │ │ ├── leave │ │ │ │ └── route.ts │ │ │ ├── join │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── challenges │ │ │ ├── actions.ts │ │ │ ├── route.ts │ │ │ ├── host │ │ │ │ └── [id] │ │ │ │ │ └── route.ts │ │ │ ├── [id] │ │ │ │ └── route.ts │ │ │ └── solve │ │ │ │ └── [id] │ │ │ │ └── route.ts │ │ ├── rankings │ │ │ ├── route.ts │ │ │ └── ctftime │ │ │ │ └── route.ts │ │ └── hosts │ │ │ └── route.ts │ ├── admin │ │ ├── hosts │ │ │ └── page.tsx │ │ ├── containers │ │ │ └── page.tsx │ │ ├── challenges │ │ │ └── page.tsx │ │ ├── teams │ │ │ ├── page.tsx │ │ │ └── [id] │ │ │ │ └── page.tsx │ │ ├── users │ │ │ ├── page.tsx │ │ │ └── [id] │ │ │ │ └── page.tsx │ │ ├── page.tsx │ │ └── settings │ │ │ └── page.tsx │ ├── rankings │ │ └── page.tsx │ ├── account │ │ └── page.tsx │ ├── challenges │ │ └── page.tsx │ └── page.tsx ├── components │ ├── Dropdown.tsx │ ├── Status.tsx │ ├── Textarea.tsx │ ├── Error.tsx │ ├── Popover.tsx │ ├── Bucket.tsx │ ├── Code.tsx │ ├── Navbar.tsx │ ├── Input.tsx │ ├── containers │ │ └── ContainerDetails.tsx │ ├── Modal.tsx │ ├── Button.tsx │ ├── Dialog.tsx │ ├── challenge │ │ ├── CreateChallenge.tsx │ │ └── ChallengeContainer.tsx │ └── HostContainer.tsx └── styles │ ├── globals.css │ └── Glitch.module.css ├── cypress.config.ts ├── tailwind.config.js ├── .eslintrc.json ├── .env.example ├── next.config.js ├── .gitignore ├── tsconfig.json ├── LICENSE.txt ├── .github └── workflows │ └── build.yml ├── README.md ├── package.json ├── prisma └── schema.prisma └── CODE_OF_CONDUCT.md /types.d.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cypress/.gitignore: -------------------------------------------------------------------------------- 1 | /screenshots 2 | /videos -------------------------------------------------------------------------------- /docs/pages/_meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "index": "Introduction" 3 | } -------------------------------------------------------------------------------- /pail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EmergencyBucket/pail/HEAD/pail.png -------------------------------------------------------------------------------- /public/bucket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EmergencyBucket/pail/HEAD/public/bucket.png -------------------------------------------------------------------------------- /docs/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "useTabs": false, 4 | "singleQuote": true 5 | } -------------------------------------------------------------------------------- /public/sponsors/sagent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EmergencyBucket/pail/HEAD/public/sponsors/sagent.png -------------------------------------------------------------------------------- /public/sponsors/catalyst.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EmergencyBucket/pail/HEAD/public/sponsors/catalyst.png -------------------------------------------------------------------------------- /public/sponsors/diodes.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EmergencyBucket/pail/HEAD/public/sponsors/diodes.webp -------------------------------------------------------------------------------- /public/sponsors/fyrehost.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EmergencyBucket/pail/HEAD/public/sponsors/fyrehost.png -------------------------------------------------------------------------------- /public/sponsors/merrill.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EmergencyBucket/pail/HEAD/public/sponsors/merrill.png -------------------------------------------------------------------------------- /public/sponsors/asteralabs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EmergencyBucket/pail/HEAD/public/sponsors/asteralabs.png -------------------------------------------------------------------------------- /docs/pages/_app.mdx: -------------------------------------------------------------------------------- 1 | export default function App({ Component, pageProps }) { 2 | return 3 | } -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/sponsors/digitalocean.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EmergencyBucket/pail/HEAD/public/sponsors/digitalocean.png -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "typescript.enablePromptUseWorkspaceTsdk": true 4 | } -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /src/lib/ClientUtils.ts: -------------------------------------------------------------------------------- 1 | import { ClassValue, clsx } from 'clsx'; 2 | import { twMerge } from 'tailwind-merge'; 3 | 4 | export function bkct(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /docs/theme.config.jsx: -------------------------------------------------------------------------------- 1 | export default { 2 | logo: , 3 | project: { 4 | link: 'https://github.com/EmergencyBucket/pail', 5 | }, 6 | // ... 7 | } -------------------------------------------------------------------------------- /public/correct.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "cypress"; 2 | 3 | export default defineConfig({ 4 | e2e: { 5 | setupNodeEvents(on, config) { 6 | // implement node event listeners here 7 | }, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /src/lib/Logger.ts: -------------------------------------------------------------------------------- 1 | import { createLogger, format, transports } from 'winston'; 2 | 3 | const logger = createLogger({ 4 | level: 'info', 5 | format: format.combine(format.timestamp(), format.json()), 6 | transports: [new transports.Console()], 7 | }); 8 | 9 | export { logger }; 10 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./src/**/*.{js,ts,jsx,tsx}", 5 | ], 6 | theme: { 7 | extend: {}, 8 | }, 9 | plugins: [ 10 | require('@tailwindcss/typography'), 11 | ], 12 | } 13 | -------------------------------------------------------------------------------- /src/app/loading.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Status, Statuses } from '@/components/Status'; 4 | 5 | export default function loading() { 6 | return ( 7 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /cypress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "es5", 6 | "dom" 7 | ], 8 | "types": [ 9 | "cypress", 10 | "node" 11 | ] 12 | }, 13 | "include": [ 14 | "**/*.ts" 15 | ] 16 | } -------------------------------------------------------------------------------- /docs/next.config.js: -------------------------------------------------------------------------------- 1 | const withNextra = require('nextra')({ 2 | theme: 'nextra-theme-docs', 3 | themeConfig: './theme.config.jsx', 4 | }) 5 | 6 | module.exports = withNextra() 7 | 8 | // If you have other Next.js configurations, you can pass them as the parameter: 9 | // module.exports = withNextra({ /* other next.js config */ }) -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "next/core-web-vitals" 4 | ], 5 | "parser": "@typescript-eslint/parser", 6 | "plugins": [ 7 | "prettier", 8 | "@typescript-eslint" 9 | ], 10 | "rules": { 11 | "prettier/prettier": 2, 12 | "@typescript-eslint/no-unused-vars": 2, 13 | "react-hooks/exhaustive-deps": "off" 14 | } 15 | } -------------------------------------------------------------------------------- /src/lib/prismadb.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | 3 | declare global { 4 | var prisma: PrismaClient; 5 | } 6 | 7 | let prisma: PrismaClient; 8 | 9 | if (process.env.NODE_ENV === 'production') { 10 | prisma = new PrismaClient(); 11 | } else { 12 | if (!global.prisma) { 13 | global.prisma = new PrismaClient(); 14 | } 15 | 16 | prisma = global.prisma; 17 | } 18 | 19 | export default prisma; 20 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "1.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "next": "^13.4.12", 13 | "nextra": "^2.10.0", 14 | "nextra-theme-docs": "^2.10.0", 15 | "react": "^18.2.0", 16 | "react-dom": "^18.2.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Environment variables declared in this file are automatically made available to Prisma. 2 | # See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema 3 | 4 | # Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB. 5 | # See the documentation for all the connection string options: https://pris.ly/d/connection-strings 6 | 7 | DATABASE_URL= 8 | GITHUB_CLIENT_ID= 9 | GITHUB_CLIENT_SECRET= 10 | NEXTAUTH_SECRET= -------------------------------------------------------------------------------- /cypress/e2e/app.cy.ts: -------------------------------------------------------------------------------- 1 | describe('Homepage', () => { 2 | it('Has our navbar home', () => { 3 | cy.visit('http://localhost:3000/') 4 | 5 | cy.get('a').contains('Home').parents().should('have.attr', 'href', '/'); 6 | 7 | cy.get('a').contains('Challenges').parents().should('have.attr', 'href', '/challenges'); 8 | 9 | cy.get('a').contains('Rankings').parents().should('have.attr', 'href', '/rankings'); 10 | 11 | cy.get('a').contains('Sign in').parents().should('have.attr', 'href', '/api/auth/signin'); 12 | }) 13 | }) -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | remotePatterns: [{ 5 | protocol: 'https', 6 | hostname: 'avatars.githubusercontent.com', 7 | port: '', 8 | pathname: '/u/**', 9 | }] 10 | }, 11 | reactStrictMode: false, 12 | experimental: { 13 | serverActions: true, 14 | serverComponentsExternalPackages: ['cpu-features', 'ssh2', 'zlib_sync', 'discord.js'], 15 | }, 16 | } 17 | 18 | module.exports = nextConfig 19 | -------------------------------------------------------------------------------- /.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 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /docs/.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 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import Navbar from '@/components/Navbar'; 2 | import '../styles/globals.css'; 3 | 4 | export default async function RootLayout({ 5 | children, 6 | }: { 7 | children: React.ReactNode; 8 | }) { 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | {children} 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /cypress/support/e2e.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/e2e.ts is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') -------------------------------------------------------------------------------- /src/app/api/user/route.ts: -------------------------------------------------------------------------------- 1 | import { StatusCodes } from 'http-status-codes'; 2 | import { getServerSession } from 'next-auth'; 3 | import { NextResponse } from 'next/server'; 4 | import prisma from '@/lib/prismadb'; 5 | 6 | export async function GET() { 7 | const session = await getServerSession(); 8 | 9 | if (!session) { 10 | return NextResponse.json( 11 | { 12 | Error: 'You must be logged in to preform this action.', 13 | }, 14 | { 15 | status: StatusCodes.UNAUTHORIZED, 16 | }, 17 | ); 18 | } 19 | 20 | let user = await prisma.user.findFirst({ 21 | where: { 22 | name: session.user?.name, 23 | }, 24 | }); 25 | 26 | return NextResponse.json(user); 27 | } 28 | -------------------------------------------------------------------------------- /docs/pages/index.mdx: -------------------------------------------------------------------------------- 1 | import { Callout } from 'nextra-theme-docs' 2 | 3 | ![Pail Logo](https://github.com/EmergencyBucket/pail/raw/main/pail.png) 4 | 5 | > Pail is a full stack CTF platform developed by the Emergency Bucket CTF team 6 | 7 | ### Railway one-click deploy 8 | [![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/new/template/DrJIzA?referralCode=GswMXR) 9 | 10 | #### Features 11 | - GitHub SSO 12 | - Team management 13 | - CTFTime ranking integration 14 | - Docker container support 15 | - Ranking charts 16 | - Discord integration 17 | 18 | ### Screenshots 19 | ![](https://camo.githubusercontent.com/f3c08ad5e9bcec86336294e3a9393a91108cbcc9d5b259dcfb53a208d0c1ca2e/68747470733a2f2f692e6d7278626f7839382e6d652f66696c652f323032332f30372f6368726f6d655f4d6b72584856755a736e2e706e67) -------------------------------------------------------------------------------- /src/app/admin/hosts/page.tsx: -------------------------------------------------------------------------------- 1 | import { admin } from '@/lib/Middleware'; 2 | import { Error } from '@/components/Error'; 3 | import prisma from '@/lib/prismadb'; 4 | import HostContainer from '@/components/HostContainer'; 5 | 6 | export const metadata = { 7 | title: 'EBucket | Admin | Hosts', 8 | }; 9 | 10 | export default async function Home() { 11 | if (await admin()) { 12 | return ; 13 | } 14 | 15 | let hosts = await prisma.host.findMany(); 16 | 17 | return ( 18 |
19 | {hosts.map((host) => ( 20 | 21 | ))} 22 | 23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/app/admin/containers/page.tsx: -------------------------------------------------------------------------------- 1 | import { admin } from '@/lib/Middleware'; 2 | import { Error } from '@/components/Error'; 3 | import prisma from '@/lib/prismadb'; 4 | import ContainerDetails from '@/components/containers/ContainerDetails'; 5 | 6 | export const metadata = { 7 | title: 'EBucket | Admin | Containers', 8 | }; 9 | 10 | export default async function Home() { 11 | if (await admin()) { 12 | return ; 13 | } 14 | 15 | let container = await prisma.container.findMany(); 16 | 17 | return ( 18 |
19 | {container.map((cont) => ( 20 | 21 | ))} 22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/lib/Utils.ts: -------------------------------------------------------------------------------- 1 | import { getServerSession } from 'next-auth'; 2 | import prisma from './prismadb'; 3 | 4 | export async function getUser() { 5 | let session = await getServerSession(); 6 | 7 | if (!session) { 8 | return null; 9 | } 10 | 11 | return await prisma.user.findFirst({ 12 | where: { 13 | email: session?.user?.email, 14 | }, 15 | }); 16 | } 17 | 18 | export async function getTeam() { 19 | let session = await getServerSession(); 20 | 21 | if (!session) { 22 | return null; 23 | } 24 | 25 | return ( 26 | await prisma.user.findFirst({ 27 | where: { 28 | email: session?.user?.email, 29 | }, 30 | include: { 31 | team: true, 32 | }, 33 | }) 34 | )?.team; 35 | } 36 | -------------------------------------------------------------------------------- /src/app/admin/challenges/page.tsx: -------------------------------------------------------------------------------- 1 | import { admin } from '@/lib/Middleware'; 2 | import { Error } from '@/components/Error'; 3 | import prisma from '@/lib/prismadb'; 4 | import { CreateChallenge } from '@/components/challenge/CreateChallenge'; 5 | 6 | export const metadata = { 7 | title: 'EBucket | Admin | Challenges', 8 | }; 9 | 10 | export default async function Home() { 11 | if (await admin()) { 12 | return ; 13 | } 14 | 15 | let challenges = await prisma.challenge.findMany(); 16 | 17 | return ( 18 |
19 | {challenges.map((chall) => ( 20 | 21 | ))} 22 | 23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/components/Dropdown.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export function useComboboxValue(initialValue = '') { 4 | const [value, setValue] = React.useState(initialValue); 5 | return [value, setValue]; 6 | } 7 | 8 | export interface SelectProps 9 | extends React.SelectHTMLAttributes { 10 | items: string[]; 11 | } 12 | 13 | const Dropdown = React.forwardRef( 14 | ({ items, ...props }, ref) => ( 15 | 24 | ), 25 | ); 26 | Dropdown.displayName = 'Dropdown'; 27 | 28 | export { Dropdown }; 29 | -------------------------------------------------------------------------------- /src/app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import { PrismaAdapter } from '@next-auth/prisma-adapter'; 2 | import GitHubProvider from 'next-auth/providers/github'; 3 | import NextAuth, { NextAuthOptions } from 'next-auth'; 4 | import prisma from '@/lib/prismadb'; 5 | 6 | const authOptions: NextAuthOptions = { 7 | adapter: PrismaAdapter(prisma), 8 | providers: [ 9 | GitHubProvider({ 10 | clientId: process.env.GITHUB_CLIENT_ID!, 11 | clientSecret: process.env.GITHUB_CLIENT_SECRET!, 12 | }), 13 | ], 14 | callbacks: { 15 | session: async ({ session, user }) => { 16 | return { 17 | ...session, 18 | user: user, 19 | }; 20 | }, 21 | }, 22 | session: { 23 | strategy: 'jwt', 24 | }, 25 | }; 26 | 27 | const handler = NextAuth(authOptions); 28 | 29 | export { handler as GET, handler as POST }; 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve", 20 | "incremental": true, 21 | "baseUrl": ".", 22 | "paths": { 23 | "@/*": [ 24 | "./src/*" 25 | ] 26 | }, 27 | "plugins": [ 28 | { 29 | "name": "next" 30 | } 31 | ] 32 | }, 33 | "include": [ 34 | "next-env.d.ts", 35 | "**/*.ts", 36 | "**/*.tsx", 37 | ".next/types/**/*.ts", 38 | "/home/pail/.next/types/**/*.ts" 39 | ], 40 | "exclude": [ 41 | "node_modules" 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /src/components/Status.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { CheckIcon, XIcon } from 'lucide-react'; 4 | import { Loader2 } from 'lucide-react'; 5 | import React from 'react'; 6 | 7 | enum Statuses { 8 | Unsubmitted, 9 | Loading, 10 | Correct, 11 | Incorrect, 12 | } 13 | 14 | interface Props extends React.HTMLAttributes { 15 | status: Statuses; 16 | } 17 | 18 | const Status = ({ status, className }: Props) => { 19 | switch (status) { 20 | case Statuses.Unsubmitted: { 21 | return <>; 22 | } 23 | case Statuses.Loading: { 24 | return ( 25 | 26 | ); 27 | } 28 | case Statuses.Correct: { 29 | return ; 30 | } 31 | case Statuses.Incorrect: { 32 | return ; 33 | } 34 | } 35 | }; 36 | 37 | export { Status, Statuses }; 38 | -------------------------------------------------------------------------------- /docs/pages/setup.mdx: -------------------------------------------------------------------------------- 1 | # Setup 2 | 3 | Pail is a full stack NextJS application meaning only one web service needs to be run along with a PostgreSQL database. 4 | The easiest way to deploy is via Railway which will manage nearly all settings for you. 5 | 6 | # Manual Setup 7 | 8 | 1. Clone the project off of GitHub. 9 | 2. Install all dependencies by running 10 | ```bash 11 | npm install -g yarn 12 | yarn install 13 | ``` 14 | 3. Install + setup PostgreSQL and set an environment variable ``DATABASE_URL_NON_POOLING`` to the full connection URL. 15 | 4. Also set a ``NEXTAUTH_SECRET`` in your ``.env`` file (this should be completely random). 16 | 5. Setup GitHub sign in via the tutorial [here](https://next-auth.js.org/providers/github) (Set the ``GITHUB_CLIENT_ID`` and ``GITHUB_CLIENT_SECRET`` in your environment variables). 17 | 6. Optionally set the ``DOCKER_USERNAME`` and ``DOCKER_PASSWORD`` to pull images from the GitHub container registry. 18 | 7. Push the db schema and build the website by running ``yarn build``. 19 | 20 | -------------------------------------------------------------------------------- /src/components/Textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { bkct } from '@/lib/ClientUtils'; 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 |