IngredientSearch.svelte 6.3 KB

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