Skip to content

Commit

Permalink
Merge pull request #147 from h8570rg/chip
Browse files Browse the repository at this point in the history
Chip
  • Loading branch information
h8570rg authored Apr 20, 2024
2 parents 166398e + 400656f commit 1ba6da8
Show file tree
Hide file tree
Showing 22 changed files with 2,888 additions and 2,485 deletions.
4 changes: 2 additions & 2 deletions app/(app)/matches/MatchCard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ export async function MatchCard({
const targetDate = dayjs(date);
const isSameYear = today.isSame(targetDate, "year");
const displayDate = isSameYear
? dayjs(date).format("M / D")
: dayjs(date).format("YYYY / M / D");
? dayjs(date).format("M/D")
: dayjs(date).format("YYYY/M/D");

return (
<NavigationCard matchId={matchId}>
Expand Down
11 changes: 11 additions & 0 deletions app/(app)/matches/MatchCard/rankCountChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@ import {
} from "recharts";
import { MatchResult } from "~/lib/utils/matchResult";

// Override console.error
// This is a hack to suppress the warning about missing defaultProps in recharts library as of version 2.12
// @link https://github.com/recharts/recharts/issues/3615
// eslint-disable-next-line no-console
const error = console.error;
// eslint-disable-next-line no-console, @typescript-eslint/no-explicit-any
console.error = (...args: any) => {
if (/defaultProps/.test(args[0])) return;
error(...args);
};

const radius = 5;

export function RankCountChart({
Expand Down
135 changes: 135 additions & 0 deletions app/(app)/matches/[matchId]/ChipInputModal/ChipForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
"use client";

import { useEffect } from "react";
import { useFormState } from "react-dom";
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { Button } from "~/components/Button";
import { Input } from "~/components/Input";
import { ModalBody, ModalFooter } from "~/components/Modal";
import { Match } from "~/lib/services/match";
import { useChipInputModal } from "../useChipInputModal";
import { addChip } from "./actions";

type Schema = {
playerChip: {
profileId: string;
name: string;
chipCount: string;
}[];
};

export function ChipForm({ match }: { match: Match }) {
const chipInputModal = useChipInputModal();
const { players, rule } = match;
const [{ errors, success }, formAction] = useFormState(
addChip.bind(null, match.id, players.length),
{},
);

const { control, watch, setValue, setFocus } = useForm<Schema>({
defaultValues: {
playerChip: players.map((player) => ({
name: player.name,
profileId: player.id,
chipCount: "",
})),
},
});

const { fields } = useFieldArray<Schema>({
control,
name: "playerChip",
});

const playerChip = watch("playerChip");

const isAutoFillAvailable =
playerChip.filter(({ chipCount }) => chipCount !== "").length ===
rule.playersCount - 1;

const totalChipCount = playerChip.reduce(
(sum, { chipCount }) => sum + Number(chipCount),
0,
);

useEffect(() => {
if (success) {
chipInputModal.onClose();
}
}, [chipInputModal, success]);

return (
<form action={formAction}>
<ModalBody>
<ul className="space-y-1">
{fields.map((field, index) => (
<li key={field.id} className="flex items-center gap-1">
<Controller
control={control}
name={`playerChip.${index}.profileId`}
render={({ field }) => <input type="text" hidden {...field} />}
/>
<div className="shrink-0 grow text-small text-foreground">
{field.name}
</div>
<Controller
control={control}
name={`playerChip.${index}.chipCount`}
render={({ field }) => (
<Input
labelPlacement="outside-left" // 中心ずれを防ぐ
classNames={{
base: "basis-[160px] shrink-0",
input: "text-right placeholder:text-default-400",
}}
type="number"
size="lg"
startContent={
isAutoFillAvailable &&
field.value === "" && (
<>
<Button
variant="flat"
size="sm"
radius="md"
color="secondary"
className="h-6 w-max min-w-0 shrink-0 gap-1 px-2 text-[10px]"
onClick={() => {
setValue(field.name, String(-1 * totalChipCount));
setFocus(field.name);
}}
>
残り入力
</Button>
</>
)
}
endContent={
<div className="pointer-events-none shrink-0">
<span className="text-small text-default-400"></span>
</div>
}
{...field}
/>
)}
/>
</li>
))}
</ul>
{errors?.playerChip && (
<p className="whitespace-pre-wrap text-tiny text-danger">
{errors.playerChip[0]}
</p>
)}
</ModalBody>
<ModalFooter>
<Button variant="light" onClick={chipInputModal.onClose}>
キャンセル
</Button>
<Button type="submit" color="primary">
保存
</Button>
</ModalFooter>
</form>
);
}
18 changes: 18 additions & 0 deletions app/(app)/matches/[matchId]/ChipInputModal/ModalController.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"use client";

import { Modal, ModalContent } from "~/components/Modal";
import { useChipInputModal } from "../useChipInputModal";

export function ModalController({ children }: { children: React.ReactNode }) {
const chipInputModal = useChipInputModal();

return (
<Modal
isOpen={chipInputModal.isOpen}
onClose={chipInputModal.onClose}
hideCloseButton
>
<ModalContent>{children}</ModalContent>
</Modal>
);
}
83 changes: 83 additions & 0 deletions app/(app)/matches/[matchId]/ChipInputModal/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"use server";

import { revalidateTag } from "next/cache";
import { z } from "zod";
import { serverServices } from "~/lib/services/server";
import { schemas } from "~/lib/utils/schemas";

type AddChipState = {
success?: boolean;
errors?: {
base?: string[];
playerChip?: string[];
};
};

const addChipSchema = z
.object({
playerChip: z.array(
z.object({
profileId: schemas.profileId,
chipCount: z.string().transform(Number),
}),
),
})
.refine(
({ playerChip }) => {
const total = playerChip.reduce((acc, { chipCount }) => {
return acc + (chipCount ?? 0);
}, 0);
return total === 0;
},
({ playerChip }) => {
const total = playerChip.reduce((acc, { chipCount }) => {
return acc + (chipCount ?? 0);
}, 0);
return {
path: ["playerChip"],
message: `チップの合計が0枚なるように入力してください\n現在: ${total.toLocaleString()}枚`,
};
},
);

export async function addChip(
matchId: string,
totalPlayersCount: number,
prevState: AddChipState,
formData: FormData,
): Promise<AddChipState> {
const playerChip = Array.from({ length: totalPlayersCount }).map((_, i) => {
return {
profileId: formData.get(`playerChip.${i}.profileId`),
chipCount: formData.get(`playerChip.${i}.chipCount`),
};
});

const validatedFields = addChipSchema.safeParse({
playerChip,
});

if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
};
}

const { addMatchChip } = serverServices();

await addMatchChip({
matchId,
playerChips: validatedFields.data.playerChip.map((playerChip) => {
return {
profileId: playerChip.profileId,
chipCount: playerChip.chipCount,
};
}),
});

revalidateTag(`match-${matchId}-chip`);

return {
success: true,
};
}
16 changes: 16 additions & 0 deletions app/(app)/matches/[matchId]/ChipInputModal/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { ModalHeader } from "~/components/Modal";
import { serverServices } from "~/lib/services/server";
import { ChipForm } from "./ChipForm";
import { ModalController } from "./ModalController";

export async function ChipInputModal({ matchId }: { matchId: string }) {
const { getMatch } = serverServices();
const [match] = await Promise.all([getMatch({ matchId })]);

return (
<ModalController>
<ModalHeader className="flex justify-between">チップ入力</ModalHeader>
<ChipForm match={match} />
</ModalController>
);
}
20 changes: 20 additions & 0 deletions app/(app)/matches/[matchId]/MatchTable/AddChipButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"use client";

import { Button } from "~/components/Button";
import { Icon } from "~/components/Icon";
import { useChipInputModal } from "../useChipInputModal";

export function AddChipButton({ isDisabled }: { isDisabled: boolean }) {
const gameInputModal = useChipInputModal();

return (
<Button
fullWidth
onClick={gameInputModal.onOpen}
isDisabled={isDisabled}
startContent={<Icon className="size-4" name="chip" />}
>
チップを入力する
</Button>
);
}
4 changes: 3 additions & 1 deletion app/(app)/matches/[matchId]/MatchTable/AddGameButton.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";

import { Button } from "~/components/Button";
import { Icon } from "~/components/Icon";
import { useGameInputModal } from "../useGameInputModal";

export function AddGameButton({ isDisabled }: { isDisabled: boolean }) {
Expand All @@ -9,9 +10,10 @@ export function AddGameButton({ isDisabled }: { isDisabled: boolean }) {
return (
<Button
fullWidth
color="primary"
onClick={gameInputModal.onOpen}
isDisabled={isDisabled}
startContent={<Icon className="size-6" name="add" />}
variant="ghost"
>
結果を入力する
</Button>
Expand Down
Loading

0 comments on commit 1ba6da8

Please sign in to comment.