Skip to content

Commit

Permalink
feat: 名前の変更モーダルの追加 (#57)
Browse files Browse the repository at this point in the history
  • Loading branch information
shun-shobon authored Nov 6, 2024
2 parents 316d085 + 49c5861 commit b6f93ae
Show file tree
Hide file tree
Showing 6 changed files with 411 additions and 62 deletions.
40 changes: 25 additions & 15 deletions app/components/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,20 @@ import { BUTTON_BG_PATTERN_ID, BUTTON_FG_PATTERN_ID } from "./Patterns";
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
background?: boolean;
foreground?: boolean;
asChild?: boolean;
}

export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
(
{ className, background = true, asChild = false, children, ...props },
{
className,
background = true,
foreground = true,
asChild = false,
children,
...props
},
ref,
) => {
const Comp = asChild ? Slot : "button";
Expand All @@ -39,20 +47,22 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
/>
</svg>
)}
<svg
className={cn(
"-rotate-3 absolute inset-0 h-full w-full skew-x-12 border-2 border-white",
)}
role="presentation"
>
<rect
x="0"
y="0"
width="100%"
height="100%"
fill={`url(#${BUTTON_FG_PATTERN_ID})`}
/>
</svg>
{foreground && (
<svg
className={cn(
"-rotate-3 absolute inset-0 h-full w-full skew-x-12 border-2 border-white",
)}
role="presentation"
>
<rect
x="0"
y="0"
width="100%"
height="100%"
fill={`url(#${BUTTON_FG_PATTERN_ID})`}
/>
</svg>
)}
</div>
<span className="drop-shadow-base">{children}</span>
</Comp>
Expand Down
38 changes: 25 additions & 13 deletions app/components/LoginCard.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
import { valibotResolver } from "@hookform/resolvers/valibot";
import type { ComponentPropsWithRef, ReactNode } from "react";
import { type SubmitHandler, useForm } from "react-hook-form";
import {
type UpdateDisplayNameFormData,
UpdateDisplayNameSchema,
} from "~/features/profile/Profile";
import { supabase } from "~/libs/supabase";
import { cn } from "~/libs/utils";
import { Button } from "./Button";
import { Card } from "./Card";
import { Input } from "./Input";

interface FormData {
displayName: string;
}

interface LoginCardProps extends ComponentPropsWithRef<"div"> {}

