Skip to content

Commit

Permalink
feat: progress on problems (#36)
Browse files Browse the repository at this point in the history
* style: run prisma format

* feat: Add UserSolvedProblem table and relation to User model

* fix: Programming language name (#31)

* feat: Add checkpoint problem (#30)

* feat: add create userSolvedProblem action

* refactor: fix Addressability directory

* feat: handle complete problem

* style: display progress

* feat: display progress and complete status

* style: fix import and tidy up

* refactor: renaming function

* refactor: use server component and Improved performance

---------

Co-authored-by: tatehito <[email protected]>
  • Loading branch information
ykit00 and Tatehito authored Feb 14, 2024
1 parent 304f2be commit 913072e
Show file tree
Hide file tree
Showing 10 changed files with 262 additions and 98 deletions.
Original file line number Diff line number Diff line change
@@ -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
);
17 changes: 15 additions & 2 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Binary file added public/crown.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
135 changes: 135 additions & 0 deletions src/app/(withAuth)/courses/[courseId]/Course.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLSelectElement>): 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 (
<main>
<Heading as="h1" marginBottom="4">
Lessons
</Heading>
<Select
marginBottom="4"
maxW="300"
placeholder="Select language"
value={selectedLanguageId}
onChange={(e) => handleSelectLanguage(e)}
>
{languageIds.map((languageId) => (
<option key={languageId} value={languageId}>
{languageIdToName[languageId]}
</option>
))}
</Select>
<VStack align="stretch">
{courseIdToProgramIdLists[courseId].map((programIds, iLesson) => (
<Box key={iLesson}>
<Accordion allowToggle>
<AccordionItem>
<AccordionButton>
<Box flex="1" textAlign="left">
{iLesson + 1}
</Box>
<AccordionIcon />
</AccordionButton>
<AccordionPanel pb={4}>
<TableContainer>
<Table>
<Thead>
<Tr>
<Th textAlign="left" width="50%">
プログラム
</Th>
<Th align="left" width="50%">
進捗
</Th>
</Tr>
</Thead>
<Tbody>
{programIds.map((programId) => (
<Tr key={programId}>
<Td>
<NextLink passHref href={`${courseId}/programs/${programId}`}>
{programIdToName[programId]}
</NextLink>
</Td>
<Td>
<Flex>
<p>
{countUserSolvedProblems(programId, selectedLanguageId)} /{' '}
{SPECIFIED_COMPLETION_COUNT}
</p>
{countUserSolvedProblems(programId, selectedLanguageId) >=
SPECIFIED_COMPLETION_COUNT && (
<Box h={4} ml={2} position={'relative'} w={4}>
<Image fill alt="完了の王冠" src="/crown.png" />
</Box>
)}
</Flex>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</AccordionPanel>
</AccordionItem>
</Accordion>
</Box>
))}
</VStack>
</main>
);
};
102 changes: 21 additions & 81 deletions src/app/(withAuth)/courses/[courseId]/page.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLSelectElement>): void => {
const inputValue = event.target.value;
setLanguageIdToSessionStorage(inputValue);
setSelectedLanguageId(inputValue);
};
const courseId = params.courseId;
const userSolvedProblems = await fetchUserSolvedProblems(user.id, courseId);

return (
<main>
<Heading as="h1" marginBottom="4">
Lessons
</Heading>
<Select
marginBottom="4"
maxW="300"
placeholder="Select language"
value={selectedLanguageId}
onChange={(e) => handleSelectLanguage(e)}
>
{languageIds.map((languageId) => (
<option key={languageId} value={languageId}>
{languageIdToName[languageId]}
</option>
))}
</Select>
<VStack align="stretch">
{courseIdToProgramIdLists[params.courseId].map((programIds, iLesson) => (
<Box key={iLesson}>
<Accordion allowToggle>
<AccordionItem>
<AccordionButton>
<Box flex="1" textAlign="left">
{iLesson + 1}
</Box>
<AccordionIcon />
</AccordionButton>
<AccordionPanel pb={4}>
<OrderedList>
{programIds.map((programId) => (
<ListItem key={programId}>
<NextLink passHref href={`/programs/${programId}`}>
{programIdToName[programId]}
</NextLink>
</ListItem>
))}
</OrderedList>
</AccordionPanel>
</AccordionItem>
</Accordion>
</Box>
))}
</VStack>
</main>
);
return <Course courseId={params.courseId} userSolvedProblems={userSolvedProblems} />;
};

export default CoursePage;
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ExecutionResultProblemProps> = ({
handleComplete,
problemProgram,
selectedLanguageId,
setStep,
Expand All @@ -31,6 +33,7 @@ export const ExecutionResultProblem: React.FC<ExecutionResultProblemProps> = ({
// TODO: 一旦アラートで表示
if (isCorrect) {
alert('正解です。この問題は終了です');
handleComplete();
} else {
alert('不正解です。チェックポイントごとに回答してください');
setStep('checkpoint');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -14,11 +14,13 @@ interface StepProblemProps {
selectedLanguageId: string;
setBeforeCheckPointLine: (line: number) => void;
setCurrentCheckPointLine: (line: number) => void;
handleComplete: () => void;
}

export const StepProblem: React.FC<StepProblemProps> = ({
beforeCheckPointLine,
currentCheckPointLine,
handleComplete,
problemProgram,
selectedLanguageId,
setBeforeCheckPointLine,
Expand All @@ -39,6 +41,7 @@ export const StepProblem: React.FC<StepProblemProps> = ({

if (currentCheckPointLine === problemProgramLines) {
alert('正解です。この問題は終了です');
handleComplete();
} else {
alert('正解です。次の行に進みます');
setBeforeCheckPointLine(currentCheckPointLine);
Expand Down
Loading

0 comments on commit 913072e

Please sign in to comment.