Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
15 changed files
with
674 additions
and
3 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
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,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
34
app/(dataEntryFix)/fix/reserve/[time]/details/_headerFooter.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,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 | ||
)} | ||
</> | ||
); | ||
} |
90 changes: 90 additions & 0 deletions
90
app/(dataEntryFix)/fix/reserve/[time]/details/_registUserInfoForm.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,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> | ||
); | ||
} |
24 changes: 24 additions & 0 deletions
24
app/(dataEntryFix)/fix/reserve/[time]/details/_reservedDateTimeViewer.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,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} | ||
/> | ||
</> | ||
); | ||
} |
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,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
72
app/(dataEntryFix)/fix/reserve/[time]/details/register/route.ts
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,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, | ||
}); | ||
} | ||
} |
Oops, something went wrong.