kahvi.juustodiilerit.fi/frontend/src/App.tsx

435 lines
17 KiB
TypeScript
Raw Normal View History

2026-01-05 23:39:34 +02:00
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