Skip to content

Commit

Permalink
feat: Add problem page (#16)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tatehito authored Jan 22, 2024
1 parent 7910a27 commit c04f5a1
Show file tree
Hide file tree
Showing 10 changed files with 443 additions and 48 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"react": "18.2.0",
"react-dom": "18.2.0",
"react-icons": "5.0.1",
"react-syntax-highlighter": "15.5.0",
"supertokens-auth-react": "0.36.1",
"supertokens-node": "16.7.1",
"supertokens-web-js": "0.8.0",
Expand All @@ -59,6 +60,7 @@
"@types/micromatch": "4.0.6",
"@types/node": "20.11.0",
"@types/react-dom": "18.2.18",
"@types/react-syntax-highlighter": "^15",
"@typescript-eslint/eslint-plugin": "6.18.1",
"@typescript-eslint/parser": "6.18.1",
"@willbooster/eslint-config-next": "1.1.0",
Expand Down
Binary file added public/character.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 6 additions & 1 deletion src/app/(withAuth)/courses/[courseId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
AccordionPanel,
} from '@chakra-ui/react';
import type { NextPage } from 'next';
import NextLink from 'next/link';

import { courseIdToProgramIdLists, programIdToName } from '../../../../problems/problemData';

Expand All @@ -32,7 +33,11 @@ const CoursePage: NextPage<{ params: { courseId: string } }> = async ({ params }
<AccordionPanel pb={4}>
<OrderedList>
{programIds.map((programId) => (
<ListItem key={programId}>{programIdToName[programId]}</ListItem>
<ListItem key={programId}>
<NextLink passHref href={`/problems/${programId}`}>
{programIdToName[programId]}
</NextLink>
</ListItem>
))}
</OrderedList>
</AccordionPanel>
Expand Down
49 changes: 49 additions & 0 deletions src/app/(withAuth)/problems/[problemId]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
'use client';

import { Box, Button, Flex, HStack, Heading, VStack } from '@chakra-ui/react';
import type { NextPage } from 'next';

import { SyntaxHighlighter } from '../../../../components/organisms/SyntaxHighlighter';
import { TurtleGraphics } from '../../../../components/organisms/TurtleGraphics';
import { programIdToName, generateProgram, getDescription } from '../../../../problems/problemData';

const GRID_COLUMNS = 12;
const GRID_ROWS = 8;
const GRID_SIZE = 40;

const ProblemPage: NextPage<{ params: { problemId: string } }> = ({ params }) => {
// TODO: 一旦Java固定 言語選択機能実装時に変更する
const programmingLanguageId = 'java';

return (
<main>
<VStack spacing="4">
<Heading as="h1">{programIdToName[params.problemId]}</Heading>
<Flex gap="6" w="100%">
<VStack spacing="10" w={GRID_COLUMNS * GRID_SIZE}>
<Box>{getDescription(params.problemId)}</Box>
<Box>
<TurtleGraphics characters={[]} gridColumns={GRID_COLUMNS} gridRows={GRID_ROWS} gridSize={GRID_SIZE} />
</Box>
</VStack>
<VStack align="end" minW="50%" overflow="hidden">
<Button colorScheme="gray">解説</Button>
{/* 画面に収まる高さに設定 */}
<Box h="calc(100vh - 370px)" w="100%">
<SyntaxHighlighter
code={generateProgram(params.problemId, programmingLanguageId)}
programmingLanguageId={programmingLanguageId}
/>
</Box>
<HStack>
<Button colorScheme="gray">リセット</Button>
<Button colorScheme="gray">解答</Button>
</HStack>
</VStack>
</Flex>
</VStack>
</main>
);
};

export default ProblemPage;
33 changes: 32 additions & 1 deletion src/app/lib/Character.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
export const CharacterColor = {
Red: 'red',
Blue: 'blue',
Green: 'green',
Yellow: 'yellow',
Purple: 'purple',
};
type Color = (typeof CharacterColor)[keyof typeof CharacterColor];

export class Character {
id: number;
name: string;
x: number;
y: number;
direction: string;
color: string;
color: Color;
penDown: boolean;
path: string[];

Expand Down Expand Up @@ -136,11 +145,33 @@ export class Character {
}
}

setColor(color: Color): void {
this.color = color;
}

putPen(): void {
this.penDown = true;
}

upPen(): void {
this.penDown = false;
}

rotateCss(): string {
switch (this.direction) {
case 'up': {
return 'rotate(180deg)';
}
case 'down': {
return 'rotate(0deg)';
}
case 'left': {
return 'rotate(90deg)';
}
case 'right': {
return 'rotate(270deg)';
}
}
return '';
}
}
21 changes: 21 additions & 0 deletions src/app/lib/TurtleGraphicsCell.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { CharacterColor } from './Character';

type Color = (typeof CharacterColor)[keyof typeof CharacterColor];

export class TurtleGraphicsCell {
id: number;
x: number;
y: number;
backgroundColor: string;

constructor(id: number, x: number, y: number, backgroundColor: string) {
this.id = id;
this.x = x;
this.y = y;
this.backgroundColor = backgroundColor;
}

setBackgroundColor(color: Color): void {
this.backgroundColor = color;
}
}
47 changes: 47 additions & 0 deletions src/components/organisms/SyntaxHighlighter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Box } from '@chakra-ui/react';
import React from 'react';
import { Prism } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/cjs/styles/prism';