export function LoginCard({ className }: LoginCardProps): ReactNode {
const { register, handleSubmit } = useForm<FormData>();
const { register, formState, handleSubmit } =
useForm<UpdateDisplayNameFormData>({
resolver: valibotResolver(UpdateDisplayNameSchema),
});

const onSubmit: SubmitHandler<FormData> = async (data) => {
const onSubmit: SubmitHandler<UpdateDisplayNameFormData> = async (data) => {
// 状態に関わらずログアウトする
await supabase.auth.signOut();

Expand All @@ -29,14 +33,22 @@ export function LoginCard({ className }: LoginCardProps): ReactNode {
};

return (
<Card
className={cn("flex flex-col items-center gap-y-4 p-4", className)}
asChild
>
<Card className={cn("grid gap-y-4 p-4", className)} asChild>
<form onSubmit={handleSubmit(onSubmit)}>
<h1 className={"text-2xl drop-shadow-base"}>名前を入力して登録!</h1>
<Input placeholder="名前" {...register("displayName")} />
<Button type="submit">登録</Button>
<h1 className={"text-center text-2xl drop-shadow-base"}>
名前を入力して登録!
</h1>
<div className="grid gap-y-1">
<Input placeholder="名前" {...register("displayName")} />
{formState.errors.displayName && (
<span className="ml-2 text-red-500 drop-shadow-base">
{formState.errors.displayName.message}
</span>
)}
</div>
<Button type="submit" className="justify-self-center">
登録
</Button>
</form>
</Card>
);
Expand Down
149 changes: 115 additions & 34 deletions app/components/ProfileCard.tsx
Original file line number Diff line number Diff line change
@@ -1,49 +1,130 @@
import type { Profile } from "~/features/profile/Profile";
import { valibotResolver } from "@hookform/resolvers/valibot";
import * as Dialog from "@radix-ui/react-dialog";
import { type ReactNode, useState } from "react";
import { useForm } from "react-hook-form";
import {
type Profile,
type UpdateDisplayNameFormData,
UpdateDisplayNameSchema,
} from "~/features/profile/Profile";
import { useMyProfile } from "~/features/profile/useMyProfile";
import { cn } from "~/libs/utils";
import { Button } from "./Button";
import { Card } from "./Card";
import { Input } from "./Input";

export interface ProfileCardProps {
profile: Profile;
onClickEdit?: () => void;
className?: string;
}

export function ProfileCard({
profile,
onClickEdit,
className,
}: ProfileCardProps) {
export function ProfileCard({ profile, className }: ProfileCardProps) {
const [open, setOpen] = useState(false);

return (
<Card className={cn("grid w-full max-w-screen-sm gap-4 p-4", className)}>
<div className="flex min-w-0 items-center justify-between gap-4 px-2">
<span className="truncate text-2xl drop-shadow-base">
{profile.displayName}
</span>
<Button className="flex-shrink-0" onClick={onClickEdit}>
編集
</Button>
</div>
<div className="grid grid-cols-[1fr_auto_1fr_auto_1fr] items-center drop-shadow-base">
<div className="grid justify-items-center gap-1 text-center">
<span>順位</span>
<span className="text-2xl">
{profile.rank ? `${profile.rank}位` : "-"}
</span>
</div>
<hr className="h-2/3 w-0 rounded-full border-2 border-white" />
<div className="grid justify-items-center gap-1 text-center">
<span>ハイスコア</span>
<span className="text-2xl">
{profile.highScore ? profile.highScore : "-"}
<Dialog.Root open={open} onOpenChange={setOpen}>
<Card className={cn("grid w-full max-w-screen-sm gap-4 p-4", className)}>
<div className="flex min-w-0 items-center justify-between gap-4 px-2">
<span className="truncate text-2xl drop-shadow-base">
{profile.displayName}
</span>
<Dialog.Trigger asChild>
<Button className="flex-shrink-0">編集</Button>
</Dialog.Trigger>
</div>
<hr className="h-2/3 w-0 rounded-full border-2 border-white" />
<div className="grid justify-items-center gap-1 text-center">
<span>プレイ回数</span>
<span className="text-2xl">{profile.playCount}</span>
<div className="grid grid-cols-[1fr_auto_1fr_auto_1fr] items-center drop-shadow-base">
<div className="grid justify-items-center gap-1 text-center">
<span>順位</span>
<span className="text-2xl">
{profile.rank ? `${profile.rank}位` : "-"}
</span>
</div>
<hr className="h-2/3 w-0 rounded-full border-2 border-white" />
<div className="grid justify-items-center gap-1 text-center">
<span>ハイスコア</span>
<span className="text-2xl">
{profile.highScore ? profile.highScore : "-"}
</span>
</div>
<hr className="h-2/3 w-0 rounded-full border-2 border-white" />
<div className="grid justify-items-center gap-1 text-center">
<span>プレイ回数</span>
<span className="text-2xl">{profile.playCount}</span>
</div>
</div>
</div>
</Card>
</Card>

<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 grid place-items-center bg-black/50 px-4">
<EditModal profile={profile} setOpen={setOpen} />
</Dialog.Overlay>
</Dialog.Portal>
</Dialog.Root>
);
}

interface EditModalProps {
profile: Profile;
setOpen: (open: boolean) => void;
}
function EditModal({ profile, setOpen }: EditModalProps): ReactNode {
const { updateDisplayName } = useMyProfile();
const { register, formState, handleSubmit, reset } =
useForm<UpdateDisplayNameFormData>({
resolver: valibotResolver(UpdateDisplayNameSchema),
defaultValues: {
displayName: profile.displayName,
},
});

const onSubmit = async (data: UpdateDisplayNameFormData) => {
try {
await updateDisplayName(data.displayName);
reset();
} finally {
setOpen(false);
}
};

return (
<Dialog.Content
asChild
// XXX: Enterを押すとダイアログが閉じてしまい、フォームが送信されないため、Enterを無効化
// ただし、フォームも送信されなくなってしまう。
onKeyDown={(e) => {
e.key === "Enter" && e.preventDefault();
}}
>
<Card asChild>
<form
onSubmit={handleSubmit(onSubmit)}
className="-rotate-2 grid w-full max-w-screen-sm gap-y-4 p-4 sm:gap-y-8"
>
<Dialog.Title className="text-center text-2xl drop-shadow-base">
名前の編集
</Dialog.Title>
<div className="grid gap-y-1">
<Input placeholder="名前" {...register("displayName")} />
{formState.errors.displayName && (
<span className="ml-2 text-red-500 drop-shadow-base">
{formState.errors.displayName.message}
</span>
)}
</div>
<div className="mt-2 grid grid-cols-2 justify-items-center gap-x-8 px-4">
<Button
className="w-full max-w-32"
foreground={false}
onClick={() => setOpen(false)}
>
キャンセル
</Button>
<Button type="submit" className="w-full max-w-32">
決定
</Button>
</div>
</form>
</Card>
</Dialog.Content>
);
}
13 changes: 13 additions & 0 deletions app/features/profile/Profile.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import * as v from "valibot";

export const MODEL_LIST = [
"jiraichan",
"necochan",
Expand Down Expand Up @@ -40,3 +42,14 @@ export interface Ranking {
highScore: number;
rank: number;
}

export const UpdateDisplayNameSchema = v.object({
displayName: v.pipe(
v.string("入力してください。"),
v.minLength(1, "1文字以上で入力してください。"),
v.maxLength(8, "8文字以内で入力してください。"),
),
});
export type UpdateDisplayNameFormData = v.InferInput<
typeof UpdateDisplayNameSchema
>;
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
},
"dependencies": {
"@builder.io/partytown": "0.10.2",
"@hookform/resolvers": "3.9.1",
"@radix-ui/react-dialog": "1.1.2",
"@radix-ui/react-navigation-menu": "1.2.1",
"@radix-ui/react-slot": "1.1.0",
"@radix-ui/react-tabs": "1.1.1",
Expand Down
Loading

0 comments on commit b6f93ae

Please sign in to comment.