-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #147 from h8570rg/chip
Chip
- Loading branch information
Showing
22 changed files
with
2,888 additions
and
2,485 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
135 changes: 135 additions & 0 deletions
135
app/(app)/matches/[matchId]/ChipInputModal/ChipForm.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
18
app/(app)/matches/[matchId]/ChipInputModal/ModalController.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.