From b8a3101452f8a60e6bf9b9da091458f0670741cd Mon Sep 17 00:00:00 2001 From: Yuki Ito <37338201+ykit00@users.noreply.github.com> Date: Fri, 2 Feb 2024 16:32:05 +0900 Subject: [PATCH 01/12] style: run prisma format --- prisma/schema.prisma | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 3ebb8699..489af027 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -13,9 +13,9 @@ datasource db { // ----------------------------------------------------------------------------- model User { - id String @id + id String @id createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - displayName String @unique + displayName String @unique } From 867aac8c90f4e4491b4ae9f88d2332f6ab2c9167 Mon Sep 17 00:00:00 2001 From: Yuki Ito <37338201+ykit00@users.noreply.github.com> Date: Fri, 2 Feb 2024 16:39:07 +0900 Subject: [PATCH 02/12] feat: Add UserSolvedProblem table and relation to User model --- .../migration.sql | 11 +++++++++++ prisma/schema.prisma | 15 ++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 prisma/migrations/20240209123948_create_user_solved_problem_table/migration.sql diff --git a/prisma/migrations/20240209123948_create_user_solved_problem_table/migration.sql b/prisma/migrations/20240209123948_create_user_solved_problem_table/migration.sql new file mode 100644 index 00000000..23be143f --- /dev/null +++ b/prisma/migrations/20240209123948_create_user_solved_problem_table/migration.sql @@ -0,0 +1,11 @@ +-- CreateTable +CREATE TABLE "UserSolvedProblem" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "userId" TEXT NOT NULL, + "courseId" TEXT NOT NULL, + "programId" TEXT NOT NULL, + "languageId" TEXT NOT NULL, + CONSTRAINT "UserSolvedProblem_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 489af027..8ed807e7 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -17,5 +17,18 @@ model User { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - displayName String @unique + displayName String @unique + userSolvedAnswers UserSolvedProblem[] +} + +model UserSolvedProblem { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User @relation(fields: [userId], references: [id]) + userId String + courseId String + programId String + languageId String } From caf1014b55deb4795550d6e6300ab428376f748d Mon Sep 17 00:00:00 2001 From: tatehito <48908346+Tatehito@users.noreply.github.com> Date: Thu, 8 Feb 2024 11:49:06 +0900 Subject: [PATCH 03/12] fix: Programming language name (#31) --- src/app/(withAuth)/courses/[courseId]/page.tsx | 9 +++++++-- src/problems/problemData.ts | 5 +++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/app/(withAuth)/courses/[courseId]/page.tsx b/src/app/(withAuth)/courses/[courseId]/page.tsx index 658e10ea..5e40d815 100644 --- a/src/app/(withAuth)/courses/[courseId]/page.tsx +++ b/src/app/(withAuth)/courses/[courseId]/page.tsx @@ -17,7 +17,12 @@ import type { NextPage } from 'next'; import NextLink from 'next/link'; import React, { useEffect, useState } from 'react'; -import { courseIdToProgramIdLists, languageIds, programIdToName } from '../../../../problems/problemData'; +import { + courseIdToProgramIdLists, + languageIdToName, + languageIds, + programIdToName, +} from '../../../../problems/problemData'; import { getLanguageIdFromSessionStorage, setLanguageIdToSessionStorage } from '../../../lib/SessionStorage'; const CoursePage: NextPage<{ params: { courseId: string } }> = ({ params }) => { @@ -47,7 +52,7 @@ const CoursePage: NextPage<{ params: { courseId: string } }> = ({ params }) => { > {languageIds.map((languageId) => ( ))} diff --git a/src/problems/problemData.ts b/src/problems/problemData.ts index 766e6f83..f755cc16 100644 --- a/src/problems/problemData.ts +++ b/src/problems/problemData.ts @@ -7,6 +7,11 @@ export type ProgramId = (typeof programIds)[number]; export const languageIds = ['js', 'java']; export type LanguageId = (typeof languageIds)[number]; +export const languageIdToName: Record = { + js: 'JavaScript', + java: 'Java', +}; + export const courseIdToName: Record = { tuBeginner1: '初級プログラミングⅠ', tuBeginner2: '初級プログラミングⅡ', From 2bbcdebbc294f9546eb5f358f4beac28306fc1d2 Mon Sep 17 00:00:00 2001 From: tatehito <48908346+Tatehito@users.noreply.github.com> Date: Thu, 8 Feb 2024 11:53:18 +0900 Subject: [PATCH 04/12] feat: Add checkpoint problem (#30) --- .../[problemId]/CheckpointProblem.tsx | 99 +++++++++++++++++++ .../[problemId]/ExecutionResultProblem.tsx | 65 ++++++++++++ .../(withAuth)/problems/[problemId]/page.tsx | 65 ++++-------- src/app/lib/solveProblem.ts | 19 +++- .../organisms/SyntaxHighlighter.tsx | 20 ++-- src/components/organisms/TurtleGraphics.tsx | 51 +++++----- src/problems/problemData.ts | 10 +- src/types.ts | 2 + tests/solveProblem.test.ts | 30 ++++++ 9 files changed, 277 insertions(+), 84 deletions(-) create mode 100644 src/app/(withAuth)/problems/[problemId]/CheckpointProblem.tsx create mode 100644 src/app/(withAuth)/problems/[problemId]/ExecutionResultProblem.tsx diff --git a/src/app/(withAuth)/problems/[problemId]/CheckpointProblem.tsx b/src/app/(withAuth)/problems/[problemId]/CheckpointProblem.tsx new file mode 100644 index 00000000..5d953304 --- /dev/null +++ b/src/app/(withAuth)/problems/[problemId]/CheckpointProblem.tsx @@ -0,0 +1,99 @@ +'use client'; + +import { Box, Button, Flex, HStack, VStack } from '@chakra-ui/react'; +import { useEffect, useRef, useState } from 'react'; + +import { SyntaxHighlighter } from '../../../../components/organisms/SyntaxHighlighter'; +import type { TurtleGraphicsHandle } from '../../../../components/organisms/TurtleGraphics'; +import { TurtleGraphics } from '../../../../components/organisms/TurtleGraphics'; +import { generateProgram } from '../../../../problems/problemData'; +import type { ProblemType } from '../../../../types'; +import { getLanguageIdFromSessionStorage } from '../../../lib/SessionStorage'; + +interface CheckpointProblemProps { + problemId: string; + setStep: (step: ProblemType) => void; +} + +export const CheckpointProblem: React.FC = ({ problemId }) => { + const turtleGraphicsRef = useRef(null); + const [selectedLanguageId, setSelectedLanguageId] = useState(''); + + // TODO: チェックポイントを取得する処理が実装できたら置き換える + const getCheckPointLines = [2, 4]; + const [problemProgram, setProblemProgram] = useState(''); + const [beforeCheckPointLine, setBeforeCheckPointLine] = useState(1); + const [currentCheckPointLine, setCurrentCheckPointLine] = useState(getCheckPointLines[0]); + + useEffect(() => { + setSelectedLanguageId(getLanguageIdFromSessionStorage()); + }, []); + + useEffect(() => { + setProblemProgram(generateProgram(problemId, selectedLanguageId)); + }, [problemId, selectedLanguageId]); + + const handleClickResetButton = (): void => { + turtleGraphicsRef.current?.init(); + }; + + const handleClickAnswerButton = (): void => { + const isCorrect = turtleGraphicsRef.current?.isCorrect(); + + // TODO: 一旦アラートで表示 + if (isCorrect) { + alert('正解です'); + + if (currentCheckPointLine === getCheckPointLines.at(-1)) return; + + setBeforeCheckPointLine(currentCheckPointLine); + setCurrentCheckPointLine(getCheckPointLines[getCheckPointLines.indexOf(currentCheckPointLine) + 1]); + } else { + alert('不正解です'); + // setStep('step'); + } + }; + + return ( + + + 茶色にハイライトされている行における盤面を作成してください。 + 青色のハイライト時点の実行結果 + + + + 茶色のハイライト時点の実行結果 + + + + + + + + + + + + + + + + ); +}; diff --git a/src/app/(withAuth)/problems/[problemId]/ExecutionResultProblem.tsx b/src/app/(withAuth)/problems/[problemId]/ExecutionResultProblem.tsx new file mode 100644 index 00000000..d5634228 --- /dev/null +++ b/src/app/(withAuth)/problems/[problemId]/ExecutionResultProblem.tsx @@ -0,0 +1,65 @@ +'use client'; + +import { Box, Button, Flex, HStack, VStack } from '@chakra-ui/react'; +import { useEffect, useState, useRef } from 'react'; + +import { SyntaxHighlighter } from '../../../../components/organisms/SyntaxHighlighter'; +import type { TurtleGraphicsHandle } from '../../../../components/organisms/TurtleGraphics'; +import { TurtleGraphics } from '../../../../components/organisms/TurtleGraphics'; +import { generateProgram } from '../../../../problems/problemData'; +import type { ProblemType } from '../../../../types'; +import { getLanguageIdFromSessionStorage } from '../../../lib/SessionStorage'; + +interface ExecutionResultProblemProps { + problemId: string; + setStep: (step: ProblemType) => void; +} + +export const ExecutionResultProblem: React.FC = ({ problemId, setStep }) => { + const turtleGraphicsRef = useRef(null); + const [selectedLanguageId, setSelectedLanguageId] = useState(''); + + useEffect(() => { + setSelectedLanguageId(getLanguageIdFromSessionStorage()); + }, []); + + const problemProgram = generateProgram(problemId, selectedLanguageId); + + const handleClickResetButton = (): void => { + turtleGraphicsRef.current?.init(); + }; + + const handleClickAnswerButton = (): void => { + const isCorrect = turtleGraphicsRef.current?.isCorrect(); + + // TODO: 一旦アラートで表示 + if (isCorrect) { + alert('正解です'); + } else { + alert('不正解です'); + setStep('checkpoint'); + } + }; + + return ( + + + プログラムの実行後の結果を解答してください。 + + + + + + + {/* 画面に収まる高さに設定 */} + + + + + + + + + + ); +}; diff --git a/src/app/(withAuth)/problems/[problemId]/page.tsx b/src/app/(withAuth)/problems/[problemId]/page.tsx index a33c1a40..61e6ee5b 100644 --- a/src/app/(withAuth)/problems/[problemId]/page.tsx +++ b/src/app/(withAuth)/problems/[problemId]/page.tsx @@ -1,37 +1,26 @@ 'use client'; -import { Box, Button, Flex, HStack, Heading, VStack } from '@chakra-ui/react'; +import { Heading, VStack } from '@chakra-ui/react'; import type { NextPage } from 'next'; -import { useEffect, useState, useRef } from 'react'; +import { useState } from 'react'; -import { SyntaxHighlighter } from '../../../../components/organisms/SyntaxHighlighter'; -import type { TurtleGraphicsHandle } from '../../../../components/organisms/TurtleGraphics'; -import { TurtleGraphics } from '../../../../components/organisms/TurtleGraphics'; -import { programIdToName, generateProgram } from '../../../../problems/problemData'; -import { getLanguageIdFromSessionStorage } from '../../../lib/SessionStorage'; +import { programIdToName } from '../../../../problems/problemData'; +import type { ProblemType } from '../../../../types'; -const ProblemPage: NextPage<{ params: { problemId: string } }> = ({ params }) => { - const turtleGraphicsRef = useRef(null); - const [selectedLanguageId, setSelectedLanguageId] = useState(''); - - useEffect(() => { - setSelectedLanguageId(getLanguageIdFromSessionStorage()); - }, []); +import { CheckpointProblem } from './CheckpointProblem'; +import { ExecutionResultProblem } from './ExecutionResultProblem'; - const problemProgram = generateProgram(params.problemId, selectedLanguageId); - - const handleClickResetButton = (): void => { - turtleGraphicsRef.current?.reset(); - }; - - const handleClickAnswerButton = (): void => { - const isCorrect = turtleGraphicsRef.current?.isCorrect(); - - // TODO: 一旦アラートで表示 - if (isCorrect) { - alert('正解です'); - } else { - alert('不正解です'); +const ProblemPage: NextPage<{ params: { problemId: string } }> = ({ params }) => { + const [step, setStep] = useState('normal'); + + const ProblemComponent: React.FC = () => { + switch (step) { + case 'normal': { + return ; + } + case 'checkpoint': { + return ; + } } }; @@ -39,25 +28,7 @@ const ProblemPage: NextPage<{ params: { problemId: string } }> = ({ params }) =>
{programIdToName[params.problemId]} - - - プログラムの実行後の結果を解答してください。 - - - - - - - {/* 画面に収まる高さに設定 */} - - - - - - - - - +
); diff --git a/src/app/lib/solveProblem.ts b/src/app/lib/solveProblem.ts index 875cf46a..d0076a93 100644 --- a/src/app/lib/solveProblem.ts +++ b/src/app/lib/solveProblem.ts @@ -53,6 +53,14 @@ export function solveProblem(program: string): SolveProblemResult { const characters = executeEval(mergedCommand); + const board = new BoardClass(); + for (const history of histories) { + if (!history.characters) continue; + + for (const character of history.characters) { + board.updateGrid(character); + } + } for (const character of characters) { board.updateGrid(character); } @@ -72,14 +80,15 @@ export function solveProblem(program: string): SolveProblemResult { export function isAnswerCorrect( problemProgram: string, answerCharacters: CharacterClass[], - answerBoard: BoardClass + answerBoard: BoardClass, + step?: number ): boolean { - const answer = solveProblem(problemProgram); + const correctAnswer = solveProblem(problemProgram).histories?.at(step || -1); - if (!answer.characters || !answer.board) return false; + if (!correctAnswer || !correctAnswer.characters) return false; // 順番は関係なく、id以外のキャラクターの状態が一致しているかチェック - const isCorrectCharacters: boolean = answer.characters.every((correctCharacter) => { + const isCorrectCharacters: boolean = correctAnswer.characters.every((correctCharacter) => { const character = answerCharacters.find( (answerCharacter) => answerCharacter.name === correctCharacter.name && @@ -93,7 +102,7 @@ export function isAnswerCorrect( }); // すべてのセルの色が一致しているかチェック - const isCorrectBoard: boolean = answer.board.grid.every((rows, rowIndex) => + const isCorrectBoard: boolean = correctAnswer.board.grid.every((rows, rowIndex) => rows.every((column, columnIndex) => { const cell = answerBoard.grid[rowIndex][columnIndex]; return cell.color === column.color; diff --git a/src/components/organisms/SyntaxHighlighter.tsx b/src/components/organisms/SyntaxHighlighter.tsx index 1eba52e1..11b63e0a 100644 --- a/src/components/organisms/SyntaxHighlighter.tsx +++ b/src/components/organisms/SyntaxHighlighter.tsx @@ -4,12 +4,16 @@ import { Prism } from 'react-syntax-highlighter'; import { vscDarkPlus } from 'react-syntax-highlighter/dist/cjs/styles/prism'; interface SyntaxHighlighterProps { + beforeCheckPointLine?: number; code: string; programmingLanguageId: string; + currentCheckPointLine?: number; } export const SyntaxHighlighter: React.FC = ({ + beforeCheckPointLine: beforeCheckPointLine, code: code, + currentCheckPointLine: currentCheckPointLine, programmingLanguageId: programmingLanguageId, }) => { return ( @@ -25,15 +29,17 @@ export const SyntaxHighlighter: React.FC = ({ }} language={programmingLanguageId === 'c' ? 'cpp' : programmingLanguageId} lineNumberStyle={{ paddingRight: 0, marginRight: 16, minWidth: '1rem' }} - // lineProps={(lineNumber) => { - lineProps={() => { - const style = { + lineProps={(lineNumber) => { + const style: React.CSSProperties = { padding: 0, + backgroundColor: '', }; - // TODO: チェックポイント問題・ステップ問題のハイライト - // if (isStepPage && problem && lineNumber === problem.traceList[highlightedLineCount].row + 1) { - // style.backgroundColor = '#744210'; - // } + // チェックポイント問題・ステップ問題のハイライト + if (lineNumber === beforeCheckPointLine) { + style.backgroundColor = '#2E3D9F'; + } else if (lineNumber === currentCheckPointLine) { + style.backgroundColor = '#744210'; + } return { style }; }} showLineNumbers={true} diff --git a/src/components/organisms/TurtleGraphics.tsx b/src/components/organisms/TurtleGraphics.tsx index 0ac8f3c8..ecb979e7 100644 --- a/src/components/organisms/TurtleGraphics.tsx +++ b/src/components/organisms/TurtleGraphics.tsx @@ -2,11 +2,11 @@ import { Box, Grid, GridItem } from '@chakra-ui/react'; import Image from 'next/image'; -import React, { useState, forwardRef, useImperativeHandle, useMemo } from 'react'; +import React, { useState, forwardRef, useImperativeHandle, useEffect, useCallback } from 'react'; import { Board } from '../../app/lib/Board'; import { Character } from '../../app/lib/Character'; -import { isAnswerCorrect } from '../../app/lib/solveProblem'; +import { isAnswerCorrect, solveProblem } from '../../app/lib/solveProblem'; import type { CellColor, CharacterDirection, SelectedCell } from '../../types'; import { TurtleGraphicsController } from '../molecules/TurtleGraphicsController'; @@ -21,37 +21,45 @@ export const GRID_SIZE = 40; interface TurtleGraphicsProps { isEnableOperation?: boolean; problemProgram: string; + currentCheckPointLine?: number; + beforeCheckPointLine?: number; } export interface TurtleGraphicsHandle { - reset(): void; + init(): void; isCorrect(): boolean; } export const TurtleGraphics = forwardRef( - ({ isEnableOperation = false, problemProgram }, ref) => { + ({ beforeCheckPointLine = 0, currentCheckPointLine, isEnableOperation = false, problemProgram }, ref) => { const [board, setBoard] = useState(new Board()); + const [characters, setCharacters] = useState([]); const [selectedCharacter, setSelectedCharacter] = useState(); const [selectedCell, setSelectedCell] = useState(); const [dragging, setDragging] = useState(false); + const init = useCallback((): void => { + if (!problemProgram) return; + + const solveResult = solveProblem(problemProgram).histories?.at(beforeCheckPointLine); + const initBoard = solveResult?.board; + const initCharacters = solveResult?.characters; + + setBoard(initBoard || new Board()); + setCharacters(initCharacters || []); + setSelectedCharacter(undefined); + setSelectedCell(undefined); + }, [beforeCheckPointLine, problemProgram]); + useImperativeHandle(ref, () => ({ // 親コンポーネントから関数を呼び出せるようにする - reset, + init, isCorrect, })); - // // TODO: プログラムから盤面を生成する処理ができたら置き換える - const getInitialBoard = (): Board => { - return new Board(); - }; - - // TODO: プログラムから盤面を生成する処理ができたら置き換える - const getInitialCharacters = (): Character[] => { - return []; - }; - const getInitialCharactersResult = useMemo(() => getInitialCharacters(), []); - const [characters, setCharacters] = useState(getInitialCharactersResult); + useEffect(() => { + init(); + }, [beforeCheckPointLine, init, problemProgram]); const updateCharacter = (updater: (char: Character) => void): void => { if (!selectedCharacter) return; @@ -71,15 +79,8 @@ export const TurtleGraphics = forwardRef { - setBoard(getInitialBoard()); - setCharacters(getInitialCharacters()); - setSelectedCharacter(undefined); - setSelectedCell(undefined); - }; - const isCorrect = (): boolean => { - return isAnswerCorrect(problemProgram, characters, board); + return isAnswerCorrect(problemProgram, characters, board, currentCheckPointLine); }; const handleClickCharacter = (character: Character): void => { @@ -187,6 +188,8 @@ export const TurtleGraphics = forwardRef { + if (!isEnableOperation) return; + setSelectedCharacter(undefined); setSelectedCell({ x, y }); }; diff --git a/src/problems/problemData.ts b/src/problems/problemData.ts index f755cc16..4399b128 100644 --- a/src/problems/problemData.ts +++ b/src/problems/problemData.ts @@ -36,7 +36,15 @@ export const courseIdToProgramIdLists: Record = { export function generateProgram(programId: ProgramId, languageId: LanguageId): string { // TODO(exKAZUu): 問題IDに紐づくプログラム(テンプレート)を取得して、乱数を使って具体的なプログラムを生成する。 - return programIdToLanguageIdToProgram[programId][languageId]; + return ( + `const character1 = new Character(); +character1.moveForward(); +character1.moveForward(); +character1.moveForward(); +character1.moveForward(); +character1.moveForward(); +` || programIdToLanguageIdToProgram[programId][languageId] + ); } export function getExplanation(programId: ProgramId, languageId: LanguageId): string { diff --git a/src/types.ts b/src/types.ts index fed17fbd..20cdeedb 100644 --- a/src/types.ts +++ b/src/types.ts @@ -31,3 +31,5 @@ export type SolveProblemResult = { }; export type CharacterDirection = 'up' | 'down' | 'left' | 'right'; + +export type ProblemType = 'normal' | 'checkpoint' | 'step'; diff --git a/tests/solveProblem.test.ts b/tests/solveProblem.test.ts index 9aff852d..d703ce95 100644 --- a/tests/solveProblem.test.ts +++ b/tests/solveProblem.test.ts @@ -118,9 +118,39 @@ test('Solve a problem (1character)', () => { [{ color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }], [{ color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }], ]); + + expect(answer.histories?.at(0)?.characters?.length).toEqual(0); + + expect(answer.histories?.at(1)?.characters?.length).toEqual(1); + expect(answer.histories?.at(1)?.characters?.[0]?.x).toEqual(1); + expect(answer.histories?.at(1)?.characters?.[0]?.y).toEqual(1); + expect(answer.histories?.at(-1)?.characters?.length).toEqual(1); expect(answer.histories?.at(-1)?.characters?.[0]?.x).toEqual(2); expect(answer.histories?.at(-1)?.characters?.[0]?.y).toEqual(3); + + // prettier-ignore + expect(answer.histories?.at(0)?.board?.grid).toEqual([ + [{ color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined },], + [{ color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined },], + [{ color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined },], + [{ color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined },], + [{ color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined },], + [{ color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined },], + [{ color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined },], + [{ color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined },], + ]); + // prettier-ignore + expect(answer.histories?.at(1)?.board?.grid).toEqual([ + [{ color: 'red' }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }], + [{ color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }], + [{ color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }], + [{ color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }], + [{ color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }], + [{ color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }], + [{ color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }], + [{ color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }], + ]); }); test('Solve a problem (multiple characters)', () => { From a6ee7aae410c4ddb686ddfdb62593df0ebe15393 Mon Sep 17 00:00:00 2001 From: Yuki Ito <37338201+ykit00@users.noreply.github.com> Date: Fri, 9 Feb 2024 21:29:57 +0900 Subject: [PATCH 05/12] feat: add create userSolvedProblem action --- src/app/lib/actions.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/app/lib/actions.ts diff --git a/src/app/lib/actions.ts b/src/app/lib/actions.ts new file mode 100644 index 00000000..176e2c08 --- /dev/null +++ b/src/app/lib/actions.ts @@ -0,0 +1,20 @@ +'use server'; +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +export async function createUserSolvedProblem( + userId: string, + courseId: string, + programId: string, + languageId: string +): Promise { + prisma.userSolvedProblem.create({ + data: { + userId, + courseId, + programId, + languageId, + }, + }); +} From 1bd05115fbe8c9045fd234cca9adbe2278cc5bad Mon Sep 17 00:00:00 2001 From: Yuki Ito <37338201+ykit00@users.noreply.github.com> Date: Tue, 13 Feb 2024 15:49:14 +0900 Subject: [PATCH 06/12] refactor: fix Addressability directory --- src/app/(withAuth)/courses/[courseId]/page.tsx | 2 +- .../programs/[programId]/CheckpointProblem.tsx | 8 ++++---- .../programs/[programId]/ExecutionResultProblem.tsx | 8 ++++---- .../[courseId]}/programs/[programId]/StepProblem.tsx | 6 +++--- .../[courseId]}/programs/[programId]/page.tsx | 6 +++--- 5 files changed, 15 insertions(+), 15 deletions(-) rename src/app/(withAuth)/{ => courses/[courseId]}/programs/[programId]/CheckpointProblem.tsx (91%) rename src/app/(withAuth)/{ => courses/[courseId]}/programs/[programId]/ExecutionResultProblem.tsx (83%) rename src/app/(withAuth)/{ => courses/[courseId]}/programs/[programId]/StepProblem.tsx (91%) rename src/app/(withAuth)/{ => courses/[courseId]}/programs/[programId]/page.tsx (91%) diff --git a/src/app/(withAuth)/courses/[courseId]/page.tsx b/src/app/(withAuth)/courses/[courseId]/page.tsx index f4971160..e8976ecd 100644 --- a/src/app/(withAuth)/courses/[courseId]/page.tsx +++ b/src/app/(withAuth)/courses/[courseId]/page.tsx @@ -71,7 +71,7 @@ const CoursePage: NextPage<{ params: { courseId: string } }> = ({ params }) => { {programIds.map((programId) => ( - + {programIdToName[programId]} diff --git a/src/app/(withAuth)/programs/[programId]/CheckpointProblem.tsx b/src/app/(withAuth)/courses/[courseId]/programs/[programId]/CheckpointProblem.tsx similarity index 91% rename from src/app/(withAuth)/programs/[programId]/CheckpointProblem.tsx rename to src/app/(withAuth)/courses/[courseId]/programs/[programId]/CheckpointProblem.tsx index 52e0b7a8..332e9001 100644 --- a/src/app/(withAuth)/programs/[programId]/CheckpointProblem.tsx +++ b/src/app/(withAuth)/courses/[courseId]/programs/[programId]/CheckpointProblem.tsx @@ -3,10 +3,10 @@ import { Box, Button, Flex, HStack, VStack } from '@chakra-ui/react'; import { useRef } from 'react'; -import { SyntaxHighlighter } from '../../../../components/organisms/SyntaxHighlighter'; -import type { TurtleGraphicsHandle } from '../../../../components/organisms/TurtleGraphics'; -import { TurtleGraphics } from '../../../../components/organisms/TurtleGraphics'; -import type { ProblemType } from '../../../../types'; +import { SyntaxHighlighter } from '../../../../../../components/organisms/SyntaxHighlighter'; +import type { TurtleGraphicsHandle } from '../../../../../../components/organisms/TurtleGraphics'; +import { TurtleGraphics } from '../../../../../../components/organisms/TurtleGraphics'; +import type { ProblemType } from '../../../../../../types'; interface CheckpointProblemProps { problemProgram: string; diff --git a/src/app/(withAuth)/programs/[programId]/ExecutionResultProblem.tsx b/src/app/(withAuth)/courses/[courseId]/programs/[programId]/ExecutionResultProblem.tsx similarity index 83% rename from src/app/(withAuth)/programs/[programId]/ExecutionResultProblem.tsx rename to src/app/(withAuth)/courses/[courseId]/programs/[programId]/ExecutionResultProblem.tsx index 45fe0166..fe7ac19d 100644 --- a/src/app/(withAuth)/programs/[programId]/ExecutionResultProblem.tsx +++ b/src/app/(withAuth)/courses/[courseId]/programs/[programId]/ExecutionResultProblem.tsx @@ -3,10 +3,10 @@ import { Box, Button, Flex, HStack, VStack } from '@chakra-ui/react'; import { useRef } from 'react'; -import { SyntaxHighlighter } from '../../../../components/organisms/SyntaxHighlighter'; -import type { TurtleGraphicsHandle } from '../../../../components/organisms/TurtleGraphics'; -import { TurtleGraphics } from '../../../../components/organisms/TurtleGraphics'; -import type { ProblemType } from '../../../../types'; +import { SyntaxHighlighter } from '../../../../../../components/organisms/SyntaxHighlighter'; +import type { TurtleGraphicsHandle } from '../../../../../../components/organisms/TurtleGraphics'; +import { TurtleGraphics } from '../../../../../../components/organisms/TurtleGraphics'; +import type { ProblemType } from '../../../../../../types'; interface ExecutionResultProblemProps { problemProgram: string; diff --git a/src/app/(withAuth)/programs/[programId]/StepProblem.tsx b/src/app/(withAuth)/courses/[courseId]/programs/[programId]/StepProblem.tsx similarity index 91% rename from src/app/(withAuth)/programs/[programId]/StepProblem.tsx rename to src/app/(withAuth)/courses/[courseId]/programs/[programId]/StepProblem.tsx index c061135e..c44b17b7 100644 --- a/src/app/(withAuth)/programs/[programId]/StepProblem.tsx +++ b/src/app/(withAuth)/courses/[courseId]/programs/[programId]/StepProblem.tsx @@ -3,9 +3,9 @@ import { Box, Button, Flex, HStack, VStack } from '@chakra-ui/react'; import { useRef } from 'react'; -import { SyntaxHighlighter } from '../../../../components/organisms/SyntaxHighlighter'; -import type { TurtleGraphicsHandle } from '../../../../components/organisms/TurtleGraphics'; -import { TurtleGraphics } from '../../../../components/organisms/TurtleGraphics'; +import { SyntaxHighlighter } from '../../../../../../components/organisms/SyntaxHighlighter'; +import type { TurtleGraphicsHandle } from '../../../../../../components/organisms/TurtleGraphics'; +import { TurtleGraphics } from '../../../../../../components/organisms/TurtleGraphics'; interface StepProblemProps { beforeCheckPointLine: number; diff --git a/src/app/(withAuth)/programs/[programId]/page.tsx b/src/app/(withAuth)/courses/[courseId]/programs/[programId]/page.tsx similarity index 91% rename from src/app/(withAuth)/programs/[programId]/page.tsx rename to src/app/(withAuth)/courses/[courseId]/programs/[programId]/page.tsx index e26f94f7..c22848df 100644 --- a/src/app/(withAuth)/programs/[programId]/page.tsx +++ b/src/app/(withAuth)/courses/[courseId]/programs/[programId]/page.tsx @@ -4,9 +4,9 @@ import { Heading, VStack } from '@chakra-ui/react'; import type { NextPage } from 'next'; import { useEffect, useState } from 'react'; -import { generateProgram, programIdToName } from '../../../../problems/problemData'; -import type { ProblemType } from '../../../../types'; -import { getLanguageIdFromSessionStorage } from '../../../lib/SessionStorage'; +import { generateProgram, programIdToName } from '../../../../../../problems/problemData'; +import type { ProblemType } from '../../../../../../types'; +import { getLanguageIdFromSessionStorage } from '../../../../../lib/SessionStorage'; import { CheckpointProblem } from './CheckpointProblem'; import { ExecutionResultProblem } from './ExecutionResultProblem'; From 7defeab5b2b23c6884a6fd0ac95a71cc066a160a Mon Sep 17 00:00:00 2001 From: Yuki Ito <37338201+ykit00@users.noreply.github.com> Date: Tue, 13 Feb 2024 15:55:56 +0900 Subject: [PATCH 07/12] feat: handle complete problem --- .../[programId]/ExecutionResultProblem.tsx | 3 +++ .../programs/[programId]/StepProblem.tsx | 3 +++ .../[courseId]/programs/[programId]/page.tsx | 16 ++++++++++++++- src/app/lib/actions.ts | 20 +++++++++++-------- 4 files changed, 33 insertions(+), 9 deletions(-) diff --git a/src/app/(withAuth)/courses/[courseId]/programs/[programId]/ExecutionResultProblem.tsx b/src/app/(withAuth)/courses/[courseId]/programs/[programId]/ExecutionResultProblem.tsx index fe7ac19d..9ee57985 100644 --- a/src/app/(withAuth)/courses/[courseId]/programs/[programId]/ExecutionResultProblem.tsx +++ b/src/app/(withAuth)/courses/[courseId]/programs/[programId]/ExecutionResultProblem.tsx @@ -12,9 +12,11 @@ interface ExecutionResultProblemProps { problemProgram: string; selectedLanguageId: string; setStep: (step: ProblemType) => void; + handleComplete: () => void; } export const ExecutionResultProblem: React.FC = ({ + handleComplete, problemProgram, selectedLanguageId, setStep, @@ -31,6 +33,7 @@ export const ExecutionResultProblem: React.FC = ({ // TODO: 一旦アラートで表示 if (isCorrect) { alert('正解です。この問題は終了です'); + handleComplete(); } else { alert('不正解です。チェックポイントごとに回答してください'); setStep('checkpoint'); diff --git a/src/app/(withAuth)/courses/[courseId]/programs/[programId]/StepProblem.tsx b/src/app/(withAuth)/courses/[courseId]/programs/[programId]/StepProblem.tsx index c44b17b7..00f8a9bd 100644 --- a/src/app/(withAuth)/courses/[courseId]/programs/[programId]/StepProblem.tsx +++ b/src/app/(withAuth)/courses/[courseId]/programs/[programId]/StepProblem.tsx @@ -14,11 +14,13 @@ interface StepProblemProps { selectedLanguageId: string; setBeforeCheckPointLine: (line: number) => void; setCurrentCheckPointLine: (line: number) => void; + handleComplete: () => void; } export const StepProblem: React.FC = ({ beforeCheckPointLine, currentCheckPointLine, + handleComplete, problemProgram, selectedLanguageId, setBeforeCheckPointLine, @@ -39,6 +41,7 @@ export const StepProblem: React.FC = ({ if (currentCheckPointLine === problemProgramLines) { alert('正解です。この問題は終了です'); + handleComplete(); } else { alert('正解です。次の行に進みます'); setBeforeCheckPointLine(currentCheckPointLine); diff --git a/src/app/(withAuth)/courses/[courseId]/programs/[programId]/page.tsx b/src/app/(withAuth)/courses/[courseId]/programs/[programId]/page.tsx index c22848df..fee3672f 100644 --- a/src/app/(withAuth)/courses/[courseId]/programs/[programId]/page.tsx +++ b/src/app/(withAuth)/courses/[courseId]/programs/[programId]/page.tsx @@ -3,16 +3,21 @@ import { Heading, VStack } from '@chakra-ui/react'; import type { NextPage } from 'next'; import { useEffect, useState } from 'react'; +import { useSessionContext } from 'supertokens-auth-react/recipe/session'; import { generateProgram, programIdToName } from '../../../../../../problems/problemData'; import type { ProblemType } from '../../../../../../types'; import { getLanguageIdFromSessionStorage } from '../../../../../lib/SessionStorage'; +import { createUserSolvedProblem } from '../../../../../lib/actions'; import { CheckpointProblem } from './CheckpointProblem'; import { ExecutionResultProblem } from './ExecutionResultProblem'; import { StepProblem } from './StepProblem'; -const ProblemPage: NextPage<{ params: { programId: string } }> = ({ params }) => { +const ProblemPage: NextPage<{ params: { courseId: string; programId: string } }> = ({ params }) => { + const session = useSessionContext(); + const userId = session.loading ? '' : session.userId; + const courseId = params.courseId; const programId = params.programId; // TODO: チェックポイントを取得する処理が実装できたら置き換える const checkPointLines = [1, 4]; @@ -31,11 +36,19 @@ const ProblemPage: NextPage<{ params: { programId: string } }> = ({ params }) => setProblemProgram(generateProgram(programId, selectedLanguageId)); }, [programId, selectedLanguageId]); + const handleSolveProblem = async (): Promise => { + console.log('handleSolveProblem'); + if (userId) { + await createUserSolvedProblem(userId, courseId, programId, selectedLanguageId); + } + }; + const ProblemComponent: React.FC = () => { switch (step) { case 'normal': { return ( = ({ params }) => { - prisma.userSolvedProblem.create({ - data: { - userId, - courseId, - programId, - languageId, - }, - }); + try { + await prisma.userSolvedProblem.create({ + data: { + userId, + courseId, + programId, + languageId, + }, + }); + } catch (error) { + console.error(error); + } } From 9cd9c3383a3022e73784a5949978eacac4d5214e Mon Sep 17 00:00:00 2001 From: Yuki Ito <37338201+ykit00@users.noreply.github.com> Date: Tue, 13 Feb 2024 19:32:33 +0900 Subject: [PATCH 08/12] style: display progress --- .../(withAuth)/courses/[courseId]/page.tsx | 44 ++++++++++++++----- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/src/app/(withAuth)/courses/[courseId]/page.tsx b/src/app/(withAuth)/courses/[courseId]/page.tsx index e8976ecd..7333cc20 100644 --- a/src/app/(withAuth)/courses/[courseId]/page.tsx +++ b/src/app/(withAuth)/courses/[courseId]/page.tsx @@ -3,8 +3,6 @@ import { Box, Heading, - OrderedList, - ListItem, VStack, Accordion, AccordionItem, @@ -12,6 +10,13 @@ import { AccordionIcon, AccordionPanel, Select, + Table, + TableContainer, + Thead, + Tbody, + Tr, + Td, + Th, } from '@chakra-ui/react'; import type { NextPage } from 'next'; import NextLink from 'next/link'; @@ -28,6 +33,8 @@ import { getLanguageIdFromSessionStorage, setLanguageIdToSessionStorage } from ' const CoursePage: NextPage<{ params: { courseId: string } }> = ({ params }) => { const [selectedLanguageId, setSelectedLanguageId] = useState(''); + const SPECIFIED_COMPLETION_COUNT = 2; + useEffect(() => { setSelectedLanguageId(getLanguageIdFromSessionStorage()); }, []); @@ -68,15 +75,30 @@ const CoursePage: NextPage<{ params: { courseId: string } }> = ({ params }) => { - - {programIds.map((programId) => ( - - - {programIdToName[programId]} - - - ))} - + + + + + + + + + + {programIds.map((programId) => ( + + + + + ))} + +
プログラム進捗
+ + {programIdToName[programId]} + + +

completedProblemCount / {SPECIFIED_COMPLETION_COUNT}

+
+
From 46651a2d32344249f63c1a2f50bb21e86f94a233 Mon Sep 17 00:00:00 2001 From: Yuki Ito <37338201+ykit00@users.noreply.github.com> Date: Tue, 13 Feb 2024 20:43:48 +0900 Subject: [PATCH 09/12] feat: display progress and complete status --- public/crown.png | Bin 0 -> 3740 bytes .../(withAuth)/courses/[courseId]/page.tsx | 40 ++++++++++++++++-- src/app/lib/actions.ts | 22 ++++++++++ 3 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 public/crown.png diff --git a/public/crown.png b/public/crown.png new file mode 100644 index 0000000000000000000000000000000000000000..efa99119e2d1bef20cb90591f81ae527671e2182 GIT binary patch literal 3740 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 literal 0 HcmV?d00001 diff --git a/src/app/(withAuth)/courses/[courseId]/page.tsx b/src/app/(withAuth)/courses/[courseId]/page.tsx index 7333cc20..7abe6381 100644 --- a/src/app/(withAuth)/courses/[courseId]/page.tsx +++ b/src/app/(withAuth)/courses/[courseId]/page.tsx @@ -17,10 +17,13 @@ import { Tr, Td, Th, + Flex, } from '@chakra-ui/react'; import type { NextPage } from 'next'; +import Image from 'next/image'; import NextLink from 'next/link'; import React, { useEffect, useState } from 'react'; +import { useSessionContext } from 'supertokens-auth-react/recipe/session'; import { courseIdToProgramIdLists, @@ -30,11 +33,18 @@ import { } from '../../../../problems/problemData'; import { getLanguageIdFromSessionStorage, setLanguageIdToSessionStorage } from '../../../lib/SessionStorage'; +import { getUserSolvedProblems } from '@/src/app/lib/actions'; + const CoursePage: NextPage<{ params: { courseId: string } }> = ({ params }) => { const [selectedLanguageId, setSelectedLanguageId] = useState(''); + const [userSolvedProblems, setUserSolvedProblems] = useState>([]); const SPECIFIED_COMPLETION_COUNT = 2; + const session = useSessionContext(); + const userId = session.loading ? '' : session.userId; + const courseId = params.courseId; + useEffect(() => { setSelectedLanguageId(getLanguageIdFromSessionStorage()); }, []); @@ -43,6 +53,15 @@ const CoursePage: NextPage<{ params: { courseId: string } }> = ({ params }) => { const inputValue = event.target.value; setLanguageIdToSessionStorage(inputValue); setSelectedLanguageId(inputValue); + getUserSolvedProblems(userId, courseId).then((data) => { + setUserSolvedProblems(data); + }); + }; + + const countUserSolvedProblems = (programId: string, languageId: string): number => { + return userSolvedProblems.filter( + (userSolvedProblem) => userSolvedProblem.programId === programId && userSolvedProblem.languageId === languageId + ).length; }; return ( @@ -79,8 +98,12 @@ const CoursePage: NextPage<{ params: { courseId: string } }> = ({ params }) => { - - + + @@ -92,7 +115,18 @@ const CoursePage: NextPage<{ params: { courseId: string } }> = ({ params }) => { ))} diff --git a/src/app/lib/actions.ts b/src/app/lib/actions.ts index a437c5c9..efe1e3e3 100644 --- a/src/app/lib/actions.ts +++ b/src/app/lib/actions.ts @@ -22,3 +22,25 @@ export async function createUserSolvedProblem( console.error(error); } } + +export async function getUserSolvedProblems( + userId: string, + courseId: string +): Promise> { + try { + const userSolvedProblems = await prisma.userSolvedProblem.findMany({ + where: { + userId, + courseId, + }, + select: { + programId: true, + languageId: true, + }, + }); + return userSolvedProblems; + } catch (error) { + console.error(error); + return []; + } +} From 51579c0ffb01d21c6be513b44be178ee7989b53e Mon Sep 17 00:00:00 2001 From: Yuki Ito <37338201+ykit00@users.noreply.github.com> Date: Tue, 13 Feb 2024 20:48:12 +0900 Subject: [PATCH 10/12] style: fix import and tidy up --- src/app/(withAuth)/courses/[courseId]/page.tsx | 3 +-- .../courses/[courseId]/programs/[programId]/page.tsx | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/app/(withAuth)/courses/[courseId]/page.tsx b/src/app/(withAuth)/courses/[courseId]/page.tsx index 7abe6381..cd0cf0e0 100644 --- a/src/app/(withAuth)/courses/[courseId]/page.tsx +++ b/src/app/(withAuth)/courses/[courseId]/page.tsx @@ -32,8 +32,7 @@ import { programIdToName, } from '../../../../problems/problemData'; import { getLanguageIdFromSessionStorage, setLanguageIdToSessionStorage } from '../../../lib/SessionStorage'; - -import { getUserSolvedProblems } from '@/src/app/lib/actions'; +import { getUserSolvedProblems } from '../../../lib/actions'; const CoursePage: NextPage<{ params: { courseId: string } }> = ({ params }) => { const [selectedLanguageId, setSelectedLanguageId] = useState(''); diff --git a/src/app/(withAuth)/courses/[courseId]/programs/[programId]/page.tsx b/src/app/(withAuth)/courses/[courseId]/programs/[programId]/page.tsx index fee3672f..488ca42c 100644 --- a/src/app/(withAuth)/courses/[courseId]/programs/[programId]/page.tsx +++ b/src/app/(withAuth)/courses/[courseId]/programs/[programId]/page.tsx @@ -37,7 +37,6 @@ const ProblemPage: NextPage<{ params: { courseId: string; programId: string } }> }, [programId, selectedLanguageId]); const handleSolveProblem = async (): Promise => { - console.log('handleSolveProblem'); if (userId) { await createUserSolvedProblem(userId, courseId, programId, selectedLanguageId); } From 7496165ab00f42a8aa5cd026defb193998f9abf9 Mon Sep 17 00:00:00 2001 From: Yuki Ito <37338201+ykit00@users.noreply.github.com> Date: Wed, 14 Feb 2024 14:52:12 +0900 Subject: [PATCH 11/12] refactor: renaming function --- src/app/(withAuth)/courses/[courseId]/page.tsx | 4 ++-- src/app/lib/actions.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/(withAuth)/courses/[courseId]/page.tsx b/src/app/(withAuth)/courses/[courseId]/page.tsx index cd0cf0e0..5d169912 100644 --- a/src/app/(withAuth)/courses/[courseId]/page.tsx +++ b/src/app/(withAuth)/courses/[courseId]/page.tsx @@ -32,7 +32,7 @@ import { programIdToName, } from '../../../../problems/problemData'; import { getLanguageIdFromSessionStorage, setLanguageIdToSessionStorage } from '../../../lib/SessionStorage'; -import { getUserSolvedProblems } from '../../../lib/actions'; +import { fetchUserSolvedProblems } from '../../../lib/actions'; const CoursePage: NextPage<{ params: { courseId: string } }> = ({ params }) => { const [selectedLanguageId, setSelectedLanguageId] = useState(''); @@ -52,7 +52,7 @@ const CoursePage: NextPage<{ params: { courseId: string } }> = ({ params }) => { const inputValue = event.target.value; setLanguageIdToSessionStorage(inputValue); setSelectedLanguageId(inputValue); - getUserSolvedProblems(userId, courseId).then((data) => { + fetchUserSolvedProblems(userId, courseId).then((data) => { setUserSolvedProblems(data); }); }; diff --git a/src/app/lib/actions.ts b/src/app/lib/actions.ts index efe1e3e3..cd66fc95 100644 --- a/src/app/lib/actions.ts +++ b/src/app/lib/actions.ts @@ -23,7 +23,7 @@ export async function createUserSolvedProblem( } } -export async function getUserSolvedProblems( +export async function fetchUserSolvedProblems( userId: string, courseId: string ): Promise> { From 4b9d50388cef7389c4d106c7f95b51a8590a3029 Mon Sep 17 00:00:00 2001 From: Yuki Ito <37338201+ykit00@users.noreply.github.com> Date: Wed, 14 Feb 2024 16:54:07 +0900 Subject: [PATCH 12/12] refactor: use server component and Improved performance --- .../(withAuth)/courses/[courseId]/Course.tsx | 135 ++++++++++++++++ .../(withAuth)/courses/[courseId]/page.tsx | 153 +++--------------- 2 files changed, 154 insertions(+), 134 deletions(-) create mode 100644 src/app/(withAuth)/courses/[courseId]/Course.tsx diff --git a/src/app/(withAuth)/courses/[courseId]/Course.tsx b/src/app/(withAuth)/courses/[courseId]/Course.tsx new file mode 100644 index 00000000..7c9d0023 --- /dev/null +++ b/src/app/(withAuth)/courses/[courseId]/Course.tsx @@ -0,0 +1,135 @@ +'use client'; + +import { + Box, + Heading, + VStack, + Accordion, + AccordionItem, + AccordionButton, + AccordionIcon, + AccordionPanel, + Select, + Table, + TableContainer, + Thead, + Tbody, + Tr, + Td, + Th, + Flex, +} from '@chakra-ui/react'; +import Image from 'next/image'; +import NextLink from 'next/link'; +import React, { useEffect, useState } from 'react'; + +import { + courseIdToProgramIdLists, + languageIdToName, + languageIds, + programIdToName, +} from '../../../../problems/problemData'; +import { getLanguageIdFromSessionStorage, setLanguageIdToSessionStorage } from '../../../lib/SessionStorage'; + +export const Course: React.FC<{ + courseId: string; + userSolvedProblems: { programId: string; languageId: string }[]; +}> = ({ courseId, userSolvedProblems }) => { + const [selectedLanguageId, setSelectedLanguageId] = useState(''); + + const SPECIFIED_COMPLETION_COUNT = 2; + + useEffect(() => { + setSelectedLanguageId(getLanguageIdFromSessionStorage()); + }, []); + + const handleSelectLanguage = (event: React.ChangeEvent): void => { + const inputValue = event.target.value; + setLanguageIdToSessionStorage(inputValue); + setSelectedLanguageId(inputValue); + }; + + const countUserSolvedProblems = (programId: string, languageId: string): number => { + return userSolvedProblems.filter( + (userSolvedProblem) => userSolvedProblem.programId === programId && userSolvedProblem.languageId === languageId + ).length; + }; + + return ( +
+ + Lessons + + + + {courseIdToProgramIdLists[courseId].map((programIds, iLesson) => ( + + + + + + 第{iLesson + 1}回 + + + + + +
プログラム進捗 + プログラム + + 進捗 +
-

completedProblemCount / {SPECIFIED_COMPLETION_COUNT}

+ +

+ {countUserSolvedProblems(programId, selectedLanguageId)} /{' '} + {SPECIFIED_COMPLETION_COUNT} +

+ {countUserSolvedProblems(programId, selectedLanguageId) >= + SPECIFIED_COMPLETION_COUNT && ( + + 完了の王冠 + + )} +
+ + + + + + + + {programIds.map((programId) => ( + + + + + ))} + +
+ プログラム + + 進捗 +
+ + {programIdToName[programId]} + + + +

+ {countUserSolvedProblems(programId, selectedLanguageId)} /{' '} + {SPECIFIED_COMPLETION_COUNT} +

+ {countUserSolvedProblems(programId, selectedLanguageId) >= + SPECIFIED_COMPLETION_COUNT && ( + + 完了の王冠 + + )} +
+
+ + + + + + ))} + + + ); +}; diff --git a/src/app/(withAuth)/courses/[courseId]/page.tsx b/src/app/(withAuth)/courses/[courseId]/page.tsx index 5d169912..0c31682e 100644 --- a/src/app/(withAuth)/courses/[courseId]/page.tsx +++ b/src/app/(withAuth)/courses/[courseId]/page.tsx @@ -1,145 +1,30 @@ -'use client'; - -import { - Box, - Heading, - VStack, - Accordion, - AccordionItem, - AccordionButton, - AccordionIcon, - AccordionPanel, - Select, - Table, - TableContainer, - Thead, - Tbody, - Tr, - Td, - Th, - Flex, -} from '@chakra-ui/react'; import type { NextPage } from 'next'; -import Image from 'next/image'; -import NextLink from 'next/link'; -import React, { useEffect, useState } from 'react'; -import { useSessionContext } from 'supertokens-auth-react/recipe/session'; +import { redirect } from 'next/navigation'; -import { - courseIdToProgramIdLists, - languageIdToName, - languageIds, - programIdToName, -} from '../../../../problems/problemData'; -import { getLanguageIdFromSessionStorage, setLanguageIdToSessionStorage } from '../../../lib/SessionStorage'; +import { prisma } from '../../../../infrastructures/prisma'; +import { getNullableSessionOnServer } from '../../../../utils/session'; import { fetchUserSolvedProblems } from '../../../lib/actions'; -const CoursePage: NextPage<{ params: { courseId: string } }> = ({ params }) => { - const [selectedLanguageId, setSelectedLanguageId] = useState(''); - const [userSolvedProblems, setUserSolvedProblems] = useState>([]); - - const SPECIFIED_COMPLETION_COUNT = 2; +import { Course } from './Course'; - const session = useSessionContext(); - const userId = session.loading ? '' : session.userId; - const courseId = params.courseId; +const CoursePage: NextPage<{ params: { courseId: string } }> = async ({ params }) => { + const { session } = await getNullableSessionOnServer(); + const user = + session && + (await prisma.user.findUnique({ + where: { + id: session.getUserId(), + }, + })); - useEffect(() => { - setSelectedLanguageId(getLanguageIdFromSessionStorage()); - }, []); + if (!user) { + return redirect('/auth'); + } - const handleSelectLanguage = (event: React.ChangeEvent): void => { - const inputValue = event.target.value; - setLanguageIdToSessionStorage(inputValue); - setSelectedLanguageId(inputValue); - fetchUserSolvedProblems(userId, courseId).then((data) => { - setUserSolvedProblems(data); - }); - }; - - const countUserSolvedProblems = (programId: string, languageId: string): number => { - return userSolvedProblems.filter( - (userSolvedProblem) => userSolvedProblem.programId === programId && userSolvedProblem.languageId === languageId - ).length; - }; + const courseId = params.courseId; + const userSolvedProblems = await fetchUserSolvedProblems(user.id, courseId); - return ( -
- - Lessons - - - - {courseIdToProgramIdLists[params.courseId].map((programIds, iLesson) => ( - - - - - - 第{iLesson + 1}回 - - - - - - - - - - - - - - {programIds.map((programId) => ( - - - - - ))} - -
- プログラム - - 進捗 -
- - {programIdToName[programId]} - - - -

- {countUserSolvedProblems(programId, selectedLanguageId)} /{' '} - {SPECIFIED_COMPLETION_COUNT} -

- {countUserSolvedProblems(programId, selectedLanguageId) >= - SPECIFIED_COMPLETION_COUNT && ( - - 完了の王冠 - - )} -
-
-
-
-
-
-
- ))} -
-
- ); + return ; }; export default CoursePage;