From e4e62a3c3ba74330cc00cf66fe304a67872ea994 Mon Sep 17 00:00:00 2001 From: 3raphat <96657413+3raphat@users.noreply.github.com> Date: Mon, 17 Feb 2025 13:13:38 +0700 Subject: [PATCH 01/70] Chess Game (#67) --- apps/web/package.json | 2 + apps/web/src/components/chess/bishop.tsx | 36 + apps/web/src/components/chess/board.tsx | 2835 ++++++++++++++++++ apps/web/src/components/chess/chess-game.tsx | 577 ++++ apps/web/src/components/chess/knight.tsx | 50 + apps/web/src/components/chess/rook.tsx | 41 + pnpm-lock.yaml | 69 +- 7 files changed, 3602 insertions(+), 8 deletions(-) create mode 100644 apps/web/src/components/chess/bishop.tsx create mode 100644 apps/web/src/components/chess/board.tsx create mode 100644 apps/web/src/components/chess/chess-game.tsx create mode 100644 apps/web/src/components/chess/knight.tsx create mode 100644 apps/web/src/components/chess/rook.tsx diff --git a/apps/web/package.json b/apps/web/package.json index 6330f5b4..1502061b 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -11,6 +11,8 @@ "check-types": "tsc --noEmit" }, "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/modifiers": "^9.0.0", "@hookform/resolvers": "^3.10.0", "@radix-ui/react-checkbox": "^1.1.3", "@radix-ui/react-label": "^2.1.1", diff --git a/apps/web/src/components/chess/bishop.tsx b/apps/web/src/components/chess/bishop.tsx new file mode 100644 index 00000000..9577e78b --- /dev/null +++ b/apps/web/src/components/chess/bishop.tsx @@ -0,0 +1,36 @@ +import { type SVGProps } from "react"; + +const Bishop = (props: SVGProps) => ( + + + + + + + + + + +); + +export default Bishop; diff --git a/apps/web/src/components/chess/board.tsx b/apps/web/src/components/chess/board.tsx new file mode 100644 index 00000000..991a514b --- /dev/null +++ b/apps/web/src/components/chess/board.tsx @@ -0,0 +1,2835 @@ +import { type SVGProps } from "react"; + +const Board = (props: SVGProps) => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +); + +export default Board; diff --git a/apps/web/src/components/chess/chess-game.tsx b/apps/web/src/components/chess/chess-game.tsx new file mode 100644 index 00000000..44ab3a65 --- /dev/null +++ b/apps/web/src/components/chess/chess-game.tsx @@ -0,0 +1,577 @@ +"use client"; + +import Bishop from "@/components/chess/bishop"; +import Board from "@/components/chess/board"; +import Knight from "@/components/chess/knight"; +import Rook from "@/components/chess/rook"; +import { cn } from "@/libs/utils"; +import { + DndContext, + KeyboardSensor, + MouseSensor, + TouchSensor, + useDraggable, + useDroppable, + useSensor, + useSensors, + type DragEndEvent, +} from "@dnd-kit/core"; +import { useCallback, useMemo, useState, type ReactNode } from "react"; + +const BOARD_SIZE = 8; + +interface Position { + x: number; + y: number; +} + +interface BaseGameState { + currentPosition: Position; + currentPiece: "knight" | "bishop" | "rook"; + moveHistory: string[]; + score: number; + noDeductionMoves: number; + nextMultiplier: number; + visitedSquares: Set; +} + +interface GameState extends BaseGameState { + prevStates: BaseGameState[]; +} + +const YELLOW_SQUARES: Record = { + a3: 100, + a5: 300, + a7: 200, + b8: 600, + c1: 200, + c3: 100, + c5: 100, + d2: 100, + d4: 200, + d6: 100, + d8: 600, + f2: 200, + f4: 300, + f6: 200, + f8: 600, + g1: 100, + g7: 200, + h2: 200, + h4: 200, + h8: 600, +}; + +const ORANGE_SQUARES: Record = { + a1: 70, + b2: 20, + b6: 30, + c7: 20, + e1: 30, + e3: 20, + e5: 30, + e7: 30, + g5: 20, + h6: 30, +}; + +const LIGHT_GREEN_SQUARES = new Set([ + "b1", + "b5", + "c2", + "c4", + "c6", + "e2", + "f5", + "g2", + "h3", +]); + +const BLUE_SQUARES: Record = { + a2: 3, + a4: 2, + b7: 3, + d3: 2, + d5: 2, + f1: 2, + g4: 3, + h7: 2, +}; + +const PINK_SQUARES: Record = { + a6: 2, + b3: 2, + e6: 2, + f3: 2, + f7: 3, + h1: 3, + h5: 2, +}; + +const BISHOP_SQUARES = new Set(["b4", "e4", "g3"]); + +const ROOK_SQUARES = new Set(["d1", "d7", "g6"]); + +const END_SQUARES = new Set(["a8", "c8", "e8", "g8"]); + +const toChessNotation = (pos: Position) => { + const file = String.fromCharCode(97 + pos.x); + const rank = BOARD_SIZE - pos.y; + return `${file}${rank}`; +}; + +const fromChessNotation = (notation: string) => { + const x = notation.charCodeAt(0) - 97; + const y = BOARD_SIZE - parseInt(notation[1]); + return { x, y }; +}; + +const isKnightMove = (from: Position, to: Position) => { + const dx = Math.abs(to.x - from.x); + const dy = Math.abs(to.y - from.y); + return (dx === 2 && dy === 1) || (dx === 1 && dy === 2); +}; + +const isBishopMove = (from: Position, to: Position) => { + const dx = Math.abs(to.x - from.x); + const dy = Math.abs(to.y - from.y); + return dx === dy; +}; + +const isRookMove = (from: Position, to: Position) => { + return from.x === to.x || from.y === to.y; +}; + +const isValidMove = ( + from: Position, + to: Position, + pieceType: "knight" | "bishop" | "rook", +) => { + if (to.x < 0 || to.x >= BOARD_SIZE || to.y < 0 || to.y >= BOARD_SIZE) { + return false; + } + + if (to.x === from.x && to.y === from.y) { + return; + } + + switch (pieceType) { + case "knight": + return isKnightMove(from, to); + case "bishop": + return isBishopMove(from, to); + case "rook": + return isRookMove(from, to); + default: + return false; + } +}; + +const getValidMoves = ( + pos: Position, + pieceType: "knight" | "bishop" | "rook", +) => { + const moves: Position[] = []; + + if (pieceType === "knight") { + const possibleMoves = [ + [-2, -1], + [-2, 1], + [2, -1], + [2, 1], + [-1, -2], + [-1, 2], + [1, -2], + [1, 2], + ]; + for (const [dx, dy] of possibleMoves) { + const newX = pos.x + dx; + const newY = pos.y + dy; + if (newX >= 0 && newX < BOARD_SIZE && newY >= 0 && newY < BOARD_SIZE) { + moves.push({ x: newX, y: newY }); + } + } + } else if (pieceType === "bishop") { + const directions = [ + [-1, -1], + [-1, 1], + [1, -1], + [1, 1], + ]; + for (const [dx, dy] of directions) { + for (let i = 1; i < BOARD_SIZE; i++) { + const newX = pos.x + dx * i; + const newY = pos.y + dy * i; + if (newX >= 0 && newX < BOARD_SIZE && newY >= 0 && newY < BOARD_SIZE) { + moves.push({ x: newX, y: newY }); + } else { + break; + } + } + } + } else if (pieceType === "rook") { + const directions = [ + [-1, 0], + [1, 0], + [0, -1], + [0, 1], + ]; + for (const [dx, dy] of directions) { + for (let i = 1; i < BOARD_SIZE; i++) { + const newX = pos.x + dx * i; + const newY = pos.y + dy * i; + if (newX >= 0 && newX < BOARD_SIZE && newY >= 0 && newY < BOARD_SIZE) { + moves.push({ x: newX, y: newY }); + } else { + break; + } + } + } + } + + return moves; +}; + +const isTransformSquare = (pos: Position) => { + const notation = toChessNotation(pos); + if (BISHOP_SQUARES.has(notation)) return "bishop"; + if (ROOK_SQUARES.has(notation)) return "rook"; + return null; +}; + +const isEndSquare = (pos: Position) => { + const notation = toChessNotation(pos); + return END_SQUARES.has(notation); +}; + +function DraggablePiece({ + id, + position, + children, +}: { + id: string; + position: Position; + children: ReactNode; +}) { + const { attributes, listeners, setNodeRef, transform, isDragging } = + useDraggable({ + id, + }); + + return ( +
+ {children} +
+ ); +} + +function DroppableSquare({ + id, + onClick, + isValid, + shouldShowGrayscale, +}: { + id: string; + onClick: () => void; + isValid: boolean; + shouldShowGrayscale: boolean; +}) { + const { setNodeRef, isOver } = useDroppable({ + id, + }); + + return ( +
+ {isValid && ( +
+ )} +
+ ); +} + +function ChessGame() { + const initialState = useMemo( + () => ({ + currentPosition: fromChessNotation("b1"), + currentPiece: "knight", + moveHistory: ["b1"], + score: 0, + noDeductionMoves: 0, + nextMultiplier: 1, + visitedSquares: new Set(["b1"]), + prevStates: [], + }), + [], + ); + + const [gameState, setGameState] = useState(initialState); + + const isReusableSquare = (pos: Position) => { + const notation = toChessNotation(pos); + return ( + LIGHT_GREEN_SQUARES.has(notation) || + notation in BLUE_SQUARES || + notation in PINK_SQUARES || + BISHOP_SQUARES.has(notation) || + ROOK_SQUARES.has(notation) + ); + }; + + const handleMove = useCallback( + (toPos: Position) => { + if ( + !isValidMove( + gameState.currentPosition, + toPos, + gameState.currentPiece, + ) || + isEndSquare(gameState.currentPosition) + ) { + return; + } + + setGameState((prev) => { + const currentState: BaseGameState = { + currentPosition: prev.currentPosition, + currentPiece: prev.currentPiece, + moveHistory: prev.moveHistory, + score: prev.score, + noDeductionMoves: prev.noDeductionMoves, + nextMultiplier: prev.nextMultiplier, + visitedSquares: prev.visitedSquares, + }; + let newScore = prev.score; + const notation = toChessNotation(toPos); + const isVisited = prev.visitedSquares.has(notation); + + if ( + prev.noDeductionMoves === 0 && + !LIGHT_GREEN_SQUARES.has(notation) && + !isEndSquare(toPos) + ) { + newScore = Math.round(newScore * 0.9); + } + + if (!isVisited || LIGHT_GREEN_SQUARES.has(notation)) { + if (YELLOW_SQUARES[notation]) { + newScore += YELLOW_SQUARES[notation] * prev.nextMultiplier; + } else if (ORANGE_SQUARES[notation]) { + const bonus = Math.round( + newScore * (ORANGE_SQUARES[notation] / 100), + ); + newScore += bonus; + } + } + + const newMoveHistory = [...prev.moveHistory, notation]; + const specialPiece = isTransformSquare(toPos); + const newVisited = new Set(prev.visitedSquares).add(notation); + + const newState: GameState = { + currentPosition: toPos, + currentPiece: specialPiece || "knight", + moveHistory: newMoveHistory, + score: newScore, + noDeductionMoves: + BLUE_SQUARES[notation] || Math.max(0, prev.noDeductionMoves - 1), + nextMultiplier: PINK_SQUARES[notation] || 1, + visitedSquares: newVisited, + prevStates: [...prev.prevStates, currentState], + }; + + return newState; + }); + }, + [gameState.currentPosition, gameState.currentPiece], + ); + + const handleClick = useCallback( + (pos: Position) => { + handleMove(pos); + }, + [handleMove], + ); + + const handleDragEnd = useCallback( + (event: DragEndEvent) => { + const { over } = event; + if (over) { + const toPos = fromChessNotation(String(over.id)); + handleMove(toPos); + } + }, + [handleMove], + ); + + const handleReset = useCallback(() => { + setGameState(initialState); + }, [initialState]); + + const handleUndo = useCallback(() => { + setGameState((prev) => { + if (prev.prevStates.length === 0) return prev; + const lastState = prev.prevStates[prev.prevStates.length - 1]; + const newPrevStates = prev.prevStates.slice(0, -1); + return { + ...lastState, + prevStates: newPrevStates, + }; + }); + }, []); + + const canSubmit = isEndSquare(gameState.currentPosition); + + const handleSubmit = useCallback(() => { + if (!canSubmit) return; + alert( + `Game completed!\nMoves: ${gameState.moveHistory.join("-")}\nFinal Score: ${gameState.score}`, + ); + }, [canSubmit, gameState.moveHistory, gameState.score]); + + const validMoves = getValidMoves( + gameState.currentPosition, + gameState.currentPiece, + ); + + const isValidMoveSquare = (pos: Position) => { + if (isEndSquare(gameState.currentPosition)) return false; + return validMoves.some((move) => move.x === pos.x && move.y === pos.y); + }; + + const shouldShowGrayscale = (pos: Position) => { + return ( + gameState.visitedSquares.has(toChessNotation(pos)) && + !isReusableSquare(pos) && + !isTransformSquare(pos) && + !isEndSquare(pos) + ); + }; + + const PieceComponent = { + knight: Knight, + bishop: Bishop, + rook: Rook, + }[gameState.currentPiece]; + + const sensors = useSensors( + useSensor(MouseSensor), + useSensor(TouchSensor), + useSensor(KeyboardSensor), + ); + + return ( + +
+
+
+ + + + + + +
+ {Array.from({ length: BOARD_SIZE * BOARD_SIZE }).map((_, i) => { + const x = i % BOARD_SIZE; + const y = Math.floor(i / BOARD_SIZE); + const isValid = isValidMoveSquare({ x, y }); + return ( + handleClick({ x, y })} + isValid={isValid} + shouldShowGrayscale={shouldShowGrayscale({ x, y })} + /> + ); + })} +
+
+
+ +
+ Current Piece:{" "} + {gameState.currentPiece.charAt(0).toUpperCase() + + gameState.currentPiece.slice(1)} +
+ +
+ Score: {gameState.score} + {gameState.noDeductionMoves > 0 && ( + + (No deduction: {gameState.noDeductionMoves} moves) + + )} + {gameState.nextMultiplier > 1 && ( + + (Next bonus x{gameState.nextMultiplier}) + + )} +
+ +
+ + + +
+ +
+

Move History:

+

{gameState.moveHistory.join(" > ")}

+
+
+
+ ); +} + +export default ChessGame; diff --git a/apps/web/src/components/chess/knight.tsx b/apps/web/src/components/chess/knight.tsx new file mode 100644 index 00000000..89ddaf7b --- /dev/null +++ b/apps/web/src/components/chess/knight.tsx @@ -0,0 +1,50 @@ +import { type SVGProps } from "react"; + +const Knight = (props: SVGProps) => ( + + + + + + + + + + +); + +export default Knight; diff --git a/apps/web/src/components/chess/rook.tsx b/apps/web/src/components/chess/rook.tsx new file mode 100644 index 00000000..f098ee61 --- /dev/null +++ b/apps/web/src/components/chess/rook.tsx @@ -0,0 +1,41 @@ +import { type SVGProps } from "react"; + +const Rook = (props: SVGProps) => ( + + + + + + + + + +); + +export default Rook; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f217c4af..a45a7061 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,7 +62,7 @@ importers: version: 10.4.15(@nestjs/common@10.4.15(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.15) '@nestjs/swagger': specifier: ^11.0.2 - version: 11.0.2(@nestjs/common@10.4.15(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.15(@nestjs/common@10.4.15(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.15)(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2) + version: 11.0.2(@nestjs/common@10.4.15(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.15)(reflect-metadata@0.2.2) '@scalar/nestjs-api-reference': specifier: ^0.3.175 version: 0.3.175 @@ -90,7 +90,7 @@ importers: version: 10.2.3(chokidar@3.6.0)(typescript@5.5.4) '@nestjs/testing': specifier: ^10.0.0 - version: 10.4.15(@nestjs/common@10.4.15(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.15(@nestjs/common@10.4.15(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.15)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.15(@nestjs/common@10.4.15(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.15)) + version: 10.4.15(@nestjs/common@10.4.15(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.15)(@nestjs/platform-express@10.4.15) '@repo/typescript-config': specifier: workspace:* version: link:../../packages/typescript-config @@ -185,6 +185,12 @@ importers: apps/web: dependencies: + '@dnd-kit/core': + specifier: ^6.3.1 + version: 6.3.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@dnd-kit/modifiers': + specifier: ^9.0.0 + version: 9.0.0(@dnd-kit/core@6.3.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0) '@hookform/resolvers': specifier: ^3.10.0 version: 3.10.0(react-hook-form@7.54.2(react@19.0.0)) @@ -566,6 +572,28 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} + '@dnd-kit/accessibility@3.1.1': + resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==} + peerDependencies: + react: '>=16.8.0' + + '@dnd-kit/core@6.3.1': + resolution: {integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@dnd-kit/modifiers@9.0.0': + resolution: {integrity: sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw==} + peerDependencies: + '@dnd-kit/core': ^6.3.0 + react: '>=16.8.0' + + '@dnd-kit/utilities@3.2.2': + resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==} + peerDependencies: + react: '>=16.8.0' + '@emnapi/runtime@1.3.1': resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==} @@ -5809,6 +5837,31 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 + '@dnd-kit/accessibility@3.1.1(react@19.0.0)': + dependencies: + react: 19.0.0 + tslib: 2.8.1 + + '@dnd-kit/core@6.3.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@dnd-kit/accessibility': 3.1.1(react@19.0.0) + '@dnd-kit/utilities': 3.2.2(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + tslib: 2.8.1 + + '@dnd-kit/modifiers@9.0.0(@dnd-kit/core@6.3.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)': + dependencies: + '@dnd-kit/core': 6.3.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@dnd-kit/utilities': 3.2.2(react@19.0.0) + react: 19.0.0 + tslib: 2.8.1 + + '@dnd-kit/utilities@3.2.2(react@19.0.0)': + dependencies: + react: 19.0.0 + tslib: 2.8.1 + '@emnapi/runtime@1.3.1': dependencies: tslib: 2.8.1 @@ -6374,7 +6427,7 @@ snapshots: transitivePeerDependencies: - chokidar - '@nestjs/swagger@11.0.2(@nestjs/common@10.4.15(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.15(@nestjs/common@10.4.15(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.15)(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2)': + '@nestjs/swagger@11.0.2(@nestjs/common@10.4.15(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.15)(reflect-metadata@0.2.2)': dependencies: '@microsoft/tsdoc': 0.15.0 '@nestjs/common': 10.4.15(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -6386,7 +6439,7 @@ snapshots: reflect-metadata: 0.2.2 swagger-ui-dist: 5.18.2 - '@nestjs/testing@10.4.15(@nestjs/common@10.4.15(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.15(@nestjs/common@10.4.15(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.15)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.15(@nestjs/common@10.4.15(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.15))': + '@nestjs/testing@10.4.15(@nestjs/common@10.4.15(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.15)(@nestjs/platform-express@10.4.15)': dependencies: '@nestjs/common': 10.4.15(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/core': 10.4.15(@nestjs/common@10.4.15(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.15)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -8306,7 +8359,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.15.0(eslint@9.15.0(jiti@2.4.2))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@9.15.0(jiti@2.4.2)))(eslint@9.15.0(jiti@2.4.2)): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.15.0(eslint@9.15.0(jiti@2.4.2))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0)(eslint@9.15.0(jiti@2.4.2)): dependencies: debug: 3.2.7 optionalDependencies: @@ -8317,7 +8370,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.15.0(eslint@9.15.0(jiti@2.4.2))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@9.15.0(jiti@2.4.2)))(eslint@9.15.0(jiti@2.4.2)): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.15.0(eslint@9.15.0(jiti@2.4.2))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0)(eslint@9.15.0(jiti@2.4.2)): dependencies: debug: 3.2.7 optionalDependencies: @@ -8339,7 +8392,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.15.0(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.15.0(eslint@9.15.0(jiti@2.4.2))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@9.15.0(jiti@2.4.2)))(eslint@9.15.0(jiti@2.4.2)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.15.0(eslint@9.15.0(jiti@2.4.2))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0)(eslint@9.15.0(jiti@2.4.2)) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -8368,7 +8421,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.15.0(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.15.0(eslint@9.15.0(jiti@2.4.2))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@9.15.0(jiti@2.4.2)))(eslint@9.15.0(jiti@2.4.2)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.15.0(eslint@9.15.0(jiti@2.4.2))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0)(eslint@9.15.0(jiti@2.4.2)) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 From 2699d2808c9e856326f2bbfb189fad4f228c29a4 Mon Sep 17 00:00:00 2001 From: beambeambeam Date: Mon, 17 Feb 2025 17:34:16 +0700 Subject: [PATCH 02/70] chore: clean main --- apps/server/src/app.controller.ts | 22 +--------------------- apps/server/src/dto/app.dto.ts | 24 ------------------------ 2 files changed, 1 insertion(+), 45 deletions(-) delete mode 100644 apps/server/src/dto/app.dto.ts diff --git a/apps/server/src/app.controller.ts b/apps/server/src/app.controller.ts index f0f7bf3d..c73d6d8d 100644 --- a/apps/server/src/app.controller.ts +++ b/apps/server/src/app.controller.ts @@ -1,27 +1,7 @@ -import { Body, Controller, Get, Post } from '@nestjs/common'; +import { Controller } from '@nestjs/common'; import { AppService } from './app.service'; -import { ApiResponse } from '@nestjs/swagger'; -import { - CheckTelPayloadDto, - CheckTelResponseDto, - GetHelloResponseDto, -} from './dto/app.dto'; @Controller() export class AppController { constructor(private readonly appService: AppService) {} - - @Get() - @ApiResponse({ status: 200, type: GetHelloResponseDto }) - getHello(): GetHelloResponseDto { - return { - id: 'test', - }; - } - - @Post() - @ApiResponse({ status: 200, type: CheckTelResponseDto, isArray: true }) - checkTel(@Body() body: CheckTelPayloadDto) { - return this.appService.checkTel(body.tel); - } } diff --git a/apps/server/src/dto/app.dto.ts b/apps/server/src/dto/app.dto.ts deleted file mode 100644 index 7138fc82..00000000 --- a/apps/server/src/dto/app.dto.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; - -export class GetHelloResponseDto { - @ApiProperty() - id: string; -} - -export class CheckTelPayloadDto { - @ApiProperty() - tel: string; -} - -export class CheckTelResponseDto { - @ApiProperty() - tel: string; - - @ApiProperty() - check: boolean; -} - -export class CheckTel2PatloadDto { - @ApiProperty() - test: File; -} From 6117437c7f9e01dd2841a57aad7fb9576435a16e Mon Sep 17 00:00:00 2001 From: beambeambeam Date: Mon, 17 Feb 2025 17:34:59 +0700 Subject: [PATCH 03/70] feat: init supabase --- apps/server/openapi.json | 71 +--------------- apps/server/package.json | 2 + .../prisma/migrations/migration_lock.toml | 3 + apps/server/prisma/schema.prisma | 85 +++++++++++++++++++ apps/web/src/libs/server/types.d.ts | 77 +---------------- package.json | 2 +- pnpm-lock.yaml | 78 ++++++++++++++++- 7 files changed, 173 insertions(+), 145 deletions(-) create mode 100644 apps/server/prisma/migrations/migration_lock.toml create mode 100644 apps/server/prisma/schema.prisma diff --git a/apps/server/openapi.json b/apps/server/openapi.json index 51ae8e88..572cd8a9 100644 --- a/apps/server/openapi.json +++ b/apps/server/openapi.json @@ -1,52 +1,6 @@ { "openapi": "3.0.0", - "paths": { - "/": { - "get": { - "operationId": "AppController_getHello", - "parameters": [], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/GetHelloResponseDto" } - } - } - } - }, - "tags": ["App"] - }, - "post": { - "operationId": "AppController_checkTel", - "parameters": [], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/CheckTelPayloadDto" } - } - } - }, - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CheckTelResponseDto" - } - } - } - } - } - }, - "tags": ["App"] - } - } - }, + "paths": {}, "info": { "title": "Comcamp36 API", "description": "Our lovely reference for Comcamp36 Website", @@ -55,26 +9,5 @@ }, "tags": [], "servers": [], - "components": { - "schemas": { - "GetHelloResponseDto": { - "type": "object", - "properties": { "id": { "type": "string" } }, - "required": ["id"] - }, - "CheckTelPayloadDto": { - "type": "object", - "properties": { "tel": { "type": "string" } }, - "required": ["tel"] - }, - "CheckTelResponseDto": { - "type": "object", - "properties": { - "tel": { "type": "string" }, - "check": { "type": "boolean" } - }, - "required": ["tel", "check"] - } - } - } + "components": { "schemas": {} } } diff --git a/apps/server/package.json b/apps/server/package.json index 4d60d825..74014961 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -24,6 +24,7 @@ "@nestjs/core": "^10.0.0", "@nestjs/platform-express": "^10.0.0", "@nestjs/swagger": "^11.0.2", + "@prisma/client": "6.3.1", "@scalar/nestjs-api-reference": "^0.3.175", "nestjs-pino": "^4.2.0", "pino-http": "^10.4.0", @@ -47,6 +48,7 @@ "eslint-plugin-prettier": "^5.0.0", "jest": "^29.5.0", "prettier": "^3.0.0", + "prisma": "^6.3.1", "source-map-support": "^0.5.21", "supertest": "^7.0.0", "ts-jest": "^29.1.0", diff --git a/apps/server/prisma/migrations/migration_lock.toml b/apps/server/prisma/migrations/migration_lock.toml new file mode 100644 index 00000000..648c57fd --- /dev/null +++ b/apps/server/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" \ No newline at end of file diff --git a/apps/server/prisma/schema.prisma b/apps/server/prisma/schema.prisma new file mode 100644 index 00000000..2861197d --- /dev/null +++ b/apps/server/prisma/schema.prisma @@ -0,0 +1,85 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model User { + id String @id @default(uuid()) + google_id String @unique + fullname String + age Int + birth DateTime + gender String + religion String + blood_group String + graduation String + school String + course String + telephone String + email String @unique + medical_coverage String + chronic_disease String + self_medicine String + drug_allergic String + food_allergic String + perfer_food String + address String + home_phone_tel String + comcamp_attendance Boolean + size String + everyday_attendence Boolean + has_laptop Boolean + travel String + parent_fullname String + parent_relation String + parent_phone String + File File[] + AnswerRegis AnswerRegis[] + AnswerAcademic AnswerAcademic[] +} + +model File { + id String @id @default(uuid()) + userId String + face_photo_filepath String + thai_nationalid_copy_filepath String + parent_permission_filepath String + p1_filepath String + p7_filepath String + + user User @relation(fields: [userId], references: [id]) + + @@index([userId]) +} + +model AnswerRegis { + id String @id @default(uuid()) + userId String + answer1 String + answer2 String + answer3 String + answer4 String + answer5 String + answer6_1 String + answer6_2 String + + user User @relation(fields: [userId], references: [id]) + + @@index([userId]) +} + +model AnswerAcademic { + id String @id @default(uuid()) + userId String + algo_answer String + chess_notation String + chess_score String + + user User @relation(fields: [userId], references: [id]) + + @@index([userId]) +} diff --git a/apps/web/src/libs/server/types.d.ts b/apps/web/src/libs/server/types.d.ts index 9d9b1a3c..0a5317ac 100644 --- a/apps/web/src/libs/server/types.d.ts +++ b/apps/web/src/libs/server/types.d.ts @@ -1,35 +1,7 @@ -export interface paths { - "/": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations["AppController_getHello"]; - put?: never; - post: operations["AppController_checkTel"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; -} +export type paths = Record; export type webhooks = Record; export interface components { - schemas: { - GetHelloResponseDto: { - id: string; - }; - CheckTelPayloadDto: { - tel: string; - }; - CheckTelResponseDto: { - tel: string; - check: boolean; - }; - }; + schemas: never; responses: never; parameters: never; requestBodies: never; @@ -37,47 +9,4 @@ export interface components { pathItems: never; } export type $defs = Record; -export interface operations { - AppController_getHello: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["GetHelloResponseDto"]; - }; - }; - }; - }; - AppController_checkTel: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["CheckTelPayloadDto"]; - }; - }; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["CheckTelResponseDto"][]; - }; - }; - }; - }; -} +export type operations = Record; diff --git a/package.json b/package.json index 9a0ecae7..22b5cf73 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "format": "prettier --write \"**/*.{ts,tsx,md}\"", "lint-staged": "lint-staged", "prepare": "husky", - "with-env": "dotenv -e .env.local --", + "with-env": "dotenv -e .env --", "db:start": "pnpm supabase start", "db:stop": "pnpm supabase stop" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a45a7061..e959c125 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -63,6 +63,9 @@ importers: '@nestjs/swagger': specifier: ^11.0.2 version: 11.0.2(@nestjs/common@10.4.15(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.15)(reflect-metadata@0.2.2) + '@prisma/client': + specifier: 6.3.1 + version: 6.3.1(prisma@6.3.1(typescript@5.5.4))(typescript@5.5.4) '@scalar/nestjs-api-reference': specifier: ^0.3.175 version: 0.3.175 @@ -127,6 +130,9 @@ importers: prettier: specifier: ^3.0.0 version: 3.3.3 + prisma: + specifier: ^6.3.1 + version: 6.3.1(typescript@5.5.4) source-map-support: specifier: ^0.5.21 version: 0.5.21 @@ -1234,6 +1240,33 @@ packages: resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@prisma/client@6.3.1': + resolution: {integrity: sha512-ARAJaPs+eBkemdky/XU3cvGRl+mIPHCN2lCXsl5Vlb0E2gV+R6IN7aCI8CisRGszEZondwIsW9Iz8EJkTdykyA==} + engines: {node: '>=18.18'} + peerDependencies: + prisma: '*' + typescript: '>=5.1.0' + peerDependenciesMeta: + prisma: + optional: true + typescript: + optional: true + + '@prisma/debug@6.3.1': + resolution: {integrity: sha512-RrEBkd+HLZx+ydfmYT0jUj7wjLiS95wfTOSQ+8FQbvb6vHh5AeKfEPt/XUQ5+Buljj8hltEfOslEW57/wQIVeA==} + + '@prisma/engines-version@6.3.0-17.acc0b9dd43eb689cbd20c9470515d719db10d0b0': + resolution: {integrity: sha512-R/ZcMuaWZT2UBmgX3Ko6PAV3f8//ZzsjRIG1eKqp3f2rqEqVtCv+mtzuH2rBPUC9ujJ5kCb9wwpxeyCkLcHVyA==} + + '@prisma/engines@6.3.1': + resolution: {integrity: sha512-sXdqEVLyGAJ5/iUoG/Ea5AdHMN71m6PzMBWRQnLmhhOejzqAaEr8rUd623ql6OJpED4s/U4vIn4dg1qkF7vGag==} + + '@prisma/fetch-engine@6.3.1': + resolution: {integrity: sha512-HOf/0umOgt+/S2xtZze+FHKoxpVg4YpVxROr6g2YG09VsI3Ipyb+rGvD6QGbCqkq5NTWAAZoOGNL+oy7t+IhaQ==} + + '@prisma/get-platform@6.3.1': + resolution: {integrity: sha512-AYLq6Hk9xG73JdLWJ3Ip9Wg/vlP7xPvftGBalsPzKDOHr/ImhwJ09eS8xC2vNT12DlzGxhfk8BkL0ve2OriNhQ==} + '@radix-ui/number@1.1.0': resolution: {integrity: sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==} @@ -4500,6 +4533,16 @@ packages: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + prisma@6.3.1: + resolution: {integrity: sha512-JKCZWvBC3enxk51tY4TWzS4b5iRt4sSU1uHn2I183giZTvonXaQonzVtjLzpOHE7qu9MxY510kAtFGJwryKe3Q==} + engines: {node: '>=18.18'} + hasBin: true + peerDependencies: + typescript: '>=5.1.0' + peerDependenciesMeta: + typescript: + optional: true + proc-log@5.0.0: resolution: {integrity: sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==} engines: {node: ^18.17.0 || >=20.5.0} @@ -6504,6 +6547,32 @@ snapshots: '@pkgr/core@0.1.1': {} + '@prisma/client@6.3.1(prisma@6.3.1(typescript@5.5.4))(typescript@5.5.4)': + optionalDependencies: + prisma: 6.3.1(typescript@5.5.4) + typescript: 5.5.4 + + '@prisma/debug@6.3.1': {} + + '@prisma/engines-version@6.3.0-17.acc0b9dd43eb689cbd20c9470515d719db10d0b0': {} + + '@prisma/engines@6.3.1': + dependencies: + '@prisma/debug': 6.3.1 + '@prisma/engines-version': 6.3.0-17.acc0b9dd43eb689cbd20c9470515d719db10d0b0 + '@prisma/fetch-engine': 6.3.1 + '@prisma/get-platform': 6.3.1 + + '@prisma/fetch-engine@6.3.1': + dependencies: + '@prisma/debug': 6.3.1 + '@prisma/engines-version': 6.3.0-17.acc0b9dd43eb689cbd20c9470515d719db10d0b0 + '@prisma/get-platform': 6.3.1 + + '@prisma/get-platform@6.3.1': + dependencies: + '@prisma/debug': 6.3.1 + '@radix-ui/number@1.1.0': {} '@radix-ui/primitive@1.1.1': {} @@ -8355,7 +8424,7 @@ snapshots: is-glob: 4.0.3 stable-hash: 0.0.4 optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.15.0(eslint@9.15.0(jiti@2.4.2))(typescript@5.5.4))(eslint-import-resolver-typescript@3.7.0)(eslint@9.15.0(jiti@2.4.2)) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.15.0(eslint@9.15.0(jiti@2.4.2))(typescript@5.7.3))(eslint-import-resolver-typescript@3.7.0)(eslint@9.15.0(jiti@2.4.2)) transitivePeerDependencies: - supports-color @@ -10334,6 +10403,13 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 + prisma@6.3.1(typescript@5.5.4): + dependencies: + '@prisma/engines': 6.3.1 + optionalDependencies: + fsevents: 2.3.3 + typescript: 5.5.4 + proc-log@5.0.0: {} process-nextick-args@2.0.1: {} From 769f2b36618284f59d6727f97723da6439ee0bf3 Mon Sep 17 00:00:00 2001 From: beambeambeam Date: Mon, 17 Feb 2025 17:49:25 +0700 Subject: [PATCH 04/70] fix: font to var --- apps/web/src/app/layout.tsx | 2 +- apps/web/src/fonts/index.ts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 7c1d48aa..2a16df1a 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -31,7 +31,7 @@ export default function RootLayout({ return ( diff --git a/apps/web/src/fonts/index.ts b/apps/web/src/fonts/index.ts index 9412b7ca..21f36a27 100644 --- a/apps/web/src/fonts/index.ts +++ b/apps/web/src/fonts/index.ts @@ -10,14 +10,17 @@ export const gameOfSquid = localFont({ export const prompt = Prompt({ weight: ["600", "500", "400", "300"], subsets: ["thai", "latin"], + variable: "--font-prompt", }); export const notoSansThaiLooped = Noto_Sans_Thai_Looped({ weight: ["400"], subsets: ["thai", "latin"], + variable: "--font-noto-sans-thai-looped", }); export const gemunuLibre = Gemunu_Libre({ weight: ["400"], subsets: ["latin"], + variable: "--font-gemunu-libre", }); From 80ae06c4894fa4f70fbce1af079e3410ad686577 Mon Sep 17 00:00:00 2001 From: beambeambeam Date: Mon, 17 Feb 2025 18:31:02 +0700 Subject: [PATCH 05/70] feat: policy init --- apps/web/package.json | 2 + apps/web/src/app/policy/policy.tsx | 39 ++++++++++ apps/web/src/app/register/layout.tsx | 14 ++++ apps/web/src/app/register/page.tsx | 4 + .../components/{ => card}/cookie-consent.tsx | 0 apps/web/src/components/card/magic-card.tsx | 2 +- .../src/components/card/policy-consent.tsx | 74 +++++++++++++++++++ apps/web/src/components/ui/checkbox.tsx | 2 +- apps/web/src/components/ui/scroll-area.tsx | 48 ++++++++++++ pnpm-lock.yaml | 65 +++++++++++++++- 10 files changed, 247 insertions(+), 3 deletions(-) create mode 100644 apps/web/src/app/policy/policy.tsx create mode 100644 apps/web/src/app/register/layout.tsx create mode 100644 apps/web/src/app/register/page.tsx rename apps/web/src/components/{ => card}/cookie-consent.tsx (100%) create mode 100644 apps/web/src/components/card/policy-consent.tsx create mode 100644 apps/web/src/components/ui/scroll-area.tsx diff --git a/apps/web/package.json b/apps/web/package.json index 1502061b..25c440d6 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -16,6 +16,7 @@ "@hookform/resolvers": "^3.10.0", "@radix-ui/react-checkbox": "^1.1.3", "@radix-ui/react-label": "^2.1.1", + "@radix-ui/react-scroll-area": "^1.2.3", "@radix-ui/react-select": "^2.1.5", "@radix-ui/react-slot": "^1.1.1", "@radix-ui/react-switch": "^1.1.2", @@ -31,6 +32,7 @@ "posthog-js": "^1.211.2", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-dropzone": "^14.3.5", "react-hook-form": "^7.54.2", "react-use-measure": "^2.1.7", "sonner": "^1.7.4", diff --git a/apps/web/src/app/policy/policy.tsx b/apps/web/src/app/policy/policy.tsx new file mode 100644 index 00000000..9e727d62 --- /dev/null +++ b/apps/web/src/app/policy/policy.tsx @@ -0,0 +1,39 @@ +function PrivacyPolicy() { + return ( +
+
+
+ นโยบายความเป็นส่วนตัว โครงการฝึกอบรมเชิงปฏิบัติการคอมพิวเตอร์ ครั้งที่ + 36 ให้ความสําคัญกับการคุ้มครองข้อมูลส่วนบุคคลของคุณ + โดยนโยบายความเป็นส่วนตัวฉบับนี้ได้อธิบายแนวปฏิบัติเกี่ยวกับการเก็บรวบรวม + ใช้ หรือ เปิดเผยข้อมูลส่วนบุคคลรวมถึงสิทธิต่าง ๆ + ของเจ้าของข้อมูลส่วนบุคคล ตามกฎหมายคุ้มครองข้อมูลส่วนบุคคล +
+
+
+
+ ); +} + +interface HeaderProps { + title: string; + index: number; +} + +function Header({ title, index }: HeaderProps) { + return ( +

+ {index}. {title} +

+ ); +} + +interface ArticleProps { + children: string; +} + +function Article({ children }: ArticleProps) { + return {children}; +} + +export default PrivacyPolicy; diff --git a/apps/web/src/app/register/layout.tsx b/apps/web/src/app/register/layout.tsx new file mode 100644 index 00000000..e396e5e3 --- /dev/null +++ b/apps/web/src/app/register/layout.tsx @@ -0,0 +1,14 @@ +import PolicyConsent from "@/components/card/policy-consent"; + +export default function RegisterLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( +
+ + {children} +
+ ); +} diff --git a/apps/web/src/app/register/page.tsx b/apps/web/src/app/register/page.tsx new file mode 100644 index 00000000..362f2c97 --- /dev/null +++ b/apps/web/src/app/register/page.tsx @@ -0,0 +1,4 @@ +function RegisterPage() { + return
; +} +export default RegisterPage; diff --git a/apps/web/src/components/cookie-consent.tsx b/apps/web/src/components/card/cookie-consent.tsx similarity index 100% rename from apps/web/src/components/cookie-consent.tsx rename to apps/web/src/components/card/cookie-consent.tsx diff --git a/apps/web/src/components/card/magic-card.tsx b/apps/web/src/components/card/magic-card.tsx index 03811b09..8547224d 100644 --- a/apps/web/src/components/card/magic-card.tsx +++ b/apps/web/src/components/card/magic-card.tsx @@ -49,7 +49,7 @@ export function MagicCard({ className, )} > -
{children}
+
{children}
{ + setIsOpen(false); + document.cookie = `${COOKIE_NAME}=true; expires=Fri, 31 Dec 9999 23:59:59 GMT`; + setTimeout(() => { + setHide(true); + }, 700); + }; + + useEffect(() => { + try { + setIsOpen(true); + if (document.cookie.includes(`${COOKIE_NAME}=true`)) { + if (!demo) { + setIsOpen(false); + setTimeout(() => { + setHide(true); + }, 700); + } + } + } catch {} + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( +
+ +
+

นโยบายข้อมูลส่วนบุคคล

+ + + + + +
+
+ + รับทราบและให้ความยินยอมตามนโยบายความเป็นส่วนตัว +
+ + +
+
+
+
+ ); +} diff --git a/apps/web/src/components/ui/checkbox.tsx b/apps/web/src/components/ui/checkbox.tsx index 5a021ff2..323e17c9 100644 --- a/apps/web/src/components/ui/checkbox.tsx +++ b/apps/web/src/components/ui/checkbox.tsx @@ -14,7 +14,7 @@ const Checkbox = React.forwardRef< , + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + {children} + + + + +)); +ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName; + +const ScrollBar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, orientation = "vertical", ...props }, ref) => ( + + + +)); +ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName; + +export { ScrollArea, ScrollBar }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e959c125..fd011621 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -206,6 +206,9 @@ importers: '@radix-ui/react-label': specifier: ^2.1.1 version: 2.1.1(@types/react-dom@19.0.3(@types/react@19.0.7))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-scroll-area': + specifier: ^1.2.3 + version: 1.2.3(@types/react-dom@19.0.3(@types/react@19.0.7))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-select': specifier: ^2.1.5 version: 2.1.5(@types/react-dom@19.0.3(@types/react@19.0.7))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -251,6 +254,9 @@ importers: react-dom: specifier: ^19.0.0 version: 19.0.0(react@19.0.0) + react-dropzone: + specifier: ^14.3.5 + version: 14.3.5(react@19.0.0) react-hook-form: specifier: ^7.54.2 version: 7.54.2(react@19.0.0) @@ -1513,6 +1519,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-scroll-area@1.2.3': + resolution: {integrity: sha512-l7+NNBfBYYJa9tNqVcP2AGvxdE3lmE6kFTBXdvHgUaZuy+4wGCL1Cl2AfaR7RKyimj7lZURGLwFO59k4eBnDJQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-select@2.1.5': resolution: {integrity: sha512-eVV7N8jBXAXnyrc+PsOF89O9AfVgGnbLxUtBb0clJ8y8ENMWLARGMI/1/SBRLz7u4HqxLgN71BJ17eono3wcjA==} peerDependencies: @@ -2188,6 +2207,10 @@ packages: resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} engines: {node: '>=8.0.0'} + attr-accept@2.2.5: + resolution: {integrity: sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==} + engines: {node: '>=4'} + available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} @@ -3075,6 +3098,10 @@ packages: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} + file-selector@2.1.2: + resolution: {integrity: sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==} + engines: {node: '>= 12'} + filelist@1.0.4: resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} @@ -4604,6 +4631,12 @@ packages: peerDependencies: react: ^19.0.0 + react-dropzone@14.3.5: + resolution: {integrity: sha512-9nDUaEEpqZLOz5v5SUcFA0CjM4vq8YbqO0WRls+EYT7+DvxUdzDPKNCPLqGfj3YL9MsniCLCD4RFA6M95V6KMQ==} + engines: {node: '>= 10.13'} + peerDependencies: + react: '>= 16.8 || 18.0.0' + react-hook-form@7.54.2: resolution: {integrity: sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg==} engines: {node: '>=18.0.0'} @@ -6790,6 +6823,23 @@ snapshots: '@types/react': 19.0.7 '@types/react-dom': 19.0.3(@types/react@19.0.7) + '@radix-ui/react-scroll-area@1.2.3(@types/react-dom@19.0.3(@types/react@19.0.7))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/number': 1.1.0 + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.7)(react@19.0.0) + '@radix-ui/react-context': 1.1.1(@types/react@19.0.7)(react@19.0.0) + '@radix-ui/react-direction': 1.1.0(@types/react@19.0.7)(react@19.0.0) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@19.0.3(@types/react@19.0.7))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.3(@types/react@19.0.7))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.7)(react@19.0.0) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.7)(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.7 + '@types/react-dom': 19.0.3(@types/react@19.0.7) + '@radix-ui/react-select@2.1.5(@types/react-dom@19.0.3(@types/react@19.0.7))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/number': 1.1.0 @@ -7669,6 +7719,8 @@ snapshots: atomic-sleep@1.0.0: {} + attr-accept@2.2.5: {} + available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.0.0 @@ -8424,7 +8476,7 @@ snapshots: is-glob: 4.0.3 stable-hash: 0.0.4 optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.15.0(eslint@9.15.0(jiti@2.4.2))(typescript@5.7.3))(eslint-import-resolver-typescript@3.7.0)(eslint@9.15.0(jiti@2.4.2)) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.15.0(eslint@9.15.0(jiti@2.4.2))(typescript@5.5.4))(eslint-import-resolver-typescript@3.7.0)(eslint@9.15.0(jiti@2.4.2)) transitivePeerDependencies: - supports-color @@ -8835,6 +8887,10 @@ snapshots: dependencies: flat-cache: 4.0.1 + file-selector@2.1.2: + dependencies: + tslib: 2.8.1 + filelist@1.0.4: dependencies: minimatch: 5.1.6 @@ -10471,6 +10527,13 @@ snapshots: react: 19.0.0 scheduler: 0.25.0 + react-dropzone@14.3.5(react@19.0.0): + dependencies: + attr-accept: 2.2.5 + file-selector: 2.1.2 + prop-types: 15.8.1 + react: 19.0.0 + react-hook-form@7.54.2(react@19.0.0): dependencies: react: 19.0.0 From d33bbb31d4e541b855e115fca7a97eefe388e278 Mon Sep 17 00:00:00 2001 From: beambeambeam Date: Mon, 17 Feb 2025 18:32:35 +0700 Subject: [PATCH 06/70] chore: clean more --- apps/server/src/app.service.ts | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/apps/server/src/app.service.ts b/apps/server/src/app.service.ts index ef342c90..c9ff19ea 100644 --- a/apps/server/src/app.service.ts +++ b/apps/server/src/app.service.ts @@ -1,18 +1,3 @@ import { Injectable } from '@nestjs/common'; -import { CheckTelResponseDto } from './dto/app.dto'; - @Injectable() -export class AppService { - getHello() { - return { - id: 'hellow', - }; - } - - checkTel(tel: string): CheckTelResponseDto { - return { - tel: tel, - check: true, - }; - } -} +export class AppService {} From 4df38c01368b8e2bd946ccd2b52ce1cef9cd9472 Mon Sep 17 00:00:00 2001 From: beambeambeam Date: Mon, 17 Feb 2025 18:37:50 +0700 Subject: [PATCH 07/70] feat: check changed --- apps/web/src/components/card/policy-consent.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/card/policy-consent.tsx b/apps/web/src/components/card/policy-consent.tsx index 60288e54..d658ad02 100644 --- a/apps/web/src/components/card/policy-consent.tsx +++ b/apps/web/src/components/card/policy-consent.tsx @@ -36,6 +36,8 @@ export default function PolicyConsent({ demo = false }) { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const [check, setCheck] = useState(false); + return (
- + setCheck(checked === true)} + /> รับทราบและให้ความยินยอมตามนโยบายความเป็นส่วนตัว
From 2e8ca85104894103f0a03e418601027260998693 Mon Sep 17 00:00:00 2001 From: beambeambeam Date: Mon, 17 Feb 2025 18:47:28 +0700 Subject: [PATCH 08/70] feat: update policy --- apps/web/src/app/policy/page.tsx | 30 ++++++++++++++++++++++++++++++ apps/web/src/app/policy/policy.tsx | 2 +- 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 apps/web/src/app/policy/page.tsx diff --git a/apps/web/src/app/policy/page.tsx b/apps/web/src/app/policy/page.tsx new file mode 100644 index 00000000..58dbaee0 --- /dev/null +++ b/apps/web/src/app/policy/page.tsx @@ -0,0 +1,30 @@ +import { TextShimmer } from "@/components/text/text-shimmer"; +import Link from "next/link"; +import PrivacyPolicy from "./policy"; + +function PolicyPage() { + return ( +
+

+ + Privacy Policy + +

+
+ +
+ + + ย้อนกลับ + + +
+ ); +} +export default PolicyPage; diff --git a/apps/web/src/app/policy/policy.tsx b/apps/web/src/app/policy/policy.tsx index 9e727d62..cbe3ceee 100644 --- a/apps/web/src/app/policy/policy.tsx +++ b/apps/web/src/app/policy/policy.tsx @@ -1,6 +1,6 @@ function PrivacyPolicy() { return ( -
+
นโยบายความเป็นส่วนตัว โครงการฝึกอบรมเชิงปฏิบัติการคอมพิวเตอร์ ครั้งที่ From 1479207d36c02ef4f17585548cda13bfb838399c Mon Sep 17 00:00:00 2001 From: beambeambeam Date: Tue, 18 Feb 2025 14:32:40 +0700 Subject: [PATCH 09/70] feat: file uploader #29 --- apps/web/src/app/register/page.tsx | 11 +- apps/web/src/components/files/card.tsx | 59 ++++++ apps/web/src/components/files/index.tsx | 195 +++++++++++++++++++ apps/web/src/hooks/use-callback-ref.ts | 27 +++ apps/web/src/hooks/use-controllable-state.ts | 67 +++++++ apps/web/src/libs/utils.ts | 20 ++ 6 files changed, 378 insertions(+), 1 deletion(-) create mode 100644 apps/web/src/components/files/card.tsx create mode 100644 apps/web/src/components/files/index.tsx create mode 100644 apps/web/src/hooks/use-callback-ref.ts create mode 100644 apps/web/src/hooks/use-controllable-state.ts diff --git a/apps/web/src/app/register/page.tsx b/apps/web/src/app/register/page.tsx index 362f2c97..ea1cae9e 100644 --- a/apps/web/src/app/register/page.tsx +++ b/apps/web/src/app/register/page.tsx @@ -1,4 +1,13 @@ +import { FileUploader } from "@/components/files"; + function RegisterPage() { - return
; + return ( +
+ + + + +
+ ); } export default RegisterPage; diff --git a/apps/web/src/components/files/card.tsx b/apps/web/src/components/files/card.tsx new file mode 100644 index 00000000..17faea03 --- /dev/null +++ b/apps/web/src/components/files/card.tsx @@ -0,0 +1,59 @@ +import { isFileWithPreview } from "@/components/files"; +import { Button } from "@/components/ui/button"; +import { formatBytes } from "@/libs/utils"; +import { FileText, X } from "lucide-react"; +import Image from "next/image"; + +interface FileCardProps { + file: File; + onRemove: () => void; +} + +export function FileCard({ file, onRemove }: FileCardProps) { + return ( +
+
+ {isFileWithPreview(file) ? : null} +
+
+

+ {file.name} +

+

+ {formatBytes(file.size)} +

+
+
+
+
+ +
+
+ ); +} + +interface FilePreviewProps { + file: File & { preview: string }; +} + +export function FilePreview({ file }: FilePreviewProps) { + if (file.type.startsWith("image/")) { + return ( + {file.name} + ); + } + + return ( +
- +
); @@ -238,7 +280,11 @@ interface ArticleProps { } function Article({ children }: ArticleProps) { - return {children}; + return {children}; +} + +function FirstArt({ children }: ArticleProps) { + return {children}; } export default PrivacyPolicy; From d731566916490734948514cea0502c0d0cd46897 Mon Sep 17 00:00:00 2001 From: Peemmaphat Sripongsai <83326313+SupeemAFK@users.noreply.github.com> Date: Wed, 19 Feb 2025 01:12:11 +0700 Subject: [PATCH 24/70] feat: del register route merge to user route, create seperate file route, create answer route --- apps/server/openapi.json | 216 ++------------ apps/server/package.json | 2 + .../src/answer/answer.controller.spec.ts | 20 ++ apps/server/src/answer/answer.controller.ts | 63 +++++ apps/server/src/answer/answer.module.ts | 9 + apps/server/src/answer/answer.service.spec.ts | 18 ++ apps/server/src/answer/answer.service.ts | 54 ++++ .../src/answer/dto/create-answer.dto.ts | 3 + .../src/answer/dto/update-answer.dto.ts | 3 + apps/server/src/app.module.ts | 10 +- apps/server/src/files/dto/upload-files.dto.ts | 43 +++ .../server/src/files/files.controller.spec.ts | 20 ++ apps/server/src/files/files.controller.ts | 28 ++ apps/server/src/files/files.module.ts | 9 + apps/server/src/files/files.service.ts | 56 ++++ apps/server/src/prisma/prisma.module.ts | 9 + .../server/src/{ => prisma}/prisma.service.ts | 0 apps/server/src/register/dto/register.dto.ts | 266 ------------------ .../src/register/register.controller.spec.ts | 18 -- .../src/register/register.controller.ts | 79 ------ apps/server/src/register/register.module.ts | 7 - apps/server/src/users/dto/create-user.dto.ts | 10 +- apps/server/src/users/dto/update-user.dto.ts | 189 +++++++++---- apps/server/src/users/users.controller.ts | 46 ++- apps/server/src/users/users.service.ts | 2 +- pnpm-lock.yaml | 148 +++++++++- 26 files changed, 685 insertions(+), 643 deletions(-) create mode 100644 apps/server/src/answer/answer.controller.spec.ts create mode 100644 apps/server/src/answer/answer.controller.ts create mode 100644 apps/server/src/answer/answer.module.ts create mode 100644 apps/server/src/answer/answer.service.spec.ts create mode 100644 apps/server/src/answer/answer.service.ts create mode 100644 apps/server/src/answer/dto/create-answer.dto.ts create mode 100644 apps/server/src/answer/dto/update-answer.dto.ts create mode 100644 apps/server/src/files/dto/upload-files.dto.ts create mode 100644 apps/server/src/files/files.controller.spec.ts create mode 100644 apps/server/src/files/files.controller.ts create mode 100644 apps/server/src/files/files.module.ts create mode 100644 apps/server/src/files/files.service.ts create mode 100644 apps/server/src/prisma/prisma.module.ts rename apps/server/src/{ => prisma}/prisma.service.ts (100%) delete mode 100644 apps/server/src/register/dto/register.dto.ts delete mode 100644 apps/server/src/register/register.controller.spec.ts delete mode 100644 apps/server/src/register/register.controller.ts delete mode 100644 apps/server/src/register/register.module.ts diff --git a/apps/server/openapi.json b/apps/server/openapi.json index 9becd61a..e9821398 100644 --- a/apps/server/openapi.json +++ b/apps/server/openapi.json @@ -9,86 +9,6 @@ "tags": ["App"] } }, - "/register": { - "get": { - "operationId": "RegisterController_register", - "parameters": [], - "responses": { "200": { "description": "" } }, - "tags": ["Register"] - } - }, - "/register/info": { - "post": { - "operationId": "RegisterController_registerInfo", - "parameters": [], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RegisterInfoPayloadDto" - } - } - } - }, - "responses": { "200": { "description": "" } }, - "tags": ["Register"] - } - }, - "/register/files": { - "post": { - "operationId": "RegisterController_registerFile", - "parameters": [], - "requestBody": { - "required": true, - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/RegsiterFilesPayloadDto" - } - } - } - }, - "responses": { "200": { "description": "" } }, - "tags": ["Register"] - } - }, - "/register/regis": { - "post": { - "operationId": "RegisterController_registerRegis", - "parameters": [], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RegisterRegisPayloadDto" - } - } - } - }, - "responses": { "200": { "description": "" } }, - "tags": ["Register"] - } - }, - "/register/academic": { - "post": { - "operationId": "RegisterController_registerAcademic", - "parameters": [], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RegisterAcademicPayloadDto" - } - } - } - }, - "responses": { "200": { "description": "" } }, - "tags": ["Register"] - } - }, "/users": { "post": { "operationId": "UsersController_create", @@ -101,7 +21,7 @@ } } }, - "responses": { "201": { "description": "" } }, + "responses": { "200": { "description": "" } }, "tags": ["Users"] }, "get": { @@ -159,6 +79,14 @@ "responses": { "200": { "description": "" } }, "tags": ["Users"] } + }, + "/files/upload": { + "post": { + "operationId": "FilesController_uploadFile", + "parameters": [], + "responses": { "200": { "description": "" } }, + "tags": ["Files"] + } } }, "info": { @@ -171,87 +99,6 @@ "servers": [], "components": { "schemas": { - "RegisterInfoPayloadDto": { - "type": "object", - "properties": { - "fullname": { "type": "string" }, - "age": { "type": "number" }, - "birth": { "type": "string", "format": "date" }, - "religion": { "type": "string" }, - "blood_group": { "type": "string" }, - "graduation": { "type": "string" }, - "school": { "type": "string" }, - "course": { "type": "string" }, - "telephone": { "type": "string" }, - "email": { "type": "string" }, - "medical_coverage": { "type": "string" }, - "chronic_disease": { "type": "string" }, - "self_medicine": { "type": "string" }, - "drug_allergic": { "type": "string" }, - "food_allergic": { "type": "string" }, - "prefer_food": { "type": "string" }, - "address": { "type": "string" }, - "home_phone_tel": { "type": "string" }, - "comcamp_attendance": { "type": "boolean" }, - "size": { "type": "string" }, - "everyday_attendence": { "type": "boolean" }, - "has_laptop": { "type": "boolean" }, - "travel": { "type": "string" }, - "parent_fullname": { "type": "string" }, - "parent_relation": { "type": "string" }, - "parent_phone": { "type": "string" } - } - }, - "RegsiterFilesPayloadDto": { - "type": "object", - "properties": { - "face_photo": { - "type": "string", - "format": "binary", - "additionalProperties": false - }, - "thai_nationalid_copy": { - "type": "string", - "format": "binary", - "additionalProperties": false - }, - "parent_permission": { - "type": "string", - "format": "binary", - "additionalProperties": false - }, - "p1": { - "type": "string", - "format": "binary", - "additionalProperties": false - }, - "p7": { - "type": "string", - "format": "binary", - "additionalProperties": false - } - } - }, - "RegisterRegisPayloadDto": { - "type": "object", - "properties": { - "answer1": { "type": "string" }, - "answer2": { "type": "string" }, - "answer3": { "type": "string" }, - "answer4": { "type": "string" }, - "answer5": { "type": "string" }, - "answer6_1": { "type": "string" }, - "answer6_2": { "type": "string" } - } - }, - "RegisterAcademicPayloadDto": { - "type": "object", - "properties": { - "algo_answer": { "type": "string" }, - "chess_notation": { "type": "string" }, - "chess_score": { "type": "string" } - } - }, "CreateUserDto": { "type": "object", "properties": { @@ -264,8 +111,8 @@ "type": "object", "properties": { "fullname": { "type": "string" }, - "age": { "type": "number" }, - "birth": { "format": "date-time", "type": "string" }, + "age": { "type": "string" }, + "birth": { "type": "string" }, "gender": { "type": "string" }, "religion": { "type": "string" }, "blood_group": { "type": "string" }, @@ -281,45 +128,16 @@ "perfer_food": { "type": "string" }, "address": { "type": "string" }, "home_phone_tel": { "type": "string" }, - "comcamp_attendance": { "type": "boolean" }, - "size": { "type": "string" }, - "everyday_attendence": { "type": "boolean" }, - "has_laptop": { "type": "boolean" }, + "comcamp_attendance": { "type": "string" }, + "shirt_size": { "type": "string" }, + "everyday_attendence": { "type": "string" }, + "has_laptop": { "type": "string" }, "travel": { "type": "string" }, "parent_fullname": { "type": "string" }, "parent_relation": { "type": "string" }, "parent_phone": { "type": "string" }, - "has_submit_answer": { "type": "boolean" } - }, - "required": [ - "fullname", - "age", - "birth", - "gender", - "religion", - "blood_group", - "graduation", - "school", - "course", - "telephone", - "medical_coverage", - "chronic_diseas", - "self_medicine", - "drug_allergic", - "food_allergic", - "perfer_food", - "address", - "home_phone_tel", - "comcamp_attendance", - "size", - "everyday_attendence", - "has_laptop", - "travel", - "parent_fullname", - "parent_relation", - "parent_phone", - "has_submit_answer" - ] + "has_submit_answer": { "type": "string" } + } } } } diff --git a/apps/server/package.json b/apps/server/package.json index 91c605d1..a8211cf4 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -21,11 +21,13 @@ }, "dependencies": { "@nestjs/common": "^10.0.0", + "@nestjs/config": "^4.0.0", "@nestjs/core": "^10.0.0", "@nestjs/platform-express": "^10.0.0", "@nestjs/swagger": "^11.0.2", "@prisma/client": "6.3.1", "@scalar/nestjs-api-reference": "^0.3.175", + "aws-sdk": "^2.1692.0", "class-validator": "^0.14.1", "nestjs-pino": "^4.2.0", "pino-http": "^10.4.0", diff --git a/apps/server/src/answer/answer.controller.spec.ts b/apps/server/src/answer/answer.controller.spec.ts new file mode 100644 index 00000000..45902e9c --- /dev/null +++ b/apps/server/src/answer/answer.controller.spec.ts @@ -0,0 +1,20 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AnswerController } from './answer.controller'; +import { AnswerService } from './answer.service'; + +describe('AnswerController', () => { + let controller: AnswerController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [AnswerController], + providers: [AnswerService], + }).compile(); + + controller = module.get(AnswerController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/apps/server/src/answer/answer.controller.ts b/apps/server/src/answer/answer.controller.ts new file mode 100644 index 00000000..72b7f828 --- /dev/null +++ b/apps/server/src/answer/answer.controller.ts @@ -0,0 +1,63 @@ +import { Controller, Get, Post, Body, Patch, Param } from '@nestjs/common'; +import { AnswerService } from './answer.service'; +import { + CreateAnswerRegisDto, + CreateAnswerAcademicDto, +} from './dto/create-answer.dto'; +import { + UpdateAnswerRegisDto, + UpdateAnswerAcademicDto, +} from './dto/update-answer.dto'; + +@Controller('answer') +export class AnswerController { + constructor(private readonly answerService: AnswerService) {} + + //Regis + @Post('regis') + createRegis(@Body() createAnswerDto: CreateAnswerRegisDto) { + return this.answerService.createRegis(createAnswerDto); + } + + @Get('regis') + findAllRegis() { + return this.answerService.findAllRegis(); + } + + @Get('regis:id') + findOneRegis(@Param('id') id: string) { + return this.answerService.findOneRegis(id); + } + + @Patch('regis:id') + updateRegis( + @Param('id') id: string, + @Body() updateAnswerDto: UpdateAnswerRegisDto, + ) { + return this.answerService.updateRegis(id, updateAnswerDto); + } + + //Academic + @Post('academic') + createAcademic(@Body() createAnswerDto: CreateAnswerAcademicDto) { + return this.answerService.createAcademic(createAnswerDto); + } + + @Get('academic') + findAllAcademic() { + return this.answerService.findAllAcademic(); + } + + @Get('academic:id') + findAcademic(@Param('id') id: string) { + return this.answerService.findOneAcademic(id); + } + + @Patch('academic:id') + updateAcademic( + @Param('id') id: string, + @Body() updateAnswerDto: UpdateAnswerAcademicDto, + ) { + return this.answerService.updateAcademic(id, updateAnswerDto); + } +} diff --git a/apps/server/src/answer/answer.module.ts b/apps/server/src/answer/answer.module.ts new file mode 100644 index 00000000..8b5c96ab --- /dev/null +++ b/apps/server/src/answer/answer.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { AnswerService } from './answer.service'; +import { AnswerController } from './answer.controller'; + +@Module({ + controllers: [AnswerController], + providers: [AnswerService], +}) +export class AnswerModule {} diff --git a/apps/server/src/answer/answer.service.spec.ts b/apps/server/src/answer/answer.service.spec.ts new file mode 100644 index 00000000..8646a95c --- /dev/null +++ b/apps/server/src/answer/answer.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AnswerService } from './answer.service'; + +describe('AnswerService', () => { + let service: AnswerService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [AnswerService], + }).compile(); + + service = module.get(AnswerService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/server/src/answer/answer.service.ts b/apps/server/src/answer/answer.service.ts new file mode 100644 index 00000000..736221f8 --- /dev/null +++ b/apps/server/src/answer/answer.service.ts @@ -0,0 +1,54 @@ +import { Injectable } from '@nestjs/common'; +import { + CreateAnswerRegisDto, + CreateAnswerAcademicDto, +} from './dto/create-answer.dto'; +import { + UpdateAnswerRegisDto, + UpdateAnswerAcademicDto, +} from './dto/update-answer.dto'; +import { PrismaService } from 'src/prisma/prisma.service'; +import { AnswerRegis, AnswerAcademic } from '@prisma/client'; + +@Injectable() +export class AnswerService { + constructor(private prisma: PrismaService) {} + + //Regis + createRegis(createAnswerRegisDto: CreateAnswerRegisDto) { + console.log(createAnswerRegisDto); + return 'This action adds a new answer'; + } + + findAllRegis(): Promise { + return this.prisma.answerRegis.findMany(); + } + + findOneRegis(id: string) { + return `This action returns a #${id} answer`; + } + + updateRegis(id: string, updateAnswerRegisDto: UpdateAnswerRegisDto) { + console.log(updateAnswerRegisDto); + return `This action updates a #${id} answer`; + } + + //Academic + createAcademic(createAnswerAcademicDto: CreateAnswerAcademicDto) { + console.log(createAnswerAcademicDto); + return 'This action adds a new answer'; + } + + findAllAcademic(): Promise { + return this.prisma.answerAcademic.findMany(); + } + + findOneAcademic(id: string) { + return `This action returns a #${id} answer`; + } + + updateAcademic(id: string, updateAnswerAcademicDto: UpdateAnswerAcademicDto) { + console.log(updateAnswerAcademicDto); + return `This action updates a #${id} answer`; + } +} diff --git a/apps/server/src/answer/dto/create-answer.dto.ts b/apps/server/src/answer/dto/create-answer.dto.ts new file mode 100644 index 00000000..e7b34367 --- /dev/null +++ b/apps/server/src/answer/dto/create-answer.dto.ts @@ -0,0 +1,3 @@ +export class CreateAnswerRegisDto {} + +export class CreateAnswerAcademicDto {} diff --git a/apps/server/src/answer/dto/update-answer.dto.ts b/apps/server/src/answer/dto/update-answer.dto.ts new file mode 100644 index 00000000..023b6642 --- /dev/null +++ b/apps/server/src/answer/dto/update-answer.dto.ts @@ -0,0 +1,3 @@ +export class UpdateAnswerRegisDto {} + +export class UpdateAnswerAcademicDto {} diff --git a/apps/server/src/app.module.ts b/apps/server/src/app.module.ts index 3dbab887..3a1ea069 100644 --- a/apps/server/src/app.module.ts +++ b/apps/server/src/app.module.ts @@ -2,11 +2,15 @@ import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { LoggerModule } from 'nestjs-pino'; -import { RegisterModule } from './register/register.module'; import { UsersModule } from './users/users.module'; +import { FilesModule } from './files/files.module'; +import { ConfigModule } from '@nestjs/config'; +import { PrismaModule } from './prisma/prisma.module'; +import { AnswerModule } from './answer/answer.module'; @Module({ imports: [ + ConfigModule.forRoot(), LoggerModule.forRoot({ pinoHttp: { transport: { @@ -25,8 +29,10 @@ import { UsersModule } from './users/users.module'; }, }, }), - RegisterModule, + PrismaModule, UsersModule, + FilesModule, + AnswerModule, ], controllers: [AppController], providers: [AppService], diff --git a/apps/server/src/files/dto/upload-files.dto.ts b/apps/server/src/files/dto/upload-files.dto.ts new file mode 100644 index 00000000..0335a6ae --- /dev/null +++ b/apps/server/src/files/dto/upload-files.dto.ts @@ -0,0 +1,43 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class UploadFileDto { + @ApiProperty({ + format: 'binary', + type: 'string', + additionalProperties: false, + required: false, + }) + face_photo?: Express.Multer.File; + + @ApiProperty({ + format: 'binary', + type: 'string', + additionalProperties: false, + required: false, + }) + thai_nationalid_copy?: Express.Multer.File; + + @ApiProperty({ + format: 'binary', + type: 'string', + additionalProperties: false, + required: false, + }) + parent_permission?: Express.Multer.File; + + @ApiProperty({ + format: 'binary', + type: 'string', + additionalProperties: false, + required: false, + }) + p1?: Express.Multer.File; + + @ApiProperty({ + format: 'binary', + type: 'string', + additionalProperties: false, + required: false, + }) + p7?: Express.Multer.File; +} diff --git a/apps/server/src/files/files.controller.spec.ts b/apps/server/src/files/files.controller.spec.ts new file mode 100644 index 00000000..b352a39d --- /dev/null +++ b/apps/server/src/files/files.controller.spec.ts @@ -0,0 +1,20 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { FilesController } from './files.controller'; +import { FilesService } from './files.service'; + +describe('FilesController', () => { + let controller: FilesController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [FilesController], + providers: [FilesService], + }).compile(); + + controller = module.get(FilesController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/apps/server/src/files/files.controller.ts b/apps/server/src/files/files.controller.ts new file mode 100644 index 00000000..24625931 --- /dev/null +++ b/apps/server/src/files/files.controller.ts @@ -0,0 +1,28 @@ +import { + Controller, + Post, + UploadedFile, + UseInterceptors, +} from '@nestjs/common'; +import { ApiConsumes, ApiResponse } from '@nestjs/swagger'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { FilesService } from './files.service'; +import { UploadFileDto } from './dto/upload-files.dto'; + +@Controller('files') +export class FilesController { + constructor(private filesService: FilesService) {} + + @ApiConsumes('multipart/form-data') + @ApiResponse({ + status: 200, + }) + @Post('upload') + @UseInterceptors(FileInterceptor('files')) + uploadFile( + @UploadedFile() + files: UploadFileDto, + ) { + return this.filesService.uploadFile(files); + } +} diff --git a/apps/server/src/files/files.module.ts b/apps/server/src/files/files.module.ts new file mode 100644 index 00000000..c290e30b --- /dev/null +++ b/apps/server/src/files/files.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { FilesController } from './files.controller'; +import { FilesService } from './files.service'; + +@Module({ + providers: [FilesService], + controllers: [FilesController], +}) +export class FilesModule {} diff --git a/apps/server/src/files/files.service.ts b/apps/server/src/files/files.service.ts new file mode 100644 index 00000000..c12b4b8a --- /dev/null +++ b/apps/server/src/files/files.service.ts @@ -0,0 +1,56 @@ +import { Injectable } from '@nestjs/common'; +import * as AWS from 'aws-sdk'; +import { UploadFileDto } from './dto/upload-files.dto'; + +@Injectable() +export class FilesService { + AWS_S3_BUCKET = 'comcamp36_user_files'; + s3 = new AWS.S3({ + accessKeyId: process.env.S3_ACCESS_KEY, + secretAccessKey: process.env.S3_SECRET_KEY, + }); + + async uploadFile(files: UploadFileDto) { + console.log(files); + const { face_photo, thai_nationalid_copy, parent_permission, p1, p7 } = + files; + const face_photo_res = await this.s3_upload(face_photo); + const thai_nationalid_copy_res = await this.s3_upload(thai_nationalid_copy); + const parent_permission_res = await this.s3_upload(parent_permission); + const p1_res = await this.s3_upload(p1); + const p7_res = await this.s3_upload(p7); + + return { + face_photo_filepath: face_photo_res.Location, + thai_nationalid_copy_filepath: thai_nationalid_copy_res.Location, + parent_permission_filepath: parent_permission_res.Location, + p1_filepath: p1_res.Location, + p7_filepath: p7_res.Location, + }; + } + + async s3_upload(file: Express.Multer.File) { + const buffer = file.buffer; + const name = file.originalname; + const mimetype = file.mimetype; + + const params = { + Bucket: this.AWS_S3_BUCKET, + Key: String(name), + Body: buffer, + ACL: 'public-read', + ContentType: mimetype, + ContentDisposition: 'inline', + CreateBucketConfiguration: { + LocationConstraint: 'ap-south-1', + }, + }; + + try { + const s3Response = await this.s3.upload(params).promise(); + return s3Response; + } catch (e) { + console.log(e); + } + } +} diff --git a/apps/server/src/prisma/prisma.module.ts b/apps/server/src/prisma/prisma.module.ts new file mode 100644 index 00000000..7207426f --- /dev/null +++ b/apps/server/src/prisma/prisma.module.ts @@ -0,0 +1,9 @@ +import { Global, Module } from '@nestjs/common'; +import { PrismaService } from './prisma.service'; + +@Global() +@Module({ + providers: [PrismaService], + exports: [PrismaService], +}) +export class PrismaModule {} diff --git a/apps/server/src/prisma.service.ts b/apps/server/src/prisma/prisma.service.ts similarity index 100% rename from apps/server/src/prisma.service.ts rename to apps/server/src/prisma/prisma.service.ts diff --git a/apps/server/src/register/dto/register.dto.ts b/apps/server/src/register/dto/register.dto.ts deleted file mode 100644 index 78da9727..00000000 --- a/apps/server/src/register/dto/register.dto.ts +++ /dev/null @@ -1,266 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; - -export class RegisterInfoPayloadDto { - @ApiProperty({ - type: 'string', - required: false, - }) - fullname?: string; - - @ApiProperty({ - type: 'number', - required: false, - }) - age?: number; - - @ApiProperty({ - format: 'date', - type: 'string', - required: false, - }) - birth?: Date; - - @ApiProperty({ - type: 'string', - required: false, - }) - religion?: string; - - @ApiProperty({ - type: 'string', - required: false, - }) - blood_group?: string; - - @ApiProperty({ - type: 'string', - required: false, - }) - graduation?: string; - - @ApiProperty({ - type: 'string', - required: false, - }) - school?: string; - - @ApiProperty({ - type: 'string', - required: false, - }) - course?: string; - - @ApiProperty({ - type: 'string', - required: false, - }) - telephone?: string; - - @ApiProperty({ - type: 'string', - required: false, - }) - email?: string; - - @ApiProperty({ - type: 'string', - required: false, - }) - medical_coverage?: string; - - @ApiProperty({ - type: 'string', - required: false, - }) - chronic_disease?: string; - - @ApiProperty({ - type: 'string', - required: false, - }) - self_medicine?: string; - - @ApiProperty({ - type: 'string', - required: false, - }) - drug_allergic?: string; - - @ApiProperty({ - type: 'string', - required: false, - }) - food_allergic?: string; - - @ApiProperty({ - type: 'string', - required: false, - }) - prefer_food?: string; - - @ApiProperty({ - type: 'string', - required: false, - }) - address?: string; - - @ApiProperty({ - type: 'string', - required: false, - }) - home_phone_tel?: string; - - @ApiProperty({ - type: 'boolean', - required: false, - }) - comcamp_attendance?: boolean; - - @ApiProperty({ - type: 'string', - required: false, - }) - size?: string; - - @ApiProperty({ - type: 'boolean', - required: false, - }) - everyday_attendence?: boolean; - - @ApiProperty({ - type: 'boolean', - required: false, - }) - has_laptop?: string; - - @ApiProperty({ - type: 'string', - required: false, - }) - travel?: string; - - @ApiProperty({ - type: 'string', - required: false, - }) - parent_fullname?: string; - - @ApiProperty({ - type: 'string', - required: false, - }) - parent_relation?: string; - - @ApiProperty({ - type: 'string', - required: false, - }) - parent_phone?: string; -} - -export class RegsiterFilesPayloadDto { - @ApiProperty({ - format: 'binary', - type: 'string', - additionalProperties: false, - required: false, - }) - face_photo?: Express.Multer.File; - - @ApiProperty({ - format: 'binary', - type: 'string', - additionalProperties: false, - required: false, - }) - thai_nationalid_copy?: Express.Multer.File; - - @ApiProperty({ - format: 'binary', - type: 'string', - additionalProperties: false, - required: false, - }) - parent_permission?: Express.Multer.File; - - @ApiProperty({ - format: 'binary', - type: 'string', - additionalProperties: false, - required: false, - }) - p1?: Express.Multer.File; - - @ApiProperty({ - format: 'binary', - type: 'string', - additionalProperties: false, - required: false, - }) - p7?: Express.Multer.File; -} - -export class RegisterRegisPayloadDto { - @ApiProperty({ - type: 'string', - required: false, - }) - answer1?: string; - - @ApiProperty({ - type: 'string', - required: false, - }) - answer2?: string; - - @ApiProperty({ - type: 'string', - required: false, - }) - answer3?: string; - - @ApiProperty({ - type: 'string', - required: false, - }) - answer4?: string; - - @ApiProperty({ - type: 'string', - required: false, - }) - answer5?: string; - - @ApiProperty({ - type: 'string', - required: false, - }) - answer6_1?: string; - - @ApiProperty({ - type: 'string', - required: false, - }) - answer6_2?: string; -} - -export class RegisterAcademicPayloadDto { - @ApiProperty({ - type: 'string', - required: false, - }) - algo_answer?: string; - - @ApiProperty({ - type: 'string', - required: false, - }) - chess_notation?: string; - - @ApiProperty({ - type: 'string', - required: false, - }) - chess_score?: string; -} diff --git a/apps/server/src/register/register.controller.spec.ts b/apps/server/src/register/register.controller.spec.ts deleted file mode 100644 index 7ac2f7bc..00000000 --- a/apps/server/src/register/register.controller.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { RegisterController } from './register.controller'; - -describe('RegisterController', () => { - let controller: RegisterController; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [RegisterController], - }).compile(); - - controller = module.get(RegisterController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); -}); diff --git a/apps/server/src/register/register.controller.ts b/apps/server/src/register/register.controller.ts deleted file mode 100644 index 75d2aec6..00000000 --- a/apps/server/src/register/register.controller.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { - Body, - Controller, - Post, - UploadedFiles, - UseInterceptors, -} from '@nestjs/common'; -import { Get } from '@nestjs/common'; -import { ApiConsumes, ApiResponse } from '@nestjs/swagger'; -import { - RegisterAcademicPayloadDto, - RegisterInfoPayloadDto, - RegisterRegisPayloadDto, - RegsiterFilesPayloadDto, -} from './dto/register.dto'; -import { FileFieldsInterceptor } from '@nestjs/platform-express'; - -@Controller('register') -export class RegisterController { - @Get() register() { - return 'Register endpoint'; - } - - @Post('/info') - @ApiResponse({ - status: 200, - }) - async registerInfo(@Body() body: RegisterInfoPayloadDto) { - console.log(body); - return 'register info endpoint'; - } - - @Post('/files') - @ApiConsumes('multipart/form-data') - @ApiResponse({ - status: 200, - }) - @UseInterceptors( - FileFieldsInterceptor([ - { name: 'face_photo', maxCount: 1 }, - { - name: 'thai_nationalid_copy', - maxCount: 1, - }, - { name: 'parent_permission', maxCount: 1 }, - { name: 'p1', maxCount: 1 }, - { name: 'p7', maxCount: 1 }, - ]), - ) - async registerFile( - @Body() body: RegsiterFilesPayloadDto, - @UploadedFiles() - files: { - face_photo?: Express.Multer.File[]; - thai_nationalid_copy?: Express.Multer.File[]; - }, - ) { - console.log(files); - return 'Register endpointd'; - } - - @Post('/regis') - @ApiResponse({ - status: 200, - }) - async registerRegis(@Body() body: RegisterRegisPayloadDto) { - console.log(body); - return 'register regis endpoint'; - } - - @Post('/academic') - @ApiResponse({ - status: 200, - }) - async registerAcademic(@Body() body: RegisterAcademicPayloadDto) { - console.log(body); - return 'register academic endpoint'; - } -} diff --git a/apps/server/src/register/register.module.ts b/apps/server/src/register/register.module.ts deleted file mode 100644 index 8357217f..00000000 --- a/apps/server/src/register/register.module.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Module } from '@nestjs/common'; -import { RegisterController } from './register.controller'; - -@Module({ - controllers: [RegisterController], -}) -export class RegisterModule {} diff --git a/apps/server/src/users/dto/create-user.dto.ts b/apps/server/src/users/dto/create-user.dto.ts index fccfb611..63ca162a 100644 --- a/apps/server/src/users/dto/create-user.dto.ts +++ b/apps/server/src/users/dto/create-user.dto.ts @@ -1,9 +1,15 @@ import { ApiProperty } from '@nestjs/swagger'; export class CreateUserDto { - @ApiProperty() + @ApiProperty({ + type: 'string', + required: true, + }) google_id: string; - @ApiProperty() + @ApiProperty({ + type: 'string', + required: true, + }) email: string; } diff --git a/apps/server/src/users/dto/update-user.dto.ts b/apps/server/src/users/dto/update-user.dto.ts index a95065cc..eb9ff4fe 100644 --- a/apps/server/src/users/dto/update-user.dto.ts +++ b/apps/server/src/users/dto/update-user.dto.ts @@ -8,138 +8,219 @@ import { } from 'class-validator'; export class UpdateUserDto { - @ApiProperty() + @ApiProperty({ + type: 'string', + required: false, + }) @IsString() @IsOptional() - fullname: string; + fullname?: string; - @ApiProperty() + @ApiProperty({ + type: 'string', + required: false, + }) @IsNumber() @IsOptional() - age: number; + age?: number; - @ApiProperty() + @ApiProperty({ + type: 'string', + required: false, + }) @IsDate() @IsOptional() - birth: Date; + birth?: Date; - @ApiProperty() + @ApiProperty({ + type: 'string', + required: false, + }) @IsString() @IsOptional() - gender: string; + gender?: string; - @ApiProperty() + @ApiProperty({ + type: 'string', + required: false, + }) @IsString() @IsOptional() - religion: string; + religion?: string; - @ApiProperty() + @ApiProperty({ + type: 'string', + required: false, + }) @IsString() @IsOptional() - blood_group: string; + blood_group?: string; - @ApiProperty() + @ApiProperty({ + type: 'string', + required: false, + }) @IsString() @IsOptional() - graduation: string; + graduation?: string; - @ApiProperty() + @ApiProperty({ + type: 'string', + required: false, + }) @IsString() @IsOptional() - school: string; + school?: string; - @ApiProperty() + @ApiProperty({ + type: 'string', + required: false, + }) @IsString() @IsOptional() - course: string; + course?: string; - @ApiProperty() + @ApiProperty({ + type: 'string', + required: false, + }) @IsString() @IsOptional() - telephone: string; + telephone?: string; - @ApiProperty() + @ApiProperty({ + type: 'string', + required: false, + }) @IsString() @IsOptional() - medical_coverage: string; + medical_coverage?: string; - @ApiProperty() + @ApiProperty({ + type: 'string', + required: false, + }) @IsString() @IsOptional() - chronic_diseas: string; + chronic_diseas?: string; - @ApiProperty() + @ApiProperty({ + type: 'string', + required: false, + }) @IsString() @IsOptional() - self_medicine: string; + self_medicine?: string; - @ApiProperty() + @ApiProperty({ + type: 'string', + required: false, + }) @IsString() @IsOptional() - drug_allergic: string; + drug_allergic?: string; - @ApiProperty() + @ApiProperty({ + type: 'string', + required: false, + }) @IsString() @IsOptional() - food_allergic: string; + food_allergic?: string; - @ApiProperty() + @ApiProperty({ + type: 'string', + required: false, + }) @IsString() @IsOptional() - perfer_food: string; + perfer_food?: string; - @ApiProperty() + @ApiProperty({ + type: 'string', + required: false, + }) @IsString() @IsOptional() - address: string; + address?: string; - @ApiProperty() + @ApiProperty({ + type: 'string', + required: false, + }) @IsString() @IsOptional() - home_phone_tel: string; + home_phone_tel?: string; - @ApiProperty() + @ApiProperty({ + type: 'string', + required: false, + }) @IsBoolean() @IsOptional() - comcamp_attendance: boolean; + comcamp_attendance?: boolean; - @ApiProperty() + @ApiProperty({ + type: 'string', + required: false, + }) @IsString() @IsOptional() - shirt_size: string; + shirt_size?: string; - @ApiProperty() + @ApiProperty({ + type: 'string', + required: false, + }) @IsBoolean() @IsOptional() - everyday_attendence: boolean; + everyday_attendence?: boolean; - @ApiProperty() + @ApiProperty({ + type: 'string', + required: false, + }) @IsBoolean() @IsOptional() - has_laptop: boolean; + has_laptop?: boolean; - @ApiProperty() + @ApiProperty({ + type: 'string', + required: false, + }) @IsString() @IsOptional() - travel: string; + travel?: string; - @ApiProperty() + @ApiProperty({ + type: 'string', + required: false, + }) @IsString() @IsOptional() - parent_fullname: string; + parent_fullname?: string; - @ApiProperty() + @ApiProperty({ + type: 'string', + required: false, + }) @IsString() @IsOptional() - parent_relation: string; + parent_relation?: string; - @ApiProperty() + @ApiProperty({ + type: 'string', + required: false, + }) @IsString() @IsOptional() - parent_phone: string; + parent_phone?: string; - @ApiProperty() + @ApiProperty({ + type: 'string', + required: false, + }) @IsBoolean() @IsOptional() - has_submit_answer: boolean; + has_submit_answer?: boolean; } diff --git a/apps/server/src/users/users.controller.ts b/apps/server/src/users/users.controller.ts index b21a8364..01f45401 100644 --- a/apps/server/src/users/users.controller.ts +++ b/apps/server/src/users/users.controller.ts @@ -6,37 +6,71 @@ import { Patch, Param, Delete, + HttpException, + HttpStatus, } from '@nestjs/common'; import { UsersService } from './users.service'; import { CreateUserDto } from './dto/create-user.dto'; import { UpdateUserDto } from './dto/update-user.dto'; +import { ApiResponse } from '@nestjs/swagger'; @Controller('users') export class UsersController { constructor(private readonly usersService: UsersService) {} @Post() - create(@Body() createUserDto: CreateUserDto) { + @ApiResponse({ + status: 200, + }) + async create(@Body() createUserDto: CreateUserDto) { + const user = await this.usersService.create(createUserDto); + if (!user) { + throw new HttpException('Not found', HttpStatus.NOT_FOUND); + } return this.usersService.create(createUserDto); } @Get() + @ApiResponse({ + status: 200, + }) findAll() { return this.usersService.findAll(); } @Get(':id') - findOne(@Param('id') id: string) { - return this.usersService.findOne(id); + @ApiResponse({ + status: 200, + }) + async findOne(@Param('id') id: string) { + const user = await this.usersService.findOne(id); + if (!user) { + throw new HttpException('Not found', HttpStatus.NOT_FOUND); + } + return user; } @Patch(':id') - update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) { + @ApiResponse({ + status: 200, + }) + async update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) { + const user = await this.usersService.findOne(id); + if (!user) { + throw new HttpException('Not found', HttpStatus.NOT_FOUND); + } return this.usersService.update(id, updateUserDto); } @Delete(':id') - remove(@Param('id') id: string) { - return this.usersService.remove(id); + @ApiResponse({ + status: 200, + }) + async remove(@Param('id') id: string) { + const user = await this.usersService.remove(id); + if (!user) { + throw new HttpException('Not found', HttpStatus.NOT_FOUND); + } + if (user) return { message: 'success removed' }; } } diff --git a/apps/server/src/users/users.service.ts b/apps/server/src/users/users.service.ts index 9d8adc21..505962e9 100644 --- a/apps/server/src/users/users.service.ts +++ b/apps/server/src/users/users.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { CreateUserDto } from './dto/create-user.dto'; import { UpdateUserDto } from './dto/update-user.dto'; -import { PrismaService } from '../prisma.service'; +import { PrismaService } from '../prisma/prisma.service'; import { User } from '@prisma/client'; @Injectable() diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e23694c0..922f0a8f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -54,6 +54,9 @@ importers: '@nestjs/common': specifier: ^10.0.0 version: 10.4.15(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/config': + specifier: ^4.0.0 + version: 4.0.0(@nestjs/common@10.4.15(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(rxjs@7.8.1) '@nestjs/core': specifier: ^10.0.0 version: 10.4.15(@nestjs/common@10.4.15(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.15)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -69,6 +72,9 @@ importers: '@scalar/nestjs-api-reference': specifier: ^0.3.175 version: 0.3.175 + aws-sdk: + specifier: ^2.1692.0 + version: 2.1692.0 class-validator: specifier: ^0.14.1 version: 0.14.1 @@ -1104,6 +1110,12 @@ packages: class-validator: optional: true + '@nestjs/config@4.0.0': + resolution: {integrity: sha512-hyhUMtVwlT+tavtPNyekl8iP0QTU1U6awKrgdOSxhMhp3TQMltx7hz2yqGTcARp+19zWPfgJudyxthuD3lPp/Q==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + rxjs: ^7.1.0 + '@nestjs/core@10.4.15': resolution: {integrity: sha512-UBejmdiYwaH6fTsz2QFBlC1cJHM+3UDeLZN+CiP9I1fRv2KlBZsmozGLbV5eS1JAVWJB4T5N5yQ0gjN8ZvcS2w==} peerDependencies: @@ -2311,6 +2323,10 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} + aws-sdk@2.1692.0: + resolution: {integrity: sha512-x511uiJ/57FIsbgUe5csJ13k3uzu25uWQE+XqfBis/sB0SFoiElJWXRkgEAUh0U6n40eT3ay5Ue4oPkRMu1LYw==} + engines: {node: '>= 10.0.0'} + axe-core@4.10.2: resolution: {integrity: sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w==} engines: {node: '>=4'} @@ -2390,6 +2406,9 @@ packages: buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + buffer@4.9.2: + resolution: {integrity: sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==} + buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} @@ -2818,6 +2837,10 @@ packages: resolution: {integrity: sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==} engines: {node: '>=12'} + dotenv-expand@12.0.1: + resolution: {integrity: sha512-LaKRbou8gt0RNID/9RoI+J2rvXsBRPMV7p+ElHlPhcSARbCPDYcYG2s1TIzAfWv4YSgyY5taidWzzs31lNV3yQ==} + engines: {node: '>=12'} + dotenv@16.4.7: resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} engines: {node: '>=12'} @@ -3111,6 +3134,10 @@ packages: eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + events@1.1.1: + resolution: {integrity: sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==} + engines: {node: '>=0.4.x'} + events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} @@ -3398,9 +3425,6 @@ packages: resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} engines: {node: '>= 0.4'} - gopd@1.0.1: - resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} - gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -3483,6 +3507,9 @@ packages: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} + ieee754@1.1.13: + resolution: {integrity: sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -3537,6 +3564,10 @@ packages: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} + is-arguments@1.2.0: + resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} + engines: {node: '>= 0.4'} + is-array-buffer@3.0.4: resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==} engines: {node: '>= 0.4'} @@ -3877,6 +3908,10 @@ packages: resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} hasBin: true + jmespath@0.16.0: + resolution: {integrity: sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==} + engines: {node: '>= 0.6.0'} + joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} @@ -4696,6 +4731,9 @@ packages: pump@3.0.2: resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} + punycode@1.3.2: + resolution: {integrity: sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -4711,6 +4749,11 @@ packages: resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} engines: {node: '>=0.6'} + querystring@0.2.0: + resolution: {integrity: sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==} + engines: {node: '>=0.4.x'} + deprecated: The querystring API is considered Legacy. new code should use the URLSearchParams API instead. + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -4926,6 +4969,9 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + sax@1.2.1: + resolution: {integrity: sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==} + scheduler@0.25.0: resolution: {integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==} @@ -5482,6 +5528,9 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + url@0.10.3: + resolution: {integrity: sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==} + use-callback-ref@1.3.3: resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} engines: {node: '>=10'} @@ -5505,10 +5554,17 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + util@0.12.5: + resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + utils-merge@1.0.1: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} + uuid@8.0.0: + resolution: {integrity: sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==} + hasBin: true + v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} @@ -5622,6 +5678,14 @@ packages: resolution: {integrity: sha512-GmqrO8WJ1NuzJ2DrziEI2o57jKAVIQNf8a18W3nCYU3H7PNWqCCVTeH6/NQE93CIllIgQS98rrmVkYgTX9fFJQ==} engines: {node: ^18.17.0 || >=20.5.0} + xml2js@0.6.2: + resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==} + engines: {node: '>=4.0.0'} + + xmlbuilder@11.0.1: + resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} + engines: {node: '>=4.0'} + xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -6556,6 +6620,14 @@ snapshots: optionalDependencies: class-validator: 0.14.1 + '@nestjs/config@4.0.0(@nestjs/common@10.4.15(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(rxjs@7.8.1)': + dependencies: + '@nestjs/common': 10.4.15(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + dotenv: 16.4.7 + dotenv-expand: 12.0.1 + lodash: 4.17.21 + rxjs: 7.8.1 + '@nestjs/core@10.4.15(@nestjs/common@10.4.15(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.15)(reflect-metadata@0.2.2)(rxjs@7.8.1)': dependencies: '@nestjs/common': 10.4.15(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -7940,6 +8012,19 @@ snapshots: dependencies: possible-typed-array-names: 1.0.0 + aws-sdk@2.1692.0: + dependencies: + buffer: 4.9.2 + events: 1.1.1 + ieee754: 1.1.13 + jmespath: 0.16.0 + querystring: 0.2.0 + sax: 1.2.1 + url: 0.10.3 + util: 0.12.5 + uuid: 8.0.0 + xml2js: 0.6.2 + axe-core@4.10.2: {} axobject-query@4.1.0: {} @@ -8066,6 +8151,12 @@ snapshots: buffer-from@1.1.2: {} + buffer@4.9.2: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + isarray: 1.0.0 + buffer@5.7.1: dependencies: base64-js: 1.5.1 @@ -8399,7 +8490,7 @@ snapshots: dependencies: es-define-property: 1.0.0 es-errors: 1.3.0 - gopd: 1.0.1 + gopd: 1.2.0 define-properties@1.2.1: dependencies: @@ -8452,6 +8543,10 @@ snapshots: dotenv-expand@10.0.0: {} + dotenv-expand@12.0.1: + dependencies: + dotenv: 16.4.7 + dotenv@16.4.7: {} dunder-proto@1.0.1: @@ -8971,6 +9066,8 @@ snapshots: eventemitter3@5.0.1: {} + events@1.1.1: {} + events@3.3.0: {} execa@5.1.1: @@ -9341,10 +9438,6 @@ snapshots: define-properties: 1.2.1 gopd: 1.2.0 - gopd@1.0.1: - dependencies: - get-intrinsic: 1.2.4 - gopd@1.2.0: {} graceful-fs@4.2.11: {} @@ -9408,6 +9501,8 @@ snapshots: dependencies: safer-buffer: 2.1.2 + ieee754@1.1.13: {} + ieee754@1.2.1: {} ignore@5.3.2: {} @@ -9481,6 +9576,11 @@ snapshots: ipaddr.js@1.9.1: {} + is-arguments@1.2.0: + dependencies: + call-bound: 1.0.3 + has-tostringtag: 1.0.2 + is-array-buffer@3.0.4: dependencies: call-bind: 1.0.7 @@ -9997,6 +10097,8 @@ snapshots: jiti@2.4.2: {} + jmespath@0.16.0: {} + joycon@3.1.1: {} js-levenshtein@1.1.6: {} @@ -10716,6 +10818,8 @@ snapshots: end-of-stream: 1.4.4 once: 1.4.0 + punycode@1.3.2: {} + punycode@2.3.1: {} pure-rand@6.1.0: {} @@ -10728,6 +10832,8 @@ snapshots: dependencies: side-channel: 1.1.0 + querystring@0.2.0: {} + queue-microtask@1.2.3: {} quick-format-unescaped@4.0.4: {} @@ -10930,6 +11036,8 @@ snapshots: safer-buffer@2.1.2: {} + sax@1.2.1: {} + scheduler@0.25.0: {} schema-utils@3.3.0: @@ -10988,7 +11096,7 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.2 get-intrinsic: 1.2.4 - gopd: 1.0.1 + gopd: 1.2.0 has-property-descriptors: 1.0.2 set-function-name@2.0.2: @@ -11551,6 +11659,11 @@ snapshots: dependencies: punycode: 2.3.1 + url@0.10.3: + dependencies: + punycode: 1.3.2 + querystring: 0.2.0 + use-callback-ref@1.3.3(@types/react@19.0.7)(react@19.0.0): dependencies: react: 19.0.0 @@ -11568,8 +11681,18 @@ snapshots: util-deprecate@1.0.2: {} + util@0.12.5: + dependencies: + inherits: 2.0.4 + is-arguments: 1.2.0 + is-generator-function: 1.0.10 + is-typed-array: 1.1.13 + which-typed-array: 1.1.15 + utils-merge@1.0.1: {} + uuid@8.0.0: {} + v8-compile-cache-lib@3.0.1: {} v8-to-istanbul@9.3.0: @@ -11729,6 +11852,13 @@ snapshots: imurmurhash: 0.1.4 signal-exit: 4.1.0 + xml2js@0.6.2: + dependencies: + sax: 1.2.1 + xmlbuilder: 11.0.1 + + xmlbuilder@11.0.1: {} + xtend@4.0.2: {} y18n@5.0.8: {} From 84a1761824c0361fa86d3e159b4cea6c737d6eb9 Mon Sep 17 00:00:00 2001 From: Peemmaphat Sripongsai <83326313+SupeemAFK@users.noreply.github.com> Date: Wed, 19 Feb 2025 01:20:06 +0700 Subject: [PATCH 25/70] fix: generate prisma client in production --- apps/server/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/package.json b/apps/server/package.json index a8211cf4..a96d9fd9 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -6,7 +6,7 @@ "private": true, "license": "UNLICENSED", "scripts": { - "build": "nest build", + "build": "prisma generate && nest build", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "start": "nest start", "dev": "nest start --watch", From 35d32a921a99246e4144799f9bdf4f7e457db850 Mon Sep 17 00:00:00 2001 From: 3raphat <96657413+3raphat@users.noreply.github.com> Date: Wed, 19 Feb 2025 03:14:07 +0700 Subject: [PATCH 26/70] feat: scroll smooth (#113) --- apps/web/src/app/layout.tsx | 2 +- apps/web/src/app/page.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 2a16df1a..530f0bcb 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -29,7 +29,7 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - + diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index 8d05982a..f70f5687 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -13,7 +13,7 @@ const Sponsors = dynamic(() => import("../components/landing/sponsor")); export default function Home() { return ( -
+
From 98a2cc3531e79e94ed974bebd437dbe9d0fffd0b Mon Sep 17 00:00:00 2001 From: Peemmaphat Sripongsai <83326313+SupeemAFK@users.noreply.github.com> Date: Wed, 19 Feb 2025 03:58:55 +0700 Subject: [PATCH 27/70] feat: add auth route and auth guard --- apps/server/openapi.json | 201 ++++++++++- apps/server/package.json | 8 + .../migration.sql | 9 + .../migration.sql | 9 + apps/server/prisma/schema.prisma | 4 +- apps/server/src/answer/answer.controller.ts | 61 +++- apps/server/src/answer/answer.service.ts | 70 +++- .../src/answer/dto/create-answer.dto.ts | 78 ++++- .../src/answer/dto/update-answer.dto.ts | 66 +++- apps/server/src/app.module.ts | 2 + apps/server/src/auth/auth.controller.ts | 20 ++ apps/server/src/auth/auth.guard.ts | 5 + apps/server/src/auth/auth.module.ts | 18 + apps/server/src/auth/auth.service.ts | 6 + apps/server/src/auth/google.strategy.ts | 29 ++ apps/server/src/main.ts | 2 + apps/server/src/users/dto/update-user.dto.ts | 12 +- apps/web/src/libs/server/types.d.ts | 314 ++++++++++++------ pnpm-lock.yaml | 242 ++++++++++++++ 19 files changed, 1011 insertions(+), 145 deletions(-) create mode 100644 apps/server/prisma/migrations/20250218183452_has_submit_default_false/migration.sql create mode 100644 apps/server/prisma/migrations/20250218183739_chess_score_int/migration.sql create mode 100644 apps/server/src/auth/auth.controller.ts create mode 100644 apps/server/src/auth/auth.guard.ts create mode 100644 apps/server/src/auth/auth.module.ts create mode 100644 apps/server/src/auth/auth.service.ts create mode 100644 apps/server/src/auth/google.strategy.ts diff --git a/apps/server/openapi.json b/apps/server/openapi.json index e9821398..f7265bc9 100644 --- a/apps/server/openapi.json +++ b/apps/server/openapi.json @@ -9,6 +9,22 @@ "tags": ["App"] } }, + "/auth": { + "get": { + "operationId": "AuthController_googleAuth", + "parameters": [], + "responses": { "200": { "description": "" } }, + "tags": ["Auth"] + } + }, + "/auth/google-redirect": { + "get": { + "operationId": "AuthController_googleAuthRedirect", + "parameters": [], + "responses": { "200": { "description": "" } }, + "tags": ["Auth"] + } + }, "/users": { "post": { "operationId": "UsersController_create", @@ -87,6 +103,126 @@ "responses": { "200": { "description": "" } }, "tags": ["Files"] } + }, + "/answer/regis": { + "post": { + "operationId": "AnswerController_createRegis", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/CreateAnswerRegisDto" } + } + } + }, + "responses": { "201": { "description": "" } }, + "tags": ["Answer"] + }, + "get": { + "operationId": "AnswerController_findAllRegis", + "parameters": [], + "responses": { "200": { "description": "" } }, + "tags": ["Answer"] + } + }, + "/answer/regis{id}": { + "get": { + "operationId": "AnswerController_findOneRegis", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { "type": "string" } + } + ], + "responses": { "200": { "description": "" } }, + "tags": ["Answer"] + }, + "patch": { + "operationId": "AnswerController_updateRegis", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { "type": "string" } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/UpdateAnswerRegisDto" } + } + } + }, + "responses": { "200": { "description": "" } }, + "tags": ["Answer"] + } + }, + "/answer/academic": { + "post": { + "operationId": "AnswerController_createAcademic", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateAnswerAcademicDto" + } + } + } + }, + "responses": { "201": { "description": "" } }, + "tags": ["Answer"] + }, + "get": { + "operationId": "AnswerController_findAllAcademic", + "parameters": [], + "responses": { "200": { "description": "" } }, + "tags": ["Answer"] + } + }, + "/answer/academic{id}": { + "get": { + "operationId": "AnswerController_findAcademic", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { "type": "string" } + } + ], + "responses": { "200": { "description": "" } }, + "tags": ["Answer"] + }, + "patch": { + "operationId": "AnswerController_updateAcademic", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { "type": "string" } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateAnswerAcademicDto" + } + } + } + }, + "responses": { "200": { "description": "" } }, + "tags": ["Answer"] + } } }, "info": { @@ -111,8 +247,8 @@ "type": "object", "properties": { "fullname": { "type": "string" }, - "age": { "type": "string" }, - "birth": { "type": "string" }, + "age": { "type": "number" }, + "birth": { "type": "number" }, "gender": { "type": "string" }, "religion": { "type": "string" }, "blood_group": { "type": "string" }, @@ -128,15 +264,68 @@ "perfer_food": { "type": "string" }, "address": { "type": "string" }, "home_phone_tel": { "type": "string" }, - "comcamp_attendance": { "type": "string" }, + "comcamp_attendance": { "type": "boolean" }, "shirt_size": { "type": "string" }, - "everyday_attendence": { "type": "string" }, - "has_laptop": { "type": "string" }, + "everyday_attendence": { "type": "boolean" }, + "has_laptop": { "type": "boolean" }, "travel": { "type": "string" }, "parent_fullname": { "type": "string" }, "parent_relation": { "type": "string" }, "parent_phone": { "type": "string" }, - "has_submit_answer": { "type": "string" } + "has_submit_answer": { "type": "boolean" } + } + }, + "CreateAnswerRegisDto": { + "type": "object", + "properties": { + "userId": { "type": "string" }, + "answer1": { "type": "string" }, + "answer2": { "type": "string" }, + "answer3": { "type": "string" }, + "answer4": { "type": "string" }, + "answer5": { "type": "string" }, + "answer6_1": { "type": "string" }, + "answer6_2": { "type": "string" } + }, + "required": [ + "userId", + "answer1", + "answer2", + "answer3", + "answer4", + "answer5", + "answer6_1", + "answer6_2" + ] + }, + "UpdateAnswerRegisDto": { + "type": "object", + "properties": { + "answer1": { "type": "string" }, + "answer2": { "type": "string" }, + "answer3": { "type": "string" }, + "answer4": { "type": "string" }, + "answer5": { "type": "string" }, + "answer6_1": { "type": "string" }, + "answer6_2": { "type": "string" } + } + }, + "CreateAnswerAcademicDto": { + "type": "object", + "properties": { + "userId": { "type": "string" }, + "algo_answer": { "type": "string" }, + "chess_notation": { "type": "string" }, + "chess_score": { "type": "number" } + }, + "required": ["userId", "algo_answer", "chess_notation", "chess_score"] + }, + "UpdateAnswerAcademicDto": { + "type": "object", + "properties": { + "algo_answer": { "type": "string" }, + "chess_notation": { "type": "string" }, + "chess_score": { "type": "number" } } } } diff --git a/apps/server/package.json b/apps/server/package.json index a96d9fd9..03aee73c 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -23,13 +23,20 @@ "@nestjs/common": "^10.0.0", "@nestjs/config": "^4.0.0", "@nestjs/core": "^10.0.0", + "@nestjs/jwt": "^11.0.0", + "@nestjs/passport": "^11.0.5", "@nestjs/platform-express": "^10.0.0", "@nestjs/swagger": "^11.0.2", "@prisma/client": "6.3.1", "@scalar/nestjs-api-reference": "^0.3.175", + "@types/passport-local": "^1.0.38", "aws-sdk": "^2.1692.0", "class-validator": "^0.14.1", + "cookie-parser": "^1.4.7", "nestjs-pino": "^4.2.0", + "passport": "^0.7.0", + "passport-google-oauth20": "^2.0.0", + "passport-local": "^1.0.0", "pino-http": "^10.4.0", "pino-pretty": "^13.0.0", "reflect-metadata": "^0.2.0", @@ -40,6 +47,7 @@ "@nestjs/schematics": "^10.0.0", "@nestjs/testing": "^10.0.0", "@repo/typescript-config": "workspace:*", + "@types/cookie-parser": "^1.4.8", "@types/express": "^5.0.0", "@types/jest": "^29.5.2", "@types/multer": "^1.4.12", diff --git a/apps/server/prisma/migrations/20250218183452_has_submit_default_false/migration.sql b/apps/server/prisma/migrations/20250218183452_has_submit_default_false/migration.sql new file mode 100644 index 00000000..e9ad2f9b --- /dev/null +++ b/apps/server/prisma/migrations/20250218183452_has_submit_default_false/migration.sql @@ -0,0 +1,9 @@ +/* + Warnings: + + - Made the column `has_submit_answer` on table `User` required. This step will fail if there are existing NULL values in that column. + +*/ +-- AlterTable +ALTER TABLE "User" ALTER COLUMN "has_submit_answer" SET NOT NULL, +ALTER COLUMN "has_submit_answer" SET DEFAULT false; diff --git a/apps/server/prisma/migrations/20250218183739_chess_score_int/migration.sql b/apps/server/prisma/migrations/20250218183739_chess_score_int/migration.sql new file mode 100644 index 00000000..fa5398bf --- /dev/null +++ b/apps/server/prisma/migrations/20250218183739_chess_score_int/migration.sql @@ -0,0 +1,9 @@ +/* + Warnings: + + - The `chess_score` column on the `AnswerAcademic` table would be dropped and recreated. This will lead to data loss if there is data in the column. + +*/ +-- AlterTable +ALTER TABLE "AnswerAcademic" DROP COLUMN "chess_score", +ADD COLUMN "chess_score" INTEGER; diff --git a/apps/server/prisma/schema.prisma b/apps/server/prisma/schema.prisma index 294d139a..3625fcc3 100644 --- a/apps/server/prisma/schema.prisma +++ b/apps/server/prisma/schema.prisma @@ -37,7 +37,7 @@ model User { parent_fullname String? parent_relation String? parent_phone String? - has_submit_answer Boolean? + has_submit_answer Boolean @default(false) file File? answer_regis AnswerRegis? answer_academic AnswerAcademic? @@ -78,7 +78,7 @@ model AnswerAcademic { userId String @unique algo_answer String? chess_notation String? - chess_score String? + chess_score Int? user User @relation(fields: [userId], references: [id]) diff --git a/apps/server/src/answer/answer.controller.ts b/apps/server/src/answer/answer.controller.ts index 72b7f828..0d28c5ad 100644 --- a/apps/server/src/answer/answer.controller.ts +++ b/apps/server/src/answer/answer.controller.ts @@ -1,4 +1,13 @@ -import { Controller, Get, Post, Body, Patch, Param } from '@nestjs/common'; +import { + Controller, + Get, + Post, + Body, + Patch, + Param, + HttpException, + HttpStatus, +} from '@nestjs/common'; import { AnswerService } from './answer.service'; import { CreateAnswerRegisDto, @@ -15,8 +24,12 @@ export class AnswerController { //Regis @Post('regis') - createRegis(@Body() createAnswerDto: CreateAnswerRegisDto) { - return this.answerService.createRegis(createAnswerDto); + async createRegis(@Body() createAnswerDto: CreateAnswerRegisDto) { + const answerRegis = await this.answerService.createRegis(createAnswerDto); + if (!answerRegis) { + throw new HttpException('Not found', HttpStatus.NOT_FOUND); + } + return answerRegis; } @Get('regis') @@ -25,8 +38,12 @@ export class AnswerController { } @Get('regis:id') - findOneRegis(@Param('id') id: string) { - return this.answerService.findOneRegis(id); + async findOneRegis(@Param('id') id: string) { + const answerRegis = await this.answerService.findOneRegis(id); + if (!answerRegis) { + throw new HttpException('Not found', HttpStatus.NOT_FOUND); + } + return answerRegis; } @Patch('regis:id') @@ -34,13 +51,22 @@ export class AnswerController { @Param('id') id: string, @Body() updateAnswerDto: UpdateAnswerRegisDto, ) { - return this.answerService.updateRegis(id, updateAnswerDto); + const answerRegis = this.answerService.updateRegis(id, updateAnswerDto); + if (!answerRegis) { + throw new HttpException('Not found', HttpStatus.NOT_FOUND); + } + return answerRegis; } //Academic @Post('academic') - createAcademic(@Body() createAnswerDto: CreateAnswerAcademicDto) { - return this.answerService.createAcademic(createAnswerDto); + async createAcademic(@Body() createAnswerDto: CreateAnswerAcademicDto) { + const answerAcademic = + await this.answerService.createAcademic(createAnswerDto); + if (!answerAcademic) { + throw new HttpException('Not found', HttpStatus.NOT_FOUND); + } + return answerAcademic; } @Get('academic') @@ -49,15 +75,26 @@ export class AnswerController { } @Get('academic:id') - findAcademic(@Param('id') id: string) { - return this.answerService.findOneAcademic(id); + async findAcademic(@Param('id') id: string) { + const answerAcademic = await this.answerService.findOneAcademic(id); + if (!answerAcademic) { + throw new HttpException('Not found', HttpStatus.NOT_FOUND); + } + return answerAcademic; } @Patch('academic:id') - updateAcademic( + async updateAcademic( @Param('id') id: string, @Body() updateAnswerDto: UpdateAnswerAcademicDto, ) { - return this.answerService.updateAcademic(id, updateAnswerDto); + const answerAcademic = await this.answerService.updateAcademic( + id, + updateAnswerDto, + ); + if (!answerAcademic) { + throw new HttpException('Not found', HttpStatus.NOT_FOUND); + } + return answerAcademic; } } diff --git a/apps/server/src/answer/answer.service.ts b/apps/server/src/answer/answer.service.ts index 736221f8..74b9a357 100644 --- a/apps/server/src/answer/answer.service.ts +++ b/apps/server/src/answer/answer.service.ts @@ -15,40 +15,78 @@ export class AnswerService { constructor(private prisma: PrismaService) {} //Regis - createRegis(createAnswerRegisDto: CreateAnswerRegisDto) { - console.log(createAnswerRegisDto); - return 'This action adds a new answer'; + createRegis( + createAnswerRegisDto: CreateAnswerRegisDto, + ): Promise { + return this.prisma.answerRegis.create({ + data: { + userId: createAnswerRegisDto.userId, + answer1: createAnswerRegisDto.answer1, + answer2: createAnswerRegisDto.answer2, + answer3: createAnswerRegisDto.answer3, + answer4: createAnswerRegisDto.answer4, + answer5: createAnswerRegisDto.answer5, + answer6_1: createAnswerRegisDto.answer6_1, + answer6_2: createAnswerRegisDto.answer6_2, + }, + }); } findAllRegis(): Promise { return this.prisma.answerRegis.findMany(); } - findOneRegis(id: string) { - return `This action returns a #${id} answer`; + findOneRegis(id: string): Promise { + return this.prisma.answerRegis.findUnique({ + where: { id }, + }); } - updateRegis(id: string, updateAnswerRegisDto: UpdateAnswerRegisDto) { - console.log(updateAnswerRegisDto); - return `This action updates a #${id} answer`; + updateRegis( + id: string, + updateAnswerRegisDto: UpdateAnswerRegisDto, + ): Promise { + return this.prisma.answerRegis.update({ + where: { id }, + data: { + ...updateAnswerRegisDto, + }, + }); } //Academic - createAcademic(createAnswerAcademicDto: CreateAnswerAcademicDto) { - console.log(createAnswerAcademicDto); - return 'This action adds a new answer'; + createAcademic( + createAnswerAcademicDto: CreateAnswerAcademicDto, + ): Promise { + return this.prisma.answerAcademic.create({ + data: { + userId: createAnswerAcademicDto.userId, + algo_answer: createAnswerAcademicDto.algo_answer, + chess_notation: createAnswerAcademicDto.chess_notation, + chess_score: createAnswerAcademicDto.chess_score, + }, + }); } findAllAcademic(): Promise { return this.prisma.answerAcademic.findMany(); } - findOneAcademic(id: string) { - return `This action returns a #${id} answer`; + findOneAcademic(id: string): Promise { + return this.prisma.answerAcademic.findUnique({ + where: { id }, + }); } - updateAcademic(id: string, updateAnswerAcademicDto: UpdateAnswerAcademicDto) { - console.log(updateAnswerAcademicDto); - return `This action updates a #${id} answer`; + updateAcademic( + id: string, + updateAnswerAcademicDto: UpdateAnswerAcademicDto, + ): Promise { + return this.prisma.answerAcademic.update({ + where: { id }, + data: { + ...updateAnswerAcademicDto, + }, + }); } } diff --git a/apps/server/src/answer/dto/create-answer.dto.ts b/apps/server/src/answer/dto/create-answer.dto.ts index e7b34367..c9b61d67 100644 --- a/apps/server/src/answer/dto/create-answer.dto.ts +++ b/apps/server/src/answer/dto/create-answer.dto.ts @@ -1,3 +1,77 @@ -export class CreateAnswerRegisDto {} +import { ApiProperty } from '@nestjs/swagger'; -export class CreateAnswerAcademicDto {} +export class CreateAnswerRegisDto { + @ApiProperty({ + type: 'string', + required: true, + }) + userId: string; + + @ApiProperty({ + type: 'string', + required: true, + }) + answer1: string; + + @ApiProperty({ + type: 'string', + required: true, + }) + answer2: string; + + @ApiProperty({ + type: 'string', + required: true, + }) + answer3: string; + + @ApiProperty({ + type: 'string', + required: true, + }) + answer4: string; + + @ApiProperty({ + type: 'string', + required: true, + }) + answer5: string; + + @ApiProperty({ + type: 'string', + required: true, + }) + answer6_1: string; + + @ApiProperty({ + type: 'string', + required: true, + }) + answer6_2: string; +} + +export class CreateAnswerAcademicDto { + @ApiProperty({ + type: 'string', + required: true, + }) + userId: string; + + @ApiProperty({ + type: 'string', + required: true, + }) + algo_answer: string; + + @ApiProperty({ + type: 'string', + required: true, + }) + chess_notation: string; + + @ApiProperty({ + type: 'number', + required: true, + }) + chess_score: number; +} diff --git a/apps/server/src/answer/dto/update-answer.dto.ts b/apps/server/src/answer/dto/update-answer.dto.ts index 023b6642..caf0a0ab 100644 --- a/apps/server/src/answer/dto/update-answer.dto.ts +++ b/apps/server/src/answer/dto/update-answer.dto.ts @@ -1,3 +1,65 @@ -export class UpdateAnswerRegisDto {} +import { ApiProperty } from '@nestjs/swagger'; -export class UpdateAnswerAcademicDto {} +export class UpdateAnswerRegisDto { + @ApiProperty({ + type: 'string', + required: false, + }) + answer1?: string; + + @ApiProperty({ + type: 'string', + required: false, + }) + answer2?: string; + + @ApiProperty({ + type: 'string', + required: false, + }) + answer3?: string; + + @ApiProperty({ + type: 'string', + required: false, + }) + answer4?: string; + + @ApiProperty({ + type: 'string', + required: false, + }) + answer5?: string; + + @ApiProperty({ + type: 'string', + required: false, + }) + answer6_1?: string; + + @ApiProperty({ + type: 'string', + required: false, + }) + answer6_2?: string; +} + +export class UpdateAnswerAcademicDto { + @ApiProperty({ + type: 'string', + required: false, + }) + algo_answer?: string; + + @ApiProperty({ + type: 'string', + required: false, + }) + chess_notation?: string; + + @ApiProperty({ + type: 'number', + required: false, + }) + chess_score?: number; +} diff --git a/apps/server/src/app.module.ts b/apps/server/src/app.module.ts index 3a1ea069..9530efb7 100644 --- a/apps/server/src/app.module.ts +++ b/apps/server/src/app.module.ts @@ -7,6 +7,7 @@ import { FilesModule } from './files/files.module'; import { ConfigModule } from '@nestjs/config'; import { PrismaModule } from './prisma/prisma.module'; import { AnswerModule } from './answer/answer.module'; +import { AuthModule } from './auth/auth.module'; @Module({ imports: [ @@ -30,6 +31,7 @@ import { AnswerModule } from './answer/answer.module'; }, }), PrismaModule, + AuthModule, UsersModule, FilesModule, AnswerModule, diff --git a/apps/server/src/auth/auth.controller.ts b/apps/server/src/auth/auth.controller.ts new file mode 100644 index 00000000..e0c73d36 --- /dev/null +++ b/apps/server/src/auth/auth.controller.ts @@ -0,0 +1,20 @@ +import { GoogleOAuthGuard } from './auth.guard'; +import { Controller, Get, Req, UseGuards } from '@nestjs/common'; +import type { Request } from 'express'; +import { AuthService } from './auth.service'; + +@Controller('auth') +export class AuthController { + constructor(private readonly authService: AuthService) {} + + @Get() + @UseGuards(GoogleOAuthGuard) + async googleAuth() {} + + @Get('google-redirect') + @UseGuards(GoogleOAuthGuard) + googleAuthRedirect(@Req() req: Request) { + this.authService.login(); + return req.user; + } +} diff --git a/apps/server/src/auth/auth.guard.ts b/apps/server/src/auth/auth.guard.ts new file mode 100644 index 00000000..1dc9447b --- /dev/null +++ b/apps/server/src/auth/auth.guard.ts @@ -0,0 +1,5 @@ +import { Injectable } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class GoogleOAuthGuard extends AuthGuard('google') {} diff --git a/apps/server/src/auth/auth.module.ts b/apps/server/src/auth/auth.module.ts new file mode 100644 index 00000000..bb59745a --- /dev/null +++ b/apps/server/src/auth/auth.module.ts @@ -0,0 +1,18 @@ +import { GoogleOauthStrategy } from './google.strategy'; +import { Module } from '@nestjs/common'; +import { AuthController } from './auth.controller'; +import { AuthService } from './auth.service'; +import { JwtModule } from '@nestjs/jwt'; + +@Module({ + imports: [ + JwtModule.register({ + global: true, + secret: process.env.JWT_SECRET, + signOptions: { expiresIn: '60s' }, + }), + ], + controllers: [AuthController], + providers: [AuthService, GoogleOauthStrategy], +}) +export class AuthModule {} diff --git a/apps/server/src/auth/auth.service.ts b/apps/server/src/auth/auth.service.ts new file mode 100644 index 00000000..8562bc41 --- /dev/null +++ b/apps/server/src/auth/auth.service.ts @@ -0,0 +1,6 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class AuthService { + login() {} +} diff --git a/apps/server/src/auth/google.strategy.ts b/apps/server/src/auth/google.strategy.ts new file mode 100644 index 00000000..bfe1fb8f --- /dev/null +++ b/apps/server/src/auth/google.strategy.ts @@ -0,0 +1,29 @@ +import { PassportStrategy } from '@nestjs/passport'; +import { Profile, Strategy } from 'passport-google-oauth20'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class GoogleOauthStrategy extends PassportStrategy(Strategy, 'google') { + constructor() { + super({ + clientID: process.env.GOOGLE_OAUTH_CLIENT_ID, + clientSecret: process.env.GOOGLE_OAUTH_CLIENT_SECRET, + callbackURL: process.env.GOOGLE_OAUTH_CALLBACK_URL, + scope: ['email', 'profile'], + }); + } + + async validate( + _accessToken: string, + _refreshToken: string, + profile: Profile, + ) { + const { id, emails } = profile; + + //req.user + return { + googleID: id, + email: emails[0].value, + }; + } +} diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts index d60430a1..18acb9ee 100644 --- a/apps/server/src/main.ts +++ b/apps/server/src/main.ts @@ -4,9 +4,11 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { apiReference } from '@scalar/nestjs-api-reference'; import { Logger } from 'nestjs-pino'; +import cookieParser from 'cookie-parser'; async function bootstrap() { const app = await NestFactory.create(AppModule); + app.use(cookieParser()); const config = new DocumentBuilder() .setTitle('Comcamp36 API') diff --git a/apps/server/src/users/dto/update-user.dto.ts b/apps/server/src/users/dto/update-user.dto.ts index eb9ff4fe..c2c473fb 100644 --- a/apps/server/src/users/dto/update-user.dto.ts +++ b/apps/server/src/users/dto/update-user.dto.ts @@ -17,7 +17,7 @@ export class UpdateUserDto { fullname?: string; @ApiProperty({ - type: 'string', + type: 'number', required: false, }) @IsNumber() @@ -25,7 +25,7 @@ export class UpdateUserDto { age?: number; @ApiProperty({ - type: 'string', + type: 'number', required: false, }) @IsDate() @@ -153,7 +153,7 @@ export class UpdateUserDto { home_phone_tel?: string; @ApiProperty({ - type: 'string', + type: 'boolean', required: false, }) @IsBoolean() @@ -169,7 +169,7 @@ export class UpdateUserDto { shirt_size?: string; @ApiProperty({ - type: 'string', + type: 'boolean', required: false, }) @IsBoolean() @@ -177,7 +177,7 @@ export class UpdateUserDto { everyday_attendence?: boolean; @ApiProperty({ - type: 'string', + type: 'boolean', required: false, }) @IsBoolean() @@ -217,7 +217,7 @@ export class UpdateUserDto { parent_phone?: string; @ApiProperty({ - type: 'string', + type: 'boolean', required: false, }) @IsBoolean() diff --git a/apps/web/src/libs/server/types.d.ts b/apps/web/src/libs/server/types.d.ts index abde0753..eecc84fe 100644 --- a/apps/web/src/libs/server/types.d.ts +++ b/apps/web/src/libs/server/types.d.ts @@ -15,14 +15,14 @@ export interface paths { patch?: never; trace?: never; }; - "/register": { + "/auth": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get: operations["RegisterController_register"]; + get: operations["AuthController_googleAuth"]; put?: never; post?: never; delete?: never; @@ -31,39 +31,55 @@ export interface paths { patch?: never; trace?: never; }; - "/register/info": { + "/auth/google-redirect": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get?: never; + get: operations["AuthController_googleAuthRedirect"]; put?: never; - post: operations["RegisterController_registerInfo"]; + post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/register/files": { + "/users": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get?: never; + get: operations["UsersController_findAll"]; put?: never; - post: operations["RegisterController_registerFile"]; + post: operations["UsersController_create"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/register/regis": { + "/users/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["UsersController_findOne"]; + put?: never; + post?: never; + delete: operations["UsersController_remove"]; + options?: never; + head?: never; + patch: operations["UsersController_update"]; + trace?: never; + }; + "/files/upload": { parameters: { query?: never; header?: never; @@ -72,107 +88,125 @@ export interface paths { }; get?: never; put?: never; - post: operations["RegisterController_registerRegis"]; + post: operations["FilesController_uploadFile"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/register/academic": { + "/answer/regis": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get?: never; + get: operations["AnswerController_findAllRegis"]; put?: never; - post: operations["RegisterController_registerAcademic"]; + post: operations["AnswerController_createRegis"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/users": { + "/answer/regis{id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get: operations["UsersController_findAll"]; + get: operations["AnswerController_findOneRegis"]; put?: never; - post: operations["UsersController_create"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch: operations["AnswerController_updateRegis"]; + trace?: never; + }; + "/answer/academic": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["AnswerController_findAllAcademic"]; + put?: never; + post: operations["AnswerController_createAcademic"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/users/{id}": { + "/answer/academic{id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get: operations["UsersController_findOne"]; + get: operations["AnswerController_findAcademic"]; put?: never; post?: never; - delete: operations["UsersController_remove"]; + delete?: never; options?: never; head?: never; - patch: operations["UsersController_update"]; + patch: operations["AnswerController_updateAcademic"]; trace?: never; }; } export type webhooks = Record; export interface components { schemas: { - RegisterInfoPayloadDto: { + CreateUserDto: { + google_id: string; + email: string; + }; + UpdateUserDto: { fullname?: string; age?: number; - /** Format: date */ - birth?: string; + birth?: number; + gender?: string; religion?: string; blood_group?: string; graduation?: string; school?: string; course?: string; telephone?: string; - email?: string; medical_coverage?: string; - chronic_disease?: string; + chronic_diseas?: string; self_medicine?: string; drug_allergic?: string; food_allergic?: string; - prefer_food?: string; + perfer_food?: string; address?: string; home_phone_tel?: string; comcamp_attendance?: boolean; - size?: string; + shirt_size?: string; everyday_attendence?: boolean; has_laptop?: boolean; travel?: string; parent_fullname?: string; parent_relation?: string; parent_phone?: string; - }; - RegsiterFilesPayloadDto: { - /** Format: binary */ - face_photo?: File; - /** Format: binary */ - thai_nationalid_copy?: File; - /** Format: binary */ - parent_permission?: File; - /** Format: binary */ - p1?: File; - /** Format: binary */ - p7?: File; - }; - RegisterRegisPayloadDto: { + has_submit_answer?: boolean; + }; + CreateAnswerRegisDto: { + userId: string; + answer1: string; + answer2: string; + answer3: string; + answer4: string; + answer5: string; + answer6_1: string; + answer6_2: string; + }; + UpdateAnswerRegisDto: { answer1?: string; answer2?: string; answer3?: string; @@ -181,44 +215,16 @@ export interface components { answer6_1?: string; answer6_2?: string; }; - RegisterAcademicPayloadDto: { + CreateAnswerAcademicDto: { + userId: string; + algo_answer: string; + chess_notation: string; + chess_score: number; + }; + UpdateAnswerAcademicDto: { algo_answer?: string; chess_notation?: string; - chess_score?: string; - }; - CreateUserDto: { - google_id: string; - email: string; - }; - UpdateUserDto: { - fullname: string; - age: number; - /** Format: date-time */ - birth: string; - gender: string; - religion: string; - blood_group: string; - graduation: string; - school: string; - course: string; - telephone: string; - medical_coverage: string; - chronic_diseas: string; - self_medicine: string; - drug_allergic: string; - food_allergic: string; - perfer_food: string; - address: string; - home_phone_tel: string; - comcamp_attendance: boolean; - size: string; - everyday_attendence: boolean; - has_laptop: boolean; - travel: string; - parent_fullname: string; - parent_relation: string; - parent_phone: string; - has_submit_answer: boolean; + chess_score?: number; }; }; responses: never; @@ -246,7 +252,7 @@ export interface operations { }; }; }; - RegisterController_register: { + AuthController_googleAuth: { parameters: { query?: never; header?: never; @@ -263,18 +269,31 @@ export interface operations { }; }; }; - RegisterController_registerInfo: { + AuthController_googleAuthRedirect: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - requestBody: { - content: { - "application/json": components["schemas"]["RegisterInfoPayloadDto"]; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; }; }; + }; + UsersController_findAll: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; responses: { 200: { headers: { @@ -284,7 +303,7 @@ export interface operations { }; }; }; - RegisterController_registerFile: { + UsersController_create: { parameters: { query?: never; header?: never; @@ -293,7 +312,7 @@ export interface operations { }; requestBody: { content: { - "multipart/form-data": components["schemas"]["RegsiterFilesPayloadDto"]; + "application/json": components["schemas"]["CreateUserDto"]; }; }; responses: { @@ -305,18 +324,35 @@ export interface operations { }; }; }; - RegisterController_registerRegis: { + UsersController_findOne: { parameters: { query?: never; header?: never; - path?: never; + path: { + id: string; + }; cookie?: never; }; - requestBody: { - content: { - "application/json": components["schemas"]["RegisterRegisPayloadDto"]; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; }; }; + }; + UsersController_remove: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; responses: { 200: { headers: { @@ -326,16 +362,18 @@ export interface operations { }; }; }; - RegisterController_registerAcademic: { + UsersController_update: { parameters: { query?: never; header?: never; - path?: never; + path: { + id: string; + }; cookie?: never; }; requestBody: { content: { - "application/json": components["schemas"]["RegisterAcademicPayloadDto"]; + "application/json": components["schemas"]["UpdateUserDto"]; }; }; responses: { @@ -347,7 +385,7 @@ export interface operations { }; }; }; - UsersController_findAll: { + FilesController_uploadFile: { parameters: { query?: never; header?: never; @@ -364,7 +402,24 @@ export interface operations { }; }; }; - UsersController_create: { + AnswerController_findAllRegis: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + AnswerController_createRegis: { parameters: { query?: never; header?: never; @@ -373,7 +428,7 @@ export interface operations { }; requestBody: { content: { - "application/json": components["schemas"]["CreateUserDto"]; + "application/json": components["schemas"]["CreateAnswerRegisDto"]; }; }; responses: { @@ -385,7 +440,7 @@ export interface operations { }; }; }; - UsersController_findOne: { + AnswerController_findOneRegis: { parameters: { query?: never; header?: never; @@ -404,7 +459,7 @@ export interface operations { }; }; }; - UsersController_remove: { + AnswerController_updateRegis: { parameters: { query?: never; header?: never; @@ -413,6 +468,27 @@ export interface operations { }; cookie?: never; }; + requestBody: { + content: { + "application/json": components["schemas"]["UpdateAnswerRegisDto"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + AnswerController_findAllAcademic: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; requestBody?: never; responses: { 200: { @@ -423,7 +499,47 @@ export interface operations { }; }; }; - UsersController_update: { + AnswerController_createAcademic: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateAnswerAcademicDto"]; + }; + }; + responses: { + 201: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + AnswerController_findAcademic: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + AnswerController_updateAcademic: { parameters: { query?: never; header?: never; @@ -434,7 +550,7 @@ export interface operations { }; requestBody: { content: { - "application/json": components["schemas"]["UpdateUserDto"]; + "application/json": components["schemas"]["UpdateAnswerAcademicDto"]; }; }; responses: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 922f0a8f..0153916a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -60,6 +60,12 @@ importers: '@nestjs/core': specifier: ^10.0.0 version: 10.4.15(@nestjs/common@10.4.15(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.15)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/jwt': + specifier: ^11.0.0 + version: 11.0.0(@nestjs/common@10.4.15(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)) + '@nestjs/passport': + specifier: ^11.0.5 + version: 11.0.5(@nestjs/common@10.4.15(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(passport@0.7.0) '@nestjs/platform-express': specifier: ^10.0.0 version: 10.4.15(@nestjs/common@10.4.15(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.15) @@ -72,15 +78,30 @@ importers: '@scalar/nestjs-api-reference': specifier: ^0.3.175 version: 0.3.175 + '@types/passport-local': + specifier: ^1.0.38 + version: 1.0.38 aws-sdk: specifier: ^2.1692.0 version: 2.1692.0 class-validator: specifier: ^0.14.1 version: 0.14.1 + cookie-parser: + specifier: ^1.4.7 + version: 1.4.7 nestjs-pino: specifier: ^4.2.0 version: 4.2.0(@nestjs/common@10.4.15(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(pino-http@10.4.0) + passport: + specifier: ^0.7.0 + version: 0.7.0 + passport-google-oauth20: + specifier: ^2.0.0 + version: 2.0.0 + passport-local: + specifier: ^1.0.0 + version: 1.0.0 pino-http: specifier: ^10.4.0 version: 10.4.0 @@ -106,6 +127,9 @@ importers: '@repo/typescript-config': specifier: workspace:* version: link:../../packages/typescript-config + '@types/cookie-parser': + specifier: ^1.4.8 + version: 1.4.8(@types/express@5.0.0) '@types/express': specifier: ^5.0.0 version: 5.0.0 @@ -1133,6 +1157,11 @@ packages: '@nestjs/websockets': optional: true + '@nestjs/jwt@11.0.0': + resolution: {integrity: sha512-v7YRsW3Xi8HNTsO+jeHSEEqelX37TVWgwt+BcxtkG/OfXJEOs6GZdbdza200d6KqId1pJQZ6UPj1F0M6E+mxaA==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + '@nestjs/mapped-types@2.1.0': resolution: {integrity: sha512-W+n+rM69XsFdwORF11UqJahn4J3xi4g/ZEOlJNL6KoW5ygWSmBB2p0S2BZ4FQeS/NDH72e6xIcu35SfJnE8bXw==} peerDependencies: @@ -1146,6 +1175,12 @@ packages: class-validator: optional: true + '@nestjs/passport@11.0.5': + resolution: {integrity: sha512-ulQX6mbjlws92PIM15Naes4F4p2JoxGnIJuUsdXQPT+Oo2sqQmENEZXM7eYuimocfHnKlcfZOuyzbA33LwUlOQ==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + passport: ^0.5.0 || ^0.6.0 || ^0.7.0 + '@nestjs/platform-express@10.4.15': resolution: {integrity: sha512-63ZZPkXHjoDyO7ahGOVcybZCRa7/Scp6mObQKjcX/fTEq1YJeU75ELvMsuQgc8U2opMGOBD7GVuc4DV0oeDHoA==} peerDependencies: @@ -1938,6 +1973,11 @@ packages: '@types/conventional-commits-parser@5.0.1': resolution: {integrity: sha512-7uz5EHdzz2TqoMfV7ee61Egf5y6NkcO4FB/1iCCQnbeiI1F3xzv3vK5dBCXUCLQgGYS+mUeigK1iKQzvED+QnQ==} + '@types/cookie-parser@1.4.8': + resolution: {integrity: sha512-l37JqFrOJ9yQfRQkljb41l0xVphc7kg5JTjjr+pLRZ0IyZ49V4BQ8vbF4Ut2C2e+WH4al3xD3ZwYwIUfnbT4NQ==} + peerDependencies: + '@types/express': '*' + '@types/cookiejar@2.1.5': resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==} @@ -1980,6 +2020,9 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/jsonwebtoken@9.0.7': + resolution: {integrity: sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==} + '@types/methods@1.1.4': resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} @@ -1992,6 +2035,15 @@ packages: '@types/node@20.17.6': resolution: {integrity: sha512-VEI7OdvK2wP7XHnsuXbAJnEpEkF6NjSN45QJlL4VGqZSXsnicpesdTWsg9RISeSdYd3yeRj/y3k5KGjUXYnFwQ==} + '@types/passport-local@1.0.38': + resolution: {integrity: sha512-nsrW4A963lYE7lNTv9cr5WmiUD1ibYJvWrpE13oxApFsRt77b0RdtZvKbCdNIY4v/QZ6TRQWaDDEwV1kCTmcXg==} + + '@types/passport-strategy@0.2.38': + resolution: {integrity: sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==} + + '@types/passport@1.0.17': + resolution: {integrity: sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==} + '@types/qs@6.9.18': resolution: {integrity: sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==} @@ -2366,6 +2418,10 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + base64url@3.0.1: + resolution: {integrity: sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==} + engines: {node: '>=6.0.0'} + bin-links@5.0.0: resolution: {integrity: sha512-sdleLVfCjBtgO5cNjA2HVRvWBJAHs4zwenaCPMNJAJU0yNxpzj80IpjOIimkpkr+mhlA+how5poQtt53PygbHA==} engines: {node: ^18.17.0 || >=20.5.0} @@ -2403,6 +2459,9 @@ packages: bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -2631,6 +2690,10 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-parser@1.4.7: + resolution: {integrity: sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==} + engines: {node: '>= 0.8.0'} + cookie-signature@1.0.6: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} @@ -2638,6 +2701,10 @@ packages: resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} engines: {node: '>= 0.6'} + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + cookiejar@2.1.4: resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} @@ -2852,6 +2919,9 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -3973,10 +4043,20 @@ packages: resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} engines: {'0': node >= 0.2.0} + jsonwebtoken@9.0.2: + resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} + engines: {node: '>=12', npm: '>=6'} + jsx-ast-utils@3.3.5: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} + jwa@1.4.1: + resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==} + + jws@3.2.2: + resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -4101,9 +4181,24 @@ packages: lodash.camelcase@4.3.0: resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + lodash.isplainobject@4.0.6: resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + lodash.kebabcase@4.1.1: resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} @@ -4116,6 +4211,9 @@ packages: lodash.mergewith@4.6.2: resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + lodash.snakecase@4.1.1: resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} @@ -4374,6 +4472,9 @@ packages: resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + oauth@0.10.0: + resolution: {integrity: sha512-1orQ9MT1vHFGQxhuy7E/0gECD3fd2fCC+PIX+/jgmU/gI3EpRocXtmtvxCO5x3WZ443FLTLFWNDjl5MPJf9u+Q==} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -4506,6 +4607,26 @@ packages: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} + passport-google-oauth20@2.0.0: + resolution: {integrity: sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ==} + engines: {node: '>= 0.4.0'} + + passport-local@1.0.0: + resolution: {integrity: sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow==} + engines: {node: '>= 0.4.0'} + + passport-oauth2@1.8.0: + resolution: {integrity: sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==} + engines: {node: '>= 0.4.0'} + + passport-strategy@1.0.0: + resolution: {integrity: sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==} + engines: {node: '>= 0.4.0'} + + passport@0.7.0: + resolution: {integrity: sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==} + engines: {node: '>= 0.4.0'} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -4547,6 +4668,9 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + pause@0.0.1: + resolution: {integrity: sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -5491,6 +5615,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + uid2@0.0.4: + resolution: {integrity: sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==} + uid@2.0.2: resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==} engines: {node: '>=8'} @@ -6644,6 +6771,12 @@ snapshots: transitivePeerDependencies: - encoding + '@nestjs/jwt@11.0.0(@nestjs/common@10.4.15(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))': + dependencies: + '@nestjs/common': 10.4.15(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@types/jsonwebtoken': 9.0.7 + jsonwebtoken: 9.0.2 + '@nestjs/mapped-types@2.1.0(@nestjs/common@10.4.15(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(class-validator@0.14.1)(reflect-metadata@0.2.2)': dependencies: '@nestjs/common': 10.4.15(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -6651,6 +6784,11 @@ snapshots: optionalDependencies: class-validator: 0.14.1 + '@nestjs/passport@11.0.5(@nestjs/common@10.4.15(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(passport@0.7.0)': + dependencies: + '@nestjs/common': 10.4.15(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + passport: 0.7.0 + '@nestjs/platform-express@10.4.15(@nestjs/common@10.4.15(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.15)': dependencies: '@nestjs/common': 10.4.15(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -7418,6 +7556,10 @@ snapshots: dependencies: '@types/node': 20.17.6 + '@types/cookie-parser@1.4.8(@types/express@5.0.0)': + dependencies: + '@types/express': 5.0.0 + '@types/cookiejar@2.1.5': {} '@types/eslint-scope@3.7.7': @@ -7471,6 +7613,10 @@ snapshots: '@types/json5@0.0.29': {} + '@types/jsonwebtoken@9.0.7': + dependencies: + '@types/node': 20.17.6 + '@types/methods@1.1.4': {} '@types/mime@1.3.5': {} @@ -7483,6 +7629,21 @@ snapshots: dependencies: undici-types: 6.19.8 + '@types/passport-local@1.0.38': + dependencies: + '@types/express': 5.0.0 + '@types/passport': 1.0.17 + '@types/passport-strategy': 0.2.38 + + '@types/passport-strategy@0.2.38': + dependencies: + '@types/express': 5.0.0 + '@types/passport': 1.0.17 + + '@types/passport@1.0.17': + dependencies: + '@types/express': 5.0.0 + '@types/qs@6.9.18': {} '@types/range-parser@1.2.7': {} @@ -8088,6 +8249,8 @@ snapshots: base64-js@1.5.1: {} + base64url@3.0.1: {} + bin-links@5.0.0: dependencies: cmd-shim: 7.0.0 @@ -8149,6 +8312,8 @@ snapshots: dependencies: node-int64: 0.4.0 + buffer-equal-constant-time@1.0.1: {} + buffer-from@1.1.2: {} buffer@4.9.2: @@ -8367,10 +8532,17 @@ snapshots: convert-source-map@2.0.0: {} + cookie-parser@1.4.7: + dependencies: + cookie: 0.7.2 + cookie-signature: 1.0.6 + cookie-signature@1.0.6: {} cookie@0.7.1: {} + cookie@0.7.2: {} + cookiejar@2.1.4: {} core-js@3.40.0: {} @@ -8557,6 +8729,10 @@ snapshots: eastasianwidth@0.2.0: {} + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + ee-first@1.1.1: {} ejs@3.1.10: @@ -10144,6 +10320,19 @@ snapshots: jsonparse@1.3.1: {} + jsonwebtoken@9.0.2: + dependencies: + jws: 3.2.2 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.6.3 + jsx-ast-utils@3.3.5: dependencies: array-includes: 3.1.8 @@ -10151,6 +10340,17 @@ snapshots: object.assign: 4.1.5 object.values: 1.2.0 + jwa@1.4.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@3.2.2: + dependencies: + jwa: 1.4.1 + safe-buffer: 5.2.1 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -10261,8 +10461,18 @@ snapshots: lodash.camelcase@4.3.0: {} + lodash.includes@4.3.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + lodash.isplainobject@4.0.6: {} + lodash.isstring@4.0.1: {} + lodash.kebabcase@4.1.1: {} lodash.memoize@4.1.2: {} @@ -10271,6 +10481,8 @@ snapshots: lodash.mergewith@4.6.2: {} + lodash.once@4.1.1: {} + lodash.snakecase@4.1.1: {} lodash.startcase@4.4.0: {} @@ -10484,6 +10696,8 @@ snapshots: dependencies: path-key: 4.0.0 + oauth@0.10.0: {} + object-assign@4.1.1: {} object-inspect@1.13.3: {} @@ -10650,6 +10864,30 @@ snapshots: parseurl@1.3.3: {} + passport-google-oauth20@2.0.0: + dependencies: + passport-oauth2: 1.8.0 + + passport-local@1.0.0: + dependencies: + passport-strategy: 1.0.0 + + passport-oauth2@1.8.0: + dependencies: + base64url: 3.0.1 + oauth: 0.10.0 + passport-strategy: 1.0.0 + uid2: 0.0.4 + utils-merge: 1.0.1 + + passport-strategy@1.0.0: {} + + passport@0.7.0: + dependencies: + passport-strategy: 1.0.0 + pause: 0.0.1 + utils-merge: 1.0.1 + path-exists@4.0.0: {} path-exists@5.0.0: {} @@ -10675,6 +10913,8 @@ snapshots: path-type@4.0.0: {} + pause@0.0.1: {} + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -11626,6 +11866,8 @@ snapshots: typescript@5.7.3: {} + uid2@0.0.4: {} + uid@2.0.2: dependencies: '@lukeed/csprng': 1.1.0 From 1acd3fd1b94c0453f401b5a062fdffbaebfba854 Mon Sep 17 00:00:00 2001 From: beam <132804471+beambeambeam@users.noreply.github.com> Date: Wed, 19 Feb 2025 15:19:27 +0700 Subject: [PATCH 28/70] ApplyFort(Wait For Design) (#114) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Atiwit🎅🏻 <44371578+atiwit@users.noreply.github.com> --- .github/workflows/test.yml | 12 +- apps/web/components.json | 21 + apps/web/package.json | 5 +- apps/web/src/app/apply/page.tsx | 31 ++ apps/web/src/components/applyformuser.tsx | 478 ++++++++++++++++++++++ pnpm-lock.yaml | 16 +- 6 files changed, 553 insertions(+), 10 deletions(-) create mode 100644 apps/web/components.json create mode 100644 apps/web/src/app/apply/page.tsx create mode 100644 apps/web/src/components/applyformuser.tsx diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index acc642d9..2f432cb4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,12 +24,12 @@ jobs: - name: "Create env file" run: | - touch .env.local - echo NEXT_PUBLIC_POSTHOG_KEY="NEXT_PUBLIC_POSTHOG_KEY" >> .env.local - echo NEXT_PUBLIC_POSTHOG_HOST="NEXT_PUBLIC_POSTHOG_HOST" >> .env.local - echo NEXT_PUBLIC_SERVER_URL="NEXT_PUBLIC_SERVER_URL" >> .env.local - echo SERVER_URL="SERVER_URL" >> .env.local - cat .env.local + touch .env + echo NEXT_PUBLIC_POSTHOG_KEY="NEXT_PUBLIC_POSTHOG_KEY" >> .env + echo NEXT_PUBLIC_POSTHOG_HOST="NEXT_PUBLIC_POSTHOG_HOST" >> .env + echo NEXT_PUBLIC_SERVER_URL="NEXT_PUBLIC_SERVER_URL" >> .env + echo SERVER_URL="SERVER_URL" >> .env + cat .env - uses: pnpm/action-setup@v4 name: Install pnpm diff --git a/apps/web/components.json b/apps/web/components.json new file mode 100644 index 00000000..275d4164 --- /dev/null +++ b/apps/web/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/index.css", + "baseColor": "zinc", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/apps/web/package.json b/apps/web/package.json index 986ae6f1..4555b92d 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -39,6 +39,7 @@ "react-use-measure": "^2.1.7", "sonner": "^1.7.4", "tailwind-merge": "^2.6.0", + "tailwindcss-animate": "^1.0.7", "vaul": "^1.1.2", "zo": "^0.0.2", "zod": "^3.24.1" @@ -47,8 +48,8 @@ "@eslint/eslintrc": "^3", "@repo/typescript-config": "workspace:*", "@tailwindcss/postcss": "^4.0.0", - "@types/node": "^20", - "@types/react": "^19", + "@types/node": "^20.17.6", + "@types/react": "^19.0.7", "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "15.1.5", diff --git a/apps/web/src/app/apply/page.tsx b/apps/web/src/app/apply/page.tsx new file mode 100644 index 00000000..472b61de --- /dev/null +++ b/apps/web/src/app/apply/page.tsx @@ -0,0 +1,31 @@ +"use client"; +import FadeObserverDiv from "@/components/landing/fade-div"; +import Footer from "@/components/navigate/footer"; +import Navbar from "@/components/navigate/navbar"; +import dynamic from "next/dynamic"; +const ApplyForm = dynamic(() => import("@/components/applyformuser")); + +function Apply() { + return ( +
+ + +
+
+ +
+

+ Apply For Comcamp36 +

+
+
+ + +
+
+
+
+ ); +} + +export default Apply; diff --git a/apps/web/src/components/applyformuser.tsx b/apps/web/src/components/applyformuser.tsx new file mode 100644 index 00000000..1ffc801a --- /dev/null +++ b/apps/web/src/components/applyformuser.tsx @@ -0,0 +1,478 @@ +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Textarea } from "@/components/ui/textarea"; +import { zodResolver } from "@hookform/resolvers/zod"; +import React from "react"; +import { Control, FieldPath, useForm } from "react-hook-form"; +import { z } from "zod"; + +const formSchema = z.object({ + fullname: z.string().min(1, "กรุณากรอกชื่อ-นามสกุล"), + age: z.number().min(15, "อายุต้องไม่น้อยกว่า 15 ปี"), + birth: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "กรุณากรอกวันเกิดให้ถูกต้อง"), + gender: z.string(), + religion: z.string(), + blood_group: z.string(), + graduation: z.string(), + school: z.string(), + course: z.string(), + telephone: z.string().min(10, "กรุณากรอกเบอร์โทรศัพท์ให้ถูกต้อง"), + email: z.string().email("กรุณากรอกอีเมลให้ถูกต้อง"), + medical_coverage: z.string(), + chronic_disease: z.string(), + self_medicine: z.string(), + drug_allergic: z.string(), + food_allergic: z.string(), + prefer_food: z.string(), + address: z.string(), + home_phone_tel: z.string(), + comcamp_attendance: z.boolean(), + size: z.string(), + everyday_attendance: z.boolean(), + has_laptop: z.boolean(), + travel: z.string(), + parent_fullname: z.string(), + parent_relation: z.string(), + parent_phone: z.string(), +}); + +const ApplyForm = () => { + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + fullname: "", + age: 16, + birth: "", + gender: "", + religion: "", + blood_group: "", + graduation: "", + school: "", + course: "", + telephone: "", + email: "", + medical_coverage: "", + chronic_disease: "", + self_medicine: "", + drug_allergic: "", + food_allergic: "", + prefer_food: "", + address: "", + home_phone_tel: "", + comcamp_attendance: true, + size: "", + everyday_attendance: true, + has_laptop: true, + travel: "", + parent_fullname: "", + parent_relation: "", + parent_phone: "", + }, + }); + + const onSubmit = (values: z.infer) => { + console.log(values); + }; + + return ( +
+
+
+

+ แบบฟอร์มสมัครเข้าร่วมค่าย +

+
+ +
+
+ + {/* Personal Information Section */} +
+

+ ข้อมูลส่วนตัว +

+
+ +
+ + +
+ + + +
+
+ + {/* Education Section */} +
+

+ ข้อมูลการศึกษา +

+
+ + + +
+
+ + {/* Contact Section */} +
+

+ ข้อมูลการติดต่อ +

+
+ + + + +
+
+ + {/* Health Section */} +
+

+ ข้อมูลสุขภาพ +

+
+ + + + + + +
+
+ + {/* Camp Information Section */} +
+

+ ข้อมูลการเข้าร่วมค่าย +

+
+ + + + + +
+
+ + {/* Parent Information Section */} +
+

+ ข้อมูลผู้ปกครอง +

+
+ + + +
+
+ +
+ +
+
+ +
+
+
+ ); +}; + +interface ApplyFormFieldProps { + name: FieldPath>; + label: string; + placeholder?: string; + description?: string; + inputType?: string; + options?: string[]; + formControl: Control>; + yesNo?: boolean; + className?: string; +} + +const ApplyFormField: React.FC = ({ + name, + label, + placeholder, + description, + inputType = "text", + options = [], + formControl, + yesNo = false, + className = "", +}) => { + return ( + ( + + {label} + + {yesNo ? ( +
+
+ field.onChange(true)} + className="border-gray-400" + /> + +
+
+ field.onChange(false)} + className="border-gray-400" + /> + +
+
+ ) : inputType === "checkbox" ? ( +
+ + +
+ ) : inputType === "select" ? ( + + ) : inputType === "textarea" ? ( +