From c5e8f6e2e5998e6f5282f385e9642b1495d30209 Mon Sep 17 00:00:00 2001 From: kennyboy55 Date: Sat, 11 May 2024 11:13:28 +0200 Subject: [PATCH] Add filtering, authentication and much more --- app/auth/session.ts | 56 ++ app/auth/validate.ts | 9 + app/components/cards/beer.tsx | 23 - app/components/cards/drink.card.tsx | 63 ++ app/components/cards/drink.page.tsx | 80 ++ app/components/filters/drink.filter.tsx | 168 ++++ app/components/header.tsx | 60 +- app/components/theme.client.tsx | 94 +++ app/models/drinks.server.ts | 130 +++ app/models/types.tsx | 25 + app/root.tsx | 81 +- app/routes/_index.tsx | 66 +- .../{beers.$beerid.tsx => beer.$beerslug.tsx} | 56 +- app/routes/beers._index.tsx | 37 - app/routes/beers.tsx | 62 -- app/routes/login.tsx | 97 +++ app/routes/logout.tsx | 23 + app/routes/{beers.new.tsx => new/beer.tsx} | 2 +- app/routes/scan.tsx | 65 ++ app/routes/soda.$sodaslug.tsx | 73 ++ app/routes/wine.$wineslug.tsx | 73 ++ app/utils/db.server.ts | 1 + package-lock.json | 33 +- package.json | 4 + prisma/schema.prisma | 148 +++- prisma/seed.ts | 759 +++++++++++++++++- public/.gitignore | 2 + schema.zmodel | 91 ++- 28 files changed, 2078 insertions(+), 303 deletions(-) create mode 100644 app/auth/session.ts create mode 100644 app/auth/validate.ts delete mode 100644 app/components/cards/beer.tsx create mode 100644 app/components/cards/drink.card.tsx create mode 100644 app/components/cards/drink.page.tsx create mode 100644 app/components/filters/drink.filter.tsx create mode 100644 app/components/theme.client.tsx create mode 100644 app/models/drinks.server.ts create mode 100644 app/models/types.tsx rename app/routes/{beers.$beerid.tsx => beer.$beerslug.tsx} (53%) delete mode 100644 app/routes/beers._index.tsx delete mode 100644 app/routes/beers.tsx create mode 100644 app/routes/login.tsx create mode 100644 app/routes/logout.tsx rename app/routes/{beers.new.tsx => new/beer.tsx} (99%) create mode 100644 app/routes/scan.tsx create mode 100644 app/routes/soda.$sodaslug.tsx create mode 100644 app/routes/wine.$wineslug.tsx create mode 100644 public/.gitignore diff --git a/app/auth/session.ts b/app/auth/session.ts new file mode 100644 index 0000000..6b9e853 --- /dev/null +++ b/app/auth/session.ts @@ -0,0 +1,56 @@ +import { createSessionStorage } from "@remix-run/node"; // or cloudflare/deno +import { db } from "~/utils/db.server"; + +function createDatabaseSessionStorage({ + cookie +}) { + // Configure your database client... + return createSessionStorage({ + cookie, + async createData(data, expires) { + var id; + + if(data && data.id){ + id = await db.session.create({data:{data: JSON.stringify({ data }), user:{connect:{id: data.id}}}}); + } + else { + id = await db.session.create({data:{data: JSON.stringify({ data })}}); + } + + return id.id; + }, + async readData(id) { + return (await db.session.findUnique({where:{id: id}, include: {user: true}})) || null; + }, + async updateData(id, data, expires) { + if(data && data.id){ + await db.session.update({data:{data: JSON.stringify({ data }), user:{connect:{id: data.id}}}, where: {id: id}}); + } + else { + await db.session.update({data:{data: JSON.stringify({ data })}, where: {id: id}}); + } + }, + async deleteData(id) { + await db.session.delete({where: {id: id}}); + }, + }); +} + +const { getSession, commitSession, destroySession } = +createDatabaseSessionStorage( + { + // a Cookie from `createCookie` or the CookieOptions to create one + cookie: { + name: "__session", + + httpOnly: true, + maxAge: 60, + path: "/", + sameSite: "lax", + secrets: ["super_secret_cookie_thingy"], + secure: false, + }, + } + ); + +export { getSession, commitSession, destroySession }; diff --git a/app/auth/validate.ts b/app/auth/validate.ts new file mode 100644 index 0000000..3def7ee --- /dev/null +++ b/app/auth/validate.ts @@ -0,0 +1,9 @@ +import { db } from "~/utils/db.server"; + +export async function validateCredentials( + username: string, + password: string +) { + var user = await db.user.findUnique({where:{username: username, password: password}, select:{id: true}}); + return user?.id; +} diff --git a/app/components/cards/beer.tsx b/app/components/cards/beer.tsx deleted file mode 100644 index a3a14a2..0000000 --- a/app/components/cards/beer.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { Link } from '@remix-run/react'; -import { Beer } from '@zenstackhq/runtime/models'; -import { Card } from 'react-bootstrap'; - -interface BeerArgments { - beer: Beer -} - -function BeerCard(options:BeerArgments) { - return ( - - - {options.beer.name} - - {options.beer.description} - - Permalink - - - ); -} - -export default BeerCard; \ No newline at end of file diff --git a/app/components/cards/drink.card.tsx b/app/components/cards/drink.card.tsx new file mode 100644 index 0000000..7daaf9a --- /dev/null +++ b/app/components/cards/drink.card.tsx @@ -0,0 +1,63 @@ + +import { Link } from '@remix-run/react'; +import { Card, ListGroup } from 'react-bootstrap'; +import { DrinkComposite, isDrinkWithContainers, isDrinkWithManufacturer, isDrinkWithStyle } from '~/models/types'; + +interface Arguments { + drink: DrinkComposite +} + +function DrinkCard(arg:Arguments) { + var trimmedDescription = arg.drink.description.length > 120 ? arg.drink.description.substring(0, 120) + "..." : arg.drink.description; + var link = "/drink/"; + + switch (arg.drink.type) { + case 'Beer': + link = "/beer/"; + break; + case 'Wine': + link = "/wine/"; + break; + case 'Soda': + link = "/soda/"; + break; + } + + var borderColor = "#bbb"; + if(isDrinkWithStyle(arg.drink)){ + borderColor = "#" + arg.drink.style.color; + } + + var manufacturer = ""; + if(isDrinkWithManufacturer(arg.drink)){ + manufacturer = arg.drink.manufacturer.name + " (" + arg.drink.manufacturer.country_id + ")"; + } + + var inventory = ""; + if(isDrinkWithContainers(arg.drink)){ + inventory = "Inventory: " + arg.drink.containers.length; + } + + return ( + + {manufacturer} + + + {arg.drink.name} + + {trimmedDescription} + + + + ABV: {arg.drink.abv}% + { isDrinkWithStyle(arg.drink) ? ( + Style: {arg.drink.style.name} + ) : ""} + Allergy: {arg.drink.gluten ? "G" : ""} {arg.drink.lactose ? "L" : ""} {arg.drink.organic ? "B" : ""} + + {inventory} + + ); +} + +export default DrinkCard; \ No newline at end of file diff --git a/app/components/cards/drink.page.tsx b/app/components/cards/drink.page.tsx new file mode 100644 index 0000000..1f979d7 --- /dev/null +++ b/app/components/cards/drink.page.tsx @@ -0,0 +1,80 @@ + +import { Link } from '@remix-run/react'; +import { Card, Col, Container, Image, ListGroup, Row } from 'react-bootstrap'; +import { LinkContainer } from 'react-router-bootstrap'; +import { DrinkComposite, isDrinkWithContainers, isDrinkWithManufacturer, isDrinkWithStyle } from '~/models/types'; + +interface Arguments { + drink: DrinkComposite +} + +function DrinkPage(arg:Arguments) { + var trimmedDescription = arg.drink.description.length > 120 ? arg.drink.description.substring(0, 120) + "..." : arg.drink.description; + var link = "/drink/"; + + switch (arg.drink.type) { + case 'Beer': + link = "/beer/"; + break; + case 'Wine': + link = "/wine/"; + break; + case 'Soda': + link = "/soda/"; + break; + } + + var borderColor = "#bbb"; + if(isDrinkWithStyle(arg.drink)){ + borderColor = "#" + arg.drink.style.color; + } + + var manufacturer = ""; + if(isDrinkWithManufacturer(arg.drink)){ + manufacturer = arg.drink.manufacturer.name + " (" + arg.drink.manufacturer.country_id + ")"; + } + + var inventory = ""; + if(isDrinkWithContainers(arg.drink)){ + inventory = "Inventory: " + arg.drink.containers.length; + } + + return ( + +
+ + + +

{arg.drink.name}

+

{arg.drink.description}

+ + + + +
+ +
+
+ + + +
+

Inventory

+ {arg.drink.containers.map((container) => ( +

{container.section.name} | {container.type} | {container.inventory}

+ ))} +
+ + + +
+

{arg.drink.manufacturer.name}

+

{arg.drink.manufacturer.description}

