435 lines
17 KiB
TypeScript
435 lines
17 KiB
TypeScript
|
|
import { useState } from 'react'
|
||
|
|
|
||
|
|
import { useAuth } from "react-oidc-context"
|
||
|
|
|
||
|
|
import { useBrewers, useCoffees, useGrinders, useMethods } from './api';
|
||
|
|
import { queryClient } from './main';
|
||
|
|
import { environment } from './environment';
|
||
|
|
|
||
|
|
import './App.css'
|
||
|
|
|
||
|
|
function Home() {
|
||
|
|
const auth = useAuth();
|
||
|
|
const token = auth.user?.access_token
|
||
|
|
const { isPending: areCoffeesPending, data: coffees } = useCoffees(token!)
|
||
|
|
const { isPending: areGrindersPending, data: grinders } = useGrinders(token!)
|
||
|
|
const { isPending: areBrewersPending, data: brewers } = useBrewers(token!)
|
||
|
|
const { isPending: areMethodsPending, data: methods } = useMethods(token!)
|
||
|
|
|
||
|
|
const [addCoffeeName, setAddCoffeeName] = useState('')
|
||
|
|
const [addCoffeeRoastLevel, setAddCoffeeRoastLevel] = useState(3)
|
||
|
|
const [addCoffeeNotes, setAddCoffeeNotes] = useState('')
|
||
|
|
|
||
|
|
const [addGrinderName, setAddGrinderName] = useState('')
|
||
|
|
const [addGrinderNotes, setAddGrinderNotes] = useState('')
|
||
|
|
|
||
|
|
const [addBrewerName, setAddBrewerName] = useState('')
|
||
|
|
const [addBrewerNotes, setAddBrewerNotes] = useState('')
|
||
|
|
|
||
|
|
const [addMethodCoffeeId, setAddMethodCoffeeId] = useState('')
|
||
|
|
const [addMethodCoffeeAmountG, setAddMethodCoffeeAmountG] = useState(15)
|
||
|
|
const [addMethodGrinderId, setAddMethodGrinderId] = useState('')
|
||
|
|
const [addMethodGrindSetting, setAddMethodGrindSetting] = useState('')
|
||
|
|
const [addMethodBrewerId, setAddMethodBrewerId] = useState('')
|
||
|
|
const [addMethodWaterAmountMl, setAddMethodWaterAmountMl] = useState(250)
|
||
|
|
const [addMethodWaterTemperatureC, setAddMethodWaterTemperatureC] = useState(90)
|
||
|
|
const [addMethodBloomingTimeSec, setAddMethodBloomingTimeSec] = useState(45)
|
||
|
|
const [addMethodBrewingTimeSec, setAddMethodBrewingTimeSec] = useState(120)
|
||
|
|
const [addMethodNotes, setAddMethodNotes] = useState('')
|
||
|
|
|
||
|
|
const [randomMix, setRandomMix] = useState(
|
||
|
|
undefined as {coffeeId: string, grinderId: string, brewerId: string} | undefined
|
||
|
|
)
|
||
|
|
const [randomMethodId, setRandomMethodId] = useState(undefined as string | undefined)
|
||
|
|
|
||
|
|
if (areCoffeesPending || areGrindersPending || areBrewersPending || areMethodsPending) {
|
||
|
|
return (
|
||
|
|
<div>
|
||
|
|
Loading...
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
function openDialog(dialogId: string) {
|
||
|
|
const dialogElement = document.getElementById(dialogId);
|
||
|
|
(dialogElement as HTMLDialogElement).showModal()
|
||
|
|
}
|
||
|
|
|
||
|
|
function closeDialog(dialogId: string) {
|
||
|
|
const dialogElement = document.getElementById(dialogId);
|
||
|
|
(dialogElement as HTMLDialogElement).close()
|
||
|
|
}
|
||
|
|
|
||
|
|
function closeDialogButton(dialogId: string) {
|
||
|
|
return <button onClick={closeDialog.bind(null, dialogId)}>Cancel</button>
|
||
|
|
}
|
||
|
|
|
||
|
|
async function postNewEntry(entryType: 'coffee'|'grinder'|'brewer'|'method', body: any) {
|
||
|
|
try {
|
||
|
|
let result = await fetch(
|
||
|
|
`${environment.platformRoot}/api/${entryType}`,
|
||
|
|
{
|
||
|
|
method: 'POST',
|
||
|
|
body: JSON.stringify(body),
|
||
|
|
headers: {
|
||
|
|
Authorization: `Bearer ${token}`,
|
||
|
|
'content-type': 'application/json'
|
||
|
|
}
|
||
|
|
}
|
||
|
|
)
|
||
|
|
if (result.status !== 200) {
|
||
|
|
window.alert(`Error: status ${result.status}; ${await result.text()}`)
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
window.alert(`Error: ${error}`)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================
|
||
|
|
// COFFEE
|
||
|
|
// ============================================
|
||
|
|
const addCoffeeDialogId = 'addCoffeeDialog'
|
||
|
|
async function postNewCoffee() {
|
||
|
|
postNewEntry(
|
||
|
|
'coffee',
|
||
|
|
{
|
||
|
|
name: addCoffeeName,
|
||
|
|
roastLevel: addCoffeeRoastLevel,
|
||
|
|
notes: addCoffeeNotes,
|
||
|
|
}
|
||
|
|
)
|
||
|
|
queryClient.invalidateQueries({ queryKey: ['coffees'] })
|
||
|
|
closeDialog(addCoffeeDialogId)
|
||
|
|
}
|
||
|
|
function renderAddCoffeeDialog() {
|
||
|
|
return (
|
||
|
|
<dialog id={addCoffeeDialogId} className='addSomethingDialog'>
|
||
|
|
<div className='column'>
|
||
|
|
<label className="" htmlFor="addCoffeeName">
|
||
|
|
Name
|
||
|
|
</label>
|
||
|
|
<input className="" id="addCoffeeName" name="addCoffeeName" placeholder="Rost Aalto, Fazer Blend..." required type="text" value={addCoffeeName} onChange={(event) => setAddCoffeeName(event.target.value)}/>
|
||
|
|
|
||
|
|
<label className="" htmlFor="addCoffeeRoastLevel">
|
||
|
|
Roast level
|
||
|
|
</label>
|
||
|
|
<input className="" id="addCoffeeRoastLevel" name="addCoffeeRoastLevel" placeholder="3" required type="number" value={addCoffeeRoastLevel} onChange={(event) => setAddCoffeeRoastLevel(Number.parseInt(event.target.value))}/>
|
||
|
|
|
||
|
|
<label className="" htmlFor="addCoffeeNotes">
|
||
|
|
Notes
|
||
|
|
</label>
|
||
|
|
<input className="" id="addCoffeeNotes" name="addCoffeeNotes" placeholder="Notes" required type="text" value={addCoffeeNotes} onChange={(event) => setAddCoffeeNotes(event.target.value)}/>
|
||
|
|
|
||
|
|
<button onClick={postNewCoffee}>
|
||
|
|
Add coffee
|
||
|
|
</button>
|
||
|
|
{closeDialogButton(addCoffeeDialogId)}
|
||
|
|
</div>
|
||
|
|
</dialog>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================
|
||
|
|
// GRINDER
|
||
|
|
// ============================================
|
||
|
|
const addGrinderDialogId = 'addGrinderDialog'
|
||
|
|
async function postNewGrinder() {
|
||
|
|
postNewEntry(
|
||
|
|
'grinder',
|
||
|
|
{
|
||
|
|
name: addGrinderName,
|
||
|
|
notes: addGrinderNotes,
|
||
|
|
}
|
||
|
|
)
|
||
|
|
queryClient.invalidateQueries({ queryKey: ['grinders'] })
|
||
|
|
closeDialog(addGrinderDialogId)
|
||
|
|
}
|
||
|
|
function renderAddGrinderDialog() {
|
||
|
|
return (
|
||
|
|
<dialog id={addGrinderDialogId} className='addSomethingDialog'>
|
||
|
|
<div className='column'>
|
||
|
|
<label className="" htmlFor="addGrinderName">
|
||
|
|
Name
|
||
|
|
</label>
|
||
|
|
<input className="" id="addGrinderName" name="addGrinderName" placeholder="Comandante C40" required type="text" value={addGrinderName} onChange={(event) => setAddGrinderName(event.target.value)}/>
|
||
|
|
|
||
|
|
<label className="" htmlFor="addGrinderNotes">
|
||
|
|
Notes
|
||
|
|
</label>
|
||
|
|
<input className="" id="addGrinderNotes" name="addGrinderNotes" placeholder="Notes" required type="text" value={addGrinderNotes} onChange={(event) => setAddGrinderNotes(event.target.value)}/>
|
||
|
|
|
||
|
|
<button onClick={postNewGrinder}>
|
||
|
|
Add grinder
|
||
|
|
</button>
|
||
|
|
{closeDialogButton(addGrinderDialogId)}
|
||
|
|
</div>
|
||
|
|
</dialog>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================
|
||
|
|
// BREWER
|
||
|
|
// ============================================
|
||
|
|
const addBrewerDialogId = 'addBrewerDialog'
|
||
|
|
async function postNewBrewer() {
|
||
|
|
postNewEntry(
|
||
|
|
'brewer',
|
||
|
|
{
|
||
|
|
name: addBrewerName,
|
||
|
|
notes: addBrewerNotes,
|
||
|
|
}
|
||
|
|
)
|
||
|
|
queryClient.invalidateQueries({ queryKey: ['brewers'] })
|
||
|
|
closeDialog(addBrewerDialogId)
|
||
|
|
}
|
||
|
|
function renderAddBrewerDialog() {
|
||
|
|
return (
|
||
|
|
<dialog id={addBrewerDialogId} className='addSomethingDialog'>
|
||
|
|
<div className='column'>
|
||
|
|
<label className="" htmlFor="addBrewerName">
|
||
|
|
Name
|
||
|
|
</label>
|
||
|
|
<input className="" id="addBrewerName" name="addBrewerName" placeholder="Bialetti Moka Express, Hario Switch..." required type="text" value={addBrewerName} onChange={(event) => setAddBrewerName(event.target.value)}/>
|
||
|
|
|
||
|
|
<label className="" htmlFor="addBrewerNotes">
|
||
|
|
Notes
|
||
|
|
</label>
|
||
|
|
<input className="" id="addBrewerNotes" name="addBrewerNotes" placeholder="Notes" required type="text" value={addBrewerNotes} onChange={(event) => setAddBrewerNotes(event.target.value)}/>
|
||
|
|
|
||
|
|
<button onClick={postNewBrewer}>
|
||
|
|
Add brewer
|
||
|
|
</button>
|
||
|
|
{closeDialogButton(addBrewerDialogId)}
|
||
|
|
</div>
|
||
|
|
</dialog>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================
|
||
|
|
// METHOD
|
||
|
|
// ============================================
|
||
|
|
const addMethodDialogId = 'addMethodDialog'
|
||
|
|
async function postNewMethod() {
|
||
|
|
postNewEntry(
|
||
|
|
'method',
|
||
|
|
{
|
||
|
|
coffeeId: addMethodCoffeeId,
|
||
|
|
coffeeAmountG: addMethodCoffeeAmountG,
|
||
|
|
grinderId: addMethodGrinderId,
|
||
|
|
grindSetting: addMethodGrindSetting,
|
||
|
|
brewerId: addMethodBrewerId,
|
||
|
|
waterAmountMl: addMethodWaterAmountMl,
|
||
|
|
waterTemperatureC: addMethodWaterTemperatureC,
|
||
|
|
bloomingTimeSec: addMethodBloomingTimeSec,
|
||
|
|
brewingTimeSec: addMethodBrewingTimeSec,
|
||
|
|
notes: addMethodNotes,
|
||
|
|
}
|
||
|
|
)
|
||
|
|
queryClient.invalidateQueries({ queryKey: ['methods'] })
|
||
|
|
closeDialog(addMethodDialogId)
|
||
|
|
}
|
||
|
|
function renderAddMethodDialog() {
|
||
|
|
const coffeeOptions = coffees.map((coffee: any) => <option key={`coffee-option-${coffee.id}`} value={coffee.id}>{coffee.name} - Roast {coffee.roast_level}</option>)
|
||
|
|
const grinderOptions = grinders.map((grinder: any) => <option key={`grinder-option-${grinder.id}`} value={grinder.id}>{grinder.name}</option>)
|
||
|
|
const brewerOptions = brewers.map((brewer: any) => <option key={`brewer-option-${brewer.id}`} value={brewer.id}>{brewer.name}</option>)
|
||
|
|
return (
|
||
|
|
<dialog id={addMethodDialogId} className='addSomethingDialog'>
|
||
|
|
<div className='column'>
|
||
|
|
<label className="" htmlFor="addMethodCoffee">
|
||
|
|
Coffee
|
||
|
|
</label>
|
||
|
|
<select id="addMethodCoffee" name="addMethodCoffee" value={addMethodCoffeeId} onChange={(event) => setAddMethodCoffeeId(event.target.value)}>
|
||
|
|
<option value="">Select a coffee</option>
|
||
|
|
{coffeeOptions}
|
||
|
|
</select>
|
||
|
|
|
||
|
|
<label className="" htmlFor="addMethodCoffeeAmountG">
|
||
|
|
Coffee amount (grams)
|
||
|
|
</label>
|
||
|
|
<input className="" id="addMethodCoffeeAmountG" name="addMethodCoffeeAmountG" placeholder="15" required type="number" value={addMethodCoffeeAmountG} onChange={(event) => setAddMethodCoffeeAmountG(Number.parseInt(event.target.value))}/>
|
||
|
|
|
||
|
|
<label className="" htmlFor="addMethodGrinder">
|
||
|
|
Grinder
|
||
|
|
</label>
|
||
|
|
<select id="addMethodGrinder" name="addMethodGrinder" value={addMethodGrinderId} onChange={(event) => setAddMethodGrinderId(event.target.value)}>
|
||
|
|
<option value="">Select a grinder</option>
|
||
|
|
{grinderOptions}
|
||
|
|
</select>
|
||
|
|
|
||
|
|
<label className="" htmlFor="addMethodGrindSetting">
|
||
|
|
Grind setting
|
||
|
|
</label>
|
||
|
|
<input className="" id="addMethodGrindSetting" name="addMethodGrindSetting" placeholder="Clicks" required type="text" value={addMethodGrindSetting} onChange={(event) => setAddMethodGrindSetting(event.target.value)}/>
|
||
|
|
|
||
|
|
<label className="" htmlFor="addMethodBrewer">
|
||
|
|
Brewer
|
||
|
|
</label>
|
||
|
|
<select id="addMethodBrewer" name="addMethodBrewer" value={addMethodBrewerId} onChange={(event) => setAddMethodBrewerId(event.target.value)}>
|
||
|
|
<option value="">Select a brewer</option>
|
||
|
|
{brewerOptions}
|
||
|
|
</select>
|
||
|
|
|
||
|
|
<label className="" htmlFor="addMethodWaterAmountMl">
|
||
|
|
Water amount (ml)
|
||
|
|
</label>
|
||
|
|
<input className="" id="addMethodWaterAmountMl" name="addMethodWaterAmountMl" placeholder="250" required type="number" value={addMethodWaterAmountMl} onChange={(event) => setAddMethodWaterAmountMl(Number.parseInt(event.target.value))}/>
|
||
|
|
|
||
|
|
<label className="" htmlFor="addMethodWaterTemperatureC">
|
||
|
|
Water temperature (°C)
|
||
|
|
</label>
|
||
|
|
<input className="" id="addMethodWaterTemperatureC" name="addMethodWaterTemperatureC" placeholder="250" required type="number" value={addMethodWaterTemperatureC} onChange={(event) => setAddMethodWaterTemperatureC(Number.parseInt(event.target.value))}/>
|
||
|
|
|
||
|
|
<label className="" htmlFor="addMethodBloomingTimeSec">
|
||
|
|
Bloom time (sec)
|
||
|
|
</label>
|
||
|
|
<input className="" id="addMethodBloomingTimeSec" name="addMethodBloomingTimeSec" placeholder="45" required type="number" value={addMethodBloomingTimeSec} onChange={(event) => setAddMethodBloomingTimeSec(Number.parseInt(event.target.value))}/>
|
||
|
|
|
||
|
|
<label className="" htmlFor="addMethodBrewingTimeSec">
|
||
|
|
Brew time (sec)
|
||
|
|
</label>
|
||
|
|
<input className="" id="addMethodBrewingTimeSec" name="addMethodBrewingTimeSec" placeholder="120" required type="number" value={addMethodBrewingTimeSec} onChange={(event) => setAddMethodBrewingTimeSec(Number.parseInt(event.target.value))}/>
|
||
|
|
|
||
|
|
<label className="" htmlFor="addMethodNotes">
|
||
|
|
Notes
|
||
|
|
</label>
|
||
|
|
<input className="" id="addMethodNotes" name="addMethodNotes" placeholder="Notes" required type="text" value={addMethodNotes} onChange={(event) => setAddMethodNotes(event.target.value)}/>
|
||
|
|
|
||
|
|
<button onClick={postNewMethod}>
|
||
|
|
Add method
|
||
|
|
</button>
|
||
|
|
{closeDialogButton(addMethodDialogId)}
|
||
|
|
</div>
|
||
|
|
</dialog>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className='column'>
|
||
|
|
<div className='mixOptions'>
|
||
|
|
{renderAddCoffeeDialog()}
|
||
|
|
<button className='coffees' onClick={openDialog.bind(null, addCoffeeDialogId)}>
|
||
|
|
Add coffee
|
||
|
|
</button>
|
||
|
|
{coffees?.map((coffee: any) => <p key={`coffee-${coffee.id}`} className={`coffees ${coffee.id === randomMix?.coffeeId ? 'randomlySelected' : ''}`}>{coffee.name} - Roast {coffee.roast_level}</p>)}
|
||
|
|
{renderAddGrinderDialog()}
|
||
|
|
<button className='grinders' onClick={openDialog.bind(null, addGrinderDialogId)}>
|
||
|
|
Add grinder
|
||
|
|
</button>
|
||
|
|
{grinders?.map((grinder: any) => <p key={`grinder-${grinder.id}`} className={`grinders ${grinder.id === randomMix?.grinderId ? 'randomlySelected' : ''}`}>{grinder.name}</p>)}
|
||
|
|
{renderAddBrewerDialog()}
|
||
|
|
<button className='brewers' onClick={openDialog.bind(null, addBrewerDialogId)}>
|
||
|
|
Add brewer
|
||
|
|
</button>
|
||
|
|
{brewers?.map((brewer: any) => <p key={`brewer-${brewer.id}`} className={`brewers ${brewer.id === randomMix?.brewerId ? 'randomlySelected' : ''}`}>{brewer.name}</p>)}
|
||
|
|
</div>
|
||
|
|
<div className='row'>
|
||
|
|
</div>
|
||
|
|
<div className='column methods'>
|
||
|
|
{renderAddMethodDialog()}
|
||
|
|
<button onClick={openDialog.bind(null, addMethodDialogId)}>
|
||
|
|
Add method
|
||
|
|
</button>
|
||
|
|
<table className='methods'>
|
||
|
|
<tbody>
|
||
|
|
{methods?.map((method: any) => {
|
||
|
|
const coffeeFound = coffees.find((coffee: any) => coffee.id === method.coffee_id)
|
||
|
|
const grinderFound = grinders.find((grinder: any) => grinder.id === method.grinder_id)
|
||
|
|
const brewerFound = brewers.find((brewer: any) => brewer.id === method.brewer_id)
|
||
|
|
const isRandomlySelected = (randomMethodId && method.id === randomMethodId)
|
||
|
|
|| (randomMix && method.coffee_id === randomMix?.coffeeId && method.grinder_id === randomMix?.grinderId && method.brewer_id === randomMix?.brewerId)
|
||
|
|
return (
|
||
|
|
<tr key={`method-${method.id}`} className={`${isRandomlySelected ? 'randomlySelected' : ''}`}>
|
||
|
|
<td>{coffeeFound.name}</td>
|
||
|
|
<td>{method.coffee_amount_g} g</td>
|
||
|
|
<td>{grinderFound.name}</td>
|
||
|
|
<td>{method.grind_setting}</td>
|
||
|
|
<td>{brewerFound.name}</td>
|
||
|
|
<td>{method.water_amount_ml} ml</td>
|
||
|
|
<td>{method.water_temperature_c} °C</td>
|
||
|
|
<td>Bloom {method.blooming_time_sec} sec</td>
|
||
|
|
<td>Brew {method.brewing_time_sec} sec</td>
|
||
|
|
</tr>
|
||
|
|
)
|
||
|
|
})}
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
<div className="row randomizerButtons">
|
||
|
|
<button onClick={() => {
|
||
|
|
setRandomMethodId(undefined)
|
||
|
|
const coffeeIndex = Math.floor(Math.random() * coffees.length)
|
||
|
|
const grinderIndex = Math.floor(Math.random() * grinders.length)
|
||
|
|
const brewerIndex = Math.floor(Math.random() * brewers.length)
|
||
|
|
setRandomMix({
|
||
|
|
coffeeId: coffees[coffeeIndex]?.id,
|
||
|
|
grinderId: grinders[grinderIndex]?.id,
|
||
|
|
brewerId: brewers[brewerIndex]?.id
|
||
|
|
})
|
||
|
|
}}>
|
||
|
|
Select a random mix
|
||
|
|
</button>
|
||
|
|
<button onClick={() => {
|
||
|
|
setRandomMix(undefined)
|
||
|
|
const randomIndex = Math.floor(Math.random() * methods.length)
|
||
|
|
setRandomMethodId(methods[randomIndex]?.id)
|
||
|
|
}}>
|
||
|
|
Select a random method
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
function App() {
|
||
|
|
const auth = useAuth();
|
||
|
|
|
||
|
|
switch (auth.activeNavigator) {
|
||
|
|
case "signinSilent":
|
||
|
|
return <div>Signing you in...</div>;
|
||
|
|
case "signoutRedirect":
|
||
|
|
return <div>Signing you out...</div>;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (auth.isLoading) {
|
||
|
|
return <div>Loading...</div>;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (auth.error) {
|
||
|
|
return <div>Oops... {auth.error.message}</div>;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (auth.isAuthenticated) {
|
||
|
|
if (document.location.search.includes('code=') && document.location.search.includes('state=')) {
|
||
|
|
document.location.href = '/'
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className='layout'>
|
||
|
|
<nav className='row'>
|
||
|
|
<h1>kahvi.juustodiilerit.fi</h1>
|
||
|
|
<button onClick={() => void auth.removeUser()}>Log out</button>
|
||
|
|
</nav>
|
||
|
|
<main className="column">
|
||
|
|
<Home/>
|
||
|
|
</main>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className='layout'>
|
||
|
|
<nav className='row'>
|
||
|
|
<h1>kahvi.juustodiilerit.fi</h1>
|
||
|
|
<button onClick={() => void auth.signinRedirect()}>Log in</button>
|
||
|
|
</nav>
|
||
|
|
<main className="column">
|
||
|
|
<button onClick={() => void auth.signinRedirect()}>Log in</button>
|
||
|
|
</main>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
export default App
|