Compare commits

...

4 Commits

Author SHA1 Message Date
taogaetz
de38a482e1 update master 2025-12-03 14:20:20 -05:00
taogaetz
278a5b44c4 edit readme 2025-12-03 14:14:32 -05:00
taogaetz
1c0ce2d741 added categories for dish and ingredient 2025-12-03 14:01:37 -05:00
taogaetz
f241b06336 update docker compose 2025-12-03 12:47:23 -05:00
14 changed files with 262 additions and 15 deletions

View File

@ -155,7 +155,3 @@ src/
3. Make your changes 3. Make your changes
4. Run tests and linting: `npm run check` 4. Run tests and linting: `npm run check`
5. Submit a pull request 5. Submit a pull request
## License
[Add your license here]

View File

@ -1,8 +1,7 @@
version: '3.8' version: '3.8'
services: services:
chefbible: chefbible:
image: git.redbackpack.ca/taogaetz/chefbible:latest build: .
container_name: chefbible container_name: chefbible
restart: unless-stopped restart: unless-stopped
ports: ports:
@ -15,10 +14,15 @@ services:
- ORIGIN=${ORIGIN} - ORIGIN=${ORIGIN}
- ACCESS_PIN=${ACCESS_PIN} - ACCESS_PIN=${ACCESS_PIN}
volumes: volumes:
- /home/chefbible/data:/app/data - chefbible-data:/app/data
healthcheck: healthcheck:
test: [ "CMD", "wget", "--quiet", "--tries=1", "--spider", "http://127.0.0.1:3000/health" ] test: [ "CMD", "wget", "--quiet", "--tries=1", "--spider", "http://127.0.0.1:3000/health" ]
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 3 retries: 3
start_period: 40s start_period: 40s
volumes:
chefbible-data:

View File

