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 3ebb8699..8ed807e7 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -13,9 +13,22 @@ datasource db { // ----------------------------------------------------------------------------- model User { - id String @id + id String @id 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 } diff --git a/public/crown.png b/public/crown.png new file mode 100644 index 00000000..efa99119 Binary files /dev/null and b/public/crown.png differ 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}回 + + + + + + + + + + + + + + {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 f4971160..0c31682e 100644 --- a/src/app/(withAuth)/courses/[courseId]/page.tsx +++ b/src/app/(withAuth)/courses/[courseId]/page.tsx @@ -1,90 +1,30 @@ -'use client'; - -import { - Box, - Heading, - OrderedList, - ListItem, - VStack, - Accordion, - AccordionItem, - AccordionButton, - AccordionIcon, - AccordionPanel, - Select, -} from '@chakra-ui/react'; import type { NextPage } from 'next'; -import NextLink from 'next/link'; -import React, { useEffect, useState } from 'react'; +import { redirect } from 'next/navigation'; + +import { prisma } from '../../../../infrastructures/prisma'; +import { getNullableSessionOnServer } from '../../../../utils/session'; +import { fetchUserSolvedProblems } from '../../../lib/actions'; -import { - courseIdToProgramIdLists, - languageIdToName, - languageIds, - programIdToName, -} from '../../../../problems/problemData'; -import { getLanguageIdFromSessionStorage, setLanguageIdToSessionStorage } from '../../../lib/SessionStorage'; +import { Course } from './Course'; -const CoursePage: NextPage<{ params: { courseId: string } }> = ({ params }) => { - const [selectedLanguageId, setSelectedLanguageId] = useState(''); +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); - }; + 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]} - - - ))} - - - - - - ))} - -
- ); + return ; }; export default CoursePage; 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 81% rename from src/app/(withAuth)/programs/[programId]/ExecutionResultProblem.tsx rename to src/app/(withAuth)/courses/[courseId]/programs/[programId]/ExecutionResultProblem.tsx index 45fe0166..9ee57985 100644 --- a/src/app/(withAuth)/programs/[programId]/ExecutionResultProblem.tsx +++ b/src/app/(withAuth)/courses/[courseId]/programs/[programId]/ExecutionResultProblem.tsx @@ -3,18 +3,20 @@ 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; 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)/programs/[programId]/StepProblem.tsx b/src/app/(withAuth)/courses/[courseId]/programs/[programId]/StepProblem.tsx similarity index 89% rename from src/app/(withAuth)/programs/[programId]/StepProblem.tsx rename to src/app/(withAuth)/courses/[courseId]/programs/[programId]/StepProblem.tsx index c061135e..00f8a9bd 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; @@ -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)/programs/[programId]/page.tsx b/src/app/(withAuth)/courses/[courseId]/programs/[programId]/page.tsx similarity index 74% rename from src/app/(withAuth)/programs/[programId]/page.tsx rename to src/app/(withAuth)/courses/[courseId]/programs/[programId]/page.tsx index e26f94f7..488ca42c 100644 --- a/src/app/(withAuth)/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 { 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,18 @@ const ProblemPage: NextPage<{ params: { programId: string } }> = ({ params }) => setProblemProgram(generateProgram(programId, selectedLanguageId)); }, [programId, selectedLanguageId]); + const handleSolveProblem = async (): Promise => { + if (userId) { + await createUserSolvedProblem(userId, courseId, programId, selectedLanguageId); + } + }; + const ProblemComponent: React.FC = () => { switch (step) { case 'normal': { return ( = ({ params }) => { + try { + await prisma.userSolvedProblem.create({ + data: { + userId, + courseId, + programId, + languageId, + }, + }); + } catch (error) { + console.error(error); + } +} + +export async function fetchUserSolvedProblems( + 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 []; + } +}