Skip to content

Commit

Permalink
feat(L5): Basic shopping site
Browse files Browse the repository at this point in the history
  • Loading branch information
bewildergeist committed Mar 1, 2022
1 parent 3340e9b commit a834cf2
Show file tree
Hide file tree
Showing 24 changed files with 12,422 additions and 0 deletions.
557 changes: 557 additions & 0 deletions lesson-04/remix-site/app/tailwind.css

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions lesson-05/remix-shopping-with-api/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
node_modules

/.cache
/build
/public/build
.env
/app/tailwind.css
61 changes: 61 additions & 0 deletions lesson-05/remix-shopping-with-api/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# WIP Remix site

This is the site we implemented in lesson 3 based on [this video tutorial by Brad Traversy](https://www.youtube.com/watch?v=d_BhzHVV4aQ), although swapping in [lowdb](https://github.com/typicode/lowdb) as a simpler alternative to setting up Prisma and SQLite.

The original README follows below.

---

# Welcome to Remix!

- [Remix Docs](https://remix.run/docs)

## Development

From your terminal:

```sh
npm run dev
```

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

## Deployment

First, build your app for production:

```sh
npm run build
```

Then run the app in production mode:

```sh
npm start
```

Now you'll need to pick a host to deploy it to.

### DIY

If you're familiar with deploying node applications, the built-in Remix app server is production-ready.

Make sure to deploy the output of `remix build`

- `build/`
- `public/build/`

### Using a Template

When you ran `npx create-remix@latest` there were a few choices for hosting. You can run that again to create a new project, then copy over your `app/` folder to the new project that's pre-configured for your target server.

```sh
cd ..
# create a new project, and pick a pre-configured host
npx create-remix@latest
cd my-new-remix-app
# remove the new project's app (not the old one!)
rm -rf app
# copy your app over
cp -R ../my-old-remix-app/app app
```
21 changes: 21 additions & 0 deletions lesson-05/remix-shopping-with-api/app/components/Breadcrumb.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Link } from "remix";

export default function Breadcrumb({ links = [] }) {
import React from "react";
const breadcrumbs = [{ to: "/", title: "Home" }, ...links];

return (
<nav className="mb-4 text-sm font-semibold">
{breadcrumbs.map((link, i) => (
<React.Fragment key={i}>
{i !== 0 && (
<span className="inline-block mx-2 text-gray-400">/</span>
)}
<Link to={link.to} className="text-blue-700 hover:underline">
{link.title}
</Link>
</React.Fragment>
))}
</nav>
);
}
13 changes: 13 additions & 0 deletions lesson-05/remix-shopping-with-api/app/components/Button.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Link } from "remix";

export default function Button({ type, destructive = false, children }) {
let className = `${
destructive ? "bg-red-500" : "bg-blue-500"
} text-white font-bold py-2 px-4 rounded my-3 inline-block`;

return (
<button className={className} type={type}>
{children}
</button>
);
}
11 changes: 11 additions & 0 deletions lesson-05/remix-shopping-with-api/app/components/LinkButton.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Link } from "remix";

export default function Button({ to, children }) {
return (
<Link
className="bg-blue-500 text-white font-bold py-2 px-4 rounded my-3 inline-block"
to={to}>
{children}
</Link>
);
}
13 changes: 13 additions & 0 deletions lesson-05/remix-shopping-with-api/app/components/PageHeader.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export default function PageHeader({ title, subtitle, children }) {
return (
<div className="flex items-center justify-between mb-5">
<div>
<h1 className="text-2xl font-bold">{title}</h1>
{subtitle && (
<h2 className="font-semibold text-gray-400">{subtitle}</h2>
)}
</div>
{children}
</div>
);
}
19 changes: 19 additions & 0 deletions lesson-05/remix-shopping-with-api/app/db/luggage/db.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"products": [
{
"id": "1",
"title": "Small suitcase",
"description": "The smallest we have."
},
{
"id": "2",
"title": "Medium suitcase",
"description": "A decent size."
},
{
"id": "3",
"title": "Large suitcase",
"description": "If you have a lot of stuff."
}
]
}
9 changes: 9 additions & 0 deletions lesson-05/remix-shopping-with-api/app/db/luggage/db.server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import path from "path";
import { JSONFileSync, LowSync } from "lowdb";

const file = path.join(__dirname, "../app/db/luggage/db.json");
const adapter = new JSONFileSync(file);
const db = new LowSync(adapter);

db.read();
export default db;
4 changes: 4 additions & 0 deletions lesson-05/remix-shopping-with-api/app/entry.client.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { hydrate } from "react-dom";
import { RemixBrowser } from "remix";

hydrate(<RemixBrowser />, document);
20 changes: 20 additions & 0 deletions lesson-05/remix-shopping-with-api/app/entry.server.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { renderToString } from "react-dom/server";
import { RemixServer } from "remix";

export default function handleRequest(
request,
responseStatusCode,
responseHeaders,
remixContext
) {
const markup = renderToString(
<RemixServer context={remixContext} url={request.url} />
);

responseHeaders.set("Content-Type", "text/html");

return new Response("<!DOCTYPE html>" + markup, {
status: responseStatusCode,
headers: responseHeaders,
});
}
136 changes: 136 additions & 0 deletions lesson-05/remix-shopping-with-api/app/root.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import {
Link,
NavLink,
Outlet,
LiveReload,
Links,
Meta,
Scripts,
useCatch,
} from "remix";
import styles from "~/tailwind.css";

