JSFiddle - React, Tailwind, and code Playground

by lid0

HTML

<div id="app">
    <div id="width-wrap">
        <header>
            <h1>Cocktail DB Browser</h1>
            <!-- TODO: replace the select control with this custom multi-select control -->
            <label>Pick some ingredients
                <div class="ingredient-selector" @change="onIngredientSelected()">
                    <!-- TODO: this span must show all the selected ingredients -->
                    <span class="selected-ingredient">
                        <span v-for="selectedIngredient in selectedIngredients" class="remove" @click="deselectIngredient(selectedIngredient)">{{selectedIngredient}}</span> <!-- TODO: clicking this should deselected the ingredient -->
                    </span>
                    <!-- TODO: this input must add ingredients on chnage -->
                    <input type="text" list="ingredient-list" v-model="ingredientSelectorText">
                    <datalist id="ingredient-list">
                        <!-- TODO: add all the ingredients as options -->
                        <option v-for="ingredient in ingredients">{{ingredient}}</option>
                    </datalist>
                </div>
            </label>
            <!-- TODO: Delete from here ... -->
            
            <!-- TODO: ... to here -->
        </header>

        <div id="results">
            <!-- TODO: This element should only appear if there are no results -->
            <div>There are no drinks that use all of the selected ingredients</div>
            <div v-if="isLoadingResults" id="loader" class="lds-ripple active"><div></div><div></div></div>
            <div v-for="r in results" class="result" :class="{ expanded: isExpanded(r.id) }" @click="handleResultClick(r.id)">
                <img class="photo" :src="r.thumbURL + ( ! isExpanded(r.id) ? '/preview' : '' )">
                <div class="info">
                    <div class="summary">
                        <div class="prop"><span class="prop-value">{{ r.name }}</span></div>
...

CSS

body {
    margin: 0;
    font-family: "Dosis", sans-serif;
    background: #000;
    color: #ee4292;
}

* {
    box-sizing: border-box;
    transition: all 200ms ease-in-out;
}

#width-wrap {
    max-width: 1024px;
    min-height: 100vh;
    margin-left: auto;
    margin-right: auto;
    background: #101;
    border: 1px solid transparent;
    padding-bottom: 100px;
}

header {
    margin: 2em 2em;
}

#results {
    display: flex;
    flex-direction: column;
}

#results > div {
    display: flex;
    cursor: pointer;
}
#results > div:hover,
#results > div.expanded {
    background: #ee429220;
}

#results > div img {
    display: block;
    width: 100px;
    object-fit: cover;
    filter: saturate(0.4);
}
#results > div:hover img,
#results > div.expanded img {
    filter: saturate(1.1);
}

#results > div.expanded img {
    width: 200px;
}

#results > div .info {
    flex-grow: 1;
    padding-bottom: 30px;
}

#results > div .name {
    margin-top: 0.5em;
    font-size: 32px;
    letter-spacing: 6px;
    line-height: 40px;
    text-transform: uppercase;
}

#results > div:hover .name,
#results > div.expanded .name {
    color: #ffb9d9;
}

#results > div .prop {
    margin: 0.5em 30px;
    display: flex;
    align-items: baseline;
}

#results > div .prop-name {
    text-transform: uppercase;
    margin-right: 1em;    
    color:
    rgba(255,255,255,0.3);
    font-size: 10px;
    letter-spacing: 2px;

    width: 20%;
}

#results > div .prop-value {
    width: 80%;
}

@media screen and ( max-width: 768px ) {
    #results > div.expanded { flex-direction: column; }
    #results > div.expanded img { width: 100%; height: 200px; }
    #results > div .prop { flex-wrap: wrap; }
    #results > div .prop-name { width: 100%; }
    #results > div .prop-value { width: 100%; }
}

#results > div .ingredients ul {
    list-style: none;
    margin: 0;
    padding-left: 0;
}

#results > div .details {
    display: none;
}
#results > div.expanded .details {
    display:...

Vue

import {createApp,defineComponent,provide, inject,ref} from "https://unpkg.com/[email protected]/dist/vue.esm-browser.js";

const Cocktails = {
    data() {
        return {
            // TODO: add a property that stores the input text
            ingredientSelectorText: "",
            selectedIngredients: [],     // TODO: make this an array of ingredients
            expandedDrinkDetails: null,
            _isLoadingDetailsFor: null,
            ingredients: [],
            isLoadingResults: false,
            results: [],
            cache: new Map(),

        }
    },

    methods: {
        onIngredientSelected: async function(){
            if(this.ingredients.includes(this.ingredientSelectorText)){
                if(!this.selectedIngredients.includes(this.ingredientSelectorText)){
                    this.selectedIngredients.push(this.ingredientSelectorText);
                    this.ingredientSelectorText ="";
                }
            }
            
            console.log(this.selectedIngredients);
        },

        deselectIngredient: async function(selectedIngredient){
            this.selectedIngredients = this.selectedIngredients.filter((v)=>v!=selectedIngredient)
            console.log( this.selectedIngredients)
             
        },

        initIngredientList: async function() {

            // Pull the ingredients list from the API and add it to the select element
            const response = await fetch('https://www.thecocktaildb.com/api/json/v1/1/list.php?i=list');
    
            if ( ! response.ok ) {
                console.log(new Error("Something went wrong"));
                return;
            }
    
            const data = await response.json();
    
            this.ingredients = data.drinks                         // Grab the drinks array of the returned JSON
                // Convert the drinks array to an array of ingredient names (the strIngredient1 property is the ingredient name)
                .map( i =>...