Sfoglia il codice sorgente

Create the layout and style for the user dashboard page

Lee Morgan 2 settimane fa
parent
commit
01241a90d2

BIN
src/lib/logo.jpg


+ 14 - 4
src/lib/server/auth.js

@@ -1,18 +1,28 @@
-import {redirect} from "@sveltejs/kit";
+import {redirect, error} from "@sveltejs/kit";
 import jwt from "jsonwebtoken";
 import jwt from "jsonwebtoken";
 import {JWT_SECRET} from "$env/static/private";
 import {JWT_SECRET} from "$env/static/private";
 import User from "$lib/server/models/User.js";
 import User from "$lib/server/models/User.js";
 
 
-export default async function userAuth(cookies){
+export default async function userAuth(cookies, rdrct = true){
     const userCookie = cookies.get("user");
     const userCookie = cookies.get("user");
-    if(!userCookie) redirect(303, "/");
+    if(!userCookie){
+        if(rdrct){
+            redirect(303, "/");
+        }else{
+            error(401, "Unauthorized");
+        }
+    }
 
 
     let user;
     let user;
     try{
     try{
         const userData = jwt.verify(userCookie, JWT_SECRET);
         const userData = jwt.verify(userCookie, JWT_SECRET);
         user = await User.findOne({_id: userData.id, token: userData.token});
         user = await User.findOne({_id: userData.id, token: userData.token});
     }catch{
     }catch{
-        redirect(303, "/");
+        if(rdrct){
+            redirect(303, "/");
+        }else{
+            error(403, "Forbidden");
+        }
     }
     }
 
 
     return user;
     return user;

+ 2 - 6
src/lib/server/db.js

@@ -1,10 +1,6 @@
 import mongoose from "mongoose";
 import mongoose from "mongoose";
 import {MONGODB_URI} from "$env/static/private";
 import {MONGODB_URI} from "$env/static/private";
 
 
-let isConnected = false;
+if(mongoose.connection.readyState === 0) mongoose.connect(MONGODB_URI);
 
 
-export async function connectDB() {
-    if(isConnected) return;
-    await mongoose.connect(MONGODB_URI);
-    isConnected = true;
-}
+export default mongoose;

+ 13 - 3
src/lib/server/models/Recipe.js

@@ -1,3 +1,4 @@
+import "$lib/server/db.js";
 import mongoose from "mongoose";
 import mongoose from "mongoose";
 
 
 const recipeSchema = new mongoose.Schema({
 const recipeSchema = new mongoose.Schema({
@@ -17,7 +18,8 @@ const recipeSchema = new mongoose.Schema({
         },
         },
         unit: {
         unit: {
             type: String,
             type: String,
-            required: true
+            required: true,
+            default: ""
         },
         },
         name: {
         name: {
             type: String,
             type: String,
@@ -25,7 +27,15 @@ const recipeSchema = new mongoose.Schema({
         }
         }
     }],
     }],
     steps: [String],
     steps: [String],
-    notes: String
+    notes: String,
+    createdAt: {
+        type: Date,
+        required: true
+    },
+    updateAt: {
+        type: Date,
+        required: true
+    }
 });
 });
 
 
-export default mongoose.model("recipes", recipeSchema);
+export default mongoose.models.recipes || mongoose.model("recipes", recipeSchema);

+ 2 - 1
src/lib/server/models/User.js

@@ -1,3 +1,4 @@
+import "$lib/server/db.js";
 import mongoose from "mongoose";
 import mongoose from "mongoose";
 
 
 const userSchema = new mongoose.Schema({
 const userSchema = new mongoose.Schema({
@@ -19,4 +20,4 @@ const userSchema = new mongoose.Schema({
     }
     }
 });
 });
 
 
-export default mongoose.model("users", userSchema);
+export default mongoose.models.users || mongoose.model("users", userSchema);

+ 5 - 2
src/routes/+layout.svelte

@@ -18,12 +18,15 @@
         <img src={logo} alt="Site logo">
         <img src={logo} alt="Site logo">
     </a>
     </a>
 
 
