Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

layout wip #4

Merged
merged 9 commits into from
Jun 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.

Expand Down
4 changes: 2 additions & 2 deletions app/models/reservation.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand All @@ -23,7 +23,7 @@ export function getReservations() {
start: true,
end: true,
court: true,
openPlay: true
openPlay: true,
},
});
}
Expand Down
3 changes: 1 addition & 2 deletions app/models/user.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Up @@ -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 },
Expand All @@ -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 />
Expand Down
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,
Expand All @@ -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 {
Expand All @@ -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,
Expand All @@ -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