Following tutorial to set up remix app

This commit is contained in:
2024-04-26 20:22:03 +02:00
parent a68339f3db
commit 6877c045c3
25 changed files with 12874 additions and 2 deletions
+84
View File
@@ -0,0 +1,84 @@
/**
* This is intended to be a basic starting point for linting in your app.
* It relies on recommended configs out of the box for simplicity, but you can
* and should modify this configuration to best suit your team's needs.
*/
/** @type {import('eslint').Linter.Config} */
module.exports = {
root: true,
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
ecmaFeatures: {
jsx: true,
},
},
env: {
browser: true,
commonjs: true,
es6: true,
},
ignorePatterns: ["!**/.server", "!**/.client"],
// Base config
extends: ["eslint:recommended"],
overrides: [
// React
{
files: ["**/*.{js,jsx,ts,tsx}"],
plugins: ["react", "jsx-a11y"],
extends: [
"plugin:react/recommended",
"plugin:react/jsx-runtime",
"plugin:react-hooks/recommended",
"plugin:jsx-a11y/recommended",
],
settings: {
react: {
version: "detect",
},
formComponents: ["Form"],
linkComponents: [
{ name: "Link", linkAttribute: "to" },
{ name: "NavLink", linkAttribute: "to" },
],
"import/resolver": {
typescript: {},
},
},
},
// Typescript
{
files: ["**/*.{ts,tsx}"],
plugins: ["@typescript-eslint", "import"],
parser: "@typescript-eslint/parser",
settings: {
"import/internal-regex": "^~/",
"import/resolver": {
node: {
extensions: [".ts", ".tsx"],
},
typescript: {
alwaysTryTypes: true,
},
},
},
extends: [
"plugin:@typescript-eslint/recommended",
"plugin:import/recommended",
"plugin:import/typescript",
],
},
// Node
{
files: [".eslintrc.cjs"],
env: {
node: true,
},
},
],
};
+8
View File
@@ -0,0 +1,8 @@
node_modules
/.cache
/build
.env
*.code-workspace
*.db
+35 -2
View File
@@ -1,3 +1,36 @@
# beer-inventory
# Welcome to Remix + Vite!
Fridge beer and other drinks inventory system.
📖 See the [Remix docs](https://remix.run/docs) and the [Remix Vite docs](https://remix.run/docs/en/main/guides/vite) for details on supported features.
## Development
Run the Vite dev server:
```shellscript
npm run dev
```
## 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 `npm run build`
- `build/server`
- `build/client`
+18
View File
@@ -0,0 +1,18 @@
/**
* By default, Remix will handle hydrating your app on the client for you.
* You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
* For more information, see https://remix.run/file-conventions/entry.client
*/
import { RemixBrowser } from "@remix-run/react";
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<RemixBrowser />
</StrictMode>
);
});
+140
View File
@@ -0,0 +1,140 @@
/**
* By default, Remix will handle generating the HTTP Response for you.
* You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
* For more information, see https://remix.run/file-conventions/entry.server
*/
import { PassThrough } from "node:stream";
import type { AppLoadContext, EntryContext } from "@remix-run/node";
import { createReadableStreamFromReadable } from "@remix-run/node";
import { RemixServer } from "@remix-run/react";
import { isbot } from "isbot";
import { renderToPipeableStream } from "react-dom/server";
const ABORT_DELAY = 5_000;
export default function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
// This is ignored so we can keep it in the template for visibility. Feel
// free to delete this parameter in your app if you're not using it!
// eslint-disable-next-line @typescript-eslint/no-unused-vars
loadContext: AppLoadContext
) {
return isbot(request.headers.get("user-agent") || "")
? handleBotRequest(
request,
responseStatusCode,
responseHeaders,
remixContext
)
: handleBrowserRequest(
request,
responseStatusCode,
responseHeaders,
remixContext
);
}
function handleBotRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext
) {
return new Promise((resolve, reject) => {
let shellRendered = false;
const { pipe, abort } = renderToPipeableStream(
<RemixServer
context={remixContext}
url={request.url}
abortDelay={ABORT_DELAY}
/>,
{
onAllReady() {
shellRendered = true;
const body = new PassThrough();
const stream = createReadableStreamFromReadable(body);
responseHeaders.set("Content-Type", "text/html");
resolve(
new Response(stream, {
headers: responseHeaders,
status: responseStatusCode,
})
);
pipe(body);
},
onShellError(error: unknown) {
reject(error);
},
onError(error: unknown) {
responseStatusCode = 500;
// Log streaming rendering errors from inside the shell. Don't log
// errors encountered during initial shell rendering since they'll
// reject and get logged in handleDocumentRequest.
if (shellRendered) {
console.error(error);
}
},
}
);
setTimeout(abort, ABORT_DELAY);
});
}
function handleBrowserRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext
) {
return new Promise((resolve, reject) => {
let shellRendered = false;
const { pipe, abort } = renderToPipeableStream(
<RemixServer
context={remixContext}
url={request.url}
abortDelay={ABORT_DELAY}
/>,
{
onShellReady() {
shellRendered = true;
const body = new PassThrough();
const stream = createReadableStreamFromReadable(body);
responseHeaders.set("Content-Type", "text/html");
resolve(
new Response(stream, {
headers: responseHeaders,
status: responseStatusCode,
})
);
pipe(body);
},
onShellError(error: unknown) {
reject(error);
},
onError(error: unknown) {
responseStatusCode = 500;
// Log streaming rendering errors from inside the shell. Don't log
// errors encountered during initial shell rendering since they'll
// reject and get logged in handleDocumentRequest.
if (shellRendered) {
console.error(error);
}
},
}
);
setTimeout(abort, ABORT_DELAY);
});
}
+44
View File
@@ -0,0 +1,44 @@
import type { LinksFunction } from "@remix-run/node";
import {
Links,
LiveReload,
Outlet
} from "@remix-run/react";
import globalLargeStylesUrl from "~/styles/global-large.css?url";
import globalMediumStylesUrl from "~/styles/global-medium.css?url";
import globalStylesUrl from "~/styles/global.css?url";
export const links: LinksFunction = () => [
{ rel: "stylesheet", href: globalStylesUrl },
{
rel: "stylesheet",
href: globalMediumStylesUrl,
media: "print, (min-width: 640px)",
},
{
rel: "stylesheet",
href: globalLargeStylesUrl,
media: "screen and (min-width: 1024px)",
},
];
export default function App() {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1"
/>
<title>Remix: So great, it's funny!</title>
<Links />
</head>
<body>
<Outlet />
<LiveReload />
</body>
</html>
);
}
+25
View File
@@ -0,0 +1,25 @@
import type { LinksFunction } from "@remix-run/node";
import stylesUrl from "~/styles/index.css?url";
export const links: LinksFunction = () => [
{ rel: "stylesheet", href: stylesUrl },
];
export default function Index() {
return (
<div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.8" }}>
<h1>Welcome to Kenneths Beer Inventory Server</h1>
<ul>
<li>
<a
href="/beers"
rel="noreferrer"
>
Meet the beers!
</a>
</li>
</ul>
</div>
);
}
+29
View File
@@ -0,0 +1,29 @@
import type { LoaderFunctionArgs } from "@remix-run/node";
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { db } from "~/utils/db.server";
export const loader = async ({
params,
}: LoaderFunctionArgs) => {
const beer = await db.beer.findUnique({
where: { id: params.beerid },
});
if (!beer) {
throw new Error("Beer not found");
}
return json({ beer });
};
export default function BeerRoute() {
const data = useLoaderData<typeof loader>();
return (
<div>
<p>You selected this beer:</p>
<p>{data.beer.name}</p>
<p>ABV {data.beer.abv}%</p>
</div>
);
}
+28
View File
@@ -0,0 +1,28 @@
import { json } from "@remix-run/node";
import { Link, useLoaderData } from "@remix-run/react";
import { db } from "~/utils/db.server";
export const loader = async () => {
const count = await db.beer.count();
const randomRowNumber = Math.floor(Math.random() * count);
const [randomBeer] = await db.beer.findMany({
skip: randomRowNumber,
take: 1,
});
return json({ randomBeer });
};
export default function BeersIndexRoute() {
const data = useLoaderData<typeof loader>();
return (
<div>
<p>Here's a random Beer:</p>
<p>{data.randomBeer.name}</p>
<Link to={data.randomBeer.id}>
"{data.randomBeer.name}" Permalink
</Link>
</div>
);
}
+50
View File
@@ -0,0 +1,50 @@
import type { ActionFunctionArgs } from "@remix-run/node";
import { redirect } from "@remix-run/node";
import { db } from "~/utils/db.server";
export const action = async ({
request,
}: ActionFunctionArgs) => {
const form = await request.formData();
const name = form.get("name");
const abv = Number(form.get("abv"));
// we do this type check to be extra sure and to make TypeScript happy
// we'll explore validation next!
if (
typeof name !== "string" ||
typeof abv !== "number"
) {
throw new Error("Form not submitted correctly.");
}
const fields = { name, abv };
const beer = await db.beer.create({ data: fields });
return redirect(`/beers/${beer.id}`);
};
export default function NewBeerRoute() {
return (
<div>
<p>Add your own beer</p>
<form method="post">
<div>
<label>
Name: <input type="text" name="name" />
</label>
</div>
<div>
<label>
ABV: <input type="number" min="0" max="100" step="0.1" name="abv" />
</label>
</div>
<div>
<button type="submit" className="button">
Add
</button>
</div>
</form>
</div>
);
}
+68
View File
@@ -0,0 +1,68 @@
import type { LinksFunction } from "@remix-run/node";
import { json } from "@remix-run/node";
import {
Link,
Outlet,
useLoaderData,
} from "@remix-run/react";
import stylesUrl from "~/styles/beers.css?url";
import { db } from "~/utils/db.server";
export const links: LinksFunction = () => [
{ rel: "stylesheet", href: stylesUrl },
];
export const loader = async () => {
return json({
beerListItems: await db.beer.findMany({
orderBy: { id: "asc" },
select: { id: true, name: true },
take: 5,
}),
});
};
export default function BeersRoute() {
const data = useLoaderData<typeof loader>();
return (
<div className="beers-layout">
<header className="beers-header">
<div className="container">
<h1 className="home-link">
<Link
to="/"
title="Remix Beers"
aria-label="Remix Beers"
>
<span className="logo">B</span>
<span className="logo-medium">Beers</span>
</Link>
</h1>
</div>
</header>
<main className="beers-main">
<div className="container">
<div className="beers-list">
<Link to=".">Get a random beer</Link>
<p>Here are a few more beers to check out:</p>
<ul>
{data.beerListItems.map(({ id, name }) => (
<li key={id}>
<Link to={id}>{name}</Link>
</li>
))}
</ul>
<Link to="new" className="button">
Add your own
</Link>
</div>
<div className="beers-outlet">
<Outlet />
</div>
</div>
</main>
</div>
);
}
+93
View File
@@ -0,0 +1,93 @@
.beers-layout {
display: flex;
flex-direction: column;
min-height: inherit;
}
.beers-header {
padding-top: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--color-border);
}
.beers-header .container {
display: flex;
justify-content: space-between;
align-items: center;
}
.beers-header .home-link {
font-family: var(--font-display);
font-size: 3rem;
}
.beers-header .home-link a {
color: var(--color-foreground);
}
.beers-header .home-link a:hover {
text-decoration: none;
}
.beers-header .logo-medium {
display: none;
}
.beers-header a:hover {
text-decoration-style: wavy;
text-decoration-thickness: 1px;
}
.beers-header .user-info {
display: flex;
gap: 1rem;
align-items: center;
white-space: nowrap;
}
.beers-main {
padding-top: 2rem;
padding-bottom: 2rem;
flex: 1 1 100%;
}
.beers-main .container {
display: flex;
gap: 1rem;
}
.beers-list {
max-width: 12rem;
}
.beers-outlet {
flex: 1;
}
.beers-footer {
padding-top: 2rem;
padding-bottom: 1rem;
border-top: 1px solid var(--color-border);
}
@media print, (min-width: 640px) {
.beers-header .logo {
display: none;
}
.beers-header .logo-medium {
display: block;
}
.beers-main {
padding-top: 3rem;
padding-bottom: 3rem;
}
}
@media (max-width: 639px) {
.beers-main .container {
flex-direction: column;
}
}
+25
View File
@@ -0,0 +1,25 @@
h1 {
font-size: 3.75rem;
line-height: 1;
}
h2 {
font-size: 1.875rem;
line-height: 2.25rem;
}
h3 {
font-size: 1.5rem;
line-height: 2rem;
}
h4 {
font-size: 1.25rem;
line-height: 1.75rem;
}
h5 {
font-size: 1.125rem;
line-height: 1.75rem;
}
+30
View File
@@ -0,0 +1,30 @@
h1 {
font-size: 3rem;
line-height: 1;
}
h2 {
font-size: 2.25rem;
line-height: 2.5rem;
}
h3 {
font-size: 1.25rem;
line-height: 1.75rem;
}
h4 {
font-size: 1.125rem;
line-height: 1.75rem;
}
h5,
h6 {
font-size: 1rem;
line-height: 1.5rem;
}
.container {
--gutter: 40px;
}
+355
View File
@@ -0,0 +1,355 @@
:root {
--hs-links: 48 100%;
--color-foreground: hsl(0, 0%, 100%);
--color-background: hsl(278, 73%, 19%);
--color-links: hsl(var(--hs-links) 50%);
--color-links-hover: hsl(var(--hs-links) 45%);
--color-border: hsl(277, 85%, 38%);
--color-invalid: hsl(356, 100%, 71%);
--gradient-background: radial-gradient(
circle,
rgba(152, 11, 238, 1) 0%,
rgba(118, 15, 181, 1) 35%,
rgba(58, 13, 85, 1) 100%
);
--font-body: -apple-system, "Segoe UI", Helvetica Neue, Helvetica,
Roboto, Arial, sans-serif, system-ui, "Apple Color Emoji",
"Segoe UI Emoji";
--font-display: var(--font-body);
}
html {
box-sizing: border-box;
}
*,
*::before,
*::after {
box-sizing: inherit;
}
:-moz-focusring {
outline: auto;
}
:focus {
outline: var(--color-links) solid 2px;
outline-offset: 2px;
}
html,
body {
padding: 0;
margin: 0;
color: var(--color-foreground);
background-color: var(--color-background);
}
[data-light] {
--color-invalid: hsl(356, 70%, 39%);
color: var(--color-background);
background-color: var(--color-foreground);
}
body {
font-family: var(--font-body);
line-height: 1.5;
background-repeat: no-repeat;
min-height: 100vh;
min-height: calc(100vh - env(safe-area-inset-bottom));
}
a {
color: var(--color-links);
text-decoration: none;
}
a:hover {
color: var(--color-links-hover);
text-decoration: underline;
}
hr {
display: block;
height: 1px;
border: 0;
background-color: var(--color-border);
margin-top: 2rem;
margin-bottom: 2rem;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: var(--font-display);
margin: 0;
}
h1 {
font-size: 2.25rem;
line-height: 2.5rem;
}
h2 {
font-size: 1.5rem;
line-height: 2rem;
}
h3 {
font-size: 1.25rem;
line-height: 1.75rem;
}
h4 {
font-size: 1.125rem;
line-height: 1.75rem;
}
h5,
h6 {
font-size: 0.875rem;
line-height: 1.25rem;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
.container {
--gutter: 16px;
width: 1024px;
max-width: calc(100% - var(--gutter) * 2);
margin-right: auto;
margin-left: auto;
}
/* buttons */
.button {
--shadow-color: hsl(var(--hs-links) 30%);
--shadow-size: 3px;
-webkit-appearance: none;
-moz-appearance: none;
cursor: pointer;
appearance: none;
display: inline-flex;
align-items: center;
justify-content: center;
background-color: var(--color-links);
color: var(--color-background);
font-family: var(--font-display);
font-weight: bold;
line-height: 1;
font-size: 1.125rem;
margin: 0;
padding: 0.625em 1em;
border: 0;
border-radius: 4px;
box-shadow: 0 var(--shadow-size) 0 0 var(--shadow-color);
outline-offset: 2px;
transform: translateY(0);
transition: background-color 50ms ease-out, box-shadow
50ms ease-out,
transform 100ms cubic-bezier(0.3, 0.6, 0.8, 1.25);
}
.button:hover {
--raise: 1px;
color: var(--color-background);
text-decoration: none;
box-shadow: 0 calc(var(--shadow-size) + var(--raise)) 0 0 var(
--shadow-color
);
transform: translateY(calc(var(--raise) * -1));
}
.button:active {
--press: 1px;
box-shadow: 0 calc(var(--shadow-size) - var(--press)) 0 0 var(
--shadow-color
);
transform: translateY(var(--press));
background-color: var(--color-links-hover);
}
.button[disabled],
.button[aria-disabled="true"] {
transform: translateY(0);
pointer-events: none;
opacity: 0.7;
}
.button:focus:not(:focus-visible) {
outline: none;
}
/* forms */
form {
display: flex;
flex-direction: column;
gap: 1rem;
width: 100%;
}
fieldset {
margin: 0;
padding: 0;
border: 0;
}
legend {
display: block;
max-width: 100%;
margin-bottom: 0.5rem;
color: inherit;
white-space: normal;
}
[type="text"],
[type="password"],
[type="date"],
[type="datetime"],
[type="datetime-local"],
[type="month"],
[type="week"],
[type="email"],
[type="number"],
[type="search"],
[type="tel"],
[type="time"],
[type="url"],
[type="color"],
textarea {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
display: block;
display: flex;
align-items: center;
width: 100%;
height: 2.5rem;
margin: 0;
padding: 0.5rem 0.75rem;
border: 1px solid var(--color-border);
border-radius: 4px;
background-color: hsl(0 0% 100% / 10%);
background-blend-mode: luminosity;
box-shadow: none;
font-family: var(--font-body);
font-size: 1rem;
font-weight: normal;
line-height: 1.5;
color: var(--color-foreground);
transition: box-shadow 200ms, border-color 50ms ease-out,
background-color 50ms ease-out, color 50ms ease-out;
}
[data-light] [type="text"],
[data-light] [type="password"],
[data-light] [type="date"],
[data-light] [type="datetime"],
[data-light] [type="datetime-local"],
[data-light] [type="month"],
[data-light] [type="week"],
[data-light] [type="email"],
[data-light] [type="number"],
[data-light] [type="search"],
[data-light] [type="tel"],
[data-light] [type="time"],
[data-light] [type="url"],
[data-light] [type="color"],
[data-light] textarea {
color: var(--color-background);
background-color: hsl(0 0% 0% / 10%);
}
[type="text"][aria-invalid="true"],
[type="password"][aria-invalid="true"],
[type="date"][aria-invalid="true"],
[type="datetime"][aria-invalid="true"],
[type="datetime-local"][aria-invalid="true"],
[type="month"][aria-invalid="true"],
[type="week"][aria-invalid="true"],
[type="email"][aria-invalid="true"],
[type="number"][aria-invalid="true"],
[type="search"][aria-invalid="true"],
[type="tel"][aria-invalid="true"],
[type="time"][aria-invalid="true"],
[type="url"][aria-invalid="true"],
[type="color"][aria-invalid="true"],
textarea[aria-invalid="true"] {
border-color: var(--color-invalid);
}
textarea {
display: block;
min-height: 50px;
max-width: 100%;
}
textarea[rows] {
height: auto;
}
input:disabled,
input[readonly],
textarea:disabled,
textarea[readonly] {
opacity: 0.7;
cursor: not-allowed;
}
[type="file"],
[type="checkbox"],
[type="radio"] {
margin: 0;
}
[type="file"] {
width: 100%;
}
label {
margin: 0;
}
[type="checkbox"] + label,
[type="radio"] + label {
margin-left: 0.5rem;
}
label > [type="checkbox"],
label > [type="radio"] {
margin-right: 0.5rem;
}
::placeholder {
color: hsl(0 0% 100% / 65%);
}
.form-validation-error {
margin: 0;
margin-top: 0.25em;
color: var(--color-invalid);
font-size: 0.8rem;
}
.error-container {
background-color: hsla(356, 77%, 59%, 0.747);
border-radius: 0.25rem;
padding: 0.5rem 1rem;
}
+76
View File
@@ -0,0 +1,76 @@
/*
* when the user visits this page, this style will apply, when they leave, it
* will get unloaded, so don't worry so much about conflicting styles between
* pages!
*/
body {
background-image: var(--gradient-background);
}
.container {
min-height: inherit;
}
.container,
.content {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.content {
padding-top: 3rem;
padding-bottom: 3rem;
}
h1 {
margin: 0;
text-shadow: 0 3px 0 rgba(0, 0, 0, 0.75);
text-align: center;
line-height: 0.5;
}
h1 span {
display: block;
font-size: 4.5rem;
line-height: 1;
text-transform: uppercase;
text-shadow: 0 0.2em 0.5em rgba(0, 0, 0, 0.5), 0 5px 0
rgba(0, 0, 0, 0.75);
}
nav ul {
list-style: none;
margin: 0;
padding: 0;
display: flex;
gap: 1rem;
font-family: var(--font-display);
font-size: 1.125rem;
line-height: 1;
}
nav ul a:hover {
text-decoration-style: wavy;
text-decoration-thickness: 1px;
}
@media print, (min-width: 640px) {
h1 span {
font-size: 6rem;
}
nav ul {
font-size: 1.25rem;
gap: 1.5rem;
}
}
@media screen and (min-width: 1024px) {
h1 span {
font-size: 8rem;
}
}
+9
View File
@@ -0,0 +1,9 @@
import { PrismaClient } from "@prisma/client";
import { singleton } from "./singleton.server";
// Hard-code a unique key, so we can look up the client when this module gets re-imported
export const db = singleton(
"prisma",
() => new PrismaClient()
);
+10
View File
@@ -0,0 +1,10 @@
export const singleton = <Value>(
name: string,
valueFactory: () => Value
): Value => {
const g = global as any;
g.__singletons ??= {};
g.__singletons[name] ??= valueFactory();
return g.__singletons[name];
};
+11607
View File
File diff suppressed because it is too large Load Diff
+46
View File
@@ -0,0 +1,46 @@
{
"name": "app",
"private": true,
"sideEffects": false,
"type": "module",
"scripts": {
"build": "remix vite:build",
"dev": "remix vite:dev",
"lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
"start": "remix-serve ./build/server/index.js",
"typecheck": "tsc"
},
"dependencies": {
"@prisma/client": "^5.13.0",
"@remix-run/node": "^2.9.1",
"@remix-run/react": "^2.9.1",
"@remix-run/serve": "^2.9.1",
"isbot": "^4.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tsx": "^4.7.3"
},
"devDependencies": {
"@remix-run/dev": "^2.9.1",
"@types/react": "^18.2.20",
"@types/react-dom": "^18.2.7",
"@typescript-eslint/eslint-plugin": "^6.7.4",
"@typescript-eslint/parser": "^6.7.4",
"eslint": "^8.38.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.28.1",
"eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"prisma": "^5.13.0",
"typescript": "^5.4.5",
"vite": "^5.1.0",
"vite-tsconfig-paths": "^4.2.1"
},
"engines": {
"node": ">=18.0.0"
},
"prisma": {
"seed": "tsx prisma/seed.ts"
}
}
+17
View File
@@ -0,0 +1,17 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
model Beer {
id String @id @default(uuid())
name String @unique
abv Float
}
+35
View File
@@ -0,0 +1,35 @@
import { PrismaClient } from "@prisma/client";
const db = new PrismaClient();
async function seed() {
await Promise.all(
getBeers().map((beer) => {
return db.beer.create({ data: beer });
})
);
}
seed();
function getBeers() {
// shout-out to https://icanhazdadjoke.com/
return [
{
name: "Ginger Paradise",
abv: 6.2
},
{
name: "Hertog Jan Pilsener",
abv: 5.1
},
{
name: "Hertog Jan Weizener",
abv: 5.7
},
{
name: "Leffe Blond 0,0%",
abv: 0.0
}
];
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

+32
View File
@@ -0,0 +1,32 @@
{
"include": [
"**/*.ts",
"**/*.tsx",
"**/.server/**/*.ts",
"**/.server/**/*.tsx",
"**/.client/**/*.ts",
"**/.client/**/*.tsx"
],
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"types": ["@remix-run/node", "vite/client"],
"isolatedModules": true,
"esModuleInterop": true,
"jsx": "react-jsx",
"module": "ESNext",
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"target": "ES2022",
"strict": true,
"allowJs": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"baseUrl": ".",
"paths": {
"~/*": ["./app/*"]
},
// Vite takes care of building everything, not tsc.
"noEmit": true
}
}
+10
View File
@@ -0,0 +1,10 @@
import { vitePlugin as remix } from "@remix-run/dev";
import { installGlobals } from "@remix-run/node";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
installGlobals();
export default defineConfig({
plugins: [remix(), tsconfigPaths()],
});