diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index b43aa342..afbebec1 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -138,7 +138,7 @@ jobs: with: repository: ictsc/ictsc-rikka path: ictsc-rikka - ref: c9fb8c2731f6bd2d1eeb3c3083732e296162fa01 + ref: 023061a63b1a2fb425939c36bcb7dd8bd2235b7c # MariaDB cache - name: Cache a MariaDB Docker image @@ -252,7 +252,7 @@ jobs: working-directory: ictsc-rikka run: | set +e # curlのエラーを無視する - cp scripts/docker-compose.override.yml docker-compose.override.yml + cp scripts/compose.ci.yml compose.override.yml make up url="http://localhost:8080" diff --git a/frontend/octavio/.env b/frontend/octavio/.env index 8ca5d405..1bc8e9f1 100644 --- a/frontend/octavio/.env +++ b/frontend/octavio/.env @@ -10,3 +10,5 @@ NEXT_PUBLIC_RULE= NEXT_PUBLIC_SHORT_RULE= # 再展開時のモーダルに表示される再展開時の注意事項 NEXT_PUBLIC_RECREATE_RULE= +# 予選モードを有効化 +NEXT_PUBLIC_PRE_ROUND_MODE= \ No newline at end of file diff --git a/frontend/octavio/__e2e__/scoring/index.test.ts b/frontend/octavio/__e2e__/scoring/index.test.ts index 9fd5de0a..9282f484 100644 --- a/frontend/octavio/__e2e__/scoring/index.test.ts +++ b/frontend/octavio/__e2e__/scoring/index.test.ts @@ -20,7 +20,7 @@ test("画面項目が表示されること", async ({ page }) => { const problem1 = problems.nth(0).locator("td"); await expect(problem1.nth(1)).toHaveText("1/1/1"); await expect(problem1.nth(2)).toHaveText( - "00000000-0000-4000-a000-000000000000" + "00000000-0000-4000-a000-000000000000", ); await expect(problem1.nth(3)).toHaveText("abc"); await expect(problem1.nth(4)).toHaveText("問題タイトル1"); @@ -28,12 +28,11 @@ test("画面項目が表示されること", async ({ page }) => { await expect(problem1.nth(6)).toHaveText("100"); await expect(problem1.nth(7)).toHaveText("100"); await expect(problem1.nth(8)).toHaveText(""); - await expect(problem1.nth(9)).toHaveText("自分"); const problem2 = problems.nth(1).locator("td"); await expect(problem2.nth(1)).toHaveText("1/1/1"); await expect(problem2.nth(2)).toHaveText( - "00000000-0000-4000-a000-000000000001" + "00000000-0000-4000-a000-000000000001", ); await expect(problem2.nth(3)).toHaveText("def"); await expect(problem2.nth(4)).toHaveText("問題タイトル2"); @@ -41,12 +40,11 @@ test("画面項目が表示されること", async ({ page }) => { await expect(problem2.nth(6)).toHaveText("200"); await expect(problem2.nth(7)).toHaveText("200"); await expect(problem2.nth(8)).toHaveText(""); - await expect(problem2.nth(9)).toHaveText("自分"); const problem3 = problems.nth(2).locator("td"); await expect(problem3.nth(1)).toHaveText("1/1/1"); await expect(problem3.nth(2)).toHaveText( - "00000000-0000-4000-a000-000000000002" + "00000000-0000-4000-a000-000000000002", ); await expect(problem3.nth(3)).toHaveText("ghi"); await expect(problem3.nth(4)).toHaveText("問題タイトル3"); @@ -54,7 +52,6 @@ test("画面項目が表示されること", async ({ page }) => { await expect(problem3.nth(6)).toHaveText("300"); await expect(problem3.nth(7)).toHaveText("300"); await expect(problem3.nth(8)).toHaveText(""); - await expect(problem3.nth(9)).toHaveText("自分"); }); test("採点ページへ遷移できること", async ({ page }) => { @@ -83,10 +80,10 @@ test("問題のプレビューができること", async ({ page }) => { // then await expect(ScoringPage.ProblemPreviewProblemInfo(page)).toHaveText( - "問題タイトル1満点100 pt 採点基準100 pt採点する" + "問題タイトル1満点100 pt 採点基準100 pt採点する", ); await expect(ScoringPage.ProblemPreviewProblemContent(page)).toHaveText( - "問題内容1" + "問題内容1", ); }); diff --git a/frontend/octavio/__e2e__/users.test.ts b/frontend/octavio/__e2e__/users.test.ts index 84896afa..9f9a104d 100644 --- a/frontend/octavio/__e2e__/users.test.ts +++ b/frontend/octavio/__e2e__/users.test.ts @@ -20,45 +20,54 @@ test("画面項目が表示されること", async ({ page }) => { const user1 = await cols.nth(0).locator("td"); await expect(user1.nth(0)).toHaveText("user1"); await expect(user1.nth(1)).toHaveText("team1"); - await expect(user1.nth(2)).toHaveText("自己紹介内容1"); + await expect(user1.nth(2)).toHaveText("user-org1"); + await expect(user1.nth(3)).toHaveText("自己紹介内容1"); const user2 = await cols.nth(1).locator("td"); await expect(user2.nth(0)).toHaveText("user2"); await expect(user2.nth(1)).toHaveText("team1"); - await expect(user2.nth(2)).toHaveText(""); + await expect(user2.nth(2)).toHaveText("user-org1"); + await expect(user2.nth(3)).toHaveText(""); const user3 = await cols.nth(2).locator("td"); await expect(user3.nth(0)).toHaveText("user3"); await expect(user3.nth(1)).toHaveText("team1"); - await expect(user3.nth(2)).toHaveText(""); + await expect(user3.nth(2)).toHaveText("user-org1"); + await expect(user3.nth(3)).toHaveText(""); const user4 = await cols.nth(3).locator("td"); await expect(user4.nth(0)).toHaveText("user4"); await expect(user4.nth(1)).toHaveText("team2"); - await expect(user4.nth(2)).toHaveText("自己紹介内容2"); + await expect(user4.nth(2)).toHaveText("user-org2"); + await expect(user4.nth(3)).toHaveText("自己紹介内容2"); const user5 = await cols.nth(4).locator("td"); await expect(user5.nth(0)).toHaveText("user5"); await expect(user5.nth(1)).toHaveText("team2"); - await expect(user5.nth(2)).toHaveText(""); + await expect(user5.nth(2)).toHaveText("user-org2"); + await expect(user5.nth(3)).toHaveText(""); const user6 = await cols.nth(5).locator("td"); await expect(user6.nth(0)).toHaveText("user6"); await expect(user6.nth(1)).toHaveText("team2"); - await expect(user6.nth(2)).toHaveText(""); + await expect(user6.nth(2)).toHaveText("user-org2"); + await expect(user6.nth(3)).toHaveText(""); const user7 = await cols.nth(6).locator("td"); await expect(user7.nth(0)).toHaveText("user7"); await expect(user7.nth(1)).toHaveText("team3"); - await expect(user7.nth(2)).toHaveText("自己紹介内容3"); + await expect(user7.nth(2)).toHaveText("user-org3"); + await expect(user7.nth(3)).toHaveText("自己紹介内容3"); const user8 = await cols.nth(7).locator("td"); await expect(user8.nth(0)).toHaveText("user8"); await expect(user8.nth(1)).toHaveText("team3"); - await expect(user8.nth(2)).toHaveText(""); + await expect(user8.nth(2)).toHaveText("user-org3"); + await expect(user8.nth(3)).toHaveText(""); const user9 = await cols.nth(8).locator("td"); await expect(user9.nth(0)).toHaveText("user9"); await expect(user9.nth(1)).toHaveText("team3"); - await expect(user9.nth(2)).toHaveText(""); + await expect(user9.nth(2)).toHaveText("user-org3"); + await expect(user9.nth(3)).toHaveText(""); }); diff --git a/frontend/octavio/__test__/pages/login.test.tsx b/frontend/octavio/__test__/pages/login.test.tsx index b56e2549..5d22eb4b 100644 --- a/frontend/octavio/__test__/pages/login.test.tsx +++ b/frontend/octavio/__test__/pages/login.test.tsx @@ -63,7 +63,7 @@ describe("Login", () => { expect(screen.queryByPlaceholderText("ユーザー名")).toBeInTheDocument(); expect(screen.queryByPlaceholderText("パスワード")).toBeInTheDocument(); expect(loginButton).toBeInTheDocument(); - expect(loginButton).not.toHaveAttribute("loading"); + expect(loginButton).not.toHaveAttribute("btn-disabled"); // verify expect(useAuth).toHaveBeenCalledTimes(1); @@ -83,10 +83,10 @@ describe("Login", () => { // then expect( - screen.queryByText("ユーザー名を入力してください") + screen.queryByText("ユーザー名を入力してください"), ).toBeInTheDocument(); expect( - screen.queryByText("パスワードを入力して下さい") + screen.queryByText("パスワードを入力して下さい"), ).toBeInTheDocument(); // verify @@ -109,10 +109,10 @@ describe("Login", () => { // then expect( - screen.queryByText("ユーザー名を入力してください") + screen.queryByText("ユーザー名を入力してください"), ).toBeInTheDocument(); expect( - screen.queryByText("パスワードを入力して下さい") + screen.queryByText("パスワードを入力して下さい"), ).not.toBeInTheDocument(); }); @@ -132,10 +132,10 @@ describe("Login", () => { // then expect( - screen.queryByText("ユーザー名を入力してください") + screen.queryByText("ユーザー名を入力してください"), ).not.toBeInTheDocument(); expect( - screen.queryByText("パスワードを入力して下さい") + screen.queryByText("パスワードを入力して下さい"), ).toBeInTheDocument(); // verify @@ -214,7 +214,7 @@ describe("Login", () => { setTimeout(() => { resolve({ code: 200 }); }, 1000); - }) + }), ); (useAuth as Mock).mockReturnValue({ user: null, @@ -233,7 +233,7 @@ describe("Login", () => { }); // then - expect(loginButton).toHaveClass("loading"); + expect(loginButton).toHaveClass("btn-disabled"); // verify expect(useAuth).toHaveBeenCalledTimes(2); diff --git a/frontend/octavio/__test__/pages/problems/index.test.tsx b/frontend/octavio/__test__/pages/problems/index.test.tsx index 2958a93e..1f65df79 100644 --- a/frontend/octavio/__test__/pages/problems/index.test.tsx +++ b/frontend/octavio/__test__/pages/problems/index.test.tsx @@ -51,7 +51,7 @@ describe("Problems", () => { expect(screen.queryByText("テスト通知タイトル")).toBeInTheDocument(); expect(screen.getAllByTestId("markdown-preview")[1]).toHaveAttribute( "data-content", - "テスト通知本文" + "テスト通知本文", ); expect(screen.queryByText("XYZ")).toBeInTheDocument(); expect(screen.queryByText("テスト問題タイトル")).toBeInTheDocument(); @@ -203,6 +203,7 @@ describe("Problems", () => { title: "title", site: "site", shortRule: "# ルール本文", + preRoundMode: false, })); (useRecoilState as Mock).mockReturnValue([[], vi.fn()]); (useProblems as Mock).mockReturnValue({ @@ -218,7 +219,7 @@ describe("Problems", () => { // then expect(screen.getAllByTestId("markdown-preview")[0]).toHaveAttribute( "data-content", - "# ルール本文" + "# ルール本文", ); // verify diff --git a/frontend/octavio/__test__/pages/scoring/index.test.tsx b/frontend/octavio/__test__/pages/scoring/index.test.tsx index d3f21100..12d8052a 100644 --- a/frontend/octavio/__test__/pages/scoring/index.test.tsx +++ b/frontend/octavio/__test__/pages/scoring/index.test.tsx @@ -130,7 +130,6 @@ describe("Scoring", () => { expect(tds[6]).toHaveTextContent("100"); expect(tds[7]).toHaveTextContent("150"); expect(tds[8]).toHaveTextContent(""); - expect(tds[9]).toHaveTextContent("自分"); // then expect(useAuth).toHaveBeenCalledTimes(1); @@ -255,26 +254,6 @@ describe("Scoring", () => { expect(useProblems).toHaveBeenCalledTimes(2); }); - test("問題作成者id が自分でない場合空文字が表示される", () => { - // setup - (useAuth as Mock).mockReturnValue({ - user: testAdminUser, - }); - (useProblems as Mock).mockReturnValue({ - problems: [{ ...testProblem, author_id: "other" }], - isLoading: false, - }); - render(<Index />); - const tds = screen.queryAllByRole("cell"); - - // when - expect(tds[9]).toHaveTextContent(""); - - // then - expect(useAuth).toHaveBeenCalledTimes(1); - expect(useProblems).toHaveBeenCalledTimes(2); - }); - test("問題をクリックした場合、問題文が表示される", async () => { // setup (useAuth as Mock).mockReturnValue({ diff --git a/frontend/octavio/__test__/pages/signUp.test.tsx b/frontend/octavio/__test__/pages/signUp.test.tsx index 5889d76c..81860f52 100644 --- a/frontend/octavio/__test__/pages/signUp.test.tsx +++ b/frontend/octavio/__test__/pages/signUp.test.tsx @@ -10,6 +10,23 @@ import { Mock, vi } from "vitest"; import SignUp from "@/app/signUp/page"; import useAuth from "@/hooks/auth"; +class CustomError extends Error { + code: number; + + response: { data: { error: string } }; + + constructor( + message: string, + code: number, + response: { data: { error: string } }, + ) { + super(message); + this.code = code; + this.response = response; + Object.setPrototypeOf(this, CustomError.prototype); + } +} + vi.mock("@/hooks/auth"); vi.mock("@/components/Alerts", () => ({ ICTSCSuccessAlert: ({ @@ -93,10 +110,10 @@ describe("SignUp", () => { // then expect( - screen.queryByText("ユーザー名を入力してください") + screen.queryByText("ユーザー名を入力してください"), ).toBeInTheDocument(); expect( - screen.queryByText("パスワードを入力して下さい") + screen.queryByText("パスワードを入力して下さい"), ).toBeInTheDocument(); // verify @@ -119,10 +136,10 @@ describe("SignUp", () => { // then expect( - screen.queryByText("ユーザー名を入力してください") + screen.queryByText("ユーザー名を入力してください"), ).toBeInTheDocument(); expect( - screen.queryByText("パスワードを入力して下さい") + screen.queryByText("パスワードを入力して下さい"), ).not.toBeInTheDocument(); // verify @@ -145,10 +162,10 @@ describe("SignUp", () => { // then expect( - screen.queryByText("ユーザー名を入力してください") + screen.queryByText("ユーザー名を入力してください"), ).not.toBeInTheDocument(); expect( - screen.queryByText("パスワードを入力して下さい") + screen.queryByText("パスワードを入力して下さい"), ).toBeInTheDocument(); // verify @@ -172,7 +189,7 @@ describe("SignUp", () => { // then expect( - screen.queryByText("パスワードは8文字以上である必要があります") + screen.queryByText("パスワードは8文字以上である必要があります"), ).toBeInTheDocument(); // verify @@ -204,7 +221,7 @@ describe("SignUp", () => { expect(alert).toBeInTheDocument(); expect(alert).toHaveAttribute( "data-message", - "ユーザー登録に成功しました!" + "ユーザー登録に成功しました!", ); expect(alert).not.toHaveAttribute("data-sub-message"); @@ -215,9 +232,13 @@ describe("SignUp", () => { test("ユーザーが既に存在する場合にエラーが表示されることを確認する", async () => { // setup - const signUp = vi.fn().mockResolvedValue({ + const signUp = vi.fn().mockRejectedValue({ code: 400, - data: "Error 1062: Duplicate entry 'user' for key 'name'", + response: { + data: { + error: "Error 1062: Duplicate entry 'user' for key 'name'", + }, + }, }); (useAuth as Mock).mockReturnValue({ @@ -241,7 +262,7 @@ describe("SignUp", () => { expect(alert).toHaveAttribute("data-message", "エラーが発生しました"); expect(alert).toHaveAttribute( "data-sub-message", - "ユーザー名が重複しています。" + "ユーザー名が重複しています。", ); // verify @@ -250,9 +271,14 @@ describe("SignUp", () => { test("UserGroupID が無効な場合にエラーが表示されることを確認する", async () => { // setup - const signUp = vi.fn().mockResolvedValue({ + const signUp = vi.fn().mockRejectedValue({ code: 400, - data: "Error:Field validation for 'UserGroupID' failed on the 'required' tag", + response: { + data: { + error: + "Error:Field validation for 'UserGroupID' failed on the 'required' tag", + }, + }, }); (useAuth as Mock).mockReturnValue({ @@ -276,7 +302,7 @@ describe("SignUp", () => { expect(alert).toHaveAttribute("data-message", "エラーが発生しました"); expect(alert).toHaveAttribute( "data-sub-message", - "無効なユーザーグループです。" + "無効なユーザーグループです。", ); // verify @@ -285,9 +311,14 @@ describe("SignUp", () => { test("UserGroupID の uuid 形式が無効な場合にエラーが表示されることを確認する", async () => { // setup - const signUp = vi.fn().mockResolvedValue({ + const signUp = vi.fn().mockRejectedValue({ code: 400, - data: "Error:Field validation for 'UserGroupID' failed on the 'uuid' tag", + response: { + data: { + error: + "Error:Field validation for 'UserGroupID' failed on the 'uuid' tag", + }, + }, }); (useAuth as Mock).mockReturnValue({ @@ -311,7 +342,7 @@ describe("SignUp", () => { expect(alert).toHaveAttribute("data-message", "エラーが発生しました"); expect(alert).toHaveAttribute( "data-sub-message", - "無効なユーザーグループです。" + "無効なユーザーグループです。", ); // verify @@ -320,9 +351,14 @@ describe("SignUp", () => { test("InvitationCode が無効の場合", async () => { // setup - const signUp = vi.fn().mockResolvedValue({ + const signUp = vi.fn().mockRejectedValue({ code: 400, - data: "Error:Field validation for 'InvitationCode' failed on the 'required' tag", + response: { + data: { + error: + "Error:Field validation for 'InvitationCode' failed on the 'required' tag", + }, + }, }); (useAuth as Mock).mockReturnValue({ @@ -390,7 +426,7 @@ describe("SignUp", () => { setTimeout(() => { resolve({ code: 200 }); }, 1000); - }) + }), ); (useAuth as Mock).mockReturnValue({ user: null, @@ -408,7 +444,7 @@ describe("SignUp", () => { }); // then - expect(button).toHaveClass("loading"); + expect(button).toHaveClass("btn-disabled"); // verify expect(useAuth).toHaveBeenCalledTimes(2); diff --git a/frontend/octavio/__test__/pages/users.test.tsx b/frontend/octavio/__test__/pages/users.test.tsx index d8c8e35a..c219f4b0 100644 --- a/frontend/octavio/__test__/pages/users.test.tsx +++ b/frontend/octavio/__test__/pages/users.test.tsx @@ -38,7 +38,8 @@ describe("Users", () => { const cells = screen.getAllByRole("cell"); expect(cells[0]).toHaveTextContent(testUser.display_name); expect(cells[1]).toHaveTextContent(testUserGroup.name); - expect(cells[2]).toHaveTextContent(testUser.profile?.self_introduction!); + expect(cells[2]).toHaveTextContent(testUserGroup.organization); + expect(cells[3]).toHaveTextContent(testUser.profile?.self_introduction!); const links = screen.getAllByRole("link"); expect(links[0]).toHaveAttribute( "href", diff --git a/frontend/octavio/app/problems/[problemId]/_components/answer-list-section.tsx b/frontend/octavio/app/problems/[problemId]/_components/answer-list-section.tsx index a22031b8..155db759 100644 --- a/frontend/octavio/app/problems/[problemId]/_components/answer-list-section.tsx +++ b/frontend/octavio/app/problems/[problemId]/_components/answer-list-section.tsx @@ -5,6 +5,7 @@ import Image from "next/image"; import clsx from "clsx"; import { DateTime } from "luxon"; +import { preRoundMode } from "@/components/_const"; import ICTSCCard from "@/components/card"; import MarkdownPreview from "@/components/markdown-preview"; import useAnswers from "@/hooks/answer"; @@ -30,8 +31,12 @@ function AnswerListSection({ problem }: AnswerSectionProps) { <th className="w-[196px]">提出日時</th> <th className="w-[100px]">問題コード</th> <th>問題</th> - <th className="w-[100px]">得点</th> - <th className="w-[100px]">チェック済み</th> + {preRoundMode && ( + <> + <th className="w-[100px]">得点</th> + <th className="w-[100px]">チェック済み</th> + </> + )} <th className="w-[50px]" aria-label="投稿内容" /> <th className="w-[50px]" aria-label="ダウンロード" /> </tr> @@ -58,10 +63,16 @@ function AnswerListSection({ problem }: AnswerSectionProps) { <td>{createdAt.toFormat("yyyy-MM-dd HH:mm:ss")}</td> <td>{problem?.code}</td> <td>{problem?.title}</td> - <td className="text-right">{answer?.point ?? "--"} pt</td> - <td className="text-center"> - {answer.point != null ? "○" : "採点中"} - </td> + {!preRoundMode && ( + <> + <td className="text-right"> + {answer?.point ?? "--"} pt + </td> + <td className="text-center"> + {answer.point != null ? "○" : "採点中"} + </td>{" "} + </> + )} <td> <a href="#preview" @@ -73,9 +84,7 @@ function AnswerListSection({ problem }: AnswerSectionProps) { </td> <td> <a - download={`ictsc-${ - problem?.code - }-${createdAt.toUnixInteger()}.md`} + download={`ictsc-${problem?.code}-${createdAt.toUnixInteger()}.md`} className="link" href={URL.createObjectURL(blob)} > @@ -109,7 +118,7 @@ function AnswerListSection({ problem }: AnswerSectionProps) { </div> <div className="answer-preview-created-at"> {DateTime.fromISO(selectedAnswer.created_at).toFormat( - "yyyy-MM-dd HH:mm:ss" + "yyyy-MM-dd HH:mm:ss", )} </div> </div> @@ -120,7 +129,7 @@ function AnswerListSection({ problem }: AnswerSectionProps) { onClick={() => setIsPreviewAnswer(false)} className={clsx( `tab tab-lifted`, - !isPreviewAnswer && "tab-active" + !isPreviewAnswer && "tab-active", )} > Markdown @@ -130,7 +139,7 @@ function AnswerListSection({ problem }: AnswerSectionProps) { onClick={() => setIsPreviewAnswer(true)} className={clsx( `tab tab-lifted`, - isPreviewAnswer && "tab-active" + isPreviewAnswer && "tab-active", )} > Preview diff --git a/frontend/octavio/app/problems/[problemId]/_components/multiple-answer-form.tsx b/frontend/octavio/app/problems/[problemId]/_components/multiple-answer-form.tsx index a0a54f69..ee57f76c 100644 --- a/frontend/octavio/app/problems/[problemId]/_components/multiple-answer-form.tsx +++ b/frontend/octavio/app/problems/[problemId]/_components/multiple-answer-form.tsx @@ -154,7 +154,7 @@ function MultipleAnswerForm({ code }: { code: string }) { type="button" className="btn btn-primary mt-4" onClick={sendButton} - value="提出確認" + value="提出" /> <input type="button" diff --git a/frontend/octavio/app/problems/[problemId]/page.tsx b/frontend/octavio/app/problems/[problemId]/page.tsx index 3691c3db..7fe2467f 100644 --- a/frontend/octavio/app/problems/[problemId]/page.tsx +++ b/frontend/octavio/app/problems/[problemId]/page.tsx @@ -30,7 +30,7 @@ function ProblemPage({ params }: { params: { problemId: string } }) { const [isReCreateModalOpen, setIsReCreateModalOpen] = useState(false); const { recreateInfo, mutate, reCreate } = useReCreateInfo( - problem?.code ?? null, + problem?.type === "normal" ? problem?.code ?? null : null, ); const formType = problem?.type ?? "normal"; const isReadOnly = user?.is_read_only ?? true; diff --git a/frontend/octavio/app/problems/page.tsx b/frontend/octavio/app/problems/page.tsx index 8fcd9a75..0d7f76ee 100644 --- a/frontend/octavio/app/problems/page.tsx +++ b/frontend/octavio/app/problems/page.tsx @@ -19,7 +19,7 @@ import { dismissNoticeIdsState } from "@/hooks/state/recoil"; function Problems() { const [dismissNoticeIds, setDismissNoticeIds] = useRecoilState( - dismissNoticeIdsState + dismissNoticeIdsState, ); const { problems, isLoading } = useProblems(); @@ -34,6 +34,13 @@ function Problems() { ); } + const normalProblems = problems.filter( + (problem) => problem.type === "normal", + ); + const multipleProblems = problems.filter( + (problem) => problem.type === "multiple", + ); + return ( <> <ICTSCTitle title="問題一覧" /> @@ -64,13 +71,32 @@ function Problems() { おしらせ一覧へ→ </Link> </div> - <ul className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-4 gap-8 container-ictsc"> - {problems.map((problem) => ( - <li key={problem.id}> - <ProblemCard problem={problem} /> - </li> - ))} - </ul> + {normalProblems.length > 0 && ( + <> + <h2 className="container-ictsc text-2xl font-bold">実技問題</h2> + <ul className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-4 gap-8 container-ictsc"> + {normalProblems.map((problem) => ( + <li key={problem.id}> + <ProblemCard problem={problem} /> + </li> + ))} + </ul> + </> + )} + {multipleProblems.length > 0 && ( + <> + <h2 className="container-ictsc text-2xl font-bold pt-2"> + 選択問題 + </h2> + <ul className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-4 gap-8 container-ictsc"> + {multipleProblems.map((problem) => ( + <li key={problem.id}> + <ProblemCard problem={problem} /> + </li> + ))} + </ul> + </> + )} </main> </> ); diff --git a/frontend/octavio/app/ranking/layout.tsx b/frontend/octavio/app/ranking/layout.tsx index ac968e38..3d5cc766 100644 --- a/frontend/octavio/app/ranking/layout.tsx +++ b/frontend/octavio/app/ranking/layout.tsx @@ -1,7 +1,9 @@ import React from "react"; import { Metadata } from "next"; +import { notFound } from "next/navigation"; +import { preRoundMode } from "@/components/_const"; import ICTSCTitle from "@/components/title"; const title = "ランキング"; @@ -11,6 +13,10 @@ export const metadata: Metadata = { }; export default function Layout({ children }: { children: React.ReactNode }) { + if (preRoundMode) { + return notFound(); + } + return ( <> <ICTSCTitle title={title} /> diff --git a/frontend/octavio/app/scoring/page.tsx b/frontend/octavio/app/scoring/page.tsx index 756e7ea3..7a7b9235 100644 --- a/frontend/octavio/app/scoring/page.tsx +++ b/frontend/octavio/app/scoring/page.tsx @@ -49,7 +49,6 @@ function Index() { <th>ポイント</th> <th>採点基準ポイント</th> <th>前提問題</th> - <th>著者</th> </tr> </thead> <tbody className="cursor-pointer"> @@ -93,7 +92,6 @@ function Index() { <td>{prob.point}</td> <td>{prob.solved_criterion}</td> <td>{prob.previous_problem_id}</td> - <td>{prob.author_id === user?.id ? "自分" : ""}</td> </tr> ))} </tbody> diff --git a/frontend/octavio/app/users/page.tsx b/frontend/octavio/app/users/page.tsx index 9a1b4b36..3634aee0 100644 --- a/frontend/octavio/app/users/page.tsx +++ b/frontend/octavio/app/users/page.tsx @@ -85,6 +85,7 @@ function Page() { <tr> <th>名前</th> <th>チーム名</th> + <th>所属</th> <th>自己紹介</th> </tr> </thead> @@ -131,6 +132,9 @@ function Page() { <td className="whitespace-normal lg:min-w-[196px]"> {userGroup.name} </td> + <td className="whitespace-normal lg:min-w-[196px]"> + {userGroup.organization} + </td> <td className="whitespace-normal"> {member.profile?.self_introduction} </td> diff --git a/frontend/octavio/components/_const.ts b/frontend/octavio/components/_const.ts index 266c5e3b..101ec43a 100644 --- a/frontend/octavio/components/_const.ts +++ b/frontend/octavio/components/_const.ts @@ -2,9 +2,10 @@ const replaceN = (str: string) => str.replace(/\\n/g, "\n"); export const apiUrl = process.env.NEXT_PUBLIC_API_URL; export const site = process.env.NEXT_PUBLIC_SITE_NAME ?? ""; -export const rule = replaceN(process.env.RULE ?? ""); +export const rule = replaceN(process.env.NEXT_PUBLIC_RULE ?? ""); export const shortRule = replaceN(process.env.NEXT_PUBLIC_SHORT_RULE ?? ""); export const recreateRule = replaceN( - process.env.NEXT_PUBLIC_RECREATE_RULE ?? "" + process.env.NEXT_PUBLIC_RECREATE_RULE ?? "", ); export const answerLimit = process.env.NEXT_PUBLIC_ANSWER_LIMIT; +export const preRoundMode = process.env.NEXT_PUBLIC_PRE_ROUND_MODE === "true"; diff --git a/frontend/octavio/components/markdown-preview.tsx b/frontend/octavio/components/markdown-preview.tsx index 57c2df65..c51c585f 100644 --- a/frontend/octavio/components/markdown-preview.tsx +++ b/frontend/octavio/components/markdown-preview.tsx @@ -101,7 +101,7 @@ function MarkdownPreview({ className, content }: Props) { count += 1; return ` - <div class="radio-buttons pt-2"> + <div class="radio-buttons pt-2 mt-4"> <fieldset class="flex flex-col space-y-4">${radioButtons.join( "", )}</fieldset> @@ -126,7 +126,7 @@ function MarkdownPreview({ className, content }: Props) { count += 1; return ` - <div class="checkboxes pt-2"> + <div class="checkboxes pt-2 mt-4"> <fieldset class="flex flex-col space-y-4">${checkboxes.join( "", )}</fieldset> diff --git a/frontend/octavio/components/navbar.tsx b/frontend/octavio/components/navbar.tsx index fb78b176..5b727e26 100644 --- a/frontend/octavio/components/navbar.tsx +++ b/frontend/octavio/components/navbar.tsx @@ -5,6 +5,7 @@ import { useRouter } from "next/navigation"; import { mutate } from "swr"; +import { preRoundMode } from "@/components/_const"; import useAuth from "@/hooks/auth"; function ICTSCNavBar() { @@ -43,15 +44,17 @@ function ICTSCNavBar() { </li> </> )} - <li> - <Link href="/ranking">順位</Link> - </li> + {!preRoundMode && ( + <li> + <Link href="/ranking">順位</Link> + </li> + )} {user !== null && ( <> <li> <Link href="/users">参加者</Link> </li> - {user.user_group.is_full_access && !user.is_read_only && ( + {user?.user_group.is_full_access && !user.is_read_only && ( <li> <Link href="/scoring">採点</Link> </li> @@ -65,14 +68,14 @@ function ICTSCNavBar() { ) : ( // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex <li tabIndex={0} className="ml-4 dropdown dropdown-end"> - <div>{user.display_name}</div> + <div>{user?.display_name}</div> <ul /* eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex */ tabIndex={0} className="menu menu-compact dropdown-content bg-base-100 mt-3 p-2 shadow rounded-box w-52 text-base-content" > <li> - <div>チーム: {user.user_group.name}</div> + <div>チーム: {user?.user_group.name}</div> </li> <li> <Link href="/profile">プロフィール</Link> diff --git a/frontend/octavio/components/problem-card.tsx b/frontend/octavio/components/problem-card.tsx index 4b6e7ac7..082c84a4 100644 --- a/frontend/octavio/components/problem-card.tsx +++ b/frontend/octavio/components/problem-card.tsx @@ -2,6 +2,7 @@ import Link from "next/link"; import clsx from "clsx"; +import { preRoundMode } from "@/components/_const"; import { Problem } from "@/types/Problem"; type Props = { @@ -10,13 +11,21 @@ type Props = { function ProblemCard({ problem }: Props) { let problemText = ""; - if ( - problem.current_point >= (problem.solved_criterion ?? problem.current_point) - ) { - problemText = "font-bold text-gray-500"; - } - if (problem.current_point === problem.point) { - problemText = "font-bold text-amber-500"; + + if (preRoundMode) { + if (problem.is_answered) { + problemText = "font-bold text-amber-500"; + } + } else { + if ( + problem.current_point >= + (problem.solved_criterion ?? problem.current_point) + ) { + problemText = "font-bold text-gray-500"; + } + if (problem.current_point === problem.point) { + problemText = "font-bold text-amber-500"; + } } return ( @@ -32,7 +41,9 @@ function ProblemCard({ problem }: Props) { </div> <div> <div className={clsx("problem-point text-right", problemText)}> - {problem.current_point}/{problem.point}pt + {preRoundMode + ? `${problem.point}pt` + : `${problem.current_point}/${problem.point}pt`} </div> <div className="font-bold text-primary">問題文へ→</div> </div> diff --git a/frontend/octavio/hooks/auth.ts b/frontend/octavio/hooks/auth.ts index ca66d92a..59fcd13a 100644 --- a/frontend/octavio/hooks/auth.ts +++ b/frontend/octavio/hooks/auth.ts @@ -3,6 +3,7 @@ import useSWR from "swr"; import useApi from "@/hooks/api"; import { PutProfileRequest } from "@/types/PutProfileRequest"; import { SignInRequest } from "@/types/SignInRequest"; +import { User } from "@/types/User"; import { AuthSelfResult, SignUpRequest } from "@/types/_api"; const useAuth = () => { @@ -10,7 +11,14 @@ const useAuth = () => { const fetcher = (url: string) => client.get<AuthSelfResult>(url); - const { data, mutate, isLoading } = useSWR("auth/self", fetcher); + const { data, mutate, isLoading, error } = useSWR("auth/self", fetcher); + + let user: User | null; + if (error) { + user = null; + } else { + user = data?.data?.user ?? null; + } const signUp = async (request: SignUpRequest) => client.post("users", request); @@ -21,7 +29,7 @@ const useAuth = () => { client.put(`users/${userId}`, request); return { - user: data?.data?.user ?? null, + user, signUp, signIn, logout, diff --git a/frontend/octavio/hooks/problems.ts b/frontend/octavio/hooks/problems.ts index 0520d746..6d623213 100644 --- a/frontend/octavio/hooks/problems.ts +++ b/frontend/octavio/hooks/problems.ts @@ -1,6 +1,7 @@ import useSWR from "swr"; import useApi from "@/hooks/api"; +import { Problem } from "@/types/Problem"; import { ProblemResult } from "@/types/_api"; const useProblems = () => { @@ -8,10 +9,17 @@ const useProblems = () => { const fetcher = (url: string) => client.get<ProblemResult>(url); - const { data, mutate, isLoading } = useSWR("problems", fetcher); + const { data, mutate, isLoading, error } = useSWR("problems", fetcher); + + let problems: Problem[]; + if (error) { + problems = []; + } else { + problems = data?.data?.problems ?? []; + } return { - problems: data?.data?.problems ?? [], + problems, mutate, isLoading, }; diff --git a/frontend/octavio/hooks/ranking.ts b/frontend/octavio/hooks/ranking.ts index c712a889..d2fd533d 100644 --- a/frontend/octavio/hooks/ranking.ts +++ b/frontend/octavio/hooks/ranking.ts @@ -1,5 +1,6 @@ import useSWR from "swr"; +import { preRoundMode } from "@/components/_const"; import useApi from "@/hooks/api"; import { RankingResult } from "@/types/_api"; @@ -9,7 +10,7 @@ const useRanking = () => { /* c8 ignore next */ const fetcher = (url: string) => client.get<RankingResult>(url); - const { data, isLoading } = useSWR(`ranking`, fetcher); + const { data, isLoading } = useSWR(preRoundMode ? null : `ranking`, fetcher); return { ranking: data?.data?.ranking ?? null, diff --git a/frontend/octavio/types/Problem.ts b/frontend/octavio/types/Problem.ts index 261a4cf9..7c772207 100644 --- a/frontend/octavio/types/Problem.ts +++ b/frontend/octavio/types/Problem.ts @@ -9,11 +9,11 @@ export interface Problem { point: number; solved_criterion: number | null; previous_problem_id: string | null; - author_id: string; unchecked: number | null; unchecked_near_overdue: number | null; unchecked_overdue: number | null; current_point: number; + is_answered: boolean; is_solved: boolean; } @@ -44,10 +44,10 @@ export const testProblem: Problem = { point: 100, solved_criterion: 150, previous_problem_id: null, - author_id: "1", unchecked: null, unchecked_near_overdue: null, unchecked_overdue: null, current_point: 100, + is_answered: false, is_solved: false, };