|
@@ -0,0 +1,262 @@
|
|
|
|
|
+<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>
|