├── .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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------