├── .github └── workflows │ └── elm.yml ├── .gitignore ├── LICENSE ├── README.md ├── front ├── .gitignore ├── elm.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo.svg │ └── robots.txt └── src │ ├── Data.elm │ ├── Main.elm │ ├── Quantity.elm │ └── index.js ├── package-lock.json └── package.json /.github/workflows/elm.yml: -------------------------------------------------------------------------------- 1 | name: Elm 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [12.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v1 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - name: install elm 21 | run: | 22 | npm install -g elm elm-analyse 23 | - name: install dependencies 24 | working-directory: ./front 25 | run: | 26 | elm make src/Main.elm 27 | - name: 28 | working-directory: ./front 29 | run: | 30 | elm-analyse 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | elm-stuff 2 | node_modules 3 | ./public 4 | .DS_Store 5 | 6 | .now -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Tom MacWright 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Old Fashioned 2 | 3 | This is the software behind oldfashioned.tech. Given the, um, fast pace of development, there's 4 | a little sawdust on the shop floor, so to speak. So, the application is in `/front`. 5 | 6 | It's written in [Elm](https://elm-lang.org/), a lovely new language, and importantly uses [elm-ui](https://github.com/mdgriffith/elm-ui), 7 | a very futuristic UI library. So it's a little different than most web apps, but **on the bright side**, 8 | this _really_ fits into the Elm model super well, and also the data representation is super approachable. 9 | 10 | ## Set up 11 | 12 | To get set up: 13 | 14 | - You need [Node.js](https://nodejs.org/en/) installe: 15 | 16 | Then install create-elm-app globally: 17 | 18 | ``` 19 | npm install create-elm-app -g 20 | ``` 21 | 22 | Then, once you've cloned this git repository, go into the `front` directory, and run 23 | 24 | ``` 25 | elm-app start 26 | ``` 27 | 28 | ## The lay of the land 29 | 30 | There's a smidge of 'modularization', in this case, basically moving things into files. The files are: 31 | 32 | - `Main.elm`: the interface 33 | - `Quantity.elm`: a system for representing quantities 34 | - `Icons.elm`: um… icons 35 | - `Data.elm`: all the recipes and stuff 36 | 37 | A few concepts to get out of the way: 38 | 39 | I refer to substances, like "Vodka", as Materials. This is because just "Vodka" is not an ingredient: 40 | 6 Cl of Vodka is an ingredient. 41 | 42 | Quantities are kind of fancy, they use Elm types! Some example quantities: 43 | 44 | ``` 45 | Dashes 2 46 | FewDashes 47 | CL 2 48 | ``` 49 | 50 | Same with glasses - there's a type for each kind of glass. 51 | 52 | Note that all of the 'liquid' quantities are measured in CL, or Centiliters, because that's what 53 | Wikipedia uses. We convert to all the other measures on the frontend. 54 | 55 | So a recipe looks like this, and goes in a long list in Data.elm 56 | 57 | ```elm 58 | -- https://en.wikipedia.org/wiki/Sour_(cocktail)#White_Lady 59 | { name = "White lady" 60 | , ingredients = 61 | [ { material = gin, quantity = CL 4 } 62 | , { material = tripleSec, quantity = CL 3 } 63 | , { material = lemonJuice, quantity = CL 2 } 64 | ] 65 | , description = """Add all ingredients into cocktail shaker filled with ice. Shake well and strain into large cocktail glass.""" 66 | , glass = Cocktail 67 | } 68 | ``` 69 | 70 | Really, making recipes, Elm will usually tell you exactly what's wrong, which is really neat. 71 | -------------------------------------------------------------------------------- /front/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | -------------------------------------------------------------------------------- /front/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "elm/browser": "1.0.2", 10 | "elm/core": "1.0.5", 11 | "elm/html": "1.0.0", 12 | "elm/http": "2.0.0", 13 | "elm/json": "1.1.3", 14 | "elm/svg": "1.0.1", 15 | "elm/url": "1.0.0", 16 | "hecrj/elm-slug": "1.0.2", 17 | "mdgriffith/elm-ui": "1.1.8", 18 | "turboMaCk/any-set": "1.4.0" 19 | }, 20 | "indirect": { 21 | "elm/bytes": "1.0.8", 22 | "elm/file": "1.0.5", 23 | "elm/regex": "1.0.0", 24 | "elm/time": "1.0.0", 25 | "elm/virtual-dom": "1.0.2", 26 | "turboMaCk/any-dict": "2.4.0" 27 | } 28 | }, 29 | "test-dependencies": { 30 | "direct": {}, 31 | "indirect": {} 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /front/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmcw/flair/0a2fd9a5b70e76bc2a1a0164db85a064eec69372/front/public/favicon.ico -------------------------------------------------------------------------------- /front/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 17 | Old Fashioned 18 | 19 | 20 | 23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /front/public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 10 | 11 | 14 | 15 | 22 | 23 | 26 | 27 | 30 | 31 | 34 | 35 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /front/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | -------------------------------------------------------------------------------- /front/src/Data.elm: -------------------------------------------------------------------------------- 1 | module Data exposing (Glass(..), Ingredient, Material, MaterialType(..), Recipe, SuperMaterial(..), Video(..), recipes) 2 | 3 | import Quantity exposing (Quantity(..)) 4 | 5 | 6 | type 7 | MaterialType 8 | -- Main alcohol types: 9 | = Base -- Fermented base: Sparkling wine, beer, cider, etc. 10 | | Fortified -- Fermented base with extra alcohol: Port, sherry, vermouth, americano, etc. 11 | | Liqueur -- Spirit is infused / macerated with herb, spice, fruit; Amari, crème, etc. 12 | | Spirit -- Fermented base is distilled: Whiskey, rum, vodka, gin, tequila, brandy, etc. 13 | -- Main non-alcoholic types: 14 | | Bitters -- Technically often high alcohol but used sparingly. 15 | | Fruit 16 | | Juice 17 | | Seasoning -- Salt, herbs, spices. 18 | | Soda -- Cola, ginger beer, etc. 19 | | Syrup 20 | | Other 21 | 22 | 23 | type Glass 24 | = -- Tumblers: 25 | Collins 26 | | Highball 27 | | OldFashioned 28 | -- | Shot 29 | -- | Table 30 | -- Stemware: 31 | -- | Absinthe 32 | -- | Chalice 33 | | ChampagneCoupe 34 | | ChampagneFlute 35 | | Cocktail 36 | | Hurricane 37 | | Margarita 38 | -- | Sherry 39 | -- | Snifter 40 | | Wine 41 | -- Other: 42 | | CopperMug 43 | | IrishCoffeeMug 44 | | SteelCup 45 | | ZombieGlass 46 | 47 | 48 | type alias Material = 49 | { name : String 50 | , t : MaterialType 51 | , super : SuperMaterial 52 | } 53 | 54 | 55 | type SuperMaterial 56 | = SuperMaterial (Maybe Material) 57 | 58 | 59 | type alias Ingredient = 60 | { material : Material 61 | , quantity : Quantity 62 | , optional : Bool 63 | } 64 | 65 | 66 | ingredient : Material -> Quantity -> Ingredient 67 | ingredient mat quantity = 68 | { material = mat 69 | , quantity = quantity 70 | , optional = False 71 | } 72 | 73 | 74 | optionalIngredient : Material -> Quantity -> Ingredient 75 | optionalIngredient mat quantity = 76 | { material = mat 77 | , quantity = quantity 78 | , optional = True 79 | } 80 | 81 | 82 | material : String -> MaterialType -> Material 83 | material name t = 84 | { name = name, t = t, super = SuperMaterial Nothing } 85 | 86 | 87 | material3 : String -> MaterialType -> Material -> Material 88 | material3 name t super = 89 | { name = name, t = t, super = SuperMaterial (Just super) } 90 | 91 | 92 | 93 | -- Whiskey 94 | 95 | 96 | whiskey : Material 97 | whiskey = 98 | material "Whiskey" Spirit 99 | 100 | 101 | scotchWhiskey : Material 102 | scotchWhiskey = 103 | material3 "Scotch whiskey" Spirit whiskey 104 | 105 | 106 | bourbonWhiskey : Material 107 | bourbonWhiskey = 108 | material3 "Bourbon whiskey" Spirit whiskey 109 | 110 | 111 | canadianWhiskey : Material 112 | canadianWhiskey = 113 | material3 "Canadian whiskey" Spirit whiskey 114 | 115 | 116 | ryeWhiskey : Material 117 | ryeWhiskey = 118 | material3 "Rye whiskey" Spirit whiskey 119 | 120 | 121 | irishWhiskey : Material 122 | irishWhiskey = 123 | material3 "Irish whiskey" Spirit whiskey 124 | 125 | 126 | drambuie : Material 127 | drambuie = 128 | material "Drambuie" Liqueur 129 | 130 | 131 | water : Material 132 | water = 133 | material "Water" Other 134 | 135 | 136 | sodaWater : Material 137 | sodaWater = 138 | material "Soda water" Soda 139 | 140 | 141 | gin : Material 142 | gin = 143 | material "Gin" Spirit 144 | 145 | 146 | oldTomGin : Material 147 | oldTomGin = 148 | material3 "Old tom gin" Spirit gin 149 | 150 | 151 | londonDryGin : Material 152 | londonDryGin = 153 | material3 "London dry gin" Spirit gin 154 | 155 | 156 | whiteCremeDeMenthe : Material 157 | whiteCremeDeMenthe = 158 | material "White crème de menthe" Liqueur 159 | 160 | 161 | cremeDeMure : Material 162 | cremeDeMure = 163 | material "Crème de mure" Liqueur 164 | 165 | 166 | maraschino : Material 167 | maraschino = 168 | material "Maraschino" Liqueur 169 | 170 | 171 | brandy : Material 172 | brandy = 173 | material "Brandy" Spirit 174 | 175 | 176 | apricotBrandy : Material 177 | apricotBrandy = 178 | material "Apricot brandy" Liqueur 179 | 180 | 181 | 182 | -- `port` is a reserved word. 183 | 184 | 185 | pport : Material 186 | pport = 187 | material "Port" Fortified 188 | 189 | 190 | calvados : Material 191 | calvados = 192 | material "Calvados" Spirit 193 | 194 | 195 | bitters : Material 196 | bitters = 197 | material "Bitters" Bitters 198 | 199 | 200 | peachBitters : Material 201 | peachBitters = 202 | material3 "Peach bitters" Bitters bitters 203 | 204 | 205 | orangeBitters : Material 206 | orangeBitters = 207 | material3 "Orange bitters" Bitters bitters 208 | 209 | 210 | angosturaBitters : Material 211 | angosturaBitters = 212 | material3 "Angostura bitters" Bitters bitters 213 | 214 | 215 | peychaudsBitters : Material 216 | peychaudsBitters = 217 | material3 "Peychaud’s bitters" Bitters bitters 218 | 219 | 220 | aromaticBitters : Material 221 | aromaticBitters = 222 | material3 "Aromatic bitters" Bitters bitters 223 | 224 | 225 | lemon : Material 226 | lemon = 227 | material "Lemon" Fruit 228 | 229 | 230 | blackberry : Material 231 | blackberry = 232 | material "Blackberry" Fruit 233 | 234 | 235 | raspberry : Material 236 | raspberry = 237 | material "Raspberry" Fruit 238 | 239 | 240 | cherry : Material 241 | cherry = 242 | material "Cherry" Fruit 243 | 244 | 245 | pineapple : Material 246 | pineapple = 247 | material "Pineapple" Fruit 248 | 249 | 250 | sweetRedVermouth : Material 251 | sweetRedVermouth = 252 | material "Sweet red vermouth" Fortified 253 | 254 | 255 | dryVermouth : Material 256 | dryVermouth = 257 | material "Dry vermouth" Fortified 258 | 259 | 260 | cognac : Material 261 | cognac = 262 | material3 "Cognac" Spirit brandy 263 | 264 | 265 | tripleSec : Material 266 | tripleSec = 267 | material "Triple sec" Liqueur 268 | 269 | 270 | grandMarnier : Material 271 | grandMarnier = 272 | material3 "Grand Marnier" Liqueur tripleSec 273 | 274 | 275 | cointreau : Material 276 | cointreau = 277 | material3 "Cointreau" Liqueur tripleSec 278 | 279 | 280 | curacao : Material 281 | curacao = 282 | material3 "Curaçao" Liqueur tripleSec 283 | 284 | 285 | grenadine : Material 286 | grenadine = 287 | material "Grenadine" Syrup 288 | 289 | 290 | oj : Material 291 | oj = 292 | material "Orange juice" Juice 293 | 294 | 295 | pineappleJuice : Material 296 | pineappleJuice = 297 | material "Pineapple juice" Juice 298 | 299 | 300 | lime : Material 301 | lime = 302 | material "Lime" Fruit 303 | 304 | 305 | cachaca : Material 306 | cachaca = 307 | material3 "Cachaça" Spirit rum 308 | 309 | 310 | absinthe : Material 311 | absinthe = 312 | material "Absinthe" Liqueur 313 | 314 | 315 | campari : Material 316 | campari = 317 | material "Campari" Liqueur 318 | 319 | 320 | fernetBranca : Material 321 | fernetBranca = 322 | material "Fernet Branca" Liqueur 323 | 324 | 325 | raspberrySyrup : Material 326 | raspberrySyrup = 327 | material "Raspberry syrup" Syrup 328 | 329 | 330 | raspberryLiqueur : Material 331 | raspberryLiqueur = 332 | material "Raspberry liqueur" Liqueur 333 | 334 | 335 | orange : Material 336 | orange = 337 | material "Orange" Fruit 338 | 339 | 340 | eggYolk : Material 341 | eggYolk = 342 | material "Egg yolk" Other 343 | 344 | 345 | eggWhite : Material 346 | eggWhite = 347 | material "Egg white" Other 348 | 349 | 350 | champagne : Material 351 | champagne = 352 | material3 "Champagne" Base sparklingWine 353 | 354 | 355 | tequila : Material 356 | tequila = 357 | material "Tequila" Spirit 358 | 359 | 360 | rum : Material 361 | rum = 362 | material "Rum" Spirit 363 | 364 | 365 | whiteRum : Material 366 | whiteRum = 367 | material3 "White rum" Spirit rum 368 | 369 | 370 | goldRum : Material 371 | goldRum = 372 | material3 "Gold rum" Spirit rum 373 | 374 | 375 | demeraraRum : Material 376 | demeraraRum = 377 | material3 "Demerara rum" Spirit rum 378 | 379 | 380 | darkRum : Material 381 | darkRum = 382 | material3 "Dark rum" Spirit rum 383 | 384 | 385 | limeJuice : Material 386 | limeJuice = 387 | material "Lime juice" Juice 388 | 389 | 390 | cream : Material 391 | cream = 392 | material "Cream" Other 393 | 394 | 395 | brownCremeDeCacao : Material 396 | brownCremeDeCacao = 397 | material "Brown crème de cacao" Liqueur 398 | 399 | 400 | whiteCremeDeCacao : Material 401 | whiteCremeDeCacao = 402 | material "White crème de cacao" Liqueur 403 | 404 | 405 | lightCream : Material 406 | lightCream = 407 | material "Light cream" Other 408 | 409 | 410 | orangeFlowerWater : Material 411 | orangeFlowerWater = 412 | material "Orange flower water" Other 413 | 414 | 415 | vanillaExtract : Material 416 | vanillaExtract = 417 | material "Vanilla extract" Bitters 418 | 419 | 420 | cola : Material 421 | cola = 422 | material "Cola" Soda 423 | 424 | 425 | nutmeg : Material 426 | nutmeg = 427 | material "Nutmeg" Seasoning 428 | 429 | 430 | lemonJuice : Material 431 | lemonJuice = 432 | material "Lemon juice" Juice 433 | 434 | 435 | vodka : Material 436 | vodka = 437 | material "Vodka" Spirit 438 | 439 | 440 | gingerBeer : Material 441 | gingerBeer = 442 | material "Ginger beer" Soda 443 | 444 | 445 | gingerAle : Material 446 | gingerAle = 447 | material "Ginger ale" Soda 448 | 449 | 450 | prosecco : Material 451 | prosecco = 452 | material3 "Prosecco" Base sparklingWine 453 | 454 | 455 | mint : Material 456 | mint = 457 | material "Mint" Seasoning 458 | 459 | 460 | peachPuree : Material 461 | peachPuree = 462 | material "Peach purée" Other 463 | 464 | 465 | coffeeLiqueur : Material 466 | coffeeLiqueur = 467 | material "Coffee liqueur" Liqueur 468 | 469 | 470 | lilletBlanc : Material 471 | lilletBlanc = 472 | material "Lillet blanc" Fortified 473 | 474 | 475 | kinaLillet : Material 476 | kinaLillet = 477 | material "Kina lillet" Fortified 478 | 479 | 480 | greenCremeDeMenthe : Material 481 | greenCremeDeMenthe = 482 | material "Green crème de menthe" Liqueur 483 | 484 | 485 | cremeDeCassis : Material 486 | cremeDeCassis = 487 | material "Crème de cassis" Liqueur 488 | 489 | 490 | amaretto : Material 491 | amaretto = 492 | material "Amaretto" Liqueur 493 | 494 | 495 | olive : Material 496 | olive = 497 | material "Olive" Fruit 498 | 499 | 500 | wine : Material 501 | wine = 502 | material "Wine" Base 503 | 504 | 505 | dryWhiteWine : Material 506 | dryWhiteWine = 507 | material3 "Dry white wine" Base wine 508 | 509 | 510 | sparklingWine : Material 511 | sparklingWine = 512 | material "Sparkling wine" Base 513 | 514 | 515 | peachSchnapps : Material 516 | peachSchnapps = 517 | material "Peach schnapps" Liqueur 518 | 519 | 520 | cherryLiqueur : Material 521 | cherryLiqueur = 522 | material "Cherry liqueur" Liqueur 523 | 524 | 525 | domBenedictine : Material 526 | domBenedictine = 527 | material "DOM Bénédictine" Liqueur 528 | 529 | 530 | oliveJuice : Material 531 | oliveJuice = 532 | material "Olive juice" Juice 533 | 534 | 535 | cranberryJuice : Material 536 | cranberryJuice = 537 | material "Cranberry juice" Juice 538 | 539 | 540 | grapefruitJuice : Material 541 | grapefruitJuice = 542 | material "Grapefruit juice" Juice 543 | 544 | 545 | tomatoJuice : Material 546 | tomatoJuice = 547 | material "Tomato juice" Juice 548 | 549 | 550 | pepper : Material 551 | pepper = 552 | material "Pepper" Seasoning 553 | 554 | 555 | salt : Material 556 | salt = 557 | material "Salt" Seasoning 558 | 559 | 560 | celerySalt : Material 561 | celerySalt = 562 | material "Celery salt" Seasoning 563 | 564 | 565 | sugar : Material 566 | sugar = 567 | material "Sugar" Seasoning 568 | 569 | 570 | simpleSyrup : Material 571 | simpleSyrup = 572 | material "Simple syrup" Syrup 573 | 574 | 575 | caneSugar : Material 576 | caneSugar = 577 | material3 "Cane sugar" Seasoning sugar 578 | 579 | 580 | powderedSugar : Material 581 | powderedSugar = 582 | material3 "Powdered sugar" Seasoning sugar 583 | 584 | 585 | aperol : Material 586 | aperol = 587 | material "Aperol" Liqueur 588 | 589 | 590 | galliano : Material 591 | galliano = 592 | material "Galliano" Liqueur 593 | 594 | 595 | pisco : Material 596 | pisco = 597 | material3 "Pisco" Spirit brandy 598 | 599 | 600 | orgeatSyrup : Material 601 | orgeatSyrup = 602 | material "Orgeat (almond) syrup" Syrup 603 | 604 | 605 | cinnamonSyrup : Material 606 | cinnamonSyrup = 607 | material "Cinnamon syrup" Syrup 608 | 609 | 610 | agaveNectar : Material 611 | agaveNectar = 612 | material "Agave nectar" Syrup 613 | 614 | 615 | coconutCream : Material 616 | coconutCream = 617 | material "Coconut cream" Other 618 | 619 | 620 | espresso : Material 621 | espresso = 622 | material "Espresso" Other 623 | 624 | 625 | coffee : Material 626 | coffee = 627 | material "Coffee" Other 628 | 629 | 630 | worcestershireSauce : Material 631 | worcestershireSauce = 632 | material "Worcestershire sauce" Other 633 | 634 | 635 | irishCream : Material 636 | irishCream = 637 | material "Irish cream liqueur" Liqueur 638 | 639 | 640 | falernum : Material 641 | falernum = 642 | material "Falernum" Liqueur 643 | 644 | 645 | tabasco : Material 646 | tabasco = 647 | material "Tabasco" Other 648 | 649 | 650 | celery : Material 651 | celery = 652 | material "Celery" Other 653 | 654 | 655 | greenChartreuse : Material 656 | greenChartreuse = 657 | material "Green Chartreuse" Liqueur 658 | 659 | 660 | cremeDeViolette : Material 661 | cremeDeViolette = 662 | material "Crème de violette" Liqueur 663 | 664 | 665 | cubanAguardiente : Material 666 | cubanAguardiente = 667 | material3 "Cuban aguardiente" Spirit rum 668 | 669 | 670 | honeySyrup : Material 671 | honeySyrup = 672 | material "Honey syrup" Syrup 673 | 674 | 675 | honey : Material 676 | honey = 677 | material "Honey" Other 678 | 679 | 680 | grapefruitSoda : Material 681 | grapefruitSoda = 682 | material "Grapefruit soda" Soda 683 | 684 | 685 | amaroNonino : Material 686 | amaroNonino = 687 | material "Amaro Nonino" Liqueur 688 | 689 | 690 | blendedScotchWhiskey : Material 691 | blendedScotchWhiskey = 692 | material3 "Blended Scotch whiskey" Spirit whiskey 693 | 694 | 695 | islaySingleMaltScotch : Material 696 | islaySingleMaltScotch = 697 | material3 "Islay Single Malt Scotch whiskey" Spirit whiskey 698 | 699 | 700 | ginger : Material 701 | ginger = 702 | material "Ginger" Other 703 | 704 | 705 | candiedGinger : Material 706 | candiedGinger = 707 | material "Candied ginger" Other 708 | 709 | 710 | elderflowerSyrup : Material 711 | elderflowerSyrup = 712 | material "Elderflower syrup" Syrup 713 | 714 | 715 | grappa : Material 716 | grappa = 717 | material3 "Grappa" Spirit brandy 718 | 719 | 720 | whiteGrape : Material 721 | whiteGrape = 722 | material "White grape" Fruit 723 | 724 | 725 | mezcal : Material 726 | mezcal = 727 | material "Mezcal" Spirit 728 | 729 | 730 | overproofWhiteRum : Material 731 | overproofWhiteRum = 732 | material3 "Overproof white rum" Spirit rum 733 | 734 | 735 | yellowChartreuse : Material 736 | yellowChartreuse = 737 | material "Yellow Chartreuse" Liqueur 738 | 739 | 740 | redWine : Material 741 | redWine = 742 | material3 "Red wine" Base wine 743 | 744 | 745 | redChiliPepper : Material 746 | redChiliPepper = 747 | material "Red chili pepper" Fruit 748 | 749 | 750 | chamomileSyrup : Material 751 | chamomileSyrup = 752 | material "Chamomile syrup" Syrup 753 | 754 | 755 | type Video 756 | = Epicurious String 757 | 758 | 759 | type alias Recipe = 760 | { name : String 761 | , ingredients : List Ingredient 762 | , description : String 763 | , glass : Glass 764 | , video : Maybe Video 765 | } 766 | 767 | 768 | recipes : List Recipe 769 | recipes = 770 | [ -- https://en.wikipedia.org/wiki/Toronto_(cocktail) 771 | { name = "Toronto" 772 | , ingredients = 773 | [ ingredient canadianWhiskey (CL 5.5) 774 | , ingredient fernetBranca (CL 0.75) 775 | , ingredient sugar (Tsp 0.25) 776 | , ingredient angosturaBitters (Dash 1) 777 | , ingredient orange (Slice 1) 778 | ] 779 | , description = """Stir in mixing glass with ice & strain. Garnish with orange slice.""" 780 | , glass = Cocktail 781 | , video = Nothing 782 | } 783 | 784 | -- https://en.wikipedia.org/wiki/French_75_(cocktail) 785 | -- https://iba-world.com/cocktails/french-75/ 786 | , { name = "French 75" 787 | , ingredients = 788 | [ ingredient gin (CL 3) 789 | , ingredient simpleSyrup (CL 1.5) 790 | , ingredient lemonJuice (CL 1.5) 791 | , ingredient champagne (CL 6) 792 | ] 793 | , description = """Pour all the ingredients, except Champagne, into a shaker. Shake well and strain into a Champagne flute. Top up with Champagne. Stir gently.""" 794 | , glass = ChampagneFlute 795 | , video = Just (Epicurious "29:55") 796 | } 797 | 798 | -- https://en.wikipedia.org/wiki/Rum_and_Coke 799 | -- https://iba-world.com/cocktails/cuba-libre/ 800 | , { name = "Cuba Libre" 801 | , ingredients = 802 | [ ingredient cola (CL 12) 803 | , ingredient whiteRum (CL 5) 804 | , ingredient limeJuice (CL 1) 805 | , ingredient lime (Wedge 1) 806 | ] 807 | , description = """Build all ingredients in a highball glass filled with ice. Garnish with lime wedge.""" 808 | , glass = Highball 809 | , video = Nothing 810 | } 811 | 812 | -- https://en.wikipedia.org/wiki/Moscow_mule 813 | -- https://iba-world.com/cocktails/moscow-mule/ 814 | , { name = "Moscow mule" 815 | , ingredients = 816 | [ ingredient vodka (CL 4.5) 817 | , ingredient gingerBeer (CL 12) 818 | , ingredient limeJuice (CL 1) 819 | , ingredient lime (Slice 1) 820 | ] 821 | , description = """Combine vodka and ginger beer in a highball glass filled with ice. Add lime juice. Stir gently. Garnish with a lime slice.""" 822 | , glass = CopperMug 823 | , video = Just (Epicurious "22:42") 824 | } 825 | 826 | -- https://en.wikipedia.org/wiki/Mimosa_(cocktail) 827 | -- https://iba-world.com/cocktails/mimosa/ 828 | , { name = "Mimosa" 829 | , ingredients = 830 | [ ingredient champagne (CL 7.5) 831 | , ingredient oj (CL 7.5) 832 | , optionalIngredient orange (Custom "twist") 833 | ] 834 | , description = """Ensure both ingredients are well chilled, then mix into the glass. Garnish with orange twist (optional).""" 835 | , glass = ChampagneFlute 836 | , video = Nothing 837 | } 838 | 839 | -- https://en.wikipedia.org/wiki/Bellini_(cocktail) 840 | -- https://iba-world.com/cocktails/bellini/ 841 | , { name = "Bellini" 842 | , ingredients = 843 | [ ingredient prosecco (CL 10) 844 | , ingredient peachPuree (CL 5) 845 | ] 846 | , description = """Pour peach purée into chilled glass, add sparkling wine. Stir gently.""" 847 | , glass = ChampagneFlute 848 | , video = Nothing 849 | } 850 | 851 | -- https://en.wikipedia.org/wiki/Black_Russian 852 | -- https://iba-world.com/cocktails/black-russian/ 853 | , { name = "Black russian" 854 | , ingredients = 855 | [ ingredient vodka (CL 5) 856 | , ingredient coffeeLiqueur (CL 2) 857 | ] 858 | , description = """Pour the ingredients into an old fashioned glass filled with ice cubes. Stir gently.""" 859 | , glass = OldFashioned 860 | , video = Nothing 861 | } 862 | 863 | -- https://en.wikipedia.org/wiki/Caipirinha 864 | -- https://iba-world.com/cocktails/caipirinha/ 865 | , { name = "Caipirinha" 866 | , ingredients = 867 | [ ingredient cachaca (CL 6) 868 | , ingredient lime (Whole 1) 869 | , ingredient caneSugar (Tsp 4) 870 | ] 871 | , description = """Place small lime wedges from one lime and sugar into old fashioned glass and muddle (mash the two ingredients together using a muddler or a wooden spoon). Fill the glass with ice and add the Cachaça. Use vodka instead of cachaça for a caipiroska; rum instead of cachaça for a caipirissima;""" 872 | , glass = OldFashioned 873 | , video = Just (Epicurious "35:50") 874 | } 875 | 876 | -- https://iba-world.com/cocktails/mojito/ 877 | , { name = "Mojito" 878 | , ingredients = 879 | [ ingredient whiteRum (CL 4.5) 880 | , ingredient limeJuice (CL 2) 881 | , ingredient mint (Sprig 6) 882 | , ingredient caneSugar (Tsp 2) 883 | , ingredient lemon (Slice 1) 884 | , ingredient sodaWater (Splash 1) 885 | ] 886 | , description = """Muddle mint springs with sugar and lime juice. Add splash of soda water and fill glass with cracked ice. Pour rum and top with soda water. Garnish with sprig of mint leaves and lemon slice. Serve with straw.""" 887 | , glass = Collins 888 | , video = Just (Epicurious "28:10") 889 | } 890 | 891 | -- https://iba-world.com/new-era-drinks/dark-n-stormy/ 892 | , { name = "Dark ’n’ Stormy" 893 | , ingredients = 894 | [ ingredient darkRum (CL 6) 895 | , ingredient gingerBeer (CL 10) 896 | , ingredient lime (Wedge 1) 897 | ] 898 | , description = """Fill glass with ice, add rum and top with ginger beer. Garnish with lime wedge.""" 899 | , glass = Highball 900 | , video = Just (Epicurious "26:53") 901 | } 902 | 903 | -- 'NEW ERA DRINKS' ----------------------------- 904 | -- https://iba-world.com/new-era-drinks/bramble-2/ 905 | , { name = "Bramble" 906 | , ingredients = 907 | [ ingredient gin (CL 4) 908 | , ingredient lemonJuice (CL 1.5) 909 | , ingredient simpleSyrup (CL 1) 910 | , ingredient cremeDeMure (CL 1.5) 911 | , ingredient lemon (Slice 1) 912 | , ingredient blackberry (Whole 2) 913 | ] 914 | , description = """Fill glass with crushed ice. Build gin, lemon juice and simple syrup over. Stir, and then pour blackberry liqueur over in a circular fashion to create marbling effect. Garnish with two blackberries and lemon slice.""" 915 | , glass = OldFashioned 916 | , video = Just (Epicurious "19:10") 917 | } 918 | , { name = "French martini" 919 | , ingredients = 920 | [ ingredient vodka (CL 4) 921 | , ingredient raspberryLiqueur (CL 1.5) 922 | , ingredient pineappleJuice (CL 1) 923 | , ingredient lemon (Custom "peel") 924 | ] 925 | , description = """Pour all ingredients into shaker with ice cubes. Shake well and strain into a chilled cocktail glass. Squeeze oil from lemon peel onto the drink.""" 926 | , glass = Cocktail 927 | , video = Nothing 928 | } 929 | 930 | -- https://iba-world.com/new-era-drinks/kamikaze-2/ 931 | , { name = "Kamikaze" 932 | , ingredients = 933 | [ ingredient vodka (CL 3) 934 | , ingredient tripleSec (CL 3) 935 | , ingredient limeJuice (CL 3) 936 | , ingredient lime (Slice 1) 937 | ] 938 | , description = """Shake all ingredients together with ice. Strain into glass. Garnish with lime slice.""" 939 | , glass = Cocktail 940 | , video = Nothing 941 | } 942 | 943 | -- https://iba-world.com/new-era-drinks/lemon-drop-martini/ 944 | , { name = "Lemon drop martini" 945 | , ingredients = 946 | [ ingredient vodka (CL 2.5) 947 | , ingredient tripleSec (CL 2) 948 | , ingredient lemonJuice (CL 1.5) 949 | , ingredient sugar None 950 | , ingredient lime (Slice 1) 951 | ] 952 | , description = """Shake and strain into a chilled cocktail glass rimmed with sugar, garnish with a slice of lemon.""" 953 | , glass = Cocktail 954 | , video = Nothing 955 | } 956 | 957 | -- https://iba-world.com/cocktails/vesper-2/ 958 | , { name = "Vesper" 959 | , ingredients = 960 | [ ingredient gin (CL 4.5) 961 | , ingredient vodka (CL 1.5) 962 | , ingredient lilletBlanc (CL 0.75) 963 | , ingredient lemon (Custom "zest") 964 | ] 965 | , description = """Shake and strain into a chilled cocktail glass. Add the garnish.""" 966 | , glass = Cocktail 967 | , video = Just (Epicurious "23:30") 968 | } 969 | 970 | -- https://en.wikipedia.org/wiki/Boulevardier_(cocktail) 971 | -- https://iba-world.com/iba-official-cocktails/boulevardier/ 972 | , { name = "Boulevardier" 973 | , ingredients = 974 | [ ingredient bourbonWhiskey (CL 4.5) 975 | , ingredient sweetRedVermouth (CL 3) 976 | , ingredient campari (CL 3) 977 | , ingredient orange (Custom "peel") 978 | , optionalIngredient lemon (Custom "zest") 979 | ] 980 | , description = """Pour all ingredients into mixing glass with ice cubes. Stir well. Strain into chilled cocktail glass. Garnish with a orange zest, optionally a lemon zest.""" 981 | , glass = OldFashioned 982 | , video = Just (Epicurious "8:15") 983 | } 984 | 985 | -- 'THE UNFORGETTABLES' -------------------------------------------- 986 | -- https://en.wikipedia.org/wiki/Alexander_(cocktail) 987 | -- https://iba-world.com/iba-official-cocktails/alexander/ 988 | , { name = "Alexander" 989 | , ingredients = 990 | [ ingredient cognac (CL 3) 991 | , ingredient brownCremeDeCacao (CL 3) 992 | , ingredient lightCream (CL 3) 993 | , ingredient nutmeg None 994 | ] 995 | , description = """Shake all ingredients with ice and strain contents into a cocktail glass. Sprinkle ground nutmeg on top and serve.""" 996 | , glass = Cocktail 997 | , video = Nothing 998 | } 999 | 1000 | -- https://en.wikipedia.org/wiki/Americano_(cocktail) 1001 | -- https://iba-world.com/iba-official-cocktails/americano/ 1002 | , { name = "Americano" 1003 | , ingredients = 1004 | [ ingredient campari (CL 3) 1005 | , ingredient sweetRedVermouth (CL 3) 1006 | , ingredient sodaWater (Splash 1) 1007 | , ingredient orange (Slice 0.5) 1008 | , ingredient lemon (Custom "twist") 1009 | ] 1010 | , description = """Pour the Campari and vermouth over ice into a highball glass, add a splash of soda water and garnish with half orange slice and a lemon twist.""" 1011 | , glass = Highball 1012 | , video = Just (Epicurious "33:41") 1013 | } 1014 | 1015 | -- https://en.wikipedia.org/wiki/Angel_Face_(cocktail) 1016 | -- https://iba-world.com/iba-official-cocktails/angel-face/ 1017 | , { name = "Angel face" 1018 | , ingredients = 1019 | [ ingredient gin (CL 3) 1020 | , ingredient apricotBrandy (CL 3) 1021 | , ingredient calvados (CL 3) 1022 | ] 1023 | , description = """Shake all ingredients with ice and strain contents into a cocktail glass.""" 1024 | , glass = Cocktail 1025 | , video = Nothing 1026 | } 1027 | 1028 | -- https://en.wikipedia.org/wiki/Aviation_(cocktail) 1029 | -- https://iba-world.com/iba-official-cocktails/aviation/ 1030 | , { name = "Aviation" 1031 | , ingredients = 1032 | [ ingredient gin (CL 4.5) 1033 | , ingredient lemonJuice (CL 1.5) 1034 | , ingredient maraschino (CL 1.5) 1035 | , ingredient cremeDeViolette (Tsp 1) 1036 | , ingredient cherry (Whole 1) 1037 | ] 1038 | , description = """Add all ingredients into cocktail shaker filled with ice. Shake well and strain into cocktail glass. Garnish with a cherry.""" 1039 | , glass = Cocktail 1040 | , video = Nothing 1041 | } 1042 | 1043 | -- https://en.wikipedia.org/wiki/Bacardi_cocktail 1044 | -- https://iba-world.com/iba-official-cocktails/bacardi/ 1045 | , { name = "Bacardi cocktail" 1046 | , ingredients = 1047 | [ ingredient whiteRum (CL 4.5) 1048 | , ingredient limeJuice (CL 2) 1049 | , ingredient grenadine (CL 1) 1050 | ] 1051 | , description = """Shake together with ice. Strain into glass and serve.""" 1052 | , glass = Cocktail 1053 | , video = Nothing 1054 | } 1055 | 1056 | -- https://en.wikipedia.org/wiki/Between_the_Sheets_(cocktail) 1057 | -- https://iba-world.com/iba-official-cocktails/between-the-sheets/ 1058 | , { name = "Between the sheets" 1059 | , ingredients = 1060 | [ ingredient whiteRum (CL 3) 1061 | , ingredient cognac (CL 3) 1062 | , ingredient tripleSec (CL 3) 1063 | , ingredient lemonJuice (CL 2) 1064 | ] 1065 | , description = """Pour all ingredients into shaker with ice cubes, shake, strain into chilled cocktail glass.""" 1066 | , glass = Cocktail 1067 | , video = Nothing 1068 | } 1069 | 1070 | -- https://en.wikipedia.org/wiki/Casino_(cocktail) 1071 | -- https://iba-world.com/iba-official-cocktails/casino/ 1072 | , { name = "Casino" 1073 | , ingredients = 1074 | [ ingredient oldTomGin (CL 4) 1075 | , ingredient maraschino (CL 1) 1076 | , ingredient orangeBitters (CL 1) 1077 | , ingredient lemonJuice (CL 1) 1078 | , ingredient lemon (Custom "twist") 1079 | , ingredient cherry (Whole 1) 1080 | ] 1081 | , description = """Pour all ingredients into shaker with ice cubes, shake well. Strain into chilled cocktail glass and garnish with a lemon twist and a marachino cherry.""" 1082 | , glass = Cocktail 1083 | , video = Nothing 1084 | } 1085 | 1086 | -- https://en.wikipedia.org/wiki/Clover_Club_Cocktail 1087 | -- https://iba-world.com/iba-official-cocktails/clover-club/ 1088 | , { name = "Clover club" 1089 | , ingredients = 1090 | [ ingredient gin (CL 4.5) 1091 | , ingredient lemonJuice (CL 1.5) 1092 | , ingredient raspberrySyrup (CL 1.5) 1093 | , ingredient eggWhite FewDrops 1094 | , ingredient raspberry (Whole 2) 1095 | ] 1096 | , description = """Pour all ingredients into cocktail shaker filled with ice. Shake well. Strain into cocktail glass. Garnish with fresh raspberries.""" 1097 | , glass = Cocktail 1098 | , video = Nothing 1099 | } 1100 | 1101 | -- https://en.wikipedia.org/wiki/Daiquiri 1102 | -- https://iba-world.com/iba-official-cocktails/daiquiri/ 1103 | , { name = "Daiquiri" 1104 | , ingredients = 1105 | [ ingredient whiteRum (CL 4.5) 1106 | , ingredient limeJuice (CL 2.5) 1107 | , ingredient simpleSyrup (CL 1.5) 1108 | ] 1109 | , description = """Pour all ingredients into shaker with ice cubes. Shake well. Double Strain in chilled cocktail glass.""" 1110 | , glass = Cocktail 1111 | , video = Just (Epicurious "26:04") 1112 | } 1113 | 1114 | -- https://en.wikipedia.org/wiki/Derby_(cocktail) 1115 | -- https://iba-world.com/iba-official-cocktails/derby/ 1116 | , { name = "Derby" 1117 | , ingredients = 1118 | [ ingredient gin (CL 6) 1119 | , ingredient peachBitters (Drop 2) 1120 | , ingredient mint (Custom "leaves") 1121 | ] 1122 | , description = """Pour all ingredients into a mixing glass with ice. Stir. Strain into a cocktail glass. Garnish with fresh mint leaves in the drink.""" 1123 | , glass = Cocktail 1124 | , video = Nothing 1125 | } 1126 | 1127 | -- https://en.wikipedia.org/wiki/Martini_(cocktail) 1128 | -- https://iba-world.com/iba-official-cocktails/dry-martini/ 1129 | , { name = "Dry martini" 1130 | , ingredients = 1131 | [ ingredient gin (CL 6) 1132 | , ingredient dryVermouth (CL 1) 1133 | , ingredient lemon (Custom "peel") 1134 | , ingredient olive (Whole 1) 1135 | ] 1136 | , description = """Straight: Pour all ingredients into mixing glass with ice cubes. Stir well. Strain into chilled martini cocktail glass. Squeeze oil from lemon peel onto the drink, or garnish with olive.""" 1137 | , glass = Cocktail 1138 | , video = Just (Epicurious "13:39") 1139 | } 1140 | 1141 | -- https://en.wikipedia.org/wiki/Fizz_(cocktail)#Gin_fizz 1142 | -- https://iba-world.com/iba-official-cocktails/gin-fizz/ 1143 | , { name = "Gin fizz" 1144 | , ingredients = 1145 | [ ingredient gin (CL 4.5) 1146 | , ingredient lemonJuice (CL 3) 1147 | , ingredient simpleSyrup (CL 1) 1148 | , ingredient sodaWater (CL 8) 1149 | , ingredient lemon (Slice 1) 1150 | ] 1151 | , description = """Shake all ingredients with ice cubes, except soda water. Pour into tumbler. Top with soda water. Garnish with lemon slice.""" 1152 | , glass = OldFashioned 1153 | , video = Nothing 1154 | } 1155 | 1156 | -- https://en.wikipedia.org/wiki/John_Collins_(cocktail) 1157 | -- https://iba-world.com/iba-official-cocktails/john-collins/ 1158 | , { name = "John collins" 1159 | , ingredients = 1160 | [ ingredient gin (CL 4.5) 1161 | , ingredient lemonJuice (CL 3) 1162 | , ingredient simpleSyrup (CL 1.5) 1163 | , ingredient sodaWater (CL 6) 1164 | , ingredient angosturaBitters (Dash 1) 1165 | , ingredient lemon (Slice 1) 1166 | , ingredient cherry (Whole 1) 1167 | ] 1168 | , description = """Pour all ingredients directly into highball glass filled with ice. Stir gently. Garnish with lemon slice and maraschino cherry. Add a dash of Angostura bitters.""" 1169 | , glass = Collins 1170 | , video = Nothing 1171 | } 1172 | 1173 | -- https://en.wikipedia.org/wiki/Manhattan_(cocktail) 1174 | -- https://iba-world.com/iba-official-cocktails/manhattan/ 1175 | , { name = "Manhattan" 1176 | , ingredients = 1177 | [ ingredient ryeWhiskey (CL 5) 1178 | , ingredient sweetRedVermouth (CL 2) 1179 | , ingredient angosturaBitters (Dash 1) 1180 | , ingredient cherry (Whole 1) 1181 | ] 1182 | , description = """Pour all ingredients into mixing glass with ice cubes. Stir well. Strain into chilled cocktail glass. Garnish with cocktail cherry.""" 1183 | , glass = Cocktail 1184 | , video = Just (Epicurious "2:54") 1185 | } 1186 | 1187 | -- https://iba-world.com/iba-official-cocktails/mary-pickford/ 1188 | -- https://en.wikipedia.org/wiki/Mary_Pickford_(cocktail) 1189 | , { name = "Mary pickford" 1190 | , ingredients = 1191 | [ ingredient whiteRum (CL 6) 1192 | , ingredient pineappleJuice (CL 6) 1193 | , ingredient grenadine (CL 1) 1194 | , ingredient maraschino (CL 1) 1195 | ] 1196 | , description = """Shake and strain into a chilled large cocktail glass""" 1197 | , glass = Cocktail 1198 | , video = Nothing 1199 | } 1200 | 1201 | -- https://en.wikipedia.org/wiki/Monkey_Gland 1202 | -- https://iba-world.com/iba-official-cocktails/monkey-gland/ 1203 | , { name = "Monkey gland" 1204 | , ingredients = 1205 | [ ingredient gin (CL 5) 1206 | , ingredient oj (CL 3) 1207 | , ingredient absinthe (Drop 2) 1208 | , ingredient grenadine (Drop 2) 1209 | ] 1210 | , description = """Shake well over ice cubes in a shaker, strain into a chilled cocktail glass.""" 1211 | , glass = Cocktail 1212 | , video = Nothing 1213 | } 1214 | 1215 | -- https://iba-world.com/iba-official-cocktails/negroni/ 1216 | , { name = "Negroni" 1217 | , ingredients = 1218 | [ ingredient gin (CL 3) 1219 | , ingredient sweetRedVermouth (CL 3) 1220 | , ingredient campari (CL 3) 1221 | , ingredient orange (Slice 0.5) 1222 | ] 1223 | , description = """Pour all ingredients directly into old-fashioned glass filled with ice. Stir gently. Garnish with half orange slice.""" 1224 | , glass = OldFashioned 1225 | , video = Just (Epicurious "15:52") 1226 | } 1227 | 1228 | -- https://en.wikipedia.org/wiki/Old_fashioned_(cocktail) 1229 | -- https://iba-world.com/iba-official-cocktails/old-fashioned/ 1230 | , { name = "Old fashioned" 1231 | , ingredients = 1232 | [ ingredient whiskey (CL 4.5) 1233 | , ingredient angosturaBitters (Dash 2) 1234 | , ingredient sugar (Cube 1) 1235 | , ingredient water FewDashes 1236 | , ingredient orange (Slice 1) 1237 | , ingredient cherry (Whole 1) 1238 | ] 1239 | , description = """Place sugar cube in old-fashioned glass and saturate with bitters, add a dash of plain water. Muddle until dissolve. Fill the glass with ice cubes and add whiskey. Garnish with orange slice and a cocktail cherry.""" 1240 | , glass = OldFashioned 1241 | , video = Just (Epicurious "1:45") 1242 | } 1243 | 1244 | -- https://en.wikipedia.org/wiki/Paradise_(cocktail) 1245 | -- https://iba-world.com/iba-official-cocktails/paradise/ 1246 | , { name = "Paradise" 1247 | , ingredients = 1248 | [ ingredient gin (CL 3.5) 1249 | , ingredient apricotBrandy (CL 2) 1250 | , ingredient oj (CL 1.5) 1251 | ] 1252 | , description = """Shake together over ice. Strain into cocktail glass and serve chilled.""" 1253 | , glass = Cocktail 1254 | , video = Nothing 1255 | } 1256 | 1257 | -- https://en.wikipedia.org/wiki/Planter%27s_punch 1258 | -- https://iba-world.com/iba-official-cocktails/planters-punch/ 1259 | , { name = "Planter’s punch" 1260 | , ingredients = 1261 | [ ingredient darkRum (CL 4.5) 1262 | , ingredient oj (CL 3.5) 1263 | , ingredient pineappleJuice (CL 3.5) 1264 | , ingredient lemonJuice (CL 2) 1265 | , ingredient grenadine (CL 1) 1266 | , ingredient simpleSyrup (CL 1) 1267 | , ingredient angosturaBitters (Dash 3) 1268 | , ingredient cherry (Whole 1) 1269 | , ingredient pineapple (Slice 1) 1270 | ] 1271 | , description = """Pour all ingredients, except the bitters, into shaker filled with ice. Shake well. Pour into large glass, filled with ice. Add dash Angostura bitters. Garnish with cocktail cherry and pineapple.""" 1272 | , glass = Cocktail 1273 | , video = Nothing 1274 | } 1275 | 1276 | -- https://en.wikipedia.org/wiki/Porto_flip 1277 | -- https://iba-world.com/iba-official-cocktails/porto-flip/ 1278 | , { name = "Porto flip" 1279 | , ingredients = 1280 | [ ingredient brandy (CL 1.5) 1281 | , ingredient pport (CL 4.5) 1282 | , ingredient eggYolk (CL 1) 1283 | , ingredient nutmeg None 1284 | ] 1285 | , description = """Pour all ingredients into cocktail shaker filled with ice. Shake well. Strain into cocktail glass. Sprinkle with fresh ground nutmeg.""" 1286 | , glass = Cocktail 1287 | , video = Nothing 1288 | } 1289 | 1290 | -- https://en.wikipedia.org/wiki/Fizz_(cocktail)#Ramos_gin_fizz 1291 | -- https://iba-world.com/iba-official-cocktails/ramos-fizz/ 1292 | , { name = "Ramos fizz" 1293 | , ingredients = 1294 | [ ingredient gin (CL 4.5) 1295 | , ingredient cream (CL 6) 1296 | , ingredient simpleSyrup (CL 3) 1297 | , ingredient limeJuice (CL 1.5) 1298 | , ingredient lemonJuice (CL 1.5) 1299 | , ingredient eggWhite (Whole 1) 1300 | , ingredient orangeFlowerWater (Dash 3) 1301 | , ingredient vanillaExtract (Drop 2) 1302 | , ingredient sodaWater None 1303 | ] 1304 | , description = """Pour all ingredients (except soda) in a mixing glass, dry shake (no ice) for two minutes, add ice and hard shake for another minute. Strain into a highball glass without ice, top with soda.""" 1305 | , glass = Highball 1306 | , video = Just (Epicurious "17:38") 1307 | } 1308 | 1309 | -- https://en.wikipedia.org/wiki/Rusty_Nail_(cocktail) 1310 | -- https://iba-world.com/iba-official-cocktails/rusty-nail/ 1311 | , { name = "Rusty nail" 1312 | , ingredients = 1313 | [ ingredient scotchWhiskey (CL 4.5) 1314 | , ingredient drambuie (CL 2.5) 1315 | , ingredient lemon (Custom "twist") 1316 | ] 1317 | , description = """Pour all ingredients directly into old-fashioned glass filled with ice. Stir gently. Garnish with a lemon twist.""" 1318 | , glass = OldFashioned 1319 | , video = Nothing 1320 | } 1321 | 1322 | -- https://en.wikipedia.org/wiki/Sazerac 1323 | -- https://iba-world.com/iba-official-cocktails/sazerac/ 1324 | , { name = "Sazerac" 1325 | , ingredients = 1326 | [ ingredient cognac (CL 5) 1327 | , ingredient absinthe (CL 1) 1328 | , ingredient sugar (Cube 1) 1329 | , ingredient peychaudsBitters (Dash 2) 1330 | , ingredient lemon (Custom "peel") 1331 | ] 1332 | , description = """Rinse a chilled old-fashioned glass with the absinthe, add crushed ice and set it aside. Stir the remaining ingredients over ice and set it aside. Discard the ice and any excess absinthe from the prepared glass, and strain the drink into the glass. Add the Lemon peel for garnish. Note: The original recipe changed after the American Civil War, rye whiskey substituted cognac as it became hard to obtain.""" 1333 | , glass = OldFashioned 1334 | , video = Just (Epicurious "5:45") 1335 | } 1336 | 1337 | -- https://en.wikipedia.org/wiki/Screwdriver_(cocktail) 1338 | -- https://iba-world.com/iba-official-cocktails/screwdriver/ 1339 | , { name = "Screwdriver" 1340 | , ingredients = 1341 | [ ingredient vodka (CL 5) 1342 | , ingredient oj (CL 10) 1343 | , ingredient orange (Slice 1) 1344 | ] 1345 | , description = """Pour all ingredients into a highball glass filled with ice. Stir gently. Garnish with an orange slice.""" 1346 | , glass = Highball 1347 | , video = Nothing 1348 | } 1349 | 1350 | -- https://iba-world.com/iba-official-cocktails/sidecar/ 1351 | , { name = "Sidecar" 1352 | , ingredients = 1353 | [ ingredient cognac (CL 5) 1354 | , ingredient tripleSec (CL 2) 1355 | , ingredient lemonJuice (CL 2) 1356 | ] 1357 | , description = """Pour all ingredients into cocktail shaker filled with ice. Shake well and strain into cocktail glass.""" 1358 | , glass = Cocktail 1359 | , video = Just (Epicurious "29:31") 1360 | } 1361 | 1362 | -- https://en.wikipedia.org/wiki/Stinger_(cocktail) 1363 | -- https://iba-world.com/iba-official-cocktails/stinger/ 1364 | , { name = "Stinger" 1365 | , ingredients = 1366 | [ ingredient cognac (CL 5) 1367 | , ingredient whiteCremeDeMenthe (CL 2) 1368 | ] 1369 | , description = """Pour in a mixing glass with ice, stir and strain into a cocktail glass. May also be served on rocks in a rocks glass.""" 1370 | , glass = Cocktail 1371 | , video = Nothing 1372 | } 1373 | 1374 | -- https://en.wikipedia.org/wiki/Tuxedo_(cocktail) 1375 | -- https://iba-world.com/iba-official-cocktails/tuxedo/ 1376 | , { name = "Tuxedo" 1377 | , ingredients = 1378 | [ ingredient oldTomGin (CL 3) 1379 | , ingredient dryVermouth (CL 3) 1380 | , ingredient maraschino (Tsp 0.5) 1381 | , ingredient absinthe (Tsp 0.25) 1382 | , ingredient orangeBitters (Dash 3) 1383 | , ingredient cherry (Whole 1) 1384 | , ingredient lemon (Custom "twist") 1385 | ] 1386 | , description = """Stir all ingredients with ice and strain into cocktail glass. Garnish with a cocktail cherry and a lemon zest twist.""" 1387 | , glass = Cocktail 1388 | , video = Nothing 1389 | } 1390 | 1391 | -- https://en.wikipedia.org/wiki/Whiskey_sour 1392 | -- https://iba-world.com/iba-official-cocktails/whiskey-sour/ 1393 | , { name = "Whiskey sour" 1394 | , ingredients = 1395 | [ ingredient bourbonWhiskey (CL 4.5) 1396 | , ingredient lemonJuice (CL 3) 1397 | , ingredient simpleSyrup (CL 1.5) 1398 | , optionalIngredient eggWhite (Dash 1) 1399 | , ingredient cherry (Whole 1) 1400 | , ingredient orange (Slice 0.5) 1401 | ] 1402 | , description = """Egg white is optional. Pour all ingredients into cocktail shaker filled with ice. Shake well (a little harder if using egg white). Strain in cocktail glass. Garnish with half orange slice and maraschino cherry.""" 1403 | , glass = OldFashioned 1404 | , video = Just (Epicurious "4:03") 1405 | } 1406 | 1407 | -- https://en.wikipedia.org/wiki/Sour_(cocktail)#White_Lady 1408 | -- https://iba-world.com/iba-official-cocktails/white-lady/ 1409 | , { name = "White lady" 1410 | , ingredients = 1411 | [ ingredient gin (CL 4) 1412 | , ingredient tripleSec (CL 3) 1413 | , ingredient lemonJuice (CL 2) 1414 | ] 1415 | , description = """Add all ingredients into cocktail shaker filled with ice. Shake well and strain into large cocktail glass.""" 1416 | , glass = Cocktail 1417 | , video = Nothing 1418 | } 1419 | 1420 | -- https://iba-world.com/cocktails/french-connection/ 1421 | , { name = "French connection" 1422 | , ingredients = 1423 | [ ingredient cognac (CL 3.5) 1424 | , ingredient amaretto (CL 3.5) 1425 | ] 1426 | , description = """Pour all ingredients directly into old fashioned glass filled with ice cubes. Stir gently.""" 1427 | , glass = OldFashioned 1428 | , video = Nothing 1429 | } 1430 | 1431 | -- https://iba-world.com/cocktails/mint-julep/ 1432 | , { name = "Mint julep" 1433 | , ingredients = 1434 | [ ingredient bourbonWhiskey (CL 6) 1435 | , ingredient mint (Sprig 5) 1436 | , ingredient water (Tsp 2) 1437 | , ingredient powderedSugar (Tsp 1) 1438 | ] 1439 | , description = """In steel cup gently muddle 4 mint sprigs with sugar and water. Fill the glass with cracked ice, add the Bourbon and stir well until the cup frosts. Garnish with a mint sprig.""" 1440 | , glass = SteelCup 1441 | , video = Just (Epicurious "12:05") 1442 | } 1443 | 1444 | -- https://en.wikipedia.org/wiki/White_Russian_(cocktail) 1445 | , { name = "White russian" 1446 | , ingredients = 1447 | [ ingredient vodka (CL 5) 1448 | , ingredient coffeeLiqueur (CL 2) 1449 | , ingredient cream (CL 3) 1450 | ] 1451 | , description = """Pour vodka and coffee liqueur into an old fashioned glass filled with ice cubes. Float fresh cream on the top and stir in slowly..""" 1452 | , glass = OldFashioned 1453 | , video = Nothing 1454 | } 1455 | 1456 | -- https://iba-world.com/cocktails/bloody-mary/ 1457 | , { name = "Bloody Mary" 1458 | , ingredients = 1459 | [ ingredient vodka (CL 4.5) 1460 | , ingredient tomatoJuice (CL 9) 1461 | , ingredient lemonJuice (CL 1.5) 1462 | , ingredient worcestershireSauce (Dash 2) 1463 | , ingredient tabasco None 1464 | , ingredient celerySalt None 1465 | , ingredient pepper None 1466 | , ingredient celery None 1467 | , ingredient lemon (Wedge 1) 1468 | ] 1469 | , description = """Stir gently all the ingredients in a mixing glass with ice. Add tabasco, celery salt, pepper to taste. Pour into rocks glass. Garnish with celery and lemon wedge. If requested served with ice, pour into highball glass.""" 1470 | , glass = Highball 1471 | , video = Nothing 1472 | } 1473 | 1474 | -- https://iba-world.com/cocktails/champagne-cocktail/ 1475 | , { name = "Champagne cocktail" 1476 | , ingredients = 1477 | [ ingredient champagne (CL 9) 1478 | , ingredient cognac (CL 1) 1479 | , ingredient angosturaBitters (Dash 2) 1480 | , ingredient sugar (Cube 1) 1481 | , optionalIngredient grandMarnier FewDrops 1482 | , ingredient orange (Custom "zest") 1483 | , ingredient cherry (Whole 1) 1484 | ] 1485 | , description = """Place the sugar cube with 2 dashes of bitters in a large Champagne glass, add the cognac. Optionally add a few drops of Grand Marnier. Pour gently chilled Champagne. Garnish with orange zest and cherry.""" 1486 | , glass = Cocktail 1487 | , video = Nothing 1488 | } 1489 | 1490 | -- https://iba-world.com/cocktails/kir/ 1491 | , { name = "Kir" 1492 | , ingredients = 1493 | [ ingredient dryWhiteWine (CL 9) 1494 | , ingredient cremeDeCassis (CL 1) 1495 | ] 1496 | , description = """Pour Crème de Cassis into glass, top up with white wine.""" 1497 | , glass = Wine 1498 | , video = Nothing 1499 | } 1500 | , { name = "Kir royal" 1501 | , ingredients = 1502 | [ ingredient champagne (CL 9) 1503 | , ingredient cremeDeCassis (CL 1) 1504 | ] 1505 | , description = """Pour Crème de Cassis into glass, top up with Champagne.""" 1506 | , glass = Wine 1507 | , video = Nothing 1508 | } 1509 | 1510 | -- https://iba-world.com/cocktails/long-island-iced-tea/ 1511 | , { name = "Long island iced tea" 1512 | , ingredients = 1513 | [ ingredient vodka (CL 1.5) 1514 | , ingredient tequila (CL 1.5) 1515 | , ingredient whiteRum (CL 1.5) 1516 | , ingredient gin (CL 1.5) 1517 | , ingredient cointreau (CL 1.5) 1518 | , ingredient lemonJuice (CL 2.5) 1519 | , ingredient simpleSyrup (CL 3) 1520 | , ingredient cola None 1521 | , ingredient lemon (Slice 1) 1522 | ] 1523 | , description = """Add all ingredients into highball glass filled with ice. Top with cola. Stir gently. Garnish with lemon slice.""" 1524 | , glass = Highball 1525 | , video = Nothing 1526 | } 1527 | 1528 | -- https://en.wikipedia.org/wiki/Mai_Tai 1529 | -- https://iba-world.com/cocktails/mai-tai/ 1530 | , { name = "Mai-tai" 1531 | , ingredients = 1532 | [ ingredient whiteRum (CL 3) -- “Amber Jamaican Rum” 1533 | , ingredient darkRum (CL 3) -- “Martinique Molasses Rhum” 1534 | , ingredient curacao (CL 1.5) 1535 | , ingredient orgeatSyrup (CL 1.5) 1536 | , ingredient limeJuice (CL 3) 1537 | , ingredient simpleSyrup (CL 0.75) 1538 | , ingredient pineapple (Custom "spear") 1539 | , ingredient mint (Custom "leaves") 1540 | , ingredient lime (Custom "peel") 1541 | ] 1542 | , description = """Add all ingredients into a shaker with ice. Shake and pour into a double rocks glass or an highball glass. Garnish with pineapple spear, mint leaves, and lime peel.""" 1543 | , glass = Highball 1544 | , video = Just (Epicurious "27:20") 1545 | } 1546 | 1547 | -- https://iba-world.com/cocktails/margarita/ 1548 | , { name = "Margarita" 1549 | , ingredients = 1550 | [ ingredient tequila (CL 5) 1551 | , ingredient tripleSec (CL 2) 1552 | , ingredient limeJuice (CL 1.5) 1553 | , optionalIngredient salt None 1554 | ] 1555 | , description = """Add all ingredients into a shaker with ice. Shake and strain into a chilled cocktail glass. Garnish with a half salt rim (optional).""" 1556 | , glass = Margarita 1557 | , video = Just (Epicurious "24:13") 1558 | } 1559 | 1560 | -- https://iba-world.com/new-era-drinks/tommys-margarita/ 1561 | , { name = "Tommy’s margarita" 1562 | , ingredients = 1563 | [ ingredient tequila (CL 4.5) 1564 | , ingredient limeJuice (CL 1.5) 1565 | , ingredient agaveNectar (Tsp 2) 1566 | ] 1567 | , description = """Shake and strain into a chilled cocktail glass.""" 1568 | , glass = Margarita 1569 | , video = Nothing 1570 | } 1571 | 1572 | -- https://iba-world.com/new-era-drinks/b52-2/ 1573 | , { name = "B52" 1574 | , ingredients = 1575 | [ ingredient coffeeLiqueur (CL 2) 1576 | , ingredient tripleSec (CL 2) 1577 | , ingredient irishCream (CL 2) 1578 | ] 1579 | , description = """Layer ingredients one at a time starting with coffee liqueur, followed by irish cream and top with triple sec. Flame the triple sec, serve while the flame is still on, accompanied with a straw on side plate.""" 1580 | , glass = OldFashioned 1581 | , video = Nothing 1582 | } 1583 | 1584 | -- https://en.wikipedia.org/wiki/Barracuda_(cocktail) 1585 | -- https://iba-world.com/new-era-drinks/barracuda/ 1586 | , { name = "Barracuda" 1587 | , ingredients = 1588 | [ ingredient goldRum (CL 4.5) 1589 | , ingredient galliano (CL 1.5) 1590 | , ingredient pineappleJuice (CL 6) 1591 | , ingredient limeJuice (Dash 1) 1592 | , ingredient prosecco None 1593 | ] 1594 | , description = """Shake together with ice. Strain into glass and serve.""" 1595 | , glass = Margarita 1596 | , video = Nothing 1597 | } 1598 | 1599 | -- https://iba-world.com/cocktails/corpse-reviver-2-all-day/ 1600 | , { name = "Corpse reviver #2" 1601 | , ingredients = 1602 | [ ingredient gin (CL 3) 1603 | , ingredient cointreau (CL 3) 1604 | , ingredient lilletBlanc (CL 3) 1605 | , ingredient lemonJuice (CL 3) 1606 | , ingredient absinthe (Dash 1) 1607 | , ingredient orange (Custom "zest") 1608 | ] 1609 | , description = """Pour all ingredients into shaker with ice. Shake well and strain in chilled cocktail glass. Garnish with orange zest.""" 1610 | , glass = Cocktail 1611 | , video = Just (Epicurious "16:10") 1612 | } 1613 | 1614 | -- https://iba-world.com/cocktails/cosmopolitan/ 1615 | , { name = "Cosmopolitan" 1616 | , ingredients = 1617 | [ ingredient vodka (CL 4) -- Vodka citron 1618 | , ingredient cointreau (CL 1.5) 1619 | , ingredient limeJuice (CL 1.5) 1620 | , ingredient cranberryJuice (CL 3) 1621 | , ingredient lemon (Custom "twist") 1622 | ] 1623 | , description = """Add all ingredients into cocktail shaker filled with ice. Shake well and strain into large cocktail glass. Garnish with lemon twist.""" 1624 | , glass = Cocktail 1625 | , video = Nothing 1626 | } 1627 | 1628 | -- https://iba-world.com/new-era-drinks/dirty-martini/ 1629 | , { name = "Dirty martini" 1630 | , ingredients = 1631 | [ ingredient vodka (CL 6) 1632 | , ingredient dryVermouth (CL 1) 1633 | , ingredient oliveJuice (CL 1) 1634 | , ingredient olive (Whole 1) 1635 | ] 1636 | , description = """Pour all ingredients into mixing glass with ice cubes. Stir well. Strain in chilled martini glass. Garnish with green olive.""" 1637 | , glass = Cocktail 1638 | , video = Nothing 1639 | } 1640 | 1641 | -- https://iba-world.com/new-era-drinks/espresso-martini/ 1642 | , { name = "Espresso martini" 1643 | , ingredients = 1644 | [ ingredient vodka (CL 5) 1645 | , ingredient coffeeLiqueur (CL 1) 1646 | , ingredient simpleSyrup None 1647 | , ingredient espresso (Custom "1 shot") 1648 | ] 1649 | , description = """Shake and strain into a chilled cocktail glass.""" 1650 | , glass = Cocktail 1651 | , video = Nothing 1652 | } 1653 | 1654 | -- https://iba-world.com/cocktails/golden-dream/ 1655 | , { name = "Golden dream" 1656 | , ingredients = 1657 | [ ingredient tripleSec (CL 2) 1658 | , ingredient galliano (CL 2) 1659 | , ingredient oj (CL 2) 1660 | , ingredient cream (CL 1) 1661 | ] 1662 | , description = """Pour all ingredients into shaker filled with ice. Shake briskly for few seconds. Strain into chilled cocktail glass.""" 1663 | , glass = Cocktail 1664 | , video = Nothing 1665 | } 1666 | 1667 | -- https://iba-world.com/cocktails/grasshopper/ 1668 | , { name = "Grasshopper" 1669 | , ingredients = 1670 | [ ingredient whiteCremeDeCacao (CL 2) 1671 | , ingredient greenCremeDeMenthe (CL 2) 1672 | , ingredient cream (CL 2) 1673 | , optionalIngredient mint (Custom "1 leave") 1674 | ] 1675 | , description = """Pour all ingredients into shaker filled with ice. Shake briskly for few seconds. Strain into chilled cocktail glass. Garnish with mint leaf (optional).""" 1676 | , glass = Cocktail 1677 | , video = Nothing 1678 | } 1679 | 1680 | -- https://iba-world.com/cocktails/hemingway-special/ 1681 | , { name = "Hemingway special" 1682 | , ingredients = 1683 | [ ingredient whiteRum (CL 6) -- IBA doesn’t specify “white” 1684 | , ingredient grapefruitJuice (CL 4) 1685 | , ingredient maraschino (CL 1.5) 1686 | , ingredient limeJuice (CL 1.5) 1687 | ] 1688 | , description = """Pour all ingredients into a shaker with ice. Shake well and strain into a large cocktail glass.""" 1689 | , glass = Cocktail 1690 | , video = Nothing 1691 | } 1692 | 1693 | -- https://iba-world.com/cocktails/horses-neck/ 1694 | , { name = "Horse’s neck" 1695 | , ingredients = 1696 | [ ingredient cognac (CL 4) 1697 | , ingredient gingerAle (CL 12) 1698 | , ingredient angosturaBitters FewDashes 1699 | , ingredient lemon (Custom "peel") 1700 | ] 1701 | , description = """Pour Cognac and ginger ale directly into highball glass with ice cubes. Stir gently. If preferred, add dashes of Angostura Bitter. Garnish with rind of one lemon spiral.""" 1702 | , glass = Collins 1703 | , video = Nothing 1704 | } 1705 | 1706 | -- https://iba-world.com/cocktails/irish-coffee/ 1707 | , { name = "Irish coffee" 1708 | , ingredients = 1709 | [ ingredient irishWhiskey (CL 5) 1710 | , ingredient coffee (CL 12) 1711 | , ingredient cream (CL 5) 1712 | , ingredient sugar (Tsp 1) 1713 | ] 1714 | , description = """Warm black coffee is poured into a pre-heated Irish coffee glass. Whiskey and at least one teaspoon of sugar is added and stirred until dissolved. Fresh thick chilled cream is carefully poured over the back of a spoon held just above the surface of the coffee. The layer of cream will float on the coffee without mixing. Plain sugar can be replaced with sugar syrup.""" 1715 | , glass = IrishCoffeeMug 1716 | , video = Nothing 1717 | } 1718 | , { name = "Tom collins" 1719 | , ingredients = 1720 | [ ingredient oldTomGin (CL 4.5) 1721 | , ingredient lemonJuice (CL 3) 1722 | , ingredient simpleSyrup (CL 1.5) 1723 | , ingredient sodaWater (CL 6) 1724 | , ingredient angosturaBitters (Dash 1) 1725 | , ingredient lemon (Slice 1) 1726 | , ingredient cherry (Whole 1) 1727 | ] 1728 | , description = """Pour all ingredients directly into highball glass filled with ice. Stir gently. Garnish with lemon slice and maraschino cherry. Add a dash of Angostura bitters.""" 1729 | , glass = Collins 1730 | , video = Just (Epicurious "17:13") 1731 | } 1732 | 1733 | -- https://iba-world.com/cocktails/pina-colada/ 1734 | , { name = "Pina Colada" 1735 | , ingredients = 1736 | [ ingredient whiteRum (CL 5) 1737 | , ingredient coconutCream (CL 3) 1738 | , ingredient pineappleJuice (CL 5) 1739 | , ingredient pineapple (Slice 1) 1740 | , ingredient cherry (Whole 1) 1741 | ] 1742 | , description = """Blend all the ingredients with ice in a electric blender, pour into a large glass and serve with straws. Garnish with a slice of pineapple with a cocktail cherry. 4 slices of fresh pineapple can be used instead of juice. Historically a few drops of fresh lime juice was added to taste.""" 1743 | , glass = Hurricane 1744 | , video = Nothing 1745 | } 1746 | 1747 | -- https://iba-world.com/new-era-drinks/pisco-sour/ 1748 | -- https://iba-world.com/cocktails/pisco-sour-2/ 1749 | , { name = "Pisco Sour" 1750 | , ingredients = 1751 | [ ingredient pisco (CL 4.5) 1752 | , ingredient simpleSyrup (CL 2) 1753 | , ingredient lemonJuice (CL 3) 1754 | , ingredient eggWhite (Whole 1) 1755 | , ingredient angosturaBitters (Dash 1) 1756 | ] 1757 | , description = """Shake and strain into a chilled champagne flute. Dash some Angostura bitters on top.""" 1758 | , glass = ChampagneFlute 1759 | , video = Just (Epicurious "35:00") 1760 | } 1761 | 1762 | -- https://iba-world.com/new-era-drinks/russian-spring-punch/ 1763 | , { name = "Russian spring punch" 1764 | , ingredients = 1765 | [ ingredient vodka (CL 2.5) 1766 | , ingredient lemonJuice (CL 2.5) 1767 | , ingredient cremeDeCassis (CL 1.5) 1768 | , ingredient simpleSyrup (CL 1) 1769 | , ingredient sparklingWine None 1770 | , ingredient lemon (Slice 1) 1771 | , ingredient blackberry (Whole 1) 1772 | ] 1773 | , description = """Shake the ingredients and pour into highball glass. Top with Sparkling wine. Garnish with a lemon slice and a blackberry.""" 1774 | , glass = Highball 1775 | , video = Nothing 1776 | } 1777 | 1778 | -- https://en.wikipedia.org/wiki/Sea_Breeze_(cocktail) 1779 | -- https://iba-world.com/cocktails/sea-breeze/ 1780 | , { name = "Sea breeze" 1781 | , ingredients = 1782 | [ ingredient vodka (CL 4) 1783 | , ingredient cranberryJuice (CL 12) 1784 | , ingredient grapefruitJuice (CL 3) 1785 | , ingredient orange (Custom "zest") 1786 | , ingredient cherry (Whole 1) 1787 | ] 1788 | , description = """Build all ingredients in a highball glass filled with ice. Garnish with an orange zest and cherry.""" 1789 | , glass = Highball 1790 | , video = Nothing 1791 | } 1792 | 1793 | -- https://en.wikipedia.org/wiki/Sex_on_the_Beach 1794 | -- https://iba-world.com/cocktails/sex-on-the-beach/ 1795 | , { name = "Sex on the beach" 1796 | , ingredients = 1797 | [ ingredient vodka (CL 4) 1798 | , ingredient peachSchnapps (CL 2) 1799 | , ingredient oj (CL 4) 1800 | , ingredient cranberryJuice (CL 4) 1801 | , ingredient grapefruitJuice (CL 3) 1802 | , ingredient orange (Slice 0.5) 1803 | ] 1804 | , description = """Build all ingredients in a highball glass filled with ice. Garnish with an orange zest and cherry.""" 1805 | , glass = Highball 1806 | , video = Nothing 1807 | } 1808 | 1809 | -- https://en.wikipedia.org/wiki/Singapore_Sling 1810 | -- https://iba-world.com/cocktails/singapore-sling/ 1811 | , { name = "Singapore sling" 1812 | , ingredients = 1813 | [ ingredient gin (CL 3) 1814 | , ingredient cherryLiqueur (CL 1.5) 1815 | , ingredient cointreau (CL 0.75) 1816 | , ingredient domBenedictine (CL 0.75) 1817 | , ingredient pineappleJuice (CL 12) 1818 | , ingredient limeJuice (CL 1.5) 1819 | , ingredient grenadine (CL 1) 1820 | , ingredient angosturaBitters (Dash 1) 1821 | , ingredient cherry (Whole 1) 1822 | , ingredient pineapple (Slice 1) 1823 | ] 1824 | , description = """Pour all ingredients into cocktail shaker filled with ice cubes. Shake well. Strain into Hurricane glass. Garnish with pineapple and maraschino cherry.""" 1825 | , glass = Hurricane 1826 | , video = Nothing 1827 | } 1828 | 1829 | -- https://en.wikipedia.org/wiki/Spritz_Veneziano 1830 | -- https://iba-world.com/cocktails/tequila-sunrise/ 1831 | , { name = "Tequila sunrise" 1832 | , ingredients = 1833 | [ ingredient tequila (CL 4.5) 1834 | , ingredient oj (CL 9) 1835 | , ingredient grenadine (CL 1.5) 1836 | , ingredient orange (Slice 0.5) 1837 | ] 1838 | , description = """Pour tequila and orange juice directly into highball glass filled with ice cubes. Add the grenadine syrup to create chromatic effect (sunrise), do not stir. Garnish with half orange slice or an orange zest.""" 1839 | , glass = Collins 1840 | , video = Nothing 1841 | } 1842 | 1843 | -- https://en.wikipedia.org/wiki/Yellow_Bird_(cocktail) 1844 | -- https://iba-world.com/new-era-drinks/yellow-bird/ 1845 | -- https://open.spotify.com/track/5ced30fVo8XlDpmoVnUZqp 1846 | , { name = "Yellow bird" 1847 | , ingredients = 1848 | [ ingredient whiteRum (CL 3) 1849 | , ingredient galliano (CL 1.5) 1850 | , ingredient tripleSec (CL 1.5) 1851 | , ingredient limeJuice (CL 1.5) 1852 | ] 1853 | , description = """Shake and strain into a chilled cocktail glass.""" 1854 | , glass = Cocktail 1855 | , video = Nothing 1856 | } 1857 | 1858 | -- https://en.wikipedia.org/wiki/Zombie_(cocktail) 1859 | -- https://iba-world.com/cocktails/zombie/ 1860 | , { name = "Zombie" 1861 | , ingredients = 1862 | [ ingredient darkRum (CL 4.5) -- Jamaican dark rum 1863 | , ingredient goldRum (CL 4.5) -- Gold Puerto Rican rum 1864 | , ingredient demeraraRum (CL 3) -- Rum from guyana 1865 | , ingredient limeJuice (CL 2) 1866 | , ingredient falernum (CL 1.5) 1867 | , ingredient grapefruitJuice (CL 1) -- With cinnamon syrup making up “Donn’s mix”. 1868 | , ingredient cinnamonSyrup (CL 0.5) 1869 | , ingredient grenadine (Tsp 1) 1870 | , ingredient angosturaBitters (Dash 1) 1871 | , ingredient absinthe (Dash 2) 1872 | , ingredient mint (Custom "leaves") 1873 | ] 1874 | , description = """Add all ingredients into an electric blender with 170 grams of cracked ice. With pulse bottom blend for a few seconds. Serve in a tall tumbler glass. Garnish with mint leaves.""" 1875 | , glass = ZombieGlass 1876 | , video = Nothing 1877 | } 1878 | 1879 | -- https://iba-world.com/iba-official-cocktails/brandy-crusta/ 1880 | , { name = "Brandy crusta" 1881 | , ingredients = 1882 | [ ingredient brandy (CL 5.25) 1883 | , ingredient lemonJuice (CL 1.5) 1884 | , ingredient maraschino (CL 0.75) 1885 | , ingredient curacao (Tsp 1) 1886 | , ingredient simpleSyrup (Tsp 1) 1887 | , ingredient aromaticBitters (Dash 2) 1888 | , ingredient orange (Slice 1) 1889 | , ingredient powderedSugar (Tsp 1) 1890 | ] 1891 | , description = """Mix together all ingredients with ice cubes in a mixing glass and strain into prepared slim cocktail glass. Rub a slice of orange (or lemon) around the rim of the glass and dip it in pulverized white sugar, so that the sugar will adhere to the edge of the glass. Carefully curling place the orange/lemon peel around the inside of the glass.""" 1892 | , glass = Cocktail 1893 | , video = Nothing 1894 | } 1895 | 1896 | -- https://en.wikipedia.org/wiki/Hanky-Panky_cocktail 1897 | -- https://iba-world.com/iba-official-cocktails/hanky-panky/ 1898 | , { name = "Hanky panky" 1899 | , ingredients = 1900 | [ ingredient londonDryGin (CL 4.5) 1901 | , ingredient sweetRedVermouth (CL 4.5) 1902 | , ingredient fernetBranca (CL 0.75) 1903 | , ingredient orange (Custom "zest") 1904 | ] 1905 | , description = """Pour all ingredients into mixing glass with ice cubes. Stir well. Strain into chilled cocktail glass. Garnish with orange zest.""" 1906 | , glass = Cocktail 1907 | , video = Nothing 1908 | } 1909 | 1910 | -- https://en.wikipedia.org/wiki/Last_Word_(cocktail) 1911 | -- https://iba-world.com/iba-official-cocktails/last-word/ 1912 | , { name = "Last word" 1913 | , ingredients = 1914 | [ ingredient gin (CL 2.25) 1915 | , ingredient greenChartreuse (CL 2.25) 1916 | , ingredient maraschino (CL 2.25) 1917 | , ingredient limeJuice (CL 2.25) 1918 | ] 1919 | , description = """Add all ingredients into a cocktail shaker. Shake with ice and strain into a chilled cocktail glass.""" 1920 | , glass = Cocktail 1921 | , video = Just (Epicurious "21:18") 1922 | } 1923 | 1924 | -- https://en.wikipedia.org/wiki/Martinez_(cocktail) 1925 | -- https://iba-world.com/iba-official-cocktails/martinez/ 1926 | , { name = "Martinez" 1927 | , ingredients = 1928 | [ ingredient londonDryGin (CL 4.5) 1929 | , ingredient sweetRedVermouth (CL 4.5) 1930 | , ingredient maraschino (Tsp 1) 1931 | , ingredient orangeBitters (Dash 2) 1932 | , ingredient lemon (Custom "zest") 1933 | ] 1934 | , description = """Pour all ingredients into mixing glass with ice cubes. Stir well. Strain into chilled cocktail glass. Garnish with Lemon zest.""" 1935 | , glass = Cocktail 1936 | , video = Just (Epicurious "14:20") 1937 | } 1938 | 1939 | -- https://iba-world.com/iba-official-cocktails/vieux-carre/ 1940 | , { name = "Vieux carré" 1941 | , ingredients = 1942 | [ ingredient ryeWhiskey (CL 3) 1943 | , ingredient cognac (CL 3) 1944 | , ingredient sweetRedVermouth (CL 3) 1945 | , ingredient domBenedictine (Tsp 1) 1946 | , ingredient peychaudsBitters (Dash 2) 1947 | , ingredient orange (Custom "zest") 1948 | , ingredient cherry (Whole 1) 1949 | ] 1950 | , description = """Pour all ingredients into mixing glass with ice cubes. Stir well. Strain into chilled cocktail glass. Garnish with orange zest and maraschino cherry.""" 1951 | , glass = Cocktail 1952 | , video = Just (Epicurious "31:04") 1953 | } 1954 | 1955 | -- https://en.wikipedia.org/wiki/Bee's_Knees_(cocktail) 1956 | -- https://iba-world.com/new-era-drinks/bees-knees/ 1957 | , { name = "Bee’s knees" 1958 | , ingredients = 1959 | [ ingredient londonDryGin (CL 5.25) -- Just “dry gin” 1960 | , ingredient honeySyrup (Tsp 2) 1961 | , ingredient lemonJuice (CL 2.25) 1962 | , ingredient oj (CL 2.25) 1963 | , optionalIngredient orange (Custom "zest") 1964 | ] 1965 | , description = """Stir honey with lemon and orange juices until it dissolves, add gin and shake with ice. Strain into a chilled cocktail glass. Optionally garnish with a lemon or orange zest.""" 1966 | , glass = Cocktail 1967 | , video = Just (Epicurious "20:46") 1968 | } 1969 | 1970 | -- https://iba-world.com/news/cachanchara/ 1971 | , { name = "Cachanchara" 1972 | , ingredients = 1973 | [ ingredient cubanAguardiente (CL 6) 1974 | , ingredient lemonJuice (CL 1.5) 1975 | , ingredient honey (CL 1.5) 1976 | , ingredient water (CL 5) 1977 | , ingredient lime (Wedge 1) 1978 | ] 1979 | , description = """Mix honey with water and lime juice and spread the mixture on the bottom and sides of the glass. Add cracked ice, and then the rum. End by energetically stirring from bottom to top. Garnish with Lime wedge.""" 1980 | , glass = OldFashioned 1981 | , video = Nothing 1982 | } 1983 | 1984 | -- https://iba-world.com/new-era-drinks/fernandito/ 1985 | , { name = "Fernandito" 1986 | , ingredients = 1987 | [ ingredient fernetBranca (CL 5) 1988 | , ingredient cola None 1989 | ] 1990 | , description = """Pour the Fernet Branca into a double old fashioned glass with ice, fill the glass up with Cola. Gently stir.""" 1991 | , glass = Highball -- Double old fashioned? 1992 | , video = Nothing 1993 | } 1994 | 1995 | -- https://iba-world.com/new-era-drinks/old-cuban/ 1996 | , { name = "Old cuban" 1997 | , ingredients = 1998 | [ ingredient rum (CL 4.5) -- “Aged rum” 1999 | , ingredient sparklingWine (CL 6) -- “Brut champagne or prosecco” 2000 | , ingredient limeJuice (CL 2.25) 2001 | , ingredient simpleSyrup (CL 3) 2002 | , ingredient angosturaBitters (Dash 2) 2003 | , ingredient mint (Sprig 3) 2004 | ] 2005 | , description = """Pour all ingredients into cocktail shaker except the wine, shake well with ice, strain into chilled elegant cocktail glass. Top up with the sparkling wine. Garnish with mint springs.""" 2006 | , glass = Cocktail 2007 | , video = Nothing 2008 | } 2009 | 2010 | -- https://iba-world.com/new-era-drinks/paloma/ 2011 | , { name = "Paloma" 2012 | , ingredients = 2013 | [ ingredient tequila (CL 5) -- “100% Agave Tequila” 2014 | , ingredient grapefruitSoda (CL 10) -- Pink Grapefruit Soda 2015 | , ingredient limeJuice (Tsp 2) 2016 | , ingredient salt None 2017 | , ingredient lime (Slice 1) 2018 | ] 2019 | , description = """Pour the tequila into a highball glass, squeeze the lime juice. Add ice and salt, fill up pink grapefruit soda. Stir gently. Garnish with a slice of lime.""" 2020 | , glass = Highball 2021 | , video = Just (Epicurious "24:47") 2022 | } 2023 | 2024 | -- https://iba-world.com/new-era-drinks/paper-plane/ 2025 | , { name = "Paper plane" 2026 | , ingredients = 2027 | [ ingredient bourbonWhiskey (CL 3) 2028 | , ingredient amaroNonino (CL 3) 2029 | , ingredient aperol (CL 3) 2030 | , ingredient lemonJuice (CL 3) 2031 | ] 2032 | , description = """Pour all ingredients into cocktail shaker, shake well with ice, strain into chilled cocktail glass.""" 2033 | , glass = Cocktail 2034 | , video = Nothing 2035 | } 2036 | 2037 | -- https://iba-world.com/new-era-drinks/penicillin/ 2038 | , { name = "Penicillin" 2039 | , ingredients = 2040 | [ ingredient blendedScotchWhiskey (CL 6) -- “Blended Scotch Whisky” 2041 | , ingredient islaySingleMaltScotch (CL 0.75) -- “Lagavulin 16y” 2042 | , ingredient lemonJuice (CL 2.25) 2043 | , ingredient honeySyrup (CL 2.25) 2044 | , ingredient ginger (Slice 3) 2045 | , ingredient candiedGinger (Whole 1) 2046 | ] 2047 | , description = """Muddle fresh ginger in a shaker and add the remaining ingredients, except for the Islay single malt whiskey. Fill the shaker with ice and shake. Double-strain into a chilled old fashioned glass with ice. Float the single malt whisky on top. Garnish with a candied ginger.""" 2048 | , glass = OldFashioned 2049 | , video = Nothing 2050 | } 2051 | 2052 | -- https://iba-world.com/new-era-drinks/southside/ 2053 | , { name = "Southside" 2054 | , ingredients = 2055 | [ ingredient londonDryGin (CL 6) 2056 | , ingredient lemonJuice (CL 3) 2057 | , ingredient simpleSyrup (CL 1.5) 2058 | , ingredient mint (Sprig 2) 2059 | , optionalIngredient eggWhite (CL 3) 2060 | ] 2061 | , description = """Egg white optional. Pour all ingredients into a cocktail shaker, shake well with ice, double-strain into chilled cocktail glass. If egg white is used shake vigorously. Garnish with mint springs.""" 2062 | , glass = Cocktail 2063 | , video = Nothing 2064 | } 2065 | 2066 | -- https://iba-world.com/new-era-drinks/spicy-fifty/ 2067 | , { name = "Spicy fifty" 2068 | , ingredients = 2069 | [ ingredient vodka (CL 5) 2070 | , ingredient elderflowerSyrup (CL 1.5) -- “Elder flower cordial” 2071 | , ingredient lemonJuice (CL 1.5) 2072 | , ingredient honeySyrup (CL 1) -- “Monin Honey Syrup” 2073 | , ingredient vanillaExtract (Drop 1) -- “Vodka Vanilla” is used by IBA, so I instead added a drop of extract. 2074 | , ingredient redChiliPepper None 2075 | ] 2076 | , description = """Pour all ingredients (including 2 thin slices of pepper) into a cocktail shaker, shake well with ice, double-strain into chilled cocktail glass. Garnish with a red chili pepper.""" 2077 | , glass = Cocktail 2078 | , video = Nothing 2079 | } 2080 | 2081 | -- https://iba-world.com/new-era-drinks/suffering-bastard/ 2082 | , { name = "Suffering bastard" 2083 | , ingredients = 2084 | [ ingredient brandy (CL 3) 2085 | , ingredient gin (CL 3) 2086 | , ingredient limeJuice (CL 1.5) 2087 | , ingredient angosturaBitters (Dash 2) 2088 | , ingredient gingerBeer None 2089 | , ingredient mint (Sprig 1) 2090 | , optionalIngredient orange (Slice 1) 2091 | ] 2092 | , description = """Pour all ingredients into cocktail shaker except the ginger beer, shake well with ice, Pour unstrained into a Collins glass or in the original S. Bastard mug and top up with ginger beer. Garnish with mint sprig and optionally an orange slice as well.""" 2093 | , glass = Collins 2094 | , video = Nothing 2095 | } 2096 | 2097 | -- https://iba-world.com/new-era-drinks/tipperary/ 2098 | , { name = "Tipperary" 2099 | , ingredients = 2100 | [ ingredient irishWhiskey (CL 5) 2101 | , ingredient sweetRedVermouth (CL 2.5) 2102 | , ingredient greenChartreuse (CL 1.5) 2103 | , ingredient angosturaBitters (Dash 2) 2104 | , ingredient orange (Slice 1) 2105 | ] 2106 | , description = """Pour all ingredients into mixing glass with ice cubes. Stir well. Strain into chilled martini cocktail glass. Garnish with a slice of orange.""" 2107 | , glass = Cocktail 2108 | , video = Nothing 2109 | } 2110 | 2111 | -- https://iba-world.com/new-era-drinks/trinidad-sour/ 2112 | , { name = "Trinidad sour" 2113 | , ingredients = 2114 | [ ingredient angosturaBitters (CL 4.5) 2115 | , ingredient orgeatSyrup (CL 3) 2116 | , ingredient lemonJuice (CL 2.25) 2117 | , ingredient ryeWhiskey (CL 1.5) 2118 | ] 2119 | , description = """Pour all ingredients into mixing glass with ice cubes. Stir well. Strain into chilled cocktail glass.""" 2120 | , glass = Cocktail 2121 | , video = Nothing 2122 | } 2123 | 2124 | -- https://iba-world.com/new-era-drinks/ve-n-to/ 2125 | , { name = "Ve.n.to" 2126 | , ingredients = 2127 | [ ingredient grappa (CL 4.5) -- “White smooth grappa” 2128 | , ingredient lemonJuice (CL 2.25) 2129 | , ingredient honeySyrup (CL 1.5) -- “Honey mix” 2130 | , ingredient chamomileSyrup (CL 1.5) -- “Chamomile cordial” 2131 | , ingredient honeySyrup (CL 1.5) -- “Honey mix” 2132 | , optionalIngredient eggWhite (CL 3) 2133 | , ingredient lemon (Custom "zest") 2134 | , ingredient whiteGrape (Whole 3) 2135 | ] 2136 | , description = """Egg white optional. Pour all ingredients into the shaker. Shake vigorously with ice. Strain into a chilled small tumbler glass filled with ice. Garnish with lemon zest and white grapes.""" 2137 | , glass = OldFashioned 2138 | , video = Nothing 2139 | } 2140 | 2141 | -- https://iba-world.com/new-era-drinks/illegal/ 2142 | , { name = "Illegal" 2143 | , ingredients = 2144 | [ ingredient mezcal (CL 3) -- “Espadin Mezcal” 2145 | , ingredient overproofWhiteRum (CL 1.5) -- “Jamaica Overproof White Rum” 2146 | , ingredient limeJuice (CL 2.25) 2147 | , ingredient falernum (CL 1.5) 2148 | , ingredient simpleSyrup (CL 1.5) 2149 | , ingredient maraschino (Tsp 1) 2150 | , optionalIngredient eggWhite (CL 3) 2151 | ] 2152 | , description = """Egg white optional. Pour all ingredients into the shaker. Shake vigorously with ice. Strain into a chilled cocktail glass, or “on the rocks” in a traditional clay or terracotta mug.""" 2153 | , glass = Cocktail 2154 | , video = Nothing 2155 | } 2156 | 2157 | -- https://iba-world.com/new-era-drinks/naked-and-famous/ 2158 | , { name = "Naked and famous" 2159 | , ingredients = 2160 | [ ingredient mezcal (CL 2.25) 2161 | , ingredient yellowChartreuse (CL 2.25) 2162 | , ingredient aperol (CL 2.25) 2163 | , ingredient limeJuice (CL 2.25) 2164 | ] 2165 | , description = """Pour all ingredients into cocktail shaker, shake well with ice, strain into chilled cocktail glass.""" 2166 | , glass = Cocktail 2167 | , video = Nothing 2168 | } 2169 | 2170 | -- https://iba-world.com/new-era-drinks/new-york-sour/ 2171 | , { name = "New York sour" 2172 | , ingredients = 2173 | [ ingredient ryeWhiskey (CL 6) -- Or bourbon 2174 | , ingredient lemonJuice (CL 3) 2175 | , ingredient eggWhite (CL 3) 2176 | , ingredient simpleSyrup (CL 2.25) 2177 | , ingredient redWine (CL 1.5) -- “Shiraz or Malbech” 2178 | , ingredient lemon (Custom "zest") 2179 | , ingredient cherry (Whole 1) 2180 | ] 2181 | , description = """Bourbon can be used instead of rye. Pour all ingredients into the shaker. Shake vigorously with ice. Strain into a chilled rocks glass filled with ice. Float the wine on top. Garnish with lemon or orange zest with cherry.""" 2182 | , glass = OldFashioned 2183 | , video = Nothing 2184 | } 2185 | 2186 | -- https://iba-world.com/new-era-drinks/spritz/ 2187 | -- Also the 'Aperol spritz' 2188 | , { name = "Spritz" 2189 | , ingredients = 2190 | [ ingredient prosecco (CL 9) 2191 | , ingredient aperol (CL 6) 2192 | , ingredient sodaWater (Splash 1) 2193 | , ingredient orange (Slice 1) 2194 | ] 2195 | , description = """Build all ingredients into a wine glass filled with ice. Stir gently. Garnish with a slice of orange.""" 2196 | , glass = Wine 2197 | , video = Just (Epicurious "33:11") 2198 | } 2199 | , { name = "Gimlet" 2200 | , ingredients = 2201 | [ ingredient limeJuice (CL 2) 2202 | , ingredient simpleSyrup (CL 2) 2203 | , ingredient gin (CL 6) 2204 | ] 2205 | , description = """Pour all ingredients into a cocktail strainer, shake well with ice, strain into chilled cocktail glass.""" 2206 | , glass = Cocktail 2207 | , video = Just (Epicurious "15:00") 2208 | } 2209 | , { name = "Martini" 2210 | , ingredients = 2211 | [ ingredient gin (CL 6) 2212 | , ingredient dryVermouth (CL 6) 2213 | , optionalIngredient lemon (Custom "twist") 2214 | ] 2215 | , description = """Mix gin and vermouth in a chilled pint glass, stir with ice. Strain into a champagne coupe.""" 2216 | , glass = ChampagneCoupe 2217 | , video = Just (Epicurious "13:39") 2218 | } 2219 | , { name = "Vodka martini" 2220 | , ingredients = 2221 | [ ingredient vodka (CL 8) 2222 | , ingredient dryVermouth (Drop 1) 2223 | , optionalIngredient lemon (Custom "twist") 2224 | , optionalIngredient olive (Whole 1) 2225 | ] 2226 | , description = """Mix vodka and dry vermouth in a pint glass. The amount of vermouth used varies and can be as little as a drop. Stir with ice. Optionally add olive brine to make a dirty martini.""" 2227 | , glass = Cocktail 2228 | , video = Just (Epicurious "21:53") 2229 | } 2230 | 2231 | -- https://en.wikipedia.org/wiki/20th_Century_(cocktail) 2232 | , { name = "20th century" 2233 | , ingredients = 2234 | [ ingredient gin (CL 4.5) 2235 | , ingredient lemonJuice (CL 2) 2236 | , ingredient whiteCremeDeCacao (CL 1.5) 2237 | , ingredient kinaLillet (CL 2) 2238 | , ingredient lemon (Custom "twist") 2239 | ] 2240 | , description = """Combine ingredients in a cocktail shaker, shake with ice, strain into a cocktail glass.""" 2241 | , glass = Cocktail 2242 | , video = Just (Epicurious "19:51") 2243 | } 2244 | , { name = "Artillery" 2245 | , ingredients = 2246 | [ ingredient sweetRedVermouth (Tsp 1.5) 2247 | , ingredient gin (CL 4.5) 2248 | , ingredient angosturaBitters (Dash 2) 2249 | ] 2250 | , description = """Stir all ingredients with ice and strain into a cocktail glass.""" 2251 | , glass = Cocktail 2252 | , video = Nothing 2253 | } 2254 | , { name = "Whiskey fix" 2255 | , ingredients = 2256 | [ ingredient lemonJuice (CL 2) 2257 | , ingredient simpleSyrup (CL 2) 2258 | , ingredient whiskey (CL 6) 2259 | , ingredient lemon (Wedge 1) 2260 | , ingredient cherry (Whole 1) 2261 | ] 2262 | , description = """Shake in a cocktail shaker with a small piece of ice. Drain into the glass and top with crushed ice and garnish with a lemon wedge and a luxardo cherry.""" 2263 | , glass = OldFashioned 2264 | , video = Just (Epicurious "7:11") 2265 | } 2266 | ] 2267 | 2268 | 2269 | 2270 | -- Presbyterian 8:58 2271 | -- Blinker 9:58 2272 | -- Improved Whiskey Cocktail 10:41 2273 | -- Monte Carlo 11:25 2274 | -- Gin Rickey 15:34 2275 | -- Aviation Number 1 16:37 2276 | -- Headless Horseman 23:19 2277 | -- Mexican Firing Squad Special 25:30 2278 | -- Hemingway Daiquiri 26:32 2279 | -- Hotel Nacional Special 28:47 2280 | -- Brandy Alexander 30:45 2281 | -- Pink Lady 31:38 2282 | -- Delmonico 32:13 2283 | -- Jack Rose 32:42 2284 | -- Pan American Clipper 33:01 2285 | -- Champagne Cocktail 34:03 2286 | -- Bamboo 34:26 2287 | -------------------------------------------------------------------------------- /front/src/Main.elm: -------------------------------------------------------------------------------- 1 | port module Main exposing (Msg(..), main, setDark, update, view) 2 | 3 | import Browser 4 | import Browser.Dom as Dom 5 | import Browser.Events 6 | import Browser.Navigation as Nav 7 | import Data exposing (Glass(..), Ingredient, Material, MaterialType(..), Recipe, SuperMaterial(..), Video(..), recipes) 8 | import Element exposing (Element, alignTop, column, el, fill, height, html, padding, paddingEach, paragraph, row, shrink, spacing, text, width) 9 | import Element.Background 10 | import Element.Border as Border 11 | import Element.Events exposing (onClick) 12 | import Element.Font as Font exposing (bold, strike) 13 | import Element.Input as Input 14 | import Element.Region 15 | import Html exposing (details, iframe, label, option, select, summary) 16 | import Html.Attributes exposing (value) 17 | import Html.Events 18 | import Http 19 | import Json.Decode as Decode exposing (Decoder, bool, field, string) 20 | import Json.Encode as Encode 21 | import Quantity exposing (Quantity(..), Units(..), printQuantity) 22 | import Set 23 | import Set.Any exposing (AnySet) 24 | import Slug 25 | import Svg exposing (path, svg) 26 | import Svg.Attributes exposing (d, stroke, strokeWidth, viewBox) 27 | import Task 28 | import Url exposing (Url) 29 | 30 | 31 | 32 | -- Types 33 | 34 | 35 | type alias CachedMaterial = 36 | ( Int, Bool, Material ) 37 | 38 | 39 | type alias MaterialsGroup = 40 | { label : String 41 | , materials : List CachedMaterial 42 | } 43 | 44 | 45 | type alias MaterialSet = 46 | AnySet String Material 47 | 48 | 49 | type Mode 50 | = Normal 51 | | Grid 52 | 53 | 54 | type Sort 55 | = Feasibility 56 | | Alphabetical 57 | 58 | 59 | type Device 60 | = Desktop 61 | | Mobile 62 | 63 | 64 | type Tab 65 | = TMaterials 66 | | TCocktails 67 | | TDetail 68 | | TSettings 69 | 70 | 71 | type alias Model = 72 | { recipes : List Recipe 73 | , materials : List Material 74 | , selectedRecipe : Maybe Recipe 75 | , availableMaterials : MaterialSet 76 | , units : Units 77 | , mode : Mode 78 | , pedantic : Bool 79 | , sort : Sort 80 | , dark : Bool 81 | , email : String 82 | , syncing : Bool 83 | , sentEmail : Bool 84 | , countedMaterials : List MaterialsGroup 85 | , device : Device 86 | , tab : Tab 87 | , key : Nav.Key 88 | } 89 | 90 | 91 | type Msg 92 | = SelectRecipe Recipe 93 | | ToggleIngredient Material Bool 94 | | SetUnits String 95 | | SetMode Mode 96 | | SetSubsitute Bool 97 | | SetDark Bool 98 | | SetSort String 99 | | MoveUp 100 | | MoveDown 101 | | Ignored 102 | | GotOk (Result Http.Error Bool) 103 | | GotMagic (Result Http.Error Bool) 104 | | GotDevice Device 105 | | GotInventory (Result Http.Error (List String)) 106 | | GetMagicLink 107 | | SetEmail String 108 | | SetTab Tab 109 | | ChangedUrl Url 110 | | ClickedLink Browser.UrlRequest 111 | 112 | 113 | 114 | -- Icons 115 | 116 | 117 | icon : String -> Element.Element Msg 118 | icon pathSpec = 119 | svg 120 | [ Svg.Attributes.width "20", Svg.Attributes.height "20", Svg.Attributes.fill "none", viewBox "0 0 20 20" ] 121 | [ path [ d pathSpec, stroke "currentColor", strokeWidth "1" ] 122 | [] 123 | ] 124 | |> html 125 | 126 | 127 | glassIcon : Recipe -> Element.Element Msg 128 | glassIcon recipe = 129 | case recipe.glass of 130 | OldFashioned -> 131 | icon "M14 5H6v10c.582.209 2.06.5 4 .5s3.612-.291 4-.5V5zm-8 5h8" 132 | 133 | Cocktail -> 134 | icon "M9 9.5L5.5 4h9l-3.501 5.5v4.75H13v1.25H7v-1.25h2.056L9 9.5zm-2.5-4h7" 135 | 136 | Collins -> 137 | icon "M13 4H7v12.5c.5.25 1.5.5 3 .5s2.712-.24 3-.5V4zm-6 6.554l6-.054" 138 | 139 | CopperMug -> 140 | icon "M13 5H5v9.973c.593.213 2.024.527 4 .527s3.605-.314 4-.527V5zm0 2.5h3V12h-3" 141 | 142 | ChampagneFlute -> 143 | icon "M9.25 17v-5.616L8 7.5 8.5 3h3l.5 4.5-1.25 3.884V17H12v.63H8V17h1.25zM8 4.856h3.616" 144 | 145 | -- These are the same because they look so similar 146 | Highball -> 147 | icon "M13 4H6v11c.512.239 1.792.5 3.5.5s3.158-.261 3.5-.5V4zm-7 6h7" 148 | 149 | ZombieGlass -> 150 | icon "M13 4H6v11c.512.239 1.792.5 3.5.5s3.158-.261 3.5-.5V4zm-7 6h7" 151 | 152 | Margarita -> 153 | icon "M9.25 16.5V9L7.8 8l-.3-1.5L5 5.25 4.5 3h11L15 5.25 12.5 6.5 12.2 8l-1.45 1v7.5h1.582v.5H7.196v-.5H9.25z" 154 | 155 | Hurricane -> 156 | icon "M9.25 17.5V14L7.5 12.5 7 10l.5-1.75.25-1.75-.25-1.75L7 3h6l-.5 1.75-.25 1.75.25 1.75L13 10l-.5 2.5-1.75 1.5v3.5h2.05v.5H7.196v-.5H9.25z" 157 | 158 | IrishCoffeeMug -> 159 | icon "M9 15v-2l-1-1-1-.5V4h6v7.5l-1 .5-1 1v2l1.332.5v.5H7.196v-.5L9 15zm4-9.5h2.5V9L13 10M7 6.5h6" 160 | 161 | SteelCup -> 162 | icon "M14.5 4h-9L7 14.5c.5.25.75.5 3 .5 1.75 0 2.75-.25 3-.5L14.5 4zM6 6h8" 163 | 164 | Wine -> 165 | icon "M9.25 16.5v-6L7.8 9 7 7l1-4h4l1 4-.8 2-1.45 1.5v6h1.582v.5H7.196v-.5H9.25z" 166 | 167 | ChampagneCoupe -> 168 | icon "M9.25 16.5v-8L8 7 6 6 4.5 4.5V3h11v1.5l-1.5 2-2 .5-1.25 1.5v8h1.582v.5H7.196v-.5H9.25z" 169 | 170 | 171 | 172 | -- Constants 173 | 174 | 175 | white : Element.Color 176 | white = 177 | Element.rgb 229 227 224 178 | 179 | 180 | black : Element.Color 181 | black = 182 | Element.rgb 0 0 0 183 | 184 | 185 | blue : Element.Color 186 | blue = 187 | Element.rgb255 29 27 124 188 | 189 | 190 | lightBlue : Element.Color 191 | lightBlue = 192 | Element.rgb255 129 127 224 193 | 194 | 195 | edges : { top : Int, right : Int, bottom : Int, left : Int } 196 | edges = 197 | { top = 0 198 | , right = 0 199 | , bottom = 0 200 | , left = 0 201 | } 202 | 203 | 204 | selectCmd : Nav.Key -> Maybe Recipe -> Cmd Msg 205 | selectCmd key selected = 206 | case selected of 207 | Just r -> 208 | Cmd.batch 209 | [ Task.attempt (\_ -> Ignored) (Dom.focus (recipeSlug r)) 210 | , Nav.replaceUrl key ("#" ++ recipeSlug r) 211 | ] 212 | 213 | Nothing -> 214 | Cmd.none 215 | 216 | 217 | 218 | -- Application loop 219 | 220 | 221 | update : Msg -> Model -> ( Model, Cmd Msg ) 222 | update msg model = 223 | case msg of 224 | SelectRecipe recipe -> 225 | ( { model 226 | | selectedRecipe = 227 | if Just recipe == model.selectedRecipe then 228 | Nothing 229 | 230 | else 231 | Just recipe 232 | , tab = TDetail 233 | } 234 | , selectCmd model.key (Just recipe) 235 | ) 236 | 237 | SetUnits units -> 238 | ( { model 239 | | units = 240 | case units of 241 | "Cl" -> 242 | Cl 243 | 244 | "Oz" -> 245 | Oz 246 | 247 | "Ml" -> 248 | Ml 249 | 250 | _ -> 251 | Cl 252 | } 253 | , Cmd.none 254 | ) 255 | 256 | SetSort sort -> 257 | ( { model 258 | | sort = 259 | case sort of 260 | "Alphabetical" -> 261 | Alphabetical 262 | 263 | "Feasibility" -> 264 | Feasibility 265 | 266 | _ -> 267 | Alphabetical 268 | } 269 | |> deriveMaterials 270 | , Cmd.none 271 | ) 272 | 273 | MoveUp -> 274 | let 275 | selected = 276 | model.selectedRecipe 277 | |> Maybe.map (\r -> getNext (List.reverse model.recipes) r) 278 | |> Maybe.withDefault (List.head model.recipes) 279 | in 280 | ( { model 281 | | selectedRecipe = selected 282 | } 283 | , selectCmd model.key selected 284 | ) 285 | 286 | MoveDown -> 287 | let 288 | selected = 289 | model.selectedRecipe 290 | |> Maybe.map (\r -> getNext model.recipes r) 291 | |> Maybe.withDefault (List.head model.recipes) 292 | in 293 | ( { model 294 | | selectedRecipe = selected 295 | } 296 | , selectCmd model.key selected 297 | ) 298 | 299 | ToggleIngredient material checked -> 300 | ( { model 301 | | availableMaterials = 302 | if checked then 303 | Set.Any.insert material model.availableMaterials 304 | 305 | else 306 | Set.Any.remove material model.availableMaterials 307 | } 308 | |> deriveMaterials 309 | , if model.syncing then 310 | saveMaterial material.name checked 311 | 312 | else 313 | Cmd.none 314 | ) 315 | 316 | SetMode mode -> 317 | ( { model | mode = mode }, Cmd.none ) 318 | 319 | SetSubsitute on -> 320 | ( { model | pedantic = on } 321 | |> deriveMaterials 322 | , Cmd.none 323 | ) 324 | 325 | SetDark on -> 326 | ( { model | dark = on }, setDark on ) 327 | 328 | SetEmail e -> 329 | ( { model | email = e }, Cmd.none ) 330 | 331 | SetTab tab -> 332 | ( { model | tab = tab }, Cmd.none ) 333 | 334 | Ignored -> 335 | ( model, Cmd.none ) 336 | 337 | GetMagicLink -> 338 | ( { model | email = "" }, getMagicLink model.email ) 339 | 340 | GotInventory result -> 341 | case result of 342 | Ok inventory -> 343 | let 344 | inventorySet = 345 | Set.fromList inventory 346 | in 347 | ( { model 348 | | availableMaterials = 349 | List.filter 350 | (\m -> 351 | Set.member 352 | m.name 353 | inventorySet 354 | ) 355 | model.materials 356 | |> Set.Any.fromList materialKey 357 | , syncing = True 358 | } 359 | |> deriveMaterials 360 | , Cmd.none 361 | ) 362 | 363 | Err _ -> 364 | ( { model 365 | | syncing = False 366 | } 367 | , Cmd.none 368 | ) 369 | 370 | GotMagic _ -> 371 | ( { model | sentEmail = True }, Cmd.none ) 372 | 373 | GotDevice device -> 374 | ( { model | device = device }, Cmd.none ) 375 | 376 | GotOk _ -> 377 | ( model, Cmd.none ) 378 | 379 | ChangedUrl _ -> 380 | ( model, Cmd.none ) 381 | 382 | ClickedLink urlRequest -> 383 | case urlRequest of 384 | Browser.Internal _ -> 385 | ( model, Cmd.none ) 386 | 387 | Browser.External href -> 388 | ( model, Nav.load href ) 389 | 390 | 391 | resizeHandler : Int -> Int -> Msg 392 | resizeHandler w _ = 393 | if w < 640 then 394 | GotDevice Mobile 395 | 396 | else 397 | GotDevice Desktop 398 | 399 | 400 | subscriptions : Model -> Sub Msg 401 | subscriptions _ = 402 | Sub.batch 403 | [ Browser.Events.onKeyDown keyDecoder 404 | , Browser.Events.onResize resizeHandler 405 | ] 406 | 407 | 408 | okDecoder : Decoder Bool 409 | okDecoder = 410 | field "ok" bool 411 | 412 | 413 | inventoryDecoder : Decoder (List String) 414 | inventoryDecoder = 415 | Decode.list string 416 | 417 | 418 | saveMaterial : String -> Bool -> Cmd Msg 419 | saveMaterial material add = 420 | Http.post 421 | { url = "/api/store" 422 | , body = 423 | Http.jsonBody 424 | (Encode.object 425 | [ ( "material", Encode.string material ) 426 | , ( "add", Encode.bool add ) 427 | ] 428 | ) 429 | , expect = Http.expectJson GotOk okDecoder 430 | } 431 | 432 | 433 | getMagicLink : String -> Cmd Msg 434 | getMagicLink email = 435 | Http.post 436 | { url = "/api/signin" 437 | , body = 438 | Http.jsonBody 439 | (Encode.object 440 | [ ( "email", Encode.string email ) 441 | ] 442 | ) 443 | , expect = Http.expectJson GotMagic okDecoder 444 | } 445 | 446 | 447 | loadInventory : Cmd Msg 448 | loadInventory = 449 | Http.get 450 | { url = "/api/inventory" 451 | , expect = Http.expectJson GotInventory inventoryDecoder 452 | } 453 | 454 | 455 | init : Bool -> Url -> Nav.Key -> ( Model, Cmd Msg ) 456 | init isMobile url key = 457 | ( { recipes = recipes 458 | , materials = [] 459 | , countedMaterials = [] 460 | , selectedRecipe = 461 | List.filter 462 | (\r -> 463 | recipeSlug r 464 | == (url.fragment 465 | |> Maybe.withDefault "" 466 | ) 467 | ) 468 | recipes 469 | |> List.head 470 | , availableMaterials = Set.Any.empty materialKey 471 | , units = Ml 472 | , mode = Normal 473 | , pedantic = True 474 | , sort = Feasibility 475 | , dark = False 476 | , syncing = False 477 | , sentEmail = False 478 | , email = "" 479 | , key = key 480 | , device = 481 | if isMobile then 482 | Mobile 483 | 484 | else 485 | Desktop 486 | , tab = TDetail 487 | } 488 | |> deriveMaterials 489 | , loadInventory 490 | ) 491 | 492 | 493 | port setDark : Bool -> Cmd msg 494 | 495 | 496 | main : Program Bool Model Msg 497 | main = 498 | Browser.application 499 | { init = init 500 | , view = view 501 | , update = update 502 | , subscriptions = subscriptions 503 | , onUrlChange = ChangedUrl 504 | , onUrlRequest = ClickedLink 505 | } 506 | 507 | 508 | materialKey : Material -> String 509 | materialKey ingredient = 510 | ingredient.name 511 | 512 | 513 | 514 | -- Translate between M:SS (minutes:seconds) syntax into 515 | -- a number of seconds, for the benefit of YouTube’s skip-to-time 516 | -- method 517 | 518 | 519 | parseTime : String -> String 520 | parseTime str = 521 | str 522 | |> String.split ":" 523 | |> List.reverse 524 | |> List.indexedMap 525 | (\idx val -> (String.toInt val |> Maybe.withDefault 0) * max 1 (idx * 60)) 526 | |> List.sum 527 | |> String.fromInt 528 | 529 | 530 | countMaterials : Model -> MaterialType -> List CachedMaterial 531 | countMaterials model t = 532 | List.filter (\material -> material.t == t) model.materials 533 | |> List.map 534 | (\material -> 535 | ( recipesWithMaterial model.pedantic recipes material 536 | , Set.Any.member material model.availableMaterials 537 | , material 538 | ) 539 | ) 540 | |> List.sortBy (\( count, _, _ ) -> -count) 541 | 542 | 543 | deriveMaterials : Model -> Model 544 | deriveMaterials model = 545 | let 546 | recipesByGap = 547 | List.map 548 | (\recipe -> ( recipe, missingIngredients model.pedantic model.availableMaterials recipe |> Set.Any.size |> toFloat )) 549 | model.recipes 550 | 551 | ( sufficient, unsortedInsufficient ) = 552 | List.partition 553 | (\( _, gapSize ) -> gapSize == 0) 554 | recipesByGap 555 | 556 | insufficient = 557 | List.sortBy 558 | (\( recipe, gapSize ) -> 559 | gapSize 560 | / (List.length recipe.ingredients 561 | |> toFloat 562 | ) 563 | ) 564 | unsortedInsufficient 565 | 566 | alphabetical = 567 | List.sortBy .name model.recipes 568 | 569 | orderedRecipes = 570 | case model.sort of 571 | Feasibility -> 572 | sufficient 573 | ++ insufficient 574 | |> List.map (\( recipe, _ ) -> recipe) 575 | 576 | Alphabetical -> 577 | alphabetical 578 | in 579 | { model 580 | | materials = 581 | List.concatMap 582 | (\recipe -> 583 | List.concatMap 584 | (\ingredient -> 585 | [ aliasMaterial False ingredient.material 586 | , ingredient.material 587 | ] 588 | ) 589 | recipe.ingredients 590 | ) 591 | model.recipes 592 | |> List.foldl (::) [] 593 | |> Set.Any.fromList materialKey 594 | |> Set.Any.toList 595 | , recipes = orderedRecipes 596 | } 597 | |> (\m -> 598 | { m 599 | | countedMaterials = 600 | List.map 601 | (\( label, t ) -> 602 | { label = label 603 | , materials = countMaterials m t 604 | } 605 | ) 606 | [ ( "SPIRITS", Spirit ) 607 | , ( "LIQUEUR", Liqueur ) 608 | , ( "FORTIFIED", Fortified ) 609 | , ( "BASE", Base ) 610 | , ( "BITTERS", Bitters ) 611 | , ( "SYRUP", Syrup ) 612 | , ( "JUICE", Juice ) 613 | , ( "SODA", Soda ) 614 | , ( "FRUIT", Fruit ) 615 | , ( "SEASONING", Seasoning ) 616 | , ( "OTHER", Other ) 617 | ] 618 | } 619 | ) 620 | 621 | 622 | keyDecoder : Decode.Decoder Msg 623 | keyDecoder = 624 | Decode.map toDirection (Decode.field "key" Decode.string) 625 | 626 | 627 | toDirection : String -> Msg 628 | toDirection string = 629 | case string of 630 | "k" -> 631 | MoveUp 632 | 633 | "j" -> 634 | MoveDown 635 | 636 | _ -> 637 | Ignored 638 | 639 | 640 | aliasMaterial : Bool -> Material -> Material 641 | aliasMaterial pedantic material = 642 | if pedantic then 643 | material 644 | 645 | else 646 | case material.super of 647 | SuperMaterial Nothing -> 648 | material 649 | 650 | SuperMaterial (Just m) -> 651 | m 652 | 653 | 654 | getMaterials : Bool -> Recipe -> MaterialSet 655 | getMaterials pedantic recipe = 656 | recipe.ingredients 657 | |> List.filter 658 | (\ingredient -> not ingredient.optional && (pedantic || (ingredient.material.t /= Fruit && ingredient.material.t /= Seasoning))) 659 | |> List.map 660 | (\ingredient -> aliasMaterial pedantic ingredient.material) 661 | |> Set.Any.fromList 662 | materialKey 663 | 664 | 665 | missingIngredients : Bool -> MaterialSet -> Recipe -> MaterialSet 666 | missingIngredients pedantic availableMaterials recipe = 667 | Set.Any.diff (getMaterials pedantic recipe) availableMaterials 668 | 669 | 670 | getNeighbors : Model -> Recipe -> List ( List Material, List Material, Recipe ) 671 | getNeighbors model recipe = 672 | model.recipes 673 | |> List.filter (\r -> r.name /= recipe.name) 674 | |> List.map 675 | (\r -> 676 | let 677 | rMaterials = 678 | getMaterials model.pedantic r 679 | 680 | recipeMaterials = 681 | getMaterials model.pedantic recipe 682 | in 683 | ( Set.Any.diff rMaterials recipeMaterials |> Set.Any.toList 684 | , Set.Any.diff recipeMaterials rMaterials |> Set.Any.toList 685 | , r 686 | ) 687 | ) 688 | |> List.filter (\( add, remove, _ ) -> List.length add + List.length remove < 4) 689 | |> List.sortBy (\( add, remove, _ ) -> List.length add + List.length remove) 690 | 691 | 692 | recipesWithMaterial : Bool -> List Recipe -> Material -> Int 693 | recipesWithMaterial pedantic allRecipes material = 694 | List.filter (\recipe -> hasMaterial pedantic recipe material) allRecipes 695 | |> List.length 696 | 697 | 698 | getNext : List Recipe -> Recipe -> Maybe Recipe 699 | getNext recipes target = 700 | List.head recipes 701 | |> Maybe.andThen 702 | (\x -> 703 | if x == target then 704 | List.drop 1 recipes |> List.head 705 | 706 | else 707 | getNext (List.drop 1 recipes) target 708 | ) 709 | 710 | 711 | 712 | -- User interface 713 | 714 | 715 | checkboxIconX : Int -> Bool -> Element msg 716 | checkboxIconX x checked = 717 | let 718 | wh = 719 | x |> String.fromInt 720 | 721 | c = 722 | x // 2 |> String.fromInt 723 | 724 | r = 725 | x // 3 |> String.fromInt 726 | in 727 | svg 728 | [ Svg.Attributes.width wh 729 | , Svg.Attributes.height wh 730 | ] 731 | [ Svg.circle 732 | [ Svg.Attributes.cx c 733 | , Svg.Attributes.cy c 734 | , Svg.Attributes.r r 735 | , Svg.Attributes.stroke "currentColor" 736 | , Svg.Attributes.strokeWidth "1" 737 | , Svg.Attributes.fill 738 | (if checked then 739 | "currentColor" 740 | 741 | else 742 | "transparent" 743 | ) 744 | ] 745 | [] 746 | ] 747 | |> html 748 | 749 | 750 | checkboxIcon : Bool -> Element Msg 751 | checkboxIcon = 752 | checkboxIconX 12 753 | 754 | 755 | checkboxIconL : Bool -> Element Msg 756 | checkboxIconL = 757 | checkboxIconX 14 758 | 759 | 760 | materialNavigationItem : CachedMaterial -> Element.Element Msg 761 | materialNavigationItem ( count, checked, material ) = 762 | Input.checkbox [] 763 | { onChange = \_ -> ToggleIngredient material (not checked) 764 | , icon = checkboxIcon 765 | , checked = checked 766 | , label = 767 | Input.labelRight [] 768 | (text 769 | (String.fromInt count 770 | ++ " " 771 | ++ (if material.name == "Islay Single Malt Scotch whiskey" then 772 | "Islay Whiskey" 773 | 774 | else 775 | material.name 776 | ) 777 | ) 778 | ) 779 | } 780 | 781 | 782 | title : String -> Element.Element Msg 783 | title name = 784 | el 785 | [ bold 786 | , paddingEach { edges | bottom = 5 } 787 | ] 788 | (text name) 789 | 790 | 791 | listMaterials : Bool -> List MaterialsGroup -> Element.Element Msg 792 | listMaterials pedantic countedMaterials = 793 | countedMaterials 794 | |> List.concatMap 795 | (\{ label, materials } -> 796 | [ column [ spacing 8, alignTop ] 797 | (title label 798 | :: List.map 799 | materialNavigationItem 800 | (if pedantic then 801 | List.filter 802 | (\( count, _, _ ) -> count > 0) 803 | materials 804 | 805 | else 806 | List.filter 807 | (\( count, _, material ) -> count > 0 && material.super == SuperMaterial Nothing) 808 | materials 809 | ) 810 | ) 811 | ] 812 | ) 813 | |> column [ spacing 20, alignTop, width shrink ] 814 | 815 | 816 | glassName : Glass -> String 817 | glassName glass = 818 | case glass of 819 | -- Tumblers 820 | Collins -> 821 | "Collins glass" 822 | 823 | Highball -> 824 | "Highball glass" 825 | 826 | OldFashioned -> 827 | "Old Fashioned glass" 828 | 829 | -- Stemware: 830 | ChampagneFlute -> 831 | "Champagne flute" 832 | 833 | Cocktail -> 834 | "Cocktail glass" 835 | 836 | Hurricane -> 837 | "Hurricane glass" 838 | 839 | Margarita -> 840 | "Margarita glass" 841 | 842 | Wine -> 843 | "Wine glass" 844 | 845 | -- Other: 846 | CopperMug -> 847 | "Copper mug" 848 | 849 | IrishCoffeeMug -> 850 | "Irish coffee mug" 851 | 852 | SteelCup -> 853 | "Steel cup" 854 | 855 | ZombieGlass -> 856 | "Zombie glass" 857 | 858 | ChampagneCoupe -> 859 | "Champagne coupe" 860 | 861 | 862 | capitalize : String -> String 863 | capitalize str = 864 | case String.toList str of 865 | x :: xs -> 866 | String.fromChar (Char.toUpper x) ++ String.fromList xs 867 | 868 | [] -> 869 | "" 870 | 871 | 872 | neighborBlock : ( List Material, List Material, Recipe ) -> Element.Element Msg 873 | neighborBlock ( add, remove, neighbor ) = 874 | column 875 | [ spacing 10 876 | , Element.pointer 877 | , width fill 878 | , height fill 879 | , alignTop 880 | , onClick (SelectRecipe neighbor) 881 | ] 882 | [ el [ Font.italic, Font.underline ] (text neighbor.name) 883 | , paragraph [] 884 | [ List.map (\a -> "add " ++ a.name) add 885 | ++ List.map (\a -> "remove " ++ a.name) remove 886 | |> String.join ", " 887 | |> capitalize 888 | |> text 889 | |> el [] 890 | ] 891 | ] 892 | 893 | 894 | recipeSlug : Recipe -> String 895 | recipeSlug recipe = 896 | Maybe.map Slug.toString 897 | (Slug.generate recipe.name) 898 | |> Maybe.withDefault "" 899 | 900 | 901 | recipeBlock : Model -> Recipe -> Element.Element Msg 902 | recipeBlock model recipe = 903 | let 904 | viable = 905 | missingIngredients model.pedantic model.availableMaterials recipe |> Set.Any.isEmpty 906 | 907 | selected = 908 | model.selectedRecipe == Just recipe 909 | in 910 | column 911 | ([ spacing 10 912 | , Element.pointer 913 | , Element.alpha 914 | (if viable then 915 | 1 916 | 917 | else 918 | 0.5 919 | ) 920 | , width fill 921 | , height fill 922 | , alignTop 923 | , onClick (SelectRecipe recipe) 924 | , Element.htmlAttribute 925 | (Html.Attributes.id (recipeSlug recipe)) 926 | , Element.htmlAttribute 927 | (Html.Attributes.tabindex 0) 928 | ] 929 | ++ (if selected then 930 | if model.dark then 931 | [ Border.color 932 | (Element.rgb255 933 | 239 934 | 237 935 | 234 936 | ) 937 | , Border.width 1 938 | , padding 9 939 | ] 940 | 941 | else 942 | [ Element.Background.color 943 | (Element.rgb255 944 | 229 945 | 227 946 | 224 947 | ) 948 | , padding 10 949 | ] 950 | 951 | else 952 | [ padding 10 953 | ] 954 | ) 955 | ) 956 | [ row [] 957 | [ glassIcon recipe 958 | , el [ Font.italic, Font.underline ] (text recipe.name) 959 | ] 960 | , recipe.ingredients 961 | |> List.filter (\ingredient -> not ingredient.optional) 962 | |> List.map 963 | (listIngredientInBlock model) 964 | |> List.intersperse 965 | (el [] (text ", ")) 966 | |> paragraph [ paddingEach { edges | left = 5 } ] 967 | ] 968 | 969 | 970 | listIngredientInBlock : Model -> Ingredient -> Element.Element Msg 971 | listIngredientInBlock model ingredient = 972 | el 973 | (if 974 | Set.Any.member (aliasMaterial model.pedantic ingredient.material) 975 | model.availableMaterials 976 | then 977 | [] 978 | 979 | else 980 | [ strike ] 981 | ) 982 | (text ingredient.material.name) 983 | 984 | 985 | noneSelected : Model -> Element.Element Msg 986 | noneSelected model = 987 | column [ spacing 20, paddingEach { edges | bottom = 40 }, alignTop ] 988 | [ el [ Font.bold ] (text "Hi.") 989 | , paragraph [ spacing 10 ] [ el [] (text """ 990 | This is a website that I made about cocktails. I'm not a huge cocktail nerd (drinking is bad, probably), but think that they're cool. 991 | And the world's pretty bad right now and making this has been calming.""") ] 992 | , paragraph [ spacing 10 ] [ el [] (text """It gave me a chance to both tinker with technology I usually don't use (Elm), 993 | and explore some of the cool properties of cocktails: notably that they're pretty similar and have standardized ingredients, 994 | so they can be described in relationship to each other.""") ] 995 | , paragraph [ spacing 10 ] [ el [] (text """So some of it might seem funky. By default, the list is sorted by 'feasibility': as you add 996 | ingredients that you have, it'll put recipes that you can make (or barely make) closer to the top. Also, click on 'Grid' for a wacky adjacency grid 997 | of cocktails and their ingredients.""") ] 998 | , paragraph [ spacing 10 ] [ el [] (text """Also, for vim fans, there’s j & k support.""") ] 999 | , el [] 1000 | (Element.link 1001 | [ Font.color 1002 | (if model.dark then 1003 | lightBlue 1004 | 1005 | else 1006 | blue 1007 | ) 1008 | , Font.underline 1009 | ] 1010 | { url = "https://tommacwright.typeform.com/to/M7tx4u", label = text "👋 Feedback welcome!" } 1011 | ) 1012 | , el [] 1013 | (Element.link 1014 | [ Font.color 1015 | (if model.dark then 1016 | lightBlue 1017 | 1018 | else 1019 | blue 1020 | ) 1021 | , Font.underline 1022 | ] 1023 | { url = "https://github.com/tmcw/flair", label = paragraph [] [ text "Pull requests welcome - this is open source on GitHub!" ] } 1024 | ) 1025 | ] 1026 | 1027 | 1028 | displayRecipe : Model -> Recipe -> Element.Element Msg 1029 | displayRecipe model recipe = 1030 | let 1031 | neighbors = 1032 | getNeighbors model recipe 1033 | in 1034 | column [ spacing 20, alignTop ] 1035 | ([ title recipe.name 1036 | , row [ spacing 4, spacing 10 ] 1037 | [ el [ alignTop ] (glassIcon recipe) 1038 | , paragraph [] [ text ("Served in a " ++ glassName recipe.glass) ] 1039 | ] 1040 | , recipe.ingredients 1041 | |> List.map 1042 | (\ingredient -> 1043 | paragraph [] 1044 | [ text 1045 | ("- " 1046 | ++ printQuantity ingredient.material.name ingredient.quantity model.units 1047 | ++ " " 1048 | ++ replacement model.pedantic ingredient 1049 | ++ (if ingredient.optional then 1050 | " (optional)" 1051 | 1052 | else 1053 | "" 1054 | ) 1055 | ) 1056 | ] 1057 | ) 1058 | |> column 1059 | [ alignTop, spacing 8 ] 1060 | , paragraph [ spacing 10, alignTop, width fill ] [ text recipe.description ] 1061 | ] 1062 | ++ (case recipe.video of 1063 | Nothing -> 1064 | [] 1065 | 1066 | Just (Epicurious time) -> 1067 | [ el [ paddingEach { edges | top = 20, bottom = 20 }, width fill ] 1068 | (details 1069 | [] 1070 | [ summary [] [ Html.text "Video" ] 1071 | , iframe 1072 | [ Html.Attributes.src ("https://www.youtube-nocookie.com/embed/b0IuTL3Z-kk?start=" ++ parseTime time) 1073 | , Html.Attributes.style "width" "100%" 1074 | , Html.Attributes.height 315 1075 | , Html.Attributes.attribute "frameborder" "0" 1076 | , Html.Attributes.attribute "allowfullscreen" "true" 1077 | ] 1078 | [] 1079 | ] 1080 | |> html 1081 | ) 1082 | ] 1083 | ) 1084 | ++ (if List.isEmpty neighbors then 1085 | [] 1086 | 1087 | else 1088 | title "NEIGHBORS" 1089 | :: List.map neighborBlock neighbors 1090 | ) 1091 | ) 1092 | 1093 | 1094 | replacement : Bool -> Ingredient -> String 1095 | replacement pedantic ingredient = 1096 | if pedantic == False && ingredient.material.super /= SuperMaterial Nothing then 1097 | " (or " ++ (aliasMaterial pedantic ingredient.material).name ++ ")" 1098 | 1099 | else 1100 | "" 1101 | 1102 | 1103 | listRecipes : Model -> Element.Element Msg 1104 | listRecipes model = 1105 | column [ spacing 10, width fill ] 1106 | (List.map 1107 | (recipeBlock model) 1108 | model.recipes 1109 | ) 1110 | 1111 | 1112 | header : Model -> Element.Element Msg 1113 | header model = 1114 | let 1115 | leftItems = 1116 | [ case model.device of 1117 | Desktop -> 1118 | Input.radioRow 1119 | [ spacing 10 1120 | ] 1121 | { onChange = SetMode 1122 | , selected = Just model.mode 1123 | , label = Input.labelHidden "Mode" 1124 | , options = 1125 | [ Input.option Normal (text "List") 1126 | , Input.option Grid (text "Grid") 1127 | ] 1128 | } 1129 | 1130 | Mobile -> 1131 | Element.none 1132 | , Input.checkbox 1133 | [] 1134 | { onChange = SetSubsitute 1135 | , icon = checkboxIconL 1136 | , checked = model.pedantic 1137 | , label = 1138 | Input.labelRight [] 1139 | (text "Pedantic") 1140 | } 1141 | , Input.checkbox 1142 | [] 1143 | { onChange = SetDark 1144 | , icon = checkboxIconL 1145 | , checked = model.dark 1146 | , label = 1147 | Input.labelRight [] 1148 | (text "Dark") 1149 | } 1150 | ] 1151 | 1152 | rightItems = 1153 | [ row [ spacing 10 ] 1154 | [ el 1155 | [] 1156 | (label [ Html.Attributes.for "sort" ] [ Html.text "Sort" ] |> html) 1157 | , el [] 1158 | (select 1159 | [ Html.Events.onInput SetSort 1160 | , Html.Attributes.id 1161 | "sort" 1162 | ] 1163 | [ option [ value "Feasibility" ] 1164 | [ Html.text "Feasibility" 1165 | ] 1166 | , option [ value "Alphabetical" ] 1167 | [ Html.text "Alphabetical" 1168 | ] 1169 | ] 1170 | |> html 1171 | ) 1172 | ] 1173 | , row [ spacing 10 ] 1174 | [ el 1175 | [] 1176 | (label [ Html.Attributes.for "units" ] [ Html.text "Units" ] |> html) 1177 | , el [] 1178 | (select 1179 | [ Html.Events.onInput SetUnits 1180 | , Html.Attributes.id 1181 | "units" 1182 | ] 1183 | [ option [ value "Ml" ] 1184 | [ Html.text "Ml" 1185 | ] 1186 | , option [ value "Cl" ] 1187 | [ Html.text "Cl" 1188 | ] 1189 | , option [ value "Oz" ] 1190 | [ Html.text "Oz" 1191 | ] 1192 | ] 1193 | |> html 1194 | ) 1195 | ] 1196 | ] 1197 | in 1198 | if model.device == Mobile then 1199 | column 1200 | [ paddingEach { edges | left = 20, right = 20 } 1201 | , spacing 20 1202 | ] 1203 | (title "SETTINGS" 1204 | :: leftItems 1205 | ++ rightItems 1206 | ) 1207 | 1208 | else 1209 | row [ width fill, paddingEach { edges | left = 20, right = 20, top = 20, bottom = 30 } ] 1210 | [ row [ width shrink, Element.alignLeft, spacing 20 ] 1211 | leftItems 1212 | , row [ width shrink, Element.alignRight, spacing 20 ] 1213 | rightItems 1214 | ] 1215 | 1216 | 1217 | 1218 | -- A fast check to see if a given recipe has an ingredient. Tries to skip 1219 | -- any allocations 1220 | 1221 | 1222 | hasMaterial : Bool -> Recipe -> Material -> Bool 1223 | hasMaterial pedantic recipe material = 1224 | List.foldl 1225 | (\i has -> 1226 | if has then 1227 | has 1228 | 1229 | else 1230 | aliasMaterial pedantic i.material == material 1231 | ) 1232 | False 1233 | recipe.ingredients 1234 | 1235 | 1236 | renderDot : Material -> Recipe -> Html.Html Msg 1237 | renderDot material recipe = 1238 | let 1239 | on = 1240 | hasMaterial False recipe material 1241 | in 1242 | svg 1243 | [ Svg.Attributes.width "22" 1244 | , Svg.Attributes.height "22" 1245 | ] 1246 | [ Svg.circle 1247 | [ Svg.Attributes.cx "11" 1248 | , Svg.Attributes.cy "11" 1249 | , Svg.Attributes.fill "currentColor" 1250 | , Svg.Attributes.r 1251 | (if on then 1252 | "4" 1253 | 1254 | else 1255 | "1" 1256 | ) 1257 | ] 1258 | [] 1259 | ] 1260 | 1261 | 1262 | getColumn : Material -> Html.Html Msg 1263 | getColumn material = 1264 | svg 1265 | [ Svg.Attributes.width "22" 1266 | , Svg.Attributes.height "160" 1267 | , Svg.Attributes.viewBox "0 0 20 160" 1268 | ] 1269 | [ Svg.text_ 1270 | [ Svg.Attributes.x "-140" 1271 | , Svg.Attributes.y "10" 1272 | , Svg.Attributes.width "22" 1273 | , Svg.Attributes.height "160" 1274 | , Svg.Attributes.rx "15" 1275 | , Svg.Attributes.ry "15" 1276 | , Svg.Attributes.transform "rotate(-90, 10, 8)" 1277 | , Svg.Attributes.textAnchor "start" 1278 | , Svg.Attributes.fill "currentColor" 1279 | ] 1280 | [ Svg.text material.name 1281 | ] 1282 | ] 1283 | 1284 | 1285 | gridView : Model -> Element.Element Msg 1286 | gridView model = 1287 | let 1288 | mod = 1289 | model 1290 | 1291 | sortedMaterials = 1292 | List.concatMap (\{ materials } -> materials) mod.countedMaterials 1293 | |> List.map (\( _, _, material ) -> material) 1294 | in 1295 | Html.table 1296 | [ Html.Attributes.class "grid" 1297 | ] 1298 | [ Html.thead [] 1299 | (Html.th [] 1300 | [ Html.text "" 1301 | ] 1302 | :: List.map 1303 | (\ingredient -> Html.th [] [ getColumn ingredient ]) 1304 | sortedMaterials 1305 | ) 1306 | , Html.tbody [] 1307 | (List.map 1308 | (\recipe -> 1309 | Html.tr [] 1310 | (Html.th [] [ Html.text recipe.name ] 1311 | :: List.map 1312 | (\material -> Html.td [] [ renderDot material recipe ]) 1313 | sortedMaterials 1314 | ) 1315 | ) 1316 | model.recipes 1317 | ) 1318 | ] 1319 | |> html 1320 | 1321 | 1322 | view : Model -> Browser.Document Msg 1323 | view model = 1324 | let 1325 | mWidth : Int -> Element.Attribute Msg 1326 | mWidth w = 1327 | if model.device == Mobile then 1328 | width fill 1329 | 1330 | else 1331 | width (Element.px w) 1332 | 1333 | recipes = 1334 | column 1335 | [ alignTop 1336 | , paddingEach 1337 | (if model.device == Desktop then 1338 | { edges | left = 20, right = 20 } 1339 | 1340 | else 1341 | { edges | left = 10, right = 10 } 1342 | ) 1343 | , spacing 5 1344 | , Element.Region.navigation 1345 | , mWidth 360 1346 | , height fill 1347 | ] 1348 | [ row 1349 | [ width fill 1350 | , spacing 10 1351 | ] 1352 | [ el 1353 | [ paddingEach { edges | left = 10 } 1354 | , width fill 1355 | ] 1356 | (title 1357 | "COCKTAILS" 1358 | ) 1359 | ] 1360 | , el 1361 | [ Element.scrollbarY, height fill ] 1362 | (listRecipes 1363 | model 1364 | ) 1365 | ] 1366 | 1367 | detail = 1368 | column 1369 | [ alignTop 1370 | , paddingEach { edges | left = 20, right = 20 } 1371 | , spacing 5 1372 | , Element.Region.mainContent 1373 | , width (Element.maximum 640 fill) 1374 | , height fill 1375 | , Element.scrollbarY 1376 | ] 1377 | [ case model.selectedRecipe of 1378 | Just r -> 1379 | displayRecipe model r 1380 | 1381 | Nothing -> 1382 | noneSelected model 1383 | ] 1384 | 1385 | materials = 1386 | column 1387 | [ spacing 5 1388 | , paddingEach { edges | left = 20, right = 20 } 1389 | , alignTop 1390 | , mWidth 260 1391 | , height 1392 | fill 1393 | , Element.scrollbarY 1394 | ] 1395 | (listMaterials model.pedantic model.countedMaterials 1396 | :: (if model.syncing then 1397 | [ el 1398 | [ paddingEach { edges | top = 30 } 1399 | ] 1400 | (Element.link 1401 | [ Font.color 1402 | (if model.dark then 1403 | lightBlue 1404 | 1405 | else 1406 | blue 1407 | ) 1408 | ] 1409 | { url = "/api/signout", label = text "Sign out" } 1410 | ) 1411 | ] 1412 | 1413 | else if model.sentEmail then 1414 | [ paragraph 1415 | [ paddingEach { edges | bottom = 10, top = 30 } 1416 | ] 1417 | [ el [] (text "Check your email!") ] 1418 | ] 1419 | 1420 | else 1421 | [ column 1422 | [ paddingEach { edges | top = 30, bottom = 50 } 1423 | , spacing 5 1424 | ] 1425 | [ paragraph 1426 | [ paddingEach { edges | bottom = 10 } 1427 | ] 1428 | [ el [] (text "Sign in to save your ingredients") ] 1429 | , Input.text 1430 | [ Html.Events.stopPropagationOn "keydown" (Decode.succeed ( Ignored, True )) 1431 | |> Element.htmlAttribute 1432 | ] 1433 | { onChange = \email -> SetEmail email 1434 | , text = model.email 1435 | , placeholder = Just (Input.placeholder [] (text "Email")) 1436 | , label = Input.labelHidden "Email" 1437 | } 1438 | , Input.button 1439 | [ Font.center 1440 | , width fill 1441 | ] 1442 | { onPress = Just GetMagicLink 1443 | , label = text "GET LINK" 1444 | } 1445 | ] 1446 | ] 1447 | ) 1448 | ) 1449 | in 1450 | { title = 1451 | "Old Fashioned" 1452 | ++ (model.selectedRecipe 1453 | |> Maybe.map (\r -> ": " ++ r.name) 1454 | |> Maybe.withDefault "" 1455 | ) 1456 | , body = 1457 | [ Element.layout 1458 | [ Font.size 13 1459 | , Font.color 1460 | (if model.dark then 1461 | white 1462 | 1463 | else 1464 | black 1465 | ) 1466 | , Font.family 1467 | [ Font.typeface "IBM Plex Mono" 1468 | , Font.typeface "SFMono-Regular" 1469 | , Font.typeface "Consolas" 1470 | , Font.typeface "Liberation Mono" 1471 | , Font.typeface "Menlo" 1472 | , Font.monospace 1473 | ] 1474 | , width fill 1475 | , height fill 1476 | ] 1477 | (column 1478 | [ width fill 1479 | , height fill 1480 | , if model.device == Mobile then 1481 | spacing 20 1482 | 1483 | else 1484 | spacing 0 1485 | ] 1486 | [ if model.device == Desktop then 1487 | header model 1488 | 1489 | else 1490 | Element.none 1491 | , if model.device == Mobile then 1492 | row 1493 | [ Border.widthEach { edges | bottom = 1 } 1494 | , Element.spaceEvenly 1495 | , padding 20 1496 | , width fill 1497 | ] 1498 | [ Input.button 1499 | [ Font.alignLeft 1500 | , if model.tab == TMaterials then 1501 | Font.underline 1502 | 1503 | else 1504 | Font.unitalicized 1505 | ] 1506 | { onPress = Just (SetTab TMaterials) 1507 | , label = text "Ingredients" 1508 | } 1509 | , Input.button 1510 | [ Font.alignLeft 1511 | , if model.tab == TCocktails then 1512 | Font.underline 1513 | 1514 | else 1515 | Font.unitalicized 1516 | ] 1517 | { onPress = Just (SetTab TCocktails) 1518 | , label = text "Cocktails" 1519 | } 1520 | , Input.button 1521 | [ Font.alignLeft 1522 | , if model.tab == TDetail then 1523 | Font.underline 1524 | 1525 | else 1526 | Font.unitalicized 1527 | ] 1528 | { onPress = Just (SetTab TDetail) 1529 | , label = text "Detail" 1530 | } 1531 | , Input.button 1532 | [ Font.alignLeft 1533 | , if model.tab == TSettings then 1534 | Font.underline 1535 | 1536 | else 1537 | Font.unitalicized 1538 | ] 1539 | { onPress = Just (SetTab TSettings) 1540 | , label = text "*" 1541 | } 1542 | ] 1543 | 1544 | else 1545 | Element.none 1546 | , if model.mode == Grid then 1547 | gridView model 1548 | 1549 | else if model.device == Mobile then 1550 | el [ Element.scrollbarY, height fill, width fill ] 1551 | (case model.tab of 1552 | TMaterials -> 1553 | materials 1554 | 1555 | TCocktails -> 1556 | recipes 1557 | 1558 | TDetail -> 1559 | detail 1560 | 1561 | TSettings -> 1562 | header model 1563 | ) 1564 | 1565 | else 1566 | row 1567 | [ width fill 1568 | , height fill 1569 | , Element.scrollbarY 1570 | ] 1571 | [ materials 1572 | , recipes 1573 | , detail 1574 | ] 1575 | ] 1576 | ) 1577 | ] 1578 | } 1579 | -------------------------------------------------------------------------------- /front/src/Quantity.elm: -------------------------------------------------------------------------------- 1 | module Quantity exposing (Quantity(..), Units(..), printQuantity) 2 | 3 | 4 | type Quantity 5 | = CL Float 6 | | Cube Int 7 | | Dash Int 8 | | Drop Int 9 | | FewDrops 10 | | FewDashes 11 | | Slice Float 12 | | Splash Int 13 | | Sprig Int 14 | | Tsp Float 15 | | Wedge Int 16 | | Whole Int 17 | | Custom String 18 | | None 19 | 20 | 21 | type Units 22 | = Cl 23 | | Ml 24 | | Oz 25 | 26 | 27 | formatInteger : Float -> String 28 | formatInteger d = 29 | if floor d == 0 then 30 | "" 31 | else 32 | String.fromInt (floor d) 33 | 34 | 35 | -- Expects number between 0 and 100, floored. 36 | formatFractional : Float -> String 37 | formatFractional d = 38 | if d == 0 then 39 | "" 40 | else if d == 10 then 41 | "⅒" 42 | else if d == 11 then 43 | "⅑" 44 | else if d == 12 then 45 | "⅛" 46 | else if d == 14 then 47 | "⅐" 48 | else if d == 16 then 49 | "⅙" 50 | else if d == 20 then 51 | "⅕" 52 | else if d == 25 then 53 | "¼" 54 | else if d == 33 then 55 | "⅓" 56 | else if d == 37 then 57 | "⅜" 58 | else if d == 40 then 59 | "⅖" 60 | else if d == 50 then 61 | "½" 62 | else if d == 60 then 63 | "⅗" 64 | else if d == 62 then 65 | "⅝" 66 | else if d == 66 then 67 | "⅔" 68 | else if d == 75 then 69 | "¾" 70 | else if d == 80 then 71 | "⅘" 72 | else if d == 83 then 73 | "⅚" 74 | else if d == 87 then 75 | "⅞" 76 | else 77 | "." ++ String.fromFloat d 78 | 79 | 80 | formatFloat : Float -> String 81 | formatFloat d = 82 | formatInteger d ++ formatFractional ( toFloat ( floor ( ( d - toFloat ( floor d ) ) * 100 ) ) ) 83 | 84 | 85 | plural : String -> String -> Int -> String 86 | plural word suffix d = 87 | if d == 1 then 88 | word 89 | else 90 | word ++ suffix 91 | 92 | 93 | printQuantity : String -> Quantity -> Units -> String 94 | printQuantity name quantity units = 95 | case quantity of 96 | CL a -> 97 | case units of 98 | Cl -> 99 | formatFloat a ++ " Cl " ++ name 100 | 101 | Ml -> 102 | formatFloat (a * 10) ++ " Ml " ++ name 103 | 104 | Oz -> 105 | formatFloat (a * 1 / 3) ++ " Oz " ++ name 106 | 107 | Cube a -> 108 | name ++ ": " ++ String.fromInt a ++ plural " cube" "s" a 109 | 110 | Dash a -> 111 | name ++ ": " ++ String.fromInt a ++ plural " dash" "es" a 112 | 113 | Drop a -> 114 | name ++ ": " ++ String.fromInt a ++ plural " drop" "s" a 115 | 116 | FewDrops -> 117 | name ++ ": Few drops" 118 | 119 | FewDashes -> 120 | name ++ ": Few dashes" 121 | 122 | Slice a -> 123 | name ++ ": " ++ formatFloat a ++ plural " slice" "s" ( floor a ) 124 | 125 | Splash a -> 126 | name ++ ": " ++ String.fromInt a ++ plural " splash" "es" a 127 | 128 | Sprig a -> 129 | name ++ ": " ++ String.fromInt a ++ plural " sprig" "s" a 130 | 131 | Tsp a -> 132 | formatFloat a ++ " Tsp " ++ name 133 | 134 | Wedge a -> 135 | name ++ ": " ++ String.fromInt a ++ plural " wedge" "s" a 136 | 137 | Whole a -> 138 | name ++ ": " ++ String.fromInt a 139 | 140 | Custom str -> 141 | name ++ ": " ++ str 142 | 143 | None -> 144 | name 145 | -------------------------------------------------------------------------------- /front/src/index.js: -------------------------------------------------------------------------------- 1 | import { Elm } from "./Main.elm"; 2 | 3 | const app = Elm.Main.init({ 4 | node: document.getElementById("root"), 5 | flags: window.innerWidth < 640, 6 | }); 7 | 8 | document.body.style.transition = "background, color 600ms ease-in-out"; 9 | document.body.style.transition = "background 600ms ease-in-out"; 10 | 11 | app.ports.setDark.subscribe((dark) => { 12 | if (dark) { 13 | document.body.style.background = "#3c3c3c"; 14 | document.body.style.color = "rgb(229, 227, 224)"; 15 | } else { 16 | document.body.style.background = "rgb(249, 247, 244)"; 17 | document.body.style.color = "#000"; 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "front", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "build": "cd front && elm-app build && mv build ../public" 8 | }, 9 | "dependencies": { 10 | "create-elm-app": "^5" 11 | } 12 | } 13 | --------------------------------------------------------------------------------