소스 검색

Move much of the new recipe stuff to a component

Lee Morgan 2 주 전
부모
커밋
07df9be0dd
3개의 변경된 파일495개의 추가작업 그리고 455개의 파일을 삭제
  1. 465 0
      src/routes/recipe/EditableRecipe.svelte
  2. 22 0
      src/routes/recipe/[recipeId]/edit/+page.server.js
  3. 8 455
      src/routes/recipe/new/+page.svelte

+ 465 - 0
src/routes/recipe/EditableRecipe.svelte

@@ -0,0 +1,465 @@
+<script>
+    import {notify} from "$lib/notifier.svelte.js";
+    import {goto} from "$app/navigation";
+
+    let nameInput, quantityInput, stepsInput;
+    let {
+        name,
+        ingredients: ingredientsProp,
+        steps: stepsProp,
+        notes,
+        visibility
+    } = $props();
+    let ingredients = $state(ingredientsProp);
+    let steps = $state(stepsProp);
+    let quantity = $state();
+    let unit = $state("");
+    let ingredientName = $state("");
+    let stepString = $state("");
+    $inspect(ingredients);
+
+    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("/recipe/new", {
+            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);
+
+        goto("/dashboard");
+    }
+
+    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}
+                <li>{i.name}, {i.quantity} {i.unit}</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}
+                <li>{s}</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>Create Recipe</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%;
+    }
+
+    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){
+        .container{
+            padding: 2rem 1rem 3rem;
+        }
+
+        .standardForm{
+            padding: 2rem;
+        }
+
+        .standardForm > textarea{
+            min-height: 6rem;
+        }
+    }
+
+    @media (max-height: 500px){
+        .container{
+            padding-top: 0.75rem;
+            padding-bottom: 1rem;
+        }
+
+        .standardForm > textarea{
+            min-height: 3.5rem;
+        }
+    }
+
+    @media (pointer: coarse){
+        .button,
+        .standardForm button,
+        .visibilityOptions span{
+            min-height: 44px;
+        }
+    }
+</style>

+ 22 - 0
src/routes/recipe/[recipeId]/edit/+page.server.js

@@ -0,0 +1,22 @@
+import auth from "$lib/server/auth.js";
+import Recipe from "$lib/server/models/Recipe.js";
+
+export async function load({cookies, params}){
+    const user = await auth(cookies);
+
+    let recipe;
+    try{
+        recipe = await Recipe.findOne({
+            _id: params.recipeId,
+            user: user._id
+        }, {"ingredients._id": 0}).lean();
+        
+        recipe.id = recipe._id.toString();
+        recipe._id = undefined;
+        recipe.user = undefined;
+
+        return recipe:
+    }catch{
+        return {recipe: null};
+    }
+}

+ 8 - 455
src/routes/recipe/new/+page.svelte

@@ -1,149 +1,15 @@
 <script>
-    import {notify} from "$lib/notifier.svelte.js";
-    import {goto} from "$app/navigation";
-
-    let nameInput, quantityInput, stepsInput;
-    let name = $state("");
-    let quantity = $state();
-    let unit = $state("");
-    let ingredientName = $state("");
-    let ingredients = $state([]);
-    let stepString = $state("");
-    let steps = $state([]);
-    let notes = $state("");
-    let visibility = $state("private");
-
-    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("/recipe/new", {
-            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);
-
-        goto("/dashboard");
-    }
-
-    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();
-    });
+    import EditableRecipe from "../EditableRecipe.svelte";
 </script>
 
 <div class="container">
-    <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}
-                    <li>{i.name}, {i.quantity} {i.unit}</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}
-                    <li>{s}</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>Create Recipe</button>
-    </form>
+    <EditableRecipe
+        name=""
+        ingredients={[]}
+        steps={[]}
+        notes=""
+        visibility="private"
+    />
 </div>
 
 <style>
@@ -160,317 +26,4 @@
         overflow-x: clip;
         box-sizing: border-box;
     }
-
-    .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%;
-    }
-
-    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){
-        .container{
-            padding: 2rem 1rem 3rem;
-        }
-
-        .standardForm{
-            padding: 2rem;
-        }
-
-        .standardForm > textarea{
-            min-height: 6rem;
-        }
-    }
-
-    @media (max-height: 500px){
-        .container{
-            padding-top: 0.75rem;
-            padding-bottom: 1rem;
-        }
-
-        .standardForm > textarea{
-            min-height: 3.5rem;
-        }
-    }
-
-    @media (pointer: coarse){
-        .button,
-        .standardForm button,
-        .visibilityOptions span{
-            min-height: 44px;
-        }
-    }
 </style>