interface SyntaxHighlighterProps {
code: string;
programmingLanguageId: string;
}

export const SyntaxHighlighter: React.FC<SyntaxHighlighterProps> = ({
code: code,
programmingLanguageId: programmingLanguageId,
}) => {
return (
<Box h="100%">
<Prism
codeTagProps={{ style: { fontSize: '1rem' } }}
customStyle={{
backgroundColor: '#011627',
marginTop: 0,
overflow: 'auto',
padding: 10,
height: '100%',
}}
language={programmingLanguageId === 'c' ? 'cpp' : programmingLanguageId}
lineNumberStyle={{ paddingRight: 0, marginRight: 16, minWidth: '1rem' }}
// lineProps={(lineNumber) => {
lineProps={() => {
const style = {
padding: 0,
};
// TODO: チェックポイント問題・ステップ問題のハイライト
// if (isStepPage && problem && lineNumber === problem.traceList[highlightedLineCount].row + 1) {
// style.backgroundColor = '#744210';
// }
return { style };
}}
showLineNumbers={true}
style={vscDarkPlus}
wrapLines={true}
>
{code ?? ''}
</Prism>
</Box>
);
};
94 changes: 48 additions & 46 deletions src/components/organisms/TurtleGraphics.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
'use client';

import { Box, Grid, GridItem } from '@chakra-ui/react';
import React, { useState, useEffect } from 'react';
import Image from 'next/image';
import React, { useState } from 'react';

import { Character } from '../../app/lib/Character';
import { TurtleGraphicsCell } from '../../app/lib/TurtleGraphicsCell';

// 原点(左上隅)の座標
const ORIGIN_X = 1;
Expand All @@ -14,34 +16,24 @@ interface TurtleGraphicsProps {
gridColumns?: number;
gridRows?: number;
gridSize?: number;
isEnableOperation?: boolean;
}

export const TurtleGraphics: React.FC<TurtleGraphicsProps> = ({
characters: initialCharacters,
gridColumns: gridColumns = 12,
gridRows: gridRows = 8,
gridSize: gridSize = 40,
isEnableOperation: isEnableOperation = false,
}) => {
const [characters, setCharacters] = useState(initialCharacters);

useEffect(() => {
// 軌跡の描画
for (const character of characters) {
if (!character.penDown) continue;

for (const position of character.path) {
const [x, y] = position.split(',').map(Number);
const gridCell = document.querySelectorAll('.grid-cell')[
x - ORIGIN_X + (y - ORIGIN_Y) * gridColumns
] as HTMLElement;

if (!gridCell) return;

gridCell.style.backgroundColor = character.color;
gridCell.style.opacity = '0.5';
}
}
}, [characters, gridColumns]);
const [cells] = useState<TurtleGraphicsCell[]>(
Array.from({ length: gridColumns * gridRows }).map((_, index) => {
const x = (index % gridColumns) + ORIGIN_X;
const y = Math.floor(index / gridColumns) + ORIGIN_Y;
return new TurtleGraphicsCell(index, x, y, '');
})
);

const updateCharacter = (character: Character, updater: (char: Character) => void): void => {
setCharacters((prevCharacters) =>
Expand Down Expand Up @@ -108,8 +100,14 @@ export const TurtleGraphics: React.FC<TurtleGraphicsProps> = ({
templateColumns={`repeat(${gridColumns}, ${gridSize}px)`}
templateRows={`repeat(${gridRows}, ${gridSize}px)`}
>
{Array.from({ length: gridColumns * gridRows }).map((_, index) => (
<GridItem key={index} borderColor="black" borderWidth="1px" className="grid-cell" />
{cells.map((cell) => (
<GridItem
key={cell.id}
backgroundColor={cell.backgroundColor}
borderColor="black"
borderWidth="1px"
className="grid-cell"
/>
))}
{characters.map((character) => (
<Box
Expand All @@ -121,34 +119,38 @@ export const TurtleGraphics: React.FC<TurtleGraphicsProps> = ({
top={(character.y - ORIGIN_Y) * gridSize + 'px'}
w={gridSize + 'px'}
>
{character.name}
<Box transform={character.rotateCss()}>
<Image alt={character.name} height={gridSize} src="/character.png" width={gridSize} />
</Box>
<Box position="absolute">{character.name}</Box>
</Box>
))}
</Grid>
{characters.map((character) => (
<div key={character.id}>
<div>{character.name}</div>
<div>
<button onClick={() => handleMoveForward(character)}>Move Forword</button>
</div>
<div>
<button onClick={() => handleMoveBack(character)}>Move Back</button>
{isEnableOperation &&
characters.map((character) => (
<div key={character.id}>
<div>{character.name}</div>
<div>
<button onClick={() => handleMoveForward(character)}>Move Forword</button>
</div>
<div>
<button onClick={() => handleMoveBack(character)}>Move Back</button>
</div>
<div>
<button onClick={() => handleTurnleft(character)}>Turn Left</button>
</div>
<div>
<button onClick={() => handleTurnRight(character)}>Turn Right</button>
</div>
<div>
<button onClick={() => handlePutPen(character)}>Put Pen</button>
</div>
<div>
<button onClick={() => handleUpPen(character)}>Up Pen</button>
</div>
<div> --- </div>
</div>
<div>
<button onClick={() => handleTurnleft(character)}>Turn Left</button>
</div>
<div>
<button onClick={() => handleTurnRight(character)}>Turn Right</button>
</div>
<div>
<button onClick={() => handlePutPen(character)}>Put Pen</button>
</div>
<div>
<button onClick={() => handleUpPen(character)}>Up Pen</button>
</div>
<div> --- </div>
</div>
))}
))}
</div>
);
};
13 changes: 13 additions & 0 deletions src/problems/problemData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ turtle.moveForward(5);` || programIdToLanguageIdToProgram[programId][languageId]
);
}

export function getDescription(programId: ProgramId): string {
return programIdToLanguageIdToDescription[programId];
}

export function getExplanation(programId: ProgramId, languageId: LanguageId): string {
return programIdToLanguageIdToExplanation[programId][languageId];
}
Expand Down Expand Up @@ -76,6 +80,15 @@ public class Curve {
},
};

export const programIdToLanguageIdToDescription: Record<ProgramId, string> = {
straight: `
直線を描くプログラムです。
`.trim(),
curve: `
曲線を描くプログラムです。
`.trim(),
};

export const programIdToLanguageIdToExplanation: Record<ProgramId, Record<LanguageId, string>> = {
straight: {
js: `
Expand Down
Loading

0 comments on commit c04f5a1

Please sign in to comment.