├── .env.example ├── src ├── styles │ ├── index.ts │ ├── fonts.ts │ └── globals.css ├── app │ ├── events │ │ ├── [event] │ │ │ ├── error.tsx │ │ │ ├── layout.tsx │ │ │ ├── participants │ │ │ │ ├── upload.ts │ │ │ │ ├── page.tsx │ │ │ │ ├── data-participants.ts │ │ │ │ ├── [groupId] │ │ │ │ │ └── page.tsx │ │ │ │ └── fake-participants.ts │ │ │ ├── table-participants.tsx │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── data-events.ts │ │ └── page.tsx │ ├── favicon.ico │ ├── globals.css │ ├── page.tsx │ ├── not-found.tsx │ ├── api │ │ └── events │ │ │ ├── [event] │ │ │ ├── participants │ │ │ │ ├── [participantId] │ │ │ │ │ ├── remove-participant │ │ │ │ │ │ └── route.ts │ │ │ │ │ ├── check-participant │ │ │ │ │ │ └── route.ts │ │ │ │ │ ├── set-winner │ │ │ │ │ │ └── route.ts │ │ │ │ │ └── select-new-random-player │ │ │ │ │ │ └── route.ts │ │ │ │ ├── edit-participant │ │ │ │ │ └── route.ts │ │ │ │ ├── register-participant │ │ │ │ │ └── route.ts │ │ │ │ ├── route.ts │ │ │ │ └── select-participants │ │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ │ └── route.ts │ └── layout.tsx ├── components │ ├── card │ │ ├── index.tsx │ │ └── card.tsx │ ├── input │ │ ├── index.tsx │ │ └── input.tsx │ ├── modal │ │ ├── index.ts │ │ └── modal.tsx │ ├── table │ │ ├── index.ts │ │ └── table.tsx │ ├── button │ │ ├── index.ts │ │ └── button.tsx │ ├── checkbox │ │ ├── index.ts │ │ └── checkbox.tsx │ ├── sidebar │ │ ├── index.ts │ │ └── sidebar.tsx │ ├── spinner │ │ ├── index.ts │ │ └── spinner.tsx │ ├── register-event-modal │ │ ├── index.ts │ │ └── modal.tsx │ ├── edit-participant-modal │ │ ├── index.ts │ │ └── modal.tsx │ ├── register-participant-modal │ │ ├── index.ts │ │ └── modal.tsx │ ├── rounds-list │ │ ├── round-item.tsx │ │ ├── participant-item.tsx │ │ ├── index.ts │ │ ├── participants-list.tsx │ │ ├── round-title.tsx │ │ ├── participant-name.tsx │ │ ├── final-round.tsx │ │ ├── rounds-list.tsx │ │ └── participant-image.tsx │ └── index.ts ├── lib │ ├── index.ts │ ├── utils.ts │ └── get-random-integer.ts ├── shared │ └── types │ │ ├── index.ts │ │ ├── round.ts │ │ ├── event.ts │ │ └── participant.ts └── prisma.ts ├── commitlint.config.js ├── public ├── ghost.jpeg ├── logo.svg ├── vercel.svg └── next.svg ├── .husky ├── pre-commit └── commit-msg ├── .prettierrc.json ├── postcss.config.js ├── prisma ├── migrations │ ├── migration_lock.toml │ └── 20230719215737_init │ │ └── migration.sql ├── seed │ ├── seed-event.ts │ ├── json-to-csv.ts │ ├── seed.ts │ └── participants.ts └── schema.prisma ├── .editorconfig ├── lint-staged.config.mjs ├── .eslintrc.json ├── next.config.js ├── .gitignore ├── tsconfig.json ├── .github └── workflows │ └── lint.yml ├── README.md ├── CONTRIBUTING.md ├── package.json └── tailwind.config.js /.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL=file:./dev.db 2 | -------------------------------------------------------------------------------- /src/styles/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./fonts" 2 | -------------------------------------------------------------------------------- /src/app/events/[event]/error.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | -------------------------------------------------------------------------------- /src/components/card/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./card" 2 | -------------------------------------------------------------------------------- /src/components/input/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./input" 2 | -------------------------------------------------------------------------------- /src/components/modal/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./modal" 2 | -------------------------------------------------------------------------------- /src/components/table/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./table" 2 | -------------------------------------------------------------------------------- /src/components/button/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./button" 2 | -------------------------------------------------------------------------------- /src/components/checkbox/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./checkbox" 2 | -------------------------------------------------------------------------------- /src/components/sidebar/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./sidebar" 2 | -------------------------------------------------------------------------------- /src/components/spinner/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./spinner" 2 | -------------------------------------------------------------------------------- /src/components/register-event-modal/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./modal" 2 | -------------------------------------------------------------------------------- /src/components/edit-participant-modal/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./modal" 2 | -------------------------------------------------------------------------------- /src/components/register-participant-modal/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./modal" 2 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {extends: ['@commitlint/config-conventional']} 2 | -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./get-random-integer" 2 | export * from "./utils" 3 | -------------------------------------------------------------------------------- /public/ghost.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeinthedarkbrasil/manage-citd/HEAD/public/ghost.jpeg -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeinthedarkbrasil/manage-citd/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | yarn tsc 5 | yarn lint 6 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "semi": false, 4 | "jsxSingleQuote": false 5 | } 6 | -------------------------------------------------------------------------------- /src/shared/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./participant" 2 | export * from "./round" 3 | export * from "./event" 4 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit ${1} 5 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /src/prisma.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client" 2 | export const prisma = new PrismaClient({ log: ["query"] }) 3 | -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | html, body { 6 | height: 100vh; 7 | } 8 | -------------------------------------------------------------------------------- /src/shared/types/round.ts: -------------------------------------------------------------------------------- 1 | import { ParticipantInGroup } from "./participant" 2 | 3 | export type Round = { 4 | participants: ParticipantInGroup[] 5 | } 6 | -------------------------------------------------------------------------------- /src/styles/fonts.ts: -------------------------------------------------------------------------------- 1 | import { Sora } from 'next/font/google' 2 | 3 | export const sora = Sora({ 4 | variable: '--font-sans', 5 | subsets: ['latin'] 6 | }) 7 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "sqlite" -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation" 2 | 3 | export default async function Home() { 4 | // TODO: Ver o que vamos fazer na página inicial 5 | redirect("/events") 6 | } 7 | -------------------------------------------------------------------------------- /src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | html, body { 6 | height: 100%; 7 | } 8 | 9 | html { 10 | font-size: 62.5%; 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /src/components/rounds-list/round-item.tsx: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren } from "react" 2 | 3 | export function RoundItem({ children }: PropsWithChildren) { 4 | return
{children}
5 | } 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_size = 2 6 | indent_style = space 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /src/lib/get-random-integer.ts: -------------------------------------------------------------------------------- 1 | import { MersenneTwister19937, integer } from 'random-js' 2 | 3 | const engine = MersenneTwister19937.autoSeed() 4 | 5 | export function getRandomInteger(to: number) { 6 | const distribution = integer(0, to) 7 | return distribution(engine) 8 | } 9 | -------------------------------------------------------------------------------- /src/components/rounds-list/participant-item.tsx: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren } from "react" 2 | 3 | export function ParticipantItem({ children }: PropsWithChildren) { 4 | return ( 5 |
  • 6 | {children} 7 |
  • 8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /src/components/rounds-list/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./rounds-list" 2 | export * from "./round-title" 3 | export * from "./round-item" 4 | export * from "./participants-list" 5 | export * from "./participant-item" 6 | export * from "./participant-image" 7 | export * from "./participant-name" 8 | export * from "./final-round" 9 | -------------------------------------------------------------------------------- /src/components/rounds-list/participants-list.tsx: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren } from "react" 2 | 3 | export function ParticipantsList({ children }: PropsWithChildren) { 4 | return ( 5 | 8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /src/components/rounds-list/round-title.tsx: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren } from "react" 2 | 3 | export function RoundTitle({ children }: PropsWithChildren) { 4 | return ( 5 | 6 | {children} 7 | 8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /src/app/not-found.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | 3 | export default function NotFound() { 4 | return ( 5 |
    6 |

    Not Found

    7 |

    Could not find requested resource

    8 | Go back 9 |
    10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /src/components/rounds-list/participant-name.tsx: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren } from "react" 2 | 3 | export function ParticipantName({ children }: PropsWithChildren) { 4 | return ( 5 |

    6 | {children} 7 |

    8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /lint-staged.config.mjs: -------------------------------------------------------------------------------- 1 | import path from "path" 2 | 3 | const buildEslintCommand = (filenames) => 4 | `next lint --file ${filenames 5 | .map((f) => path.relative(process.cwd(), f)) 6 | .join(" --file ")}` 7 | 8 | const config = { 9 | "src/**/*.{ts,tsx}": [ 10 | () => "yarn type-check", 11 | "yarn prettier", 12 | buildEslintCommand, 13 | ], 14 | } 15 | 16 | export default config 17 | -------------------------------------------------------------------------------- /src/app/events/layout.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { QueryClientProvider, QueryClient } from "@tanstack/react-query" 4 | import { PropsWithChildren } from "react" 5 | 6 | const queryClient = new QueryClient() 7 | 8 | export default function EventLayout({ children }: PropsWithChildren) { 9 | return ( 10 | {children} 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /src/app/events/[event]/layout.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { QueryClientProvider, QueryClient } from "@tanstack/react-query" 4 | import { PropsWithChildren } from "react" 5 | 6 | const queryClient = new QueryClient() 7 | 8 | export default function EventLayout({ children }: PropsWithChildren) { 9 | return ( 10 | {children} 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /prisma/seed/seed-event.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from "@/prisma" 2 | 3 | async function main() { 4 | await prisma.event.create({ 5 | data: { 6 | name: "Code in the Dark 2023", 7 | slug: "2023", 8 | }, 9 | }) 10 | } 11 | 12 | main() 13 | .then(async () => { 14 | await prisma.$disconnect() 15 | }) 16 | .catch(async (e) => { 17 | console.log(e) 18 | await prisma.$disconnect() 19 | process.exit(1) 20 | }) 21 | -------------------------------------------------------------------------------- /src/components/rounds-list/final-round.tsx: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren } from "react" 2 | 3 | type FinalRoundProps = PropsWithChildren & { 4 | size?: "big" 5 | } 6 | export function FinalRound({ children, size }: FinalRoundProps) { 7 | return ( 8 |
    9 |
    10 | {children} 11 |
    12 |
    13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "next/core-web-vitals", 4 | "plugin:@tanstack/eslint-plugin-query/recommended", 5 | "plugin:tailwindcss/recommended" 6 | ], 7 | "root": true, 8 | "rules": { 9 | "tailwindcss/classnames-order": [ 10 | 1, 11 | { 12 | "callees": [ 13 | "classNames", 14 | "clsx", 15 | "cn", 16 | "cva" 17 | ] 18 | } 19 | ] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/components/rounds-list/rounds-list.tsx: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren } from "react" 2 | 3 | export function RoundsList({ children }: PropsWithChildren) { 4 | return ( 5 |
    6 |
    7 | {children} 8 |
    9 |
    10 |
    11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | export * from "./sidebar" 4 | export * from "./rounds-list" 5 | export * from "./sidebar" 6 | export * from "./card" 7 | export * from "./button" 8 | export * from "./checkbox" 9 | export * from "./table" 10 | export * from "./input" 11 | export * from "./modal" 12 | export * from "./register-participant-modal" 13 | export * from "./edit-participant-modal" 14 | export * from "./register-event-modal" 15 | export * from "./spinner" 16 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | experimental: { 4 | serverActions: true, 5 | }, 6 | images: { 7 | remotePatterns: [ 8 | { 9 | protocol: "https", 10 | hostname: "avatars.githubusercontent.com", 11 | port: "", 12 | pathname: "/u/**", 13 | }, 14 | { 15 | protocol: "https", 16 | hostname: "github.com", 17 | port: "", 18 | }, 19 | ], 20 | }, 21 | } 22 | 23 | module.exports = nextConfig 24 | -------------------------------------------------------------------------------- /.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 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | # sqlite 39 | prisma/dev.db* 40 | frontin.csv 41 | -------------------------------------------------------------------------------- /src/components/spinner/spinner.tsx: -------------------------------------------------------------------------------- 1 | export function Spinner() { 2 | return ( 3 | 4 | 12 | 21 | 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /prisma/seed/json-to-csv.ts: -------------------------------------------------------------------------------- 1 | import { writeFile } from "node:fs/promises" 2 | import { participants } from "./participants" 3 | 4 | async function main() { 5 | const title = 6 | "N. do pedido,Data do pedido,Status do participante,Nome,Sobrenome,E-mail\n" 7 | let content = title 8 | content += participants 9 | .map(({ name, email }) => { 10 | const [firstName, ...lastNameArray] = name.split(" ") 11 | const lastName = lastNameArray.join(" ") 12 | return `0,0/0/00 10:40 AM,0,${firstName},${lastName},${email}` 13 | }) 14 | .join("\n") 15 | 16 | await writeFile("./frontin.csv", content) 17 | } 18 | 19 | main() 20 | -------------------------------------------------------------------------------- /src/app/api/events/[event]/participants/[participantId]/remove-participant/route.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from "@/prisma" 2 | import { NextResponse } from "next/server" 3 | 4 | type RemoveParticipantInput = { 5 | params: { 6 | participantId: string 7 | event: string 8 | } 9 | } 10 | 11 | export async function DELETE(_: unknown, { params }: RemoveParticipantInput) { 12 | const { participantId, event } = params 13 | 14 | await prisma.play.delete({ 15 | where: { 16 | userId_eventSlug: { 17 | userId: participantId, 18 | eventSlug: event, 19 | }, 20 | }, 21 | }) 22 | 23 | return new NextResponse() 24 | } 25 | -------------------------------------------------------------------------------- /src/app/events/data-events.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type CITDEvent, 3 | type RegisterEvent, 4 | eventSchema, 5 | arrayOfEventsSchema, 6 | } from "@/shared/types" 7 | 8 | export async function getEvents(): Promise { 9 | const result = await fetch("/api/events") 10 | const events = await result.json() 11 | return arrayOfEventsSchema.parse(events) 12 | } 13 | 14 | export async function registerEvent(data: RegisterEvent): Promise { 15 | const result = await fetch("/api/events", { 16 | method: "POST", 17 | body: JSON.stringify(data), 18 | }) 19 | const event = await result.json() 20 | return eventSchema.parse(event) 21 | } 22 | -------------------------------------------------------------------------------- /src/app/api/events/[event]/participants/edit-participant/route.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from "@/prisma" 2 | import { NextRequest, NextResponse } from "next/server" 3 | 4 | export async function POST(request: NextRequest) { 5 | // TODO: validar com Zod 6 | const { 7 | id, 8 | name, 9 | email, 10 | github, 11 | }: { id: string; name: string; email: string; github?: string } = 12 | await request.json() 13 | 14 | const participant = await prisma.user.update({ 15 | where: { 16 | id, 17 | }, 18 | data: { 19 | name, 20 | email, 21 | github: !!github ? github : undefined, 22 | }, 23 | }) 24 | 25 | return NextResponse.json({ 26 | data: participant, 27 | status: 201, 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /src/components/card/card.tsx: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren } from "react"; 2 | 3 | export function Card({ children }: PropsWithChildren) { 4 | return ( 5 |
    6 | {children} 7 |
    8 | ) 9 | } 10 | 11 | export function CardTitle({ children }: PropsWithChildren) { 12 | return ( 13 |

    14 | {children} 15 |

    16 | ) 17 | } 18 | 19 | export function CardText({ children }: PropsWithChildren) { 20 | return ( 21 |

    22 | {children} 23 |

    24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./src/*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules", ".next"] 28 | } 29 | -------------------------------------------------------------------------------- /src/shared/types/event.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod" 2 | import slugify from "slugify" 3 | 4 | // TODO: Remover (ou ver o que fazer) 5 | export type EventProps = { 6 | params: { 7 | event: string 8 | } 9 | } 10 | 11 | export const registerEventSchema = z.object({ 12 | name: z.string(), 13 | slug: z 14 | .string() 15 | .refine( 16 | (val) => val === slugify(val, { trim: true, lower: true }), 17 | "Slug inválido", 18 | ), 19 | }) 20 | 21 | export type RegisterEvent = z.infer 22 | 23 | export const eventSchema = z.object({ 24 | id: z.string(), 25 | name: z.string(), 26 | slug: z.string(), 27 | participantsCount: z.number(), 28 | }) 29 | 30 | export const arrayOfEventsSchema = z.array(eventSchema) 31 | export type CITDEvent = z.infer 32 | -------------------------------------------------------------------------------- /src/app/api/events/[event]/participants/register-participant/route.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from "@/prisma" 2 | import { NextRequest, NextResponse } from "next/server" 3 | 4 | type RegisterParticipantParams = { 5 | params: { 6 | event: string 7 | } 8 | } 9 | 10 | export async function POST( 11 | request: NextRequest, 12 | { params }: RegisterParticipantParams, 13 | ) { 14 | const { event } = params 15 | 16 | // TODO: validar com Zod 17 | const { 18 | name, 19 | email, 20 | github, 21 | }: { name: string; email: string; github?: string } = await request.json() 22 | 23 | const participant = await prisma.user.create({ 24 | data: { 25 | name, 26 | email, 27 | github: !!github ? github : undefined, 28 | play: { 29 | create: { 30 | eventSlug: event, 31 | }, 32 | }, 33 | }, 34 | }) 35 | 36 | return NextResponse.json({ 37 | data: participant, 38 | status: 201, 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Linter & Type Checking 2 | on: 3 | pull_request: 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | 11 | - name: Cache node modules 12 | uses: actions/cache@v2 13 | env: 14 | cache-name: cache-node-modules 15 | with: 16 | path: ~/.npm 17 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }} 18 | restore-keys: | 19 | ${{ runner.os }}-build-${{ env.cache-name }}- 20 | ${{ runner.os }}-build- 21 | ${{ runner.os }}- 22 | 23 | - name: Setup Node.js 24 | uses: actions/setup-node@v3 25 | with: 26 | node-version: 18 27 | 28 | - name: Install dependencies 29 | run: yarn 30 | 31 | - name: Type Checking 32 | run: yarn tsc 33 | 34 | - name: Lint 35 | run: yarn lint 36 | -------------------------------------------------------------------------------- /src/app/api/events/[event]/participants/[participantId]/check-participant/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server" 2 | import { prisma } from "@/prisma" 3 | 4 | type CheckParticipantInput = { 5 | params: { 6 | event: string 7 | participantId: string 8 | } 9 | } 10 | 11 | export async function POST( 12 | request: NextRequest, 13 | { params }: CheckParticipantInput, 14 | ) { 15 | const { event, participantId } = params 16 | // TODO: Validar o body com zod 17 | const body: { checked: boolean } = await request.json() 18 | 19 | await prisma.play.update({ 20 | where: { 21 | userId_eventSlug: { 22 | userId: participantId, 23 | eventSlug: event, 24 | }, 25 | }, 26 | data: { 27 | wannaPlay: body.checked, 28 | gonnaPlay: body.checked === false ? false : undefined, 29 | groupId: body.checked === false ? null : undefined, 30 | winner: body.checked === false ? false : undefined, 31 | }, 32 | }) 33 | 34 | return new NextResponse() 35 | } 36 | -------------------------------------------------------------------------------- /src/app/api/events/[event]/participants/[participantId]/set-winner/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server" 2 | import { prisma } from "@/prisma" 3 | 4 | type SetWinnerInput = { 5 | params: { 6 | event: string 7 | participantId: string 8 | } 9 | } 10 | export async function POST(request: NextRequest, { params }: SetWinnerInput) { 11 | const { participantId, event } = params 12 | // TODO: Validar com Zod 13 | const { groupId }: { groupId: number } = await request.json() 14 | 15 | await prisma.$transaction([ 16 | prisma.play.updateMany({ 17 | where: { 18 | eventSlug: event, 19 | groupId, 20 | winner: true, 21 | }, 22 | data: { 23 | winner: false, 24 | }, 25 | }), 26 | 27 | prisma.play.update({ 28 | where: { 29 | userId_eventSlug: { 30 | userId: participantId, 31 | eventSlug: event, 32 | }, 33 | }, 34 | data: { 35 | winner: true, 36 | }, 37 | }), 38 | ]) 39 | 40 | return new NextResponse() 41 | } 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Backoffice —— Code in the Dark 2 | 3 | > Backoffice application for Code in the Dark ⚡ 4 | 5 | **IMPORTANT: Node.js v20 is required to run this project.** 6 | 7 | ## Setup ⚙️ 8 | 9 | 1. Install the deps: 10 | 11 | ```sh 12 | yarn 13 | ``` 14 | 15 | 2. Copy `.env.example` file to a new `.env` file: 16 | 17 | ```sh 18 | cp .env.example .env 19 | ``` 20 | 21 | 3. Run migrations to create the database: 22 | 23 | ```sh 24 | yarn migrate 25 | ``` 26 | 27 | 4. Run seed to fill up the database: 28 | 29 | ```sh 30 | yarn seed 31 | ``` 32 | 33 | 6. Run the dev server: 34 | 35 | ```sh 36 | yarn dev 37 | ``` 38 | 39 | If you want to check the data in the database, you can run `prisma studio`: 40 | 41 | ```sh 42 | yarn prisma studio 43 | ``` 44 | 45 | To re-run the seed, first delete the file `prisma/dev.db`, then run steps `3` and `4` again. 46 | 47 | ## Contributing 📖 48 | 49 | Follow our [guide](./CONTRIBUTING.md). 50 | 51 | ## Design 🎨 52 | 53 | [Layout Figma](https://www.figma.com/file/PsD124B5jvDdxyYCqxIbys/Code-in-The-Dark---Back-Office?type=design&t=FQNPMNk5uC2gSuxr-0) 54 | 55 | ## License ⚖️ 56 | 57 | MIT 58 | -------------------------------------------------------------------------------- /src/app/api/events/[event]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server" 2 | import { prisma } from "@/prisma" 3 | 4 | type GetEventInput = { 5 | params: { 6 | event: string 7 | } 8 | } 9 | 10 | // TODO: Tipar retorno das funções e criar um tipo padrão de erro 11 | export async function GET(_: unknown, { params }: GetEventInput) { 12 | const eventDataFromDb = await prisma.event.findFirst({ 13 | where: { 14 | slug: params.event, 15 | }, 16 | select: { 17 | id: true, 18 | slug: true, 19 | name: true, 20 | _count: { 21 | select: { 22 | play: true, 23 | }, 24 | }, 25 | }, 26 | }) 27 | 28 | if (!eventDataFromDb) { 29 | return NextResponse.json( 30 | { message: `Não existe o evento ${params.event}` }, 31 | { 32 | status: 404, 33 | }, 34 | ) 35 | } 36 | 37 | const eventData = { 38 | id: eventDataFromDb.id, 39 | slug: eventDataFromDb.slug, 40 | name: eventDataFromDb.name, 41 | participantsCount: eventDataFromDb._count.play, 42 | } 43 | 44 | return NextResponse.json(eventData) 45 | } 46 | -------------------------------------------------------------------------------- /src/components/sidebar/sidebar.tsx: -------------------------------------------------------------------------------- 1 | import { usePathname } from "next/navigation" 2 | import Link from "next/link" 3 | import { PropsWithChildren } from "react" 4 | 5 | type SidebarLinkProps = PropsWithChildren<{ href: string }> 6 | 7 | export const Sidebar = ({ children }: PropsWithChildren) => ( 8 | 11 | ) 12 | 13 | export const SidebarItem = ({ children }: PropsWithChildren) => ( 14 |
  • {children}
  • 15 | ) 16 | export const SidebarList = ({ children }: PropsWithChildren) => ( 17 |
      {children}
    18 | ) 19 | 20 | export const SidebarLink = ({ children, href }: SidebarLinkProps) => { 21 | const pathname = usePathname() 22 | const isActive = pathname.startsWith(href) 23 | const colorStyles = isActive ? "text-primary-100" : "text-neutral-300" 24 | 25 | return ( 26 | 30 | {children} 31 | 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /src/app/api/events/[event]/participants/route.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from "@/prisma" 2 | import { NextResponse } from "next/server" 3 | import { Participant } from "@/shared/types" 4 | 5 | type GetParticipantsInput = { 6 | params: { 7 | event: string 8 | } 9 | } 10 | 11 | export async function GET( 12 | _: unknown, 13 | { params }: GetParticipantsInput, 14 | ): Promise> { 15 | const users = await prisma.user.findMany({ 16 | where: { 17 | play: { 18 | some: { 19 | eventSlug: params.event, 20 | }, 21 | }, 22 | }, 23 | include: { 24 | play: { 25 | where: { 26 | eventSlug: params.event, 27 | }, 28 | }, 29 | }, 30 | }) 31 | 32 | const result = users.map((user) => ({ 33 | id: user.id, 34 | name: user.name, 35 | email: user.email, 36 | github: user.github, 37 | wannaPlay: user.play[0].wannaPlay, 38 | gonnaPlay: user.play[0].gonnaPlay, 39 | winner: user.play[0].winner, 40 | groupId: user.play[0].groupId, 41 | })) 42 | 43 | return NextResponse.json(result) 44 | } 45 | 46 | export async function POST(_request: Request) {} 47 | -------------------------------------------------------------------------------- /prisma/seed/seed.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from "@/prisma" 2 | import { participants } from "./participants" 3 | import { getRandomInteger } from "@/lib" 4 | 5 | async function main() { 6 | const event1 = await prisma.event.create({ 7 | data: { 8 | name: "Code in the Dark 2022", 9 | slug: "2022", 10 | }, 11 | }) 12 | 13 | const event2 = await prisma.event.create({ 14 | data: { 15 | name: "Code in the Dark 2023", 16 | slug: "2023", 17 | }, 18 | }) 19 | 20 | const events = [event1, event2] 21 | 22 | const promiseParticipants = participants.map((data) => { 23 | const rand = getRandomInteger(1) 24 | 25 | // const event = events[rand] 26 | const event = events[1] 27 | return prisma.user.create({ 28 | data: { 29 | ...data, 30 | play: { 31 | create: { 32 | eventSlug: event.slug, 33 | }, 34 | }, 35 | }, 36 | }) 37 | }) 38 | await Promise.all(promiseParticipants) 39 | } 40 | 41 | main() 42 | .then(async () => { 43 | await prisma.$disconnect() 44 | }) 45 | .catch(async (e) => { 46 | console.log(e) 47 | await prisma.$disconnect() 48 | process.exit(1) 49 | }) 50 | -------------------------------------------------------------------------------- /src/components/rounds-list/participant-image.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image" 2 | 3 | type ParticipantImageProps = { 4 | src: string 5 | alt: string 6 | lined: boolean 7 | hasWinner?: boolean 8 | winner?: boolean 9 | } 10 | 11 | const afterClasses = 12 | " after:content-[''] after:h-[1px] after:width-1/2 after:right-1 after:w-full after:bg-primary-100 after:absolute after:top-1/2 relative after:translate-x-1/2 after:left-1/2" 13 | 14 | export function ParticipantImage({ 15 | src, 16 | alt, 17 | lined, 18 | hasWinner = false, 19 | winner = false, 20 | }: ParticipantImageProps) { 21 | return ( 22 |
    23 |
    24 | {hasWinner && !winner && ( 25 |
    26 | )} 27 | {alt} 34 |
    35 |
    36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "sqlite" 10 | url = env("DATABASE_URL") 11 | } 12 | 13 | model Event { 14 | id String @id @default(uuid()) 15 | name String @unique 16 | slug String @unique 17 | createdAt DateTime @default(now()) 18 | updatedAt DateTime @updatedAt 19 | play Play[] 20 | } 21 | 22 | model User { 23 | id String @id @default(uuid()) 24 | name String 25 | email String @unique 26 | github String @default("ghost") 27 | createdAt DateTime @default(now()) 28 | updatedAt DateTime @updatedAt 29 | play Play[] 30 | } 31 | 32 | model Play { 33 | userId String 34 | eventSlug String 35 | wannaPlay Boolean @default(false) 36 | gonnaPlay Boolean @default(false) 37 | groupId Int? 38 | winner Boolean @default(false) 39 | createdAt DateTime @default(now()) 40 | updatedAt DateTime @updatedAt 41 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 42 | event Event @relation(fields: [eventSlug], references: [slug]) 43 | 44 | @@id([userId, eventSlug]) 45 | } 46 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for considering contributing to project. 4 | 5 | ## Opening issues 6 | 7 | If you find a bug, please feel free to [open an issue](https://github.com/codeinthedarkbrasil/manage-citd/issues). 8 | 9 | If you taking the time to mention a problem, even a seemingly minor one, it is greatly appreciated, and a totally valid contribution to this project. Thank you! 10 | 11 | ## Fixing bugs 12 | 13 | 1. [Fork this repository](https://github.com/codeinthedarkbrasil/manage-citd/fork) and then clone it locally. 14 | 15 | 2. Create a topic branch for your changes: 16 | 17 | ```bash 18 | git checkout -b fix/fix-for-that-thing 19 | ``` 20 | 3. Commit a failing test for the bug: 21 | 22 | ```bash 23 | git commit -am "fix: adds a failing test to demonstrate that thing" 24 | ``` 25 | 26 | 4. Commit a fix that makes the test pass: 27 | 28 | ```bash 29 | git commit -am "fix: adds a fix for that thing!" 30 | ``` 31 | 32 | 6. If everything looks good, push to your fork: 33 | 34 | ```bash 35 | git push origin fix/fix-for-that-thing 36 | ``` 37 | 38 | 7. [Submit a pull request.](https://help.github.com/articles/creating-a-pull-request) 39 | 40 | 41 | ## Adding new features 42 | 43 | Thinking of adding a new feature? Cool! [Open an issue](https://github.com/codeinthedarkbrasil/manage-citd/issues) and let’s design it together. 44 | -------------------------------------------------------------------------------- /src/app/api/events/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server" 2 | import { prisma } from "@/prisma" 3 | import { registerEventSchema } from "@/shared/types" 4 | 5 | export async function GET() { 6 | const eventsData = await prisma.event.findMany({ 7 | select: { 8 | id: true, 9 | name: true, 10 | slug: true, 11 | _count: { 12 | select: { 13 | play: true, 14 | }, 15 | }, 16 | }, 17 | }) 18 | 19 | const events = eventsData.map((event) => ({ 20 | id: event.id, 21 | name: event.name, 22 | slug: event.slug, 23 | participantsCount: event._count.play, 24 | })) 25 | 26 | return NextResponse.json(events) 27 | } 28 | 29 | export async function POST(request: NextRequest) { 30 | const registerEventDataFromRequest = await request.json() 31 | const data = registerEventSchema.parse(registerEventDataFromRequest) 32 | const newEvent = await prisma.event.create({ 33 | data, 34 | select: { 35 | id: true, 36 | name: true, 37 | slug: true, 38 | _count: { 39 | select: { 40 | play: true, 41 | }, 42 | }, 43 | }, 44 | }) 45 | 46 | const event = { 47 | id: newEvent.id, 48 | name: newEvent.name, 49 | slug: newEvent.slug, 50 | participantsCount: newEvent._count.play, 51 | } 52 | 53 | return NextResponse.json(event) 54 | } 55 | -------------------------------------------------------------------------------- /prisma/migrations/20230719215737_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Event" ( 3 | "id" TEXT NOT NULL PRIMARY KEY, 4 | "name" TEXT NOT NULL, 5 | "slug" TEXT NOT NULL, 6 | "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 7 | "updatedAt" DATETIME NOT NULL 8 | ); 9 | 10 | -- CreateTable 11 | CREATE TABLE "User" ( 12 | "id" TEXT NOT NULL PRIMARY KEY, 13 | "name" TEXT NOT NULL, 14 | "email" TEXT NOT NULL, 15 | "github" TEXT NOT NULL DEFAULT 'ghost', 16 | "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 17 | "updatedAt" DATETIME NOT NULL 18 | ); 19 | 20 | -- CreateTable 21 | CREATE TABLE "Play" ( 22 | "userId" TEXT NOT NULL, 23 | "eventSlug" TEXT NOT NULL, 24 | "wannaPlay" BOOLEAN NOT NULL DEFAULT false, 25 | "gonnaPlay" BOOLEAN NOT NULL DEFAULT false, 26 | "groupId" INTEGER, 27 | "winner" BOOLEAN NOT NULL DEFAULT false, 28 | "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 29 | "updatedAt" DATETIME NOT NULL, 30 | 31 | PRIMARY KEY ("userId", "eventSlug"), 32 | CONSTRAINT "Play_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE, 33 | CONSTRAINT "Play_eventSlug_fkey" FOREIGN KEY ("eventSlug") REFERENCES "Event" ("slug") ON DELETE RESTRICT ON UPDATE CASCADE 34 | ); 35 | 36 | -- CreateIndex 37 | CREATE UNIQUE INDEX "Event_name_key" ON "Event"("name"); 38 | 39 | -- CreateIndex 40 | CREATE UNIQUE INDEX "Event_slug_key" ON "Event"("slug"); 41 | 42 | -- CreateIndex 43 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); 44 | -------------------------------------------------------------------------------- /src/shared/types/participant.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod" 2 | 3 | const participantSchema = z.object({ 4 | id: z.string(), 5 | name: z.string(), 6 | email: z.string(), 7 | github: z.string(), 8 | wannaPlay: z.coerce.boolean(), 9 | gonnaPlay: z.coerce.boolean(), 10 | winner: z.coerce.boolean(), 11 | }) 12 | 13 | const groupIdSchema = z.object({ 14 | groupId: z.number(), 15 | }) 16 | 17 | const groupIdNullableSchema = z.object({ 18 | groupId: z.number().nullable(), 19 | }) 20 | 21 | const participantOutsideGroupSchema = z.intersection( 22 | participantSchema, 23 | groupIdNullableSchema, 24 | ) 25 | const participantInGroupSchema = z.intersection( 26 | participantSchema, 27 | groupIdSchema, 28 | ) 29 | 30 | export const arrayOfParticipantsSchema = z.array(participantOutsideGroupSchema) 31 | 32 | export const arrayOfParticipantsInGroupSchema = z.array( 33 | participantInGroupSchema, 34 | ) 35 | 36 | export type Participant = z.infer 37 | export type ParticipantInGroup = z.infer 38 | 39 | export const registerParticipantSchema = z.object({ 40 | name: z.string().min(1, { message: "Name is required" }), 41 | email: z.string().email().min(1, { message: "Email is required" }), 42 | github: z.string().optional(), 43 | }) 44 | 45 | export type RegisterParticipant = z.infer 46 | 47 | export const participantSchemaWithId = z.object({ 48 | id: z.string().uuid(), 49 | }) 50 | export const editParticipantSchema = z.intersection( 51 | participantSchemaWithId, 52 | registerParticipantSchema, 53 | ) 54 | 55 | export type EditParticipant = z.infer 56 | -------------------------------------------------------------------------------- /src/app/events/[event]/participants/upload.ts: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import { prisma } from "@/prisma" 4 | 5 | export const upload = async (data: FormData) => { 6 | const file = data.get("file") 7 | const event = data.get("event") 8 | if (!file) { 9 | // TODO 10 | console.log("Não veio arquivo nenhum nem evento") 11 | return 12 | } 13 | 14 | if (!event) { 15 | // TODO 16 | console.log("Não veio o evento.") 17 | return 18 | } 19 | 20 | if (!(file instanceof File)) { 21 | // TODO 22 | console.log("Não é um arquivo válido") 23 | return 24 | } 25 | 26 | if (typeof event !== "string") { 27 | // TODO 28 | console.log("evento não é string") 29 | return 30 | } 31 | 32 | const bytes = await file.arrayBuffer() 33 | const csv = Buffer.from(bytes).toString() 34 | await saveInDb({ csv, event }) 35 | } 36 | 37 | type SaveInDbInput = { 38 | csv: string 39 | event: string 40 | } 41 | 42 | async function saveInDb({ csv, event }: SaveInDbInput) { 43 | const participants = csv.split("\n").slice(1) 44 | const participantsFiltered = participants.filter(Boolean) 45 | 46 | const promiseParticipants = participantsFiltered.map((line) => { 47 | const [, , , name, lastName, email] = line.split(",") 48 | 49 | return prisma.user.create({ 50 | data: { 51 | name: name.trim() + " " + lastName.trim(), 52 | email: email.trim(), 53 | play: { 54 | create: { 55 | eventSlug: event, 56 | }, 57 | }, 58 | }, 59 | }) 60 | }) 61 | 62 | const result = await Promise.allSettled(promiseParticipants) 63 | result 64 | .filter((r) => r.status === "rejected") 65 | .forEach((r) => console.log("Deu ruim na hora de salvar no banco:", r)) 66 | } 67 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "@/styles/globals.css" 2 | import { PropsWithChildren } from "react" 3 | import { sora } from "@/styles" 4 | import Image from "next/image" 5 | import { LayoutDashboard, Medal } from "lucide-react" 6 | import { Sidebar, SidebarItem, SidebarLink, SidebarList } from "@/components" 7 | 8 | export const metadata = { 9 | title: "Code in the Dark", 10 | description: "", 11 | } 12 | 13 | export default function RootLayout({ children }: PropsWithChildren) { 14 | return ( 15 | 16 | 17 | 18 | 19 | 20 |
    21 | 22 | CTD Logo 23 | 24 | 25 | 26 | 31 | 32 | 33 | 34 | 35 | 40 | 41 | 42 | 43 | 44 |
    45 |
    {children}
    46 |
    47 |
    48 | 49 | 50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /src/components/input/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes { 7 | icon?: React.ReactNode 8 | label?: string 9 | error?: string 10 | } 11 | 12 | const Input = React.forwardRef( 13 | ({ id, label, icon, error, children, ...props }, ref) => { 14 | return ( 15 |
    16 |
    17 | 20 |
    21 | {!!icon && ( 22 |
    23 | {icon} 24 |
    25 | )} 26 | 36 |
    37 |
    38 | {!!error && ( 39 | 40 | {error} 41 | 42 | )} 43 |
    44 | ) 45 | }, 46 | ) 47 | Input.displayName = "Input" 48 | 49 | export { Input } 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "citd", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "type-check": "tsc --project tsconfig.json --pretty --noEmit", 11 | "seed": "tsx prisma/seed/seed.ts", 12 | "seed-event": "tsx prisma/seed/seed-event.ts", 13 | "migrate": "prisma migrate dev", 14 | "reset-db": "prisma migrate reset", 15 | "jsontocsv": "tsx prisma/seed/json-to-csv.ts", 16 | "prepare": "husky install" 17 | }, 18 | "dependencies": { 19 | "@hookform/resolvers": "3.1.1", 20 | "@prisma/client": "4.16.0", 21 | "@radix-ui/react-checkbox": "1.0.4", 22 | "@radix-ui/react-dialog": "1.0.4", 23 | "@radix-ui/react-slot": "1.0.2", 24 | "@tanstack/react-query": "4.29.14", 25 | "@types/node": "20.3.1", 26 | "@types/react": "18.2.12", 27 | "@types/react-dom": "18.2.5", 28 | "autoprefixer": "10.4.14", 29 | "class-variance-authority": "0.6.0", 30 | "clsx": "1.2.1", 31 | "eslint": "8.42.0", 32 | "eslint-config-next": "13.4.5", 33 | "lucide-react": "0.246.0", 34 | "next": "13.4.5", 35 | "postcss": "8.4.24", 36 | "prettier": "2.8.8", 37 | "random-js": "2.1.0", 38 | "react": "18.2.0", 39 | "react-dom": "18.2.0", 40 | "react-hook-form": "7.45.1", 41 | "slugify": "1.6.6", 42 | "tailwind-merge": "1.13.2", 43 | "tailwindcss": "3.3.2", 44 | "tailwindcss-animate": "1.0.6", 45 | "typescript": "5.1.3", 46 | "zod": "3.21.4" 47 | }, 48 | "devDependencies": { 49 | "@commitlint/cli": "17.6.5", 50 | "@commitlint/config-conventional": "17.6.5", 51 | "@tanstack/eslint-plugin-query": "4.29.9", 52 | "eslint-plugin-tailwindcss": "3.13.0", 53 | "husky": "8.0.3", 54 | "prisma": "4.16.0", 55 | "tsx": "3.12.7" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/components/button/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | import { cn } from "@/lib/utils" 5 | import { Spinner } from "../spinner" 6 | 7 | const buttonVariants = cva( 8 | ` 9 | inline-flex 10 | items-center 11 | justify-center 12 | font-sans 13 | text-[1.4rem] 14 | font-bold 15 | transition-colors 16 | focus-visible:outline-none 17 | focus-visible:ring-2 18 | focus-visible:ring-offset-2 19 | disabled:pointer-events-none 20 | disabled:opacity-50 21 | `, 22 | { 23 | variants: { 24 | variant: { 25 | primary: "bg-primary-100 text-neutral-100 hover:bg-primary-100/80", 26 | text: "text-primary-100 hover:bg-primary-100 hover:text-neutral-100", 27 | file: "pointer-events-none absolute z-[base] w-full bg-neutral-100 text-primary-100 group-hover:cursor-pointer group-hover:bg-primary-100 group-hover:text-neutral-100", 28 | }, 29 | size: { 30 | sm: "h-[42px] rounded-1 px-2", 31 | }, 32 | }, 33 | defaultVariants: { 34 | variant: "primary", 35 | size: "sm", 36 | }, 37 | }, 38 | ) 39 | 40 | export interface ButtonProps 41 | extends React.ButtonHTMLAttributes, 42 | VariantProps { 43 | asChild?: boolean 44 | loading?: boolean 45 | } 46 | 47 | const Button = React.forwardRef( 48 | ({ variant, size, asChild = false, loading, children, ...props }, ref) => { 49 | const Component = asChild ? Slot : "button" 50 | return ( 51 | 56 | {loading ? : children} 57 | 58 | ) 59 | }, 60 | ) 61 | Button.displayName = "Button" 62 | 63 | export { Button } 64 | -------------------------------------------------------------------------------- /src/app/api/events/[event]/participants/[participantId]/select-new-random-player/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server" 2 | import { getRandomInteger } from "@/lib" 3 | import { prisma } from "@/prisma" 4 | 5 | type SelectNewRandomPlayerInput = { 6 | params: { 7 | event: string 8 | participantId: string 9 | } 10 | } 11 | export async function POST( 12 | request: NextRequest, 13 | { params }: SelectNewRandomPlayerInput, 14 | ) { 15 | const { participantId, event } = params 16 | // TODO: Validar com Zod 17 | const { groupId }: { groupId: number } = await request.json() 18 | 19 | return prisma.$transaction(async (prisma) => { 20 | const usersThatWannaPlay = await prisma.user.findMany({ 21 | select: { 22 | id: true, 23 | }, 24 | where: { 25 | play: { 26 | some: { 27 | wannaPlay: true, 28 | gonnaPlay: false, 29 | }, 30 | }, 31 | }, 32 | }) 33 | 34 | if (usersThatWannaPlay.length === 0) { 35 | return NextResponse.json( 36 | { 37 | message: 38 | "Selecione mais de 16 pessoas para participar antes de tentar trocar alguém.", 39 | }, 40 | { 41 | status: 400, 42 | }, 43 | ) 44 | } 45 | 46 | const rnd = getRandomInteger(usersThatWannaPlay.length - 1) 47 | const randomUserId = usersThatWannaPlay[rnd].id 48 | 49 | await prisma.play.update({ 50 | where: { 51 | userId_eventSlug: { 52 | userId: participantId, 53 | eventSlug: event, 54 | }, 55 | }, 56 | data: { 57 | wannaPlay: false, 58 | gonnaPlay: false, 59 | groupId: null, 60 | }, 61 | }) 62 | 63 | await prisma.play.update({ 64 | where: { 65 | userId_eventSlug: { 66 | userId: randomUserId, 67 | eventSlug: event, 68 | }, 69 | }, 70 | data: { 71 | groupId, 72 | gonnaPlay: true, 73 | }, 74 | }) 75 | 76 | return NextResponse.json({ 77 | status: 204, 78 | }) 79 | }) 80 | } 81 | -------------------------------------------------------------------------------- /src/components/checkbox/checkbox.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox" 5 | import { Check } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | export const Checkbox = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, ...props }, ref) => ( 13 | 20 | 23 | 24 | 25 | 26 | )) 27 | 28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName 29 | 30 | const debounce = (fn: T, wait: number) => { 31 | let timer: ReturnType 32 | return async (...args: A[]) => { 33 | clearTimeout(timer) 34 | timer = setTimeout(() => fn(...args), wait) 35 | } 36 | } 37 | 38 | export const DebouncedCheckbox = React.forwardRef< 39 | React.ElementRef, 40 | React.ComponentPropsWithoutRef 41 | >(({ onCheckedChange, ...props }, ref) => { 42 | const onChange = onCheckedChange ?? ((_checked: unknown) => null) 43 | const debouncedOnChange = React.useRef(debounce(onChange, 1000)) 44 | return ( 45 | 50 | ) 51 | }) 52 | 53 | DebouncedCheckbox.displayName = CheckboxPrimitive.Root.displayName 54 | -------------------------------------------------------------------------------- /src/components/modal/modal.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as ModalPrimitive from "@radix-ui/react-dialog" 3 | import { X } from "lucide-react" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const Modal = ModalPrimitive.Root 8 | 9 | const ModalTrigger = ModalPrimitive.Trigger 10 | 11 | const ModalPortal = ({ 12 | className, 13 | ...props 14 | }: ModalPrimitive.DialogPortalProps) => ( 15 | 16 | ) 17 | ModalPortal.displayName = ModalPrimitive.Portal.displayName 18 | 19 | const ModalOverlay = React.forwardRef< 20 | React.ElementRef, 21 | React.ComponentPropsWithoutRef 22 | >(({ className, ...props }, ref) => ( 23 | 31 | )) 32 | ModalOverlay.displayName = ModalPrimitive.Overlay.displayName 33 | 34 | const ModalContent = React.forwardRef< 35 | React.ElementRef, 36 | React.ComponentPropsWithoutRef 37 | >(({ className, children, ...props }, ref) => ( 38 | 39 | 40 | 48 | {children} 49 | 50 | 51 | Close 52 | 53 | 54 | 55 | )) 56 | 57 | export { Modal, ModalTrigger, ModalContent } 58 | -------------------------------------------------------------------------------- /src/app/events/[event]/table-participants.tsx: -------------------------------------------------------------------------------- 1 | import type { EditParticipant, Participant } from "@/shared/types" 2 | import { 3 | DebouncedCheckbox, 4 | Table, 5 | TableHeader, 6 | TableBody, 7 | TableRow, 8 | TableHead, 9 | TableCell, 10 | } from "@/components" 11 | import { Trash2 as RemoveIcon } from "lucide-react" 12 | 13 | type TableBodyProps = { 14 | participants: Participant[] 15 | onCheckParticipant: (args: { id: string; checked: boolean }) => void 16 | onRemoveParticipant: (id: string) => void 17 | editParticipantModal: (data: EditParticipant) => React.ReactNode 18 | } 19 | 20 | export function TableParticipants({ 21 | participants, 22 | onCheckParticipant, 23 | onRemoveParticipant, 24 | editParticipantModal, 25 | }: TableBodyProps) { 26 | return ( 27 | 28 | 29 | 30 | Sorteio 31 | Nome 32 | Email 33 | Github 34 | Ações 35 | 36 | 37 | 38 | {participants.map((participant) => ( 39 | 43 | 44 | { 48 | if (typeof checked === "boolean") { 49 | onCheckParticipant({ id: participant.id, checked }) 50 | } 51 | }} 52 | /> 53 | 54 | {participant.name} 55 | {participant.email} 56 | 57 | 61 | @{participant.github} 62 | 63 | 64 | 65 |
    66 | {participant.gonnaPlay === false && ( 67 | 70 | )} 71 | 72 | {editParticipantModal(participant)} 73 |
    74 |
    75 |
    76 | ))} 77 |
    78 |
    79 | ) 80 | } 81 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const radiusAndSpacing = { 2 | 0: "0", 3 | 1: "8px", 4 | 1.5: "12px", 5 | 2: "16px", 6 | 3: "24px", 7 | 4: "32px", 8 | 8: "64px", 9 | 9: "72px", 10 | 10: "80px", 11 | 16: "128px", 12 | 17: "136px", 13 | } 14 | 15 | /** @type {import('tailwindcss').Config} */ 16 | module.exports = { 17 | content: ["./src/**/*.{js,ts,jsx,tsx,mdx}"], 18 | theme: { 19 | spacing: radiusAndSpacing, 20 | borderRadius: { 21 | ...radiusAndSpacing, 22 | full: "9999px", 23 | }, 24 | colors: { 25 | "current-color": "currentColor", 26 | primary: { 27 | 100: "#2ECB7A", 28 | }, 29 | neutral: { 30 | 100: "#101217", 31 | 200: "#272B35", 32 | 300: "#535A6B", 33 | 400: "#706F75", 34 | 500: "#8B8C8E", 35 | 900: "#FFFFFF", 36 | }, 37 | danger: { 38 | 100: "#A14445", 39 | }, 40 | }, 41 | fontSize: { 42 | "body-xs": "1.2rem", 43 | "body-sm": "1.4rem", 44 | "body-md": "2.0rem", 45 | "body-lg": "2.4rem", 46 | "title-sm": "2.0rem", 47 | "title-lg": "4.0rem", 48 | }, 49 | zIndex: { 50 | hidden: -1, 51 | base: 5, 52 | above: 10, 53 | aboveAll: 15, 54 | }, 55 | screens: { 56 | xs: "0px", 57 | sm: "475px", 58 | md: "920px", 59 | lg: "1280px", 60 | xl: "1920px", 61 | }, 62 | extend: { 63 | fontFamily: { 64 | sans: ["var(--font-sans)"], 65 | }, 66 | width: { 67 | sidebar: "52px", 68 | }, 69 | maxWidth: { 70 | wrapper: "924px", 71 | }, 72 | keyframes: { 73 | fillAnimation: { 74 | "0%": { 75 | transform: "scale(0)", 76 | }, 77 | "50%": { 78 | transform: "scale(1.25)", 79 | }, 80 | "100%": { 81 | transform: "scale(1)", 82 | }, 83 | }, 84 | rotate: { 85 | "100%": { 86 | transform: "rotate(360deg)", 87 | }, 88 | }, 89 | dash: { 90 | "0%": { 91 | strokeDasharray: "1, 150", 92 | strokeDashoffset: 0, 93 | }, 94 | "50%": { 95 | strokeDasharray: "90, 150", 96 | }, 97 | "100%": { 98 | strokeDasharray: "90, 150", 99 | strokeDashoffset: -124, 100 | }, 101 | }, 102 | }, 103 | animation: { 104 | fillAnimation: "fillAnimation 300ms forwards", 105 | dash: "dash 1.5s ease-in-out infinite", 106 | rotate: "rotate 2s linear infinite", 107 | }, 108 | }, 109 | }, 110 | plugins: [require("tailwindcss-animate")], 111 | } 112 | -------------------------------------------------------------------------------- /src/app/api/events/[event]/participants/select-participants/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server" 2 | import { prisma } from "@/prisma" 3 | import { ParticipantInGroup } from "@/shared/types" 4 | 5 | type GetSelectedParticipantsInput = { 6 | params: { 7 | event: string 8 | } 9 | } 10 | 11 | export async function GET( 12 | _: unknown, 13 | { params }: GetSelectedParticipantsInput, 14 | ) { 15 | const users = await prisma.user.findMany({ 16 | include: { 17 | play: { 18 | select: { 19 | wannaPlay: true, 20 | gonnaPlay: true, 21 | winner: true, 22 | groupId: true, 23 | }, 24 | where: { 25 | event: { 26 | slug: params.event, 27 | }, 28 | }, 29 | orderBy: { 30 | groupId: { 31 | sort: "asc", 32 | }, 33 | }, 34 | }, 35 | }, 36 | 37 | where: { 38 | play: { 39 | some: { 40 | gonnaPlay: true, 41 | }, 42 | }, 43 | }, 44 | }) 45 | 46 | const result: ParticipantInGroup[] = users.map((user) => { 47 | const groupId = user.play[0].groupId ?? -1 48 | return { 49 | id: user.id, 50 | name: user.name, 51 | email: user.email, 52 | github: user.github, 53 | wannaPlay: user.play[0].wannaPlay, 54 | gonnaPlay: user.play[0].gonnaPlay, 55 | winner: user.play[0].winner, 56 | groupId, 57 | } 58 | }) 59 | 60 | return NextResponse.json(result) 61 | } 62 | 63 | type SelectParticipantsInput = { 64 | params: { 65 | event: string 66 | } 67 | } 68 | 69 | export async function POST( 70 | request: NextRequest, 71 | { params }: SelectParticipantsInput, 72 | ) { 73 | const { event } = params 74 | // TODO: Validar o body com zod 75 | const body: string[] = await request.json() 76 | let group = 1 77 | let usersInGroup = 0 78 | 79 | const updateUsers = body.map((id) => { 80 | if (usersInGroup === 4) { 81 | usersInGroup = 0 82 | group++ 83 | } 84 | 85 | usersInGroup++ 86 | 87 | return prisma.play.update({ 88 | where: { 89 | userId_eventSlug: { 90 | eventSlug: event, 91 | userId: id, 92 | }, 93 | }, 94 | data: { 95 | gonnaPlay: true, 96 | groupId: group, 97 | }, 98 | }) 99 | }) 100 | 101 | await prisma.$transaction([ 102 | prisma.play.updateMany({ 103 | where: { 104 | eventSlug: event, 105 | gonnaPlay: true, 106 | }, 107 | data: { 108 | gonnaPlay: false, 109 | groupId: null, 110 | winner: false, 111 | }, 112 | }), 113 | 114 | ...updateUsers, 115 | ]) 116 | 117 | return new NextResponse() 118 | } 119 | -------------------------------------------------------------------------------- /src/app/events/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useState } from "react" 4 | import Link from "next/link" 5 | import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" 6 | import { RegisterEvent } from "@/shared/types" 7 | import { 8 | Button, 9 | Card, 10 | CardText, 11 | CardTitle, 12 | Modal, 13 | ModalTrigger, 14 | RegisterEventModal, 15 | } from "@/components" 16 | import { getEvents, registerEvent } from "./data-events" 17 | 18 | export default function Events() { 19 | const queryClient = useQueryClient() 20 | 21 | const eventsQuery = useQuery({ 22 | queryKey: ["events"], 23 | queryFn: getEvents, 24 | }) 25 | 26 | const registerEventMutation = useMutation({ 27 | mutationFn: registerEvent, 28 | onSuccess: () => { 29 | queryClient.invalidateQueries({ queryKey: ["events"] }) 30 | }, 31 | }) 32 | 33 | const events = eventsQuery.data ?? [] 34 | 35 | const handleRegisterEvent = async (data: RegisterEvent) => { 36 | registerEventMutation.mutate(data) 37 | } 38 | 39 | return ( 40 |
    41 |
    42 |

    Eventos

    43 | 49 |
    50 |
    51 | {eventsQuery.isLoading &&

    Carregando eventos...

    } 52 | {events.length === 0 && eventsQuery.isSuccess && ( 53 |

    Nenhum evento cadastrado.

    54 | )} 55 | {events.map((event) => ( 56 | 57 | 58 |
    59 | {event.name} 60 | {event.participantsCount} Participantes 61 |
    62 |
    63 | 64 | ))} 65 |
    66 |
    67 | ) 68 | } 69 | 70 | type RegisterEventModalContainerProps = { 71 | onRegisterEvent: (data: RegisterEvent) => Promise 72 | isLoading: boolean 73 | isSuccess: boolean 74 | error: unknown 75 | } 76 | function RegisterEventModalContainer({ 77 | onRegisterEvent, 78 | isLoading, 79 | isSuccess, 80 | error, 81 | }: RegisterEventModalContainerProps) { 82 | const [open, setOpen] = useState(false) 83 | 84 | return ( 85 | 86 | 87 | 88 | 89 | 90 | 97 | 98 | ) 99 | } 100 | -------------------------------------------------------------------------------- /src/components/table/table.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Table = React.forwardRef< 6 | HTMLTableElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
    10 | 11 | 12 | )) 13 | Table.displayName = "Table" 14 | 15 | const TableHeader = React.forwardRef< 16 | HTMLTableSectionElement, 17 | React.HTMLAttributes 18 | >(({ className, ...props }, ref) => ( 19 | 20 | )) 21 | TableHeader.displayName = "TableHeader" 22 | 23 | const TableBody = React.forwardRef< 24 | HTMLTableSectionElement, 25 | React.HTMLAttributes 26 | >(({ className, ...props }, ref) => ( 27 | 28 | )) 29 | TableBody.displayName = "TableBody" 30 | 31 | const TableFooter = React.forwardRef< 32 | HTMLTableSectionElement, 33 | React.HTMLAttributes 34 | >(({ className, ...props }, ref) => ( 35 | 40 | )) 41 | TableFooter.displayName = "TableFooter" 42 | 43 | const TableRow = React.forwardRef< 44 | HTMLTableRowElement, 45 | React.HTMLAttributes 46 | >(({ className, ...props }, ref) => ( 47 | 55 | )) 56 | TableRow.displayName = "TableRow" 57 | 58 | const TableHead = React.forwardRef< 59 | HTMLTableCellElement, 60 | React.ThHTMLAttributes 61 | >(({ className, ...props }, ref) => ( 62 |
    70 | )) 71 | TableHead.displayName = "TableHead" 72 | 73 | const TableCell = React.forwardRef< 74 | HTMLTableCellElement, 75 | React.TdHTMLAttributes 76 | >(({ className, ...props }, ref) => ( 77 | 85 | )) 86 | TableCell.displayName = "TableCell" 87 | 88 | const TableCaption = React.forwardRef< 89 | HTMLTableCaptionElement, 90 | React.HTMLAttributes 91 | >(({ className, ...props }, ref) => ( 92 |
    97 | )) 98 | TableCaption.displayName = "TableCaption" 99 | 100 | export { 101 | Table, 102 | TableHeader, 103 | TableBody, 104 | TableFooter, 105 | TableHead, 106 | TableRow, 107 | TableCell, 108 | TableCaption, 109 | } 110 | -------------------------------------------------------------------------------- /prisma/seed/participants.ts: -------------------------------------------------------------------------------- 1 | import { RegisterParticipant } from "@/shared/types" 2 | 3 | export const participants: RegisterParticipant[] = [ 4 | { 5 | name: "Eal Nottle", 6 | email: "enottle0@php.net", 7 | }, 8 | { 9 | name: "Sibyl Spence", 10 | email: "sspence1@surveymonkey.com", 11 | }, 12 | { 13 | name: "Shari Coan", 14 | email: "scoan2@hibu.com", 15 | }, 16 | { 17 | name: "Sheena Bend", 18 | email: "sbend3@blogger.com", 19 | }, 20 | { 21 | name: "Bartlet Boote", 22 | email: "bboote4@utexas.edu", 23 | }, 24 | { 25 | name: "Jourdan Kemson", 26 | email: "jkemson5@wikimedia.org", 27 | }, 28 | { 29 | name: "Rudy McGoon", 30 | email: "rmcgoon6@walmart.com", 31 | }, 32 | { 33 | name: "Katalin Grief", 34 | email: "kgrief7@techcrunch.com", 35 | }, 36 | { 37 | name: "Tuckie Hartzenberg", 38 | email: "thartzenberg8@who.int", 39 | }, 40 | { 41 | name: "Gabby Sivior", 42 | email: "gsivior9@slate.com", 43 | }, 44 | { 45 | name: "Bliss Madgwich", 46 | email: "bmadgwicha@webmd.com", 47 | }, 48 | { 49 | name: "Shaylah Wetherell", 50 | email: "swetherellb@forbes.com", 51 | }, 52 | { 53 | name: "Barnie Chsteney", 54 | email: "bchsteneyc@wix.com", 55 | }, 56 | { 57 | name: "Orlan Beamond", 58 | email: "obeamondd@businesswire.com", 59 | }, 60 | { 61 | name: "Jacinta McKevin", 62 | email: "jmckevine@tripod.com", 63 | }, 64 | { 65 | name: "Hasheem Teers", 66 | email: "hteersf@163.com", 67 | }, 68 | { 69 | name: "Wernher Grim", 70 | email: "wgrimg@jalbum.net", 71 | }, 72 | { 73 | name: "Nelia Tambling", 74 | email: "ntamblingh@sciencedirect.com", 75 | }, 76 | { 77 | name: "Tore Cumberlidge", 78 | email: "tcumberlidgei@ed.gov", 79 | }, 80 | { 81 | name: "Carmelle Padson", 82 | email: "cpadsonj@cmu.edu", 83 | }, 84 | { 85 | name: "Prent Stannett", 86 | email: "pstannettk@vinaora.com", 87 | }, 88 | { 89 | name: "Jennine Cundict", 90 | email: "jcundictl@last.fm", 91 | }, 92 | { 93 | name: "Micaela Ebbotts", 94 | email: "mebbottsm@etsy.com", 95 | }, 96 | { 97 | name: "Case Wilsher", 98 | email: "cwilshern@seesaa.net", 99 | }, 100 | { 101 | name: "Rubia Nielson", 102 | email: "rnielsono@ustream.tv", 103 | }, 104 | { 105 | name: "Darn Agget", 106 | email: "daggetp@51.la", 107 | }, 108 | { 109 | name: "Lottie Gilligan", 110 | email: "lgilliganq@princeton.edu", 111 | }, 112 | { 113 | name: "Brucie Pollock", 114 | email: "bpollockr@shop-pro.jp", 115 | }, 116 | { 117 | name: "Laina Cripin", 118 | email: "lcripins@dedecms.com", 119 | }, 120 | { 121 | name: "Garth Padmore", 122 | email: "gpadmoret@exblog.jp", 123 | }, 124 | { 125 | name: "Madlin Boler", 126 | email: "mboleru@microsoft.com", 127 | }, 128 | { 129 | name: "Timmy Cobden", 130 | email: "tcobdenv@360.cn", 131 | }, 132 | { 133 | name: "Ingaborg Ashfold", 134 | email: "iashfoldw@telegraph.co.uk", 135 | }, 136 | { 137 | name: "Mitzi Rollett", 138 | email: "mrollettx@ocn.ne.jp", 139 | }, 140 | { 141 | name: "Keriann Spellworth", 142 | email: "kspellworthy@stumbleupon.com", 143 | }, 144 | { 145 | name: "Donovan Hucquart", 146 | email: "dhucquartz@github.com", 147 | }, 148 | { 149 | name: "Saba Klaassens", 150 | email: "sklaassens10@nytimes.com", 151 | }, 152 | { 153 | name: "Niki Penella", 154 | email: "npenella11@kickstarter.com", 155 | }, 156 | { 157 | name: "Gerda Moxsom", 158 | email: "gmoxsom12@guardian.co.uk", 159 | }, 160 | { 161 | name: "Dion Jakubiak", 162 | email: "djakubiak13@prlog.org", 163 | }, 164 | ] 165 | -------------------------------------------------------------------------------- /src/app/events/[event]/participants/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { 4 | FinalRound, 5 | ParticipantImage, 6 | ParticipantItem, 7 | ParticipantName, 8 | ParticipantsList, 9 | RoundItem, 10 | RoundTitle, 11 | RoundsList, 12 | } from "@/components" 13 | import { EventProps, Round } from "@/shared/types" 14 | import { useQuery } from "@tanstack/react-query" 15 | 16 | import { getSelectedParticipants } from "./data-participants" 17 | import Link from "next/link" 18 | 19 | export default function Participants({ params }: EventProps) { 20 | const { event } = params 21 | 22 | const query = useQuery({ 23 | queryKey: ["selected-participants", { event }], 24 | queryFn: () => getSelectedParticipants(event), 25 | }) 26 | 27 | const selectedParticipants = query.data ?? [] 28 | 29 | const rounds = selectedParticipants.reduce((acc, participant) => { 30 | const groupId = participant.groupId 31 | 32 | acc[groupId - 1] = acc[groupId - 1] ?? {} 33 | acc[groupId - 1].participants = acc[groupId - 1].participants ?? [] 34 | acc[groupId - 1].participants.push(participant) 35 | return acc 36 | }, []) 37 | 38 | const finalRound = rounds.reduce( 39 | (acc, round) => { 40 | const winner = round.participants.find( 41 | (participant) => participant.winner === true, 42 | ) 43 | if (winner) { 44 | acc.participants.push(winner) 45 | } 46 | return acc 47 | }, 48 | { participants: [] }, 49 | ) 50 | 51 | return ( 52 |
    53 |

    54 | Code in The Dark {params.event} 55 |

    56 |

    57 | Gerenciar Chave 58 |

    59 | 60 | {rounds.map((round, index) => ( 61 | 62 | 63 | {index + 1}º Round 64 | 65 | 66 | {round.participants.map((participant, index) => ( 67 | 68 |
    69 | p.winner)} 74 | winner={participant.winner} 75 | /> 76 |
    77 | 78 | 79 | {round.participants.some((p) => p.winner) 80 | ? participant.name 81 | : "??"} 82 | 83 |
    84 | ))} 85 |
    86 |
    87 | ))} 88 |
    89 | 90 | {finalRound.participants.length > 0 && ( 91 | 92 | 93 | Final 94 | 95 | 96 | {finalRound.participants.map((participant, index) => ( 97 | 98 | 107 | {participant.name} 108 | 109 | ))} 110 | 111 | 112 | )} 113 |
    114 | ) 115 | } 116 | -------------------------------------------------------------------------------- /src/components/edit-participant-modal/modal.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react" 2 | 3 | import { Button } from "../button" 4 | import { Input } from "../input" 5 | import { ModalContent } from "../modal" 6 | import { User, AtSign, Github, ArrowLeft, XCircle } from "lucide-react" 7 | 8 | import { useForm, SubmitHandler } from "react-hook-form" 9 | import { zodResolver } from "@hookform/resolvers/zod" 10 | 11 | import { EditParticipant, editParticipantSchema } from "@/shared/types" 12 | 13 | export type EditParticipantModalProps = { 14 | onEditParticipant: (data: EditParticipant) => Promise 15 | loading?: boolean 16 | success: boolean | null 17 | error: string | null 18 | initialData: EditParticipant 19 | } 20 | 21 | export function EditParticipantModal({ 22 | onEditParticipant, 23 | loading, 24 | success, 25 | error, 26 | initialData, 27 | }: EditParticipantModalProps) { 28 | const { 29 | register, 30 | handleSubmit, 31 | formState: { errors }, 32 | reset, 33 | } = useForm({ 34 | resolver: zodResolver(editParticipantSchema), 35 | }) 36 | 37 | const [internalError, setInternalError] = useState(error) 38 | 39 | useEffect(() => { 40 | setInternalError(error) 41 | }, [success, error]) 42 | 43 | useEffect(() => { 44 | if (success) { 45 | reset() 46 | } 47 | }, [success, reset]) 48 | 49 | const handleEditParticipant: SubmitHandler = async ( 50 | data, 51 | ) => { 52 | await onEditParticipant(data) 53 | } 54 | 55 | const handleBackToForm = () => { 56 | setInternalError(null) 57 | } 58 | 59 | return ( 60 | 61 |
    62 | {internalError && ( 63 | <> 64 |
    65 | 68 |
    69 | 70 |
    71 |
    72 | {!!internalError && ( 73 | <> 74 | 75 |

    76 | {error} 77 |

    78 | 79 | )} 80 |
    81 |
    82 | 83 | )} 84 | 85 | {!internalError && ( 86 | <> 87 |

    88 | Editar Participante 89 |

    90 |
    94 | 95 | } 99 | error={errors.name?.message} 100 | {...register("name")} 101 | /> 102 | } 106 | error={errors.email?.message} 107 | {...register("email")} 108 | /> 109 | } 113 | error={errors.github?.message} 114 | {...register("github")} 115 | /> 116 | 117 | 120 |
    121 | 122 | )} 123 |
    124 |
    125 | ) 126 | } 127 | -------------------------------------------------------------------------------- /src/components/register-event-modal/modal.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react" 2 | 3 | import { Button } from "../button" 4 | import { Input } from "../input" 5 | import { ModalContent } from "../modal" 6 | import { 7 | User, 8 | ArrowDownAZ, 9 | ArrowLeft, 10 | CheckCircle, 11 | XCircle, 12 | } from "lucide-react" 13 | 14 | import { useForm, SubmitHandler } from "react-hook-form" 15 | import { zodResolver } from "@hookform/resolvers/zod" 16 | 17 | import { RegisterEvent, registerEventSchema } from "@/shared/types" 18 | 19 | export type RegisterEventModalProps = { 20 | onRegisterEvent: (data: RegisterEvent) => Promise 21 | loading?: boolean 22 | success: boolean | null 23 | error: string | null 24 | open: boolean 25 | } 26 | 27 | export function RegisterEventModal({ 28 | onRegisterEvent, 29 | loading, 30 | success, 31 | error, 32 | open, 33 | }: RegisterEventModalProps) { 34 | const { 35 | register, 36 | handleSubmit, 37 | formState: { errors }, 38 | reset, 39 | } = useForm({ 40 | resolver: zodResolver(registerEventSchema), 41 | }) 42 | 43 | const [internalSuccess, setInternalSuccess] = useState(success) 44 | const [internalError, setInternalError] = useState(error) 45 | 46 | useEffect(() => { 47 | if (open === false) { 48 | handleBackToForm() 49 | } 50 | }, [open]) 51 | 52 | useEffect(() => { 53 | setInternalSuccess(success) 54 | setInternalError(error) 55 | }, [success, error]) 56 | 57 | useEffect(() => { 58 | if (success) { 59 | reset() 60 | } 61 | }, [success, reset]) 62 | 63 | const handleRegisterEvent: SubmitHandler = async (data) => { 64 | await onRegisterEvent(data) 65 | } 66 | 67 | const handleBackToForm = () => { 68 | setInternalSuccess(null) 69 | setInternalError(null) 70 | } 71 | 72 | return ( 73 | 74 |
    75 | {(internalSuccess || internalError) && ( 76 | <> 77 |
    78 | 81 |
    82 | 83 |
    84 |
    85 | {internalSuccess && ( 86 | <> 87 | 88 |

    89 | Registrado com sucesso! 90 |

    91 | 92 | )} 93 | {!!internalError && ( 94 | <> 95 | 96 |

    97 | {error} 98 |

    99 | 100 | )} 101 |
    102 |
    103 | 104 | )} 105 | 106 | {!internalSuccess && !internalError && ( 107 | <> 108 |

    109 | Novo Evento 110 |

    111 |
    115 | } 118 | error={errors.name?.message} 119 | {...register("name")} 120 | /> 121 | 125 | } 126 | error={errors.slug?.message} 127 | {...register("slug")} 128 | /> 129 | 130 | 133 |
    134 | 135 | )} 136 |
    137 |
    138 | ) 139 | } 140 | -------------------------------------------------------------------------------- /src/components/register-participant-modal/modal.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react" 2 | 3 | import { Button } from "../button" 4 | import { Input } from "../input" 5 | import { ModalContent } from "../modal" 6 | import { 7 | User, 8 | AtSign, 9 | Github, 10 | ArrowLeft, 11 | CheckCircle, 12 | XCircle, 13 | } from "lucide-react" 14 | 15 | import { useForm, SubmitHandler } from "react-hook-form" 16 | import { zodResolver } from "@hookform/resolvers/zod" 17 | 18 | import { RegisterParticipant, registerParticipantSchema } from "@/shared/types" 19 | 20 | export type RegisterParticipantModalProps = { 21 | onRegisterParticipant: (data: RegisterParticipant) => Promise 22 | loading?: boolean 23 | success: boolean | null 24 | error: string | null 25 | open: boolean 26 | } 27 | 28 | export function RegisterParticipantModal({ 29 | onRegisterParticipant, 30 | loading, 31 | success, 32 | error, 33 | open, 34 | }: RegisterParticipantModalProps) { 35 | const { 36 | register, 37 | handleSubmit, 38 | formState: { errors }, 39 | reset, 40 | } = useForm({ 41 | resolver: zodResolver(registerParticipantSchema), 42 | }) 43 | 44 | const [internalSuccess, setInternalSuccess] = useState(success) 45 | const [internalError, setInternalError] = useState(error) 46 | 47 | useEffect(() => { 48 | if (open === false) { 49 | handleBackToForm() 50 | } 51 | }, [open]) 52 | 53 | useEffect(() => { 54 | setInternalSuccess(success) 55 | setInternalError(error) 56 | }, [success, error]) 57 | 58 | useEffect(() => { 59 | if (success) { 60 | reset() 61 | } 62 | }, [success, reset]) 63 | 64 | const handleRegisterParticipant: SubmitHandler = async ( 65 | data, 66 | ) => { 67 | await onRegisterParticipant(data) 68 | } 69 | 70 | const handleBackToForm = () => { 71 | setInternalSuccess(null) 72 | setInternalError(null) 73 | } 74 | 75 | return ( 76 | 77 |
    78 | {(internalSuccess || internalError) && ( 79 | <> 80 |
    81 | 84 |
    85 | 86 |
    87 |
    88 | {internalSuccess && ( 89 | <> 90 | 91 |

    92 | Registrado com sucesso! 93 |

    94 | 95 | )} 96 | {!!internalError && ( 97 | <> 98 | 99 |

    100 | {error} 101 |

    102 | 103 | )} 104 |
    105 |
    106 | 107 | )} 108 | 109 | {!internalSuccess && !internalError && ( 110 | <> 111 |

    112 | Novo Participante 113 |

    114 |
    118 | } 121 | error={errors.name?.message} 122 | {...register("name")} 123 | /> 124 | } 127 | error={errors.email?.message} 128 | {...register("email")} 129 | /> 130 | } 133 | error={errors.github?.message} 134 | {...register("github")} 135 | /> 136 | 137 | 140 |
    141 | 142 | )} 143 |
    144 |
    145 | ) 146 | } 147 | -------------------------------------------------------------------------------- /src/app/events/[event]/participants/data-participants.ts: -------------------------------------------------------------------------------- 1 | import { 2 | arrayOfParticipantsSchema, 3 | arrayOfParticipantsInGroupSchema, 4 | type Participant, 5 | type ParticipantInGroup, 6 | type RegisterParticipant, 7 | EditParticipant, 8 | CITDEvent, 9 | eventSchema, 10 | } from "@/shared/types" 11 | 12 | export async function getParticipants(event: string): Promise { 13 | const data = await fetch(`/api/events/${event}/participants`) 14 | const participants = await data.json() 15 | return arrayOfParticipantsSchema.parse(participants) 16 | } 17 | 18 | export async function getSelectedParticipants( 19 | event: string, 20 | ): Promise { 21 | const data = await fetch( 22 | `/api/events/${event}/participants/select-participants`, 23 | ) 24 | const selectedParticipants = await data.json() 25 | return arrayOfParticipantsInGroupSchema.parse(selectedParticipants) 26 | } 27 | 28 | type SetSelectedParticipantsInput = { 29 | event: string 30 | ids: string[] 31 | } 32 | export async function setSelectedParticipants({ 33 | event, 34 | ids, 35 | }: SetSelectedParticipantsInput) { 36 | await fetch(`/api/events/${event}/participants/select-participants`, { 37 | method: "POST", 38 | headers: { 39 | "Content-Type": "application/json", 40 | }, 41 | body: JSON.stringify(ids), 42 | }) 43 | } 44 | 45 | type CheckParticipantInput = { 46 | id: string 47 | event: string 48 | checked: boolean 49 | } 50 | 51 | export async function checkParticipant({ 52 | id, 53 | checked, 54 | event, 55 | }: CheckParticipantInput) { 56 | console.log("check participant checked?", checked) 57 | await fetch(`/api/events/${event}/participants/${id}/check-participant`, { 58 | method: "POST", 59 | body: JSON.stringify({ checked }), 60 | headers: { 61 | "content-type": "application/json", 62 | }, 63 | }) 64 | } 65 | 66 | type RegisterParticipantInput = { 67 | event: string 68 | data: RegisterParticipant 69 | } 70 | 71 | export async function registerParticipant({ 72 | event, 73 | data, 74 | }: RegisterParticipantInput) { 75 | const result = await fetch( 76 | `/api/events/${event}/participants/register-participant`, 77 | { 78 | method: "POST", 79 | body: JSON.stringify(data), 80 | headers: { 81 | "content-type": "application/json", 82 | }, 83 | }, 84 | ) 85 | 86 | const dataOrError = await result.json() 87 | 88 | if (!result.ok) { 89 | const { message } = dataOrError 90 | // TODO: handle the error in the future 91 | return Promise.reject(message) 92 | } 93 | 94 | return { data: dataOrError } 95 | } 96 | 97 | type EditParticipantInput = { 98 | event: string 99 | data: EditParticipant 100 | } 101 | 102 | export async function editParticipant({ event, data }: EditParticipantInput) { 103 | const result = await fetch( 104 | `/api/events/${event}/participants/edit-participant`, 105 | { 106 | method: "POST", 107 | body: JSON.stringify(data), 108 | headers: { 109 | "content-type": "application/json", 110 | }, 111 | }, 112 | ) 113 | 114 | const dataOrError = await result.json() 115 | 116 | if (!result.ok) { 117 | const { message } = dataOrError 118 | // TODO: handle the error in the future 119 | return Promise.reject(message) 120 | } 121 | 122 | return { data: dataOrError } 123 | } 124 | 125 | type SetWinnerInput = { 126 | userId: string 127 | event: string 128 | groupId: number 129 | } 130 | export async function setWinner({ userId, event, groupId }: SetWinnerInput) { 131 | await fetch(`/api/events/${event}/participants/${userId}/set-winner`, { 132 | method: "POST", 133 | body: JSON.stringify({ groupId }), 134 | headers: { 135 | "content-type": "application/json", 136 | }, 137 | }) 138 | } 139 | 140 | type SelectNewRandomPlayerInput = { 141 | userId: string 142 | event: string 143 | groupId: number 144 | } 145 | export async function selectNewRandomPlayer({ 146 | userId, 147 | event, 148 | groupId, 149 | }: SelectNewRandomPlayerInput) { 150 | await fetch( 151 | `/api/events/${event}/participants/${userId}/select-new-random-player`, 152 | { 153 | method: "POST", 154 | body: JSON.stringify({ groupId }), 155 | headers: { 156 | "content-type": "application/json", 157 | }, 158 | }, 159 | ) 160 | } 161 | 162 | type RemoveParticipantInput = { 163 | id: string 164 | event: string 165 | } 166 | export async function removeParticipant({ id, event }: RemoveParticipantInput) { 167 | await fetch(`/api/events/${event}/participants/${id}/remove-participant`, { 168 | method: "DELETE", 169 | }) 170 | } 171 | 172 | export async function getEvent(event: string): Promise { 173 | const result = await fetch(`/api/events/${event}`) 174 | if (!result.ok) { 175 | throw new Error("Evento não encontrado") 176 | } 177 | const data = await result.json() 178 | return eventSchema.parse(data) 179 | } 180 | -------------------------------------------------------------------------------- /src/app/events/[event]/participants/[groupId]/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { 4 | FinalRound, 5 | ParticipantImage, 6 | ParticipantItem, 7 | ParticipantName, 8 | ParticipantsList, 9 | } from "@/components" 10 | import { Round } from "@/shared/types" 11 | import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" 12 | import { RefreshCcw, ThumbsUp } from "lucide-react" 13 | 14 | import { 15 | getSelectedParticipants, 16 | selectNewRandomPlayer, 17 | setWinner, 18 | } from "@/app/events/[event]/participants/data-participants" 19 | import Link from "next/link" 20 | 21 | type ParticipantsInGroupProps = { 22 | params: { 23 | event: string 24 | groupId: string 25 | } 26 | } 27 | 28 | export default function ParticipantsInGroup({ 29 | params, 30 | }: ParticipantsInGroupProps) { 31 | const { event } = params 32 | const queryClient = useQueryClient() 33 | 34 | const query = useQuery({ 35 | queryKey: ["selected-participants", { event }], 36 | queryFn: () => getSelectedParticipants(event), 37 | }) 38 | 39 | const winnerMutation = useMutation({ 40 | mutationFn: setWinner, 41 | onSuccess: () => { 42 | queryClient.invalidateQueries({ 43 | queryKey: ["selected-participants", { event }], 44 | }) 45 | }, 46 | }) 47 | 48 | const selectNewRandomPlayerMutation = useMutation({ 49 | mutationFn: selectNewRandomPlayer, 50 | onSuccess: () => { 51 | queryClient.invalidateQueries({ 52 | queryKey: ["participants", { event }], 53 | }) 54 | queryClient.invalidateQueries({ 55 | queryKey: ["selected-participants", { event }], 56 | }) 57 | }, 58 | }) 59 | 60 | const selectedParticipants = query.data ?? [] 61 | 62 | const rounds = selectedParticipants.reduce((acc, participant) => { 63 | const groupId = participant.groupId 64 | 65 | acc[groupId - 1] = acc[groupId - 1] ?? {} 66 | acc[groupId - 1].participants = acc[groupId - 1].participants ?? [] 67 | acc[groupId - 1].participants.push(participant) 68 | return acc 69 | }, []) 70 | 71 | const finalRound = rounds.reduce( 72 | (acc, round) => { 73 | const winner = round.participants.find( 74 | (participant) => participant.winner === true, 75 | ) 76 | if (winner) { 77 | acc.participants.push(winner) 78 | } 79 | return acc 80 | }, 81 | { participants: [] }, 82 | ) 83 | 84 | const round = 85 | params.groupId === "final" ? finalRound : rounds[+params.groupId - 1] 86 | 87 | console.log({ round }) 88 | 89 | type HandleSetWinnerInput = { 90 | userId: string 91 | event: string 92 | groupId: number 93 | } 94 | 95 | const handleSetWinner = 96 | ({ userId, event, groupId }: HandleSetWinnerInput) => 97 | () => { 98 | winnerMutation.mutate({ userId, event, groupId }) 99 | } 100 | 101 | type HandleSelectNewRandomPlayerInput = { 102 | userId: string 103 | event: string 104 | groupId: number 105 | } 106 | const handleSelectNewRandomPlayer = 107 | ({ userId, event, groupId }: HandleSelectNewRandomPlayerInput) => 108 | () => { 109 | selectNewRandomPlayerMutation.mutate({ userId, event, groupId }) 110 | } 111 | 112 | return ( 113 |
    114 |

    115 | 116 | Code in The Dark {params.event} 117 | 118 |

    119 |

    120 | Grupo {params.groupId} 121 |

    122 | 123 | 124 | 125 | {round?.participants.map((participant, index) => ( 126 | 127 |
    128 | p.winner)} 137 | winner={participant.winner} 138 | /> 139 | 140 | {params.groupId !== "final" && ( 141 | <> 142 | 152 | 153 | 163 | 164 | )} 165 |
    166 | 167 | {participant.name} 168 |
    169 | ))} 170 |
    171 |
    172 |
    173 | ) 174 | } 175 | -------------------------------------------------------------------------------- /src/app/events/[event]/participants/fake-participants.ts: -------------------------------------------------------------------------------- 1 | import { Participant } from "@/shared/types" 2 | 3 | export const participants: Participant[] = [ 4 | { 5 | id: "6051318015", 6 | name: "Eal Nottle", 7 | email: "enottle0@php.net", 8 | github: "enottle0", 9 | wannaPlay: false, 10 | gonnaPlay: false, 11 | winner: false, 12 | groupId: null, 13 | }, 14 | { 15 | id: "3494260686", 16 | name: "Sibyl Spence", 17 | email: "sspence1@surveymonkey.com", 18 | github: "sspence1", 19 | wannaPlay: false, 20 | gonnaPlay: false, 21 | winner: false, 22 | groupId: null, 23 | }, 24 | { 25 | id: "9566202070", 26 | name: "Shari Coan", 27 | email: "scoan2@hibu.com", 28 | github: "scoan2", 29 | wannaPlay: false, 30 | gonnaPlay: false, 31 | winner: false, 32 | groupId: null, 33 | }, 34 | { 35 | id: "5491035706", 36 | name: "Sheena Bend", 37 | email: "sbend3@blogger.com", 38 | github: "sbend3", 39 | wannaPlay: false, 40 | gonnaPlay: false, 41 | winner: false, 42 | groupId: null, 43 | }, 44 | { 45 | id: "0027457519", 46 | name: "Bartlet Boote", 47 | email: "bboote4@utexas.edu", 48 | github: "bboote4", 49 | wannaPlay: false, 50 | gonnaPlay: false, 51 | winner: false, 52 | groupId: null, 53 | }, 54 | { 55 | id: "8197850668", 56 | name: "Jourdan Kemson", 57 | email: "jkemson5@wikimedia.org", 58 | github: "jkemson5", 59 | wannaPlay: false, 60 | gonnaPlay: false, 61 | winner: false, 62 | groupId: null, 63 | }, 64 | { 65 | id: "4739706055", 66 | name: "Rudy McGoon", 67 | email: "rmcgoon6@walmart.com", 68 | github: "rmcgoon6", 69 | wannaPlay: false, 70 | gonnaPlay: false, 71 | winner: false, 72 | groupId: null, 73 | }, 74 | { 75 | id: "6167293158", 76 | name: "Katalin Grief", 77 | email: "kgrief7@techcrunch.com", 78 | github: "kgrief7", 79 | wannaPlay: false, 80 | gonnaPlay: false, 81 | winner: false, 82 | groupId: null, 83 | }, 84 | { 85 | id: "4548235061", 86 | name: "Tuckie Hartzenberg", 87 | email: "thartzenberg8@who.int", 88 | github: "thartzenberg8", 89 | wannaPlay: false, 90 | gonnaPlay: false, 91 | winner: false, 92 | groupId: null, 93 | }, 94 | { 95 | id: "6959175710", 96 | name: "Gabby Sivior", 97 | email: "gsivior9@slate.com", 98 | github: "gsivior9", 99 | wannaPlay: false, 100 | gonnaPlay: false, 101 | winner: false, 102 | groupId: null, 103 | }, 104 | { 105 | id: "9350462303", 106 | name: "Bliss Madgwich", 107 | email: "bmadgwicha@webmd.com", 108 | github: "bmadgwicha", 109 | wannaPlay: false, 110 | gonnaPlay: false, 111 | winner: false, 112 | groupId: null, 113 | }, 114 | { 115 | id: "2870307747", 116 | name: "Shaylah Wetherell", 117 | email: "swetherellb@forbes.com", 118 | github: "swetherellb", 119 | wannaPlay: false, 120 | gonnaPlay: false, 121 | winner: false, 122 | groupId: null, 123 | }, 124 | { 125 | id: "5305026624", 126 | name: "Barnie Chsteney", 127 | email: "bchsteneyc@wix.com", 128 | github: "bchsteneyc", 129 | wannaPlay: false, 130 | gonnaPlay: false, 131 | winner: false, 132 | groupId: null, 133 | }, 134 | { 135 | id: "1268677280", 136 | name: "Orlan Beamond", 137 | email: "obeamondd@businesswire.com", 138 | github: "obeamondd", 139 | wannaPlay: false, 140 | gonnaPlay: false, 141 | winner: false, 142 | groupId: null, 143 | }, 144 | { 145 | id: "6072344282", 146 | name: "Jacinta McKevin", 147 | email: "jmckevine@tripod.com", 148 | github: "jmckevine", 149 | wannaPlay: false, 150 | gonnaPlay: false, 151 | winner: false, 152 | groupId: null, 153 | }, 154 | { 155 | id: "9944429562", 156 | name: "Hasheem Teers", 157 | email: "hteersf@163.com", 158 | github: "hteersf", 159 | wannaPlay: false, 160 | gonnaPlay: false, 161 | winner: false, 162 | groupId: null, 163 | }, 164 | { 165 | id: "4715646448", 166 | name: "Wernher Grim", 167 | email: "wgrimg@jalbum.net", 168 | github: "wgrimg", 169 | wannaPlay: false, 170 | gonnaPlay: false, 171 | winner: false, 172 | groupId: null, 173 | }, 174 | { 175 | id: "5118963854", 176 | name: "Nelia Tambling", 177 | email: "ntamblingh@sciencedirect.com", 178 | github: "ntamblingh", 179 | wannaPlay: false, 180 | gonnaPlay: false, 181 | winner: false, 182 | groupId: null, 183 | }, 184 | { 185 | id: "7256456855", 186 | name: "Tore Cumberlidge", 187 | email: "tcumberlidgei@ed.gov", 188 | github: "tcumberlidgei", 189 | wannaPlay: false, 190 | gonnaPlay: false, 191 | winner: false, 192 | groupId: null, 193 | }, 194 | { 195 | id: "9912326576", 196 | name: "Carmelle Padson", 197 | email: "cpadsonj@cmu.edu", 198 | github: "cpadsonj", 199 | wannaPlay: false, 200 | gonnaPlay: false, 201 | winner: false, 202 | groupId: null, 203 | }, 204 | { 205 | id: "3102711916", 206 | name: "Prent Stannett", 207 | email: "pstannettk@vinaora.com", 208 | github: "pstannettk", 209 | wannaPlay: false, 210 | gonnaPlay: false, 211 | winner: false, 212 | groupId: null, 213 | }, 214 | { 215 | id: "1486939014", 216 | name: "Jennine Cundict", 217 | email: "jcundictl@last.fm", 218 | github: "jcundictl", 219 | wannaPlay: false, 220 | gonnaPlay: false, 221 | winner: false, 222 | groupId: null, 223 | }, 224 | { 225 | id: "2848954913", 226 | name: "Micaela Ebbotts", 227 | email: "mebbottsm@etsy.com", 228 | github: "mebbottsm", 229 | wannaPlay: false, 230 | gonnaPlay: false, 231 | winner: false, 232 | groupId: null, 233 | }, 234 | { 235 | id: "6936926411", 236 | name: "Case Wilsher", 237 | email: "cwilshern@seesaa.net", 238 | github: "cwilshern", 239 | wannaPlay: false, 240 | gonnaPlay: false, 241 | winner: false, 242 | groupId: null, 243 | }, 244 | { 245 | id: "3398121175", 246 | name: "Rubia Nielson", 247 | email: "rnielsono@ustream.tv", 248 | github: "rnielsono", 249 | wannaPlay: false, 250 | gonnaPlay: false, 251 | winner: false, 252 | groupId: null, 253 | }, 254 | { 255 | id: "1111389807", 256 | name: "Darn Agget", 257 | email: "daggetp@51.la", 258 | github: "daggetp", 259 | wannaPlay: false, 260 | gonnaPlay: false, 261 | winner: false, 262 | groupId: null, 263 | }, 264 | { 265 | id: "9622596294", 266 | name: "Lottie Gilligan", 267 | email: "lgilliganq@princeton.edu", 268 | github: "lgilliganq", 269 | wannaPlay: false, 270 | gonnaPlay: false, 271 | winner: false, 272 | groupId: null, 273 | }, 274 | { 275 | id: "9776624499", 276 | name: "Brucie Pollock", 277 | email: "bpollockr@shop-pro.jp", 278 | github: "bpollockr", 279 | wannaPlay: false, 280 | gonnaPlay: false, 281 | winner: false, 282 | groupId: null, 283 | }, 284 | { 285 | id: "1061755757", 286 | name: "Laina Cripin", 287 | email: "lcripins@dedecms.com", 288 | github: "lcripins", 289 | wannaPlay: false, 290 | gonnaPlay: false, 291 | winner: false, 292 | groupId: null, 293 | }, 294 | { 295 | id: "2911567510", 296 | name: "Garth Padmore", 297 | email: "gpadmoret@exblog.jp", 298 | github: "gpadmoret", 299 | wannaPlay: false, 300 | gonnaPlay: false, 301 | winner: false, 302 | groupId: null, 303 | }, 304 | { 305 | id: "3618011368", 306 | name: "Madlin Boler", 307 | email: "mboleru@microsoft.com", 308 | github: "mboleru", 309 | wannaPlay: false, 310 | gonnaPlay: false, 311 | winner: false, 312 | groupId: null, 313 | }, 314 | { 315 | id: "2653233106", 316 | name: "Timmy Cobden", 317 | email: "tcobdenv@360.cn", 318 | github: "tcobdenv", 319 | wannaPlay: false, 320 | gonnaPlay: false, 321 | winner: false, 322 | groupId: null, 323 | }, 324 | { 325 | id: "0696623800", 326 | name: "Ingaborg Ashfold", 327 | email: "iashfoldw@telegraph.co.uk", 328 | github: "iashfoldw", 329 | wannaPlay: false, 330 | gonnaPlay: false, 331 | winner: false, 332 | groupId: null, 333 | }, 334 | { 335 | id: "0112716260", 336 | name: "Mitzi Rollett", 337 | email: "mrollettx@ocn.ne.jp", 338 | github: "mrollettx", 339 | wannaPlay: false, 340 | gonnaPlay: false, 341 | winner: false, 342 | groupId: null, 343 | }, 344 | { 345 | id: "4592696363", 346 | name: "Keriann Spellworth", 347 | email: "kspellworthy@stumbleupon.com", 348 | github: "kspellworthy", 349 | wannaPlay: false, 350 | gonnaPlay: false, 351 | winner: false, 352 | groupId: null, 353 | }, 354 | { 355 | id: "5196302623", 356 | name: "Donovan Hucquart", 357 | email: "dhucquartz@github.com", 358 | github: "dhucquartz", 359 | wannaPlay: false, 360 | gonnaPlay: false, 361 | winner: false, 362 | groupId: null, 363 | }, 364 | { 365 | id: "3775446162", 366 | name: "Saba Klaassens", 367 | email: "sklaassens10@nytimes.com", 368 | github: "sklaassens10", 369 | wannaPlay: false, 370 | gonnaPlay: false, 371 | winner: false, 372 | groupId: null, 373 | }, 374 | { 375 | id: "7517431683", 376 | name: "Niki Penella", 377 | email: "npenella11@kickstarter.com", 378 | github: "npenella11", 379 | wannaPlay: false, 380 | gonnaPlay: false, 381 | winner: false, 382 | groupId: null, 383 | }, 384 | { 385 | id: "3667209386", 386 | name: "Gerda Moxsom", 387 | email: "gmoxsom12@guardian.co.uk", 388 | github: "gmoxsom12", 389 | wannaPlay: true, 390 | gonnaPlay: false, 391 | winner: false, 392 | groupId: null, 393 | }, 394 | { 395 | id: "1162164608", 396 | name: "Dion Jakubiak", 397 | email: "djakubiak13@prlog.org", 398 | github: "djakubiak13", 399 | wannaPlay: false, 400 | gonnaPlay: false, 401 | winner: false, 402 | groupId: null, 403 | }, 404 | ] 405 | -------------------------------------------------------------------------------- /src/app/events/[event]/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { ChangeEvent, useCallback, useState } from "react" 4 | import { useRouter } from "next/navigation" 5 | import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" 6 | import { TableParticipants } from "./table-participants" 7 | import { 8 | Button, 9 | Checkbox, 10 | EditParticipantModal, 11 | Input, 12 | Modal, 13 | ModalTrigger, 14 | RegisterParticipantModal, 15 | } from "@/components" 16 | import { Filter as FilterIcon, Edit as EditIcon } from "lucide-react" 17 | import { 18 | getParticipants, 19 | setSelectedParticipants, 20 | checkParticipant, 21 | registerParticipant, 22 | removeParticipant, 23 | editParticipant, 24 | getEvent, 25 | } from "./participants/data-participants" 26 | import { upload } from "./participants/upload" 27 | import { getRandomInteger } from "@/lib/get-random-integer" 28 | import { EditParticipant, RegisterParticipant } from "@/shared/types" 29 | 30 | type EventProps = { 31 | params: { 32 | event: string 33 | } 34 | } 35 | 36 | type GenerateGroupsInput = { 37 | event: string 38 | ids: Set 39 | } 40 | const generateGroups = async ({ event, ids }: GenerateGroupsInput) => { 41 | return setSelectedParticipants({ event, ids: Array.from(ids) }) 42 | } 43 | 44 | export default function Event({ params }: EventProps) { 45 | const { event } = params 46 | const router = useRouter() 47 | 48 | const [value, setValue] = useState("") 49 | 50 | const [isOnlyRaffle, setIsOnlyRaffle] = useState(false) 51 | 52 | const queryClient = useQueryClient() 53 | 54 | const query = useQuery({ 55 | queryKey: ["participants", { event }], 56 | queryFn: () => getParticipants(event), 57 | }) 58 | 59 | const eventQuery = useQuery({ 60 | queryKey: ["event", { event }], 61 | queryFn: () => getEvent(event), 62 | retry: 0, 63 | }) 64 | 65 | const generateGroupsMutation = useMutation({ 66 | mutationFn: generateGroups, 67 | onSuccess: () => { 68 | queryClient.invalidateQueries({ queryKey: ["participants", { event }] }) 69 | router.push(`/events/${event}/participants`) 70 | }, 71 | }) 72 | 73 | const checkParticipantMutation = useMutation({ 74 | mutationFn: checkParticipant, 75 | onSuccess: () => { 76 | queryClient.invalidateQueries({ queryKey: ["participants", { event }] }) 77 | }, 78 | }) 79 | 80 | const registerParticipantMutation = useMutation({ 81 | mutationFn: registerParticipant, 82 | onSuccess: () => { 83 | queryClient.invalidateQueries({ queryKey: ["participants", { event }] }) 84 | }, 85 | }) 86 | 87 | const removeParticipantMutation = useMutation({ 88 | mutationFn: removeParticipant, 89 | onSuccess: () => { 90 | queryClient.invalidateQueries({ queryKey: ["participants", { event }] }) 91 | }, 92 | }) 93 | 94 | const editParticipantMutation = useMutation({ 95 | mutationFn: editParticipant, 96 | onSuccess: () => { 97 | queryClient.invalidateQueries({ queryKey: ["participants", { event }] }) 98 | }, 99 | }) 100 | 101 | const handleRegisterParticipant = async (data: RegisterParticipant) => { 102 | registerParticipantMutation.mutate({ data, event }) 103 | } 104 | 105 | const handleEditParticipant = useCallback( 106 | async (data: EditParticipant) => { 107 | console.log("edit participant:", data) 108 | await editParticipantMutation.mutateAsync({ data, event }) 109 | }, 110 | [editParticipantMutation, event], 111 | ) 112 | 113 | const editParticipantModal = useCallback( 114 | (participant: EditParticipant) => ( 115 | 122 | ), 123 | [ 124 | editParticipantMutation.error, 125 | editParticipantMutation.isLoading, 126 | editParticipantMutation.isError, 127 | handleEditParticipant, 128 | ], 129 | ) 130 | 131 | const handleCheckParticipant = ({ 132 | id, 133 | checked, 134 | }: { 135 | id: string 136 | checked: boolean 137 | }) => { 138 | checkParticipantMutation.mutate({ id, checked, event }) 139 | } 140 | 141 | const handleSetOnlyRaffle = () => { 142 | setIsOnlyRaffle((prev) => !prev) 143 | } 144 | 145 | const participants = query.data ?? [] 146 | const filteredParticipants = participants.filter((p) => { 147 | const isOnlyRaffleCondition = isOnlyRaffle ? p.wannaPlay : true 148 | if (value === "") return isOnlyRaffleCondition 149 | 150 | const name = p.name.toLowerCase() 151 | const email = p.email.toLowerCase() 152 | const github = p.github.toLowerCase() 153 | 154 | const search = value.toLowerCase() 155 | 156 | return ( 157 | (name.includes(search) || 158 | email.includes(search) || 159 | github.includes(search)) && 160 | isOnlyRaffleCondition 161 | ) 162 | }) 163 | 164 | const handleGenerateGroups = () => { 165 | const selected = new Set() 166 | const checkedParticipants = filteredParticipants.filter((p) => p.wannaPlay) 167 | if (checkedParticipants.length < 16) { 168 | // TODO: Mostrar erro na tela 169 | console.log("Não tem a quantidade mínima selecionada") 170 | return 171 | } 172 | 173 | while (selected.size < 16) { 174 | const rnd = getRandomInteger(filteredParticipants.length - 1) 175 | const participant = filteredParticipants[rnd] 176 | if (participant.wannaPlay) { 177 | selected.add(filteredParticipants[rnd].id) 178 | } 179 | } 180 | generateGroupsMutation.mutate({ event, ids: selected }) 181 | } 182 | 183 | const handleFileUpload = (e: ChangeEvent) => { 184 | e.target.form?.requestSubmit() 185 | } 186 | 187 | const handleRemoveParticipant = (id: string) => { 188 | removeParticipantMutation.mutate({ id, event }) 189 | } 190 | 191 | if (eventQuery.isError) { 192 | return router.push("/events") 193 | } 194 | 195 | if (eventQuery.isLoading) { 196 | return

    Carregando dados do evento...

    197 | } 198 | 199 | return ( 200 |
    201 |
    202 |

    203 | {eventQuery.data?.name} 204 |

    205 | 229 |
    230 | 231 |
    232 |
    233 |
    234 | setValue(e.target.value)} 237 | icon={ 238 | 239 | } 240 | placeholder="Buscar Participante" 241 | /> 242 |
    243 |
    244 |
    245 | 250 | 256 |
    257 |
    258 |
    259 |
    260 | 263 |
    264 |
    265 | 266 | 272 |
    273 | ) 274 | } 275 | 276 | type EditParticipantModalContainerProps = { 277 | participant: EditParticipant 278 | onEditParticipant: (data: EditParticipant) => Promise 279 | isLoading: boolean 280 | isError: boolean 281 | error: unknown 282 | } 283 | function EditParticipantModalContainer({ 284 | participant, 285 | onEditParticipant, 286 | isLoading, 287 | isError, 288 | error, 289 | }: EditParticipantModalContainerProps) { 290 | const [openEditModal, setOpenEditModal] = useState(false) 291 | 292 | const handleEditParticipant = async (participant: EditParticipant) => { 293 | await onEditParticipant(participant) 294 | setOpenEditModal(false) 295 | } 296 | 297 | return ( 298 | 299 | 300 | 301 | 302 | 309 | 310 | ) 311 | } 312 | 313 | type RegisterParticipantModalContainerProps = { 314 | onRegisterParticipant: (data: RegisterParticipant) => Promise 315 | isLoading: boolean 316 | isSuccess: boolean 317 | error: unknown 318 | } 319 | 320 | function RegisterParticipantModalContainer({ 321 | onRegisterParticipant, 322 | isLoading, 323 | isSuccess, 324 | error, 325 | }: RegisterParticipantModalContainerProps) { 326 | const [open, setOpen] = useState(false) 327 | 328 | return ( 329 | 330 | 331 | 332 | 333 | 340 | 341 | ) 342 | } 343 | --------------------------------------------------------------------------------