diff --git a/app/models/reservation.server.ts b/app/models/reservation.server.ts index 0fd43ec..4585ea9 100644 --- a/app/models/reservation.server.ts +++ b/app/models/reservation.server.ts @@ -11,7 +11,7 @@ export function getReservation({ }) { userId; return prisma.reservation.findFirst({ - select: { id: true, start: true, end: true, court: true, user: true }, + select: { id: true, start: true, end: true, court: true, user: true, openPlay: true }, where: { id }, }); } @@ -61,7 +61,7 @@ export async function createReservation({ } // bonus: warn if after dusk - if (compareAsc(end, addHours(startOfToday(), 20)) === 1) { + if (compareAsc(end, addHours(startOfDay(start), 20)) === 1) { throw new Error('Reservations must conclude by 20:00') } @@ -80,6 +80,7 @@ export async function createReservation({ const sameDay = await prisma.reservation.findFirst({ where: { userId, + court, start: { gte: startOfDay(start), lte: addDays(startOfDay(start), 1) diff --git a/app/routes/Header.tsx b/app/routes/Header.tsx new file mode 100644 index 0000000..42e5925 --- /dev/null +++ b/app/routes/Header.tsx @@ -0,0 +1,64 @@ +import type { User } from "@prisma/client"; +import { Form, Link } from "@remix-run/react"; + +import { useOptionalUser } from "~/utils"; + +export function Header({ + hideLoginLinks = false, +}: { + hideLoginLinks?: boolean; +}) { + const user: User | undefined = useOptionalUser(); + + return ( +
+
+
+ + Court dibs + +

Call dibs on one of our sportsball courts

+
+
+ pball +
+
+ tennis racquet +
+
+ bball +
+
+
+
+ {hideLoginLinks ? null : user ? ( + <> +

{user.email}

+
+ +
+ + ) : ( + + Sign up or sign in + + )} +
+
+
+ ); +} diff --git a/app/routes/HeaderLeft.tsx b/app/routes/HeaderLeft.tsx deleted file mode 100644 index bc7c7d9..0000000 --- a/app/routes/HeaderLeft.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { Link } from "@remix-run/react"; - -export const HeaderLeft = () => ( -
- - Court dibs - -

Call dibs on one of our sportsball courts

-
-
- pball -
-
- tennis racquet -
-
- bball -
-
-
-); diff --git a/app/routes/ReservationList.tsx b/app/routes/ReservationList.tsx index 765d479..c99ee45 100644 --- a/app/routes/ReservationList.tsx +++ b/app/routes/ReservationList.tsx @@ -24,7 +24,7 @@ export interface Rez { openPlay: boolean; } -const dateToHeader = (date: Date) => { +export const dateToHeader = (date: Date) => { const prefix = isToday(date) ? "Today - " : isTomorrow(date) diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx index a402496..40481de 100644 --- a/app/routes/_index.tsx +++ b/app/routes/_index.tsx @@ -1,12 +1,12 @@ import type { User } from "@prisma/client"; import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; -import { Form, Link, json, useLoaderData } from "@remix-run/react"; +import { json, useLoaderData } from "@remix-run/react"; import { getReservations } from "~/models/reservation.server"; import { getSession } from "~/session.server"; import { useOptionalUser } from "~/utils"; -import { HeaderLeft } from "./HeaderLeft"; +import { Header } from "./Header"; import { ReservationList, Rez } from "./ReservationList"; export const meta: MetaFunction = () => [{ title: "Court dibs" }]; @@ -30,30 +30,7 @@ export default function Index() { return ( <> -
-
- -
- {user ? ( - <> -

{user.email}

-
- -
- - ) : ( - - Sign up or sign in - - )} -
-
-
+
[{ title: "Magic" }]; export default function Magic() { return ( <> -
-
- -
-
-
+

Check your inbox for a magic link.

diff --git a/app/routes/reservations.$reservationId.tsx b/app/routes/reservations.$reservationId.tsx index 0a1fc22..5d44b74 100644 --- a/app/routes/reservations.$reservationId.tsx +++ b/app/routes/reservations.$reservationId.tsx @@ -13,6 +13,8 @@ import { deleteReservation, getReservation } from "~/models/reservation.server"; import { requireUserId } from "~/session.server"; import { useUser } from "~/utils"; +import { Header } from "./Header"; + export const loader = async ({ params, request }: LoaderFunctionArgs) => { const userId = await requireUserId(request); invariant(params.reservationId, "reservationId not found"); @@ -37,38 +39,64 @@ export const action = async ({ params, request }: ActionFunctionArgs) => { return redirect("/"); }; +const courtIcon = (val: string) => + val === "pb" ? ( +
+ pball +
+ ) : val === "10s" ? ( +
+ tennis racquet +
+ ) : ( +
+ bball +
+ ); + export default function ReservationDetailsPage() { const data = useLoaderData(); const currUser = useUser(); - const { start, end, user } = data.reservation; + const { start, end, user, court, openPlay } = data.reservation; const canDelete = currUser.id === user.id; return ( -
-

{format(start, "iiii, MMMM dd")}

-

- {user.email} -
- {format(start, "h:mm bbb")} -  -  - {format(end, "h:mm bbb")} -

- - {canDelete ? ( - <> -
-
- -
- - ) : null} -
+ <> +
+
+
+

{format(start, "iiii, MMMM dd")}

+

{user.email}

+

+ {format(start, "h:mm bbb")} +  -  + {format(end, "h:mm bbb")} +

+

{openPlay ? "Open play" : null}

+

{courtIcon(court)}

+ {canDelete ? ( + <> +
+
+ +
+ + ) : null} +
+
+ ); } diff --git a/app/routes/reservations.new.tsx b/app/routes/reservations.new.tsx index 6749ed6..723d42d 100644 --- a/app/routes/reservations.new.tsx +++ b/app/routes/reservations.new.tsx @@ -2,11 +2,14 @@ import type { ActionFunctionArgs } from "@remix-run/node"; import { json, redirect } from "@remix-run/node"; import { Form, Link, useActionData, useSearchParams } from "@remix-run/react"; import { addMinutes, startOfToday } from "date-fns"; -import { useEffect, useRef } from "react"; +import { useRef } from "react"; import { createReservation } from "~/models/reservation.server"; import { requireUserId } from "~/session.server"; +import { Header } from "./Header"; +import { dateToHeader } from "./ReservationList"; + export const action = async ({ request }: ActionFunctionArgs) => { const userId = await requireUserId(request); const formData = await request.formData(); @@ -66,141 +69,146 @@ export default function NewReservationPage() { const courtRef = useRef(null); const durationRef = useRef(null); - useEffect(() => { - if (actionData?.errors?.start) { - startTimeRef.current?.focus(); - } - }, [actionData]); - return ( -
-
-
-
- -

{params.get("start") + " on " + params.get("day")}

-
-
- -
-
-
-
- How long are you playing? -
- - - + <> +
+
+ +
+
+

What day?

+

{dateToHeader(params.get("day") as unknown as Date)}

-
-
console.log(e.target.value)} - > - Which court? -
- - -
+
+
+ + How long are you playing? + +
+ + + +
+
+
console.log(e.target.value)} + > + Which court? +
+ + + +
+
+
+
+
+ +
+
+
+ + +
+ + {actionData?.errors?.start ? ( +
+ {actionData.errors.start}
-
-
-
- - -
- - {actionData?.errors?.start ? ( -
- {actionData.errors.start} + ) : null} + +
+ + + Cancel +
- ) : null} - -
- - - Cancel - -
- -
+ +
+ ); } diff --git a/app/routes/start.tsx b/app/routes/start.tsx index 3b10edf..2556e97 100644 --- a/app/routes/start.tsx +++ b/app/routes/start.tsx @@ -8,7 +8,7 @@ import invariant from "tiny-invariant"; import { createUser, getUserByEmail } from "~/models/user.server"; import { validateCoordinates, validateEmail } from "~/utils"; -import { HeaderLeft } from "./HeaderLeft"; +import { Header } from "./Header"; const HALF = "AIzaSyBI_vhCo"; const OTHER_HALF = "hiRS0dvt5Yk7sAJ-978T_mUwd8"; @@ -152,12 +152,7 @@ export default function Start() { return ( <> -
-
- -
-
-
+

Sign up or log in to your account, no password needed!

diff --git a/app/styles.css b/app/styles.css index 3de35c8..c1a283a 100644 --- a/app/styles.css +++ b/app/styles.css @@ -26,6 +26,7 @@ --font-family-header: "Roboto"; --body-font-size: 18px; --small-font-size: 16px; + --large-font-size: 20px; /* layout */ --wrapper-width: 1000px; --mobile-width: 700px; @@ -595,3 +596,29 @@ input:checked ~ .newRes_box:before { border-radius: 50%; background: white; } + +/* existing reservation detail */ + +.existingRes { + margin: 0 auto; + max-width: var(--wrapper-width); + padding: 40px 20px; +} + +.existingRes_stack { + display: flex; + flex-direction: column; + justify-content: center; +} + +.existingRes_label { + font-size: var(--large-font-size); + font-weight: bold; +} + +.existingRes_courtIcon { + border-radius: 10px; + box-shadow: var(--neutral-dark-shadow); + height: 20px; + width: 20px; +}