Skip to content

Commit

Permalink
initial layout scaffolding= (#4)
Browse files Browse the repository at this point in the history
* ditch the boilerplate sonic youth layout
* display available (and reserved) timeslots in a grid instead of a list
* start styling the form for submitting a new reservation
  • Loading branch information
mel-thomas authored Jun 1, 2024
1 parent 3bb6bf8 commit 1177b13
Showing 29 changed files with 1,119 additions and 351 deletions.
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
## setup

```sh
npm install
npm run setup
```sh
npm install
npm run setup
```

## development

```sh
npm run dev
open http://localhost:3000
```
```sh
npm run dev
open http://localhost:3000
```

This starts the app in development mode, rebuilding assets on file changes.

4 changes: 2 additions & 2 deletions app/models/reservation.server.ts
Original file line number Diff line number Diff line change
@@ -8,7 +8,7 @@ export function getReservation({
}: Pick<Reservation, "id"> & {
userId: User["id"];
}) {
userId
userId;
return prisma.reservation.findFirst({
select: { id: true, start: true, end: true, court: true, user: true },
where: { id },
@@ -23,7 +23,7 @@ export function getReservations() {
start: true,
end: true,
court: true,
openPlay: true
openPlay: true,
},
});
}
3 changes: 1 addition & 2 deletions app/models/user.server.ts
Original file line number Diff line number Diff line change
@@ -20,12 +20,11 @@ export async function createUser(email: User["email"], stytchId: string) {
return prisma.user.create({
data: {
email,
stytchId
stytchId,
},
});
}

export async function deleteUserByEmail(email: User["email"]) {
return prisma.user.delete({ where: { email } });
}

9 changes: 8 additions & 1 deletion app/root.tsx
Original file line number Diff line number Diff line change
@@ -12,6 +12,7 @@ import {

import { getUser } from "~/session.server";
import stylesheet from "~/tailwind.css";
import "./styles.css";

export const links: LinksFunction = () => [
{ rel: "stylesheet", href: stylesheet },
@@ -28,10 +29,16 @@ export default function App() {
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" />
<link
href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap"
rel="stylesheet"
/>
<Meta />
<Links />
</head>
<body className="h-full">
<body>
<Outlet />
<ScrollRestoration />
<Scripts />
30 changes: 30 additions & 0 deletions app/routes/HeaderLeft.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Link } from "@remix-run/react";

export const HeaderLeft = () => (
<div className="header_left">
<Link to="/" className="h1">
Court dibs
</Link>
<h2 className="h2">Call dibs on one of our sportsball courts</h2>
<div className="header_illustration">
<div className="header_icon header_icon___pickleball">
<img
alt="pball"
src="https://cdn.glitch.global/5f00a93b-ae9c-4d9a-b9cf-472487408ff8/pickleball-solid.svg?v=1714837585684"
/>
</div>
<div className="header_icon header_icon___tennis">
<img
alt="tennis racquet"
src="https://cdn.glitch.global/5f00a93b-ae9c-4d9a-b9cf-472487408ff8/tennis-ball-solid.svg?v=1714837585529"
/>
</div>
<div className="header_icon header_icon___basketball">
<img
alt="bball"
src="https://cdn.glitch.global/5f00a93b-ae9c-4d9a-b9cf-472487408ff8/basketball-solid.svg?v=1714837585367"
/>
</div>
</div>
</div>
);
235 changes: 185 additions & 50 deletions app/routes/ReservationList.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import type { User } from "@prisma/client";
import { Link, NavLink } from "@remix-run/react";
import { Link, useNavigate } from "@remix-run/react";
import cn from "classnames";
import {
addDays,
addHours,
areIntervalsOverlapping,
format,
isEqual,
isToday,
@@ -11,9 +14,6 @@ import {
} from "date-fns";
import React from "react";

const courtEmoji = (court: string) =>
court === "pb" ? `🏓` : court === "bball" ? `🏀` : `🎾`;

// it would be nicer to use Reservation from @prisma/client
// but start/end are serialized to strings by useLoaderData 🙃
export interface Rez {
@@ -34,49 +34,161 @@ const dateToHeader = (date: Date) => {
return prefix + format(date, "iiii, MMMM dd");
};

const DayList = ({
const rezTimes = [...Array(12).keys()].map((v: number) => v + 8);

type CourtType = "pb" | "bball" | "10s";

const isOverlapping = (r: Rez, date: Date, hour: number) => {
return (
areIntervalsOverlapping(
{ start: r.start, end: r.end },
{
start: addHours(date, hour),
end: addHours(date, hour + 0.01),
},
) && !r.openPlay
);
};

const Guts = ({
reservations,
isLoggedIn = false,
court,
date,
}: {
reservations: Rez[];
isLoggedIn?: boolean;
court: CourtType;
date: Date;
}) => {
return reservations.length === 0 ? (
<p className="p-4">No reservations yet</p>
) : (
<ol>
{reservations
.sort((a, b) => (a.start.valueOf() > b.start.valueOf() ? 1 : -1))
.map((rez) => (
<li key={rez.id}>
{isLoggedIn ? (
<NavLink
className={({ isActive }) =>
`block border-b p-4 text-xl ${isActive ? "bg-white" : ""}`
}
to={"/reservations/" + rez.id}
>
{courtEmoji(rez.court)}&nbsp;
{format(rez.start, "h:mm bbb")}
&nbsp;-&nbsp;
{format(rez.end, "h:mm bbb")}
&nbsp;
{rez.openPlay ? "🌍" : ""}
</NavLink>
) : (
<p className="p-2">
{courtEmoji(rez.court)}&nbsp;
{format(rez.start, "h:mm bbb")}
&nbsp;-&nbsp;
{format(rez.end, "h:mm bbb")}
</p>
)}
</li>
))}
</ol>
isLoggedIn;

const navigate = useNavigate();

return (
<div
className={cn("schedule_content", {
schedule_content___pickleball: court === "pb",
schedule_content___basketball: court === "bball",
schedule_content___tennis: court === "10s",
})}
>
{rezTimes.map((num) => {
const onHourPrivate = reservations.some(
(r) => isOverlapping(r, date, num) && !r.openPlay,
);

const onHourOpenPlay = reservations.some(
(r) => isOverlapping(r, date, num) && r.openPlay,
);

const halfHourPrivate = reservations.some(
(r) => isOverlapping(r, date, num + 0.5) && !r.openPlay,
);

const halfHourOpenPlay = reservations.some(
(r) => isOverlapping(r, date, num + 0.5) && r.openPlay,
);

const canReserveOnHour = !onHourPrivate && !onHourOpenPlay;
const canReserveOnHalfHour = !halfHourPrivate && !halfHourOpenPlay;

return (
<div className="schedule_row" key={num}>
<button
className={cn("schedule_button", {
schedule_button___private: onHourPrivate,
schedule_button___open: onHourOpenPlay,
})}
onClick={() =>
canReserveOnHour
? navigate(
`/reservations/new?day=${date
.toISOString()
.slice(0, 10)}`,
)
: undefined
}
>
{num + ":00"}
</button>
<button
className={cn("schedule_button", {
schedule_button___private: halfHourPrivate,
schedule_button___open: halfHourOpenPlay,
})}
onClick={() =>
canReserveOnHalfHour
? navigate(
`/reservations/new?day=${date
.toISOString()
.slice(0, 10)}`,
)
: console.log("cant touch this")
}
>
{num + ":30"}
</button>
</div>
);
})}
</div>
);
};

const TimeSlots = ({
reservations,
isLoggedIn = false,
court,
date,
}: {
reservations: Rez[];
isLoggedIn?: boolean;
court: CourtType;
date: Date;
}) => (
<div
className={cn("schedule", {
schedule___basketball: court === "bball",
schedule___tennis: court === "10s",
})}
>
<h3 className="schedule_header">
<div className="schedule_icon">
{court === "pb" ? (
<img
alt="pickleball paddle"
src="https://cdn.glitch.global/5f00a93b-ae9c-4d9a-b9cf-472487408ff8/pickleball.svg?v=1714921535243"
/>
) : court === "10s" ? (
<img
alt="tennis racquet"
src="https://cdn.glitch.global/5f00a93b-ae9c-4d9a-b9cf-472487408ff8/tennis.svg?v=1714921535054"
/>
) : (
<img
alt="basketball"
src="https://cdn.glitch.global/5f00a93b-ae9c-4d9a-b9cf-472487408ff8/basketball.svg?v=1714921534845"
/>
)}
</div>
{court === "pb" ? (
<div>Pickleball</div>
) : court === "10s" ? (
<div>Tennis</div>
) : (
<div>Basketball</div>
)}
</h3>
<Guts
reservations={reservations}
isLoggedIn={isLoggedIn}
court={court}
date={date}
/>
</div>
);

export const ReservationList = ({
reservations,
user,
@@ -96,18 +208,41 @@ export const ReservationList = ({

return availableDays.map(({ date, existingReservations }) => (
<React.Fragment key={date.toISOString()}>
<h1 className="text-2xl font-bold">
{dateToHeader(date)}&nbsp;
{user ? (
<Link
to={`/reservations/new?day=${date.toISOString().slice(0, 10)}`}
className="text-blue-500"
>
+
</Link>
) : null}
</h1>
<DayList reservations={existingReservations} isLoggedIn={!!user} />
<nav className="nav">
<div className="nav_content">
<a href="/" className="nav_link">
{dateToHeader(date)}&nbsp;
</a>
</div>
</nav>
{user ? (
<Link
to={`/reservations/new?day=${date.toISOString().slice(0, 10)}`}
className="text-blue-500"
>
+
</Link>
) : null}
<main className="main">
<TimeSlots
reservations={existingReservations.filter((r) => r.court === "pb")}
isLoggedIn={!!user}
court="pb"
date={date}
/>
<TimeSlots
reservations={existingReservations.filter((r) => r.court === "bball")}
isLoggedIn={!!user}
court="bball"
date={date}
/>
<TimeSlots
reservations={existingReservations.filter((r) => r.court === "10s")}
isLoggedIn={!!user}
court="10s"
date={date}
/>
</main>
</React.Fragment>
));
};
Loading

0 comments on commit 1177b13

Please sign in to comment.