+    {#if data.loggedIn && page.url.pathname !== "/dashboard"}
+        <a href="/dashboard" class="button">Dashboard</a>
+    {/if}
+
     {#if page.url.pathname === "/"}
     {#if page.url.pathname === "/"}
-        {#if data.loggedIn === false}
+        {#if !data.loggedIn}
             <a href="/login" class="button">Sign In</a>
             <a href="/login" class="button">Sign In</a>
             <a href="/register" class="button">Sign Up</a>
             <a href="/register" class="button">Sign Up</a>
         {:else}
         {:else}
-            <a href="dashboard" class="button">Dashboard</a>
             <a href="logout" class="button">Log Out</a>
             <a href="logout" class="button">Log Out</a>
         {/if}
         {/if}
     {/if}
     {/if}

+ 11 - 2
src/routes/dashboard/+page.server.js

@@ -2,6 +2,15 @@ import Recipe from "$lib/server/models/Recipe.js";
 import userAuth from "$lib/server/auth.js";
 import userAuth from "$lib/server/auth.js";
 
 
 export async function load({cookies}){
 export async function load({cookies}){
-    const user = await userAuth(cookies);
-    return {recipes: await Recipe.find({user: user._id})};
+    const userCookie = await userAuth(cookies);
+    const recipes = await Recipe.aggregate([
+        {$match: {user: userCookie._id}},
+        {$project: {
+            _id: 0,
+            id: {$toString: "$_id"},
+            name: 1,
+            updatedAt: 1
+        }}
+    ]);
+    return {recipes};
 }
 }

+ 124 - 3
src/routes/dashboard/+page.svelte

@@ -2,7 +2,128 @@
     let {data} = $props();
     let {data} = $props();
 </script>
 </script>
 
 
-<div>
-    <a href="/recipe/new">New Recipe</a>
-    <h1>Recipes</h1>
+<div class="container">
+    <div class="topbar">
+        <h1>Recipes</h1>
+        <a href="/recipe/new" class="button">New Recipe</a>
+    </div>
+
+    <div class="recipes">
+        {#each data.recipes as r}
+            <article class="recipe">
+                <p class="name">{r.name}</p>
+                <p class="date">{r.updatedAt ? new Date(r.updatedAt).toLocaleDateString() : ""}</p>
+            </article>
+        {:else}
+            <p class="empty">No recipes yet. Create your first one.</p>
+        {/each}
+    </div>
 </div>
 </div>
+
+<style>
+    .container{
+        width: min(720px, 100%);
+        min-height: calc(100% - 75px);
+        min-height: calc(100dvh - 75px);
+        margin: 0 auto;
+        padding: 1rem max(0.75rem, env(safe-area-inset-right, 0px)) max(2rem, env(safe-area-inset-bottom, 0px)) max(0.75rem, env(safe-area-inset-left, 0px));
+        color: var(--color-text);
+        overflow-x: clip;
+    }
+
+    .topbar{
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        gap: 0.75rem 1rem;
+        margin-bottom: 1.25rem;
+    }
+
+    h1{
+        font-size: clamp(1.35rem, 1rem + 1.6vw, 1.75rem);
+        font-weight: 700;
+        line-height: 1.2;
+        min-width: 0;
+    }
+
+    .button{
+        margin: 0;
+        flex-shrink: 0;
+        text-align: center;
+    }
+
+    .recipes{
+        display: flex;
+        flex-direction: column;
+        gap: 0.5rem;
+    }
+
+    .recipe{
+        display: flex;
+        flex-direction: row;
+        align-items: baseline;
+        justify-content: space-between;
+        gap: 0.75rem 1.5rem;
+        min-width: 0;
+        padding: 0.85rem 1rem;
+        background: var(--color-surface);
+        border: 1px solid var(--color-border);
+        border-left: 3px solid var(--color-primary);
+        border-radius: 10px;
+    }
+
+    .name{
+        flex: 1;
+        min-width: 0;
+        font-size: 1.05rem;
+        font-weight: 600;
+        line-height: 1.35;
+        overflow-wrap: anywhere;
+    }
+
+    .date{
+        flex-shrink: 0;
+        color: var(--color-text-muted);
+        font-size: 0.85rem;
+        line-height: 1.3;
+        white-space: nowrap;
+        text-align: right;
+    }
+
+    .date:empty{
+        display: none;
+    }
+
+    .empty{
+        margin: 0;
+        padding: 2.5rem 1.25rem;
+        text-align: center;
+        color: var(--color-text-muted);
+        background: var(--color-surface);
+        border: 1px dashed var(--color-border);
+        border-radius: 10px;
+        font-size: 0.95rem;
+        line-height: 1.45;
+    }
+
+    @media (min-width: 560px){
+        .container{
+            padding: 1.5rem 1rem 3rem;
+        }
+    }
+
+    @media (min-width: 900px){
+        .container{
+            padding-top: 2rem;
+        }
+    }
+
+    @media (pointer: coarse){
+        .button{
+            min-height: 44px;
+            display: inline-flex;
+            align-items: center;
+            justify-content: center;
+        }
+    }
+</style>

+ 0 - 3
src/routes/login/+page.server.js

@@ -3,7 +3,6 @@ import User from "$lib/server/models/User.js";
 import bcrypt from "bcrypt";
 import bcrypt from "bcrypt";
 import jwt from "jsonwebtoken";
 import jwt from "jsonwebtoken";
 import {JWT_SECRET} from "$env/static/private";
 import {JWT_SECRET} from "$env/static/private";
-import {connectDB} from "$lib/server/db";
 
 
 export const actions = {
 export const actions = {
     default: async ({request, cookies})=>{
     default: async ({request, cookies})=>{
@@ -11,8 +10,6 @@ export const actions = {
         const email = data.get("email").toLowerCase();
         const email = data.get("email").toLowerCase();
         const password = data.get("password");
         const password = data.get("password");
 
 
-        await connectDB();
-
         const user = await User.findOne({email: email});
         const user = await User.findOne({email: email});
 
 
         if(!user) return fail(401, "Wrong email or password");
         if(!user) return fail(401, "Wrong email or password");

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

@@ -1,9 +1,10 @@
 <script>
 <script>
     import {notify} from "$lib/notifier.svelte.js";
     import {notify} from "$lib/notifier.svelte.js";
+    import {goto} from "$app/navigation";
 
 
-    let nameInput;
+    let nameInput, quantityInput, stepsInput;
     let name = $state("");
     let name = $state("");
-    let quantity = $state(0);
+    let quantity = $state();
     let unit = $state("");
     let unit = $state("");
     let ingredientName = $state("");
     let ingredientName = $state("");
     let ingredients = $state([]);
     let ingredients = $state([]);
@@ -11,9 +12,28 @@
     let steps = $state([]);
     let steps = $state([]);
     let notes = $state("");
     let notes = $state("");
 
 
-    const submit = (event)=>{
+    const submit = async (event)=>{
         event.preventDefault();
         event.preventDefault();
-        console.log("submitting");
+
+        if(!name){
+            notify("error", "Please enter recipe name");
+            nameInput.focus();
+            return;
+        }
+
+        const data = {name, ingredients, steps, notes};
+
+        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 = ()=>{
     const addIngredient = ()=>{
@@ -27,15 +47,18 @@
             name: ingredientName
             name: ingredientName
         });
         });
 
 
-        quantity = 0;
+        quantity = null;
         unit = "";
         unit = "";
         ingredientName = "";
         ingredientName = "";
+
+        quantityInput.focus();
     }
     }
 
 
     const addStep = ()=>{
     const addStep = ()=>{
         if(!stepString) return notify("error", "Please enter some text");
         if(!stepString) return notify("error", "Please enter some text");
         steps.push(stepString);
         steps.push(stepString);
         stepString = "";
         stepString = "";
+        stepsInput.focus();
     }
     }
 
 
     $effect(()=>{
     $effect(()=>{
@@ -61,7 +84,7 @@
             </ul>
             </ul>
 
 
             <label>Quantity
             <label>Quantity
-                <input type="number" min="0" bind:value={quantity}>
+                <input type="number" min="0" bind:value={quantity} bind:this={quantityInput}>
             </label>
             </label>
 
 
             <label>Unit
             <label>Unit
@@ -84,12 +107,12 @@
                 {/each}
                 {/each}
             </ol>
             </ol>
 
 
-            <textarea bind:value={stepString} rows="2"></textarea>
+            <textarea bind:value={stepString} rows="2" bind:this={stepsInput}></textarea>
 
 
             <button class="button" type="button" onclick={addStep}>Add</button>
             <button class="button" type="button" onclick={addStep}>Add</button>
         </div>
         </div>
 
 
-        <textarea bind:value={notes} rows="4"></textarea>
+        <textarea bind:value={notes} rows="4" placeholder="Notes"></textarea>
 
 
         <button>Create Recipe</button>
         <button>Create Recipe</button>
     </form>
     </form>

+ 31 - 0
src/routes/recipe/new/+server.js

@@ -0,0 +1,31 @@
+import auth from "$lib/server/auth.js";
+import Recipe from "$lib/server/models/Recipe.js";
+import {error, json} from "@sveltejs/kit";
+
+export async function POST({request, cookies}){
+    const user = await auth(cookies, false);
+    const body = await request.json();
+
+    if(!body.name) error(400, "Recipe name required");
+    if(body.ingredients.length <= 0) error(400, "Ingredients required");
+    if(body.steps.length <= 0) error(400, "Preparation steps required");
+
+    for(let i = 0; i < body.ingredients.length; i++){
+        if(body.ingredients[i].quantity <= 0) error(400, "Quantity must be greater than 0");
+        if(!body.ingredients[i].name) error(400, "Ingredient name required");
+    }
+
+    const recipe = new Recipe({
+        user: user._id,
+        name: body.name,
+        ingredients: body.ingredients,
+        steps: body.steps,
+        notes: body.notes,
+        createdAt: new Date(),
+        updatedAt: new Date()
+    });
+
+    await recipe.save();
+
+    return json({id: recipe._id.toString()}, {status: 201});
+}

+ 0 - 3
src/routes/register/+page.server.js

@@ -1,7 +1,6 @@
 import {fail, redirect} from "@sveltejs/kit";
 import {fail, redirect} from "@sveltejs/kit";
 import validator from "validator";
 import validator from "validator";
 import bcrypt from "bcrypt";
 import bcrypt from "bcrypt";
-import {connectDB} from "$lib/server/db";
 import User from "$lib/server/models/User.js";
 import User from "$lib/server/models/User.js";
 import {randomUUID} from "crypto";
 import {randomUUID} from "crypto";
 
 
@@ -21,8 +20,6 @@ export const actions = {
         if(password !== confirmPassword) return fail(400, "Passwords do not match");
         if(password !== confirmPassword) return fail(400, "Passwords do not match");
         if(password.length < 12) return fail(400, "Password must contain at least 12 characters");
         if(password.length < 12) return fail(400, "Password must contain at least 12 characters");
 
 
-        await connectDB();
-
         const checkUser = await User.findOne({email: email});
         const checkUser = await User.findOne({email: email});
         if(checkUser) return fail(400, "User with this email already exists");
         if(checkUser) return fail(400, "User with this email already exists");