+page.svelte 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. <script>
  2. import {notify} from "$lib/notifier.svelte.js";
  3. import {goto} from "$app/navigation";
  4. let nameInput, quantityInput, stepsInput;
  5. let name = $state("");
  6. let quantity = $state();
  7. let unit = $state("");
  8. let ingredientName = $state("");
  9. let ingredients = $state([]);
  10. let stepString = $state("");
  11. let steps = $state([]);
  12. let notes = $state("");
  13. const submit = async (event)=>{
  14. event.preventDefault();
  15. if(!name){
  16. notify("error", "Please enter recipe name");
  17. nameInput.focus();
  18. return;
  19. }
  20. const data = {name, ingredients, steps, notes};
  21. const response = await fetch("/recipe/new", {
  22. method: "POST",
  23. headers: {"Content-Type": "application/json"},
  24. credentials: "include",
  25. body: JSON.stringify(data)
  26. });
  27. const recipe = await response.json();
  28. if(!response.ok) return notify("error", recipe.message);
  29. goto("/dashboard");
  30. }
  31. const addIngredient = ()=>{
  32. if(quantity <= 0) return notify("error", "Quantity must be greater than 0");
  33. if(!unit) return notify("error", "Unit is required");
  34. if(!ingredientName) return notify("error", "Ingredient name is required");
  35. ingredients.push({
  36. quantity: quantity,
  37. unit: unit,
  38. name: ingredientName
  39. });
  40. quantity = null;
  41. unit = "";
  42. ingredientName = "";
  43. quantityInput.focus();
  44. }
  45. const addStep = ()=>{
  46. if(!stepString) return notify("error", "Please enter some text");
  47. steps.push(stepString);
  48. stepString = "";
  49. stepsInput.focus();
  50. }
  51. $effect(()=>{
  52. nameInput?.focus();
  53. });
  54. </script>
  55. <div class="container">
  56. <form class="standardForm" onsubmit={submit}>
  57. <h1>New Recipe</h1>
  58. <label>Name
  59. <input type="text" bind:this={nameInput} bind:value={name}>
  60. </label>
  61. <div class="ingredients">
  62. <h2>Ingredients</h2>
  63. <ul>
  64. {#each ingredients as i}
  65. <li>{i.name}, {i.quantity} {i.unit}</li>
  66. {/each}
  67. </ul>
  68. <label>Quantity
  69. <input type="number" min="0" bind:value={quantity} bind:this={quantityInput}>
  70. </label>
  71. <label>Unit
  72. <input type="text" bind:value={unit}>
  73. </label>
  74. <label>Name
  75. <input type="text" bind:value={ingredientName}>
  76. </label>
  77. <button class="button" type="button" onclick={addIngredient}>Add</button>
  78. </div>
  79. <div class="steps">
  80. <h2>Steps</h2>
  81. <ol>
  82. {#each steps as s}
  83. <li>{s}</li>
  84. {/each}
  85. </ol>
  86. <textarea bind:value={stepString} rows="2" bind:this={stepsInput}></textarea>
  87. <button class="button" type="button" onclick={addStep}>Add</button>
  88. </div>
  89. <textarea bind:value={notes} rows="4" placeholder="Notes"></textarea>
  90. <button>Create Recipe</button>
  91. </form>
  92. </div>
  93. <style>
  94. .container{
  95. display: flex;
  96. justify-content: center;
  97. align-items: flex-start;
  98. width: 100%;
  99. max-width: 100%;
  100. min-height: calc(100% - 75px);
  101. min-height: calc(100dvh - 75px);
  102. padding: 1rem max(0.75rem, env(safe-area-inset-right, 0px)) max(1.5rem, env(safe-area-inset-bottom, 0px)) max(0.75rem, env(safe-area-inset-left, 0px));
  103. color: var(--color-text);
  104. overflow-x: clip;
  105. box-sizing: border-box;
  106. }
  107. .standardForm{
  108. width: min(640px, 100%);
  109. max-width: 100%;
  110. min-width: 0;
  111. padding: 1.15rem 0.9rem;
  112. container-type: inline-size;
  113. container-name: recipe-form;
  114. }
  115. h1{
  116. font-size: clamp(1.25rem, 1rem + 2vw, 1.5rem);
  117. }
  118. h2{
  119. color: var(--color-text);
  120. font-size: clamp(1rem, 0.9rem + 0.5vw, 1.1rem);
  121. font-weight: 700;
  122. letter-spacing: 0.01em;
  123. }
  124. input,
  125. textarea{
  126. width: 100%;
  127. min-width: 0;
  128. max-width: 100%;
  129. font-size: 1rem;
  130. }
  131. .ingredients,
  132. .steps{
  133. display: flex;
  134. flex-direction: column;
  135. gap: 0.75rem;
  136. padding: 0.75rem;
  137. min-width: 0;
  138. max-width: 100%;
  139. background: var(--color-surface-alt);
  140. border: 1px solid var(--color-border);
  141. border-radius: 8px;
  142. }
  143. ul,
  144. ol{
  145. margin: 0;
  146. padding: 0;
  147. display: flex;
  148. flex-direction: column;
  149. gap: 0.45rem;
  150. list-style: none;
  151. min-width: 0;
  152. max-width: 100%;
  153. }
  154. ul:not(:has(li)),
  155. ol:not(:has(li)){
  156. display: none;
  157. }
  158. li{
  159. padding: 0.55rem 0.7rem;
  160. background: var(--color-surface);
  161. border: 1px solid var(--color-border);
  162. border-left: 3px solid var(--color-primary);
  163. border-radius: 6px;
  164. font-size: 0.95rem;
  165. line-height: 1.4;
  166. color: var(--color-text);
  167. overflow-wrap: anywhere;
  168. word-break: break-word;
  169. min-width: 0;
  170. max-width: 100%;
  171. }
  172. ol{
  173. counter-reset: step;
  174. }
  175. ol li{
  176. display: grid;
  177. grid-template-columns: 1.5rem minmax(0, 1fr);
  178. align-items: start;
  179. gap: 0.7rem;
  180. counter-increment: step;
  181. }
  182. ol li::before{
  183. content: counter(step);
  184. flex-shrink: 0;
  185. display: flex;
  186. align-items: center;
  187. justify-content: center;
  188. width: 1.5rem;
  189. height: 1.5rem;
  190. border-radius: 50%;
  191. background: var(--color-primary);
  192. color: var(--color-text-on-primary);
  193. font-size: 0.75rem;
  194. font-weight: 700;
  195. line-height: 1;
  196. }
  197. .ingredients{
  198. display: grid;
  199. grid-template-columns: minmax(0, 1fr);
  200. align-items: stretch;
  201. gap: 0.75rem;
  202. }
  203. .ingredients > *{
  204. min-width: 0;
  205. max-width: 100%;
  206. }
  207. .button{
  208. margin: 0;
  209. width: 100%;
  210. height: auto;
  211. min-height: 2.5rem;
  212. white-space: nowrap;
  213. }
  214. textarea{
  215. resize: vertical;
  216. min-height: 3.5rem;
  217. }
  218. .standardForm > textarea{
  219. min-height: 5rem;
  220. }
  221. @container recipe-form (min-width: 260px){
  222. .ingredients{
  223. grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  224. align-items: end;
  225. }
  226. .ingredients h2,
  227. .ingredients ul,
  228. .ingredients label:last-of-type,
  229. .ingredients .button{
  230. grid-column: 1 / -1;
  231. }
  232. }
  233. @container recipe-form (min-width: 480px){
  234. .ingredients,
  235. .steps{
  236. padding: 1rem;
  237. }
  238. .ingredients{
  239. grid-template-columns: minmax(0, 5.5rem) minmax(0, 6.5rem) minmax(0, 1fr) auto;
  240. align-items: end;
  241. }
  242. .ingredients h2,
  243. .ingredients ul{
  244. grid-column: 1 / -1;
  245. }
  246. .ingredients label:last-of-type,
  247. .ingredients .button{
  248. grid-column: auto;
  249. }
  250. .ingredients .button{
  251. width: auto;
  252. min-height: 0;
  253. height: fit-content;
  254. }
  255. }
  256. @media (min-width: 640px){
  257. .container{
  258. padding: 2rem 1rem 3rem;
  259. }
  260. .standardForm{
  261. padding: 2rem;
  262. }
  263. .standardForm > textarea{
  264. min-height: 6rem;
  265. }
  266. }
  267. @media (max-height: 500px){
  268. .container{
  269. padding-top: 0.75rem;
  270. padding-bottom: 1rem;
  271. }
  272. .standardForm > textarea{
  273. min-height: 3.5rem;
  274. }
  275. }
  276. @media (pointer: coarse){
  277. .button,
  278. .standardForm button{
  279. min-height: 44px;
  280. }
  281. }
  282. </style>