Skip to content

Commit

Permalink
vercel/next.js/issues/54173 の対策ルート
Browse files Browse the repository at this point in the history
  • Loading branch information
kght6123 committed Oct 29, 2023
1 parent 1e5d66c commit 34a7066
Show file tree
Hide file tree
Showing 15 changed files with 674 additions and 3 deletions.
2 changes: 1 addition & 1 deletion app/(auth)/page.tsx
Expand Up @@ -18,7 +18,7 @@ export default async function Home() {
</p>
{user ? (
<>
<Link.Basic color="primary" href="/reserve" shallow>
<Link.Basic color="primary" href="/fix/reserve" shallow>
予約を開始する
</Link.Basic>
<LogoutButton>ログアウトする</LogoutButton>
Expand Down
6 changes: 4 additions & 2 deletions app/(dataEntry)/reserve/completed/_headerFooter.tsx
Expand Up @@ -13,14 +13,16 @@ export default function HeaderFooter() {
createPortal(
<>
<div />
<h1 className="text-base font-black">○○○○○○○ 予約登録・変更完了</h1>
<h1 className="text-base font-black">
○○○○○○○ 予約登録・変更完了Fix
</h1>
<div />
</>,
headerEl
)}
{footerEl &&
createPortal(
<NextLink href="/reserve">
<NextLink href="/fix/reserve">
<Button.Basic color="secondary">予約を追加する</Button.Basic>
</NextLink>,
footerEl
Expand Down
12 changes: 12 additions & 0 deletions app/(dataEntryFix)/_hooks.ts
@@ -0,0 +1,12 @@
"use client";
import { useEffect, useState } from "react";

export function useHeaderFooter() {
const [headerEl, setHeaderEl] = useState<HTMLElement | null>(null);
const [footerEl, setFooterEl] = useState<HTMLElement | null>(null);
useEffect(() => {
setHeaderEl(document.getElementById("header"));
setFooterEl(document.getElementById("footer"));
}, []);
return [headerEl, footerEl];
}
34 changes: 34 additions & 0 deletions app/(dataEntryFix)/fix/reserve/[time]/details/_headerFooter.tsx
@@ -0,0 +1,34 @@
"use client";
import { useHeaderFooter } from "$/(dataEntry)/_hooks";
import { Button } from "$/_ui/atoms/button";
import { ChevronLeftIcon } from "@heroicons/react/20/solid";
import { useRouter } from "next/navigation";
import React from "react";
import { createPortal } from "react-dom";

export default function HeaderFooter() {
const [headerEl, footerEl] = useHeaderFooter();
const router = useRouter();
return (
<>
{headerEl &&
createPortal(
<>
<Button.Back onClick={() => router.back()}>
<ChevronLeftIcon className="h-5 w-5" />
</Button.Back>
<h1 className="text-base font-black">○○○○○○○ 予約登録・変更Fix</h1>
<div />
</>,
headerEl
)}
{footerEl &&
createPortal(
<Button.Basic color="primary" form="registUserInfoForm" type="submit">
予約する
</Button.Basic>,
footerEl
)}
</>
);
}
@@ -0,0 +1,90 @@
"use client";
import {
ErrorResponse,
ReserveDataResponse,
ReserveDetail,
} from "$/(dataEntry)/reserve/_schema";
import { Button } from "$/_ui/atoms/button";
import clsx from "clsx";
import { useParams, usePathname, useRouter } from "next/navigation";
import React, { useEffect, useId, useState } from "react";

export default function RegistUserInfoForm({
children,
modal = false,
onSuccess,
}: {
children: React.ReactNode;
modal?: boolean;
onSuccess?: () => void;
}) {
const router = useRouter();
return (
<form
// action={action}
className={clsx(modal ? "modal-box" : "w-full")}
// FIXME: https://github.com/vercel/next.js/issues/54676 の不具合でServer Actionでredirectが使えないので、onSubmitでAPIを叩くようにする
id="registUserInfoForm"
onSubmit={async (e) => {
e.preventDefault();
// 準備
const formData = new FormData(
(e.target as HTMLFormElement) || undefined
);
// バリデーション
const result = ReserveDetail.safeParse(formData);
if (result.success === false) {
console.error("validation error", result.error);
const pathName = result.error.errors
.map((e) =>
e.path.map((p) =>
p === "tel"
? "電話番号"
: p === "realName"
? "氏名"
: p === "time"
? "予約時間"
: p
)
)
.join("、");
alert(`${pathName}の入力に誤りがあります。`);
return;
}
// 登録
const { realName, tel, time } = result.data;
console.log("registData", realName, tel, time);
const res = await fetch(`/fix/reserve/${time}/details/register`, {
body: formData,
method: "POST",
});
const data = await res.json();
console.log("responseData", data);
// 判定
const respDataReturn = ReserveDataResponse.safeParse(data);
if (respDataReturn.success === true) {
// 成功の場合
onSuccess && onSuccess();
const { time } = respDataReturn.data;
const date = new Date(time);
alert(
`予約が完了しました。\n(${date.getMonth()}${
date.getDate() - 1
}${date.getHours()}時)`
);
// 画面移動
router.push("/fix/reserve/completed");
} else {
// エラーの場合
const errorDataReturn = ErrorResponse.safeParse(data);
if (errorDataReturn.success === true) {
const { message } = errorDataReturn.data;
alert(message);
}
}
}}
>
{children}
</form>
);
}
@@ -0,0 +1,24 @@
"use client";
import { CalendarNow } from "$/_ui/molecules/calendar";
import { TimelineBase } from "$/_ui/molecules/timeline";
import React from "react";

export default function ReservedDateTimeViewer({
reservedTimeList,
unixTime,
}: {
reservedTimeList?: string[];
unixTime: number;
}) {
return (
<>
<CalendarNow className="w-full" unixTime={unixTime} />
<TimelineBase
className="w-full"
disabledTimeList={["12:00"]}
reservedTimeList={reservedTimeList}
unixTime={unixTime}
/>
</>
);
}
104 changes: 104 additions & 0 deletions app/(dataEntryFix)/fix/reserve/[time]/details/page.tsx
@@ -0,0 +1,104 @@
import RegistUserInfoForm from "$/(dataEntry)/reserve/[time]/details/_registUserInfoForm";
import { Circle } from "$/_ui/atoms/circle";
import { Input } from "$/_ui/atoms/input";
import { authOptions } from "$/api/auth/[...nextauth]/route";
import { UserPlusIcon } from "@heroicons/react/20/solid";
import { getServerSession } from "next-auth/next";
import { Suspense } from "react";

import { getReserves } from "../../page";
import HeaderFooter from "./_headerFooter";
import ReserveDateTimeSelector from "./_reservedDateTimeViewer";

// MEMO: これがないと、F5リロードなどしたときに404エラーになる。
// MEMO: 実際にモーダルの表示をしているのは、 app/(dataEntry)/@modal/(.)reserveDateTime/registUserInfo/page.tsx である。
export default async function RegistUserInfo({
params: { time },
}: {
params: { time?: string };
}) {
// TODO: 他の画面へリダイレクトもできる
// redirect("/reserve");
// 準備
const date = new Date(
time !== undefined ? parseInt(time) : new Date().getTime()
);
date.setMinutes(0);
date.setSeconds(0);
date.setMilliseconds(0);
const session = await getServerSession(authOptions);
// 検証
const userId = session?.user?.id;
if (userId === undefined) throw new Error("userId is undefined");
// 取得
const reserveDateTimeList = await getReserves({
unixTime: date.getTime(),
userId,
});
// 加工
const reservedTimeList = reserveDateTimeList?.map((reserveDateTime) => {
// 0:00の形式へ変換する
return `${reserveDateTime.reserved_at.getHours()}:${(
"0" + reserveDateTime.reserved_at.getMinutes()
).slice(-2)}`;
});
return (
<>
<HeaderFooter />
<RegistUserInfoForm /*action={create}*/>
<div className="flex flex-row justify-center gap-8 px-4 py-8">
<div className="flex w-1/2 flex-col">
<Suspense fallback={<div>Loading...</div>}>
<ReserveDateTimeSelector
reservedTimeList={reservedTimeList}
unixTime={date.getTime()}
/>
</Suspense>
</div>
<div className="flex flex-col content-center items-center justify-start gap-4 pt-44">
<Circle.Basic className="relative h-28 w-28" color="secondary">
<UserPlusIcon className="absolute right-3 top-4 h-20 w-20 fill-none stroke-secondary-900 stroke-[.5]" />
</Circle.Basic>
<div className="flex flex-col items-center gap-4">
<h1 className="text-2xl font-black">予約情報登録</h1>
<p className="text-xs">
氏名、電話番号を入力して、予約情報を登録します。
</p>
</div>
<div className="space-y-1 pb-4">
<div className="space-y-2">
<label className="text-xs font-bold">氏名</label>
<Input.Basic
autoFocus
color="secondary"
name="realName"
placeholder="氏名を入力して下さい"
required
title="氏名を入力して下さい"
/>
</div>
<div className="space-y-2">
<label className="text-xs font-bold">電話番号</label>
<Input.Basic
color="secondary"
name="tel"
pattern="\d{2,4}-?\d{2,4}-?\d{3,4}"
placeholder="090-1234-5678"
required
title="電話番号は半角数字または半角ハイフン(‐)で入力して下さい"
type="tel"
/>
</div>
</div>
<input
form="registUserInfoForm"
name="time"
type="hidden"
value={date.getTime().toString()}
/>
</div>
</div>
</RegistUserInfoForm>
</>
);
}
72 changes: 72 additions & 0 deletions app/(dataEntryFix)/fix/reserve/[time]/details/register/route.ts
@@ -0,0 +1,72 @@
import { db, reserveDateTimes, reserveUserDetails } from "#/schema";
import { ReserveDetail } from "$/(dataEntry)/reserve/_schema";
import { authOptions } from "$/api/auth/[...nextauth]/route";
import { revalidatePath } from "next/cache";
import { NextResponse } from "next/server";
import { getServerSession } from "next-auth/next";

export async function POST(
request: Request
// { params }: { params: { time: string } }
) {
// 準備
const { realName, tel, time } = ReserveDetail.parse(await request.formData());
const session = await getServerSession(authOptions);
const userId = session?.user?.id;
// バリデーション
if (!userId) {
return NextResponse.json(
{ message: "ログイン情報が存在しません。" },
{ status: 401 }
);
}
// 登録
const result = await db
.transaction(async (tx) => {
const r1 = db
.insert(reserveDateTimes)
.values({
reserved_at: new Date(time),
userId,
})
.run();
console.log(r1);
if (r1.changes !== 1) {
tx.rollback();
return { message: "予約に失敗しました。" };
}
const r2 = db
.insert(reserveUserDetails)
.values({
id: userId,
realName,
tel,
})
// 同一IDが存在する場合に更新する処理
.onConflictDoUpdate({
set: { realName, tel },
target: reserveUserDetails.id,
})
.run();
console.log(r2);
if (r2.changes !== 1) {
tx.rollback();
return { message: "予約に失敗しました。" };
}
})
.catch((e) => {
console.error(e);
return { message: "予約に失敗しました。" };
});
if (result?.message) {
return NextResponse.json(result, { status: 500 });
} else {
// キャッシュを更新 TODO: このバグが原因で動かない https://github.com/vercel/next.js/issues/54173
revalidatePath("/fix/reserve");
return NextResponse.json({
name: realName,
tel,
time,
});
}
}

0 comments on commit 34a7066

Please sign in to comment.