@ -0,0 +1,21 @@
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Recipe" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"description" TEXT,
"instructions" TEXT,
"photoUrl" TEXT,
"time" TEXT NOT NULL DEFAULT 'Medium',
"station" TEXT NOT NULL DEFAULT 'Pans',
"type" TEXT NOT NULL DEFAULT 'Dish',
"hidden" BOOLEAN NOT NULL DEFAULT false,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
INSERT INTO "new_Recipe" ("createdAt", "description", "hidden", "id", "instructions", "name", "photoUrl", "station", "time", "updatedAt") SELECT "createdAt", "description", "hidden", "id", "instructions", "name", "photoUrl", "station", "time", "updatedAt" FROM "Recipe";
DROP TABLE "Recipe";
ALTER TABLE "new_Recipe" RENAME TO "Recipe";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@ -25,6 +25,7 @@ model Recipe {
photoUrl String? photoUrl String?
time String @default("Medium") // Quick, Medium, Long time String @default("Medium") // Quick, Medium, Long
station String @default("Pans") // Garde Manger, Pans, Grill station String @default("Pans") // Garde Manger, Pans, Grill
type String @default("Dish") // Ingredient or Dish
hidden Boolean @default(false) // Hidden from non-chefs hidden Boolean @default(false) // Hidden from non-chefs
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt

View File

@ -111,6 +111,9 @@
<span class="badge badge-sm badge-success"> <span class="badge badge-sm badge-success">
{recipe.station || 'Pans'} {recipe.station || 'Pans'}
</span> </span>
<span class="badge badge-outline badge-sm">
{recipe.type || 'Dish'}
</span>
{#if authenticated} {#if authenticated}
<button <button
class="btn btn-outline btn-xs btn-primary" class="btn btn-outline btn-xs btn-primary"

View File

@ -54,18 +54,96 @@
{#if data.authenticated} {#if data.authenticated}
<section class="mx-auto max-w-7xl p-10 lg:p-6"> <section class="mx-auto max-w-7xl p-10 lg:p-6">
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"> <div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
{#each data.recipes as recipe} <a
<RecipeCard {recipe} authenticated={data.authenticated} /> href="/ingredients"
{/each} class="card border border-base-200 bg-white p-8 shadow-lg transition-shadow hover:shadow-xl"
>
<div class="flex flex-col items-center gap-4 text-center">
<svg
width="48"
height="48"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
class="text-primary"
>
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
</svg>
<h2 class="text-2xl font-bold">Ingredients</h2>
<p class="text-base-content/70">View all ingredient recipes</p>
</div>
</a>
<a
href="/dishes"
class="card border border-base-200 bg-white p-8 shadow-lg transition-shadow hover:shadow-xl"
>
<div class="flex flex-col items-center gap-4 text-center">
<svg
width="48"
height="48"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
class="text-secondary"
>
<circle cx="12" cy="12" r="10" />
<path d="M12 6v6l4 2" />
</svg>
<h2 class="text-2xl font-bold">Dishes</h2>
<p class="text-base-content/70">View all dish recipes</p>
</div>
</a>
</div> </div>
</section> </section>
{:else} {:else}
<section class="mx-auto max-w-7xl p-10 lg:p-6"> <section class="mx-auto max-w-7xl p-10 lg:p-6">
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"> <div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
{#each data.recipes as recipe} <a
<RecipeCard {recipe} authenticated={data.authenticated} /> href="/ingredients"
{/each} class="card border border-base-200 bg-white p-8 shadow-lg transition-shadow hover:shadow-xl"
>
<div class="flex flex-col items-center gap-4 text-center">
<svg
width="48"
height="48"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
class="text-primary"
>
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
</svg>
<h2 class="text-2xl font-bold">Ingredients</h2>
<p class="text-base-content/70">View all ingredient recipes</p>
</div>
</a>
<a
href="/dishes"
class="card border border-base-200 bg-white p-8 shadow-lg transition-shadow hover:shadow-xl"
>
<div class="flex flex-col items-center gap-4 text-center">
<svg
width="48"
height="48"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
class="text-secondary"
>
<circle cx="12" cy="12" r="10" />
<path d="M12 6v6l4 2" />
</svg>
<h2 class="text-2xl font-bold">Dishes</h2>
<p class="text-base-content/70">View all dish recipes</p>
</div>
</a>
</div> </div>
</section> </section>
{/if} {/if}

View File

@ -0,0 +1,29 @@
import type { PageServerLoad } from './$types';
import prisma from '$lib/server/prisma';
export const load: PageServerLoad = async ({ locals }) => {
// Get dish recipes only
// If not authenticated, filter out hidden recipes
const recipes = await prisma.recipe.findMany({
where: {
type: 'Dish',
...(locals.authenticated ? {} : { hidden: false })
},
include: {
ingredients: {
include: {
ingredient: true
}
}
},
orderBy: {
createdAt: 'desc'
}
});
return {
recipes,
authenticated: locals.authenticated,
hasAccess: locals.hasAccess
};
};

View File

@ -0,0 +1,33 @@
<script lang="ts">
import type { PageProps } from './$types';
import RecipeCard from '$lib/components/RecipeCard.svelte';
let { data }: PageProps = $props();
</script>
<svelte:head>
<title>Dishes - Chef Bible</title>
<meta name="description" content="Dish recipes" />
</svelte:head>
<main class="m-0 min-h-screen bg-gradient-to-br from-base-200 to-base-300 p-0">
<section class="mx-auto max-w-7xl p-10 lg:p-6">
<div class="mb-4">
<h1 class="text-sm text-base-content/60">
<a href="/" class="hover:underline">Home</a> / Dishes
</h1>
</div>
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{#each data.recipes as recipe}
<RecipeCard {recipe} authenticated={data.authenticated} />
{/each}
</div>
{#if data.recipes.length === 0}
<div class="mt-12 text-center">
<p class="text-base-content/60">No dish recipes found.</p>
</div>
{/if}
</section>
</main>

View File

@ -0,0 +1,29 @@
import type { PageServerLoad } from './$types';
import prisma from '$lib/server/prisma';
export const load: PageServerLoad = async ({ locals }) => {
// Get ingredient recipes only
// If not authenticated, filter out hidden recipes
const recipes = await prisma.recipe.findMany({
where: {
type: 'Ingredient',
...(locals.authenticated ? {} : { hidden: false })
},
include: {
ingredients: {
include: {
ingredient: true
}
}
},
orderBy: {
createdAt: 'desc'
}
});
return {
recipes,
authenticated: locals.authenticated,
hasAccess: locals.hasAccess
};
};

View File

@ -0,0 +1,33 @@
<script lang="ts">
import type { PageProps } from './$types';
import RecipeCard from '$lib/components/RecipeCard.svelte';
let { data }: PageProps = $props();
</script>
<svelte:head>
<title>Ingredients - Chef Bible</title>
<meta name="description" content="Ingredient recipes" />
</svelte:head>
<main class="m-0 min-h-screen bg-gradient-to-br from-base-200 to-base-300 p-0">
<section class="mx-auto max-w-7xl p-10 lg:p-6">
<div class="mb-4">
<h1 class="text-sm text-base-content/60">
<a href="/" class="hover:underline">Home</a> / Ingredients
</h1>
</div>
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{#each data.recipes as recipe}
<RecipeCard {recipe} authenticated={data.authenticated} />
{/each}
</div>
{#if data.recipes.length === 0}
<div class="mt-12 text-center">
<p class="text-base-content/60">No ingredient recipes found.</p>
</div>
{/if}
</section>
</main>

View File

@ -514,6 +514,14 @@
<option value="Grill" selected={data.recipe?.station === 'Grill'}>Grill</option> <option value="Grill" selected={data.recipe?.station === 'Grill'}>Grill</option>
</select> </select>
</label> </label>
<label class="form-control min-w-0">
<span class="label"><span class="label-text font-bold">Type</span></span>
<select name="type" class="select-bordered select w-full min-w-0">
<option value="Dish" selected={data.recipe?.type === 'Dish'}>Dish</option>
<option value="Ingredient" selected={data.recipe?.type === 'Ingredient'}>Ingredient</option>
</select>
</label>
</div> </div>
<label class="form-control min-w-0"> <label class="form-control min-w-0">

View File

@ -24,6 +24,7 @@ export const POST: RequestHandler = async ({ request, params }) => {
const instructions = (formData.get('instructions') as string | null)?.trim() || null; const instructions = (formData.get('instructions') as string | null)?.trim() || null;
const time = ((formData.get('time') as string | null)?.trim() || 'Medium') as string; const time = ((formData.get('time') as string | null)?.trim() || 'Medium') as string;
const station = ((formData.get('station') as string | null)?.trim() || 'Pans') as string; const station = ((formData.get('station') as string | null)?.trim() || 'Pans') as string;
const type = ((formData.get('type') as string | null)?.trim() || 'Dish') as string;
const hidden = formData.get('hidden') === 'on'; // Checkbox returns 'on' when checked const hidden = formData.get('hidden') === 'on'; // Checkbox returns 'on' when checked
const photo = formData.get('photo') as File | null; const photo = formData.get('photo') as File | null;
const parsedIngredientsRaw = formData.get('parsedIngredients') as string | null; const parsedIngredientsRaw = formData.get('parsedIngredients') as string | null;
@ -101,6 +102,7 @@ export const POST: RequestHandler = async ({ request, params }) => {
photoUrl: photoUrl || existingRecipe.photoUrl, // Keep existing photo if no new one uploaded photoUrl: photoUrl || existingRecipe.photoUrl, // Keep existing photo if no new one uploaded
time, time,
station, station,
type,
hidden hidden
} }
}); });

View File

@ -469,6 +469,14 @@
<option>Grill</option> <option>Grill</option>
</select> </select>
</label> </label>
<label class="form-control">
<span class="label"><span class="label-text font-bold">Type</span></span>
<select name="type" class="select-bordered select w-full min-w-0">
<option value="Dish" selected>Dish</option>
<option value="Ingredient">Ingredient</option>
</select>
</label>
</div> </div>
<label class="form-control min-w-0"> <label class="form-control min-w-0">

View File

@ -24,6 +24,7 @@ export const POST: RequestHandler = async ({ request }) => {
const instructions = (formData.get('instructions') as string | null)?.trim() || null; const instructions = (formData.get('instructions') as string | null)?.trim() || null;
const time = ((formData.get('time') as string | null)?.trim() || 'Medium') as string; const time = ((formData.get('time') as string | null)?.trim() || 'Medium') as string;
const station = ((formData.get('station') as string | null)?.trim() || 'Pans') as string; const station = ((formData.get('station') as string | null)?.trim() || 'Pans') as string;
const type = ((formData.get('type') as string | null)?.trim() || 'Dish') as string;
const hidden = formData.get('hidden') === 'on'; // Checkbox returns 'on' when checked const hidden = formData.get('hidden') === 'on'; // Checkbox returns 'on' when checked
const photo = formData.get('photo') as File | null; const photo = formData.get('photo') as File | null;
const parsedIngredientsRaw = formData.get('parsedIngredients') as string | null; const parsedIngredientsRaw = formData.get('parsedIngredients') as string | null;
@ -91,6 +92,7 @@ export const POST: RequestHandler = async ({ request }) => {
photoUrl, photoUrl,
time, time,
station, station,
type,
hidden hidden
} }
}); });