+
+ +
+
+ ); +} + +export default DrinkPage; \ No newline at end of file diff --git a/app/components/filters/drink.filter.tsx b/app/components/filters/drink.filter.tsx new file mode 100644 index 0000000..fcb7d20 --- /dev/null +++ b/app/components/filters/drink.filter.tsx @@ -0,0 +1,168 @@ + +import { useSubmit } from '@remix-run/react'; +import { BeerStyle, Manufacturer, WineStyle } from '@zenstackhq/runtime/models'; +import { useEffect, useRef, useState } from 'react'; +import { Button, Form, Col, Row } from 'react-bootstrap'; +import ReactSlider from 'react-slider' + +interface Arguments { + beerStyles: BeerStyle[]; + wineStyles: WineStyle[]; + manufacturers: Manufacturer[]; + searchParams: URLSearchParams +} + +function DrinkFilter(arg : Arguments) { + + const submit = useSubmit(); + const [abv, setAbv] = useState({min: Number(arg.searchParams.get("min-abv") || 0), max: Number(arg.searchParams.get("max-abv") || 50)}) + + const showBeerFilter = arg.searchParams.has("drink", "Beer"); + const showWineFilter = arg.searchParams.has("drink", "Wine"); + const showSodaFilter = arg.searchParams.has("drink", "Soda"); + const showCocktailFilter = arg.searchParams.has("drink", "Cocktail"); + + const form = useRef(null); + + const submitAfterRender = function(){ + setTimeout(() => submit(form.current), 10); + } + + const slider = () => + (
+
{state.valueNow}%
} + pearling + minDistance={2} + onAfterChange={(number, index) => {setAbv({min: number[0], max: number[1]}); submitAfterRender()}} + /> + + + +
); + + return ( +
{submit(event.currentTarget); }}> + + + + Drink type + { ["Beer", "Wine", "Soda", "Cocktail"].map(key => ( + + ))} + + + + Drink + { ["Gluten-free", "Lactose-free", "Organic"].map(key => ( + + ))} + ABV + {slider()} + + + Manufacturer + { arg.manufacturers.map(manufacturer => ( + + ))} + + + + { showBeerFilter ? ( + + Beer style + { arg.beerStyles.map(style => ( + + ))} + + ) : ""} + { showWineFilter ? ( + + Wine style + { arg.wineStyles.map(style => ( + + ))} + + ) : ""} + { showSodaFilter ? ( + + Soda + + + ) : ""} + { showCocktailFilter ? ( + + Cocktail + + + ) : ""} + + No Inventory + + + + +
+ ); +} + +export default DrinkFilter; \ No newline at end of file diff --git a/app/components/header.tsx b/app/components/header.tsx index 645df7b..11950ca 100644 --- a/app/components/header.tsx +++ b/app/components/header.tsx @@ -1,36 +1,50 @@ import {LinkContainer} from 'react-router-bootstrap' import {Container, Nav, Navbar, NavDropdown} from 'react-bootstrap'; +import { User } from '@zenstackhq/runtime/models'; +import { Link } from '@remix-run/react'; + +export type HeaderData = {user?: (User)} & {loggedin: boolean}; + +type Arguments = {data: HeaderData}; + +function Header(data: Arguments) { + var loggedin = data.data.loggedin; + + var username = ""; + if(loggedin){ + username = data.data.user!.username; + } -function Header() { return ( - + K-FRIDGE - - + + - + { data.data.loggedin ? ( + + ) : ( + + )} diff --git a/app/components/theme.client.tsx b/app/components/theme.client.tsx new file mode 100644 index 0000000..364d8aa --- /dev/null +++ b/app/components/theme.client.tsx @@ -0,0 +1,94 @@ + +export default function ClientThemeComponent(){ + + const getPreferredTheme = () => { + return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' + } + + const setTheme = (theme:string) => { + if (theme === 'auto') { + document.documentElement.setAttribute('data-bs-theme', (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')) + } else { + document.documentElement.setAttribute('data-bs-theme', theme) + } + } + + setTheme(getPreferredTheme()) + + const showActiveTheme = (theme:string, focus = false) => { + const themeSwitcher = document.querySelector('#bd-theme') + + if (!themeSwitcher) { + return + } + + const themeSwitcherText = document.querySelector('#bd-theme-text') + const activeThemeIcon = document.querySelector('.theme-icon-active use') + const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`) + const svgOfActiveBtn = btnToActive!.querySelector('svg use')!.getAttribute('href') + + document.querySelectorAll('[data-bs-theme-value]').forEach(element => { + element.classList.remove('active') + element.setAttribute('aria-pressed', 'false') + }) + + btnToActive!.classList.add('active') + btnToActive!.setAttribute('aria-pressed', 'true') + activeThemeIcon!.setAttribute('href', svgOfActiveBtn!) + const themeSwitcherLabel = `${themeSwitcherText!.textContent} (${btnToActive!.dataset!.bsThemeValue})` + themeSwitcher.setAttribute('aria-label', themeSwitcherLabel) + + if (focus) { + themeSwitcher.focus() + } + } + + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { + setTheme(getPreferredTheme()) + }) + + window.addEventListener('DOMContentLoaded', () => { + showActiveTheme(getPreferredTheme()) + + document.querySelectorAll('[data-bs-theme-value]') + .forEach(toggle => { + toggle.addEventListener('click', () => { + const theme = toggle.getAttribute('data-bs-theme-value') || "light"; + setTheme(theme) + showActiveTheme(theme, true) + }) + }) + }); + + return ( +
+ +
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+
+ ); +} \ No newline at end of file diff --git a/app/models/drinks.server.ts b/app/models/drinks.server.ts new file mode 100644 index 0000000..38a168a --- /dev/null +++ b/app/models/drinks.server.ts @@ -0,0 +1,130 @@ + +import { enhance } from "@zenstackhq/runtime"; +import { db } from "~/utils/db.server"; +import { DrinkComposite } from "./types"; +import lodash from "lodash" +import { DrinkType, Manufacturer } from "@prisma/client"; + +export async function findIdFromSlug(slug:string | undefined) : Promise { + if(slug){ + const drink = await db.drink.findUnique({where: {slug: slug}, select: {id: true}}); + return drink?.id; + } + return undefined; +} + +export async function findManufacturersFromSearch(searchParams: URLSearchParams) : Promise { + const dbe = enhance(db); + + var result : Manufacturer[] = []; + + if(searchParams.has("drink")){ + result = result.concat(await dbe.manufacturer.findMany({ + where: {drinks: { some: { type: { in: searchParams.getAll("drink").map((str) => {return DrinkType[str as keyof typeof DrinkType]})}}}} + })); + } + else { + result = result.concat(await dbe.manufacturer.findMany()); + } + + return result; +} +export async function findDrinksFromSearch(searchParams: URLSearchParams) : Promise { + const dbe = enhance(db); + + var result : DrinkComposite[] = []; + var noDrinkSelected = searchParams.getAll("drink").length == 0; + + let whereBase = {delegate_aux_drink: {AND: []}}; + + if(searchParams.has("gluten-free")){ + whereBase.delegate_aux_drink.AND.push({NOT: {gluten: true}}); + } + if(searchParams.has("lactose-free")){ + whereBase.delegate_aux_drink.AND.push({NOT: {lactose: true}}); + } + if(searchParams.has("organic")){ + whereBase.delegate_aux_drink.AND.push({organic: true}); + } + + if(searchParams.has("min-abv")){ + let minabv = Number(searchParams.get("min-abv")); + whereBase.delegate_aux_drink.AND.push({abv: {gte: minabv}}); + } + if(searchParams.has("max-abv")){ + let maxabv = Number(searchParams.get("max-abv")); + whereBase.delegate_aux_drink.AND.push({abv: {lte: maxabv}}); + } + + if(searchParams.has("manufacturer")){ + let whereManufacturer = {manufacturer: {id: {in: searchParams.getAll("manufacturer").map(Number)}}}; + whereBase = lodash.merge(whereBase, whereManufacturer); + } + + if(!searchParams.has("inventory")){ + let whereInventory = {containers: {some: {inventory: {gt: 0}}}}; + whereBase = lodash.merge(whereBase, whereInventory); + } + + // Beers + if(searchParams.has("drink", "Beer") || noDrinkSelected){ + + let where = Object.assign({}, whereBase, {style: {}}); + + // Beer styles + if(searchParams.has("beerstyle")){ + where.style = {id: {in: searchParams.getAll("beerstyle").map(Number)}}; + } + + result = result.concat(await dbe.beer.findMany({ + include: {style: true, manufacturer: true, containers: {include: {section: true}}}, + where: where + })); + } + + if(searchParams.has("drink", "Wine") || noDrinkSelected){ + + let where = Object.assign({}, whereBase, {style: {}}); + + // Beer styles + if(searchParams.has("winestyle")){ + where.style = {id: {in: searchParams.getAll("winestyle").map(Number)}}; + } + + result = result.concat(await dbe.wine.findMany({ + include: {style: true, manufacturer: true, containers: {include: {section: true}}}, + where: where + })); + } + + if(searchParams.has("drink", "Soda") || noDrinkSelected){ + + let where = whereBase; + + // Carbonated + if(searchParams.has("carbonated")){ + where = Object.assign({}, whereBase, {carbonated: true}); + } + + result = result.concat(await dbe.soda.findMany({ + include: {manufacturer: true, containers: {include: {section: true}}}, + where: where + })); + } + + if(searchParams.has("drink", "Cocktail") || noDrinkSelected){ + let where = whereBase; + + // Carbonated + if(searchParams.has("mix")){ + where = Object.assign({}, whereBase, {mix: true}); + } + + result = result.concat(await dbe.cocktail.findMany({ + include: {manufacturer: true, containers: {include: {section: true}}}, + where: where + })); + } + + return result; +} \ No newline at end of file diff --git a/app/models/types.tsx b/app/models/types.tsx new file mode 100644 index 0000000..98a8390 --- /dev/null +++ b/app/models/types.tsx @@ -0,0 +1,25 @@ +import { Drink, Beer, BeerStyle, Wine, WineStyle, Soda, Manufacturer, Container, Cocktail, Section } from '@zenstackhq/runtime/models'; + +export type ContainerWithSection = Container & { section: Section} +export type DrinkWithContainers = (Drink & { containers: ContainerWithSection[]}) | (Drink & { containers: Container[]}) +export type DrinkWithManufacturer = Drink & { manufacturer: Manufacturer} + +export type BeerWithStyle = Beer & { style : BeerStyle } +export type WineWithStyle = Wine & { style : WineStyle } + +export type DrinkComposite = Drink | BeerWithStyle | WineWithStyle | Soda | Cocktail | DrinkWithManufacturer | DrinkWithContainers + +export function isDrinkWithContainers(drink:DrinkComposite) : drink is DrinkWithContainers{ + return ((drink as { containers: Container[]}).containers != undefined) || ((drink as { containers: ContainerWithSection[]}).containers != undefined); +} + +export function isDrinkWithManufacturer(drink:DrinkComposite) : drink is DrinkWithManufacturer{ + return (drink as { manufacturer: Manufacturer}).manufacturer != undefined; +} + +export function isDrinkWithStyle(drink:DrinkComposite) : drink is WineWithStyle | BeerWithStyle{ + const beerstyle = (drink as { style: BeerStyle}).style != undefined; + const winestyle = (drink as { style: WineStyle}).style != undefined; + + return beerstyle || winestyle; +} \ No newline at end of file diff --git a/app/root.tsx b/app/root.tsx index 44a7fb2..54d19dc 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -1,10 +1,13 @@ -import type { LinksFunction } from "@remix-run/node"; +import type { LinksFunction, LoaderFunctionArgs } from "@remix-run/node"; import { Links, Scripts, Outlet, useRouteError, - isRouteErrorResponse + isRouteErrorResponse, + json, + redirect, + useLoaderData } from "@remix-run/react"; import type { PropsWithChildren } from "react"; @@ -14,14 +17,36 @@ export const links: LinksFunction = () => [ { rel: "stylesheet", href: stylesheet }, ]; -import Header from "~/components/header"; +import Header, { HeaderData } from "~/components/header"; +import { Col, Container, Row } from "react-bootstrap"; +import { getSession } from "./auth/session"; +import { head } from "lodash"; +import { User } from "@zenstackhq/runtime/models"; + +export async function loader({ + request, +}: LoaderFunctionArgs) { + const session = await getSession( + request.headers.get("Cookie") + ); + + let data : HeaderData = {loggedin: false}; + + if(session.has("user_id")){ + data.user = session.get("user"); + data.loggedin = true; + } + + return json({data}); +} function Document({ children, + headerData, title = "K-FRIDGE", -}: PropsWithChildren<{ title?: string }>) { +}: PropsWithChildren<{ title?: string, headerData: HeaderData }>) { return ( - + -
- {children} +
+
+
+
+ {children} +
@@ -41,8 +70,10 @@ function Document({ } export default function App() { + const data = useLoaderData(); + return ( - + ); @@ -50,17 +81,23 @@ export default function App() { export function ErrorBoundary() { const error = useRouteError(); + const data = useLoaderData(); if (isRouteErrorResponse(error)) { return ( - -
-

- {error.status} {error.statusText} -

-
+ + + +
+

{error.status}

+ {error.statusText} +
+ +
+
); } @@ -70,11 +107,17 @@ export function ErrorBoundary() { ? error.message : "Unknown error"; return ( - -
-

App Error

-
{errorMessage}
-
+ + + + +
+

App Error

+ {errorMessage} +
+ +
+
); } \ No newline at end of file diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx index d5de973..7aab43a 100644 --- a/app/routes/_index.tsx +++ b/app/routes/_index.tsx @@ -1,17 +1,55 @@ -export default function Index() { +import { LoaderFunctionArgs, json } from "@remix-run/node"; +import { useLoaderData, useSearchParams } from "@remix-run/react"; +import { enhance } from "@zenstackhq/runtime"; +import { Col, Container, Row } from "react-bootstrap"; +import DrinkCard from "~/components/cards/drink.card"; +import DrinkFilter from "~/components/filters/drink.filter"; +import { findDrinksFromSearch, findManufacturersFromSearch } from "~/models/drinks.server"; + +import { db } from "~/utils/db.server"; + +export const loader = async ({request} : LoaderFunctionArgs) => { + const dbe = enhance(db); + + const url = new URL(request.url); + + const beerStyles = await dbe.beerStyle.findMany(); + const wineStyles = await dbe.wineStyle.findMany(); + const manufacturers = await findManufacturersFromSearch(url.searchParams); + + const drinkResults = await findDrinksFromSearch(url.searchParams); + + return json({beerStyles, wineStyles, manufacturers, drinkResults}); +}; + +export default function BeersRoute() { + const loadData = useLoaderData(); + + const [searchParams, setSearchParams] = useSearchParams(); + return ( -
-

Welcome to Kenneths Beer Inventory Server

- -
+ + + + + + + + + {loadData.drinkResults.map((drink) => ( + + + + ))} + + + + ); } + + diff --git a/app/routes/beers.$beerid.tsx b/app/routes/beer.$beerslug.tsx similarity index 53% rename from app/routes/beers.$beerid.tsx rename to app/routes/beer.$beerslug.tsx index 8237026..0c7c944 100644 --- a/app/routes/beers.$beerid.tsx +++ b/app/routes/beer.$beerslug.tsx @@ -1,51 +1,30 @@ import type { - LoaderFunctionArgs, - ActionFunctionArgs + LoaderFunctionArgs } from "@remix-run/node"; -import { json, redirect } from "@remix-run/node"; +import { json } from "@remix-run/node"; import { useLoaderData, useParams, isRouteErrorResponse, useRouteError } from "@remix-run/react"; - -import BeerCard from "~/components/cards/beer"; + +import DrinkPage from "~/components/cards/drink.page"; import { db } from "~/utils/db.server"; import { enhance } from "@zenstackhq/runtime"; - -export const action = async ({ - params, - request, - }: ActionFunctionArgs) => { - const dbe = enhance(db); - const form = await request.formData(); - if (form.get("intent") !== "delete") { - throw new Response( - `The intent ${form.get("intent")} is not supported`, - { status: 400 } - ); - } - const beerid = parseInt(params.beerid!); - const beer = await dbe.beer.findUnique({ - where: { id: beerid }, - }); - if (!beer) { - throw new Response("Can't delete what does not exist", { - status: 404, - }); - } - await dbe.beer.delete({ where: { id: beerid } }); - return redirect("/beers"); - }; +import { findIdFromSlug } from "~/models/drinks.server"; +import { Container, Row, Col } from "react-bootstrap"; export const loader = async ({ params, }: LoaderFunctionArgs) => { const dbe = enhance(db); - const beerid = parseInt(params.beerid!); + + const beerid = await findIdFromSlug(params.beerslug); + const beer = await dbe.beer.findUnique({ where: { id: beerid }, + include: { style: true, manufacturer: true, containers: { include: { section: true}}} }); if (!beer) { throw new Response("Beer not found.", { @@ -59,20 +38,7 @@ export default function BeerRoute() { const data = useLoaderData(); return ( -
-

You selected this beer:

- -
- -
-
+ ); } diff --git a/app/routes/beers._index.tsx b/app/routes/beers._index.tsx deleted file mode 100644 index b475caa..0000000 --- a/app/routes/beers._index.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { json } from "@remix-run/node"; -import { Link, useLoaderData } from "@remix-run/react"; -import { enhance } from "@zenstackhq/runtime"; - -import BeerCard from "~/components/cards/beer"; - -import { db } from "~/utils/db.server"; - -export const loader = async () => { - const dbe = enhance(db); - const count = await dbe.beer.count(); - const randomRowNumber = Math.floor(Math.random() * count); - const [randomBeer] = await dbe.beer.findMany({ - skip: randomRowNumber, - take: 1, - }); - return json({ randomBeer }); -}; - -export default function BeersIndexRoute() { - const data = useLoaderData(); - - return ( -
-

Here's a random Beer:

- -
- ); -} - -export function ErrorBoundary() { - return ( -
- Something unexpected went wrong. Sorry about that. -
- ); - } \ No newline at end of file diff --git a/app/routes/beers.tsx b/app/routes/beers.tsx deleted file mode 100644 index 3cb5058..0000000 --- a/app/routes/beers.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { json } from "@remix-run/node"; -import { - Link, - Outlet, - useLoaderData, -} from "@remix-run/react"; -import { enhance } from "@zenstackhq/runtime"; -import BeerCard from "~/components/cards/beer"; - -import { db } from "~/utils/db.server"; - -export const loader = async () => { - const dbe = enhance(db); - return json({ - beerListItems: await dbe.beer.findMany({ - orderBy: { id: "asc" }, - take: 5, - }), - }); -}; - -export default function BeersRoute() { - const data = useLoaderData(); - - return ( -
-
-
-

- - B - Beers - -

-
-
-
-
-
- Get a random beer -

Here are a few more beers to check out:

- {data.beerListItems.map((beer) => ( - - ))} -
- - Add your own - -
-
-
- -
-
-
-
- ); -} diff --git a/app/routes/login.tsx b/app/routes/login.tsx new file mode 100644 index 0000000..ffda2cf --- /dev/null +++ b/app/routes/login.tsx @@ -0,0 +1,97 @@ +import type { + ActionFunctionArgs, + LoaderFunctionArgs, + } from "@remix-run/node"; // or cloudflare/deno + import { json, redirect } from "@remix-run/node"; // or cloudflare/deno + import { useLoaderData } from "@remix-run/react"; +import { Button, Col, Container, Form, InputGroup, Row } from "react-bootstrap"; + + import { getSession, commitSession } from "~/auth/session"; +import { validateCredentials } from "~/auth/validate"; + + export async function loader({ + request, + }: LoaderFunctionArgs) { + const session = await getSession( + request.headers.get("Cookie") + ); + + if (session.has("id")) { + // Redirect to the home page if they are already signed in. + return redirect("/"); + } + + const data = { error: session.get("error") }; + + return json(data); + } + + export async function action({ + request, + }: ActionFunctionArgs) { + const session = await getSession( + request.headers.get("Cookie") + ); + const form = await request.formData(); + const username = form.get("username"); + const password = form.get("password"); + + const id = await validateCredentials( + username?.toString() || "", + password?.toString() || "" + ); + + if (id == null) { + session.flash("error", "Invalid username/password"); + + // Redirect back to the login page with errors. + return redirect("/login"); + } + + session.set("id", id); + + // Login succeeded, send them to the home page. + return redirect("/", { + headers: { + "Set-Cookie": await commitSession(session), + }, + }); + } + + export default function Login() { + const { error } = useLoaderData(); + + return ( + + + {error ?
{error}
: null} +
+ + + + + + + + + + + +
+
+
+ ); + } + \ No newline at end of file diff --git a/app/routes/logout.tsx b/app/routes/logout.tsx new file mode 100644 index 0000000..0cf6b90 --- /dev/null +++ b/app/routes/logout.tsx @@ -0,0 +1,23 @@ +import type { + LoaderFunctionArgs, + } from "@remix-run/node"; + import { redirect } from "@remix-run/node"; + + import { getSession, destroySession } from "~/auth/session"; + + export async function loader({ + request, + }: LoaderFunctionArgs) { + const session = await getSession( + request.headers.get("Cookie") + ); + + if (session.has("id")) { + // Logout + await destroySession(session); + return redirect("/login"); + } + + return redirect("/"); + } + \ No newline at end of file diff --git a/app/routes/beers.new.tsx b/app/routes/new/beer.tsx similarity index 99% rename from app/routes/beers.new.tsx rename to app/routes/new/beer.tsx index 5efe5da..5df3e53 100644 --- a/app/routes/beers.new.tsx +++ b/app/routes/new/beer.tsx @@ -57,7 +57,7 @@ export const action = async ({ } const beer = await dbe.beer.create({ data: fields }); - return redirect(`/beers/${beer.id}`); + return redirect(`/beer/${beer.id}`); }; export default function NewBeerRoute() { diff --git a/app/routes/scan.tsx b/app/routes/scan.tsx new file mode 100644 index 0000000..518c68d --- /dev/null +++ b/app/routes/scan.tsx @@ -0,0 +1,65 @@ +import type { ActionFunctionArgs } from "@remix-run/node"; +import { redirect } from "@remix-run/node"; +import { useActionData } from "@remix-run/react"; +import { enhance } from "@zenstackhq/runtime"; +import { Col, Container, Form, Row } from "react-bootstrap"; + +import { db } from "~/utils/db.server"; +import { badRequest } from "~/utils/request.server"; + +export const action = async ({ + request, +}: ActionFunctionArgs) => { + const dbe = enhance(db); + + const form = await request.formData(); + const barcode = form.get("barcode"); + // we do this type check to be extra sure and to make TypeScript happy + // we'll explore validation next! + if ( + typeof barcode !== "string" + ) { + return badRequest({ + fieldErrors: null, + fields: null, + formError: "Form not submitted correctly.", + }); + } + + const drink = await dbe.drink.findFirst({ + select: {slug: true}, + where: {containers: {some: {barcode: barcode}}} + }); + if(drink) return redirect(`/beer/${drink.slug}`); +}; + +export default function ScanRoute() { + return ( + + + +
+
+ +
+
+ +
+
+ ); + } + + export function ErrorBoundary() { + return ( +
+ Something unexpected went wrong. Sorry about that. +
+ ); + } \ No newline at end of file diff --git a/app/routes/soda.$sodaslug.tsx b/app/routes/soda.$sodaslug.tsx new file mode 100644 index 0000000..c9e4f10 --- /dev/null +++ b/app/routes/soda.$sodaslug.tsx @@ -0,0 +1,73 @@ +import type { + LoaderFunctionArgs +} from "@remix-run/node"; +import { json } from "@remix-run/node"; +import { + useLoaderData, + useParams, + isRouteErrorResponse, + useRouteError } from "@remix-run/react"; + +import DrinkCard from "~/components/cards/drink.card"; + +import { db } from "~/utils/db.server"; +import { enhance } from "@zenstackhq/runtime"; +import { findIdFromSlug } from "~/models/drinks.server"; + +export const loader = async ({ + params, +}: LoaderFunctionArgs) => { + const dbe = enhance(db); + + const sodaid = await findIdFromSlug(params.sodaslug); + + const soda = await dbe.soda.findUnique({ + include: { manufacturer: true, containers: true}, + where: { id: sodaid } + }); + if (!soda) { + throw new Response("Soda not found.", { + status: 404, + }); + } + return json({ soda }); +}; + +export default function SodaRoute() { + const data = useLoaderData(); + + return ( +
+ +
+ ); +} + +export function ErrorBoundary() { + const { sodaid } = useParams(); + const error = useRouteError(); + + if (isRouteErrorResponse(error)) { + if (error.status === 400) { + return ( +
+ What you're trying to do is not allowed. +
+ ); + } + if (error.status === 404) { + return ( +
+ Huh? What the heck is "{sodaid}"? +
+ ); + } + } + + return ( +
+ There was an error loading soda by the id "{sodaid}". + Sorry. +
+ ); + } \ No newline at end of file diff --git a/app/routes/wine.$wineslug.tsx b/app/routes/wine.$wineslug.tsx new file mode 100644 index 0000000..e3dcfd2 --- /dev/null +++ b/app/routes/wine.$wineslug.tsx @@ -0,0 +1,73 @@ +import type { + LoaderFunctionArgs +} from "@remix-run/node"; +import { json } from "@remix-run/node"; +import { + useLoaderData, + useParams, + isRouteErrorResponse, + useRouteError } from "@remix-run/react"; + +import DrinkCard from "~/components/cards/drink.card"; + +import { db } from "~/utils/db.server"; +import { enhance } from "@zenstackhq/runtime"; +import { findIdFromSlug } from "~/models/drinks.server"; + +export const loader = async ({ + params, +}: LoaderFunctionArgs) => { + const dbe = enhance(db); + + const wineid = await findIdFromSlug(params.wineslug); + + const wine = await dbe.wine.findUnique({ + include: { style: true, manufacturer: true, containers: true}, + where: { id: wineid }, + }); + if (!wine) { + throw new Response("Wine not found.", { + status: 404, + }); + } + return json({ wine }); +}; + +export default function WineRoute() { + const data = useLoaderData(); + + return ( +
+ +
+ ); +} + +export function ErrorBoundary() { + const { wineid } = useParams(); + const error = useRouteError(); + + if (isRouteErrorResponse(error)) { + if (error.status === 400) { + return ( +
+ What you're trying to do is not allowed. +
+ ); + } + if (error.status === 404) { + return ( +
+ Huh? What the heck is "{wineid}"? +
+ ); + } + } + + return ( +
+ There was an error loading wine by the id "{wineid}". + Sorry. +
+ ); + } \ No newline at end of file diff --git a/app/utils/db.server.ts b/app/utils/db.server.ts index 26d7179..8736437 100644 --- a/app/utils/db.server.ts +++ b/app/utils/db.server.ts @@ -7,3 +7,4 @@ export const db = singleton( "prisma", () => new PrismaClient() ); + diff --git a/package-lock.json b/package-lock.json index 07f27e0..ffc3d36 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,18 +13,22 @@ "@zenstackhq/runtime": "2.0.1", "bootstrap": "^5.3.3", "isbot": "^4.1.0", + "lodash": "^4.17.21", "react": "^18.2.0", "react-bootstrap": "^2.10.2", "react-dom": "^18.2.0", "react-router-bootstrap": "^0.26.2", + "react-slider": "^2.0.6", "tsx": "^4.7.3" }, "devDependencies": { "@remix-run/dev": "^2.9.1", "@tailwindcss/forms": "^0.5.7", + "@types/lodash": "^4.17.1", "@types/react": "^18.2.20", "@types/react-dom": "^18.2.7", "@types/react-router-bootstrap": "^0.26.6", + "@types/react-slider": "^1.3.6", "@typescript-eslint/eslint-plugin": "^6.7.4", "@typescript-eslint/parser": "^6.7.4", "eslint": "^8.38.0", @@ -2381,6 +2385,12 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/lodash": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.1.tgz", + "integrity": "sha512-X+2qazGS3jxLAIz5JDXDzglAF3KpijdhFxlf/V1+hEsOUc+HnWi81L/uv/EvGuV90WY+7mPGFCUDGfQC3Gj95Q==", + "dev": true + }, "node_modules/@types/mdast": { "version": "3.0.15", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", @@ -2443,6 +2453,15 @@ "@types/react": "*" } }, + "node_modules/@types/react-slider": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@types/react-slider/-/react-slider-1.3.6.tgz", + "integrity": "sha512-RS8XN5O159YQ6tu3tGZIQz1/9StMLTg/FCIPxwqh2gwVixJnlfIodtVx+fpXVMZHe7A58lAX1Q4XTgAGOQaCQg==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/react-transition-group": { "version": "4.4.10", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", @@ -7255,8 +7274,7 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "node_modules/lodash.camelcase": { "version": "4.3.0", @@ -9913,6 +9931,17 @@ "react-dom": ">=16.8" } }, + "node_modules/react-slider": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/react-slider/-/react-slider-2.0.6.tgz", + "integrity": "sha512-gJxG1HwmuMTJ+oWIRCmVWvgwotNCbByTwRkFZC6U4MBsHqJBmxwbYRJUmxy4Tke1ef8r9jfXjgkmY/uHOCEvbA==", + "dependencies": { + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": "^16 || ^17 || ^18" + } + }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", diff --git a/package.json b/package.json index a8bbfdd..0ad79e4 100644 --- a/package.json +++ b/package.json @@ -18,18 +18,22 @@ "@zenstackhq/runtime": "2.0.1", "bootstrap": "^5.3.3", "isbot": "^4.1.0", + "lodash": "^4.17.21", "react": "^18.2.0", "react-bootstrap": "^2.10.2", "react-dom": "^18.2.0", "react-router-bootstrap": "^0.26.2", + "react-slider": "^2.0.6", "tsx": "^4.7.3" }, "devDependencies": { "@remix-run/dev": "^2.9.1", "@tailwindcss/forms": "^0.5.7", + "@types/lodash": "^4.17.1", "@types/react": "^18.2.20", "@types/react-dom": "^18.2.7", "@types/react-router-bootstrap": "^0.26.6", + "@types/react-slider": "^1.3.6", "@typescript-eslint/eslint-plugin": "^6.7.4", "@typescript-eslint/parser": "^6.7.4", "eslint": "^8.38.0", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 4cfa146..46d8fa9 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -12,11 +12,18 @@ generator client { provider = "prisma-client-js" } +enum UserType { + User + Scanner + Admin +} + enum DrinkType { Drink Beer Wine Soda + Cocktail } enum ContainerType { @@ -28,25 +35,33 @@ enum ContainerType { } /// @@delegate(type) -/// @@allow('all', true) +/// @@allow('read', true) +/// @@allow('all', auth().type == Admin) model Drink { - id Int @id() @default(autoincrement()) - slug String @unique() - manufacturer_id Int - manufacturer Manufacturer @relation(fields: [manufacturer_id], references: [id]) - type DrinkType - name String @unique() - description String - abv Float - image String? - containers Container[] - delegate_aux_beer Beer? - delegate_aux_wine Wine? - delegate_aux_soda Soda? + id Int @id() @default(autoincrement()) + slug String @unique() + manufacturer_id Int + manufacturer Manufacturer @relation(fields: [manufacturer_id], references: [id]) + type DrinkType + name String @unique() + description String + abv Float + image String? + link String? + gluten Boolean + lactose Boolean + organic Boolean + containers Container[] + delegate_aux_beer Beer? + delegate_aux_wine Wine? + delegate_aux_soda Soda? + delegate_aux_cocktail Cocktail? } -/// @@allow('all', true) -/// @@allow('all', true) +/// @@allow('read', true) +/// @@allow('all', auth().type == Admin) +/// @@allow('read', true) +/// @@allow('all', auth().type == Admin) model Beer { id Int @id() style_id Int @@ -55,15 +70,19 @@ model Beer { delegate_aux_drink Drink @relation(fields: [id], references: [id], onDelete: Cascade, onUpdate: Cascade) } -/// @@allow('all', true) +/// @@allow('read', true) +/// @@allow('all', auth().type == Admin) model BeerStyle { id Int @id() @default(autoincrement()) name String @unique() + color String beers Beer[] } -/// @@allow('all', true) -/// @@allow('all', true) +/// @@allow('read', true) +/// @@allow('all', auth().type == Admin) +/// @@allow('read', true) +/// @@allow('all', auth().type == Admin) model Wine { id Int @id() style_id Int @@ -76,45 +95,98 @@ model Wine { delegate_aux_drink Drink @relation(fields: [id], references: [id], onDelete: Cascade, onUpdate: Cascade) } -/// @@allow('all', true) +/// @@allow('read', true) +/// @@allow('all', auth().type == Admin) model WineStyle { id Int @id() @default(autoincrement()) name String @unique() + color String wines Wine[] } -/// @@allow('all', true) -/// @@allow('all', true) +/// @@allow('read', true) +/// @@allow('all', auth().type == Admin) +/// @@allow('read', true) +/// @@allow('all', auth().type == Admin) model Soda { id Int @id() carbonated Boolean delegate_aux_drink Drink @relation(fields: [id], references: [id], onDelete: Cascade, onUpdate: Cascade) } -/// @@allow('all', true) +/// @@allow('read', true) +/// @@allow('all', auth().type == Admin) +/// @@allow('read', true) +/// @@allow('all', auth().type == Admin) +model Cocktail { + id Int @id() + mix Boolean + delegate_aux_drink Drink @relation(fields: [id], references: [id], onDelete: Cascade, onUpdate: Cascade) +} + +/// @@allow('read', true) +/// @@allow('update', auth().type == Scanner) +/// @@allow('all', auth().type == Admin) model Container { - barcode String @id() - drink_id Int - drink Drink @relation(fields: [drink_id], references: [id]) - type ContainerType - volume Int - portions Int? - inventory Int @default(0) + id Int @id() @default(autoincrement()) + barcode String? @unique() + drink_id Int + drink Drink @relation(fields: [drink_id], references: [id]) + section_id Int + section Section @relation(fields: [section_id], references: [id]) + type ContainerType + volume Int + portions Int? + price Float @default(0.0) + inventory Int @default(0) } -/// @@allow('all', true) +/// @@allow('read', true) +/// @@allow('all', auth().type == Admin) +model Section { + id Int @id() @default(autoincrement()) + name String @unique() + containers Container[] +} + +/// @@allow('read', true) +/// @@allow('all', auth().type == Admin) model Manufacturer { - id Int @id() @default(autoincrement()) - country_id String - country Country @relation(fields: [country_id], references: [code]) - name String @unique() - image String? - drinks Drink[] + id Int @id() @default(autoincrement()) + country_id String + country Country @relation(fields: [country_id], references: [code]) + name String @unique() + description String? + image String? + drinks Drink[] } -/// @@allow('all', true) +/// @@allow('read', true) +/// @@allow('all', auth().type == Admin) model Country { code String @id() name String manufacturers Manufacturer[] } + +/// @@allow('all', auth() == this) +/// @@allow('all', auth().type == Admin) +model User { + id Int @id() @default(autoincrement()) + username String @unique() + /// @password + /// @omit + password String + type UserType + sessions Session[] +} + +/// @@allow('all', auth().id == user.id) +/// @@allow('all', auth().type == Admin) +model Session { + id String @id() @default(uuid()) + createdAt DateTime @default(now()) + user_id Int? + user User? @relation(fields: [user_id], references: [id]) + data String +} diff --git a/prisma/seed.ts b/prisma/seed.ts index 6529b8c..1bba0e2 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -1,17 +1,32 @@ -import { ContainerType, PrismaClient } from "@prisma/client"; -import { enhance } from "@zenstackhq/runtime"; -import { BeerStyle, Country, Manufacturer } from "@zenstackhq/runtime/models"; +import { ContainerType, PrismaClient, UserType } from "@prisma/client"; +import { EnhancementContext, enhance } from "@zenstackhq/runtime"; +import { BeerStyle, Country, Manufacturer, Section, Soda, User, Wine, WineStyle } from "@zenstackhq/runtime/models"; const pr = new PrismaClient(); -const db = enhance(pr); + async function seed() { + await pr.user.upsert({ create: {username: "kenneth", password: "asdf1239", type: UserType.Admin}, update: {}, where: {username: "kenneth"} }); + await pr.user.upsert({ create: {username: "scanner", password: "asdf1239", type: UserType.Scanner}, update: {}, where: {username: "scanner"} }); + const user = await pr.user.findUnique({where: {username: "kenneth"}}) || undefined; + + // Delete all sessions + pr.session.deleteMany({}); + + const db = enhance(pr, {user}); + await db.container.deleteMany({}); await db.beer.deleteMany({}); + await db.wine.deleteMany({}); + await db.soda.deleteMany({}); + await db.cocktail.deleteMany({}); await db.beerStyle.deleteMany({}); + await db.wineStyle.deleteMany({}); + await db.section.deleteMany({}); await db.manufacturer.deleteMany({}); await db.country.deleteMany({}); + await Promise.all( getCountries().map((country) => { return db.country.create({ data: country }); @@ -36,6 +51,36 @@ async function seed() { }) ); + await Promise.all( + getWineStyles().map((style) => { + return db.wineStyle.create({ data: style }); + }) + ); + + await Promise.all( + getWines().map((wine) => { + return db.wine.create({ data: wine }); + }) + ); + + await Promise.all( + getSodas().map((soda) => { + return db.soda.create({ data: soda }); + }) + ); + + await Promise.all( + getCocktails().map((cocktail) => { + return db.cocktail.create({ data: cocktail }); + }) + ); + + await Promise.all( + getSections().map((section) => { + return db.section.create({ data: section }); + }) + ); + await Promise.all( getContainers().map((container) => { return db.container.create({ data: container }); @@ -53,6 +98,57 @@ interface BeerQuery { abv: number; style: any; ibu: number | null; + image: string | null; + link: string | null; + + gluten: boolean; + lactose: boolean; + organic: boolean; +} + +interface WineQuery { + slug: string; + manufacturer: any; + name: string; + description: string; + abv: number; + style: any; + image: string | null; + link: string | null; + + gluten: boolean; + lactose: boolean; + organic: boolean; +} + +interface SodaQuery { + slug: string; + manufacturer: any; + name: string; + description: string; + abv: number; + image: string | null; + + carbonated: boolean; + + gluten: boolean; + lactose: boolean; + organic: boolean; +} + +interface CocktailQuery { + slug: string; + manufacturer: any; + name: string; + description: string; + abv: number; + image: string | null; + + mix: boolean; + + gluten: boolean; + lactose: boolean; + organic: boolean; } interface ContainerQuery { @@ -61,6 +157,8 @@ interface ContainerQuery { type: ContainerType; volume: number; portions: number | null; + inventory: number | 0; + section: any; } function getCountries() : Country[]{ @@ -70,8 +168,8 @@ function getCountries() : Country[]{ name: "Netherlands" }, { - code: "de", - name: "Germany" + code: "ir", + name: "Ireland" }, { code: "be", @@ -85,27 +183,108 @@ function getManufacturers() : Manufacturer[] { { id: 1, country_id: "nl", - name: "Hertog Jan", - image: null + name: "Desperados", //Heineken + description: null, + image: "/manufacturers/desperados.jpeg" }, { id: 2, country_id: "nl", - name: "Heineken", - image: null + name: "Hertog Jan", // AbInBev + description: "From our Award-winning Pilsener to our great pride Grand Prestige. And from the fresh Weizener to our new hoppy Enkel. All Hertog Jan beers have something special, call it character. Learn everything about our beers at hertogjan.nl", + image: "/manufacturers/hertog_jan.jpeg" }, { id: 3, - country_id: "nl", - name: "Jupiler", - image: null + country_id: "be", + name: "Brouwerij Bosteels", // AbInBev + description: "In the hands of the Bosteels family for over 200 years, the brewery relies today on the craftmanship of seven generations. In 1791, Jean-Baptist Bosteels established the brewery, and has been followed by generations who took over the brewery and never stop brewing, even during the world wars. At that time, the brewery played such an important role in the town, the family Bosteels were such influential people that we are not surprised that 3 of the 7 generations have been Town Mayors. In the year 1930’s Antoine Bosteels, 5th generation, carried on the brewing dynasty and played an important role expanding the sales of the Bosteels Pils into other regions such as Ghent, Antwerp and Brussels. His son Ivo Bosteels honored the iconic beer of Pauwel Kwak by bringing it back to the market in 1980; this is the beginning of the strong beers for Brewery Bosteels. In the year 1990, Ivo’s son, Antoine Bosteels, the 7th generation, understood the potential of strong speciality beers and especially the trend for blond ones. Driven by passion of the art of brewing and guided by his creativity, Antoine is at the roots of the succes of Tripel Karmeliet and DeuS Brut des Flandres.", + image: "/manufacturers/brouwerij_bosteels.jpeg" }, { id: 4, country_id: "nl", - name: "Brouwerij Het IJ", + name: "Bierbrouwerij De Koningshoeven", // Royal Swinkels + description: "La Trappe Trappist is genuinely brewed ale from Berkel-Enshot, The Netherlands. It has been brewed within the walls of the Abbey of Our Lady of Koningshoeven since 1884, where the Trappist monks live a life of prayer and work, in peace and quiet, away from the hustle and bustle of everyday life. La Trappe Trappist was awarded the exclusive ‘Authentic Trappist Product’ (ATP) label by the International Trappist Association. This label guarantees that the Trappist ale is brewed under the supervision and responsibility of the monks, within the abbey walls, and that part of the profits are donated to charity. These age-old principles are still respected to this day. The result is an ale brewed in blissful silence with a taste like no other..", + image: "/manufacturers/bierbrouwerij_de_koningshoeven.jpeg" + }, + { + id: 5, + country_id: "be", + name: "Abbaye de Leffe", // AbInBev + description: "Leffe, a brewing tradition since 1240 Founded in 1152, Notre-Dame de Leffe was an abbey of Premonstratensian canons, i.e. monks living in a community characterised by its hospitality. The Leffe abbey in 1740, in its heyday: in the foreground we can see the mill with its half-timbered gable, using the water of the Leffe river. In the background, facing each other, are two enemy fortresses: the Montorgueil tower and the castle of Crevecoeur. A lay master brewer worked for the abbey and made a Leffe beer that was so delicious that the parishioners preferred to drink a Leffe on Sundays rather than go to church. The abbot had to take forceful action. The abbey and the brewery were closed during the French Revolution and seemed to be nothing but a distant memory, until the abbey was re-established in 1929. In 1952 abbot Nys and Albert Lootvoet decided to once again take up the brewing tradition of Leffe with its well-guarded recipe and offer a range of delicious Leffe beers. In the meantime, AB-InBev has taken up the torch and has made a commitment to honour the tradition of the Leffe beer, which has been brewed according to the same recipe since 1240.", + image: "/manufacturers/abbaye_de_leffe.jpeg" + }, + { + id: 6, + country_id: "ir", + name: "Guinness", //Diageo + description: "The Guinness® brand enjoys a global reputation as a uniquely authentic beer and the best-selling stout in the world. Famous for its dark color, creamy head and unique surge and settle, this distinctive beer originated at the St. James’s Gate brewery in Dublin, Ireland. Over 10 million glasses of Guinness beer are enjoyed every single day around the world, and 1.8 billion pints are sold every year. Guinness beer is available in well over 100 countries worldwide and is brewed in almost 50. About Diageo Diageo is a global leader in beverage alcohol with an outstanding collection of brands across spirits, beer and wine categories. These brands include Johnnie Walker, Crown Royal, JεB, Buchanan’s, Windsor and Bushmills whiskies, Smirnoff, Cîroc and Ketel One vodkas, Captain Morgan, Baileys, Don Julio, Tanqueray and Guinness. Diageo is a global company, and our products are sold in more than 180 countries around the world. The company is listed on both the London Stock Exchange (DGE) and the New York Stock Exchange (DEO). For more information about Diageo, our people, our brands, and performance, visit us at www.diageo.com. Visit Diageo’s global responsible drinking resource, www.DRINKiQ.com, for information, initiatives, and ways to share best practice. Celebrating life, every day, everywhere.", + image: "/manufacturers/guinness_manu.jpeg" + }, + { + id: 7, + country_id: "be", + name: "Brouwerij der Trappisten van Westmalle", + description: "For over 200 years, the monks of Westmalle have been choosing to live a life of prayer and work. True to the Rule of Saint Benedict, they ensure their own means of sustenance. For this reason, there is a farm, a cheese dairy and a brewery inside the walls of the Trappist abbey. These three things are deliberately kept to a small scale, and particular care is taken of people and the environment. The brewery’s income is used to make the necessary investments in this respect, to make changes in line with developments in brewing technology, to support Trappist communities and to carry out charity work.", + image: "/manufacturers/brouwerij_der_trappisten_van_westmalle.jpeg" + }, + { + id: 8, + country_id: "be", + name: "Brouwerij Van Steenberge", + description: "The Van Steenberge brewery stands for independence, progress and growth. It emphasises the traditional art of brewing adapted to the current technological developments and guarantees a reliable quality and service. Respect for our employees and care for the environment are central to our policy. Our beers excel because of the craft that created them. They are made according to the traditional rules of the art of brewing with the technology of today. Authentic top beers for the beer lovers of today. Gulden Draak : A unique, Belgian craft beer that is without equal: a strong, dark tripel with secondary fermentation in the bottle. The wine yeast used for the secondary fermentation contributes to the beer’s unparalleled flavour. Augustijn : The historic yeast strains give Augustijn monastery beer its mild, smooth, but very rich flavour. A beer for any occasion that excels when savoured in a relaxed atmosphere. Piraat : The power of Piraat takes you on a journey through some unique flavours based on the brewery's rich traditions. A living beer with a rich past. Set sail! Baptist is brewed on the occasion of the opening of Bar Baptist at the Van Steenberge brewery and as a tribute to Jan Baptist de Bruyne, who founded the brewery in 1784. And many more to discover! www.vansteenberge.com", + image: "/manufacturers/brouwerij_van_steenberge.jpeg" + }, + { + id: 9, + country_id: "be", + name: "Brasserie d'Achouffe", // Duvel + description: "Towards the end of the 1970s, Pierre Gobron and Christian Bauweraerts (two brother-in-laws) decided to create their own beer in their own brewery. With initially only a small amount of money available to them (200.000Bfr, less than 5.000€), they started out on what the fans of the brewery call the ‘Chouffe story’. At the beginning its founders thought of it as a hobby, but the brewery developed at such a rate that, one by one, they decided to devote themselves to the adventure full-time. The first ‘brassin’ (brewing mix) of LA CHOUFFE (49 litres) was finished on 27th August 1982. The elves of Achouffe were impatient to discover new countries, and their Dutch cousins were the first to give them a warm welcome. Even today, the Netherlands is the major destination of Achouffe Beers outside Belgium. Nowadays, more than 20 countries worldwide are supplied with our ‘nectar’ from the Ardennes forests. Coveted international awards have also been received year after year to reward their unique taste. At the end of the summer of 2006 the founders of the Brewery chose to entrust the future of their dear elves to the Duvel Moortgat Brewery. The group’s wish is to invest in Achouffe and to develop the full potential of the Brewery.", + image: "/manufacturers/brasserie_d_achouffe.jpeg" + }, + { + id: 10, + country_id: "be", + name: "Brouwerij Haacht Brasserie", + description: "An independent, family-run brewery for over 125 years. True Belgian craftsmanship. That's Haacht Brewery.", + image: "/manufacturers/brouwerij_haacht_brasserie.jpeg" + }, + { + id: 11, + country_id: "nl", + name: "Brouwerij 't IJ", //duvel + description: "Amsterdam based brewery, brewing beer in a former bathhouse since 1985. All beers are unfiltered and unpasteurised and brewed with our own characteristic yeast.", + image: "/manufacturers/brouwerij_t_ij.jpeg" + }, + { + id: 12, + country_id: "be", + name: "Omer Vander Ghinste", + description: "When Omer Vander Ghinste started brewing in 1892 in the beautiful West-Flemish village of Bellegem, brand names did not yet exist. He promoted his beers by placing stained glasswindows with the words “Beers Omer Vander Ghinste” in the front windows of the pubs. As these windows were very expensive, it was no option to replace them at every change of generation. Therefore every firstborn son was baptised ‘Omer’. A smart move, with an unexpected effect. A tradition that is still kept alive today, 130 years and 5 generations later.", + image: "/manufacturers/omer_vander_ghinste.jpeg" + }, + { + id: 13, + country_id: "be", + name: "Duvel Moortgat", + description: "It all began when Jan-Léonard Moortgat and his wife founded the Moortgat brewery farm in 1871. Around the turn of the century, Moortgat was one of the over 3,000 breweries operating in Belgium. Jan-Leonard experimented by trial and error, and his top-fermented beers were soon greatly appreciated in the brewery's home town of Puurs and far beyond. Before long, the Brussels bourgeoisie was also won over by his beers. Business was booming and Jan-Leonard's two sons, Albert and Victor, joined the company. There was a clear division of labour: Albert became the brewer, Victor was responsible for delivering the beer to Brussels by horse and dray. In 1923 the production of Duvel began with just a few crates. Today, Duvel is enjoyed literally all around the world (in over 50 countries) by countless beer lovers. The beer is still brewed with profound respect for the original recipe and the time it needs to mature.", + image: "/manufacturers/duvel_moortgat.jpeg" + }, + { + id: 14, + country_id: "be", + name: "Delirium - Huyghe Brewery", + description: "Huyghe Brewery is one of Belgium’s renowned brewers that focuses on speciality Beers and Premium Brands. Huyghe Brewery is the proud owner of the Delirium brand, Averbode abbey beer, La guillotine Gold Blond multigrain, Mongozo gluten free beer and many more. We focus on quality products and we strongly believe in renewable energy and corporate social responsibility. Which led us to be the Most Sustainable Belgium Family Brewer. Today, Delirium is served and enjoyed all over the world. And we believe that the Pink Elephant will keep on growing!", + image: "delirium_huyghe_brewery.jpeg" + }, + { + id: 15, + country_id: "nl", + name: "Coca Cola ofzo", + description: "Yay", image: null - } + }, ]; } @@ -113,15 +292,48 @@ function getBeerStyles() : BeerStyle[] { return [ { id: 1, - name: "Pilsener", + name: "Lager", + color: "fad96f" }, { id: 2, - name: "Dubbel" + name: "Weizen", + color: "fad96f" }, { id: 3, - name: "Tripel" + name: "Tripel", + color: "eebc42" + }, + { + id: 4, + name: "Blond", + color: "f7d360" + }, + { + id: 5, + name: "Quadrupel", + color: "c06202" + }, + { + id: 6, + name: "Dubbel", + color: "d77e00" + }, + { + id: 7, + name: "Gekruid", + color: "e59419" + }, + { + id: 8, + name: "Stout", + color: "380e0f" + }, + { + id: 9, + name: "Sterk Blond", + color: "f7d360" } ]; } @@ -129,26 +341,486 @@ function getBeerStyles() : BeerStyle[] { function getBeers() : BeerQuery[] { return [ { - slug: "ginger_paradise", - name: "Ginger Paradise", - abv: 6.2, - ibu: 5, + slug: "desperados_mojito", + name: "Desperados Mojito", + abv: 5.9, + ibu: 20, + + manufacturer: {connect: { id: 1}}, + style: {connect: { id: 1}}, + + description: "Mojito is there when your night's getting started, a refreshing tequila flavoured beer shaken up with a twist of mojito.", + image: "/beers/desperados_mojito.jpeg", + link: "https://untappd.com/b/desperados-nl-desperados-mojito/1988737", + + gluten: true, + lactose: false, + organic: false + }, + { + slug: "weizener", + name: "Weizener", + abv: 5.7, + ibu: 16, + + manufacturer: {connect: { id: 2}}, + style: {connect: { id: 2}}, + + description: "This willful wheat beer combines the best of a German weizen and Belgian witbier. The hop, malted wheat, fresh yeast, coriander and orange peel give it a full, fresh flavor and a deep gold color with a slight haze. The secondary fermentation on the bottle creates a fresher and richer taste and aroma.", + image: "/beers/weizener.jpeg", + link: null, + + gluten: true, + lactose: false, + organic: false + }, + { + slug: "tripel_karmeliet", + name: "Tripel Karmeliet", + abv: 8.4, + ibu: 16, + + manufacturer: {connect: { id: 3}}, + style: {connect: { id: 3}}, + + description: "Tripel Karmeliet is still brewed to an authentic beer recipe from 1679 originating in the former Carmelite monastery in Dendermonde. Written over 300 years ago, this recipe describes the use of three kinds of grain: wheat, oats and barley.", + image: "/beers/tripel_karmeliet.jpeg", + link: null, + + gluten: true, + lactose: false, + organic: false + }, + { + slug: "la_trappe_blond", + name: "La Trappe Blond", + abv: 6.5, + ibu: 20, + + manufacturer: {connect: { id: 4}}, + style: {connect: { id: 4}}, + + description: "Golden yellow Trappist ale with a white head and a rich flavour. The fruity and refreshing aromas go well with the sweet smell of malt and aromas reminiscent of spices. The result is an agreeable and tingling ale with a lightly sweet, smoothly bitter and malty taste. The aftertaste is characterised by a combination of sweetness and smooth bitterness.", + image: "/beers/la_trappe_blond.jpeg", + link: null, + + gluten: true, + lactose: false, + organic: false + }, + { + slug: "la_trappe_tripel", + name: "La Trappe Tripel", + abv: 8, + ibu: 27, manufacturer: {connect: { id: 4}}, style: {connect: { id: 3}}, - description: "Some desciption on why this beer is so great and it has this and that flavour", + description: "A golden blonde Trappist ale with a white head. Fruity aromas of peach and apricot combined with a floral aroma. La Trappe Tripel is a classic Trappist ale with a powerful and full flavour. In addition, this ale has a candy-sweet and light malty character. The aftertaste is bitter and slightly dry.", + image: "/beers/la_trappe_tripel.jpeg", + link: null, + + gluten: true, + lactose: false, + organic: false + }, + { + slug: "la_trappe_quadrupel", + name: "La Trappe Quadrupel", + abv: 10, + ibu: 22, + + manufacturer: {connect: { id: 4}}, + style: {connect: { id: 5}}, + + description: "A characteristically Trappist ale with a warm amber colour and a cream-coloured head. The aroma is associated with clover and nuts, balanced by the sweet aromas of vanilla, raisins and banana. La Trappe Quadrupel is the heaviest of all the La Trappe Trappist ales and it is also the source for the name of this style. A full, heart-warming and intense taste. Malty with sweet tones of dates and caramel. The aftertaste is smooth and slightly bitter.", + image: "/beers/la_trappe_quadrupel.jpeg", + link: null, + + gluten: true, + lactose: false, + organic: false + }, + { + slug: "leffe_blond", + name: "Leffe Blond", + abv: 6.6, + ibu: 20, + + manufacturer: {connect: { id: 5}}, + style: {connect: { id: 4}}, + + description: "Leffe Blond is the flagship of Leffe. The unique recipe is the fruit of centuries of experience in the art of brewing, which brings a broad palette of aromas into balance. It is elegant, smooth and fruity, and it has a spicy aftertaste with a hint of bitter orange. Its light, sunny colour is due to the use of pale malt.", + image: "/beers/leffe_blond.jpeg", + link: null, + + gluten: true, + lactose: false, + organic: false + }, + { + slug: "westmalle_trappist_dubbel", + name: "Westmalle Trappist Dubbel", + abv: 7, + ibu: 30, + + manufacturer: {connect: { id: 7}}, + style: {connect: { id: 6}}, + + description: "Westmalle Dubbel is a dark, reddish-brown Trappist beer with a secondary fermentation in the bottle. The creamy head has the fragrance of special malt and leaves an attractive lace pattern in the glass. The flavour is rich and complex, herby and fruity with a fresh-bitter finish. It is a balanced quality beer with a soft feel in the mouth and a long, dry aftertaste.", + image: "/beers/westmalle_trappist_dubbel.jpeg", + link: null, + + gluten: true, + lactose: false, + organic: false + }, + { + slug: "gulden_draak_classic", + name: "Gulden Draak Classic", + abv: 10.5, + ibu: 30, + + manufacturer: {connect: { id: 8}}, + style: {connect: { id: 3}}, + + description: "Such an impressive symbol that has been around for more than 6 centuries requires an equally impressive beer. Like the dragon at the top of the city, Gulden Draak has been one of the world's best beers for years. It is a dark tripel, which in itself makes it an exceptional beer, but the complex taste with notes of caramel, roasted malt and coffee makes it unique.", + image: "/beers/gulden_draak_classic.jpeg", + link: null, + + gluten: true, + lactose: false, + organic: false + }, + { + slug: "la_chouffe_blond", + name: "La Chouffe Blond", + abv: 8, + ibu: 20, + + manufacturer: {connect: { id: 9}}, + style: {connect: { id: 4}}, + + description: "The gnomes of Fairyland are particularly fond of this golden beer. LA CHOUFFE, with its slight hoppy taste, combining notes of fresh coriander and fruity tones, is the drink which gives them their zest for life. At least, that's what these imps say when they are thirsty. Their secret used to be jealously guarded from one generation to the next until the day they shared the recipe with humans to seal their friendship. Of all the legends from the wonderful region of the Belgian Ardennes, the tale of LA CHOUFFE is the one which most merits re-telling.", + image: "/beers/la_chouffe_blond.jpeg", + link: null, + + gluten: true, + lactose: false, + organic: false + }, + { + slug: "tongerlo_tripel", + name: "Tongerlo Tripel", + abv: 9, + ibu: 0, + + manufacturer: {connect: { id: 10}}, + style: {connect: { id: 3}}, + + description: "This authentic Belgian triple stands out thanks to the scale of fruity notes and the re-fermentation in the bottle, which accentuates the refined bitterness of the aromatic hops. Re-fermentation in the bottle Re-fermentation in the bottle gives the beer a more intense taste and aroma as well as a longer shelf life.", + image: "/beers/tongerlo_tripel.jpeg", + link: null, + + gluten: true, + lactose: false, + organic: false }, { slug: "ginger_paradise", name: "Ginger Paradise", abv: 6.2, - ibu: 5, + ibu: 20, - manufacturer: {connect: { id: 4}}, + manufacturer: {connect: { id: 11}}, + style: {connect: { id: 7}}, + + description: "This is our summer special for 2021. A special Blond spiced with ginger and paradise seed. Brewed with a lot of wheatmalt for a velvet smooth mouthfeel. Aroma of ginger and slightly spicy which is done by the paradise seed.", + image: "/beers/ginger_paradise.jpeg", + link: null, + + gluten: true, + lactose: false, + organic: false + }, + { + slug: "omer_blond", + name: "Omer Blond", + abv: 8, + ibu: 27, + + manufacturer: {connect: { id: 12}}, + style: {connect: { id: 4}}, + + description: "OMER. Traditional Blond is the result of 130 years of brewing tradition. Since 1892 the brewers are named Omer from father to son, now already for five generations. Top-fermented beer, refermented in the bottle. Characterised with a fine fruity aroma and a subtle bitterness.", + image: "/beers/omer_blond.jpeg", + link: null, + + gluten: true, + lactose: false, + organic: false + }, + { + slug: "guinness", + name: "Guinness", + abv: 4.2, + ibu: 45, + + manufacturer: {connect: { id: 6}}, + style: {connect: { id: 8}}, + + description: "Swirling clouds tumble as the storm begins to calm. Settle. Breathe in the moment, then break through the smooth, light head to the bittersweet reward. Unmistakeably GUINNESS, from the first velvet sip to the last, lingering drop. And every deep-dark satisfying mouthful in between. Pure beauty. Pure GUINNESS.", + image: "/beers/guinness.jpeg", + link: null, + + gluten: true, + lactose: false, + organic: false + }, + { + slug: "duvel", + name: "Duvel", + abv: 8.5, + ibu: 33, + + manufacturer: {connect: { id: 13}}, + style: {connect: { id: 9}}, + + description: "Duvel is a natural beer with a subtle bitterness, a refined flavour and a distinctive hop character. The unique brewing process, which takes about 90 days, guarantees a pure character, delicate effervescence and a pleasant sweet taste of alcohol. Apart from pure spring water, which is the main ingredient of beer, barley is the most important raw material. Barley must germinate for five days in the malt house, after which malt remains. The colour of the malt and as a consequence also of the beer is determined by the temperature. Duvel obtains its typical bitterness by adding various varieties of aromatic Slovenian and Czech hops. We use only exclusive hops that are renowned for their constant, outstanding quality. Duvel ferments for the first time in tanks at 20 to 26°C. The brewer uses his own culture for this. The original yeast strain, which Victor Moortgat himself selected in the 1920’s, originates from Scotland. After maturing in storage tanks in which the beer is cooled down to -2°C, the drink is ready for bottling. Thanks to the addition of extra sugars and yeast, the beer ferments again in the bottle. This occurs in warm cellars (24°C) and takes two weeks. Then the beer is moved to cold cellars, where it continues to mature and stabilise for a further six weeks. This extra long maturation period is unique and contributes to the refined flavour and pure taste of Duvel.", + image: "/beers/duvel.jpeg", + link: null, + + gluten: true, + lactose: false, + organic: false + }, + { + slug: "tripel_lefort", + name: "Tripel LeFort", + abv: 8.8, + ibu: 20, + + manufacturer: {connect: { id: 12}}, style: {connect: { id: 3}}, - description: "Some desciption on why this beer is so great and it has this and that flavour", + description: "Tripel LeFort is a golden blonde beer of 8.8% ABV and sets itself apart through its crisp, fruity aroma and beautifully rounded full flavour. The top-fermenting yeast used for Tripel LeFort results in a beer with vanilla-clove aromas as well as a fruity taste of bananas and red apples. Pleasant hints of citrus, lime and roses make this into a balanced beer that is intriguingly complex at the same time.", + image: "/beers/tripel_lefort.jpeg", + link: null, + + gluten: true, + lactose: false, + organic: false + }, + { + slug: "delirium_tremens", + name: "Delirium Tremens", + abv: 8.5, + ibu: 24, + + manufacturer: {connect: { id: 14}}, + style: {connect: { id: 9}}, + link: null, + + description: "The allusion to pink elephants and the choice of names is not due to chance. With a particular character, the unique taste results from triple fermentation and the use of three different yeast strains. Sweet, biscuit malt backbone, supported by pleasant warmth and spice, finishes well rounded, floral, and dry.", + image: "/beers/delirium_tremens.jpeg", + + gluten: true, + lactose: false, + organic: false + } + ]; +} + +function getWineStyles() : WineStyle[] { + return [ + { + id: 1, + name: "Wit", + color: "f5ed7d" + }, + { + id: 2, + name: "Rose", + color: "ff666d" + }, + { + id: 3, + name: "Rood", + color: "b2153a" + } + ]; +} + +function getWines() : WineQuery[] { + return [ + { + name: "White wine", + slug: "white_wine", + image: null, + description: "A white wine", + abv: 12, + + manufacturer: {connect: {id: 4}}, + style: {connect: { id: 1}}, + + link: null, + + gluten: false, + lactose: false, + organic: false + }, + { + name: "Rosé", + slug: "rose", + image: null, + description: "A rosé", + abv: 12, + + manufacturer: {connect: {id: 2}}, + style: {connect: { id: 2}}, + + link: null, + + gluten: false, + lactose: false, + organic: false + }, + { + name: "Red wine", + slug: "red_wine", + image: null, + description: "A Red wine", + abv: 12, + + manufacturer: {connect: {id: 2}}, + style: {connect: { id: 3}}, + + link: null, + + gluten: false, + lactose: false, + organic: false + } + ]; +} + +function getSodas() : SodaQuery[] { + return [ + { + name: "Ice Tea Green", + slug: "ice_tea_green", + image: null, + description: "Refreshing soda ice tea green", + abv: 0, + + manufacturer: {connect: {id: 15}}, + carbonated: false, + + gluten: false, + lactose: false, + organic: false + + }, + { + name: "Coca Cola", + slug: "coca_cola", + image: null, + description: "Way too sweet", + abv: 0, + + manufacturer: {connect: {id: 15}}, + carbonated: true, + + gluten: false, + lactose: false, + organic: false + + }, + { + name: "Rhubard drink", + slug: "rhubarb", + image: null, + description: "Some organic drink for Mario", + abv: 0, + + manufacturer: {connect: {id: 15}}, + carbonated: true, + + gluten: false, + lactose: false, + organic: true + + } + ]; +} + +function getCocktails() : CocktailQuery[] { + return [ + { + name: "Some coktail mix", + slug: "cocktail_mix", + image: null, + description: "Lekker lekker cocktail", + abv: 3, + + manufacturer: {connect: {id: 15}}, + mix: true, + + gluten: true, + lactose: false, + organic: false + }, + { + name: "Tequila", + slug: "tequila", + image: null, + description: "Lekker lekker tequila", + abv: 35, + + manufacturer: {connect: {id: 15}}, + mix: false, + + gluten: false, + lactose: false, + organic: false + } + ]; +} + +function getSections() : Section[] { + return [ + { + id: 1, + name: "Zomerbier" + }, + { + id: 2, + name: "Tripels" + }, + { + id: 3, + name: "Frisdrank" + }, + { + id: 4, + name: "Dubbels" + }, + { + id: 5, + name: "Blond" + }, + { + id: 6, + name: "Random" + }, + { + id: 7, + name: "Pils" + }, + { + id: 8, + name: "Alcoholvrij" } ]; } @@ -156,11 +828,40 @@ function getBeers() : BeerQuery[] { function getContainers() : ContainerQuery[] { return [ { - barcode: "temporary1", - drink: {connect: { slug: "ginger_paradise"}}, + barcode: "test", + drink: {connect: {slug: "ice_tea_green"}}, + section : {connect: {id: 3}}, + portions: 1, + type: ContainerType.PlasticBottle, + volume: 1000, + inventory: 1 + }, + { + barcode: "5412343152332", + drink: {connect: {slug: "westmalle_trappist_dubbel"}}, + section : {connect: {id: 4}}, + portions: 1, type: ContainerType.BeerBottle, volume: 330, - portions: 1 + inventory: 1 + }, + { + barcode: "5411858100067", + drink: {connect: {slug: "tripel_lefort"}}, + section : {connect: {id: 6}}, + portions: 1, + type: ContainerType.BeerBottle, + volume: 330, + inventory: 1 + }, + { + barcode: "5412186000098", + drink: {connect: {slug: "delirium_tremens"}}, + section : {connect: {id: 6}}, + portions: 1, + type: ContainerType.BeerBottle, + volume: 330, + inventory: 1 } ]; } diff --git a/public/.gitignore b/public/.gitignore new file mode 100644 index 0000000..4886146 --- /dev/null +++ b/public/.gitignore @@ -0,0 +1,2 @@ +beers/* +manufacturers/* \ No newline at end of file diff --git a/schema.zmodel b/schema.zmodel index 76c9da1..fbf2ca8 100644 --- a/schema.zmodel +++ b/schema.zmodel @@ -10,11 +10,18 @@ datasource db { url = env("DATABASE_URL") } +enum UserType { + User + Scanner + Admin +} + enum DrinkType { Drink Beer Wine Soda + Cocktail } enum ContainerType { @@ -38,12 +45,18 @@ model Drink { description String abv Float image String? + link String? + + gluten Boolean + lactose Boolean + organic Boolean containers Container[] @@delegate(type) - @@allow('all', true) + @@allow('read', true) + @@allow('all', auth().type == Admin) } model Beer extends Drink { @@ -52,17 +65,20 @@ model Beer extends Drink { ibu Float? - @@allow('all', true) + @@allow('read', true) + @@allow('all', auth().type == Admin) } model BeerStyle { id Int @id @default(autoincrement()) name String @unique + color String beers Beer[] - @@allow('all', true) + @@allow('read', true) + @@allow('all', auth().type == Admin) } model Wine extends Drink { @@ -75,38 +91,66 @@ model Wine extends Drink { fresh_score Int? notes String? - @@allow('all', true) + @@allow('read', true) + @@allow('all', auth().type == Admin) } model WineStyle { id Int @id @default(autoincrement()) name String @unique + color String wines Wine[] - @@allow('all', true) + @@allow('read', true) + @@allow('all', auth().type == Admin) } model Soda extends Drink { carbonated Boolean - @@allow('all', true) + @@allow('read', true) + @@allow('all', auth().type == Admin) +} + +model Cocktail extends Drink { + mix Boolean + + @@allow('read', true) + @@allow('all', auth().type == Admin) } model Container { - barcode String @id + id Int @id @default(autoincrement()) + barcode String? @unique drink_id Int drink Drink @relation(fields: [drink_id], references: [id]) + section_id Int + section Section @relation(fields: [section_id], references: [id]) + type ContainerType volume Int portions Int? + price Float @default(0.0) inventory Int @default(0) - @@allow('all', true) + @@allow('read', true) + @@allow('update', auth().type == Scanner) + @@allow('all', auth().type == Admin) +} + +model Section { + id Int @id @default(autoincrement()) + name String @unique + + containers Container[] + + @@allow('read', true) + @@allow('all', auth().type == Admin) } model Manufacturer { @@ -116,11 +160,13 @@ model Manufacturer { country Country @relation(fields: [country_id], references: [code]) name String @unique + description String? image String? drinks Drink[] - @@allow('all', true) + @@allow('read', true) + @@allow('all', auth().type == Admin) } model Country { @@ -129,5 +175,30 @@ model Country { manufacturers Manufacturer[] - @@allow('all', true) + @@allow('read', true) + @@allow('all', auth().type == Admin) } + +// Authentication +model User { + id Int @id @default(autoincrement()) + username String @unique + password String @password @omit + type UserType + + sessions Session[] + + @@allow('all', auth() == this) + @@allow('all', auth().type == Admin) +} + +model Session { + id String @id @default(uuid()) + createdAt DateTime @default(now()) + user_id Int? + user User? @relation(fields: [user_id], references: [id]) + data String + + @@allow('all', auth().id == user.id) + @@allow('all', auth().type == Admin) +} \ No newline at end of file