|
|
@@ -0,0 +1,500 @@
|
|
|
+<script>
|
|
|
+ import {notify} from "$lib/notifier.svelte.js";
|
|
|
+ import {goto} from "$app/navigation";
|
|
|
+
|
|
|
+ let nameInput, quantityInput, stepsInput;
|
|
|
+ let {
|
|
|
+ id,
|
|
|
+ name,
|
|
|
+ ingredients: ingredientsProp,
|
|
|
+ steps: stepsProp,
|
|
|
+ notes,
|
|
|
+ visibility,
|
|
|
+ route
|
|
|
+ } = $props();
|
|
|
+ let ingredients = $state(ingredientsProp);
|
|
|
+ let steps = $state(stepsProp);
|
|
|
+ let quantity = $state();
|
|
|
+ let unit = $state("");
|
|
|
+ let ingredientName = $state("");
|
|
|
+ let stepString = $state("");
|
|
|
+
|
|
|
+ const submit = async (event)=>{
|
|
|
+ event.preventDefault();
|
|
|
+
|
|
|
+ if(!name){
|
|
|
+ notify("error", "Please enter recipe name");
|
|
|
+ nameInput.focus();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const data = {
|
|
|
+ name, ingredients, steps, notes,
|
|
|
+ private: visibility === "private" ? true : false
|
|
|
+ };
|
|
|
+
|
|
|
+ const response = await fetch(route, {
|
|
|
+ method: "POST",
|
|
|
+ headers: {"Content-Type": "application/json"},
|
|
|
+ credentials: "include",
|
|
|
+ body: JSON.stringify(data)
|
|
|
+ });
|
|
|
+
|
|
|
+ const recipe = await response.json();
|
|
|
+ if(!response.ok) return notify("error", recipe.message);
|
|
|
+
|
|
|
+ let redirect = route === "/recipe/new" ? "/dashboard" : `/recipe/${id}`;
|
|
|
+ goto(redirect);
|
|
|
+ }
|
|
|
+
|
|
|
+ const addIngredient = ()=>{
|
|
|
+ if(quantity <= 0) return notify("error", "Quantity must be greater than 0");
|
|
|
+ if(!unit) return notify("error", "Unit is required");
|
|
|
+ if(!ingredientName) return notify("error", "Ingredient name is required");
|
|
|
+
|
|
|
+ ingredients.push({
|
|
|
+ quantity: quantity,
|
|
|
+ unit: unit,
|
|
|
+ name: ingredientName
|
|
|
+ });
|
|
|
+
|
|
|
+ quantity = null;
|
|
|
+ unit = "";
|
|
|
+ ingredientName = "";
|
|
|
+
|
|
|
+ quantityInput.focus();
|
|
|
+ }
|
|
|
+
|
|
|
+ const addStep = ()=>{
|
|
|
+ if(!stepString) return notify("error", "Please enter some text");
|
|
|
+ steps.push(stepString);
|
|
|
+ stepString = "";
|
|
|
+ stepsInput.focus();
|
|
|
+ }
|
|
|
+
|
|
|
+ $effect(()=>{
|
|
|
+ nameInput?.focus();
|
|
|
+ });
|
|
|
+</script>
|
|
|
+
|
|
|
+<form class="standardForm" onsubmit={submit}>
|
|
|
+ <h1>New Recipe</h1>
|
|
|
+
|
|
|
+ <label>Name
|
|
|
+ <input type="text" bind:this={nameInput} bind:value={name}>
|
|
|
+ </label>
|
|
|
+
|
|
|
+ <div class="ingredients">
|
|
|
+ <h2>Ingredients</h2>
|
|
|
+
|
|
|
+ <ul>
|
|
|
+ {#each ingredients as i, idx}
|
|
|
+ <li><button
|
|
|
+ onclick={()=>ingredients.splice(idx, 1)}
|
|
|
+ type="button"
|
|
|
+ >{i.name}, {i.quantity} {i.unit}</button></li>
|
|
|
+ {/each}
|
|
|
+ </ul>
|
|
|
+
|
|
|
+ <label>Quantity
|
|
|
+ <input type="number" min="0" bind:value={quantity} bind:this={quantityInput}>
|
|
|
+ </label>
|
|
|
+
|
|
|
+ <label>Unit
|
|
|
+ <input type="text" bind:value={unit}>
|
|
|
+ </label>
|
|
|
+
|
|
|
+ <label>Name
|
|
|
+ <input type="text" bind:value={ingredientName}>
|
|
|
+ </label>
|
|
|
+
|
|
|
+ <button class="button" type="button" onclick={addIngredient}>Add</button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="steps">
|
|
|
+ <h2>Steps</h2>
|
|
|
+
|
|
|
+ <ol>
|
|
|
+ {#each steps as s, idx}
|
|
|
+ <li><button
|
|
|
+ onclick={()=>{steps.splice(idx, 1)}}
|
|
|
+ type="button"
|
|
|
+ >{s}</button></li>
|
|
|
+ {/each}
|
|
|
+ </ol>
|
|
|
+
|
|
|
+ <textarea bind:value={stepString} rows="2" bind:this={stepsInput}></textarea>
|
|
|
+
|
|
|
+ <button class="button" type="button" onclick={addStep}>Add</button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <textarea bind:value={notes} rows="4" placeholder="Notes"></textarea>
|
|
|
+
|
|
|
+ <fieldset class="visibility">
|
|
|
+ <legend>Visibility</legend>
|
|
|
+ <div class="visibilityOptions">
|
|
|
+ <label>
|
|
|
+ <input
|
|
|
+ type="radio"
|
|
|
+ name="visibility"
|
|
|
+ value="private"
|
|
|
+ bind:group={visibility}
|
|
|
+ >
|
|
|
+ <span>Private</span>
|
|
|
+ </label>
|
|
|
+ <label>
|
|
|
+ <input
|
|
|
+ type="radio"
|
|
|
+ name="visibility"
|
|
|
+ value="public"
|
|
|
+ bind:group={visibility}
|
|
|
+ >
|
|
|
+ <span>Public</span>
|
|
|
+ </label>
|
|
|
+ </div>
|
|
|
+ </fieldset>
|
|
|
+
|
|
|
+ <button class="button">
|
|
|
+ {#if route === "/recipe/new"}
|
|
|
+ Create Recipe
|
|
|
+ {:else}
|
|
|
+ Update Recipe
|
|
|
+ {/if}
|
|
|
+ </button>
|
|
|
+</form>
|
|
|
+
|
|
|
+<style>
|
|
|
+ .standardForm{
|
|
|
+ width: min(640px, 100%);
|
|
|
+ max-width: 100%;
|
|
|
+ min-width: 0;
|
|
|
+ padding: 1.15rem 0.9rem;
|
|
|
+ container-type: inline-size;
|
|
|
+ container-name: recipe-form;
|
|
|
+ }
|
|
|
+
|
|
|
+ h1{
|
|
|
+ font-size: clamp(1.25rem, 1rem + 2vw, 1.5rem);
|
|
|
+ }
|
|
|
+
|
|
|
+ h2{
|
|
|
+ color: var(--color-text);
|
|
|
+ font-size: clamp(1rem, 0.9rem + 0.5vw, 1.1rem);
|
|
|
+ font-weight: 700;
|
|
|
+ letter-spacing: 0.01em;
|
|
|
+ }
|
|
|
+
|
|
|
+ input,
|
|
|
+ textarea{
|
|
|
+ width: 100%;
|
|
|
+ min-width: 0;
|
|
|
+ max-width: 100%;
|
|
|
+ font-size: 1rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .ingredients,
|
|
|
+ .steps{
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 0.75rem;
|
|
|
+ padding: 0.75rem;
|
|
|
+ min-width: 0;
|
|
|
+ max-width: 100%;
|
|
|
+ background: var(--color-surface-alt);
|
|
|
+ border: 1px solid var(--color-border);
|
|
|
+ border-radius: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ ul,
|
|
|
+ ol{
|
|
|
+ margin: 0;
|
|
|
+ padding: 0;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 0.45rem;
|
|
|
+ list-style: none;
|
|
|
+ min-width: 0;
|
|
|
+ max-width: 100%;
|
|
|
+ }
|
|
|
+
|
|
|
+ ul:not(:has(li)),
|
|
|
+ ol:not(:has(li)){
|
|
|
+ display: none;
|
|
|
+ }
|
|
|
+
|
|
|
+ li{
|
|
|
+ padding: 0.55rem 0.7rem;
|
|
|
+ background: var(--color-surface);
|
|
|
+ border: 1px solid var(--color-border);
|
|
|
+ border-left: 3px solid var(--color-primary);
|
|
|
+ border-radius: 6px;
|
|
|
+ font-size: 0.95rem;
|
|
|
+ line-height: 1.4;
|
|
|
+ color: var(--color-text);
|
|
|
+ overflow-wrap: anywhere;
|
|
|
+ word-break: break-word;
|
|
|
+ min-width: 0;
|
|
|
+ max-width: 100%;
|
|
|
+ }
|
|
|
+
|
|
|
+ ul li{
|
|
|
+ padding: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ li button{
|
|
|
+ display: block;
|
|
|
+ width: 100%;
|
|
|
+ margin: 0;
|
|
|
+ padding: 0.55rem 0.7rem;
|
|
|
+ border: none;
|
|
|
+ outline: none;
|
|
|
+ background: none;
|
|
|
+ color: inherit;
|
|
|
+ font: inherit;
|
|
|
+ font-size: inherit;
|
|
|
+ line-height: inherit;
|
|
|
+ text-align: left;
|
|
|
+ cursor: pointer;
|
|
|
+ overflow-wrap: anywhere;
|
|
|
+ word-break: break-word;
|
|
|
+ }
|
|
|
+
|
|
|
+ ol li button{
|
|
|
+ padding: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ ul li:hover,
|
|
|
+ ol li:hover{
|
|
|
+ border-color: var(--color-primary);
|
|
|
+ }
|
|
|
+
|
|
|
+ ol{
|
|
|
+ counter-reset: step;
|
|
|
+ }
|
|
|
+
|
|
|
+ ol li{
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 1.5rem minmax(0, 1fr);
|
|
|
+ align-items: start;
|
|
|
+ gap: 0.7rem;
|
|
|
+ counter-increment: step;
|
|
|
+ }
|
|
|
+
|
|
|
+ ol li::before{
|
|
|
+ content: counter(step);
|
|
|
+ flex-shrink: 0;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ width: 1.5rem;
|
|
|
+ height: 1.5rem;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: var(--color-primary);
|
|
|
+ color: var(--color-text-on-primary);
|
|
|
+ font-size: 0.75rem;
|
|
|
+ font-weight: 700;
|
|
|
+ line-height: 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ .ingredients{
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: minmax(0, 1fr);
|
|
|
+ align-items: stretch;
|
|
|
+ gap: 0.75rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .ingredients > *{
|
|
|
+ min-width: 0;
|
|
|
+ max-width: 100%;
|
|
|
+ }
|
|
|
+
|
|
|
+ .button{
|
|
|
+ margin: 0;
|
|
|
+ width: 100%;
|
|
|
+ height: auto;
|
|
|
+ min-height: 2.5rem;
|
|
|
+ white-space: nowrap;
|
|
|
+ }
|
|
|
+
|
|
|
+ textarea{
|
|
|
+ resize: vertical;
|
|
|
+ min-height: 3.5rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .standardForm > textarea{
|
|
|
+ min-height: 5rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .visibility{
|
|
|
+ border: 0;
|
|
|
+ margin: 0;
|
|
|
+ padding: 0;
|
|
|
+ min-width: 0;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 0.4rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .visibility legend{
|
|
|
+ float: left;
|
|
|
+ width: 100%;
|
|
|
+ padding: 0;
|
|
|
+ margin: 0;
|
|
|
+ color: var(--color-text);
|
|
|
+ font-size: 0.9rem;
|
|
|
+ font-weight: 600;
|
|
|
+ }
|
|
|
+
|
|
|
+ .visibilityOptions{
|
|
|
+ position: relative;
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 1fr 1fr;
|
|
|
+ gap: 0;
|
|
|
+ width: 100%;
|
|
|
+ clear: both;
|
|
|
+ padding: 0.2rem;
|
|
|
+ background: var(--color-surface-alt);
|
|
|
+ border: 1px solid var(--color-border);
|
|
|
+ border-radius: 999px;
|
|
|
+ isolation: isolate;
|
|
|
+ }
|
|
|
+
|
|
|
+ .visibilityOptions::before{
|
|
|
+ content: "";
|
|
|
+ position: absolute;
|
|
|
+ top: 0.2rem;
|
|
|
+ left: 0.2rem;
|
|
|
+ width: calc(50% - 0.2rem);
|
|
|
+ height: calc(100% - 0.4rem);
|
|
|
+ background: var(--color-primary);
|
|
|
+ border-radius: 999px;
|
|
|
+ transition: transform 0.2s ease;
|
|
|
+ pointer-events: none;
|
|
|
+ z-index: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .visibilityOptions:has(input[value="public"]:checked)::before{
|
|
|
+ transform: translateX(100%);
|
|
|
+ }
|
|
|
+
|
|
|
+ .visibilityOptions label{
|
|
|
+ position: relative;
|
|
|
+ z-index: 1;
|
|
|
+ flex-direction: row;
|
|
|
+ align-items: stretch;
|
|
|
+ justify-content: stretch;
|
|
|
+ gap: 0;
|
|
|
+ margin: 0;
|
|
|
+ cursor: pointer;
|
|
|
+ }
|
|
|
+
|
|
|
+ .visibilityOptions input{
|
|
|
+ position: absolute;
|
|
|
+ inset: 0;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ margin: 0;
|
|
|
+ padding: 0;
|
|
|
+ opacity: 0;
|
|
|
+ border: 0;
|
|
|
+ background: none;
|
|
|
+ appearance: none;
|
|
|
+ cursor: pointer;
|
|
|
+ }
|
|
|
+
|
|
|
+ .visibilityOptions span{
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ width: 100%;
|
|
|
+ min-height: 2.5rem;
|
|
|
+ padding: 0.45rem 0.8rem;
|
|
|
+ background: transparent;
|
|
|
+ border: 0;
|
|
|
+ border-radius: 999px;
|
|
|
+ color: var(--color-text-muted);
|
|
|
+ font-size: 0.95rem;
|
|
|
+ font-weight: 600;
|
|
|
+ line-height: 1.2;
|
|
|
+ text-align: center;
|
|
|
+ transition: color 0.15s ease;
|
|
|
+ }
|
|
|
+
|
|
|
+ .visibilityOptions input:hover + span{
|
|
|
+ color: var(--color-text);
|
|
|
+ }
|
|
|
+
|
|
|
+ .visibilityOptions input:checked + span{
|
|
|
+ color: var(--color-text-on-primary);
|
|
|
+ }
|
|
|
+
|
|
|
+ .visibilityOptions input:focus-visible + span{
|
|
|
+ outline: 2px solid var(--color-primary);
|
|
|
+ outline-offset: 2px;
|
|
|
+ }
|
|
|
+
|
|
|
+ @container recipe-form (min-width: 260px){
|
|
|
+ .ingredients{
|
|
|
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
|
|
+ align-items: end;
|
|
|
+ }
|
|
|
+
|
|
|
+ .ingredients h2,
|
|
|
+ .ingredients ul,
|
|
|
+ .ingredients label:last-of-type,
|
|
|
+ .ingredients .button{
|
|
|
+ grid-column: 1 / -1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @container recipe-form (min-width: 480px){
|
|
|
+ .ingredients,
|
|
|
+ .steps{
|
|
|
+ padding: 1rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .ingredients{
|
|
|
+ grid-template-columns: minmax(0, 5.5rem) minmax(0, 6.5rem) minmax(0, 1fr) auto;
|
|
|
+ align-items: end;
|
|
|
+ }
|
|
|
+
|
|
|
+ .ingredients h2,
|
|
|
+ .ingredients ul{
|
|
|
+ grid-column: 1 / -1;
|
|
|
+ }
|
|
|
+
|
|
|
+ .ingredients label:last-of-type,
|
|
|
+ .ingredients .button{
|
|
|
+ grid-column: auto;
|
|
|
+ }
|
|
|
+
|
|
|
+ .ingredients .button{
|
|
|
+ width: auto;
|
|
|
+ min-height: 0;
|
|
|
+ height: fit-content;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @media (min-width: 640px){
|
|
|
+ .standardForm{
|
|
|
+ padding: 2rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .standardForm > textarea{
|
|
|
+ min-height: 6rem;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @media (max-height: 500px){
|
|
|
+ .standardForm > textarea{
|
|
|
+ min-height: 3.5rem;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @media (pointer: coarse){
|
|
|
+ .button,
|
|
|
+ .visibilityOptions span{
|
|
|
+ min-height: 44px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+</style>
|