| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262 |
- <script>
- import {untrack} from "svelte";
- import {goto} from "$app/navigation";
- import {notify} from "$lib/notifier.svelte.js";
- let {
- startingIngredients,
- autofocus = false,
- compact = false
- } = $props();
- let nameInput;
- let name = $state("");
- let ingredients = $state(untrack(()=>startingIngredients ? [...startingIngredients] : []));
- $effect(()=>{
- if(!startingIngredients) return;
- const next = [...startingIngredients];
- untrack(()=>{
- ingredients = next;
- });
- });
- const addIngredient = (event)=>{
- event.preventDefault();
- const value = name.trim();
- if(!value) return;
- ingredients.push(value);
- name = "";
- nameInput?.focus();
- };
- const searchRecipes = ()=>{
- const pending = name.trim();
- const terms = pending ? [...ingredients, pending] : [...ingredients];
- if(terms.length === 0){
- notify("error", "Add at least one ingredient");
- nameInput?.focus();
- return;
- }
- if(pending){
- ingredients.push(pending);
- name = "";
- }
- const params = new URLSearchParams();
- for(const term of terms) params.append("i", term);
- goto(`/search?${params}`);
- };
- $effect(()=>{
- if(autofocus) nameInput?.focus();
- });
- </script>
- <form class="composer" class:compact onsubmit={addIngredient}>
- {#if !compact}
- <h1>Ingredients</h1>
- <p class="hint">Add as many as you like</p>
- {/if}
- <div class="row">
- <input
- type="text"
- placeholder="Type an ingredient"
- aria-label="Ingredient name"
- bind:this={nameInput}
- bind:value={name}
- >
- <button class="add" type="submit">Add</button>
- </div>
- {#if ingredients.length}
- <ul>
- {#each ingredients as ingredient, idx}
- <li>
- <button
- type="button"
- onclick={()=>ingredients.splice(idx, 1)}
- aria-label="Remove {ingredient}"
- >{ingredient}</button>
- </li>
- {/each}
- </ul>
- {/if}
- <button class="search" type="button" onclick={searchRecipes}>
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true">
- <path d="M17 17L21 21" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"></path>
- <path d="M3 11C3 15.4183 6.58172 19 11 19C13.213 19 15.2161 18.1015 16.6644 16.6493C18.1077 15.2022 19 13.2053 19 11C19 6.58172 15.4183 3 11 3C6.58172 3 3 6.58172 3 11Z" stroke="currentColor" stroke-width="1.8"></path>
- </svg>
- Search recipes
- </button>
- </form>
- <style>
- .composer{
- display: flex;
- flex-direction: column;
- gap: 0.75rem;
- width: min(480px, 100%);
- padding: 1.25rem;
- background: var(--color-surface);
- border: 1px solid var(--color-border);
- border-radius: 12px;
- }
- .composer.compact{
- width: 100%;
- padding: 1rem;
- }
- h1{
- margin: 0;
- font-size: clamp(1.25rem, 1rem + 1.4vw, 1.6rem);
- font-weight: 700;
- line-height: 1.2;
- text-align: center;
- color: var(--color-text);
- }
- .hint{
- margin: 0;
- text-align: center;
- color: var(--color-text-muted);
- font-size: 0.95rem;
- line-height: 1.35;
- }
- .row{
- display: flex;
- align-items: stretch;
- gap: 0.5rem;
- min-width: 0;
- }
- input{
- flex: 1;
- min-width: 0;
- border: 1px solid var(--color-border);
- outline: none;
- background: var(--color-surface-alt);
- color: var(--color-text);
- border-radius: 8px;
- padding: 0.7rem 0.9rem;
- font-size: 1rem;
- font-family: inherit;
- }
- input:focus{
- border-color: var(--color-primary);
- }
- .add{
- flex-shrink: 0;
- margin: 0;
- border: 1px solid var(--color-border);
- outline: none;
- cursor: pointer;
- background: var(--color-surface-alt);
- color: var(--color-text);
- border-radius: 8px;
- padding: 0.7rem 1.1rem;
- font-weight: 600;
- font-size: 1rem;
- font-family: inherit;
- line-height: 1.2;
- }
- .add:hover{
- border-color: var(--color-primary);
- color: var(--color-primary);
- }
- .add:active{
- transform: translateY(1px);
- }
- .search{
- display: inline-flex;
- align-items: center;
- justify-content: center;
- align-self: center;
- gap: 0.45rem;
- margin: 0.5rem 0 0;
- border: none;
- outline: none;
- cursor: pointer;
- background: var(--color-primary);
- color: var(--color-text-on-primary);
- border-radius: 999px;
- padding: 0.7rem 1.35rem;
- font-weight: 600;
- font-size: 1rem;
- font-family: inherit;
- line-height: 1;
- white-space: nowrap;
- transition: background 0.15s ease, transform 0.05s ease;
- }
- .search:hover{
- background: var(--color-primary-hover);
- }
- .search:active{
- transform: translateY(1px);
- }
- .search svg{
- display: block;
- flex-shrink: 0;
- }
- ul{
- display: flex;
- flex-wrap: wrap;
- gap: 0.45rem;
- margin: 0.25rem 0 0;
- padding: 0;
- list-style: none;
- }
- li button{
- display: inline-flex;
- align-items: center;
- max-width: 100%;
- margin: 0;
- border: 1px solid var(--color-border);
- outline: none;
- cursor: pointer;
- background: var(--color-surface-alt);
- color: var(--color-text);
- border-radius: 999px;
- padding: 0.4rem 0.8rem;
- font-size: 0.95rem;
- font-family: inherit;
- line-height: 1.3;
- overflow-wrap: anywhere;
- word-break: break-word;
- }
- li button:hover{
- border-color: var(--color-primary);
- }
- @media (min-width: 640px){
- .composer:not(.compact){
- padding: 1.5rem;
- }
- }
- @media (pointer: coarse){
- .add,
- .search,
- input,
- li button{
- min-height: 44px;
- }
- }
- </style>
|