IngredientSearch.svelte 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. <script>
  2. import {untrack} from "svelte";
  3. import {goto} from "$app/navigation";
  4. import {notify} from "$lib/notifier.svelte.js";
  5. let {
  6. startingIngredients,
  7. autofocus = false,
  8. compact = false
  9. } = $props();
  10. let nameInput;
  11. let name = $state("");
  12. let ingredients = $state(untrack(()=>startingIngredients ? [...startingIngredients] : []));
  13. const addIngredient = (event)=>{
  14. event.preventDefault();
  15. const value = name.trim();
  16. if(!value) return;
  17. ingredients.push(value);
  18. name = "";
  19. nameInput?.focus();
  20. };
  21. const searchRecipes = ()=>{
  22. const pending = name.trim();
  23. const terms = pending ? [...ingredients, pending] : [...ingredients];
  24. if(terms.length === 0){
  25. notify("error", "Add at least one ingredient");
  26. nameInput?.focus();
  27. return;
  28. }
  29. if(pending){
  30. ingredients.push(pending);
  31. name = "";
  32. }
  33. const params = new URLSearchParams();
  34. for(const term of terms) params.append("i", term);
  35. goto(`/search?${params}`);
  36. };
  37. $effect(()=>{
  38. if(autofocus) nameInput?.focus();
  39. });
  40. </script>
  41. <form class="composer" class:compact onsubmit={addIngredient}>
  42. {#if !compact}
  43. <h1>Ingredients</h1>
  44. <p class="hint">Add as many as you like</p>
  45. {/if}
  46. <div class="row">
  47. <input
  48. type="text"
  49. placeholder="Type an ingredient"
  50. aria-label="Ingredient name"
  51. bind:this={nameInput}
  52. bind:value={name}
  53. >
  54. <button class="add" type="submit">Add</button>
  55. </div>
  56. {#if ingredients.length}
  57. <ul>
  58. {#each ingredients as ingredient, idx}
  59. <li>
  60. <button
  61. type="button"
  62. onclick={()=>ingredients.splice(idx, 1)}
  63. aria-label="Remove {ingredient}"
  64. >{ingredient}</button>
  65. </li>
  66. {/each}
  67. </ul>
  68. {/if}
  69. <button class="search" type="button" onclick={searchRecipes}>
  70. <svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true">
  71. <path d="M17 17L21 21" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"></path>
  72. <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>
  73. </svg>
  74. Search recipes
  75. </button>
  76. </form>
  77. <style>
  78. .composer{
  79. display: flex;
  80. flex-direction: column;
  81. gap: 0.75rem;
  82. width: min(480px, 100%);
  83. padding: 1.25rem;
  84. background: var(--color-surface);
  85. border: 1px solid var(--color-border);
  86. border-radius: 12px;
  87. }
  88. .composer.compact{
  89. width: 100%;
  90. padding: 1rem;
  91. }
  92. h1{
  93. margin: 0;
  94. font-size: clamp(1.25rem, 1rem + 1.4vw, 1.6rem);
  95. font-weight: 700;
  96. line-height: 1.2;
  97. text-align: center;
  98. color: var(--color-text);
  99. }
  100. .hint{
  101. margin: 0;
  102. text-align: center;
  103. color: var(--color-text-muted);
  104. font-size: 0.95rem;
  105. line-height: 1.35;
  106. }
  107. .row{
  108. display: flex;
  109. align-items: stretch;
  110. gap: 0.5rem;
  111. min-width: 0;
  112. }
  113. input{
  114. flex: 1;
  115. min-width: 0;
  116. border: 1px solid var(--color-border);
  117. outline: none;
  118. background: var(--color-surface-alt);
  119. color: var(--color-text);
  120. border-radius: 8px;
  121. padding: 0.7rem 0.9rem;
  122. font-size: 1rem;
  123. font-family: inherit;
  124. }
  125. input:focus{
  126. border-color: var(--color-primary);
  127. }
  128. .add{
  129. flex-shrink: 0;
  130. margin: 0;
  131. border: 1px solid var(--color-border);
  132. outline: none;
  133. cursor: pointer;
  134. background: var(--color-surface-alt);
  135. color: var(--color-text);
  136. border-radius: 8px;
  137. padding: 0.7rem 1.1rem;
  138. font-weight: 600;
  139. font-size: 1rem;
  140. font-family: inherit;
  141. line-height: 1.2;
  142. }
  143. .add:hover{
  144. border-color: var(--color-primary);
  145. color: var(--color-primary);
  146. }
  147. .add:active{
  148. transform: translateY(1px);
  149. }
  150. .search{
  151. display: inline-flex;
  152. align-items: center;
  153. justify-content: center;
  154. align-self: center;
  155. gap: 0.45rem;
  156. margin: 0.5rem 0 0;
  157. border: none;
  158. outline: none;
  159. cursor: pointer;
  160. background: var(--color-primary);
  161. color: var(--color-text-on-primary);
  162. border-radius: 999px;
  163. padding: 0.7rem 1.35rem;
  164. font-weight: 600;
  165. font-size: 1rem;
  166. font-family: inherit;
  167. line-height: 1;
  168. white-space: nowrap;
  169. transition: background 0.15s ease, transform 0.05s ease;
  170. }
  171. .search:hover{
  172. background: var(--color-primary-hover);
  173. }
  174. .search:active{
  175. transform: translateY(1px);
  176. }
  177. .search svg{
  178. display: block;
  179. flex-shrink: 0;
  180. }
  181. ul{
  182. display: flex;
  183. flex-wrap: wrap;
  184. gap: 0.45rem;
  185. margin: 0.25rem 0 0;
  186. padding: 0;
  187. list-style: none;
  188. }
  189. li button{
  190. display: inline-flex;
  191. align-items: center;
  192. max-width: 100%;
  193. margin: 0;
  194. border: 1px solid var(--color-border);
  195. outline: none;
  196. cursor: pointer;
  197. background: var(--color-surface-alt);
  198. color: var(--color-text);
  199. border-radius: 999px;
  200. padding: 0.4rem 0.8rem;
  201. font-size: 0.95rem;
  202. font-family: inherit;
  203. line-height: 1.3;
  204. overflow-wrap: anywhere;
  205. word-break: break-word;
  206. }
  207. li button:hover{
  208. border-color: var(--color-primary);
  209. }
  210. @media (min-width: 640px){
  211. .composer:not(.compact){
  212. padding: 1.5rem;
  213. }
  214. }
  215. @media (pointer: coarse){
  216. .add,
  217. .search,
  218. input,
  219. li button{
  220. min-height: 44px;
  221. }
  222. }
  223. </style>