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 >
2026-01-06 01:01:17 +02:00
< div className = 'methods' >
< table >
< 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 >
2026-01-05 23:39:34 +02:00
< 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