mirror of
https://github.com/taogaetz/chefbible.git
synced 2025-12-06 11:47:24 -05:00
Compare commits
4 Commits
7a6bd4b92c
...
de38a482e1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de38a482e1 | ||
|
|
278a5b44c4 | ||
|
|
1c0ce2d741 | ||
|
|
f241b06336 |
@ -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]
|
||||
|
||||
@ -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:
|
||||
|
||||
|
||||
|
||||
@ -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;
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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}
|
||||
|
||||
29
src/routes/dishes/+page.server.ts
Normal file
29
src/routes/dishes/+page.server.ts
Normal 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
|
||||
};
|
||||
};
|
||||
33
src/routes/dishes/+page.svelte
Normal file
33
src/routes/dishes/+page.svelte
Normal 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>
|
||||
29
src/routes/ingredients/+page.server.ts
Normal file
29
src/routes/ingredients/+page.server.ts
Normal 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
|
||||
};
|
||||
};
|
||||
33
src/routes/ingredients/+page.svelte
Normal file
33
src/routes/ingredients/+page.svelte
Normal 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>
|
||||
@ -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">
|
||||
|
||||
@ -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
|
||||
}
|
||||
});
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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
|
||||
}
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user