aboutsummaryrefslogtreecommitdiffhomepage
path: root/frontend/app/components
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2025-03-08 10:51:41 +0900
committernsfisis <nsfisis@gmail.com>2025-03-08 10:51:41 +0900
commita7ce31249948e4f0c1950de93f3c4f7cdda51cf4 (patch)
treec4c740f0cccd15f825596f7a115f3b8f8eb8ffa7 /frontend/app/components
parent7f4d16dca85263dcbc7b3bb29f5fc50f4371739d (diff)
parentc06d46eae30c9468535fb6af5e9b822acadbbdb6 (diff)
downloadphperkaigi-2025-albatross-a7ce31249948e4f0c1950de93f3c4f7cdda51cf4.tar.gz
phperkaigi-2025-albatross-a7ce31249948e4f0c1950de93f3c4f7cdda51cf4.tar.zst
phperkaigi-2025-albatross-a7ce31249948e4f0c1950de93f3c4f7cdda51cf4.zip
Merge branch 'phperkaigi-2025'
Diffstat (limited to 'frontend/app/components')
-rw-r--r--frontend/app/components/BorderedContainer.tsx2
-rw-r--r--frontend/app/components/Gaming/CodeBlock.tsx31
-rw-r--r--frontend/app/components/Gaming/ExecStatusIndicatorIcon.tsx11
-rw-r--r--frontend/app/components/Gaming/SubmitResult.tsx34
-rw-r--r--frontend/app/components/GolfPlayApp.client.tsx188
-rw-r--r--frontend/app/components/GolfPlayApp.tsx141
-rw-r--r--frontend/app/components/GolfPlayApps/GolfPlayAppConnecting.tsx9
-rw-r--r--frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx19
-rw-r--r--frontend/app/components/GolfPlayApps/GolfPlayAppStarting.tsx2
-rw-r--r--frontend/app/components/GolfPlayApps/GolfPlayAppWaiting.tsx2
-rw-r--r--frontend/app/components/GolfWatchApp.client.tsx197
-rw-r--r--frontend/app/components/GolfWatchApp.tsx143
-rw-r--r--frontend/app/components/GolfWatchAppWithAudioPlayRequest.client.tsx34
-rw-r--r--frontend/app/components/GolfWatchApps/GolfWatchAppConnecting.tsx9
-rw-r--r--frontend/app/components/GolfWatchApps/GolfWatchAppGaming1v1.tsx (renamed from frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx)35
-rw-r--r--frontend/app/components/GolfWatchApps/GolfWatchAppGamingMultiplayer.tsx102
-rw-r--r--frontend/app/components/GolfWatchApps/GolfWatchAppStarting.tsx2
-rw-r--r--frontend/app/components/GolfWatchApps/GolfWatchAppWaiting1v1.tsx (renamed from frontend/app/components/GolfWatchApps/GolfWatchAppWaiting.tsx)4
-rw-r--r--frontend/app/components/GolfWatchApps/GolfWatchAppWaitingMultiplayer.tsx15
-rw-r--r--frontend/app/components/InputText.tsx2
-rw-r--r--frontend/app/components/NavigateLink.tsx2
-rw-r--r--frontend/app/components/SubmitButton.tsx2
-rw-r--r--frontend/app/components/SubmitStatusLabel.tsx8
-rw-r--r--frontend/app/components/UserIcon.tsx4
24 files changed, 477 insertions, 521 deletions
diff --git a/frontend/app/components/BorderedContainer.tsx b/frontend/app/components/BorderedContainer.tsx
index cbbfbde..fe15c3b 100644
--- a/frontend/app/components/BorderedContainer.tsx
+++ b/frontend/app/components/BorderedContainer.tsx
@@ -6,7 +6,7 @@ type Props = {
export default function BorderedContainer({ children }: Props) {
return (
- <div className="bg-white border-2 border-pink-600 rounded-xl p-4">
+ <div className="bg-white border-2 border-blue-600 rounded-xl p-4">
{children}
</div>
);
diff --git a/frontend/app/components/Gaming/CodeBlock.tsx b/frontend/app/components/Gaming/CodeBlock.tsx
index b7d45c0..0a9a2e5 100644
--- a/frontend/app/components/Gaming/CodeBlock.tsx
+++ b/frontend/app/components/Gaming/CodeBlock.tsx
@@ -1,8 +1,5 @@
-import Prism, { highlight, languages } from "prismjs";
-import "prismjs/components/prism-swift";
-import "prismjs/themes/prism.min.css";
-
-Prism.manual = true;
+import { useEffect, useState } from "react";
+import { codeToHtml } from "shiki";
type Props = {
code: string;
@@ -10,11 +7,31 @@ type Props = {
};
export default function CodeBlock({ code, language }: Props) {
- const highlighted = highlight(code, languages[language]!, language);
+ const [highlightedCode, setHighlightedCode] = useState<string | null>(null);
+
+ useEffect(() => {
+ let isMounted = true;
+
+ (async () => {
+ const highlighted = await codeToHtml(code, {
+ lang: language,
+ theme: "github-light",
+ });
+ if (isMounted) {
+ setHighlightedCode(highlighted);
+ }
+ })();
+
+ return () => {
+ isMounted = false;
+ };
+ }, [code, language]);
return (
<pre className="h-full w-full p-2 bg-gray-50 rounded-lg border border-gray-300 whitespace-pre-wrap break-words">
- <code dangerouslySetInnerHTML={{ __html: highlighted }} />
+ {highlightedCode === null ? null : (
+ <code dangerouslySetInnerHTML={{ __html: highlightedCode }} />
+ )}
</pre>
);
}
diff --git a/frontend/app/components/Gaming/ExecStatusIndicatorIcon.tsx b/frontend/app/components/Gaming/ExecStatusIndicatorIcon.tsx
index a717a48..44d28ad 100644
--- a/frontend/app/components/Gaming/ExecStatusIndicatorIcon.tsx
+++ b/frontend/app/components/Gaming/ExecStatusIndicatorIcon.tsx
@@ -1,20 +1,19 @@
import {
- faBan,
faCircle,
faCircleCheck,
faCircleExclamation,
faRotate,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import type { ExecResultStatus } from "../../types/ExecResult";
+import type { components } from "../../api/schema";
type Props = {
- status: ExecResultStatus;
+ status: components["schemas"]["ExecutionStatus"];
};
export default function ExecStatusIndicatorIcon({ status }: Props) {
switch (status) {
- case "waiting_submission":
+ case "none":
return (
<FontAwesomeIcon icon={faCircle} fixedWidth className="text-gray-400" />
);
@@ -35,10 +34,6 @@ export default function ExecStatusIndicatorIcon({ status }: Props) {
className="text-sky-500"
/>
);
- case "canceled":
- return (
- <FontAwesomeIcon icon={faBan} fixedWidth className="text-gray-400" />
- );
default:
return (
<FontAwesomeIcon
diff --git a/frontend/app/components/Gaming/SubmitResult.tsx b/frontend/app/components/Gaming/SubmitResult.tsx
index c626910..a78c79e 100644
--- a/frontend/app/components/Gaming/SubmitResult.tsx
+++ b/frontend/app/components/Gaming/SubmitResult.tsx
@@ -1,47 +1,21 @@
import React from "react";
-import type { SubmitResult } from "../../types/SubmitResult";
-import BorderedContainer from "../BorderedContainer";
+import type { components } from "../../api/schema";
import SubmitStatusLabel from "../SubmitStatusLabel";
-import ExecStatusIndicatorIcon from "./ExecStatusIndicatorIcon";
type Props = {
- result: SubmitResult;
+ status: components["schemas"]["ExecutionStatus"];
submitButton?: React.ReactNode;
};
-export default function SubmitResult({ result, submitButton }: Props) {
+export default function SubmitResult({ status, 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} />
+ <SubmitStatusLabel status={status} />
</div>
</div>
- <ul className="flex flex-col gap-4">
- {result.execResults.map((r) => (
- <li key={r.testcase_id ?? -1}>
- <BorderedContainer>
- <div className="flex flex-col gap-2">
- <div className="flex gap-2">
- <div className="my-auto">
- <ExecStatusIndicatorIcon status={r.status} />
- </div>
- <div className="font-semibold">{r.label}</div>
- </div>
- {r.stdout + r.stderr && (
- <pre className="overflow-y-hidden max-h-96 p-2 bg-gray-50 rounded-lg border border-gray-300 whitespace-pre-wrap break-words">
- <code>
- {r.stdout}
- {r.stderr}
- </code>
- </pre>
- )}
- </div>
- </BorderedContainer>
- </li>
- ))}
- </ul>
</div>
);
}
diff --git a/frontend/app/components/GolfPlayApp.client.tsx b/frontend/app/components/GolfPlayApp.client.tsx
deleted file mode 100644
index 4bd464b..0000000
--- a/frontend/app/components/GolfPlayApp.client.tsx
+++ /dev/null
@@ -1,188 +0,0 @@
-import { useAtomValue, useSetAtom } from "jotai";
-import { useEffect } from "react";
-import { useTimer } from "react-use-precision-timer";
-import { useDebouncedCallback } from "use-debounce";
-import type { components } from "../.server/api/schema";
-import useWebSocket, { ReadyState } from "../hooks/useWebSocket";
-import {
- gameStartAtom,
- gameStateKindAtom,
- handleSubmitCodeAtom,
- handleWsConnectionClosedAtom,
- handleWsExecResultMessageAtom,
- handleWsSubmitResultMessageAtom,
- setCurrentTimestampAtom,
- setGameStateConnectingAtom,
- setGameStateWaitingAtom,
-} from "../states/play";
-import GolfPlayAppConnecting from "./GolfPlayApps/GolfPlayAppConnecting";
-import GolfPlayAppFinished from "./GolfPlayApps/GolfPlayAppFinished";
-import GolfPlayAppGaming from "./GolfPlayApps/GolfPlayAppGaming";
-import GolfPlayAppStarting from "./GolfPlayApps/GolfPlayAppStarting";
-import GolfPlayAppWaiting from "./GolfPlayApps/GolfPlayAppWaiting";
-
-type GamePlayerMessageS2C = components["schemas"]["GamePlayerMessageS2C"];
-type GamePlayerMessageC2S = components["schemas"]["GamePlayerMessageC2S"];
-
-type Game = components["schemas"]["Game"];
-type User = components["schemas"]["User"];
-
-type Props = {
- game: Game;
- player: User;
- initialCode: string;
- sockToken: string;
-};
-
-export default function GolfPlayApp({
- game,
- player,
- initialCode,
- sockToken,
-}: Props) {
- const socketUrl =
- process.env.NODE_ENV === "development"
- ? `ws://localhost:8002/iosdc-japan/2024/code-battle/sock/golf/${game.game_id}/play?token=${sockToken}`
- : `wss://t.nil.ninja/iosdc-japan/2024/code-battle/sock/golf/${game.game_id}/play?token=${sockToken}`;
-
- const gameStateKind = useAtomValue(gameStateKindAtom);
- const setCurrentTimestamp = useSetAtom(setCurrentTimestampAtom);
- const gameStart = useSetAtom(gameStartAtom);
- const setGameStateConnecting = useSetAtom(setGameStateConnectingAtom);
- const setGameStateWaiting = useSetAtom(setGameStateWaitingAtom);
- const handleWsConnectionClosed = useSetAtom(handleWsConnectionClosedAtom);
- const handleWsExecResultMessage = useSetAtom(handleWsExecResultMessageAtom);
- const handleWsSubmitResultMessage = useSetAtom(
- handleWsSubmitResultMessageAtom,
- );
- const handleSubmitCode = useSetAtom(handleSubmitCodeAtom);
-
- useTimer({ delay: 1000, startImmediately: true }, setCurrentTimestamp);
-
- const { sendJsonMessage, lastJsonMessage, readyState } = useWebSocket<
- GamePlayerMessageS2C,
- GamePlayerMessageC2S
- >(socketUrl);
-
- const playerProfile = {
- displayName: player.display_name,
- iconPath: player.icon_path ?? null,
- };
-
- const onCodeChange = useDebouncedCallback((code: string) => {
- console.log("player:c2s:code");
- sendJsonMessage({
- type: "player:c2s:code",
- data: { code },
- });
- const baseKey = `playerState:${game.game_id}:${player.user_id}`;
- window.localStorage.setItem(`${baseKey}:code`, code);
- }, 1000);
-
- const onCodeSubmit = useDebouncedCallback((code: string) => {
- if (code === "") {
- return;
- }
- console.log("player:c2s:submit");
- sendJsonMessage({
- type: "player:c2s:submit",
- data: { code },
- });
- handleSubmitCode();
- }, 1000);
-
- if (readyState === ReadyState.UNINSTANTIATED) {
- throw new Error("WebSocket is not connected");
- }
-
- useEffect(() => {
- if (readyState === ReadyState.CLOSING || readyState === ReadyState.CLOSED) {
- handleWsConnectionClosed();
- } else if (readyState === ReadyState.CONNECTING) {
- setGameStateConnecting();
- } else if (readyState === ReadyState.OPEN) {
- if (lastJsonMessage !== null) {
- console.log(lastJsonMessage.type);
- console.log(lastJsonMessage.data);
- if (lastJsonMessage.type === "player:s2c:start") {
- const { start_at } = lastJsonMessage.data;
- gameStart(start_at);
- } else if (lastJsonMessage.type === "player:s2c:execresult") {
- handleWsExecResultMessage(
- lastJsonMessage.data,
- (submissionResult) => {
- const baseKey = `playerState:${game.game_id}:${player.user_id}`;
- window.localStorage.setItem(
- `${baseKey}:submissionResult`,
- JSON.stringify(submissionResult),
- );
- },
- );
- } else if (lastJsonMessage.type === "player:s2c:submitresult") {
- handleWsSubmitResultMessage(
- lastJsonMessage.data,
- (submissionResult, score) => {
- const baseKey = `playerState:${game.game_id}:${player.user_id}`;
- window.localStorage.setItem(
- `${baseKey}:submissionResult`,
- JSON.stringify(submissionResult),
- );
- window.localStorage.setItem(
- `${baseKey}:score`,
- score === null ? "" : score.toString(),
- );
- },
- );
- }
- } else {
- if (game.started_at) {
- gameStart(game.started_at);
- } else {
- setGameStateWaiting();
- }
- }
- }
- }, [
- game.game_id,
- game.started_at,
- player.user_id,
- sendJsonMessage,
- lastJsonMessage,
- readyState,
- gameStart,
- handleWsConnectionClosed,
- handleWsExecResultMessage,
- handleWsSubmitResultMessage,
- setGameStateConnecting,
- setGameStateWaiting,
- ]);
-
- if (gameStateKind === "connecting") {
- return <GolfPlayAppConnecting />;
- } else if (gameStateKind === "waiting") {
- return (
- <GolfPlayAppWaiting
- gameDisplayName={game.display_name}
- playerProfile={playerProfile}
- />
- );
- } else if (gameStateKind === "starting") {
- return <GolfPlayAppStarting gameDisplayName={game.display_name} />;
- } else if (gameStateKind === "gaming") {
- return (
- <GolfPlayAppGaming
- gameDisplayName={game.display_name}
- playerProfile={playerProfile}
- problemTitle={game.problem.title}
- problemDescription={game.problem.description}
- initialCode={initialCode}
- onCodeChange={onCodeChange}
- onCodeSubmit={onCodeSubmit}
- />
- );
- } else if (gameStateKind === "finished") {
- return <GolfPlayAppFinished />;
- } else {
- return null;
- }
-}
diff --git a/frontend/app/components/GolfPlayApp.tsx b/frontend/app/components/GolfPlayApp.tsx
new file mode 100644
index 0000000..e8fafbd
--- /dev/null
+++ b/frontend/app/components/GolfPlayApp.tsx
@@ -0,0 +1,141 @@
+import { useAtomValue, useSetAtom } from "jotai";
+import { useContext, useEffect, useState } from "react";
+import { useTimer } from "react-use-precision-timer";
+import { useDebouncedCallback } from "use-debounce";
+import {
+ ApiAuthTokenContext,
+ apiGetGame,
+ apiGetGamePlayLatestState,
+ apiPostGamePlayCode,
+ apiPostGamePlaySubmit,
+} from "../api/client";
+import type { components } from "../api/schema";
+import {
+ gameStateKindAtom,
+ handleSubmitCodePostAtom,
+ handleSubmitCodePreAtom,
+ setCurrentTimestampAtom,
+ setGameStartedAtAtom,
+ setLatestGameStateAtom,
+} from "../states/play";
+import GolfPlayAppFinished from "./GolfPlayApps/GolfPlayAppFinished";
+import GolfPlayAppGaming from "./GolfPlayApps/GolfPlayAppGaming";
+import GolfPlayAppStarting from "./GolfPlayApps/GolfPlayAppStarting";
+import GolfPlayAppWaiting from "./GolfPlayApps/GolfPlayAppWaiting";
+
+type Game = components["schemas"]["Game"];
+type User = components["schemas"]["User"];
+
+type Props = {
+ game: Game;
+ player: User;
+ initialCode: string;
+};
+
+export default function GolfPlayApp({ game, player, initialCode }: Props) {
+ const apiAuthToken = useContext(ApiAuthTokenContext);
+
+ const gameStateKind = useAtomValue(gameStateKindAtom);
+ const setGameStartedAt = useSetAtom(setGameStartedAtAtom);
+ const setCurrentTimestamp = useSetAtom(setCurrentTimestampAtom);
+ const handleSubmitCodePre = useSetAtom(handleSubmitCodePreAtom);
+ const handleSubmitCodePost = useSetAtom(handleSubmitCodePostAtom);
+ const setLatestGameState = useSetAtom(setLatestGameStateAtom);
+
+ useTimer({ delay: 1000, startImmediately: true }, setCurrentTimestamp);
+
+ const playerProfile = {
+ id: player.user_id,
+ displayName: player.display_name,
+ iconPath: player.icon_path ?? null,
+ };
+
+ const onCodeChange = useDebouncedCallback(async (code: string) => {
+ console.log("player:c2s:code");
+ if (game.game_type === "1v1") {
+ await apiPostGamePlayCode(apiAuthToken, game.game_id, code);
+ }
+ }, 1000);
+
+ const onCodeSubmit = useDebouncedCallback(async (code: string) => {
+ if (code === "") {
+ return;
+ }
+ console.log("player:c2s:submit");
+ handleSubmitCodePre();
+ await apiPostGamePlaySubmit(apiAuthToken, game.game_id, code);
+ handleSubmitCodePost();
+ }, 1000);
+
+ const [isDataPolling, setIsDataPolling] = useState(false);
+
+ useEffect(() => {
+ if (isDataPolling) {
+ return;
+ }
+ const timerId = setInterval(async () => {
+ if (isDataPolling) {
+ return;
+ }
+ setIsDataPolling(true);
+
+ try {
+ if (gameStateKind === "waiting") {
+ const { game: g } = await apiGetGame(apiAuthToken, game.game_id);
+ if (g.started_at != null) {
+ setGameStartedAt(g.started_at);
+ }
+ } else if (gameStateKind === "gaming") {
+ const { state } = await apiGetGamePlayLatestState(
+ apiAuthToken,
+ game.game_id,
+ );
+ setLatestGameState(state);
+ }
+ } catch (error) {
+ console.error(error);
+ } finally {
+ setIsDataPolling(false);
+ }
+ }, 1000);
+
+ return () => {
+ clearInterval(timerId);
+ };
+ }, [
+ isDataPolling,
+ apiAuthToken,
+ game.game_id,
+ gameStateKind,
+ setGameStartedAt,
+ setLatestGameState,
+ ]);
+
+ if (gameStateKind === "waiting") {
+ return (
+ <GolfPlayAppWaiting
+ gameDisplayName={game.display_name}
+ playerProfile={playerProfile}
+ />
+ );
+ } else if (gameStateKind === "starting") {
+ return <GolfPlayAppStarting gameDisplayName={game.display_name} />;
+ } else if (gameStateKind === "gaming") {
+ return (
+ <GolfPlayAppGaming
+ gameDisplayName={game.display_name}
+ playerProfile={playerProfile}
+ problemTitle={game.problem.title}
+ problemDescription={game.problem.description}
+ sampleCode={game.problem.sample_code}
+ initialCode={initialCode}
+ onCodeChange={onCodeChange}
+ onCodeSubmit={onCodeSubmit}
+ />
+ );
+ } else if (gameStateKind === "finished") {
+ return <GolfPlayAppFinished />;
+ } else {
+ return null;
+ }
+}
diff --git a/frontend/app/components/GolfPlayApps/GolfPlayAppConnecting.tsx b/frontend/app/components/GolfPlayApps/GolfPlayAppConnecting.tsx
deleted file mode 100644
index 4b80f8f..0000000
--- a/frontend/app/components/GolfPlayApps/GolfPlayAppConnecting.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-export default function GolfPlayAppConnecting() {
- return (
- <div className="min-h-screen bg-gray-100 flex items-center justify-center">
- <div className="text-center">
- <div className="text-6xl font-bold text-black">接続中...</div>
- </div>
- </div>
- );
-}
diff --git a/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx b/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx
index 0aa6b3d..bc205fb 100644
--- a/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx
+++ b/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx
@@ -5,10 +5,11 @@ import SubmitButton from "../../components/SubmitButton";
import {
gamingLeftTimeSecondsAtom,
scoreAtom,
- submitResultAtom,
+ statusAtom,
} from "../../states/play";
import type { PlayerProfile } from "../../types/PlayerProfile";
import BorderedContainer from "../BorderedContainer";
+import CodeBlock from "../Gaming/CodeBlock";
import SubmitResult from "../Gaming/SubmitResult";
import UserIcon from "../UserIcon";
@@ -17,6 +18,7 @@ type Props = {
playerProfile: PlayerProfile;
problemTitle: string;
problemDescription: string;
+ sampleCode: string;
initialCode: string;
onCodeChange: (code: string) => void;
onCodeSubmit: (code: string) => void;
@@ -27,13 +29,14 @@ export default function GolfPlayAppGaming({
playerProfile,
problemTitle,
problemDescription,
+ sampleCode,
initialCode,
onCodeChange,
onCodeSubmit,
}: Props) {
const leftTimeSeconds = useAtomValue(gamingLeftTimeSecondsAtom)!;
const score = useAtomValue(scoreAtom);
- const submitResult = useAtomValue(submitResultAtom);
+ const status = useAtomValue(statusAtom);
const textareaRef = useRef<HTMLTextAreaElement>(null);
@@ -55,7 +58,7 @@ export default function GolfPlayAppGaming({
return (
<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="text-white bg-sky-600 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>
@@ -80,10 +83,16 @@ export default function GolfPlayAppGaming({
<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">
+ <div className="p-2 grid gap-4">
<BorderedContainer>
<div className="text-gray-700">{problemDescription}</div>
</BorderedContainer>
+ <BorderedContainer>
+ <div>
+ <h2>サンプルコード</h2>
+ <CodeBlock code={sampleCode} language="php" />
+ </div>
+ </BorderedContainer>
</div>
</div>
<div className="p-4">
@@ -96,7 +105,7 @@ export default function GolfPlayAppGaming({
</div>
<div className="p-4">
<SubmitResult
- result={submitResult}
+ status={status}
submitButton={
<SubmitButton onClick={handleSubmitButtonClick}>
提出
diff --git a/frontend/app/components/GolfPlayApps/GolfPlayAppStarting.tsx b/frontend/app/components/GolfPlayApps/GolfPlayAppStarting.tsx
index b41dfed..07b93d6 100644
--- a/frontend/app/components/GolfPlayApps/GolfPlayAppStarting.tsx
+++ b/frontend/app/components/GolfPlayApps/GolfPlayAppStarting.tsx
@@ -10,7 +10,7 @@ export default function GolfPlayAppStarting({ gameDisplayName }: Props) {
return (
<div className="min-h-screen bg-gray-100 flex flex-col">
- <div className="text-white bg-iosdc-japan p-10 text-center">
+ <div className="text-white bg-sky-600 p-10 text-center">
<div className="text-4xl font-bold">{gameDisplayName}</div>
</div>
<div className="text-center text-black font-black text-10xl">
diff --git a/frontend/app/components/GolfPlayApps/GolfPlayAppWaiting.tsx b/frontend/app/components/GolfPlayApps/GolfPlayAppWaiting.tsx
index 706dc8f..4b74c0d 100644
--- a/frontend/app/components/GolfPlayApps/GolfPlayAppWaiting.tsx
+++ b/frontend/app/components/GolfPlayApps/GolfPlayAppWaiting.tsx
@@ -12,7 +12,7 @@ export default function GolfPlayAppWaiting({
}: Props) {
return (
<div className="min-h-screen bg-gray-100 flex flex-col font-bold text-center">
- <div className="text-white bg-iosdc-japan p-10">
+ <div className="text-white bg-sky-600 p-10">
<div className="text-4xl">{gameDisplayName}</div>
</div>
<div className="grow grid mx-auto text-black">
diff --git a/frontend/app/components/GolfWatchApp.client.tsx b/frontend/app/components/GolfWatchApp.client.tsx
deleted file mode 100644
index f02bfb9..0000000
--- a/frontend/app/components/GolfWatchApp.client.tsx
+++ /dev/null
@@ -1,197 +0,0 @@
-import { useAtomValue, useSetAtom } from "jotai";
-import { useCallback, useEffect } from "react";
-import { useTimer } from "react-use-precision-timer";
-import type { components } from "../.server/api/schema";
-import useWebSocket, { ReadyState } from "../hooks/useWebSocket";
-import {
- gameStartAtom,
- gameStateKindAtom,
- handleWsCodeMessageAtom,
- handleWsConnectionClosedAtom,
- handleWsExecResultMessageAtom,
- handleWsSubmitMessageAtom,
- handleWsSubmitResultMessageAtom,
- setCurrentTimestampAtom,
- setGameStateConnectingAtom,
- setGameStateWaitingAtom,
-} from "../states/watch";
-import GolfWatchAppConnecting from "./GolfWatchApps/GolfWatchAppConnecting";
-import GolfWatchAppGaming from "./GolfWatchApps/GolfWatchAppGaming";
-import GolfWatchAppStarting from "./GolfWatchApps/GolfWatchAppStarting";
-import GolfWatchAppWaiting from "./GolfWatchApps/GolfWatchAppWaiting";
-
-type GameWatcherMessageS2C = components["schemas"]["GameWatcherMessageS2C"];
-type GameWatcherMessageC2S = never;
-
-type Game = components["schemas"]["Game"];
-
-export type Props = {
- game: Game;
- sockToken: string;
-};
-
-export default function GolfWatchApp({ game, sockToken }: Props) {
- const socketUrl =
- process.env.NODE_ENV === "development"
- ? `ws://localhost:8002/iosdc-japan/2024/code-battle/sock/golf/${game.game_id}/watch?token=${sockToken}`
- : `wss://t.nil.ninja/iosdc-japan/2024/code-battle/sock/golf/${game.game_id}/watch?token=${sockToken}`;
-
- const gameStateKind = useAtomValue(gameStateKindAtom);
- const setCurrentTimestamp = useSetAtom(setCurrentTimestampAtom);
- const gameStart = useSetAtom(gameStartAtom);
- const setGameStateConnecting = useSetAtom(setGameStateConnectingAtom);
- const setGameStateWaiting = useSetAtom(setGameStateWaitingAtom);
- const handleWsConnectionClosed = useSetAtom(handleWsConnectionClosedAtom);
- const handleWsCodeMessage = useSetAtom(handleWsCodeMessageAtom);
- const handleWsSubmitMessage = useSetAtom(handleWsSubmitMessageAtom);
- const handleWsExecResultMessage = useSetAtom(handleWsExecResultMessageAtom);
- const handleWsSubmitResultMessage = useSetAtom(
- handleWsSubmitResultMessageAtom,
- );
-
- useTimer({ delay: 1000, startImmediately: true }, setCurrentTimestamp);
-
- const { lastJsonMessage, readyState } = useWebSocket<
- GameWatcherMessageS2C,
- GameWatcherMessageC2S
- >(socketUrl);
-
- const playerA = game.players[0]!;
- const playerB = game.players[1]!;
-
- const getTargetAtomByPlayerId: <T>(
- player_id: number,
- atomA: T,
- atomB: T,
- ) => T = useCallback(
- (player_id, atomA, atomB) =>
- player_id === playerA.user_id ? atomA : atomB,
- [playerA.user_id],
- );
-
- const playerProfileA = {
- displayName: playerA.display_name,
- iconPath: playerA.icon_path ?? null,
- };
- const playerProfileB = {
- displayName: playerB.display_name,
- iconPath: playerB.icon_path ?? null,
- };
-
- if (readyState === ReadyState.UNINSTANTIATED) {
- throw new Error("WebSocket is not connected");
- }
-
- useEffect(() => {
- if (readyState === ReadyState.CLOSING || readyState === ReadyState.CLOSED) {
- handleWsConnectionClosed();
- } else if (readyState === ReadyState.CONNECTING) {
- setGameStateConnecting();
- } else if (readyState === ReadyState.OPEN) {
- if (lastJsonMessage !== null) {
- console.log(lastJsonMessage.type);
- console.log(lastJsonMessage.data);
- if (lastJsonMessage.type === "watcher:s2c:start") {
- const { start_at } = lastJsonMessage.data;
- gameStart(start_at);
- } else if (lastJsonMessage.type === "watcher:s2c:code") {
- handleWsCodeMessage(
- lastJsonMessage.data,
- getTargetAtomByPlayerId,
- (player_id, code) => {
- const baseKey = `watcherState:${game.game_id}:${player_id}`;
- window.localStorage.setItem(`${baseKey}:code`, code);
- },
- );
- } else if (lastJsonMessage.type === "watcher:s2c:submit") {
- handleWsSubmitMessage(
- lastJsonMessage.data,
- getTargetAtomByPlayerId,
- (player_id, submissionResult) => {
- const baseKey = `watcherState:${game.game_id}:${player_id}`;
- window.localStorage.setItem(
- `${baseKey}:submissionResult`,
- JSON.stringify(submissionResult),
- );
- },
- );
- } else if (lastJsonMessage.type === "watcher:s2c:execresult") {
- handleWsExecResultMessage(
- lastJsonMessage.data,
- getTargetAtomByPlayerId,
- (player_id, submissionResult) => {
- const baseKey = `watcherState:${game.game_id}:${player_id}`;
- window.localStorage.setItem(
- `${baseKey}:submissionResult`,
- JSON.stringify(submissionResult),
- );
- },
- );
- } else if (lastJsonMessage.type === "watcher:s2c:submitresult") {
- handleWsSubmitResultMessage(
- lastJsonMessage.data,
- getTargetAtomByPlayerId,
- (player_id, submissionResult, score) => {
- const baseKey = `watcherState:${game.game_id}:${player_id}`;
- window.localStorage.setItem(
- `${baseKey}:submissionResult`,
- JSON.stringify(submissionResult),
- );
- window.localStorage.setItem(
- `${baseKey}:score`,
- score === null ? "" : score.toString(),
- );
- },
- );
- }
- } else {
- if (game.started_at) {
- gameStart(game.started_at);
- } else {
- setGameStateWaiting();
- }
- }
- }
- }, [
- game.started_at,
- game.game_id,
- lastJsonMessage,
- readyState,
- gameStart,
- getTargetAtomByPlayerId,
- handleWsCodeMessage,
- handleWsConnectionClosed,
- handleWsExecResultMessage,
- handleWsSubmitMessage,
- handleWsSubmitResultMessage,
- setGameStateConnecting,
- setGameStateWaiting,
- ]);
-
- if (gameStateKind === "connecting") {
- return <GolfWatchAppConnecting />;
- } else if (gameStateKind === "waiting") {
- return (
- <GolfWatchAppWaiting
- gameDisplayName={game.display_name}
- playerProfileA={playerProfileA}
- playerProfileB={playerProfileB}
- />
- );
- } else if (gameStateKind === "starting") {
- return <GolfWatchAppStarting gameDisplayName={game.display_name} />;
- } else if (gameStateKind === "gaming" || gameStateKind === "finished") {
- return (
- <GolfWatchAppGaming
- gameDisplayName={game.display_name}
- playerProfileA={playerProfileA}
- playerProfileB={playerProfileB}
- problemTitle={game.problem.title}
- problemDescription={game.problem.description}
- gameResult={null /* TODO */}
- />
- );
- } else {
- return null;
- }
-}
diff --git a/frontend/app/components/GolfWatchApp.tsx b/frontend/app/components/GolfWatchApp.tsx
new file mode 100644
index 0000000..402884f
--- /dev/null
+++ b/frontend/app/components/GolfWatchApp.tsx
@@ -0,0 +1,143 @@
+import { useAtom, useAtomValue, useSetAtom } from "jotai";
+import { useContext, useEffect, useState } from "react";
+import { useTimer } from "react-use-precision-timer";
+import {
+ ApiAuthTokenContext,
+ apiGetGame,
+ apiGetGameWatchLatestStates,
+ apiGetGameWatchRanking,
+} from "../api/client";
+import type { components } from "../api/schema";
+import {
+ gameStateKindAtom,
+ rankingAtom,
+ setCurrentTimestampAtom,
+ setGameStartedAtAtom,
+ setLatestGameStatesAtom,
+} from "../states/watch";
+import GolfWatchAppGaming1v1 from "./GolfWatchApps/GolfWatchAppGaming1v1";
+import GolfWatchAppGamingMultiplayer from "./GolfWatchApps/GolfWatchAppGamingMultiplayer";
+import GolfWatchAppStarting from "./GolfWatchApps/GolfWatchAppStarting";
+import GolfWatchAppWaiting1v1 from "./GolfWatchApps/GolfWatchAppWaiting1v1";
+import GolfWatchAppWaitingMultiplayer from "./GolfWatchApps/GolfWatchAppWaitingMultiplayer";
+
+type Game = components["schemas"]["Game"];
+
+export type Props = {
+ game: Game;
+};
+
+export default function GolfWatchApp({ game }: Props) {
+ const apiAuthToken = useContext(ApiAuthTokenContext);
+
+ const gameStateKind = useAtomValue(gameStateKindAtom);
+ const setGameStartedAt = useSetAtom(setGameStartedAtAtom);
+ const setCurrentTimestamp = useSetAtom(setCurrentTimestampAtom);
+ const setLatestGameStates = useSetAtom(setLatestGameStatesAtom);
+ const [ranking, setRanking] = useAtom(rankingAtom);
+
+ useTimer({ delay: 1000, startImmediately: true }, setCurrentTimestamp);
+
+ const playerA = game.main_players[0];
+ const playerB = game.main_players[1];
+
+ const playerProfileA = playerA
+ ? {
+ id: playerA.user_id,
+ displayName: playerA.display_name,
+ iconPath: playerA.icon_path ?? null,
+ }
+ : null;
+ const playerProfileB = playerB
+ ? {
+ id: playerB.user_id,
+ displayName: playerB.display_name,
+ iconPath: playerB.icon_path ?? null,
+ }
+ : null;
+
+ const [isDataPolling, setIsDataPolling] = useState(false);
+
+ useEffect(() => {
+ if (isDataPolling) {
+ return;
+ }
+ const timerId = setInterval(async () => {
+ if (isDataPolling) {
+ return;
+ }
+ setIsDataPolling(true);
+
+ try {
+ if (gameStateKind === "waiting") {
+ const { game: g } = await apiGetGame(apiAuthToken, game.game_id);
+ if (g.started_at != null) {
+ setGameStartedAt(g.started_at);
+ }
+ } else if (gameStateKind === "gaming") {
+ const { states } = await apiGetGameWatchLatestStates(
+ apiAuthToken,
+ game.game_id,
+ );
+ setLatestGameStates(states);
+ const { ranking } = await apiGetGameWatchRanking(
+ apiAuthToken,
+ game.game_id,
+ );
+ setRanking(ranking);
+ }
+ } catch (error) {
+ console.error(error);
+ } finally {
+ setIsDataPolling(false);
+ }
+ }, 1000);
+
+ return () => {
+ clearInterval(timerId);
+ };
+ }, [
+ isDataPolling,
+ apiAuthToken,
+ game.game_id,
+ gameStateKind,
+ setGameStartedAt,
+ setLatestGameStates,
+ setRanking,
+ ]);
+
+ if (gameStateKind === "waiting") {
+ return game.game_type === "1v1" ? (
+ <GolfWatchAppWaiting1v1
+ gameDisplayName={game.display_name}
+ playerProfileA={playerProfileA!}
+ playerProfileB={playerProfileB!}
+ />
+ ) : (
+ <GolfWatchAppWaitingMultiplayer gameDisplayName={game.display_name} />
+ );
+ } else if (gameStateKind === "starting") {
+ return <GolfWatchAppStarting gameDisplayName={game.display_name} />;
+ } else if (gameStateKind === "gaming" || gameStateKind === "finished") {
+ return game.game_type === "1v1" ? (
+ <GolfWatchAppGaming1v1
+ gameDisplayName={game.display_name}
+ playerProfileA={playerProfileA!}
+ playerProfileB={playerProfileB!}
+ problemTitle={game.problem.title}
+ problemDescription={game.problem.description}
+ gameResult={null /* TODO */}
+ />
+ ) : (
+ <GolfWatchAppGamingMultiplayer
+ gameDisplayName={game.display_name}
+ ranking={ranking}
+ problemTitle={game.problem.title}
+ problemDescription={game.problem.description}
+ gameResult={null /* TODO */}
+ />
+ );
+ } else {
+ return null;
+ }
+}
diff --git a/frontend/app/components/GolfWatchAppWithAudioPlayRequest.client.tsx b/frontend/app/components/GolfWatchAppWithAudioPlayRequest.client.tsx
deleted file mode 100644
index ce5a59c..0000000
--- a/frontend/app/components/GolfWatchAppWithAudioPlayRequest.client.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import { useAtom } from "jotai";
-import { AudioController } from "../.client/audio/AudioController";
-import { audioControllerAtom } from "../states/watch";
-import GolfWatchApp, { type Props } from "./GolfWatchApp.client";
-import SubmitButton from "./SubmitButton";
-
-export default function GolfWatchAppWithAudioPlayRequest({
- game,
- sockToken,
-}: Omit<Props, "audioController">) {
- const [audioController, setAudioController] = useAtom(audioControllerAtom);
- const audioPlayPermitted = audioController !== null;
-
- if (audioPlayPermitted) {
- return <GolfWatchApp game={game} sockToken={sockToken} />;
- } else {
- return (
- <div className="min-h-screen bg-gray-100 flex items-center justify-center">
- <div className="text-center">
- <SubmitButton
- onClick={async () => {
- const audioController = new AudioController();
- await audioController.loadAll();
- await audioController.playDummySoundEffect();
- setAudioController(audioController);
- }}
- >
- 開始
- </SubmitButton>
- </div>
- </div>
- );
- }
-}
diff --git a/frontend/app/components/GolfWatchApps/GolfWatchAppConnecting.tsx b/frontend/app/components/GolfWatchApps/GolfWatchAppConnecting.tsx
deleted file mode 100644
index 07a1be8..0000000
--- a/frontend/app/components/GolfWatchApps/GolfWatchAppConnecting.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-export default function GolfWatchAppConnecting() {
- return (
- <div className="min-h-screen bg-gray-100 flex items-center justify-center">
- <div className="text-center">
- <div className="text-6xl font-bold text-black">接続中...</div>
- </div>
- </div>
- );
-}
diff --git a/frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx b/frontend/app/components/GolfWatchApps/GolfWatchAppGaming1v1.tsx
index 2907f5a..033186c 100644
--- a/frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx
+++ b/frontend/app/components/GolfWatchApps/GolfWatchAppGaming1v1.tsx
@@ -1,12 +1,7 @@
import { useAtomValue } from "jotai";
import {
- codeAAtom,
- codeBAtom,
gamingLeftTimeSecondsAtom,
- scoreAAtom,
- scoreBAtom,
- submitResultAAtom,
- submitResultBAtom,
+ latestGameStatesAtom,
} from "../../states/watch";
import type { PlayerProfile } from "../../types/PlayerProfile";
import BorderedContainer from "../BorderedContainer";
@@ -24,7 +19,7 @@ type Props = {
gameResult: "winA" | "winB" | "draw" | null;
};
-export default function GolfWatchAppGaming({
+export default function GolfWatchAppGaming1v1({
gameDisplayName,
playerProfileA,
playerProfileB,
@@ -33,12 +28,16 @@ export default function GolfWatchAppGaming({
gameResult,
}: Props) {
const leftTimeSeconds = useAtomValue(gamingLeftTimeSecondsAtom)!;
- const codeA = useAtomValue(codeAAtom);
- const codeB = useAtomValue(codeBAtom);
- const scoreA = useAtomValue(scoreAAtom);
- const scoreB = useAtomValue(scoreBAtom);
- const submitResultA = useAtomValue(submitResultAAtom);
- const submitResultB = useAtomValue(submitResultBAtom);
+ const latestGameStates = useAtomValue(latestGameStatesAtom);
+
+ const stateA = latestGameStates[playerProfileA.id];
+ const codeA = stateA?.code ?? "";
+ const scoreA = stateA?.score ?? null;
+ const statusA = stateA?.status ?? "none";
+ const stateB = latestGameStates[playerProfileB.id];
+ const codeB = stateB?.code ?? "";
+ const scoreB = stateB?.score ?? null;
+ const statusB = stateB?.status ?? "none";
const leftTime = (() => {
const m = Math.floor(leftTimeSeconds / 60);
@@ -52,7 +51,7 @@ export default function GolfWatchAppGaming({
: gameResult === "winB"
? "bg-purple-400"
: "bg-pink-500"
- : "bg-iosdc-japan";
+ : "bg-sky-600";
return (
<div className="min-h-screen bg-gray-100 flex flex-col">
@@ -109,11 +108,11 @@ export default function GolfWatchAppGaming({
bgB="bg-purple-400"
/>
<div className="grow grid grid-cols-3 p-4 gap-4">
- <CodeBlock code={codeA} language="swift" />
+ <CodeBlock code={codeA} language="php" />
<div className="flex flex-col gap-4">
<div className="grid grid-cols-2 gap-4">
- <SubmitResult result={submitResultA} />
- <SubmitResult result={submitResultB} />
+ <SubmitResult status={statusA} />
+ <SubmitResult status={statusB} />
</div>
<div>
<div className="mb-2 text-center text-xl font-bold">
@@ -122,7 +121,7 @@ export default function GolfWatchAppGaming({
<BorderedContainer>{problemDescription}</BorderedContainer>
</div>
</div>
- <CodeBlock code={codeB} language="swift" />
+ <CodeBlock code={codeB} language="php" />
</div>
</div>
);
diff --git a/frontend/app/components/GolfWatchApps/GolfWatchAppGamingMultiplayer.tsx b/frontend/app/components/GolfWatchApps/GolfWatchAppGamingMultiplayer.tsx
new file mode 100644
index 0000000..b6d2ac3
--- /dev/null
+++ b/frontend/app/components/GolfWatchApps/GolfWatchAppGamingMultiplayer.tsx
@@ -0,0 +1,102 @@
+import { useAtomValue } from "jotai";
+import type { components } from "../../api/schema";
+import { gamingLeftTimeSecondsAtom } from "../../states/watch";
+import BorderedContainer from "../BorderedContainer";
+
+type RankingEntry = components["schemas"]["RankingEntry"];
+
+type Props = {
+ gameDisplayName: string;
+ ranking: RankingEntry[];
+ problemTitle: string;
+ problemDescription: string;
+ gameResult: "winA" | "winB" | "draw" | null;
+};
+
+export default function GolfWatchAppGamingMultiplayer({
+ gameDisplayName,
+ ranking,
+ problemTitle,
+ problemDescription,
+ gameResult,
+}: Props) {
+ const leftTimeSeconds = useAtomValue(gamingLeftTimeSecondsAtom)!;
+
+ const leftTime = (() => {
+ const m = Math.floor(leftTimeSeconds / 60);
+ const s = leftTimeSeconds % 60;
+ return `${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`;
+ })();
+
+ const topBg = gameResult
+ ? gameResult === "winA"
+ ? "bg-orange-400"
+ : gameResult === "winB"
+ ? "bg-purple-400"
+ : "bg-pink-500"
+ : "bg-sky-600";
+
+ return (
+ <div className="min-h-screen bg-gray-100 flex flex-col">
+ <div className={`text-white ${topBg} grid grid-cols-3 px-4 py-2`}>
+ <div className="font-bold flex justify-between my-auto"></div>
+ <div className="font-bold text-center">
+ <div className="text-gray-100">{gameDisplayName}</div>
+ <div className="text-3xl">{leftTime}</div>
+ </div>
+ <div className="font-bold flex justify-between my-auto"></div>
+ </div>
+ <div className="grow grid grid-cols-2 p-4 gap-4">
+ <div className="flex flex-col gap-4">
+ <div>
+ <div className="mb-2 text-center text-xl font-bold">
+ {problemTitle}
+ </div>
+ <BorderedContainer>{problemDescription}</BorderedContainer>
+ </div>
+ </div>
+ <div>
+ <table className="min-w-full divide-y divide-gray-200">
+ <thead className="bg-gray-50">
+ <tr>
+ <th
+ scope="col"
+ className="px-6 py-3 text-left font-medium text-gray-800 uppercase tracking-wider"
+ >
+ 順位
+ </th>
+ <th
+ scope="col"
+ className="px-6 py-3 text-left font-medium text-gray-800 uppercase tracking-wider"
+ >
+ 名前
+ </th>
+ <th
+ scope="col"
+ className="px-6 py-3 text-left font-medium text-gray-800 uppercase tracking-wider"
+ >
+ スコア
+ </th>
+ </tr>
+ </thead>
+ <tbody className="bg-white divide-y divide-gray-200">
+ {ranking.map((entry, index) => (
+ <tr key={entry.player.user_id}>
+ <td className="px-6 py-4 whitespace-nowrap text-gray-900">
+ {index + 1}
+ </td>
+ <td className="px-6 py-4 whitespace-nowrap text-gray-900">
+ {entry.player.display_name}
+ </td>
+ <td className="px-6 py-4 whitespace-nowrap text-gray-900">
+ {entry.score}
+ </td>
+ </tr>
+ ))}
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </div>
+ );
+}
diff --git a/frontend/app/components/GolfWatchApps/GolfWatchAppStarting.tsx b/frontend/app/components/GolfWatchApps/GolfWatchAppStarting.tsx
index 684d2af..82e5334 100644
--- a/frontend/app/components/GolfWatchApps/GolfWatchAppStarting.tsx
+++ b/frontend/app/components/GolfWatchApps/GolfWatchAppStarting.tsx
@@ -10,7 +10,7 @@ export default function GolfWatchAppStarting({ gameDisplayName }: Props) {
return (
<div className="min-h-screen bg-gray-100 flex flex-col">
- <div className="text-white bg-iosdc-japan p-10 text-center">
+ <div className="text-white bg-sky-600 p-10 text-center">
<div className="text-4xl font-bold">{gameDisplayName}</div>
</div>
<div className="text-center text-black font-black text-10xl">
diff --git a/frontend/app/components/GolfWatchApps/GolfWatchAppWaiting.tsx b/frontend/app/components/GolfWatchApps/GolfWatchAppWaiting1v1.tsx
index 0e964e3..fb315ff 100644
--- a/frontend/app/components/GolfWatchApps/GolfWatchAppWaiting.tsx
+++ b/frontend/app/components/GolfWatchApps/GolfWatchAppWaiting1v1.tsx
@@ -7,14 +7,14 @@ type Props = {
playerProfileB: PlayerProfile;
};
-export default function GolfWatchAppWaiting({
+export default function GolfWatchAppWaiting1v1({
gameDisplayName,
playerProfileA,
playerProfileB,
}: Props) {
return (
<div className="min-h-screen bg-gray-100 flex flex-col font-bold text-center">
- <div className="text-white bg-iosdc-japan p-10">
+ <div className="text-white bg-sky-600 p-10">
<div className="text-4xl">{gameDisplayName}</div>
</div>
<div className="grow grid grid-cols-3 gap-10 mx-auto text-black">
diff --git a/frontend/app/components/GolfWatchApps/GolfWatchAppWaitingMultiplayer.tsx b/frontend/app/components/GolfWatchApps/GolfWatchAppWaitingMultiplayer.tsx
new file mode 100644
index 0000000..13bcc10
--- /dev/null
+++ b/frontend/app/components/GolfWatchApps/GolfWatchAppWaitingMultiplayer.tsx
@@ -0,0 +1,15 @@
+type Props = {
+ gameDisplayName: string;
+};
+
+export default function GolfWatchAppWaitingMultiplayer({
+ gameDisplayName,
+}: Props) {
+ return (
+ <div className="min-h-screen bg-gray-100 flex flex-col font-bold text-center">
+ <div className="text-white bg-sky-600 p-10">
+ <div className="text-4xl">{gameDisplayName}</div>
+ </div>
+ </div>
+ );
+}
diff --git a/frontend/app/components/InputText.tsx b/frontend/app/components/InputText.tsx
index 3f2c526..ed68206 100644
--- a/frontend/app/components/InputText.tsx
+++ b/frontend/app/components/InputText.tsx
@@ -6,7 +6,7 @@ export default function InputText(props: InputProps) {
return (
<input
{...props}
- className="p-2 block w-full border border-pink-600 rounded-md transition duration-300 focus:ring focus:ring-pink-400 focus:outline-none"
+ className="p-2 block w-full border border-sky-600 rounded-md transition duration-300 focus:ring focus:ring-sky-400 focus:outline-none"
/>
);
}
diff --git a/frontend/app/components/NavigateLink.tsx b/frontend/app/components/NavigateLink.tsx
index b749cea..02aae3e 100644
--- a/frontend/app/components/NavigateLink.tsx
+++ b/frontend/app/components/NavigateLink.tsx
@@ -4,7 +4,7 @@ export default function NavigateLink(props: LinkProps) {
return (
<Link
{...props}
- className="text-lg text-white bg-pink-600 px-4 py-2 rounded transition duration-300 hover:bg-pink-500 focus:ring focus:ring-pink-400 focus:outline-none"
+ className="text-lg text-white bg-sky-600 px-4 py-2 border-2 border-sky-50 rounded transition duration-300 hover:bg-sky-500 focus:ring focus:ring-sky-400 focus:outline-none"
/>
);
}
diff --git a/frontend/app/components/SubmitButton.tsx b/frontend/app/components/SubmitButton.tsx
index 1400a7b..643b3f5 100644
--- a/frontend/app/components/SubmitButton.tsx
+++ b/frontend/app/components/SubmitButton.tsx
@@ -6,7 +6,7 @@ export default function SubmitButton(props: ButtonProps) {
return (
<button
{...props}
- className="text-lg text-white bg-pink-600 px-4 py-2 rounded transition duration-300 hover:bg-pink-500 focus:ring focus:ring-pink-400 focus:outline-none"
+ className="text-lg text-white bg-sky-600 px-4 py-2 rounded transition duration-300 hover:bg-sky-500 focus:ring focus:ring-sky-400 focus:outline-none"
/>
);
}
diff --git a/frontend/app/components/SubmitStatusLabel.tsx b/frontend/app/components/SubmitStatusLabel.tsx
index d1dc89c..8384e95 100644
--- a/frontend/app/components/SubmitStatusLabel.tsx
+++ b/frontend/app/components/SubmitStatusLabel.tsx
@@ -1,12 +1,12 @@
-import type { SubmitResultStatus } from "../types/SubmitResult";
+import type { components } from "../api/schema";
type Props = {
- status: SubmitResultStatus;
+ status: components["schemas"]["ExecutionStatus"];
};
export default function SubmitStatusLabel({ status }: Props) {
switch (status) {
- case "waiting_submission":
+ case "none":
return "提出待ち";
case "running":
return "実行中...";
@@ -16,8 +16,6 @@ export default function SubmitStatusLabel({ status }: Props) {
return "テスト失敗";
case "timeout":
return "時間切れ";
- case "compile_error":
- return "コンパイルエラー";
case "runtime_error":
return "実行時エラー";
case "internal_error":
diff --git a/frontend/app/components/UserIcon.tsx b/frontend/app/components/UserIcon.tsx
index 656c170..e14a571 100644
--- a/frontend/app/components/UserIcon.tsx
+++ b/frontend/app/components/UserIcon.tsx
@@ -9,8 +9,8 @@ export default function UserIcon({ iconPath, displayName, className }: Props) {
<img
src={
process.env.NODE_ENV === "development"
- ? `http://localhost:8002/iosdc-japan/2024/code-battle${iconPath}`
- : `/iosdc-japan/2024/code-battle${iconPath}`
+ ? `http://localhost:8003/phperkaigi/2025/code-battle${iconPath}`
+ : `/phperkaigi/2025/code-battle${iconPath}`
}
alt={`${displayName} のアイコン`}
className={`rounded-full border-4 border-white ${className}`}