export const links = () => [
{
rel: "stylesheet",
href: styles,
},
];

export const meta = () => ({
description: "A collection of great recipes",
keywords: "cooking, food, recipes",
});

export default function App() {
return (
<Document title="Remixed Recipes">
<Layout>
<Outlet />
</Layout>
</Document>
);
}

function Document({ children, title }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
<title>{title}</title>
</head>
<body className="bg-slate-100 text-gray-800">
{children}
{process.env.NODE_ENV === "development" ? <LiveReload /> : null}
<Scripts />
</body>
</html>
);
}

function Layout({ children }) {
return (
<>
<header className="container mx-auto">
<nav className="text-left">
<Link to="/" className="group transition-colors mt-5 mb-6 block">
<span className="block font-extrabold text-4xl text-amber-500 group-hover:text-orange-400 ">
Kablamazon
</span>
<span className="block font-semibold italic text-gray-400 group-hover:text-orange-400 ">
Shop for anything
</span>
</Link>
</nav>
</header>
<div className="container mx-auto flex flex-row">
<nav className="bg-gray-50 border border-gray-200 rounded mr-6 shadow-inner">
<MenuLink to="/luggage">Luggage</MenuLink>
<MenuLink to="/books">Books</MenuLink>
<MenuLink to="/electronics">Electronics</MenuLink>
<MenuLink to="/computers">Computers</MenuLink>
<MenuLink to="/toys">Toys</MenuLink>
<MenuLink to="/music">Music</MenuLink>
<MenuLink to="/musical-instruments">Musical instruments</MenuLink>
<MenuLink to="/movies">Movies</MenuLink>
<MenuLink to="/sports">Sports equipment</MenuLink>
<MenuLink to="/video-games">Video games</MenuLink>
<MenuLink to="/home-and-kitchen">Home and kitchen</MenuLink>
<MenuLink to="/crafts">Arts and crafts</MenuLink>
<MenuLink to="/beauty">Beauty and personal care</MenuLink>
<MenuLink to="/womens-fashion">Women's fashion</MenuLink>
<MenuLink to="/mens-fashion">Men's fashion</MenuLink>
<MenuLink to="/kids-fashion">Kid's fashion</MenuLink>
<MenuLink to="/pet-supplies">Pet supplies</MenuLink>
</nav>
<main className="flex-grow">{children}</main>
</div>
</>
);
}

function MenuLink({ to, children }) {
return (
<NavLink
to={to}
className={({ isActive }) =>
`block hover:bg-gray-200 transition-all text-lg font-bold px-4 py-1 ${
isActive ? "bg-gray-200" : ""
}`
}>
{children}
</NavLink>
);
}

export function ErrorBoundary({ error }) {
return (
<Document>
<Layout>
<div className="bg-red-100 text-red-700 border border-red-200 p-4 rounded">
<h1 className="text-2xl mb-3 font-bold">Error</h1>
<p>{error.message}</p>
</div>
</Layout>
</Document>
);
}

export function CatchBoundary() {
const caught = useCatch();
return (
<Document>
<Layout>
<div className="bg-red-100 text-red-700 border border-red-200 p-4 rounded">
<h1 className="text-2xl mb-3 font-bold">Whoops</h1>
<p>
{caught.status} {caught.statusText}
</p>
</div>
</Layout>
</Document>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import db from "~/db/luggage/db.server.js";

export async function loader({ params }) {
const product = db.data.products?.find((p) => p.id === params.productId);
return product;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import db from "~/db/luggage/db.server.js";

export async function loader() {
return db.data.products ?? [];
}
12 changes: 12 additions & 0 deletions lesson-05/remix-shopping-with-api/app/routes/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import LinkButton from "~/components/LinkButton.jsx";

export default function Home() {
return (
<section className="flex-grow items-center justify-center">
<h1 className="text-4xl font-bold text-gray-400">
The web's premiere destination for shopping absolutely{" "}
<i className="italic">anything</i>.
</h1>
</section>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { redirect, Link } from "remix";
import { useLoaderData } from "remix";
import PageHeader from "~/components/PageHeader";
import Button from "~/components/Button.jsx";
import Breadcrumb from "~/components/Breadcrumb.jsx";
import db from "~/db/luggage/db.server";

export const loader = async function ({ params }) {
const product = await fetch(
`http://localhost:3000/api/luggage/${params.productId}`
);

if (!product) {
throw new Error("Product not found");
}

return product;
};

export const action = async function ({ request, params }) {
const form = await request.formData();
if (form.get("_method") === "delete") {
// TODO: Create an API route and send a DELETE request to it
throw new Error("Delete not implemented");
return redirect("/luggage");
}
};

export default function Post() {
const product = useLoaderData();

return (
<div>
<Breadcrumb links={[{ to: "/luggage", title: "Luggage" }]} />
<PageHeader title={product.title} />
<p>{product.description}</p>
<form method="post" className="mt-5 pt-2 border-t border-gray-200">
<input type="hidden" name="_method" value="delete" />
<Button type="submit" destructive>
Delete
</Button>
</form>
</div>
);
}
Loading

0 comments on commit a834cf2

Please sign in to comment.