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
4. Run tests and linting: `npm run check`
5. Submit a pull request
## License
[Add your license here]

View File

@ -1,8 +1,7 @@
version: '3.8'
services:
chefbible:
image: git.redbackpack.ca/taogaetz/chefbible:latest
build: .
container_name: chefbible
restart: unless-stopped
ports:
@ -15,10 +14,15 @@ services:
- ORIGIN=${ORIGIN}
- ACCESS_PIN=${ACCESS_PIN}
volumes:
- /home/chefbible/data:/app/data
- chefbible-data:/app/data
healthcheck:
test: [ "CMD", "wget", "--quiet", "--tries=1", "--spider", "http://127.0.0.1:3000/health" ]
interval: 30s
timeout: 10s
retries: 3
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?
time String @default("Medium") // Quick, Medium, Long
station String @default("Pans") // Garde Manger, Pans, Grill
type String @default("Dish") // Ingredient or Dish
hidden Boolean @default(false) // Hidden from non-chefs
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

View File

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

View File

@ -54,18 +54,96 @@
{#if data.authenticated}
<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">
{#each data.recipes as recipe}
<RecipeCard {recipe} authenticated={data.authenticated} />
{/each}
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
<a
href="/ingredients"
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>
</section>
{:else}
<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">
{#each data.recipes as recipe}
<RecipeCard {recipe} authenticated={data.authenticated} />
{/each}
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
<a
href="/ingredients"
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>
</section>
{/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>
</select>
</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>
<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 time = ((formData.get('time') as string | null)?.trim() || 'Medium') 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 photo = formData.get('photo') as File | 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
time,
station,
type,
hidden
}
});

View File

@ -469,6 +469,14 @@
<option>Grill</option>
</select>
</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>
<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 time = ((formData.get('time') as string | null)?.trim() || 'Medium') 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 photo = formData.get('photo') as File | null;
const parsedIngredientsRaw = formData.get('parsedIngredients') as string | null;
@ -91,6 +92,7 @@ export const POST: RequestHandler = async ({ request }) => {
photoUrl,
time,
station,
type,
hidden
}
});