aboutsummaryrefslogtreecommitdiffhomepage
path: root/frontend
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2024-08-12 05:54:49 +0900
committernsfisis <nsfisis@gmail.com>2024-08-12 05:54:49 +0900
commit3074f8d74330a2c238040755b758230d682a4bc4 (patch)
tree3c45267ee25aa78be1ad4c31e0c09894e656b506 /frontend
parent7527e54bba0c528015ce402bfa4534c1ab6ca1da (diff)
parentb37d6f213c2f3b19631e5067f39a7106859faaed (diff)
downloadiosdc-japan-2024-albatross-3074f8d74330a2c238040755b758230d682a4bc4.tar.gz
iosdc-japan-2024-albatross-3074f8d74330a2c238040755b758230d682a4bc4.tar.zst
iosdc-japan-2024-albatross-3074f8d74330a2c238040755b758230d682a4bc4.zip
Merge branch 'feat/play-page'
Diffstat (limited to 'frontend')
-rw-r--r--frontend/app/.server/api/schema.d.ts30
-rw-r--r--frontend/app/components/ExecStatusIndicatorIcon.tsx8
-rw-r--r--frontend/app/components/GolfPlayApp.client.tsx99
-rw-r--r--frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx117
-rw-r--r--frontend/app/components/GolfWatchApp.client.tsx104
-rw-r--r--frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx140
-rw-r--r--frontend/app/components/SubmitStatusLabel.tsx26
-rw-r--r--frontend/app/models/ExecResult.ts18
-rw-r--r--frontend/app/models/PlayerInfo.ts9
-rw-r--r--frontend/app/models/SubmitResult.ts16
-rw-r--r--frontend/app/routes/_index.tsx2
-rw-r--r--frontend/app/routes/dashboard.tsx4
-rw-r--r--frontend/app/routes/golf.$gameId.play.tsx7
-rw-r--r--frontend/app/tailwind.css6
14 files changed, 376 insertions, 210 deletions
diff --git a/frontend/app/.server/api/schema.d.ts b/frontend/app/.server/api/schema.d.ts
index 2d116b9..7fd612e 100644
--- a/frontend/app/.server/api/schema.d.ts
+++ b/frontend/app/.server/api/schema.d.ts
@@ -113,9 +113,9 @@ export interface components {
started_at?: number;
problem: components["schemas"]["Problem"];
players: components["schemas"]["User"][];
- verification_steps: components["schemas"]["VerificationStep"][];
+ exec_steps: components["schemas"]["ExecStep"][];
};
- VerificationStep: {
+ ExecStep: {
/** @example 1 */
testcase_id: number | null;
/** @example Test case 1 */
@@ -130,7 +130,7 @@ export interface components {
description: string;
};
GamePlayerMessage: components["schemas"]["GamePlayerMessageS2C"] | components["schemas"]["GamePlayerMessageC2S"];
- GamePlayerMessageS2C: components["schemas"]["GamePlayerMessageS2CStart"] | components["schemas"]["GamePlayerMessageS2CExecResult"];
+ GamePlayerMessageS2C: components["schemas"]["GamePlayerMessageS2CStart"] | components["schemas"]["GamePlayerMessageS2CExecResult"] | components["schemas"]["GamePlayerMessageS2CSubmitResult"];
GamePlayerMessageS2CStart: {
/** @constant */
type: "player:s2c:start";
@@ -146,11 +146,29 @@ export interface components {
data: components["schemas"]["GamePlayerMessageS2CExecResultPayload"];
};
GamePlayerMessageS2CExecResultPayload: {
+ /** @example 1 */
+ testcase_id: number | null;
+ /**
+ * @example success
+ * @enum {string}
+ */
+ status: "success" | "wrong_answer" | "timeout" | "runtime_error" | "internal_error" | "compile_error";
+ /** @example Hello, world! */
+ stdout: string;
+ /** @example */
+ stderr: string;
+ };
+ GamePlayerMessageS2CSubmitResult: {
+ /** @constant */
+ type: "player:s2c:submitresult";
+ data: components["schemas"]["GamePlayerMessageS2CSubmitResultPayload"];
+ };
+ GamePlayerMessageS2CSubmitResultPayload: {
/**
* @example success
* @enum {string}
*/
- status: "success" | "failure" | "timeout" | "internal_error" | "compile_error" | "wrong_answer";
+ status: "success" | "wrong_answer" | "timeout" | "runtime_error" | "internal_error" | "compile_error";
/** @example 100 */
score: number | null;
};
@@ -203,8 +221,6 @@ export interface components {
GameWatcherMessageS2CSubmitPayload: {
/** @example 1 */
player_id: number;
- /** @example 100 */
- preliminary_score: number;
};
GameWatcherMessageS2CExecResult: {
/** @constant */
@@ -239,6 +255,8 @@ export interface components {
* @enum {string}
*/
status: "success" | "wrong_answer" | "timeout" | "runtime_error" | "internal_error" | "compile_error";
+ /** @example 100 */
+ score: number | null;
};
};
responses: {
diff --git a/frontend/app/components/ExecStatusIndicatorIcon.tsx b/frontend/app/components/ExecStatusIndicatorIcon.tsx
index a76e957..5277bfa 100644
--- a/frontend/app/components/ExecStatusIndicatorIcon.tsx
+++ b/frontend/app/components/ExecStatusIndicatorIcon.tsx
@@ -1,17 +1,23 @@
import {
faBan,
+ faCircle,
faCircleCheck,
faCircleExclamation,
faRotate,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import type { ExecResultStatus } from "../models/ExecResult";
type Props = {
- status: string;
+ status: ExecResultStatus;
};
export default function ExecStatusIndicatorIcon({ status }: Props) {
switch (status) {
+ case "waiting_submission":
+ return (
+ <FontAwesomeIcon icon={faCircle} fixedWidth className="text-gray-400" />
+ );
case "running":
return (
<FontAwesomeIcon
diff --git a/frontend/app/components/GolfPlayApp.client.tsx b/frontend/app/components/GolfPlayApp.client.tsx
index 4aebd52..d527e07 100644
--- a/frontend/app/components/GolfPlayApp.client.tsx
+++ b/frontend/app/components/GolfPlayApp.client.tsx
@@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
import { useDebouncedCallback } from "use-debounce";
import type { components } from "../.server/api/schema";
import useWebSocket, { ReadyState } from "../hooks/useWebSocket";
+import type { PlayerInfo } from "../models/PlayerInfo";
import GolfPlayAppConnecting from "./GolfPlayApps/GolfPlayAppConnecting";
import GolfPlayAppFinished from "./GolfPlayApps/GolfPlayAppFinished";
import GolfPlayAppGaming from "./GolfPlayApps/GolfPlayAppGaming";
@@ -12,14 +13,17 @@ type GamePlayerMessageS2C = components["schemas"]["GamePlayerMessageS2C"];
type GamePlayerMessageC2S = components["schemas"]["GamePlayerMessageC2S"];
type Game = components["schemas"]["Game"];
+type User = components["schemas"]["User"];
type GameState = "connecting" | "waiting" | "starting" | "gaming" | "finished";
export default function GolfPlayApp({
game,
+ player,
sockToken,
}: {
game: Game;
+ player: User;
sockToken: string;
}) {
const socketUrl =
@@ -39,16 +43,17 @@ export default function GolfPlayApp({
const [leftTimeSeconds, setLeftTimeSeconds] = useState<number | null>(null);
useEffect(() => {
- if (gameState === "starting" && startedAt !== null) {
+ if (
+ (gameState === "starting" || gameState === "gaming") &&
+ startedAt !== null
+ ) {
const timer1 = setInterval(() => {
setLeftTimeSeconds((prev) => {
if (prev === null) {
return null;
}
if (prev <= 1) {
- clearInterval(timer1);
setGameState("gaming");
- return 0;
}
return prev - 1;
});
@@ -70,9 +75,21 @@ export default function GolfPlayApp({
}
}, [gameState, startedAt, game.duration_seconds]);
- const [currentScore, setCurrentScore] = useState<number | null>(null);
-
- const [lastExecStatus, setLastExecStatus] = useState<string | null>(null);
+ const [playerInfo, setPlayerInfo] = useState<Omit<PlayerInfo, "code">>({
+ displayName: player.display_name,
+ iconPath: player.icon_path ?? null,
+ score: null,
+ submitResult: {
+ status: "waiting_submission",
+ execResults: game.exec_steps.map((r) => ({
+ testcase_id: r.testcase_id,
+ status: "waiting_submission",
+ label: r.label,
+ stdout: "",
+ stderr: "",
+ })),
+ },
+ });
const onCodeChange = useDebouncedCallback((code: string) => {
console.log("player:c2s:code");
@@ -83,11 +100,26 @@ export default function GolfPlayApp({
}, 1000);
const onCodeSubmit = useDebouncedCallback((code: string) => {
+ if (code === "") {
+ return;
+ }
console.log("player:c2s:submit");
sendJsonMessage({
type: "player:c2s:submit",
data: { code },
});
+ setPlayerInfo((prev) => ({
+ ...prev,
+ submitResult: {
+ status: "running",
+ execResults: prev.submitResult.execResults.map((r) => ({
+ ...r,
+ status: "running",
+ stdout: "",
+ stderr: "",
+ })),
+ },
+ }));
}, 1000);
if (readyState === ReadyState.UNINSTANTIATED) {
@@ -117,14 +149,46 @@ export default function GolfPlayApp({
setGameState("starting");
}
} else if (lastJsonMessage.type === "player:s2c:execresult") {
+ const { testcase_id, status, stdout, stderr } = lastJsonMessage.data;
+ setPlayerInfo((prev) => {
+ const ret = { ...prev };
+ ret.submitResult = {
+ ...prev.submitResult,
+ execResults: prev.submitResult.execResults.map((r) =>
+ r.testcase_id === testcase_id && r.status === "running"
+ ? {
+ ...r,
+ status,
+ stdout,
+ stderr,
+ }
+ : r,
+ ),
+ };
+ return ret;
+ });
+ } else if (lastJsonMessage.type === "player:s2c:submitresult") {
const { status, score } = lastJsonMessage.data;
- if (
- score !== null &&
- (currentScore === null || score < currentScore)
- ) {
- setCurrentScore(score);
- }
- setLastExecStatus(status);
+ setPlayerInfo((prev) => {
+ const ret = { ...prev };
+ ret.submitResult = {
+ ...prev.submitResult,
+ status,
+ };
+ if (status === "success") {
+ if (score) {
+ if (ret.score === null || score < ret.score) {
+ ret.score = score;
+ }
+ }
+ } else {
+ ret.submitResult.execResults = prev.submitResult.execResults.map(
+ (r) =>
+ r.status === "running" ? { ...r, status: "canceled" } : r,
+ );
+ }
+ return ret;
+ });
}
} else {
if (game.started_at) {
@@ -133,7 +197,7 @@ export default function GolfPlayApp({
// The game has already started.
if (gameState !== "gaming" && gameState !== "finished") {
setStartedAt(game.started_at);
- setLeftTimeSeconds(0);
+ setLeftTimeSeconds(game.started_at - nowSec);
setGameState("gaming");
}
} else {
@@ -159,7 +223,6 @@ export default function GolfPlayApp({
lastJsonMessage,
readyState,
gameState,
- currentScore,
]);
if (gameState === "connecting") {
@@ -171,12 +234,14 @@ export default function GolfPlayApp({
} else if (gameState === "gaming") {
return (
<GolfPlayAppGaming
+ gameDisplayName={game.display_name}
+ gameDurationSeconds={game.duration_seconds}
+ leftTimeSeconds={leftTimeSeconds!}
+ playerInfo={playerInfo}
problemTitle={game.problem.title}
problemDescription={game.problem.description}
onCodeChange={onCodeChange}
onCodeSubmit={onCodeSubmit}
- currentScore={currentScore}
- lastExecStatus={lastExecStatus}
/>
);
} else if (gameState === "finished") {
diff --git a/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx b/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx
index 31927a5..4730583 100644
--- a/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx
+++ b/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx
@@ -1,21 +1,33 @@
+import { faArrowDown } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { Link } from "@remix-run/react";
import React, { useRef } from "react";
+import SubmitButton from "../../components/SubmitButton";
+import type { PlayerInfo } from "../../models/PlayerInfo";
+import BorderedContainer from "../BorderedContainer";
+import ExecStatusIndicatorIcon from "../ExecStatusIndicatorIcon";
+import SubmitStatusLabel from "../SubmitStatusLabel";
type Props = {
+ gameDisplayName: string;
+ gameDurationSeconds: number;
+ leftTimeSeconds: number;
+ playerInfo: Omit<PlayerInfo, "code">;
problemTitle: string;
problemDescription: string;
onCodeChange: (code: string) => void;
onCodeSubmit: (code: string) => void;
- currentScore: number | null;
- lastExecStatus: string | null;
};
export default function GolfPlayAppGaming({
+ gameDisplayName,
+ gameDurationSeconds,
+ leftTimeSeconds,
+ playerInfo,
problemTitle,
problemDescription,
onCodeChange,
onCodeSubmit,
- currentScore,
- lastExecStatus,
}: Props) {
const textareaRef = useRef<HTMLTextAreaElement>(null);
@@ -29,36 +41,81 @@ export default function GolfPlayAppGaming({
}
};
+ const leftTime = (() => {
+ const k = gameDurationSeconds + leftTimeSeconds;
+ const m = Math.floor(k / 60);
+ const s = k % 60;
+ return `${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`;
+ })();
+
return (
- <div className="min-h-screen flex">
- <div className="mx-auto flex min-h-full flex-grow">
- <div className="flex w-1/2 flex-col justify-between p-4">
- <div>
- <div className="mb-2 text-xl font-bold">{problemTitle}</div>
- <div className="text-gray-700">{problemDescription}</div>
- </div>
- <div className="mb-4 mt-auto">
- <div className="mb-2">
- <div className="font-semibold text-green-500">
- Score: {currentScore ?? "-"} ({lastExecStatus ?? "-"})
- </div>
- </div>
- <button
- onClick={handleSubmitButtonClick}
- className="focus:shadow-outline rounded bg-blue-500 px-4 py-2 font-bold text-white hover:bg-blue-700 focus:outline-none"
- >
- Submit
- </button>
+ <div className="min-h-screen bg-gray-100 flex flex-col">
+ <div className="text-white bg-iosdc-japan flex flex-row justify-between px-4 py-2">
+ <div className="font-bold">
+ <div className="text-gray-100">{gameDisplayName}</div>
+ <div className="text-2xl">{leftTime}</div>
+ </div>
+ <div className="font-bold text-end">
+ <Link to={"/dashboard"} className="text-gray-100">
+ {playerInfo.displayName}
+ </Link>
+ <div className="text-2xl">{playerInfo.score}</div>
+ </div>
+ </div>
+ <div className="grow grid grid-cols-3 divide-x divide-gray-300">
+ <div className="p-4">
+ <div className="mb-2 text-xl font-bold">{problemTitle}</div>
+ <div className="p-2">
+ <BorderedContainer>
+ <div className="text-gray-700">{problemDescription}</div>
+ </BorderedContainer>
</div>
</div>
- <div className="w-1/2 p-4 flex">
- <div className="flex-grow">
- <textarea
- ref={textareaRef}
- onChange={handleTextChange}
- className="h-full w-full rounded-lg border border-gray-300 p-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
- ></textarea>
+ <div className="p-4">
+ <textarea
+ ref={textareaRef}
+ onChange={handleTextChange}
+ className="resize-none h-full w-full rounded-lg border border-gray-300 p-2 focus:outline-none focus:ring-2 focus:ring-gray-400 transition duration-300"
+ ></textarea>
+ </div>
+ <div className="p-4 flex flex-col gap-4">
+ <div className="flex">
+ <SubmitButton onClick={handleSubmitButtonClick}>提出</SubmitButton>
+ <div className="grow font-bold text-xl text-center m-1">
+ <SubmitStatusLabel status={playerInfo.submitResult.status} />
+ </div>
</div>
+ <ul className="flex flex-col gap-2">
+ {playerInfo.submitResult.execResults.map((r, idx) => (
+ <li key={r.testcase_id ?? -1} className="flex gap-2">
+ <div className="flex flex-col gap-2 p-2">
+ <div className="w-6">
+ <ExecStatusIndicatorIcon status={r.status} />
+ </div>
+ {idx !== playerInfo.submitResult.execResults.length - 1 && (
+ <div>
+ <FontAwesomeIcon
+ icon={faArrowDown}
+ fixedWidth
+ className="text-gray-500"
+ />
+ </div>
+ )}
+ </div>
+ <div className="grow p-2 overflow-x-scroll">
+ <BorderedContainer>
+ <div className="font-semibold">{r.label}</div>
+ <div>
+ <code>
+ {r.stdout}
+ {r.stderr}
+ </code>
+ </div>
+ </BorderedContainer>
+ </div>
+ </li>
+ ))}
+ </ul>
</div>
</div>
</div>
diff --git a/frontend/app/components/GolfWatchApp.client.tsx b/frontend/app/components/GolfWatchApp.client.tsx
index 448a966..b2f3b69 100644
--- a/frontend/app/components/GolfWatchApp.client.tsx
+++ b/frontend/app/components/GolfWatchApp.client.tsx
@@ -1,11 +1,10 @@
import { useEffect, useState } from "react";
import type { components } from "../.server/api/schema";
import useWebSocket, { ReadyState } from "../hooks/useWebSocket";
+import type { PlayerInfo } from "../models/PlayerInfo";
import GolfWatchAppConnecting from "./GolfWatchApps/GolfWatchAppConnecting";
import GolfWatchAppFinished from "./GolfWatchApps/GolfWatchAppFinished";
-import GolfWatchAppGaming, {
- PlayerInfo,
-} from "./GolfWatchApps/GolfWatchAppGaming";
+import GolfWatchAppGaming from "./GolfWatchApps/GolfWatchAppGaming";
import GolfWatchAppStarting from "./GolfWatchApps/GolfWatchAppStarting";
import GolfWatchAppWaiting from "./GolfWatchApps/GolfWatchAppWaiting";
@@ -40,16 +39,17 @@ export default function GolfWatchApp({
const [leftTimeSeconds, setLeftTimeSeconds] = useState<number | null>(null);
useEffect(() => {
- if (gameState === "starting" && startedAt !== null) {
+ if (
+ (gameState === "starting" || gameState === "gaming") &&
+ startedAt !== null
+ ) {
const timer1 = setInterval(() => {
setLeftTimeSeconds((prev) => {
if (prev === null) {
return null;
}
if (prev <= 1) {
- clearInterval(timer1);
setGameState("gaming");
- return 0;
}
return prev - 1;
});
@@ -79,14 +79,32 @@ export default function GolfWatchApp({
iconPath: playerA?.icon_path ?? null,
score: null,
code: "",
- submissionResult: undefined,
+ submitResult: {
+ status: "waiting_submission",
+ execResults: game.exec_steps.map((r) => ({
+ testcase_id: r.testcase_id,
+ status: "waiting_submission",
+ label: r.label,
+ stdout: "",
+ stderr: "",
+ })),
+ },
});
const [playerInfoB, setPlayerInfoB] = useState<PlayerInfo>({
displayName: playerB?.display_name ?? null,
iconPath: playerB?.icon_path ?? null,
score: null,
code: "",
- submissionResult: undefined,
+ submitResult: {
+ status: "waiting_submission",
+ execResults: game.exec_steps.map((r) => ({
+ testcase_id: r.testcase_id,
+ status: "waiting_submission",
+ label: r.label,
+ stdout: "",
+ stderr: "",
+ })),
+ },
});
if (readyState === ReadyState.UNINSTANTIATED) {
@@ -121,18 +139,16 @@ export default function GolfWatchApp({
player_id === playerA?.user_id ? setPlayerInfoA : setPlayerInfoB;
setter((prev) => ({ ...prev, code }));
} else if (lastJsonMessage.type === "watcher:s2c:submit") {
- const { player_id, preliminary_score } = lastJsonMessage.data;
+ const { player_id } = lastJsonMessage.data;
const setter =
player_id === playerA?.user_id ? setPlayerInfoA : setPlayerInfoB;
setter((prev) => ({
...prev,
- submissionResult: {
+ submitResult: {
status: "running",
- preliminaryScore: preliminary_score,
- verificationResults: game.verification_steps.map((v) => ({
- testcase_id: v.testcase_id,
+ execResults: prev.submitResult.execResults.map((r) => ({
+ ...r,
status: "running",
- label: v.label,
stdout: "",
stderr: "",
})),
@@ -145,50 +161,42 @@ export default function GolfWatchApp({
player_id === playerA?.user_id ? setPlayerInfoA : setPlayerInfoB;
setter((prev) => {
const ret = { ...prev };
- if (ret.submissionResult === undefined) {
- return ret;
- }
- ret.submissionResult = {
- ...ret.submissionResult,
- verificationResults: ret.submissionResult.verificationResults.map(
- (v) =>
- v.testcase_id === testcase_id && v.status === "running"
- ? {
- ...v,
- status,
- stdout,
- stderr,
- }
- : v,
+ ret.submitResult = {
+ ...prev.submitResult,
+ execResults: prev.submitResult.execResults.map((r) =>
+ r.testcase_id === testcase_id && r.status === "running"
+ ? {
+ ...r,
+ status,
+ stdout,
+ stderr,
+ }
+ : r,
),
};
return ret;
});
} else if (lastJsonMessage.type === "watcher:s2c:submitresult") {
- const { player_id, status } = lastJsonMessage.data;
+ const { player_id, status, score } = lastJsonMessage.data;
const setter =
player_id === playerA?.user_id ? setPlayerInfoA : setPlayerInfoB;
setter((prev) => {
const ret = { ...prev };
- if (ret.submissionResult === undefined) {
- return ret;
- }
- ret.submissionResult = {
- ...ret.submissionResult,
+ ret.submitResult = {
+ ...prev.submitResult,
status,
};
if (status === "success") {
- if (
- ret.score === null ||
- ret.submissionResult.preliminaryScore < ret.score
- ) {
- ret.score = ret.submissionResult.preliminaryScore;
+ if (score) {
+ if (ret.score === null || score < ret.score) {
+ ret.score = score;
+ }
}
} else {
- ret.submissionResult.verificationResults =
- ret.submissionResult.verificationResults.map((v) =>
- v.status === "running" ? { ...v, status: "canceled" } : v,
- );
+ ret.submitResult.execResults = prev.submitResult.execResults.map(
+ (r) =>
+ r.status === "running" ? { ...r, status: "canceled" } : r,
+ );
}
return ret;
});
@@ -200,7 +208,7 @@ export default function GolfWatchApp({
// The game has already started.
if (gameState !== "gaming" && gameState !== "finished") {
setStartedAt(game.started_at);
- setLeftTimeSeconds(0);
+ setLeftTimeSeconds(game.started_at - nowSec);
setGameState("gaming");
}
} else {
@@ -221,7 +229,6 @@ export default function GolfWatchApp({
}
}
}, [
- game.verification_steps,
game.started_at,
lastJsonMessage,
readyState,
@@ -239,10 +246,11 @@ export default function GolfWatchApp({
} else if (gameState === "gaming") {
return (
<GolfWatchAppGaming
- problem={game.problem!.description}
+ gameDurationSeconds={game.duration_seconds}
+ leftTimeSeconds={leftTimeSeconds!}
playerInfoA={playerInfoA}
playerInfoB={playerInfoB}
- leftTimeSeconds={leftTimeSeconds!}
+ problem={game.problem!.description}
/>
);
} else if (gameState === "finished") {
diff --git a/frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx b/frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx
index 65cd35e..f9647b3 100644
--- a/frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx
+++ b/frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx
@@ -1,83 +1,29 @@
+import { PlayerInfo } from "../../models/PlayerInfo";
import ExecStatusIndicatorIcon from "../ExecStatusIndicatorIcon";
+import SubmitStatusLabel from "../SubmitStatusLabel";
type Props = {
- problem: string;
+ gameDurationSeconds: number;
+ leftTimeSeconds: number;
playerInfoA: PlayerInfo;
playerInfoB: PlayerInfo;
- leftTimeSeconds: number;
-};
-
-export type PlayerInfo = {
- displayName: string | null;
- iconPath: string | null;
- score: number | null;
- code: string | null;
- submissionResult?: SubmissionResult;
-};
-
-type SubmissionResult = {
- status:
- | "running"
- | "success"
- | "wrong_answer"
- | "timeout"
- | "compile_error"
- | "runtime_error"
- | "internal_error";
- preliminaryScore: number;
- verificationResults: VerificationResult[];
-};
-
-type VerificationResult = {
- testcase_id: number | null;
- status:
- | "running"
- | "success"
- | "wrong_answer"
- | "timeout"
- | "compile_error"
- | "runtime_error"
- | "internal_error"
- | "canceled";
- label: string;
- stdout: string;
- stderr: string;
+ problem: string;
};
-function submissionResultStatusToLabel(
- status: SubmissionResult["status"] | null,
-) {
- switch (status) {
- case null:
- return "-";
- case "running":
- return "Running...";
- case "success":
- return "Accepted";
- case "wrong_answer":
- return "Wrong Answer";
- case "timeout":
- return "Time Limit Exceeded";
- case "compile_error":
- return "Compile Error";
- case "runtime_error":
- return "Runtime Error";
- case "internal_error":
- return "Internal Error";
- }
-}
-
export default function GolfWatchAppGaming({
- problem,
+ gameDurationSeconds,
+ leftTimeSeconds,
playerInfoA,
playerInfoB,
- leftTimeSeconds,
+ problem,
}: Props) {
const leftTime = (() => {
- const m = Math.floor(leftTimeSeconds / 60);
- const s = leftTimeSeconds % 60;
+ const k = gameDurationSeconds + leftTimeSeconds;
+ const m = Math.floor(k / 60);
+ const s = k % 60;
return `${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`;
})();
+
const scoreRatio = (() => {
const scoreA = playerInfoA.score ?? 0;
const scoreB = playerInfoB.score ?? 0;
@@ -118,29 +64,24 @@ export default function GolfWatchAppGaming({
</div>
<div>
<div>
- {submissionResultStatusToLabel(
- playerInfoA.submissionResult?.status ?? null,
- )}{" "}
- ({playerInfoA.submissionResult?.preliminaryScore})
+ <SubmitStatusLabel status={playerInfoA.submitResult.status} />
</div>
<div>
<ol>
- {playerInfoA.submissionResult?.verificationResults.map(
- (result) => (
- <li key={result.testcase_id ?? -1}>
+ {playerInfoA.submitResult?.execResults.map((result) => (
+ <li key={result.testcase_id ?? -1}>
+ <div>
+ <div>
+ <ExecStatusIndicatorIcon status={result.status} />{" "}
+ {result.label}
+ </div>
<div>
- <div>
- <ExecStatusIndicatorIcon status={result.status} />{" "}
- {result.label}
- </div>
- <div>
- {result.stdout}
- {result.stderr}
- </div>
+ {result.stdout}
+ {result.stderr}
</div>
- </li>
- ),
- )}
+ </div>
+ </li>
+ ))}
</ol>
</div>
</div>
@@ -151,29 +92,24 @@ export default function GolfWatchAppGaming({
</div>
<div>
<div>
- {submissionResultStatusToLabel(
- playerInfoB.submissionResult?.status ?? null,
- )}{" "}
- ({playerInfoB.submissionResult?.preliminaryScore ?? "-"})
+ <SubmitStatusLabel status={playerInfoB.submitResult.status} />
</div>
<div>
<ol>
- {playerInfoB.submissionResult?.verificationResults.map(
- (result, idx) => (
- <li key={idx}>
+ {playerInfoB.submitResult?.execResults.map((result, idx) => (
+ <li key={idx}>
+ <div>
+ <div>
+ <ExecStatusIndicatorIcon status={result.status} />{" "}
+ {result.label}
+ </div>
<div>
- <div>
- <ExecStatusIndicatorIcon status={result.status} />{" "}
- {result.label}
- </div>
- <div>
- {result.stdout}
- {result.stderr}
- </div>
+ {result.stdout}
+ {result.stderr}
</div>
- </li>
- ),
- )}
+ </div>
+ </li>
+ ))}
</ol>
</div>
</div>
diff --git a/frontend/app/components/SubmitStatusLabel.tsx b/frontend/app/components/SubmitStatusLabel.tsx
new file mode 100644
index 0000000..e0ecc27
--- /dev/null
+++ b/frontend/app/components/SubmitStatusLabel.tsx
@@ -0,0 +1,26 @@
+import type { SubmitResultStatus } from "../models/SubmitResult";
+
+type Props = {
+ status: SubmitResultStatus;
+};
+
+export default function SubmitStatusLabel({ status }: Props) {
+ switch (status) {
+ case "waiting_submission":
+ return null;
+ case "running":
+ return "実行中...";
+ case "success":
+ return "成功";
+ case "wrong_answer":
+ return "テスト失敗";
+ case "timeout":
+ return "時間切れ";
+ case "compile_error":
+ return "コンパイルエラー";
+ case "runtime_error":
+ return "実行時エラー";
+ case "internal_error":
+ return "!内部エラー!";
+ }
+}
diff --git a/frontend/app/models/ExecResult.ts b/frontend/app/models/ExecResult.ts
new file mode 100644
index 0000000..e0b6bb4
--- /dev/null
+++ b/frontend/app/models/ExecResult.ts
@@ -0,0 +1,18 @@
+export type ExecResultStatus =
+ | "waiting_submission"
+ | "running"
+ | "success"
+ | "wrong_answer"
+ | "timeout"
+ | "compile_error"
+ | "runtime_error"
+ | "internal_error"
+ | "canceled";
+
+export type ExecResult = {
+ testcase_id: number | null;
+ status: ExecResultStatus;
+ label: string;
+ stdout: string;
+ stderr: string;
+};
diff --git a/frontend/app/models/PlayerInfo.ts b/frontend/app/models/PlayerInfo.ts
new file mode 100644
index 0000000..8092ab3
--- /dev/null
+++ b/frontend/app/models/PlayerInfo.ts
@@ -0,0 +1,9 @@
+import type { SubmitResult } from "./SubmitResult";
+
+export type PlayerInfo = {
+ displayName: string | null;
+ iconPath: string | null;
+ score: number | null;
+ code: string | null;
+ submitResult: SubmitResult;
+};
diff --git a/frontend/app/models/SubmitResult.ts b/frontend/app/models/SubmitResult.ts
new file mode 100644
index 0000000..6df00b6
--- /dev/null
+++ b/frontend/app/models/SubmitResult.ts
@@ -0,0 +1,16 @@
+import type { ExecResult } from "./ExecResult";
+
+export type SubmitResultStatus =
+ | "waiting_submission"
+ | "running"
+ | "success"
+ | "wrong_answer"
+ | "timeout"
+ | "compile_error"
+ | "runtime_error"
+ | "internal_error";
+
+export type SubmitResult = {
+ status: SubmitResultStatus;
+ execResults: ExecResult[];
+};
diff --git a/frontend/app/routes/_index.tsx b/frontend/app/routes/_index.tsx
index 9be594f..24fff9f 100644
--- a/frontend/app/routes/_index.tsx
+++ b/frontend/app/routes/_index.tsx
@@ -22,7 +22,7 @@ export default function Index() {
className="w-24 h-24"
/>
<div className="text-center">
- <div className="font-bold text-transparent bg-clip-text bg-gradient-to-r from-orange-400 via-pink-500 to-purple-400 flex flex-col gap-y-2">
+ <div className="font-bold text-transparent bg-clip-text bg-iosdc-japan flex flex-col gap-y-2">
<div className="text-3xl">iOSDC Japan 2024</div>
<div className="text-6xl">Swift Code Battle</div>
</div>
diff --git a/frontend/app/routes/dashboard.tsx b/frontend/app/routes/dashboard.tsx
index 4c751d9..99c64f2 100644
--- a/frontend/app/routes/dashboard.tsx
+++ b/frontend/app/routes/dashboard.tsx
@@ -31,8 +31,8 @@ export default function Dashboard() {
return (
<div className="p-6 bg-gray-100 min-h-screen flex flex-col items-center">
- <h1 className="text-2xl font-bold mb-4 text-gray-800">
- <span>{user.display_name}</span>
+ <h1 className="text-2xl font-bold mb-4">
+ <span className="text-gray-800">{user.display_name}</span>
<span className="text-gray-500 ml-2">@{user.username}</span>
</h1>
<h2 className="text-xl font-semibold mb-4 text-gray-700">試合</h2>
diff --git a/frontend/app/routes/golf.$gameId.play.tsx b/frontend/app/routes/golf.$gameId.play.tsx
index 3712df2..ea1b8fd 100644
--- a/frontend/app/routes/golf.$gameId.play.tsx
+++ b/frontend/app/routes/golf.$gameId.play.tsx
@@ -15,7 +15,7 @@ export const meta: MetaFunction<typeof loader> = ({ data }) => [
];
export async function loader({ params, request }: LoaderFunctionArgs) {
- const { token } = await ensureUserLoggedIn(request);
+ const { token, user } = await ensureUserLoggedIn(request);
const fetchGame = async () => {
return (await apiGetGame(token, Number(params.gameId))).game;
@@ -27,16 +27,17 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
const [game, sockToken] = await Promise.all([fetchGame(), fetchSockToken()]);
return {
game,
+ player: user,
sockToken,
};
}
export default function GolfPlay() {
- const { game, sockToken } = useLoaderData<typeof loader>();
+ const { game, player, sockToken } = useLoaderData<typeof loader>();
return (
<ClientOnly fallback={<GolfPlayAppConnecting />}>
- {() => <GolfPlayApp game={game} sockToken={sockToken} />}
+ {() => <GolfPlayApp game={game} player={player} sockToken={sockToken} />}
</ClientOnly>
);
}
diff --git a/frontend/app/tailwind.css b/frontend/app/tailwind.css
index b5c61c9..6d3faee 100644
--- a/frontend/app/tailwind.css
+++ b/frontend/app/tailwind.css
@@ -1,3 +1,9 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
+
+@layer components {
+ .bg-iosdc-japan {
+ @apply bg-gradient-to-r from-orange-400 via-pink-500 to-purple-400;
+ }
+}