Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add problem page #16

Merged
merged 18 commits into from
Jan 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading