aboutsummaryrefslogtreecommitdiffhomepage
path: root/frontend/app/components
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/app/components')
-rw-r--r--frontend/app/components/BorderedContainer.test.tsx38
-rw-r--r--frontend/app/components/BorderedContainer.tsx18
-rw-r--r--frontend/app/components/BorderedContainerWithCaption.test.tsx54
-rw-r--r--frontend/app/components/BorderedContainerWithCaption.tsx24
-rw-r--r--frontend/app/components/FoldableBorderedContainerWithCaption.test.tsx82
-rw-r--r--frontend/app/components/FoldableBorderedContainerWithCaption.tsx64
-rw-r--r--frontend/app/components/Gaming/CodeBlock.tsx94
-rw-r--r--frontend/app/components/Gaming/CodePopover.tsx62
-rw-r--r--frontend/app/components/Gaming/DataTable.test.tsx104
-rw-r--r--frontend/app/components/Gaming/DataTable.tsx64
-rw-r--r--frontend/app/components/Gaming/ExecStatusIndicatorIcon.tsx72
-rw-r--r--frontend/app/components/Gaming/InlineCode.tsx12
-rw-r--r--frontend/app/components/Gaming/LeftTime.test.tsx58
-rw-r--r--frontend/app/components/Gaming/LeftTime.tsx38
-rw-r--r--frontend/app/components/Gaming/ProblemColumn.tsx34
-rw-r--r--frontend/app/components/Gaming/ProblemColumnContent.tsx146
-rw-r--r--frontend/app/components/Gaming/RankingTable.tsx70
-rw-r--r--frontend/app/components/Gaming/Score.tsx48
-rw-r--r--frontend/app/components/Gaming/ScoreBar.tsx48
-rw-r--r--frontend/app/components/GolfPlayApp.tsx300
-rw-r--r--frontend/app/components/GolfPlayApps/GolfPlayAppGaming.test.tsx168
-rw-r--r--frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx272
-rw-r--r--frontend/app/components/GolfPlayApps/GolfPlayAppLoading.tsx14
-rw-r--r--frontend/app/components/GolfPlayApps/GolfPlayAppStarting.tsx24
-rw-r--r--frontend/app/components/GolfPlayApps/GolfPlayAppWaiting.tsx28
-rw-r--r--frontend/app/components/GolfWatchApp.tsx236
-rw-r--r--frontend/app/components/GolfWatchApps/GolfWatchAppGaming1v1.tsx264
-rw-r--r--frontend/app/components/GolfWatchApps/GolfWatchAppGamingMultiplayer.tsx68
-rw-r--r--frontend/app/components/GolfWatchApps/GolfWatchAppLoading.tsx14
-rw-r--r--frontend/app/components/GolfWatchApps/GolfWatchAppStarting.tsx24
-rw-r--r--frontend/app/components/GolfWatchApps/GolfWatchAppWaiting1v1.tsx52
-rw-r--r--frontend/app/components/GolfWatchApps/GolfWatchAppWaitingMultiplayer.tsx18
-rw-r--r--frontend/app/components/InputText.test.tsx42
-rw-r--r--frontend/app/components/InputText.tsx12
-rw-r--r--frontend/app/components/NavigateLink.tsx24
-rw-r--r--frontend/app/components/PlayerNameAndIcon.test.tsx50
-rw-r--r--frontend/app/components/PlayerNameAndIcon.tsx26
-rw-r--r--frontend/app/components/ProtectedRoute.tsx20
-rw-r--r--frontend/app/components/PublicOnlyRoute.tsx20
-rw-r--r--frontend/app/components/SubmitButton.test.tsx50
-rw-r--r--frontend/app/components/SubmitButton.tsx12
-rw-r--r--frontend/app/components/SubmitStatusLabel.test.tsx66
-rw-r--r--frontend/app/components/SubmitStatusLabel.tsx38
-rw-r--r--frontend/app/components/ThreeColumnLayout.tsx12
-rw-r--r--frontend/app/components/TitledColumn.tsx18
-rw-r--r--frontend/app/components/TwoColumnLayout.tsx12
-rw-r--r--frontend/app/components/UserIcon.test.tsx94
-rw-r--r--frontend/app/components/UserIcon.tsx28
48 files changed, 1568 insertions, 1568 deletions
diff --git a/frontend/app/components/BorderedContainer.test.tsx b/frontend/app/components/BorderedContainer.test.tsx
index 22a0aec..2f60499 100644
--- a/frontend/app/components/BorderedContainer.test.tsx
+++ b/frontend/app/components/BorderedContainer.test.tsx
@@ -6,28 +6,28 @@ import { afterEach, describe, expect, test } from "vitest";
import BorderedContainer from "./BorderedContainer";
afterEach(() => {
- cleanup();
+ cleanup();
});
describe("BorderedContainer", () => {
- test("renders children", () => {
- render(<BorderedContainer>Hello World</BorderedContainer>);
- expect(screen.getByText("Hello World")).toBeDefined();
- });
+ test("renders children", () => {
+ render(<BorderedContainer>Hello World</BorderedContainer>);
+ expect(screen.getByText("Hello World")).toBeDefined();
+ });
- test("applies custom className", () => {
- render(
- <BorderedContainer className="custom-class">Content</BorderedContainer>,
- );
- const container = screen.getByText("Content").closest("div");
- expect(container?.className).toContain("custom-class");
- });
+ test("applies custom className", () => {
+ render(
+ <BorderedContainer className="custom-class">Content</BorderedContainer>,
+ );
+ const container = screen.getByText("Content").closest("div");
+ expect(container?.className).toContain("custom-class");
+ });
- test("has default border styling", () => {
- render(<BorderedContainer>Styled</BorderedContainer>);
- const container = screen.getByText("Styled").closest("div");
- expect(container?.className).toContain("border-2");
- expect(container?.className).toContain("border-brand-600");
- expect(container?.className).toContain("rounded-xl");
- });
+ test("has default border styling", () => {
+ render(<BorderedContainer>Styled</BorderedContainer>);
+ const container = screen.getByText("Styled").closest("div");
+ expect(container?.className).toContain("border-2");
+ expect(container?.className).toContain("border-brand-600");
+ expect(container?.className).toContain("rounded-xl");
+ });
});
diff --git a/frontend/app/components/BorderedContainer.tsx b/frontend/app/components/BorderedContainer.tsx
index a1bff35..ec6c00e 100644
--- a/frontend/app/components/BorderedContainer.tsx
+++ b/frontend/app/components/BorderedContainer.tsx
@@ -1,16 +1,16 @@
import React from "react";
type Props = {
- children: React.ReactNode;
- className?: string;
+ children: React.ReactNode;
+ className?: string;
};
export default function BorderedContainer({ children, className }: Props) {
- return (
- <div
- className={`bg-white border-2 border-brand-600 rounded-xl p-4 ${className}`}
- >
- {children}
- </div>
- );
+ return (
+ <div
+ className={`bg-white border-2 border-brand-600 rounded-xl p-4 ${className}`}
+ >
+ {children}
+ </div>
+ );
}
diff --git a/frontend/app/components/BorderedContainerWithCaption.test.tsx b/frontend/app/components/BorderedContainerWithCaption.test.tsx
index 621dcb3..da6246b 100644
--- a/frontend/app/components/BorderedContainerWithCaption.test.tsx
+++ b/frontend/app/components/BorderedContainerWithCaption.test.tsx
@@ -6,36 +6,36 @@ import { afterEach, describe, expect, test } from "vitest";
import BorderedContainerWithCaption from "./BorderedContainerWithCaption";
afterEach(() => {
- cleanup();
+ cleanup();
});
describe("BorderedContainerWithCaption", () => {
- test("renders caption as heading", () => {
- render(
- <BorderedContainerWithCaption caption="Test Caption">
- Content
- </BorderedContainerWithCaption>,
- );
- expect(screen.getByText("Test Caption")).toBeDefined();
- expect(screen.getByText("Test Caption").tagName).toBe("H2");
- });
+ test("renders caption as heading", () => {
+ render(
+ <BorderedContainerWithCaption caption="Test Caption">
+ Content
+ </BorderedContainerWithCaption>,
+ );
+ expect(screen.getByText("Test Caption")).toBeDefined();
+ expect(screen.getByText("Test Caption").tagName).toBe("H2");
+ });
- test("renders children", () => {
- render(
- <BorderedContainerWithCaption caption="Title">
- Child Content
- </BorderedContainerWithCaption>,
- );
- expect(screen.getByText("Child Content")).toBeDefined();
- });
+ test("renders children", () => {
+ render(
+ <BorderedContainerWithCaption caption="Title">
+ Child Content
+ </BorderedContainerWithCaption>,
+ );
+ expect(screen.getByText("Child Content")).toBeDefined();
+ });
- test("wraps in bordered container with blue border", () => {
- render(
- <BorderedContainerWithCaption caption="Title">
- Content
- </BorderedContainerWithCaption>,
- );
- const container = screen.getByText("Content").closest(".border-2");
- expect(container).not.toBeNull();
- });
+ test("wraps in bordered container with blue border", () => {
+ render(
+ <BorderedContainerWithCaption caption="Title">
+ Content
+ </BorderedContainerWithCaption>,
+ );
+ const container = screen.getByText("Content").closest(".border-2");
+ expect(container).not.toBeNull();
+ });
});
diff --git a/frontend/app/components/BorderedContainerWithCaption.tsx b/frontend/app/components/BorderedContainerWithCaption.tsx
index 5446ddc..4cee1c9 100644
--- a/frontend/app/components/BorderedContainerWithCaption.tsx
+++ b/frontend/app/components/BorderedContainerWithCaption.tsx
@@ -2,20 +2,20 @@ import React from "react";
import BorderedContainer from "./BorderedContainer";
type Props = {
- caption: string;
- children: React.ReactNode;
+ caption: string;
+ children: React.ReactNode;
};
export default function BorderedContainerWithCaption({
- caption,
- children,
+ caption,
+ children,
}: Props) {
- return (
- <BorderedContainer>
- <div className="flex flex-col gap-4">
- <h2 className="text-center text-lg font-semibold">{caption}</h2>
- {children}
- </div>
- </BorderedContainer>
- );
+ return (
+ <BorderedContainer>
+ <div className="flex flex-col gap-4">
+ <h2 className="text-center text-lg font-semibold">{caption}</h2>
+ {children}
+ </div>
+ </BorderedContainer>
+ );
}
diff --git a/frontend/app/components/FoldableBorderedContainerWithCaption.test.tsx b/frontend/app/components/FoldableBorderedContainerWithCaption.test.tsx
index a4434ff..bfbb496 100644
--- a/frontend/app/components/FoldableBorderedContainerWithCaption.test.tsx
+++ b/frontend/app/components/FoldableBorderedContainerWithCaption.test.tsx
@@ -6,51 +6,51 @@ import { afterEach, describe, expect, test } from "vitest";
import FoldableBorderedContainerWithCaption from "./FoldableBorderedContainerWithCaption";
afterEach(() => {
- cleanup();
+ cleanup();
});
describe("FoldableBorderedContainerWithCaption", () => {
- test("renders caption", () => {
- render(
- <FoldableBorderedContainerWithCaption caption="Foldable Title">
- Content
- </FoldableBorderedContainerWithCaption>,
- );
- expect(screen.getByText("Foldable Title")).toBeDefined();
- });
+ test("renders caption", () => {
+ render(
+ <FoldableBorderedContainerWithCaption caption="Foldable Title">
+ Content
+ </FoldableBorderedContainerWithCaption>,
+ );
+ expect(screen.getByText("Foldable Title")).toBeDefined();
+ });
- test("shows children by default (open state)", () => {
- render(
- <FoldableBorderedContainerWithCaption caption="Title">
- <div data-testid="child">Visible</div>
- </FoldableBorderedContainerWithCaption>,
- );
- const child = screen.getByTestId("child");
- expect(child.parentElement?.className).not.toContain("hidden");
- });
+ test("shows children by default (open state)", () => {
+ render(
+ <FoldableBorderedContainerWithCaption caption="Title">
+ <div data-testid="child">Visible</div>
+ </FoldableBorderedContainerWithCaption>,
+ );
+ const child = screen.getByTestId("child");
+ expect(child.parentElement?.className).not.toContain("hidden");
+ });
- test("hides children when toggle button is clicked", () => {
- render(
- <FoldableBorderedContainerWithCaption caption="Title">
- <div data-testid="child">Content</div>
- </FoldableBorderedContainerWithCaption>,
- );
- const toggleButton = screen.getByRole("button");
- fireEvent.click(toggleButton);
- const child = screen.getByTestId("child");
- expect(child.parentElement?.className).toContain("hidden");
- });
+ test("hides children when toggle button is clicked", () => {
+ render(
+ <FoldableBorderedContainerWithCaption caption="Title">
+ <div data-testid="child">Content</div>
+ </FoldableBorderedContainerWithCaption>,
+ );
+ const toggleButton = screen.getByRole("button");
+ fireEvent.click(toggleButton);
+ const child = screen.getByTestId("child");
+ expect(child.parentElement?.className).toContain("hidden");
+ });
- test("shows children again when toggle button is clicked twice", () => {
- render(
- <FoldableBorderedContainerWithCaption caption="Title">
- <div data-testid="child">Content</div>
- </FoldableBorderedContainerWithCaption>,
- );
- const toggleButton = screen.getByRole("button");
- fireEvent.click(toggleButton);
- fireEvent.click(toggleButton);
- const child = screen.getByTestId("child");
- expect(child.parentElement?.className).not.toContain("hidden");
- });
+ test("shows children again when toggle button is clicked twice", () => {
+ render(
+ <FoldableBorderedContainerWithCaption caption="Title">
+ <div data-testid="child">Content</div>
+ </FoldableBorderedContainerWithCaption>,
+ );
+ const toggleButton = screen.getByRole("button");
+ fireEvent.click(toggleButton);
+ fireEvent.click(toggleButton);
+ const child = screen.getByTestId("child");
+ expect(child.parentElement?.className).not.toContain("hidden");
+ });
});
diff --git a/frontend/app/components/FoldableBorderedContainerWithCaption.tsx b/frontend/app/components/FoldableBorderedContainerWithCaption.tsx
index 2d21b61..49b1d2e 100644
--- a/frontend/app/components/FoldableBorderedContainerWithCaption.tsx
+++ b/frontend/app/components/FoldableBorderedContainerWithCaption.tsx
@@ -4,42 +4,42 @@ import React, { useState } from "react";
import BorderedContainer from "./BorderedContainer";
type Props = {
- caption: string;
- children: React.ReactNode;
+ caption: string;
+ children: React.ReactNode;
};
export default function FoldableBorderedContainerWithCaption({
- caption,
- children,
+ caption,
+ children,
}: Props) {
- const [isOpen, setIsOpen] = useState(true);
+ const [isOpen, setIsOpen] = useState(true);
- const handleToggle = () => {
- setIsOpen((prev) => !prev);
- };
+ const handleToggle = () => {
+ setIsOpen((prev) => !prev);
+ };
- return (
- <BorderedContainer>
- <div className="flex flex-col gap-4">
- <div className="flex items-center">
- <div className="flex-1 text-center">
- <h2 className="text-lg font-semibold">{caption}</h2>
- </div>
- <div className="flex-shrink-0">
- <button
- onClick={handleToggle}
- className="p-1 bg-gray-50 border-1 border-gray-300 rounded-sm"
- >
- <FontAwesomeIcon
- icon={isOpen ? faChevronUp : faChevronDown}
- fixedWidth
- className="text-gray-500"
- />
- </button>
- </div>
- </div>
- <div className={isOpen ? "" : "hidden"}>{children}</div>
- </div>
- </BorderedContainer>
- );
+ return (
+ <BorderedContainer>
+ <div className="flex flex-col gap-4">
+ <div className="flex items-center">
+ <div className="flex-1 text-center">
+ <h2 className="text-lg font-semibold">{caption}</h2>
+ </div>
+ <div className="flex-shrink-0">
+ <button
+ onClick={handleToggle}
+ className="p-1 bg-gray-50 border-1 border-gray-300 rounded-sm"
+ >
+ <FontAwesomeIcon
+ icon={isOpen ? faChevronUp : faChevronDown}
+ fixedWidth
+ className="text-gray-500"
+ />
+ </button>
+ </div>
+ </div>
+ <div className={isOpen ? "" : "hidden"}>{children}</div>
+ </div>
+ </BorderedContainer>
+ );
}
diff --git a/frontend/app/components/Gaming/CodeBlock.tsx b/frontend/app/components/Gaming/CodeBlock.tsx
index 2107f94..f048a38 100644
--- a/frontend/app/components/Gaming/CodeBlock.tsx
+++ b/frontend/app/components/Gaming/CodeBlock.tsx
@@ -4,60 +4,60 @@ import { JSX, useLayoutEffect, useState } from "react";
import { type BundledLanguage, highlight } from "../../highlight";
type Props = {
- code: string;
- language: BundledLanguage;
+ code: string;
+ language: BundledLanguage;
};
function Plaintext({ code }: { code: string }) {
- const lines = code.split("\n");
- return (
- <pre>
- <code>
- {lines.map((line, i) => (
- <span key={i} className="line">
- {line}
- {i < lines.length - 1 ? "\n" : ""}
- </span>
- ))}
- </code>
- </pre>
- );
+ const lines = code.split("\n");
+ return (
+ <pre>
+ <code>
+ {lines.map((line, i) => (
+ <span key={i} className="line">
+ {line}
+ {i < lines.length - 1 ? "\n" : ""}
+ </span>
+ ))}
+ </code>
+ </pre>
+ );
}
export default function CodeBlock({ code, language }: Props) {
- const [nodes, setNodes] = useState<JSX.Element | null>(null);
- const [showCopied, setShowCopied] = useState(false);
+ const [nodes, setNodes] = useState<JSX.Element | null>(null);
+ const [showCopied, setShowCopied] = useState(false);
- useLayoutEffect(() => {
- highlight(code, language)
- .then(setNodes)
- .catch(() => setNodes(null));
- }, [code, language]);
+ useLayoutEffect(() => {
+ highlight(code, language)
+ .then(setNodes)
+ .catch(() => setNodes(null));
+ }, [code, language]);
- const handleCopy = () => {
- navigator.clipboard.writeText(code).then(() => {
- setShowCopied(true);
- setTimeout(() => setShowCopied(false), 3000);
- });
- };
+ const handleCopy = () => {
+ navigator.clipboard.writeText(code).then(() => {
+ setShowCopied(true);
+ setTimeout(() => setShowCopied(false), 3000);
+ });
+ };
- return (
- <div className="relative">
- {code !== "" && (
- <button
- onClick={handleCopy}
- className="absolute top-2 right-2 z-10 px-2 py-1 bg-white border border-gray-300 rounded shadow-md hover:bg-gray-100 transition-colors"
- title="コードをコピーする"
- >
- <FontAwesomeIcon icon={faCopy} className="text-gray-600" />
- {showCopied && (
- <span className="ml-1 text-xs text-brand-600">Copied!</span>
- )}
- </button>
- )}
- <div className="shiki h-full w-full p-2 pr-12 bg-white rounded-lg border border-gray-300 whitespace-pre-wrap break-words">
- {nodes ?? <Plaintext code={code} />}
- </div>
- </div>
- );
+ return (
+ <div className="relative">
+ {code !== "" && (
+ <button
+ onClick={handleCopy}
+ className="absolute top-2 right-2 z-10 px-2 py-1 bg-white border border-gray-300 rounded shadow-md hover:bg-gray-100 transition-colors"
+ title="コードをコピーする"
+ >
+ <FontAwesomeIcon icon={faCopy} className="text-gray-600" />
+ {showCopied && (
+ <span className="ml-1 text-xs text-brand-600">Copied!</span>
+ )}
+ </button>
+ )}
+ <div className="shiki h-full w-full p-2 pr-12 bg-white rounded-lg border border-gray-300 whitespace-pre-wrap break-words">
+ {nodes ?? <Plaintext code={code} />}
+ </div>
+ </div>
+ );
}
diff --git a/frontend/app/components/Gaming/CodePopover.tsx b/frontend/app/components/Gaming/CodePopover.tsx
index 91245df..c4065ed 100644
--- a/frontend/app/components/Gaming/CodePopover.tsx
+++ b/frontend/app/components/Gaming/CodePopover.tsx
@@ -7,39 +7,39 @@ import BorderedContainer from "../BorderedContainer";
import CodeBlock from "../Gaming/CodeBlock";
type Props = {
- code: string;
- language: SupportedLanguage;
+ code: string;
+ language: SupportedLanguage;
};
export default function CodePopover({ code, language }: Props) {
- const codeSize = calcCodeSize(code, language);
+ const codeSize = calcCodeSize(code, language);
- return (
- <Popover.Root>
- <Popover.Trigger>
- <FontAwesomeIcon icon={faCode} fixedWidth />
- </Popover.Trigger>
- <Popover.Portal>
- <Popover.Positioner>
- <Popover.Popup>
- <BorderedContainer className="grow flex flex-col gap-4">
- <div className="flex flex-row gap-2 items-center">
- <div className="grow font-semibold text-lg">
- コードサイズ: {codeSize}
- </div>
- <Popover.Close className="p-1 bg-gray-50 border-1 border-gray-300 rounded-sm">
- <FontAwesomeIcon
- icon={faXmark}
- fixedWidth
- className="text-gray-500"
- />
- </Popover.Close>
- </div>
- <CodeBlock code={code} language={language} />
- </BorderedContainer>
- </Popover.Popup>
- </Popover.Positioner>
- </Popover.Portal>
- </Popover.Root>
- );
+ return (
+ <Popover.Root>
+ <Popover.Trigger>
+ <FontAwesomeIcon icon={faCode} fixedWidth />
+ </Popover.Trigger>
+ <Popover.Portal>
+ <Popover.Positioner>
+ <Popover.Popup>
+ <BorderedContainer className="grow flex flex-col gap-4">
+ <div className="flex flex-row gap-2 items-center">
+ <div className="grow font-semibold text-lg">
+ コードサイズ: {codeSize}
+ </div>
+ <Popover.Close className="p-1 bg-gray-50 border-1 border-gray-300 rounded-sm">
+ <FontAwesomeIcon
+ icon={faXmark}
+ fixedWidth
+ className="text-gray-500"
+ />
+ </Popover.Close>
+ </div>
+ <CodeBlock code={code} language={language} />
+ </BorderedContainer>
+ </Popover.Popup>
+ </Popover.Positioner>
+ </Popover.Portal>
+ </Popover.Root>
+ );
}
diff --git a/frontend/app/components/Gaming/DataTable.test.tsx b/frontend/app/components/Gaming/DataTable.test.tsx
index 2a4446c..08c7336 100644
--- a/frontend/app/components/Gaming/DataTable.test.tsx
+++ b/frontend/app/components/Gaming/DataTable.test.tsx
@@ -6,65 +6,65 @@ import { afterEach, describe, expect, test } from "vitest";
import DataTable, { DataTableCell, formatUnixTimestamp } from "./DataTable";
afterEach(() => {
- cleanup();
+ cleanup();
});
describe("DataTable", () => {
- test("renders headers", () => {
- render(
- <DataTable headers={["A", "B", "C"]}>
- <tr>
- <DataTableCell>1</DataTableCell>
- <DataTableCell>2</DataTableCell>
- <DataTableCell>3</DataTableCell>
- </tr>
- </DataTable>,
- );
- expect(screen.getByText("A")).toBeDefined();
- expect(screen.getByText("B")).toBeDefined();
- expect(screen.getByText("C")).toBeDefined();
- });
+ test("renders headers", () => {
+ render(
+ <DataTable headers={["A", "B", "C"]}>
+ <tr>
+ <DataTableCell>1</DataTableCell>
+ <DataTableCell>2</DataTableCell>
+ <DataTableCell>3</DataTableCell>
+ </tr>
+ </DataTable>,
+ );
+ expect(screen.getByText("A")).toBeDefined();
+ expect(screen.getByText("B")).toBeDefined();
+ expect(screen.getByText("C")).toBeDefined();
+ });
- test("renders body cells", () => {
- render(
- <DataTable headers={["H"]}>
- <tr>
- <DataTableCell>cell content</DataTableCell>
- </tr>
- </DataTable>,
- );
- expect(screen.getByText("cell content")).toBeDefined();
- });
+ test("renders body cells", () => {
+ render(
+ <DataTable headers={["H"]}>
+ <tr>
+ <DataTableCell>cell content</DataTableCell>
+ </tr>
+ </DataTable>,
+ );
+ expect(screen.getByText("cell content")).toBeDefined();
+ });
- test("renders multiple rows", () => {
- render(
- <DataTable headers={["Name"]}>
- <tr>
- <DataTableCell>Alice</DataTableCell>
- </tr>
- <tr>
- <DataTableCell>Bob</DataTableCell>
- </tr>
- </DataTable>,
- );
- expect(screen.getByText("Alice")).toBeDefined();
- expect(screen.getByText("Bob")).toBeDefined();
- });
+ test("renders multiple rows", () => {
+ render(
+ <DataTable headers={["Name"]}>
+ <tr>
+ <DataTableCell>Alice</DataTableCell>
+ </tr>
+ <tr>
+ <DataTableCell>Bob</DataTableCell>
+ </tr>
+ </DataTable>,
+ );
+ expect(screen.getByText("Alice")).toBeDefined();
+ expect(screen.getByText("Bob")).toBeDefined();
+ });
});
describe("formatUnixTimestamp", () => {
- test("formats timestamp correctly", () => {
- // 2026-03-01 12:30 JST (UTC+9) = 2026-03-01 03:30 UTC
- const timestamp = Date.UTC(2026, 2, 1, 3, 30, 0) / 1000;
- const result = formatUnixTimestamp(timestamp);
- // Result depends on local timezone; just check the format pattern
- expect(result).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/);
- });
+ test("formats timestamp correctly", () => {
+ // 2026-03-01 12:30 JST (UTC+9) = 2026-03-01 03:30 UTC
+ const timestamp = Date.UTC(2026, 2, 1, 3, 30, 0) / 1000;
+ const result = formatUnixTimestamp(timestamp);
+ // Result depends on local timezone; just check the format pattern
+ expect(result).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/);
+ });
- test("pads single-digit months and days", () => {
- // Use a date where month and day are single digits
- const timestamp = Date.UTC(2026, 0, 5, 0, 0, 0) / 1000;
- const result = formatUnixTimestamp(timestamp);
- expect(result).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/);
- });
+ test("pads single-digit months and days", () => {
+ // Use a date where month and day are single digits
+ const timestamp = Date.UTC(2026, 0, 5, 0, 0, 0) / 1000;
+ const result = formatUnixTimestamp(timestamp);
+ expect(result).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/);
+ });
});
diff --git a/frontend/app/components/Gaming/DataTable.tsx b/frontend/app/components/Gaming/DataTable.tsx
index 098f4a2..f909b1e 100644
--- a/frontend/app/components/Gaming/DataTable.tsx
+++ b/frontend/app/components/Gaming/DataTable.tsx
@@ -1,45 +1,45 @@
import type React from "react";
type Props = {
- headers: React.ReactNode[];
- children: React.ReactNode;
+ headers: React.ReactNode[];
+ children: React.ReactNode;
};
export default function DataTable({ headers, children }: Props) {
- return (
- <div className="overflow-x-auto border-2 border-brand-600 rounded-xl">
- <table className="min-w-full divide-y divide-gray-400 border-collapse">
- <thead className="bg-gray-50">
- <tr>
- {headers.map((header, i) => (
- <th
- key={i}
- scope="col"
- className="px-6 py-3 text-left font-medium text-gray-800"
- >
- {header}
- </th>
- ))}
- </tr>
- </thead>
- <tbody className="bg-white divide-y divide-gray-300">{children}</tbody>
- </table>
- </div>
- );
+ return (
+ <div className="overflow-x-auto border-2 border-brand-600 rounded-xl">
+ <table className="min-w-full divide-y divide-gray-400 border-collapse">
+ <thead className="bg-gray-50">
+ <tr>
+ {headers.map((header, i) => (
+ <th
+ key={i}
+ scope="col"
+ className="px-6 py-3 text-left font-medium text-gray-800"
+ >
+ {header}
+ </th>
+ ))}
+ </tr>
+ </thead>
+ <tbody className="bg-white divide-y divide-gray-300">{children}</tbody>
+ </table>
+ </div>
+ );
}
export function DataTableCell({ children }: { children: React.ReactNode }) {
- return (
- <td className="px-6 py-4 whitespace-nowrap text-gray-900">{children}</td>
- );
+ return (
+ <td className="px-6 py-4 whitespace-nowrap text-gray-900">{children}</td>
+ );
}
export function formatUnixTimestamp(timestamp: number): string {
- const date = new Date(timestamp * 1000);
- const year = date.getFullYear();
- const month = (date.getMonth() + 1).toString().padStart(2, "0");
- const day = date.getDate().toString().padStart(2, "0");
- const hours = date.getHours().toString().padStart(2, "0");
- const minutes = date.getMinutes().toString().padStart(2, "0");
- return `${year}-${month}-${day} ${hours}:${minutes}`;
+ const date = new Date(timestamp * 1000);
+ const year = date.getFullYear();
+ const month = (date.getMonth() + 1).toString().padStart(2, "0");
+ const day = date.getDate().toString().padStart(2, "0");
+ const hours = date.getHours().toString().padStart(2, "0");
+ const minutes = date.getMinutes().toString().padStart(2, "0");
+ return `${year}-${month}-${day} ${hours}:${minutes}`;
}
diff --git a/frontend/app/components/Gaming/ExecStatusIndicatorIcon.tsx b/frontend/app/components/Gaming/ExecStatusIndicatorIcon.tsx
index e4260ec..2bf088b 100644
--- a/frontend/app/components/Gaming/ExecStatusIndicatorIcon.tsx
+++ b/frontend/app/components/Gaming/ExecStatusIndicatorIcon.tsx
@@ -1,46 +1,46 @@
import {
- faCircle,
- faCircleCheck,
- faCircleExclamation,
- faRotate,
+ faCircle,
+ faCircleCheck,
+ faCircleExclamation,
+ faRotate,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import type { components } from "../../api/schema";
type Props = {
- status: components["schemas"]["ExecutionStatus"];
+ status: components["schemas"]["ExecutionStatus"];
};
export default function ExecStatusIndicatorIcon({ status }: Props) {
- switch (status) {
- case "none":
- return (
- <FontAwesomeIcon icon={faCircle} fixedWidth className="text-gray-400" />
- );
- case "running":
- return (
- <FontAwesomeIcon
- icon={faRotate}
- spin
- fixedWidth
- className="text-gray-700"
- />
- );
- case "success":
- return (
- <FontAwesomeIcon
- icon={faCircleCheck}
- fixedWidth
- className="text-brand-500"
- />
- );
- default:
- return (
- <FontAwesomeIcon
- icon={faCircleExclamation}
- fixedWidth
- className="text-red-500"
- />
- );
- }
+ switch (status) {
+ case "none":
+ return (
+ <FontAwesomeIcon icon={faCircle} fixedWidth className="text-gray-400" />
+ );
+ case "running":
+ return (
+ <FontAwesomeIcon
+ icon={faRotate}
+ spin
+ fixedWidth
+ className="text-gray-700"
+ />
+ );
+ case "success":
+ return (
+ <FontAwesomeIcon
+ icon={faCircleCheck}
+ fixedWidth
+ className="text-brand-500"
+ />
+ );
+ default:
+ return (
+ <FontAwesomeIcon
+ icon={faCircleExclamation}
+ fixedWidth
+ className="text-red-500"
+ />
+ );
+ }
}
diff --git a/frontend/app/components/Gaming/InlineCode.tsx b/frontend/app/components/Gaming/InlineCode.tsx
index c90cad4..0b5e061 100644
--- a/frontend/app/components/Gaming/InlineCode.tsx
+++ b/frontend/app/components/Gaming/InlineCode.tsx
@@ -1,11 +1,11 @@
type Props = {
- code: string;
+ code: string;
};
export default function InlineCode({ code }: Props) {
- return (
- <code className="bg-gray-50 rounded-lg border border-gray-300 p-1">
- {code}
- </code>
- );
+ return (
+ <code className="bg-gray-50 rounded-lg border border-gray-300 p-1">
+ {code}
+ </code>
+ );
}
diff --git a/frontend/app/components/Gaming/LeftTime.test.tsx b/frontend/app/components/Gaming/LeftTime.test.tsx
index 742d8eb..28f2fc4 100644
--- a/frontend/app/components/Gaming/LeftTime.test.tsx
+++ b/frontend/app/components/Gaming/LeftTime.test.tsx
@@ -6,42 +6,42 @@ import { afterEach, describe, expect, test } from "vitest";
import LeftTime from "./LeftTime";
afterEach(() => {
- cleanup();
+ cleanup();
});
describe("LeftTime", () => {
- test("renders MM:SS format for short durations", () => {
- render(<LeftTime sec={65} />);
- expect(screen.getByText("01:05")).toBeDefined();
- });
+ test("renders MM:SS format for short durations", () => {
+ render(<LeftTime sec={65} />);
+ expect(screen.getByText("01:05")).toBeDefined();
+ });
- test("renders 00:00 for zero seconds", () => {
- render(<LeftTime sec={0} />);
- expect(screen.getByText("00:00")).toBeDefined();
- });
+ test("renders 00:00 for zero seconds", () => {
+ render(<LeftTime sec={0} />);
+ expect(screen.getByText("00:00")).toBeDefined();
+ });
- test("renders MM:SS with leading zeros", () => {
- render(<LeftTime sec={5} />);
- expect(screen.getByText("00:05")).toBeDefined();
- });
+ test("renders MM:SS with leading zeros", () => {
+ render(<LeftTime sec={5} />);
+ expect(screen.getByText("00:05")).toBeDefined();
+ });
- test("renders 59:59 for max MM:SS range", () => {
- render(<LeftTime sec={3599} />);
- expect(screen.getByText("59:59")).toBeDefined();
- });
+ test("renders 59:59 for max MM:SS range", () => {
+ render(<LeftTime sec={3599} />);
+ expect(screen.getByText("59:59")).toBeDefined();
+ });
- test("renders long format with hours", () => {
- render(<LeftTime sec={3661} />);
- expect(screen.getByText("1h 1m 1s")).toBeDefined();
- });
+ test("renders long format with hours", () => {
+ render(<LeftTime sec={3661} />);
+ expect(screen.getByText("1h 1m 1s")).toBeDefined();
+ });
- test("renders long format with days", () => {
- render(<LeftTime sec={90061} />);
- expect(screen.getByText("1d 1h 1m 1s")).toBeDefined();
- });
+ test("renders long format with days", () => {
+ render(<LeftTime sec={90061} />);
+ expect(screen.getByText("1d 1h 1m 1s")).toBeDefined();
+ });
- test("renders long format omitting zero day and minute", () => {
- render(<LeftTime sec={3605} />);
- expect(screen.getByText("1h 5s")).toBeDefined();
- });
+ test("renders long format omitting zero day and minute", () => {
+ render(<LeftTime sec={3605} />);
+ expect(screen.getByText("1h 5s")).toBeDefined();
+ });
});
diff --git a/frontend/app/components/Gaming/LeftTime.tsx b/frontend/app/components/Gaming/LeftTime.tsx
index 5013c76..a7678d6 100644
--- a/frontend/app/components/Gaming/LeftTime.tsx
+++ b/frontend/app/components/Gaming/LeftTime.tsx
@@ -1,26 +1,26 @@
type Props = {
- sec: number;
+ sec: number;
};
export default function LeftTime({ sec }: Props) {
- const s = sec % 60;
- const m = Math.floor(sec / 60) % 60;
- const h = Math.floor(sec / 3600) % 24;
- const d = Math.floor(sec / 86400);
+ const s = sec % 60;
+ const m = Math.floor(sec / 60) % 60;
+ const h = Math.floor(sec / 3600) % 24;
+ const d = Math.floor(sec / 86400);
- let leftTime = "";
- if (d > 0 || h > 0) {
- // 1d 2h 3m 4s
- leftTime = [
- d > 0 ? `${d}d` : "",
- h > 0 ? `${h}h` : "",
- m > 0 ? `${m}m` : "",
- `${s}s`,
- ].join(" ");
- } else {
- // 03:04
- leftTime = `${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`;
- }
+ let leftTime = "";
+ if (d > 0 || h > 0) {
+ // 1d 2h 3m 4s
+ leftTime = [
+ d > 0 ? `${d}d` : "",
+ h > 0 ? `${h}h` : "",
+ m > 0 ? `${m}m` : "",
+ `${s}s`,
+ ].join(" ");
+ } else {
+ // 03:04
+ leftTime = `${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`;
+ }
- return <div className="text-2xl md:text-3xl">{leftTime}</div>;
+ return <div className="text-2xl md:text-3xl">{leftTime}</div>;
}
diff --git a/frontend/app/components/Gaming/ProblemColumn.tsx b/frontend/app/components/Gaming/ProblemColumn.tsx
index a355ac4..40d0716 100644
--- a/frontend/app/components/Gaming/ProblemColumn.tsx
+++ b/frontend/app/components/Gaming/ProblemColumn.tsx
@@ -3,25 +3,25 @@ import TitledColumn from "../TitledColumn";
import ProblemColumnContent from "./ProblemColumnContent";
type Props = {
- title: string;
- description: string;
- language: SupportedLanguage;
- sampleCode: string;
+ title: string;
+ description: string;
+ language: SupportedLanguage;
+ sampleCode: string;
};
export default function ProblemColumn({
- title,
- description,
- language,
- sampleCode,
+ title,
+ description,
+ language,
+ sampleCode,
}: Props) {
- return (
- <TitledColumn title={title}>
- <ProblemColumnContent
- description={description}
- sampleCode={sampleCode}
- language={language}
- />
- </TitledColumn>
- );
+ return (
+ <TitledColumn title={title}>
+ <ProblemColumnContent
+ description={description}
+ sampleCode={sampleCode}
+ language={language}
+ />
+ </TitledColumn>
+ );
}
diff --git a/frontend/app/components/Gaming/ProblemColumnContent.tsx b/frontend/app/components/Gaming/ProblemColumnContent.tsx
index 1a7cb36..bc6b43a 100644
--- a/frontend/app/components/Gaming/ProblemColumnContent.tsx
+++ b/frontend/app/components/Gaming/ProblemColumnContent.tsx
@@ -4,87 +4,87 @@ import CodeBlock from "./CodeBlock";
import InlineCode from "./InlineCode";
function PhpNotice() {
- return (
- <FoldableBorderedContainerWithCaption caption="スコア計算・PHP 環境">
- <div className="text-gray-700 flex flex-col gap-2">
- <p>
- スコアはコード中の全 ASCII
- 空白文字を除去した後のバイト数です。また、先頭や末尾に置かれた PHP
- タグ (<InlineCode code="<?php" />、<InlineCode code="<?" />、
- <InlineCode code="?>" />) はカウントされません。
- </p>
- <p>
- 同じスコアを出した場合、より提出が早かったプレイヤーの勝ちとなります。
- </p>
- <p>
- この環境の PHP バージョンは{" "}
- <strong className="font-bold">8.5.3</strong> です。 mbstring
- を除くほとんどの拡張は無効化されています。
- また、ファイルやネットワークアクセスはできません。
- </p>
- <p>
- テストの成否は、標準出力へ出力された文字列を比較して判定されます。
- 末尾の改行はあってもなくても構いません。
- 標準エラー出力の内容は無視されますが、fatal error
- 等で実行が中断された場合は失敗扱いとなります。
- </p>
- <p>
- なお、
- <InlineCode code="error_reporting" /> は{" "}
- <InlineCode code="E_ALL &amp; ~E_WARNING &amp; ~E_NOTICE &amp; ~E_DEPRECATED" />{" "}
- に設定されています。
- </p>
- </div>
- </FoldableBorderedContainerWithCaption>
- );
+ return (
+ <FoldableBorderedContainerWithCaption caption="スコア計算・PHP 環境">
+ <div className="text-gray-700 flex flex-col gap-2">
+ <p>
+ スコアはコード中の全 ASCII
+ 空白文字を除去した後のバイト数です。また、先頭や末尾に置かれた PHP
+ タグ (<InlineCode code="<?php" />、<InlineCode code="<?" />、
+ <InlineCode code="?>" />) はカウントされません。
+ </p>
+ <p>
+ 同じスコアを出した場合、より提出が早かったプレイヤーの勝ちとなります。
+ </p>
+ <p>
+ この環境の PHP バージョンは{" "}
+ <strong className="font-bold">8.5.3</strong> です。 mbstring
+ を除くほとんどの拡張は無効化されています。
+ また、ファイルやネットワークアクセスはできません。
+ </p>
+ <p>
+ テストの成否は、標準出力へ出力された文字列を比較して判定されます。
+ 末尾の改行はあってもなくても構いません。
+ 標準エラー出力の内容は無視されますが、fatal error
+ 等で実行が中断された場合は失敗扱いとなります。
+ </p>
+ <p>
+ なお、
+ <InlineCode code="error_reporting" /> は{" "}
+ <InlineCode code="E_ALL &amp; ~E_WARNING &amp; ~E_NOTICE &amp; ~E_DEPRECATED" />{" "}
+ に設定されています。
+ </p>
+ </div>
+ </FoldableBorderedContainerWithCaption>
+ );
}
function SwiftNotice() {
- return (
- <FoldableBorderedContainerWithCaption caption="スコア計算・Swift 環境">
- <div className="text-gray-700 flex flex-col gap-2">
- <p>スコアはコード中の全 ASCII 空白文字を除去した後のバイト数です。</p>
- <p>
- 同じスコアを出した場合、より提出が早かったプレイヤーの勝ちとなります。
- </p>
- <p>
- この環境の Swift バージョンは{" "}
- <strong className="font-bold">6.1.2</strong> です。
- ファイルアクセスやネットワークアクセスはできません。
- </p>
- <p>
- テストの成否は、標準出力へ出力された文字列を比較して判定されます。
- 末尾の改行はあってもなくても構いません。
- 標準エラー出力の内容は無視されますが、fatal error
- 等で実行が中断された場合は失敗扱いとなります。
- </p>
- </div>
- </FoldableBorderedContainerWithCaption>
- );
+ return (
+ <FoldableBorderedContainerWithCaption caption="スコア計算・Swift 環境">
+ <div className="text-gray-700 flex flex-col gap-2">
+ <p>スコアはコード中の全 ASCII 空白文字を除去した後のバイト数です。</p>
+ <p>
+ 同じスコアを出した場合、より提出が早かったプレイヤーの勝ちとなります。
+ </p>
+ <p>
+ この環境の Swift バージョンは{" "}
+ <strong className="font-bold">6.1.2</strong> です。
+ ファイルアクセスやネットワークアクセスはできません。
+ </p>
+ <p>
+ テストの成否は、標準出力へ出力された文字列を比較して判定されます。
+ 末尾の改行はあってもなくても構いません。
+ 標準エラー出力の内容は無視されますが、fatal error
+ 等で実行が中断された場合は失敗扱いとなります。
+ </p>
+ </div>
+ </FoldableBorderedContainerWithCaption>
+ );
}
type Props = {
- description: string;
- language: SupportedLanguage;
- sampleCode: string;
+ description: string;
+ language: SupportedLanguage;
+ sampleCode: string;
};
export default function ProblemColumnContent({
- description,
- language,
- sampleCode,
+ description,
+ language,
+ sampleCode,
}: Props) {
- return (
- <>
- <FoldableBorderedContainerWithCaption caption="問題">
- <pre className="text-gray-700 whitespace-pre-wrap break-words">
- {description}
- </pre>
- </FoldableBorderedContainerWithCaption>
- <FoldableBorderedContainerWithCaption caption="サンプルコード">
- <CodeBlock code={sampleCode} language={language} />
- </FoldableBorderedContainerWithCaption>
- {language === "php" ? <PhpNotice /> : <SwiftNotice />}
- </>
- );
+ return (
+ <>
+ <FoldableBorderedContainerWithCaption caption="問題">
+ <pre className="text-gray-700 whitespace-pre-wrap break-words">
+ {description}
+ </pre>
+ </FoldableBorderedContainerWithCaption>
+ <FoldableBorderedContainerWithCaption caption="サンプルコード">
+ <CodeBlock code={sampleCode} language={language} />
+ </FoldableBorderedContainerWithCaption>
+ {language === "php" ? <PhpNotice /> : <SwiftNotice />}
+ </>
+ );
}
diff --git a/frontend/app/components/Gaming/RankingTable.tsx b/frontend/app/components/Gaming/RankingTable.tsx
index 60f4808..b0a6116 100644
--- a/frontend/app/components/Gaming/RankingTable.tsx
+++ b/frontend/app/components/Gaming/RankingTable.tsx
@@ -5,43 +5,43 @@ import CodePopover from "./CodePopover";
import DataTable, { DataTableCell, formatUnixTimestamp } from "./DataTable";
type Props = {
- problemLanguage: SupportedLanguage;
+ problemLanguage: SupportedLanguage;
};
export default function RankingTable({ problemLanguage }: Props) {
- const ranking = useAtomValue(rankingAtom);
- const showCode = ranking.some((entry) => entry.code != null);
+ const ranking = useAtomValue(rankingAtom);
+ const showCode = ranking.some((entry) => entry.code != null);
- return (
- <DataTable
- headers={[
- "順位",
- "プレイヤー",
- "スコア",
- "提出時刻",
- ...(showCode ? ["コード"] : []),
- ]}
- >
- {ranking.map((entry, index) => (
- <tr key={entry.player.user_id}>
- <DataTableCell>{index + 1}</DataTableCell>
- <DataTableCell>
- {entry.player.display_name}
- {entry.player.label && ` (${entry.player.label})`}
- </DataTableCell>
- <DataTableCell>{entry.score}</DataTableCell>
- <DataTableCell>
- {formatUnixTimestamp(entry.submitted_at)}
- </DataTableCell>
- {showCode && (
- <DataTableCell>
- {entry.code && (
- <CodePopover code={entry.code} language={problemLanguage} />
- )}
- </DataTableCell>
- )}
- </tr>
- ))}
- </DataTable>
- );
+ return (
+ <DataTable
+ headers={[
+ "順位",
+ "プレイヤー",
+ "スコア",
+ "提出時刻",
+ ...(showCode ? ["コード"] : []),
+ ]}
+ >
+ {ranking.map((entry, index) => (
+ <tr key={entry.player.user_id}>
+ <DataTableCell>{index + 1}</DataTableCell>
+ <DataTableCell>
+ {entry.player.display_name}
+ {entry.player.label && ` (${entry.player.label})`}
+ </DataTableCell>
+ <DataTableCell>{entry.score}</DataTableCell>
+ <DataTableCell>
+ {formatUnixTimestamp(entry.submitted_at)}
+ </DataTableCell>
+ {showCode && (
+ <DataTableCell>
+ {entry.code && (
+ <CodePopover code={entry.code} language={problemLanguage} />
+ )}
+ </DataTableCell>
+ )}
+ </tr>
+ ))}
+ </DataTable>
+ );
}
diff --git a/frontend/app/components/Gaming/Score.tsx b/frontend/app/components/Gaming/Score.tsx
index ee23a6c..8e1e61d 100644
--- a/frontend/app/components/Gaming/Score.tsx
+++ b/frontend/app/components/Gaming/Score.tsx
@@ -1,36 +1,36 @@
import { useEffect, useState } from "react";
type Props = {
- status: string | null;
- score: number | null;
+ status: string | null;
+ score: number | null;
};
export default function Score({ status, score }: Props) {
- const [randomScore, setRandomScore] = useState<number | null>(null);
+ const [randomScore, setRandomScore] = useState<number | null>(null);
- useEffect(() => {
- if (status !== "running") {
- return;
- }
+ useEffect(() => {
+ if (status !== "running") {
+ return;
+ }
- const intervalId = setInterval(() => {
- const maxValue = Math.pow(10, String(score ?? 100).length) - 1;
- const minValue = Math.pow(10, String(score ?? 100).length - 1);
- const randomValue =
- Math.floor(Math.random() * (maxValue - minValue + 1)) + minValue;
- setRandomScore(randomValue);
- }, 50);
+ const intervalId = setInterval(() => {
+ const maxValue = Math.pow(10, String(score ?? 100).length) - 1;
+ const minValue = Math.pow(10, String(score ?? 100).length - 1);
+ const randomValue =
+ Math.floor(Math.random() * (maxValue - minValue + 1)) + minValue;
+ setRandomScore(randomValue);
+ }, 50);
- return () => {
- clearInterval(intervalId);
- };
- }, [status, score]);
+ return () => {
+ clearInterval(intervalId);
+ };
+ }, [status, score]);
- const displayScore = status === "running" ? randomScore : score;
+ const displayScore = status === "running" ? randomScore : score;
- return (
- <span className={status === "running" ? "animate-pulse" : ""}>
- {displayScore}
- </span>
- );
+ return (
+ <span className={status === "running" ? "animate-pulse" : ""}>
+ {displayScore}
+ </span>
+ );
}
diff --git a/frontend/app/components/Gaming/ScoreBar.tsx b/frontend/app/components/Gaming/ScoreBar.tsx
index 6a291cd..50a2402 100644
--- a/frontend/app/components/Gaming/ScoreBar.tsx
+++ b/frontend/app/components/Gaming/ScoreBar.tsx
@@ -1,30 +1,30 @@
type Props = {
- scoreA: number | null;
- scoreB: number | null;
- bgA: string;
- bgB: string;
+ 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 {
- const rawRatio = scoreB / (scoreA + scoreB);
- const k = 3.0;
- const emphasizedRatio =
- Math.pow(rawRatio, k) /
- (Math.pow(rawRatio, k) + Math.pow(1 - rawRatio, k));
- scoreRatio = emphasizedRatio * 100;
- }
+ let scoreRatio;
+ if (scoreA === null && scoreB === null) {
+ scoreRatio = 50;
+ } else if (scoreA === null) {
+ scoreRatio = 0;
+ } else if (scoreB === null) {
+ scoreRatio = 100;
+ } else {
+ const rawRatio = scoreB / (scoreA + scoreB);
+ const k = 3.0;
+ const emphasizedRatio =
+ Math.pow(rawRatio, k) /
+ (Math.pow(rawRatio, k) + Math.pow(1 - rawRatio, k));
+ scoreRatio = emphasizedRatio * 100;
+ }
- return (
- <div className={`w-full ${bgB}`}>
- <div className={`h-10 ${bgA}`} style={{ width: `${scoreRatio}%` }}></div>
- </div>
- );
+ return (
+ <div className={`w-full ${bgB}`}>
+ <div className={`h-10 ${bgA}`} style={{ width: `${scoreRatio}%` }}></div>
+ </div>
+ );
}
diff --git a/frontend/app/components/GolfPlayApp.tsx b/frontend/app/components/GolfPlayApp.tsx
index 5d00239..e9fa410 100644
--- a/frontend/app/components/GolfPlayApp.tsx
+++ b/frontend/app/components/GolfPlayApp.tsx
@@ -6,13 +6,13 @@ import { useDebouncedCallback } from "use-debounce";
import { ApiClientContext } from "../api/client";
import type { components } from "../api/schema";
import {
- gameStateKindAtom,
- handleSubmitCodePostAtom,
- handleSubmitCodePreAtom,
- setCurrentTimestampAtom,
- setDurationSecondsAtom,
- setGameStartedAtAtom,
- setLatestGameStateAtom,
+ gameStateKindAtom,
+ handleSubmitCodePostAtom,
+ handleSubmitCodePreAtom,
+ setCurrentTimestampAtom,
+ setDurationSecondsAtom,
+ setGameStartedAtAtom,
+ setLatestGameStateAtom,
} from "../states/play";
import GolfPlayAppGaming from "./GolfPlayApps/GolfPlayAppGaming";
import GolfPlayAppLoading from "./GolfPlayApps/GolfPlayAppLoading";
@@ -25,163 +25,163 @@ type Submission = components["schemas"]["Submission"];
type LatestGameState = components["schemas"]["LatestGameState"];
type Props = {
- game: Game;
- player: User;
- initialGameState: LatestGameState;
+ game: Game;
+ player: User;
+ initialGameState: LatestGameState;
};
export default function GolfPlayApp({ game, player, initialGameState }: Props) {
- useHydrateAtoms([
- [setDurationSecondsAtom, game.duration_seconds],
- [setGameStartedAtAtom, game.started_at ?? null],
- [setLatestGameStateAtom, initialGameState],
- ]);
+ useHydrateAtoms([
+ [setDurationSecondsAtom, game.duration_seconds],
+ [setGameStartedAtAtom, game.started_at ?? null],
+ [setLatestGameStateAtom, initialGameState],
+ ]);
- const apiClient = useContext(ApiClientContext)!;
+ const apiClient = useContext(ApiClientContext)!;
- const gameStateKind = useAtomValue(gameStateKindAtom);
- const setGameStartedAt = useSetAtom(setGameStartedAtAtom);
- const setCurrentTimestamp = useSetAtom(setCurrentTimestampAtom);
- const handleSubmitCodePre = useSetAtom(handleSubmitCodePreAtom);
- const handleSubmitCodePost = useSetAtom(handleSubmitCodePostAtom);
- const setLatestGameState = useSetAtom(setLatestGameStateAtom);
+ 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);
+ useTimer({ delay: 1000, startImmediately: true }, setCurrentTimestamp);
- const playerProfile = {
- id: player.user_id,
- displayName: player.display_name,
- iconPath: player.icon_path ?? null,
- };
+ const playerProfile = {
+ id: player.user_id,
+ displayName: player.display_name,
+ iconPath: player.icon_path ?? null,
+ };
- const onCodeChange = useDebouncedCallback(async (code: string) => {
- if (game.game_type === "1v1") {
- console.log("player:c2s:code");
- await apiClient.postGamePlayCode(game.game_id, code);
- }
- }, 1000);
+ const onCodeChange = useDebouncedCallback(async (code: string) => {
+ if (game.game_type === "1v1") {
+ console.log("player:c2s:code");
+ await apiClient.postGamePlayCode(game.game_id, code);
+ }
+ }, 1000);
- const onCodeSubmit = useDebouncedCallback(
- async (code: string) => {
- if (code === "") {
- return;
- }
- console.log("player:c2s:submit");
- handleSubmitCodePre();
- await apiClient.postGamePlaySubmit(game.game_id, code);
- await new Promise((resolve) => setTimeout(resolve, 1000));
- handleSubmitCodePost();
- },
- 1000,
- { leading: true },
- );
+ const onCodeSubmit = useDebouncedCallback(
+ async (code: string) => {
+ if (code === "") {
+ return;
+ }
+ console.log("player:c2s:submit");
+ handleSubmitCodePre();
+ await apiClient.postGamePlaySubmit(game.game_id, code);
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+ handleSubmitCodePost();
+ },
+ 1000,
+ { leading: true },
+ );
- const [submissions, setSubmissions] = useState<Submission[]>([]);
- const [isDataPolling, setIsDataPolling] = useState(false);
+ const [submissions, setSubmissions] = useState<Submission[]>([]);
+ const [isDataPolling, setIsDataPolling] = useState(false);
- const fetchSubmissions = useCallback(async () => {
- try {
- const { submissions } = await apiClient.getGamePlaySubmissions(
- game.game_id,
- );
- setSubmissions(submissions);
- } catch (error) {
- console.error(error);
- }
- }, [apiClient, game.game_id]);
+ const fetchSubmissions = useCallback(async () => {
+ try {
+ const { submissions } = await apiClient.getGamePlaySubmissions(
+ game.game_id,
+ );
+ setSubmissions(submissions);
+ } catch (error) {
+ console.error(error);
+ }
+ }, [apiClient, game.game_id]);
- useEffect(() => {
- if (gameStateKind === "finished") {
- fetchSubmissions();
- }
- }, [gameStateKind, fetchSubmissions]);
+ useEffect(() => {
+ if (gameStateKind === "finished") {
+ fetchSubmissions();
+ }
+ }, [gameStateKind, fetchSubmissions]);
- useEffect(() => {
- if (isDataPolling) {
- return;
- }
- const timerId = setInterval(async () => {
- if (isDataPolling) {
- return;
- }
- setIsDataPolling(true);
+ useEffect(() => {
+ if (isDataPolling) {
+ return;
+ }
+ const timerId = setInterval(async () => {
+ if (isDataPolling) {
+ return;
+ }
+ setIsDataPolling(true);
- try {
- if (gameStateKind === "waiting") {
- const { game: g } = await apiClient.getGame(game.game_id);
- if (g.started_at != null) {
- setGameStartedAt(g.started_at);
- }
- } else if (gameStateKind === "gaming") {
- const { state } = await apiClient.getGamePlayLatestState(
- game.game_id,
- );
- setLatestGameState(state);
- await fetchSubmissions();
- }
- } catch (error) {
- console.error(error);
- } finally {
- setIsDataPolling(false);
- }
- }, 1000);
+ try {
+ if (gameStateKind === "waiting") {
+ const { game: g } = await apiClient.getGame(game.game_id);
+ if (g.started_at != null) {
+ setGameStartedAt(g.started_at);
+ }
+ } else if (gameStateKind === "gaming") {
+ const { state } = await apiClient.getGamePlayLatestState(
+ game.game_id,
+ );
+ setLatestGameState(state);
+ await fetchSubmissions();
+ }
+ } catch (error) {
+ console.error(error);
+ } finally {
+ setIsDataPolling(false);
+ }
+ }, 1000);
- return () => {
- clearInterval(timerId);
- };
- }, [
- isDataPolling,
- apiClient,
- game.game_id,
- gameStateKind,
- setGameStartedAt,
- setLatestGameState,
- fetchSubmissions,
- ]);
+ return () => {
+ clearInterval(timerId);
+ };
+ }, [
+ isDataPolling,
+ apiClient,
+ game.game_id,
+ gameStateKind,
+ setGameStartedAt,
+ setLatestGameState,
+ fetchSubmissions,
+ ]);
- if (gameStateKind === "loading") {
- return <GolfPlayAppLoading />;
- } else if (gameStateKind === "waiting") {
- if (player.is_admin) {
- return (
- <GolfPlayAppGaming
- gameDisplayName={game.display_name}
- playerProfile={playerProfile}
- problemTitle={game.problem.title}
- problemDescription={game.problem.description}
- problemLanguage={game.problem.language}
- sampleCode={game.problem.sample_code}
- initialCode={initialGameState.code}
- onCodeChange={onCodeChange}
- onCodeSubmit={onCodeSubmit}
- isFinished={false}
- submissions={submissions}
- />
- );
- }
- return (
- <GolfPlayAppWaiting
- gameDisplayName={game.display_name}
- playerProfile={playerProfile}
- />
- );
- } else if (gameStateKind === "starting") {
- return <GolfPlayAppStarting gameDisplayName={game.display_name} />;
- } else if (gameStateKind === "gaming" || gameStateKind === "finished") {
- return (
- <GolfPlayAppGaming
- gameDisplayName={game.display_name}
- playerProfile={playerProfile}
- problemTitle={game.problem.title}
- problemDescription={game.problem.description}
- problemLanguage={game.problem.language}
- sampleCode={game.problem.sample_code}
- initialCode={initialGameState.code}
- onCodeChange={onCodeChange}
- onCodeSubmit={onCodeSubmit}
- isFinished={gameStateKind === "finished"}
- submissions={submissions}
- />
- );
- }
+ if (gameStateKind === "loading") {
+ return <GolfPlayAppLoading />;
+ } else if (gameStateKind === "waiting") {
+ if (player.is_admin) {
+ return (
+ <GolfPlayAppGaming
+ gameDisplayName={game.display_name}
+ playerProfile={playerProfile}
+ problemTitle={game.problem.title}
+ problemDescription={game.problem.description}
+ problemLanguage={game.problem.language}
+ sampleCode={game.problem.sample_code}
+ initialCode={initialGameState.code}
+ onCodeChange={onCodeChange}
+ onCodeSubmit={onCodeSubmit}
+ isFinished={false}
+ submissions={submissions}
+ />
+ );
+ }
+ return (
+ <GolfPlayAppWaiting
+ gameDisplayName={game.display_name}
+ playerProfile={playerProfile}
+ />
+ );
+ } else if (gameStateKind === "starting") {
+ return <GolfPlayAppStarting gameDisplayName={game.display_name} />;
+ } else if (gameStateKind === "gaming" || gameStateKind === "finished") {
+ return (
+ <GolfPlayAppGaming
+ gameDisplayName={game.display_name}
+ playerProfile={playerProfile}
+ problemTitle={game.problem.title}
+ problemDescription={game.problem.description}
+ problemLanguage={game.problem.language}
+ sampleCode={game.problem.sample_code}
+ initialCode={initialGameState.code}
+ onCodeChange={onCodeChange}
+ onCodeSubmit={onCodeSubmit}
+ isFinished={gameStateKind === "finished"}
+ submissions={submissions}
+ />
+ );
+ }
}
diff --git a/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.test.tsx b/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.test.tsx
index 2d51d66..ae5381b 100644
--- a/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.test.tsx
+++ b/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.test.tsx
@@ -5,103 +5,103 @@ import { cleanup, render, screen } from "@testing-library/react";
import { createStore, Provider } from "jotai";
import { afterEach, describe, expect, test } from "vitest";
import {
- setCurrentTimestampAtom,
- setDurationSecondsAtom,
- setGameStartedAtAtom,
- setLatestGameStateAtom,
+ setCurrentTimestampAtom,
+ setDurationSecondsAtom,
+ setGameStartedAtAtom,
+ setLatestGameStateAtom,
} from "../../states/play";
import GolfPlayAppGaming from "./GolfPlayAppGaming";
afterEach(() => {
- cleanup();
+ cleanup();
});
function createTestStore() {
- const store = createStore();
- const now = Math.floor(Date.now() / 1000);
- store.set(setCurrentTimestampAtom);
- store.set(setDurationSecondsAtom, 600);
- store.set(setGameStartedAtAtom, now - 60);
- store.set(setLatestGameStateAtom, {
- status: "none",
- code: "",
- score: null,
- best_score_submitted_at: null,
- });
- return store;
+ const store = createStore();
+ const now = Math.floor(Date.now() / 1000);
+ store.set(setCurrentTimestampAtom);
+ store.set(setDurationSecondsAtom, 600);
+ store.set(setGameStartedAtAtom, now - 60);
+ store.set(setLatestGameStateAtom, {
+ status: "none",
+ code: "",
+ score: null,
+ best_score_submitted_at: null,
+ });
+ return store;
}
const defaultProps = {
- gameDisplayName: "Test Game",
- playerProfile: {
- id: 1,
- displayName: "Test Player",
- iconPath: null,
- },
- problemTitle: "Test Problem",
- problemDescription: "Description",
- problemLanguage: "php" as const,
- sampleCode: "<?php echo 1;",
- initialCode: "",
- onCodeChange: () => {},
- onCodeSubmit: () => {},
- isFinished: false,
+ gameDisplayName: "Test Game",
+ playerProfile: {
+ id: 1,
+ displayName: "Test Player",
+ iconPath: null,
+ },
+ problemTitle: "Test Problem",
+ problemDescription: "Description",
+ problemLanguage: "php" as const,
+ sampleCode: "<?php echo 1;",
+ initialCode: "",
+ onCodeChange: () => {},
+ onCodeSubmit: () => {},
+ isFinished: false,
};
describe("GolfPlayAppGaming submission history", () => {
- test("shows placeholder row when no submissions", () => {
- const store = createTestStore();
- render(
- <Provider store={store}>
- <GolfPlayAppGaming {...defaultProps} submissions={[]} />
- </Provider>,
- );
- expect(screen.getByText("提出待ち")).toBeDefined();
- const dashes = screen.getAllByText("-");
- expect(dashes.length).toBe(3);
- });
+ test("shows placeholder row when no submissions", () => {
+ const store = createTestStore();
+ render(
+ <Provider store={store}>
+ <GolfPlayAppGaming {...defaultProps} submissions={[]} />
+ </Provider>,
+ );
+ expect(screen.getByText("提出待ち")).toBeDefined();
+ const dashes = screen.getAllByText("-");
+ expect(dashes.length).toBe(3);
+ });
- test("renders submission rows with status and code size", () => {
- const store = createTestStore();
- const submissions = [
- {
- submission_id: 1,
- game_id: 1,
- status: "success" as const,
- code: "<?php echo 1;",
- code_size: 7,
- created_at: 1740000000,
- },
- {
- submission_id: 2,
- game_id: 1,
- status: "wrong_answer" as const,
- code: "<?php echo 2;",
- code_size: 10,
- created_at: 1740000060,
- },
- ];
- render(
- <Provider store={store}>
- <GolfPlayAppGaming {...defaultProps} submissions={submissions} />
- </Provider>,
- );
- expect(screen.getByText("成功")).toBeDefined();
- expect(screen.getByText("テスト失敗")).toBeDefined();
- expect(screen.getByText("7")).toBeDefined();
- expect(screen.getByText("10")).toBeDefined();
- });
+ test("renders submission rows with status and code size", () => {
+ const store = createTestStore();
+ const submissions = [
+ {
+ submission_id: 1,
+ game_id: 1,
+ status: "success" as const,
+ code: "<?php echo 1;",
+ code_size: 7,
+ created_at: 1740000000,
+ },
+ {
+ submission_id: 2,
+ game_id: 1,
+ status: "wrong_answer" as const,
+ code: "<?php echo 2;",
+ code_size: 10,
+ created_at: 1740000060,
+ },
+ ];
+ render(
+ <Provider store={store}>
+ <GolfPlayAppGaming {...defaultProps} submissions={submissions} />
+ </Provider>,
+ );
+ expect(screen.getByText("成功")).toBeDefined();
+ expect(screen.getByText("テスト失敗")).toBeDefined();
+ expect(screen.getByText("7")).toBeDefined();
+ expect(screen.getByText("10")).toBeDefined();
+ });
- test("renders table headers", () => {
- const store = createTestStore();
- render(
- <Provider store={store}>
- <GolfPlayAppGaming {...defaultProps} submissions={[]} />
- </Provider>,
- );
- expect(screen.getByText("ステータス")).toBeDefined();
- expect(screen.getByText("スコア")).toBeDefined();
- expect(screen.getByText("提出時刻")).toBeDefined();
- expect(screen.getByText("コード")).toBeDefined();
- });
+ test("renders table headers", () => {
+ const store = createTestStore();
+ render(
+ <Provider store={store}>
+ <GolfPlayAppGaming {...defaultProps} submissions={[]} />
+ </Provider>,
+ );
+ expect(screen.getByText("ステータス")).toBeDefined();
+ expect(screen.getByText("スコア")).toBeDefined();
+ expect(screen.getByText("提出時刻")).toBeDefined();
+ expect(screen.getByText("コード")).toBeDefined();
+ });
});
diff --git a/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx b/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx
index e590df0..3e1ab67 100644
--- a/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx
+++ b/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx
@@ -3,18 +3,18 @@ import React, { useRef, useState } from "react";
import { Link } from "wouter";
import type { components } from "../../api/schema";
import {
- calcCodeSize,
- gamingLeftTimeSecondsAtom,
- scoreAtom,
- statusAtom,
+ calcCodeSize,
+ gamingLeftTimeSecondsAtom,
+ scoreAtom,
+ statusAtom,
} from "../../states/play";
import type { PlayerProfile } from "../../types/PlayerProfile";
import type { SupportedLanguage } from "../../types/SupportedLanguage";
import BorderedContainer from "../BorderedContainer";
import CodePopover from "../Gaming/CodePopover";
import DataTable, {
- DataTableCell,
- formatUnixTimestamp,
+ DataTableCell,
+ formatUnixTimestamp,
} from "../Gaming/DataTable";
import LeftTime from "../Gaming/LeftTime";
import ProblemColumn from "../Gaming/ProblemColumn";
@@ -27,142 +27,142 @@ import UserIcon from "../UserIcon";
type Submission = components["schemas"]["Submission"];
type Props = {
- gameDisplayName: string;
- playerProfile: PlayerProfile;
- problemTitle: string;
- problemDescription: string;
- problemLanguage: SupportedLanguage;
- sampleCode: string;
- initialCode: string;
- onCodeChange: (code: string) => void;
- onCodeSubmit: (code: string) => void;
- isFinished: boolean;
- submissions: Submission[];
+ gameDisplayName: string;
+ playerProfile: PlayerProfile;
+ problemTitle: string;
+ problemDescription: string;
+ problemLanguage: SupportedLanguage;
+ sampleCode: string;
+ initialCode: string;
+ onCodeChange: (code: string) => void;
+ onCodeSubmit: (code: string) => void;
+ isFinished: boolean;
+ submissions: Submission[];
};
export default function GolfPlayAppGaming({
- gameDisplayName,
- playerProfile,
- problemTitle,
- problemDescription,
- problemLanguage,
- sampleCode,
- initialCode,
- onCodeChange,
- onCodeSubmit,
- isFinished,
- submissions,
+ gameDisplayName,
+ playerProfile,
+ problemTitle,
+ problemDescription,
+ problemLanguage,
+ sampleCode,
+ initialCode,
+ onCodeChange,
+ onCodeSubmit,
+ isFinished,
+ submissions,
}: Props) {
- const leftTimeSeconds = useAtomValue(gamingLeftTimeSecondsAtom);
- const score = useAtomValue(scoreAtom);
- const status = useAtomValue(statusAtom);
+ const leftTimeSeconds = useAtomValue(gamingLeftTimeSecondsAtom);
+ const score = useAtomValue(scoreAtom);
+ const status = useAtomValue(statusAtom);
- const [codeSize, setCodeSize] = useState(
- calcCodeSize(initialCode, problemLanguage),
- );
- const textareaRef = useRef<HTMLTextAreaElement>(null);
+ const [codeSize, setCodeSize] = useState(
+ calcCodeSize(initialCode, problemLanguage),
+ );
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
- const handleTextChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
- setCodeSize(calcCodeSize(e.target.value, problemLanguage));
- if (!isFinished) {
- onCodeChange(e.target.value);
- }
- };
+ const handleTextChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
+ setCodeSize(calcCodeSize(e.target.value, problemLanguage));
+ if (!isFinished) {
+ onCodeChange(e.target.value);
+ }
+ };
- const handleSubmitButtonClick = () => {
- if (textareaRef.current && !isFinished) {
- onCodeSubmit(textareaRef.current.value);
- }
- };
+ const handleSubmitButtonClick = () => {
+ if (textareaRef.current && !isFinished) {
+ onCodeSubmit(textareaRef.current.value);
+ }
+ };
- return (
- <div className="min-h-screen bg-gray-100 flex flex-col">
- <div className="text-white bg-brand-600 flex flex-row justify-between px-4 py-2">
- <div className="font-bold">
- <div className="text-gray-100">{gameDisplayName}</div>
- {isFinished ? (
- <div className="text-2xl md:text-3xl">終了</div>
- ) : leftTimeSeconds === null ? (
- <div className="text-2xl md:text-3xl">未開始</div>
- ) : (
- <LeftTime sec={leftTimeSeconds} />
- )}
- </div>
- <Link to={"/dashboard"}>
- <div className="flex gap-6 items-center font-bold">
- <div className="text-2xl md:text-6xl">{score}</div>
- <div className="hidden md:block text-4xl">
- {playerProfile.displayName}
- </div>
- {playerProfile.iconPath && (
- <UserIcon
- iconPath={playerProfile.iconPath}
- displayName={playerProfile.displayName}
- className="w-12 h-12 my-auto"
- />
- )}
- </div>
- </Link>
- </div>
- <ThreeColumnLayout>
- <ProblemColumn
- title={problemTitle}
- description={problemDescription}
- language={problemLanguage}
- sampleCode={sampleCode}
- />
- <TitledColumn title="ソースコード">
- <BorderedContainer className="grow flex flex-col gap-4">
- <div className="flex flex-row gap-2 items-center">
- <div className="grow font-semibold text-lg">
- コードサイズ: {codeSize}
- </div>
- <SubmitButton
- onClick={handleSubmitButtonClick}
- disabled={isFinished}
- >
- 提出
- </SubmitButton>
- </div>
- <textarea
- ref={textareaRef}
- defaultValue={initialCode}
- onChange={handleTextChange}
- className="grow resize-none h-full w-full p-2 bg-gray-50 rounded-lg border border-gray-300 focus:outline-hidden focus:ring-2 focus:ring-gray-400 transition duration-300"
- rows={10}
- />
- </BorderedContainer>
- </TitledColumn>
- <TitledColumn title="提出結果">
- <DataTable headers={["ステータス", "スコア", "提出時刻", "コード"]}>
- {submissions.length === 0 ? (
- <tr>
- <DataTableCell>
- <SubmitStatusLabel status={status} />
- </DataTableCell>
- <DataTableCell>-</DataTableCell>
- <DataTableCell>-</DataTableCell>
- <DataTableCell>-</DataTableCell>
- </tr>
- ) : (
- submissions.map((s) => (
- <tr key={s.submission_id}>
- <DataTableCell>
- <SubmitStatusLabel status={s.status} />
- </DataTableCell>
- <DataTableCell>{s.code_size}</DataTableCell>
- <DataTableCell>
- {formatUnixTimestamp(s.created_at)}
- </DataTableCell>
- <DataTableCell>
- <CodePopover code={s.code} language={problemLanguage} />
- </DataTableCell>
- </tr>
- ))
- )}
- </DataTable>
- </TitledColumn>
- </ThreeColumnLayout>
- </div>
- );
+ return (
+ <div className="min-h-screen bg-gray-100 flex flex-col">
+ <div className="text-white bg-brand-600 flex flex-row justify-between px-4 py-2">
+ <div className="font-bold">
+ <div className="text-gray-100">{gameDisplayName}</div>
+ {isFinished ? (
+ <div className="text-2xl md:text-3xl">終了</div>
+ ) : leftTimeSeconds === null ? (
+ <div className="text-2xl md:text-3xl">未開始</div>
+ ) : (
+ <LeftTime sec={leftTimeSeconds} />
+ )}
+ </div>
+ <Link to={"/dashboard"}>
+ <div className="flex gap-6 items-center font-bold">
+ <div className="text-2xl md:text-6xl">{score}</div>
+ <div className="hidden md:block text-4xl">
+ {playerProfile.displayName}
+ </div>
+ {playerProfile.iconPath && (
+ <UserIcon
+ iconPath={playerProfile.iconPath}
+ displayName={playerProfile.displayName}
+ className="w-12 h-12 my-auto"
+ />
+ )}
+ </div>
+ </Link>
+ </div>
+ <ThreeColumnLayout>
+ <ProblemColumn
+ title={problemTitle}
+ description={problemDescription}
+ language={problemLanguage}
+ sampleCode={sampleCode}
+ />
+ <TitledColumn title="ソースコード">
+ <BorderedContainer className="grow flex flex-col gap-4">
+ <div className="flex flex-row gap-2 items-center">
+ <div className="grow font-semibold text-lg">
+ コードサイズ: {codeSize}
+ </div>
+ <SubmitButton
+ onClick={handleSubmitButtonClick}
+ disabled={isFinished}
+ >
+ 提出
+ </SubmitButton>
+ </div>
+ <textarea
+ ref={textareaRef}
+ defaultValue={initialCode}
+ onChange={handleTextChange}
+ className="grow resize-none h-full w-full p-2 bg-gray-50 rounded-lg border border-gray-300 focus:outline-hidden focus:ring-2 focus:ring-gray-400 transition duration-300"
+ rows={10}
+ />
+ </BorderedContainer>
+ </TitledColumn>
+ <TitledColumn title="提出結果">
+ <DataTable headers={["ステータス", "スコア", "提出時刻", "コード"]}>
+ {submissions.length === 0 ? (
+ <tr>
+ <DataTableCell>
+ <SubmitStatusLabel status={status} />
+ </DataTableCell>
+ <DataTableCell>-</DataTableCell>
+ <DataTableCell>-</DataTableCell>
+ <DataTableCell>-</DataTableCell>
+ </tr>
+ ) : (
+ submissions.map((s) => (
+ <tr key={s.submission_id}>
+ <DataTableCell>
+ <SubmitStatusLabel status={s.status} />
+ </DataTableCell>
+ <DataTableCell>{s.code_size}</DataTableCell>
+ <DataTableCell>
+ {formatUnixTimestamp(s.created_at)}
+ </DataTableCell>
+ <DataTableCell>
+ <CodePopover code={s.code} language={problemLanguage} />
+ </DataTableCell>
+ </tr>
+ ))
+ )}
+ </DataTable>
+ </TitledColumn>
+ </ThreeColumnLayout>
+ </div>
+ );
}
diff --git a/frontend/app/components/GolfPlayApps/GolfPlayAppLoading.tsx b/frontend/app/components/GolfPlayApps/GolfPlayAppLoading.tsx
index 7b424f2..ccb716f 100644
--- a/frontend/app/components/GolfPlayApps/GolfPlayAppLoading.tsx
+++ b/frontend/app/components/GolfPlayApps/GolfPlayAppLoading.tsx
@@ -1,9 +1,9 @@
export default function GolfPlayAppLoading() {
- 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>
- );
+ 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/GolfPlayAppStarting.tsx b/frontend/app/components/GolfPlayApps/GolfPlayAppStarting.tsx
index b3378fc..951e001 100644
--- a/frontend/app/components/GolfPlayApps/GolfPlayAppStarting.tsx
+++ b/frontend/app/components/GolfPlayApps/GolfPlayAppStarting.tsx
@@ -2,20 +2,20 @@ import { useAtomValue } from "jotai";
import { startingLeftTimeSecondsAtom } from "../../states/play";
type Props = {
- gameDisplayName: string;
+ gameDisplayName: string;
};
export default function GolfPlayAppStarting({ gameDisplayName }: Props) {
- const leftTimeSeconds = useAtomValue(startingLeftTimeSecondsAtom)!;
+ const leftTimeSeconds = useAtomValue(startingLeftTimeSecondsAtom)!;
- return (
- <div className="min-h-screen bg-gray-100 flex flex-col">
- <div className="text-white bg-brand-600 p-10 text-center">
- <div className="text-4xl font-bold">{gameDisplayName}</div>
- </div>
- <div className="text-center text-black font-black text-10xl">
- {leftTimeSeconds}
- </div>
- </div>
- );
+ return (
+ <div className="min-h-screen bg-gray-100 flex flex-col">
+ <div className="text-white bg-brand-600 p-10 text-center">
+ <div className="text-4xl font-bold">{gameDisplayName}</div>
+ </div>
+ <div className="text-center text-black font-black text-10xl">
+ {leftTimeSeconds}
+ </div>
+ </div>
+ );
}
diff --git a/frontend/app/components/GolfPlayApps/GolfPlayAppWaiting.tsx b/frontend/app/components/GolfPlayApps/GolfPlayAppWaiting.tsx
index 1341073..5c4e94f 100644
--- a/frontend/app/components/GolfPlayApps/GolfPlayAppWaiting.tsx
+++ b/frontend/app/components/GolfPlayApps/GolfPlayAppWaiting.tsx
@@ -2,22 +2,22 @@ import type { PlayerProfile } from "../../types/PlayerProfile";
import PlayerNameAndIcon from "../PlayerNameAndIcon";
type Props = {
- gameDisplayName: string;
- playerProfile: PlayerProfile;
+ gameDisplayName: string;
+ playerProfile: PlayerProfile;
};
export default function GolfPlayAppWaiting({
- gameDisplayName,
- playerProfile,
+ gameDisplayName,
+ playerProfile,
}: Props) {
- return (
- <div className="min-h-screen bg-gray-100 flex flex-col font-bold text-center">
- <div className="text-white bg-brand-600 p-10">
- <div className="text-4xl">{gameDisplayName}</div>
- </div>
- <div className="grow grid mx-auto text-black">
- <PlayerNameAndIcon profile={playerProfile} />
- </div>
- </div>
- );
+ return (
+ <div className="min-h-screen bg-gray-100 flex flex-col font-bold text-center">
+ <div className="text-white bg-brand-600 p-10">
+ <div className="text-4xl">{gameDisplayName}</div>
+ </div>
+ <div className="grow grid mx-auto text-black">
+ <PlayerNameAndIcon profile={playerProfile} />
+ </div>
+ </div>
+ );
}
diff --git a/frontend/app/components/GolfWatchApp.tsx b/frontend/app/components/GolfWatchApp.tsx
index 41b5a01..b7feae7 100644
--- a/frontend/app/components/GolfWatchApp.tsx
+++ b/frontend/app/components/GolfWatchApp.tsx
@@ -5,12 +5,12 @@ import { useTimer } from "react-use-precision-timer";
import { ApiClientContext } from "../api/client";
import type { components } from "../api/schema";
import {
- gameStateKindAtom,
- rankingAtom,
- setCurrentTimestampAtom,
- setDurationSecondsAtom,
- setGameStartedAtAtom,
- setLatestGameStatesAtom,
+ gameStateKindAtom,
+ rankingAtom,
+ setCurrentTimestampAtom,
+ setDurationSecondsAtom,
+ setGameStartedAtAtom,
+ setLatestGameStatesAtom,
} from "../states/watch";
import GolfWatchAppGaming1v1 from "./GolfWatchApps/GolfWatchAppGaming1v1";
import GolfWatchAppGamingMultiplayer from "./GolfWatchApps/GolfWatchAppGamingMultiplayer";
@@ -24,130 +24,130 @@ type LatestGameState = components["schemas"]["LatestGameState"];
type RankingEntry = components["schemas"]["RankingEntry"];
export type Props = {
- game: Game;
- initialGameStates: { [key: string]: LatestGameState };
- initialRanking: RankingEntry[];
+ game: Game;
+ initialGameStates: { [key: string]: LatestGameState };
+ initialRanking: RankingEntry[];
};
export default function GolfWatchApp({
- game,
- initialGameStates,
- initialRanking,
+ game,
+ initialGameStates,
+ initialRanking,
}: Props) {
- useHydrateAtoms([
- [rankingAtom, initialRanking],
- [setDurationSecondsAtom, game.duration_seconds],
- [setGameStartedAtAtom, game.started_at ?? null],
- [setLatestGameStatesAtom, initialGameStates],
- ]);
+ useHydrateAtoms([
+ [rankingAtom, initialRanking],
+ [setDurationSecondsAtom, game.duration_seconds],
+ [setGameStartedAtAtom, game.started_at ?? null],
+ [setLatestGameStatesAtom, initialGameStates],
+ ]);
- const apiClient = useContext(ApiClientContext)!;
+ const apiClient = useContext(ApiClientContext)!;
- const gameStateKind = useAtomValue(gameStateKindAtom);
- const setGameStartedAt = useSetAtom(setGameStartedAtAtom);
- const setCurrentTimestamp = useSetAtom(setCurrentTimestampAtom);
- const setLatestGameStates = useSetAtom(setLatestGameStatesAtom);
- const setRanking = useSetAtom(rankingAtom);
+ const gameStateKind = useAtomValue(gameStateKindAtom);
+ const setGameStartedAt = useSetAtom(setGameStartedAtAtom);
+ const setCurrentTimestamp = useSetAtom(setCurrentTimestampAtom);
+ const setLatestGameStates = useSetAtom(setLatestGameStatesAtom);
+ const setRanking = useSetAtom(rankingAtom);
- useTimer({ delay: 1000, startImmediately: true }, setCurrentTimestamp);
+ useTimer({ delay: 1000, startImmediately: true }, setCurrentTimestamp);
- const playerA = game.main_players[0];
- const playerB = game.main_players[1];
+ 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 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);
+ const [isDataPolling, setIsDataPolling] = useState(false);
- useEffect(() => {
- if (isDataPolling) {
- return;
- }
- const timerId = setInterval(async () => {
- if (isDataPolling) {
- return;
- }
- setIsDataPolling(true);
+ useEffect(() => {
+ if (isDataPolling) {
+ return;
+ }
+ const timerId = setInterval(async () => {
+ if (isDataPolling) {
+ return;
+ }
+ setIsDataPolling(true);
- try {
- if (gameStateKind === "waiting") {
- const { game: g } = await apiClient.getGame(game.game_id);
- if (g.started_at != null) {
- setGameStartedAt(g.started_at);
- }
- } else if (gameStateKind === "gaming") {
- const { states } = await apiClient.getGameWatchLatestStates(
- game.game_id,
- );
- setLatestGameStates(states);
- const { ranking } = await apiClient.getGameWatchRanking(game.game_id);
- setRanking(ranking);
- }
- } catch (error) {
- console.error(error);
- } finally {
- setIsDataPolling(false);
- }
- }, 1000);
+ try {
+ if (gameStateKind === "waiting") {
+ const { game: g } = await apiClient.getGame(game.game_id);
+ if (g.started_at != null) {
+ setGameStartedAt(g.started_at);
+ }
+ } else if (gameStateKind === "gaming") {
+ const { states } = await apiClient.getGameWatchLatestStates(
+ game.game_id,
+ );
+ setLatestGameStates(states);
+ const { ranking } = await apiClient.getGameWatchRanking(game.game_id);
+ setRanking(ranking);
+ }
+ } catch (error) {
+ console.error(error);
+ } finally {
+ setIsDataPolling(false);
+ }
+ }, 1000);
- return () => {
- clearInterval(timerId);
- };
- }, [
- isDataPolling,
- apiClient,
- game.game_id,
- gameStateKind,
- setGameStartedAt,
- setLatestGameStates,
- setRanking,
- ]);
+ return () => {
+ clearInterval(timerId);
+ };
+ }, [
+ isDataPolling,
+ apiClient,
+ game.game_id,
+ gameStateKind,
+ setGameStartedAt,
+ setLatestGameStates,
+ setRanking,
+ ]);
- if (gameStateKind === "loading") {
- return <GolfWatchAppLoading />;
- } else 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}
- problemLanguage={game.problem.language}
- sampleCode={game.problem.sample_code}
- />
- ) : (
- <GolfWatchAppGamingMultiplayer
- gameDisplayName={game.display_name}
- problemTitle={game.problem.title}
- problemDescription={game.problem.description}
- problemLanguage={game.problem.language}
- sampleCode={game.problem.sample_code}
- />
- );
- }
+ if (gameStateKind === "loading") {
+ return <GolfWatchAppLoading />;
+ } else 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}
+ problemLanguage={game.problem.language}
+ sampleCode={game.problem.sample_code}
+ />
+ ) : (
+ <GolfWatchAppGamingMultiplayer
+ gameDisplayName={game.display_name}
+ problemTitle={game.problem.title}
+ problemDescription={game.problem.description}
+ problemLanguage={game.problem.language}
+ sampleCode={game.problem.sample_code}
+ />
+ );
+ }
}
diff --git a/frontend/app/components/GolfWatchApps/GolfWatchAppGaming1v1.tsx b/frontend/app/components/GolfWatchApps/GolfWatchAppGaming1v1.tsx
index 3d2784e..f032607 100644
--- a/frontend/app/components/GolfWatchApps/GolfWatchAppGaming1v1.tsx
+++ b/frontend/app/components/GolfWatchApps/GolfWatchAppGaming1v1.tsx
@@ -1,10 +1,10 @@
import { useAtomValue } from "jotai";
import {
- calcCodeSize,
- checkGameResultKind,
- gameStateKindAtom,
- gamingLeftTimeSecondsAtom,
- latestGameStatesAtom,
+ calcCodeSize,
+ checkGameResultKind,
+ gameStateKindAtom,
+ gamingLeftTimeSecondsAtom,
+ latestGameStatesAtom,
} from "../../states/watch";
import type { PlayerProfile } from "../../types/PlayerProfile";
import type { SupportedLanguage } from "../../types/SupportedLanguage";
@@ -21,140 +21,140 @@ import TitledColumn from "../TitledColumn";
import UserIcon from "../UserIcon";
type Props = {
- gameDisplayName: string;
- playerProfileA: PlayerProfile | null;
- playerProfileB: PlayerProfile | null;
- problemTitle: string;
- problemDescription: string;
- problemLanguage: SupportedLanguage;
- sampleCode: string;
+ gameDisplayName: string;
+ playerProfileA: PlayerProfile | null;
+ playerProfileB: PlayerProfile | null;
+ problemTitle: string;
+ problemDescription: string;
+ problemLanguage: SupportedLanguage;
+ sampleCode: string;
};
export default function GolfWatchAppGaming1v1({
- gameDisplayName,
- playerProfileA,
- playerProfileB,
- problemTitle,
- problemDescription,
- problemLanguage,
- sampleCode,
+ gameDisplayName,
+ playerProfileA,
+ playerProfileB,
+ problemTitle,
+ problemDescription,
+ problemLanguage,
+ sampleCode,
}: Props) {
- const gameStateKind = useAtomValue(gameStateKindAtom);
- const leftTimeSeconds = useAtomValue(gamingLeftTimeSecondsAtom)!;
- const latestGameStates = useAtomValue(latestGameStatesAtom);
+ const gameStateKind = useAtomValue(gameStateKindAtom);
+ const leftTimeSeconds = useAtomValue(gamingLeftTimeSecondsAtom)!;
+ const latestGameStates = useAtomValue(latestGameStatesAtom);
- const stateA =
- playerProfileA && (latestGameStates[`${playerProfileA.id}`] ?? null);
- const codeA = stateA?.code ?? "";
- const scoreA = stateA?.score ?? null;
- const statusA = stateA?.status ?? "none";
- const stateB =
- playerProfileB && (latestGameStates[`${playerProfileB.id}`] ?? null);
- const codeB = stateB?.code ?? "";
- const scoreB = stateB?.score ?? null;
- const statusB = stateB?.status ?? "none";
+ const stateA =
+ playerProfileA && (latestGameStates[`${playerProfileA.id}`] ?? null);
+ const codeA = stateA?.code ?? "";
+ const scoreA = stateA?.score ?? null;
+ const statusA = stateA?.status ?? "none";
+ const stateB =
+ playerProfileB && (latestGameStates[`${playerProfileB.id}`] ?? null);
+ const codeB = stateB?.code ?? "";
+ const scoreB = stateB?.score ?? null;
+ const statusB = stateB?.status ?? "none";
- const codeSizeA = calcCodeSize(codeA, problemLanguage);
- const codeSizeB = calcCodeSize(codeB, problemLanguage);
+ const codeSizeA = calcCodeSize(codeA, problemLanguage);
+ const codeSizeB = calcCodeSize(codeB, problemLanguage);
- const gameResultKind = checkGameResultKind(gameStateKind, stateA, stateB);
+ const gameResultKind = checkGameResultKind(gameStateKind, stateA, stateB);
- const topBg = gameResultKind
- ? gameResultKind === "winA"
- ? "bg-orange-400"
- : gameResultKind === "winB"
- ? "bg-purple-400"
- : "bg-brand-600"
- : "bg-brand-600";
+ const topBg = gameResultKind
+ ? gameResultKind === "winA"
+ ? "bg-orange-400"
+ : gameResultKind === "winB"
+ ? "bg-purple-400"
+ : "bg-brand-600"
+ : "bg-brand-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 gap-4 justify-start md:justify-between items-center my-auto">
- <div className="flex gap-6 items-center">
- {playerProfileA?.iconPath && (
- <UserIcon
- iconPath={playerProfileA.iconPath}
- displayName={playerProfileA.displayName}
- className="w-12 h-12 my-auto"
- />
- )}
- <div className="hidden md:block text-4xl">
- {playerProfileA?.displayName}
- </div>
- </div>
- <div className="text-2xl md:text-6xl">
- <Score status={statusA} score={scoreA} />
- </div>
- </div>
- <div className="font-bold text-center">
- <div className="text-gray-100">{gameDisplayName}</div>
- {gameResultKind ? (
- <div className="text-3xl">
- {gameResultKind === "winA"
- ? `勝者 ${playerProfileA!.displayName}`
- : gameResultKind === "winB"
- ? `勝者 ${playerProfileB!.displayName}`
- : "引き分け"}
- </div>
- ) : (
- <LeftTime sec={leftTimeSeconds} />
- )}
- </div>
- <div className="font-bold flex gap-4 justify-end md:justify-between items-center my-auto">
- <div className="text-2xl md:text-6xl">
- <Score status={statusB} score={scoreB} />
- </div>
- <div className="flex gap-6 items-center text-end">
- <div className="hidden md:block text-4xl">
- {playerProfileB?.displayName}
- </div>
- {playerProfileB?.iconPath && (
- <UserIcon
- iconPath={playerProfileB.iconPath}
- displayName={playerProfileB.displayName}
- className="w-12 h-12 my-auto"
- />
- )}
- </div>
- </div>
- </div>
- <ScoreBar
- scoreA={scoreA}
- scoreB={scoreB}
- bgA="bg-orange-400"
- bgB="bg-purple-400"
- />
- <ThreeColumnLayout>
- <TitledColumn
- title={<SubmitStatusLabel status={statusA} />}
- className="order-2 md:order-1"
- >
- <FoldableBorderedContainerWithCaption
- caption={`コードサイズ: ${codeSizeA}`}
- >
- <CodeBlock code={codeA} language={problemLanguage} />
- </FoldableBorderedContainerWithCaption>
- </TitledColumn>
- <TitledColumn title={problemTitle} className="order-1 md:order-2">
- <ProblemColumnContent
- description={problemDescription}
- language={problemLanguage}
- sampleCode={sampleCode}
- />
- <RankingTable problemLanguage={problemLanguage} />
- </TitledColumn>
- <TitledColumn
- title={<SubmitStatusLabel status={statusB} />}
- className="order-3"
- >
- <FoldableBorderedContainerWithCaption
- caption={`コードサイズ: ${codeSizeB}`}
- >
- <CodeBlock code={codeB} language={problemLanguage} />
- </FoldableBorderedContainerWithCaption>
- </TitledColumn>
- </ThreeColumnLayout>
- </div>
- );
+ 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 gap-4 justify-start md:justify-between items-center my-auto">
+ <div className="flex gap-6 items-center">
+ {playerProfileA?.iconPath && (
+ <UserIcon
+ iconPath={playerProfileA.iconPath}
+ displayName={playerProfileA.displayName}
+ className="w-12 h-12 my-auto"
+ />
+ )}
+ <div className="hidden md:block text-4xl">
+ {playerProfileA?.displayName}
+ </div>
+ </div>
+ <div className="text-2xl md:text-6xl">
+ <Score status={statusA} score={scoreA} />
+ </div>
+ </div>
+ <div className="font-bold text-center">
+ <div className="text-gray-100">{gameDisplayName}</div>
+ {gameResultKind ? (
+ <div className="text-3xl">
+ {gameResultKind === "winA"
+ ? `勝者 ${playerProfileA!.displayName}`
+ : gameResultKind === "winB"
+ ? `勝者 ${playerProfileB!.displayName}`
+ : "引き分け"}
+ </div>
+ ) : (
+ <LeftTime sec={leftTimeSeconds} />
+ )}
+ </div>
+ <div className="font-bold flex gap-4 justify-end md:justify-between items-center my-auto">
+ <div className="text-2xl md:text-6xl">
+ <Score status={statusB} score={scoreB} />
+ </div>
+ <div className="flex gap-6 items-center text-end">
+ <div className="hidden md:block text-4xl">
+ {playerProfileB?.displayName}
+ </div>
+ {playerProfileB?.iconPath && (
+ <UserIcon
+ iconPath={playerProfileB.iconPath}
+ displayName={playerProfileB.displayName}
+ className="w-12 h-12 my-auto"
+ />
+ )}
+ </div>
+ </div>
+ </div>
+ <ScoreBar
+ scoreA={scoreA}
+ scoreB={scoreB}
+ bgA="bg-orange-400"
+ bgB="bg-purple-400"
+ />
+ <ThreeColumnLayout>
+ <TitledColumn
+ title={<SubmitStatusLabel status={statusA} />}
+ className="order-2 md:order-1"
+ >
+ <FoldableBorderedContainerWithCaption
+ caption={`コードサイズ: ${codeSizeA}`}
+ >
+ <CodeBlock code={codeA} language={problemLanguage} />
+ </FoldableBorderedContainerWithCaption>
+ </TitledColumn>
+ <TitledColumn title={problemTitle} className="order-1 md:order-2">
+ <ProblemColumnContent
+ description={problemDescription}
+ language={problemLanguage}
+ sampleCode={sampleCode}
+ />
+ <RankingTable problemLanguage={problemLanguage} />
+ </TitledColumn>
+ <TitledColumn
+ title={<SubmitStatusLabel status={statusB} />}
+ className="order-3"
+ >
+ <FoldableBorderedContainerWithCaption
+ caption={`コードサイズ: ${codeSizeB}`}
+ >
+ <CodeBlock code={codeB} language={problemLanguage} />
+ </FoldableBorderedContainerWithCaption>
+ </TitledColumn>
+ </ThreeColumnLayout>
+ </div>
+ );
}
diff --git a/frontend/app/components/GolfWatchApps/GolfWatchAppGamingMultiplayer.tsx b/frontend/app/components/GolfWatchApps/GolfWatchAppGamingMultiplayer.tsx
index 320709b..ca2e16f 100644
--- a/frontend/app/components/GolfWatchApps/GolfWatchAppGamingMultiplayer.tsx
+++ b/frontend/app/components/GolfWatchApps/GolfWatchAppGamingMultiplayer.tsx
@@ -8,43 +8,43 @@ import TitledColumn from "../TitledColumn";
import TwoColumnLayout from "../TwoColumnLayout";
type Props = {
- gameDisplayName: string;
- problemTitle: string;
- problemDescription: string;
- problemLanguage: SupportedLanguage;
- sampleCode: string;
+ gameDisplayName: string;
+ problemTitle: string;
+ problemDescription: string;
+ problemLanguage: SupportedLanguage;
+ sampleCode: string;
};
export default function GolfWatchAppGamingMultiplayer({
- gameDisplayName,
- problemTitle,
- problemDescription,
- problemLanguage,
- sampleCode,
+ gameDisplayName,
+ problemTitle,
+ problemDescription,
+ problemLanguage,
+ sampleCode,
}: Props) {
- const leftTimeSeconds = useAtomValue(gamingLeftTimeSecondsAtom)!;
+ const leftTimeSeconds = useAtomValue(gamingLeftTimeSecondsAtom)!;
- return (
- <div className="min-h-screen bg-gray-100 flex flex-col">
- <div className="text-white bg-brand-600 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>
- <LeftTime sec={leftTimeSeconds} />
- </div>
- <div className="font-bold flex justify-between my-auto"></div>
- </div>
- <TwoColumnLayout>
- <ProblemColumn
- title={problemTitle}
- description={problemDescription}
- language={problemLanguage}
- sampleCode={sampleCode}
- />
- <TitledColumn title="順位表">
- <RankingTable problemLanguage={problemLanguage} />
- </TitledColumn>
- </TwoColumnLayout>
- </div>
- );
+ return (
+ <div className="min-h-screen bg-gray-100 flex flex-col">
+ <div className="text-white bg-brand-600 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>
+ <LeftTime sec={leftTimeSeconds} />
+ </div>
+ <div className="font-bold flex justify-between my-auto"></div>
+ </div>
+ <TwoColumnLayout>
+ <ProblemColumn
+ title={problemTitle}
+ description={problemDescription}
+ language={problemLanguage}
+ sampleCode={sampleCode}
+ />
+ <TitledColumn title="順位表">
+ <RankingTable problemLanguage={problemLanguage} />
+ </TitledColumn>
+ </TwoColumnLayout>
+ </div>
+ );
}
diff --git a/frontend/app/components/GolfWatchApps/GolfWatchAppLoading.tsx b/frontend/app/components/GolfWatchApps/GolfWatchAppLoading.tsx
index 7169d24..a1b3f77 100644
--- a/frontend/app/components/GolfWatchApps/GolfWatchAppLoading.tsx
+++ b/frontend/app/components/GolfWatchApps/GolfWatchAppLoading.tsx
@@ -1,9 +1,9 @@
export default function GolfWatchAppLoading() {
- 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>
- );
+ 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/GolfWatchAppStarting.tsx b/frontend/app/components/GolfWatchApps/GolfWatchAppStarting.tsx
index d9e004e..3083a07 100644
--- a/frontend/app/components/GolfWatchApps/GolfWatchAppStarting.tsx
+++ b/frontend/app/components/GolfWatchApps/GolfWatchAppStarting.tsx
@@ -2,20 +2,20 @@ import { useAtomValue } from "jotai";
import { startingLeftTimeSecondsAtom } from "../../states/watch";
type Props = {
- gameDisplayName: string;
+ gameDisplayName: string;
};
export default function GolfWatchAppStarting({ gameDisplayName }: Props) {
- const leftTimeSeconds = useAtomValue(startingLeftTimeSecondsAtom)!;
+ const leftTimeSeconds = useAtomValue(startingLeftTimeSecondsAtom)!;
- return (
- <div className="min-h-screen bg-gray-100 flex flex-col">
- <div className="text-white bg-brand-600 p-10 text-center">
- <div className="text-4xl font-bold">{gameDisplayName}</div>
- </div>
- <div className="text-center text-black font-black text-10xl">
- {leftTimeSeconds}
- </div>
- </div>
- );
+ return (
+ <div className="min-h-screen bg-gray-100 flex flex-col">
+ <div className="text-white bg-brand-600 p-10 text-center">
+ <div className="text-4xl font-bold">{gameDisplayName}</div>
+ </div>
+ <div className="text-center text-black font-black text-10xl">
+ {leftTimeSeconds}
+ </div>
+ </div>
+ );
}
diff --git a/frontend/app/components/GolfWatchApps/GolfWatchAppWaiting1v1.tsx b/frontend/app/components/GolfWatchApps/GolfWatchAppWaiting1v1.tsx
index c7db3a7..48c3ab2 100644
--- a/frontend/app/components/GolfWatchApps/GolfWatchAppWaiting1v1.tsx
+++ b/frontend/app/components/GolfWatchApps/GolfWatchAppWaiting1v1.tsx
@@ -2,34 +2,34 @@ import type { PlayerProfile } from "../../types/PlayerProfile";
import PlayerNameAndIcon from "../PlayerNameAndIcon";
type Props = {
- gameDisplayName: string;
- playerProfileA: PlayerProfile | null;
- playerProfileB: PlayerProfile | null;
+ gameDisplayName: string;
+ playerProfileA: PlayerProfile | null;
+ playerProfileB: PlayerProfile | null;
};
export default function GolfWatchAppWaiting1v1({
- gameDisplayName,
- playerProfileA,
- playerProfileB,
+ 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-brand-600 p-10">
- <div className="text-4xl">{gameDisplayName}</div>
- </div>
- <div className="grow grid grid-cols-3 gap-10 mx-auto text-black">
- {playerProfileA ? (
- <PlayerNameAndIcon profile={playerProfileA} />
- ) : (
- <div></div>
- )}
- <div className="text-8xl my-auto">vs.</div>
- {playerProfileB ? (
- <PlayerNameAndIcon profile={playerProfileB} />
- ) : (
- <div></div>
- )}
- </div>
- </div>
- );
+ return (
+ <div className="min-h-screen bg-gray-100 flex flex-col font-bold text-center">
+ <div className="text-white bg-brand-600 p-10">
+ <div className="text-4xl">{gameDisplayName}</div>
+ </div>
+ <div className="grow grid grid-cols-3 gap-10 mx-auto text-black">
+ {playerProfileA ? (
+ <PlayerNameAndIcon profile={playerProfileA} />
+ ) : (
+ <div></div>
+ )}
+ <div className="text-8xl my-auto">vs.</div>
+ {playerProfileB ? (
+ <PlayerNameAndIcon profile={playerProfileB} />
+ ) : (
+ <div></div>
+ )}
+ </div>
+ </div>
+ );
}
diff --git a/frontend/app/components/GolfWatchApps/GolfWatchAppWaitingMultiplayer.tsx b/frontend/app/components/GolfWatchApps/GolfWatchAppWaitingMultiplayer.tsx
index 72757f3..de742b4 100644
--- a/frontend/app/components/GolfWatchApps/GolfWatchAppWaitingMultiplayer.tsx
+++ b/frontend/app/components/GolfWatchApps/GolfWatchAppWaitingMultiplayer.tsx
@@ -1,15 +1,15 @@
type Props = {
- gameDisplayName: string;
+ gameDisplayName: string;
};
export default function GolfWatchAppWaitingMultiplayer({
- gameDisplayName,
+ gameDisplayName,
}: Props) {
- return (
- <div className="min-h-screen bg-gray-100 flex flex-col font-bold text-center">
- <div className="text-white bg-brand-600 p-10">
- <div className="text-4xl">{gameDisplayName}</div>
- </div>
- </div>
- );
+ return (
+ <div className="min-h-screen bg-gray-100 flex flex-col font-bold text-center">
+ <div className="text-white bg-brand-600 p-10">
+ <div className="text-4xl">{gameDisplayName}</div>
+ </div>
+ </div>
+ );
}
diff --git a/frontend/app/components/InputText.test.tsx b/frontend/app/components/InputText.test.tsx
index 6fbdd27..8acf6f2 100644
--- a/frontend/app/components/InputText.test.tsx
+++ b/frontend/app/components/InputText.test.tsx
@@ -6,31 +6,31 @@ import { afterEach, describe, expect, test } from "vitest";
import InputText from "./InputText";
afterEach(() => {
- cleanup();
+ cleanup();
});
describe("InputText", () => {
- test("renders an input element", () => {
- render(<InputText data-testid="input" />);
- const input = screen.getByTestId("input");
- expect(input.tagName).toBe("INPUT");
- });
+ test("renders an input element", () => {
+ render(<InputText data-testid="input" />);
+ const input = screen.getByTestId("input");
+ expect(input.tagName).toBe("INPUT");
+ });
- test("passes placeholder prop", () => {
- render(<InputText placeholder="Enter text" data-testid="input" />);
- const input = screen.getByTestId("input") as HTMLInputElement;
- expect(input.placeholder).toBe("Enter text");
- });
+ test("passes placeholder prop", () => {
+ render(<InputText placeholder="Enter text" data-testid="input" />);
+ const input = screen.getByTestId("input") as HTMLInputElement;
+ expect(input.placeholder).toBe("Enter text");
+ });
- test("passes type prop", () => {
- render(<InputText type="password" data-testid="input" />);
- const input = screen.getByTestId("input") as HTMLInputElement;
- expect(input.type).toBe("password");
- });
+ test("passes type prop", () => {
+ render(<InputText type="password" data-testid="input" />);
+ const input = screen.getByTestId("input") as HTMLInputElement;
+ expect(input.type).toBe("password");
+ });
- test("has border styling", () => {
- render(<InputText data-testid="input" />);
- const input = screen.getByTestId("input");
- expect(input.className).toContain("border-brand-600");
- });
+ test("has border styling", () => {
+ render(<InputText data-testid="input" />);
+ const input = screen.getByTestId("input");
+ expect(input.className).toContain("border-brand-600");
+ });
});
diff --git a/frontend/app/components/InputText.tsx b/frontend/app/components/InputText.tsx
index 9b57a74..76ca860 100644
--- a/frontend/app/components/InputText.tsx
+++ b/frontend/app/components/InputText.tsx
@@ -3,10 +3,10 @@ import React from "react";
type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
export default function InputText(props: InputProps) {
- return (
- <input
- {...props}
- className="p-2 block w-full border border-brand-600 rounded-md transition duration-300 focus:ring-3 focus:ring-brand-400 focus:outline-hidden"
- />
- );
+ return (
+ <input
+ {...props}
+ className="p-2 block w-full border border-brand-600 rounded-md transition duration-300 focus:ring-3 focus:ring-brand-400 focus:outline-hidden"
+ />
+ );
}
diff --git a/frontend/app/components/NavigateLink.tsx b/frontend/app/components/NavigateLink.tsx
index 8741121..712c5ce 100644
--- a/frontend/app/components/NavigateLink.tsx
+++ b/frontend/app/components/NavigateLink.tsx
@@ -1,18 +1,18 @@
import { Link } from "wouter";
export default function NavigateLink({
- to,
- children,
+ to,
+ children,
}: {
- to: string;
- children: React.ReactNode;
+ to: string;
+ children: React.ReactNode;
}) {
- return (
- <Link
- to={to}
- className="text-lg text-white bg-brand-600 px-4 py-2 border-2 border-brand-50 rounded-sm transition duration-300 hover:bg-brand-500 focus:ring-3 focus:ring-brand-400 focus:outline-hidden"
- >
- {children}
- </Link>
- );
+ return (
+ <Link
+ to={to}
+ className="text-lg text-white bg-brand-600 px-4 py-2 border-2 border-brand-50 rounded-sm transition duration-300 hover:bg-brand-500 focus:ring-3 focus:ring-brand-400 focus:outline-hidden"
+ >
+ {children}
+ </Link>
+ );
}
diff --git a/frontend/app/components/PlayerNameAndIcon.test.tsx b/frontend/app/components/PlayerNameAndIcon.test.tsx
index 5a9dd49..e7b851c 100644
--- a/frontend/app/components/PlayerNameAndIcon.test.tsx
+++ b/frontend/app/components/PlayerNameAndIcon.test.tsx
@@ -6,34 +6,34 @@ import { afterEach, describe, expect, test } from "vitest";
import PlayerNameAndIcon from "./PlayerNameAndIcon";
afterEach(() => {
- cleanup();
+ cleanup();
});
describe("PlayerNameAndIcon", () => {
- test("renders display name", () => {
- render(
- <PlayerNameAndIcon
- profile={{ id: 1, displayName: "Alice", iconPath: null }}
- />,
- );
- expect(screen.getByText("Alice")).toBeDefined();
- });
+ test("renders display name", () => {
+ render(
+ <PlayerNameAndIcon
+ profile={{ id: 1, displayName: "Alice", iconPath: null }}
+ />,
+ );
+ expect(screen.getByText("Alice")).toBeDefined();
+ });
- test("does not render icon when iconPath is null", () => {
- render(
- <PlayerNameAndIcon
- profile={{ id: 1, displayName: "Bob", iconPath: null }}
- />,
- );
- expect(screen.queryByRole("img")).toBeNull();
- });
+ test("does not render icon when iconPath is null", () => {
+ render(
+ <PlayerNameAndIcon
+ profile={{ id: 1, displayName: "Bob", iconPath: null }}
+ />,
+ );
+ expect(screen.queryByRole("img")).toBeNull();
+ });
- test("renders icon when iconPath is provided", () => {
- render(
- <PlayerNameAndIcon
- profile={{ id: 1, displayName: "Carol", iconPath: "icons/carol.png" }}
- />,
- );
- expect(screen.getByAltText("Carol のアイコン")).toBeDefined();
- });
+ test("renders icon when iconPath is provided", () => {
+ render(
+ <PlayerNameAndIcon
+ profile={{ id: 1, displayName: "Carol", iconPath: "icons/carol.png" }}
+ />,
+ );
+ expect(screen.getByAltText("Carol のアイコン")).toBeDefined();
+ });
});
diff --git a/frontend/app/components/PlayerNameAndIcon.tsx b/frontend/app/components/PlayerNameAndIcon.tsx
index 92b757d..56423c1 100644
--- a/frontend/app/components/PlayerNameAndIcon.tsx
+++ b/frontend/app/components/PlayerNameAndIcon.tsx
@@ -2,20 +2,20 @@ import { PlayerProfile } from "../types/PlayerProfile";
import UserIcon from "./UserIcon";
type Props = {
- profile: PlayerProfile;
+ profile: PlayerProfile;
};
export default function PlayerNameAndIcon({ profile }: Props) {
- return (
- <div className="flex flex-col gap-6 my-auto items-center">
- <div className="text-6xl">{profile.displayName}</div>
- {profile.iconPath && (
- <UserIcon
- iconPath={profile.iconPath}
- displayName={profile.displayName}
- className="w-48 h-48"
- />
- )}
- </div>
- );
+ return (
+ <div className="flex flex-col gap-6 my-auto items-center">
+ <div className="text-6xl">{profile.displayName}</div>
+ {profile.iconPath && (
+ <UserIcon
+ iconPath={profile.iconPath}
+ displayName={profile.displayName}
+ className="w-48 h-48"
+ />
+ )}
+ </div>
+ );
}
diff --git a/frontend/app/components/ProtectedRoute.tsx b/frontend/app/components/ProtectedRoute.tsx
index b943696..5f564a0 100644
--- a/frontend/app/components/ProtectedRoute.tsx
+++ b/frontend/app/components/ProtectedRoute.tsx
@@ -2,19 +2,19 @@ import { Redirect } from "wouter";
import { useAuth } from "../hooks/useAuth";
export default function ProtectedRoute({
- children,
+ children,
}: {
- children: React.ReactNode;
+ children: React.ReactNode;
}) {
- const { isLoggedIn, isLoading } = useAuth();
+ const { isLoggedIn, isLoading } = useAuth();
- if (isLoading) {
- return null;
- }
+ if (isLoading) {
+ return null;
+ }
- if (!isLoggedIn) {
- return <Redirect to="/login" />;
- }
+ if (!isLoggedIn) {
+ return <Redirect to="/login" />;
+ }
- return <>{children}</>;
+ return <>{children}</>;
}
diff --git a/frontend/app/components/PublicOnlyRoute.tsx b/frontend/app/components/PublicOnlyRoute.tsx
index 7b3ef9d..e28f5ee 100644
--- a/frontend/app/components/PublicOnlyRoute.tsx
+++ b/frontend/app/components/PublicOnlyRoute.tsx
@@ -2,19 +2,19 @@ import { Redirect } from "wouter";
import { useAuth } from "../hooks/useAuth";
export default function PublicOnlyRoute({
- children,
+ children,
}: {
- children: React.ReactNode;
+ children: React.ReactNode;
}) {
- const { isLoggedIn, isLoading } = useAuth();
+ const { isLoggedIn, isLoading } = useAuth();
- if (isLoading) {
- return null;
- }
+ if (isLoading) {
+ return null;
+ }
- if (isLoggedIn) {
- return <Redirect to="/dashboard" />;
- }
+ if (isLoggedIn) {
+ return <Redirect to="/dashboard" />;
+ }
- return <>{children}</>;
+ return <>{children}</>;
}
diff --git a/frontend/app/components/SubmitButton.test.tsx b/frontend/app/components/SubmitButton.test.tsx
index 9a8085e..06cf5f5 100644
--- a/frontend/app/components/SubmitButton.test.tsx
+++ b/frontend/app/components/SubmitButton.test.tsx
@@ -6,36 +6,36 @@ import { afterEach, describe, expect, test } from "vitest";
import SubmitButton from "./SubmitButton";
afterEach(() => {
- cleanup();
+ cleanup();
});
describe("SubmitButton", () => {
- test("renders children text", () => {
- render(<SubmitButton>Submit</SubmitButton>);
- expect(screen.getByText("Submit")).toBeDefined();
- });
+ test("renders children text", () => {
+ render(<SubmitButton>Submit</SubmitButton>);
+ expect(screen.getByText("Submit")).toBeDefined();
+ });
- test("renders as a button element", () => {
- render(<SubmitButton>Click</SubmitButton>);
- const button = screen.getByText("Click");
- expect(button.tagName).toBe("BUTTON");
- });
+ test("renders as a button element", () => {
+ render(<SubmitButton>Click</SubmitButton>);
+ const button = screen.getByText("Click");
+ expect(button.tagName).toBe("BUTTON");
+ });
- test("can be disabled", () => {
- render(<SubmitButton disabled>Submit</SubmitButton>);
- const button = screen.getByText("Submit") as HTMLButtonElement;
- expect(button.disabled).toBe(true);
- });
+ test("can be disabled", () => {
+ render(<SubmitButton disabled>Submit</SubmitButton>);
+ const button = screen.getByText("Submit") as HTMLButtonElement;
+ expect(button.disabled).toBe(true);
+ });
- test("is not disabled by default", () => {
- render(<SubmitButton>Submit</SubmitButton>);
- const button = screen.getByText("Submit") as HTMLButtonElement;
- expect(button.disabled).toBe(false);
- });
+ test("is not disabled by default", () => {
+ render(<SubmitButton>Submit</SubmitButton>);
+ const button = screen.getByText("Submit") as HTMLButtonElement;
+ expect(button.disabled).toBe(false);
+ });
- test("has brand-600 background styling", () => {
- render(<SubmitButton>Submit</SubmitButton>);
- const button = screen.getByText("Submit");
- expect(button.className).toContain("bg-brand-600");
- });
+ test("has brand-600 background styling", () => {
+ render(<SubmitButton>Submit</SubmitButton>);
+ const button = screen.getByText("Submit");
+ expect(button.className).toContain("bg-brand-600");
+ });
});
diff --git a/frontend/app/components/SubmitButton.tsx b/frontend/app/components/SubmitButton.tsx
index 916c20f..acb66f6 100644
--- a/frontend/app/components/SubmitButton.tsx
+++ b/frontend/app/components/SubmitButton.tsx
@@ -3,10 +3,10 @@ import React from "react";
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement>;
export default function SubmitButton(props: ButtonProps) {
- return (
- <button
- {...props}
- className="text-lg text-white px-4 py-2 bg-brand-600 disabled:bg-gray-400 disabled:cursor-not-allowed rounded-sm transition duration-300 hover:bg-brand-500 focus:ring-3 focus:ring-brand-400 focus:outline-hidden"
- />
- );
+ return (
+ <button
+ {...props}
+ className="text-lg text-white px-4 py-2 bg-brand-600 disabled:bg-gray-400 disabled:cursor-not-allowed rounded-sm transition duration-300 hover:bg-brand-500 focus:ring-3 focus:ring-brand-400 focus:outline-hidden"
+ />
+ );
}
diff --git a/frontend/app/components/SubmitStatusLabel.test.tsx b/frontend/app/components/SubmitStatusLabel.test.tsx
index cdedf9e..08ba636 100644
--- a/frontend/app/components/SubmitStatusLabel.test.tsx
+++ b/frontend/app/components/SubmitStatusLabel.test.tsx
@@ -6,47 +6,47 @@ import { afterEach, describe, expect, test } from "vitest";
import SubmitStatusLabel from "./SubmitStatusLabel";
afterEach(() => {
- cleanup();
+ cleanup();
});
describe("SubmitStatusLabel", () => {
- test("renders '提出待ち' for none status", () => {
- render(<SubmitStatusLabel status="none" />);
- expect(screen.getByText("提出待ち")).toBeDefined();
- });
+ test("renders '提出待ち' for none status", () => {
+ render(<SubmitStatusLabel status="none" />);
+ expect(screen.getByText("提出待ち")).toBeDefined();
+ });
- test("renders '実行中...' for running status", () => {
- render(<SubmitStatusLabel status="running" />);
- expect(screen.getByText("実行中...")).toBeDefined();
- });
+ test("renders '実行中...' for running status", () => {
+ render(<SubmitStatusLabel status="running" />);
+ expect(screen.getByText("実行中...")).toBeDefined();
+ });
- test("renders '成功' for success status", () => {
- render(<SubmitStatusLabel status="success" />);
- expect(screen.getByText("成功")).toBeDefined();
- });
+ test("renders '成功' for success status", () => {
+ render(<SubmitStatusLabel status="success" />);
+ expect(screen.getByText("成功")).toBeDefined();
+ });
- test("renders 'テスト失敗' for wrong_answer status", () => {
- render(<SubmitStatusLabel status="wrong_answer" />);
- expect(screen.getByText("テスト失敗")).toBeDefined();
- });
+ test("renders 'テスト失敗' for wrong_answer status", () => {
+ render(<SubmitStatusLabel status="wrong_answer" />);
+ expect(screen.getByText("テスト失敗")).toBeDefined();
+ });
- test("renders '時間切れ' for timeout status", () => {
- render(<SubmitStatusLabel status="timeout" />);
- expect(screen.getByText("時間切れ")).toBeDefined();
- });
+ test("renders '時間切れ' for timeout status", () => {
+ render(<SubmitStatusLabel status="timeout" />);
+ expect(screen.getByText("時間切れ")).toBeDefined();
+ });
- test("renders 'コンパイルエラー' for compile_error status", () => {
- render(<SubmitStatusLabel status="compile_error" />);
- expect(screen.getByText("コンパイルエラー")).toBeDefined();
- });
+ test("renders 'コンパイルエラー' for compile_error status", () => {
+ render(<SubmitStatusLabel status="compile_error" />);
+ expect(screen.getByText("コンパイルエラー")).toBeDefined();
+ });
- test("renders '実行時エラー' for runtime_error status", () => {
- render(<SubmitStatusLabel status="runtime_error" />);
- expect(screen.getByText("実行時エラー")).toBeDefined();
- });
+ test("renders '実行時エラー' for runtime_error status", () => {
+ render(<SubmitStatusLabel status="runtime_error" />);
+ expect(screen.getByText("実行時エラー")).toBeDefined();
+ });
- test("renders '!内部エラー!' for internal_error status", () => {
- render(<SubmitStatusLabel status="internal_error" />);
- expect(screen.getByText("!内部エラー!")).toBeDefined();
- });
+ test("renders '!内部エラー!' for internal_error status", () => {
+ render(<SubmitStatusLabel status="internal_error" />);
+ expect(screen.getByText("!内部エラー!")).toBeDefined();
+ });
});
diff --git a/frontend/app/components/SubmitStatusLabel.tsx b/frontend/app/components/SubmitStatusLabel.tsx
index b3091b0..e511db2 100644
--- a/frontend/app/components/SubmitStatusLabel.tsx
+++ b/frontend/app/components/SubmitStatusLabel.tsx
@@ -1,26 +1,26 @@
import type { components } from "../api/schema";
type Props = {
- status: components["schemas"]["ExecutionStatus"];
+ status: components["schemas"]["ExecutionStatus"];
};
export default function SubmitStatusLabel({ status }: Props) {
- switch (status) {
- case "none":
- return "提出待ち";
- 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 "!内部エラー!";
- }
+ switch (status) {
+ case "none":
+ return "提出待ち";
+ 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/components/ThreeColumnLayout.tsx b/frontend/app/components/ThreeColumnLayout.tsx
index d2a5ba5..694b9cb 100644
--- a/frontend/app/components/ThreeColumnLayout.tsx
+++ b/frontend/app/components/ThreeColumnLayout.tsx
@@ -1,13 +1,13 @@
import React from "react";
type Props = {
- children: React.ReactNode;
+ children: React.ReactNode;
};
export default function ThreeColumnLayout({ children }: Props) {
- return (
- <div className="grow grid grid-cols-1 md:grid-cols-3 md:divide-x divide-gray-300">
- {children}
- </div>
- );
+ return (
+ <div className="grow grid grid-cols-1 md:grid-cols-3 md:divide-x divide-gray-300">
+ {children}
+ </div>
+ );
}
diff --git a/frontend/app/components/TitledColumn.tsx b/frontend/app/components/TitledColumn.tsx
index a26271b..ac43091 100644
--- a/frontend/app/components/TitledColumn.tsx
+++ b/frontend/app/components/TitledColumn.tsx
@@ -1,16 +1,16 @@
import React from "react";
type Props = {
- children: React.ReactNode;
- title: React.ReactNode;
- className?: string;
+ children: React.ReactNode;
+ title: React.ReactNode;
+ className?: string;
};
export default function TitledColumn({ children, title, className }: Props) {
- return (
- <div className={`p-4 flex flex-col gap-4 ${className}`}>
- <div className="text-center text-xl font-bold">{title}</div>
- {children}
- </div>
- );
+ return (
+ <div className={`p-4 flex flex-col gap-4 ${className}`}>
+ <div className="text-center text-xl font-bold">{title}</div>
+ {children}
+ </div>
+ );
}
diff --git a/frontend/app/components/TwoColumnLayout.tsx b/frontend/app/components/TwoColumnLayout.tsx
index 68eab32..83640ec 100644
--- a/frontend/app/components/TwoColumnLayout.tsx
+++ b/frontend/app/components/TwoColumnLayout.tsx
@@ -1,13 +1,13 @@
import React from "react";
type Props = {
- children: React.ReactNode;
+ children: React.ReactNode;
};
export default function TwoColumnLayout({ children }: Props) {
- return (
- <div className="grow grid grid-cols-1 md:grid-cols-2 md:divide-x divide-gray-300">
- {children}
- </div>
- );
+ return (
+ <div className="grow grid grid-cols-1 md:grid-cols-2 md:divide-x divide-gray-300">
+ {children}
+ </div>
+ );
}
diff --git a/frontend/app/components/UserIcon.test.tsx b/frontend/app/components/UserIcon.test.tsx
index 964aae1..5311234 100644
--- a/frontend/app/components/UserIcon.test.tsx
+++ b/frontend/app/components/UserIcon.test.tsx
@@ -6,57 +6,57 @@ import { afterEach, describe, expect, test } from "vitest";
import UserIcon from "./UserIcon";
afterEach(() => {
- cleanup();
+ cleanup();
});
describe("UserIcon", () => {
- test("renders an img element", () => {
- render(
- <UserIcon
- iconPath="icons/test.png"
- displayName="TestUser"
- className="w-16 h-16"
- />,
- );
- const img = screen.getByAltText("TestUser のアイコン");
- expect(img.tagName).toBe("IMG");
- });
+ test("renders an img element", () => {
+ render(
+ <UserIcon
+ iconPath="icons/test.png"
+ displayName="TestUser"
+ className="w-16 h-16"
+ />,
+ );
+ const img = screen.getByAltText("TestUser のアイコン");
+ expect(img.tagName).toBe("IMG");
+ });
- test("sets alt text with display name", () => {
- render(
- <UserIcon
- iconPath="icons/test.png"
- displayName="Alice"
- className="w-16 h-16"
- />,
- );
- expect(screen.getByAltText("Alice のアイコン")).toBeDefined();
- });
+ test("sets alt text with display name", () => {
+ render(
+ <UserIcon
+ iconPath="icons/test.png"
+ displayName="Alice"
+ className="w-16 h-16"
+ />,
+ );
+ expect(screen.getByAltText("Alice のアイコン")).toBeDefined();
+ });
- test("applies rounded-full and border classes", () => {
- render(
- <UserIcon
- iconPath="icons/test.png"
- displayName="Bob"
- className="w-16 h-16"
- />,
- );
- const img = screen.getByAltText("Bob のアイコン");
- expect(img.className).toContain("rounded-full");
- expect(img.className).toContain("border-4");
- expect(img.className).toContain("border-white");
- });
+ test("applies rounded-full and border classes", () => {
+ render(
+ <UserIcon
+ iconPath="icons/test.png"
+ displayName="Bob"
+ className="w-16 h-16"
+ />,
+ );
+ const img = screen.getByAltText("Bob のアイコン");
+ expect(img.className).toContain("rounded-full");
+ expect(img.className).toContain("border-4");
+ expect(img.className).toContain("border-white");
+ });
- test("applies custom className", () => {
- render(
- <UserIcon
- iconPath="icons/test.png"
- displayName="Bob"
- className="w-48 h-48"
- />,
- );
- const img = screen.getByAltText("Bob のアイコン");
- expect(img.className).toContain("w-48");
- expect(img.className).toContain("h-48");
- });
+ test("applies custom className", () => {
+ render(
+ <UserIcon
+ iconPath="icons/test.png"
+ displayName="Bob"
+ className="w-48 h-48"
+ />,
+ );
+ const img = screen.getByAltText("Bob のアイコン");
+ expect(img.className).toContain("w-48");
+ expect(img.className).toContain("h-48");
+ });
});
diff --git a/frontend/app/components/UserIcon.tsx b/frontend/app/components/UserIcon.tsx
index b2750dd..9c57c42 100644
--- a/frontend/app/components/UserIcon.tsx
+++ b/frontend/app/components/UserIcon.tsx
@@ -1,21 +1,21 @@
import { BASE_PATH } from "../config";
type Props = {
- iconPath: string;
- displayName: string;
- className: string;
+ iconPath: string;
+ displayName: string;
+ className: string;
};
export default function UserIcon({ iconPath, displayName, className }: Props) {
- return (
- <img
- src={
- process.env.NODE_ENV === "development"
- ? `http://localhost:8007${BASE_PATH}${iconPath}`
- : `${BASE_PATH}${iconPath}`
- }
- alt={`${displayName} のアイコン`}
- className={`rounded-full border-4 border-white ${className}`}
- />
- );
+ return (
+ <img
+ src={
+ process.env.NODE_ENV === "development"
+ ? `http://localhost:8007${BASE_PATH}${iconPath}`
+ : `${BASE_PATH}${iconPath}`
+ }
+ alt={`${displayName} のアイコン`}
+ className={`rounded-full border-4 border-white ${className}`}
+ />
+ );
}