diff options
| author | nsfisis <nsfisis@gmail.com> | 2024-08-18 01:46:36 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2024-08-18 01:46:36 +0900 |
| commit | ed796dddde6db433c88b04a1ad154bf88a24faa4 (patch) | |
| tree | de82ed4ae74c75ba7ddf9e478cb6129177ee4c89 | |
| parent | 7653eb2b28911a0479b3b673c9b63fd490aedb6b (diff) | |
| parent | 9bd1f89febe6311781aca3e8ee2b6a706a606e3c (diff) | |
| download | phperkaigi-2025-albatross-ed796dddde6db433c88b04a1ad154bf88a24faa4.tar.gz phperkaigi-2025-albatross-ed796dddde6db433c88b04a1ad154bf88a24faa4.tar.zst phperkaigi-2025-albatross-ed796dddde6db433c88b04a1ad154bf88a24faa4.zip | |
Merge branch 'feat/watch-page'
| -rw-r--r-- | backend/game/hub.go | 27 | ||||
| -rw-r--r-- | frontend/app/components/Gaming/CodeBlock.tsx | 11 | ||||
| -rw-r--r-- | frontend/app/components/Gaming/ExecStatusIndicatorIcon.tsx (renamed from frontend/app/components/ExecStatusIndicatorIcon.tsx) | 2 | ||||
| -rw-r--r-- | frontend/app/components/Gaming/ScoreBar.tsx | 25 | ||||
| -rw-r--r-- | frontend/app/components/Gaming/SubmitResult.tsx | 54 | ||||
| -rw-r--r-- | frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx | 52 | ||||
| -rw-r--r-- | frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx | 129 | ||||
| -rw-r--r-- | frontend/app/root.tsx | 1 | ||||
| -rw-r--r-- | frontend/app/routes/_index.tsx | 1 |
9 files changed, 136 insertions, 166 deletions
diff --git a/backend/game/hub.go b/backend/game/hub.go index 7c06e05..d385746 100644 --- a/backend/game/hub.go +++ b/backend/game/hub.go @@ -138,7 +138,7 @@ func (hub *gameHub) run() { } } -func (hub *gameHub) sendExecResultMessage(playerID int, testcaseID nullable.Nullable[int], status string, stdout string, stderr string) { +func (hub *gameHub) sendExecResult(playerID int, testcaseID nullable.Nullable[int], status string, stdout string, stderr string) { hub.sendToPlayer(playerID, &playerMessageS2CExecResult{ Type: playerMessageTypeS2CExecResult, Data: playerMessageS2CExecResultPayload{ @@ -218,7 +218,7 @@ func (hub *gameHub) processTaskResults() { case *taskqueue.TaskResultCompileSwiftToWasm: err := hub.processTaskResultCompileSwiftToWasm(taskResult) if err != nil { - hub.sendExecResultMessage( + hub.sendExecResult( taskResult.TaskPayload.UserID(), nullable.NewNullNullable[int](), err.Status, @@ -234,7 +234,7 @@ func (hub *gameHub) processTaskResults() { case *taskqueue.TaskResultCompileWasmToNativeExecutable: err := hub.processTaskResultCompileWasmToNativeExecutable(taskResult) if err != nil { - hub.sendExecResultMessage( + hub.sendExecResult( taskResult.TaskPayload.UserID(), nullable.NewNullNullable[int](), err.Status, @@ -247,12 +247,11 @@ func (hub *gameHub) processTaskResults() { nullable.NewNullNullable[int](), ) } else { - hub.sendExecResultMessage( + hub.sendExecResult( taskResult.TaskPayload.UserID(), nullable.NewNullNullable[int](), "success", - // TODO: inherit the command stdout/stderr. - "Successfully compiled", + "", "", ) } @@ -270,13 +269,12 @@ func (hub *gameHub) processTaskResults() { Stderr: "", }) if err != nil { - hub.sendExecResultMessage( + hub.sendExecResult( taskResult.TaskPayload.UserID(), nullable.NewNullableWithValue(int(taskResult.TaskPayload.TestcaseID)), "internal_error", - // TODO: inherit the command stdout/stderr? "", - "internal error", + "", ) hub.sendSubmitResult( taskResult.TaskPayload.UserID(), @@ -286,20 +284,19 @@ func (hub *gameHub) processTaskResults() { continue } if err1 != nil { - hub.sendExecResultMessage( + hub.sendExecResult( taskResult.TaskPayload.UserID(), nullable.NewNullableWithValue(int(taskResult.TaskPayload.TestcaseID)), aggregatedStatus, - err1.Stdout, - err1.Stderr, + "", + "", ) } else { - hub.sendExecResultMessage( + hub.sendExecResult( taskResult.TaskPayload.UserID(), nullable.NewNullableWithValue(int(taskResult.TaskPayload.TestcaseID)), "success", - // TODO: inherit the command stdout/stderr? - "Testcase passed", + "", "", ) } diff --git a/frontend/app/components/Gaming/CodeBlock.tsx b/frontend/app/components/Gaming/CodeBlock.tsx new file mode 100644 index 0000000..20cd425 --- /dev/null +++ b/frontend/app/components/Gaming/CodeBlock.tsx @@ -0,0 +1,11 @@ +type Props = { + code: string; +}; + +export default function CodeBlock({ code }: Props) { + return ( + <pre className="bg-white resize-none h-full w-full rounded-lg border border-gray-300 p-2"> + <code>{code}</code> + </pre> + ); +} diff --git a/frontend/app/components/ExecStatusIndicatorIcon.tsx b/frontend/app/components/Gaming/ExecStatusIndicatorIcon.tsx index 5277bfa..8daf48c 100644 --- a/frontend/app/components/ExecStatusIndicatorIcon.tsx +++ b/frontend/app/components/Gaming/ExecStatusIndicatorIcon.tsx @@ -6,7 +6,7 @@ import { faRotate, } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import type { ExecResultStatus } from "../models/ExecResult"; +import type { ExecResultStatus } from "../../models/ExecResult"; type Props = { status: ExecResultStatus; diff --git a/frontend/app/components/Gaming/ScoreBar.tsx b/frontend/app/components/Gaming/ScoreBar.tsx new file mode 100644 index 0000000..4eac3ad --- /dev/null +++ b/frontend/app/components/Gaming/ScoreBar.tsx @@ -0,0 +1,25 @@ +type Props = { + scoreA: number | null; + scoreB: number | null; + bgA: string; + bgB: string; +}; + +export default function ScoreBar({ scoreA, scoreB, bgA, bgB }: Props) { + let scoreRatio; + if (scoreA === null && scoreB === null) { + scoreRatio = 50; + } else if (scoreA === null) { + scoreRatio = 0; + } else if (scoreB === null) { + scoreRatio = 100; + } else { + scoreRatio = (scoreB / (scoreA + scoreB)) * 100; + } + + return ( + <div className={`w-full ${bgB}`}> + <div className={`h-6 ${bgA}`} style={{ width: `${scoreRatio}%` }}></div> + </div> + ); +} diff --git a/frontend/app/components/Gaming/SubmitResult.tsx b/frontend/app/components/Gaming/SubmitResult.tsx new file mode 100644 index 0000000..5b08ef1 --- /dev/null +++ b/frontend/app/components/Gaming/SubmitResult.tsx @@ -0,0 +1,54 @@ +import { faArrowDown } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import React from "react"; +import type { SubmitResult } from "../../models/SubmitResult"; +import BorderedContainer from "../BorderedContainer"; +import SubmitStatusLabel from "../SubmitStatusLabel"; +import ExecStatusIndicatorIcon from "./ExecStatusIndicatorIcon"; + +type Props = { + result: SubmitResult; + submitButton?: React.ReactNode; +}; + +export default function SubmitResult({ result, submitButton }: Props) { + return ( + <div className="flex flex-col gap-2"> + <div className="flex"> + {submitButton} + <div className="grow font-bold text-xl text-center"> + <SubmitStatusLabel status={result.status} /> + </div> + </div> + <ul className="flex flex-col gap-2"> + {result.execResults.map((r, idx) => ( + <li key={r.testcase_id ?? -1} className="flex gap-2"> + <div className="flex flex-col gap-2 p-2"> + <ExecStatusIndicatorIcon status={r.status} /> + {idx !== result.execResults.length - 1 && ( + <div> + <FontAwesomeIcon + icon={faArrowDown} + fixedWidth + className="text-gray-500" + /> + </div> + )} + </div> + <div className="grow p-2"> + <BorderedContainer> + <div className="font-semibold">{r.label}</div> + <div> + <code> + {r.stdout} + {r.stderr} + </code> + </div> + </BorderedContainer> + </div> + </li> + ))} + </ul> + </div> + ); +} diff --git a/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx b/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx index a6c4550..e9139ba 100644 --- a/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx +++ b/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx @@ -1,12 +1,9 @@ -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"; +import SubmitResult from "../Gaming/SubmitResult"; type Props = { gameDisplayName: string; @@ -92,44 +89,15 @@ export default function GolfPlayAppGaming({ 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 className="p-4"> + <SubmitResult + result={playerInfo.submitResult} + submitButton={ + <SubmitButton onClick={handleSubmitButtonClick}> + 提出 + </SubmitButton> + } + /> </div> </div> </div> diff --git a/frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx b/frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx index 63c232b..abdc855 100644 --- a/frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx +++ b/frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx @@ -1,9 +1,8 @@ -import { faArrowDown } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { PlayerInfo } from "../../models/PlayerInfo"; import BorderedContainer from "../BorderedContainer"; -import ExecStatusIndicatorIcon from "../ExecStatusIndicatorIcon"; -import SubmitStatusLabel from "../SubmitStatusLabel"; +import CodeBlock from "../Gaming/CodeBlock"; +import ScoreBar from "../Gaming/ScoreBar"; +import SubmitResult from "../Gaming/SubmitResult"; type Props = { gameDisplayName: string; @@ -21,6 +20,8 @@ export default function GolfWatchAppGaming({ leftTimeSeconds, playerInfoA, playerInfoB, + problemTitle, + problemDescription, }: Props) { const leftTime = (() => { const k = gameDurationSeconds + leftTimeSeconds; @@ -29,20 +30,6 @@ export default function GolfWatchAppGaming({ return `${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`; })(); - const scoreRatio = (() => { - const scoreA = playerInfoA.score; - const scoreB = playerInfoB.score; - if (scoreA === null && scoreB === null) { - return 50; - } else if (scoreA === null) { - return 0; - } else if (scoreB === null) { - return 100; - } else { - return (scoreB / (scoreA + scoreB)) * 100; - } - })(); - return ( <div className="min-h-screen bg-gray-100 flex flex-col"> <div className="text-white bg-iosdc-japan grid grid-cols-3 px-4 py-2"> @@ -91,99 +78,27 @@ export default function GolfWatchAppGaming({ </div> </div> </div> - <div className="w-full bg-purple-400"> - <div - className="h-6 bg-orange-400" - style={{ width: `${scoreRatio}%` }} - ></div> - </div> - <div className="grow grid grid-cols-10 p-4 gap-4"> - <div className="col-span-3"> - <pre className="bg-white resize-none h-full w-full rounded-lg border border-gray-300 p-2"> - <code>{playerInfoA.code}</code> - </pre> - </div> - <div className="col-span-2 flex flex-col gap-4"> - <div className="flex"> - <div className="grow font-bold text-xl text-center"> - <SubmitStatusLabel status={playerInfoA.submitResult.status} /> - </div> + <ScoreBar + scoreA={playerInfoA.score} + scoreB={playerInfoB.score} + bgA="bg-orange-400" + bgB="bg-purple-400" + /> + <div className="grow grid grid-cols-3 p-4 gap-4"> + <CodeBlock code={playerInfoA.code ?? ""} /> + <div className="flex flex-col gap-4 justify-between"> + <div className="grid grid-cols-2 gap-4"> + <SubmitResult result={playerInfoA.submitResult} /> + <SubmitResult result={playerInfoB.submitResult} /> </div> - <ul className="flex flex-col gap-2"> - {playerInfoA.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 !== playerInfoA.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 className="col-span-2 flex flex-col gap-4"> - <div className="flex"> - <div className="grow font-bold text-xl text-center"> - <SubmitStatusLabel status={playerInfoB.submitResult.status} /> + <div> + <div className="mb-2 text-center text-xl font-bold"> + {problemTitle} </div> + <BorderedContainer>{problemDescription}</BorderedContainer> </div> - <ul className="flex flex-col gap-2"> - {playerInfoB.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 !== playerInfoB.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 className="col-span-3"> - <pre className="bg-white resize-none h-full w-full rounded-lg border border-gray-300 p-2"> - <code>{playerInfoB.code}</code> - </pre> </div> + <CodeBlock code={playerInfoB.code ?? ""} /> </div> </div> ); diff --git a/frontend/app/root.tsx b/frontend/app/root.tsx index 7924ce7..c05020c 100644 --- a/frontend/app/root.tsx +++ b/frontend/app/root.tsx @@ -1,4 +1,5 @@ import { config } from "@fortawesome/fontawesome-svg-core"; +import "@fortawesome/fontawesome-svg-core/styles.css"; import type { LinksFunction } from "@remix-run/node"; import { Links, diff --git a/frontend/app/routes/_index.tsx b/frontend/app/routes/_index.tsx index 24fff9f..baa3909 100644 --- a/frontend/app/routes/_index.tsx +++ b/frontend/app/routes/_index.tsx @@ -1,5 +1,4 @@ import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; -import "@fortawesome/fontawesome-svg-core/styles.css"; import { ensureUserNotLoggedIn } from "../.server/auth"; import BorderedContainer from "../components/BorderedContainer"; import NavigateLink from "../components/NavigateLink"; |
