Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
039d3c5415 | ||
|
|
ec00519b7a | ||
|
|
d53fd8742d |
+1
-1
@@ -138,7 +138,7 @@ Rezepte und Site-Daten in normalisierten Postgres-Tabellen, optional lokal per D
|
||||
|
||||
- [x] **`DATABASE_URL` & Docker Compose**
|
||||
- [x] `docker-compose.dev.yml` für lokale Postgres-Instanz
|
||||
- [x] `init_db()` legt Tabellen an; `scripts/db-seed.php` seedet aus `scripts/seed-data.php`
|
||||
- [x] `init_db()` legt Tabellen an; `scripts/db-seed.php` seedet aus `data/recipes.json`
|
||||
- [x] **PHP-Datenzugriff**
|
||||
- [x] `load_recipes()` / `save_recipe()` ohne JSON-Fallback
|
||||
- [x] `load_recipe_by_slug()` in `recipe.php`
|
||||
|
||||
+3
-3
@@ -4,7 +4,7 @@ This document summarizes architectural knowledge, conventions, and learnings for
|
||||
|
||||
## 1. Project Architecture & Stack
|
||||
- **Backend:** Vanilla PHP. No heavy frameworks.
|
||||
- **Database:** PostgreSQL required (`DATABASE_URL` in `.env`). Normalized tables in `scripts/schema.sql`; `load_recipes()` / `save_recipe()` and `load_site_settings()` / `save_site_settings()` in `helpers.php`. No runtime JSON files. One-time import: `php scripts/db-seed.php` from `scripts/seed-data.php`.
|
||||
- **Database:** PostgreSQL required (`DATABASE_URL` in `.env`). Normalized tables in `scripts/schema.sql`; `load_recipes()` / `save_recipe()` and `load_site_settings()` / `save_site_settings()` in `helpers.php`. Recipes read at runtime only from Postgres. One-time import of sample data: `php scripts/db-seed.php` from `data/recipes.json`.
|
||||
- **Site settings (legal pages):** Stored in Postgres table `site_settings`; no flat-file fallback.
|
||||
- **Admin Panel (`admin.php`):** Lightweight CMS. Textareas use one line per array element (`ingredients`, `steps`, `step_videos`, `step_timers`).
|
||||
- **Frontend:** Server-rendered PHP (`index.php`, `recipe.php`, …), Vanilla JS/CSS. Profile/favorites via `assets/fc-local.js`.
|
||||
@@ -42,7 +42,7 @@ PHP still exposes the same nested arrays (`i18n`, `nutrition`, …) via `hydrate
|
||||
1. `DATABASE_URL` required → `require_database()` or HTTP 503 (`maintenance/db-unavailable.php`).
|
||||
2. `load_recipes()` → SQL → PHP arrays for templates.
|
||||
3. Admin: `save_recipe()`, `delete_recipe()`, `clear_featured_recipes()`.
|
||||
4. One-time import: `php scripts/db-seed.php` from `scripts/seed-data.php`.
|
||||
4. One-time import: `php scripts/db-seed.php` from `data/recipes.json`.
|
||||
|
||||
### `config.php` functions
|
||||
- `load_env()` — parses `.env`.
|
||||
@@ -125,7 +125,7 @@ See `.agents/TODO.md` (e.g. PWA & offline support). README `Local Postgres (Dock
|
||||
|------|---------|
|
||||
| `config.php` | `.env`, Firebase config, PDO |
|
||||
| `helpers.php` | `init_db`, `load_recipes`, `save_recipe`, site settings |
|
||||
| `scripts/seed-data.php` | Seed arrays for recipes and site settings |
|
||||
| `data/recipes.json` | Seed data for recipes (imported by `scripts/db-seed.php`) |
|
||||
| `docker-compose.dev.yml` | Local Postgres |
|
||||
| `scripts/db-check.php` | Connection + seed smoke test |
|
||||
| `partials/head.php` | Firebase init via `get_firebase_config()` |
|
||||
|
||||
@@ -9,3 +9,4 @@ DATABASE_URL="postgresql://flixcooks:flixcooks_dev@127.0.0.1:5432/flixcooks_dev"
|
||||
# DATABASE_URL="postgresql://user:pass@postgresql-service:5432/flixcooks"
|
||||
# FLIXCOOKS_ADMIN_KEY="langes-zufaelliges-passwort"
|
||||
# RUN_DB_SEED="true" # nur beim allerersten Deploy: Rezepte + Site-Daten in Postgres seeden
|
||||
# APP_PORT=8080
|
||||
@@ -0,0 +1,97 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project overview
|
||||
|
||||
FlixCooks is a premium recipe/food-blog website built as **vanilla server-rendered PHP** — no framework, no build step, no JS bundler, no Composer dependencies. PHP renders HTML directly with heavy inline `<script>`/`<style>` blocks per page. All data lives in PostgreSQL (no JSON files, no ORM).
|
||||
|
||||
## Commands
|
||||
|
||||
There is no build step, package manager, linter, or test suite in this repo (no `composer.json`, no `phpunit`, no `package.json`). Work is verified by running the app directly.
|
||||
|
||||
```bash
|
||||
# Local app server
|
||||
php -S localhost:8000
|
||||
|
||||
# Local Postgres (separate from production)
|
||||
docker compose -f docker-compose.dev.yml up -d
|
||||
docker compose -f docker-compose.dev.yml ps # wait for healthy
|
||||
docker compose -f docker-compose.dev.yml down -v # wipe + re-seed on next run
|
||||
|
||||
# .env setup (never commit .env)
|
||||
cp .env.example .env # then set DATABASE_URL
|
||||
|
||||
# Seed / verify DB
|
||||
php scripts/db-seed.php # one-time import of data/recipes.json into Postgres
|
||||
php scripts/db-check.php # connect, init schema, print recipe count
|
||||
|
||||
# Inspect DB directly
|
||||
docker exec -it flixcooks-postgres-dev psql -U flixcooks -d flixcooks_dev
|
||||
|
||||
# Full stack via Docker (production-like image)
|
||||
docker compose build && docker compose up -d # http://127.0.0.1:8080
|
||||
```
|
||||
|
||||
Requires PHP 8.x with the `pgsql`/`pdo_pgsql` extension (`sudo apt install php-pgsql` on Debian/Ubuntu). Without a working `DATABASE_URL`, every page returns HTTP 503 and renders `maintenance/db-unavailable.php` — there is no file/JSON fallback.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Request flow
|
||||
Every entry-point page (`index.php`, `recipe.php`, `admin.php`, `login.php`) follows the same pattern:
|
||||
1. `require __DIR__ . '/helpers.php'` (which itself requires `config.php` and starts the session).
|
||||
2. Call `load_recipes()` / `load_site_settings()` inside a `try/catch (DatabaseUnavailableException $e)` that calls `handle_database_unavailable($e)` on failure.
|
||||
3. Resolve `$lang` from `?lang=de|en` (default `en`) and build a `$copy`/`$t` array of inline translation strings for that page.
|
||||
4. Render HTML directly (`partials/head.php` → `partials/header.php` → page body → `partials/footer.php`), reading from `helpers.php` data structures.
|
||||
|
||||
There is no router and no templating engine — each `.php` file is both controller and view.
|
||||
|
||||
### Data layer (`config.php` + `helpers.php`)
|
||||
- `config.php`: parses `.env` (`load_env()`), exposes `get_db_connection()` (PDO, memoized in a `static` var) and `DatabaseUnavailableException`.
|
||||
- `helpers.php`: everything else — schema bootstrap (`ensure_recipe_schema()` applies `scripts/schema.sql` idempotently on first DB use, no migrations system), and CRUD:
|
||||
- `load_recipes()` / `load_recipe_by_slug()` — hydrate recipes from normalized tables into the nested PHP array shape templates expect (`hydrate_recipes_from_db()`).
|
||||
- `save_recipe()` / `delete_recipe()` — used by `admin.php`; `save_recipe()` deletes+reinserts child rows (translations/tags/ingredients/utensils/steps) inside a transaction rather than diffing.
|
||||
- `load_site_settings()` / `save_site_settings()` — imprint/privacy legal-page copy, stored as EAV rows (`site_settings(section, lang, setting_key, setting_value)`), merged over `default_site_settings()`.
|
||||
- `localize_recipe()` / `localize_recipes()` — flatten a recipe's `i18n[lang]` block onto the top level for the current request's language.
|
||||
- `e()` — the only HTML-escaping helper (`htmlspecialchars` wrapper); always use it when echoing user- or DB-sourced strings.
|
||||
|
||||
### Recipe data shape
|
||||
A recipe row + its children hydrate into:
|
||||
```php
|
||||
[
|
||||
'slug', 'hero', 'prep_time', 'cook_time', 'total_time', 'servings',
|
||||
'featured', 'coming_soon',
|
||||
'nutrition' => ['calories','protein','carbs','fat'],
|
||||
'i18n' => [
|
||||
'en' => ['title','description','category','difficulty','tags','ingredients','utensils','steps','step_videos','step_timers'],
|
||||
'de' => [ ... same shape ... ],
|
||||
],
|
||||
]
|
||||
```
|
||||
`step_videos` and `step_timers` are parallel arrays indexed the same as `steps` (one optional video URL / timer-in-minutes per step). In `admin.php` these are edited as one-line-per-array-element `<textarea>` fields.
|
||||
|
||||
### Database schema (`scripts/schema.sql`)
|
||||
Normalized Postgres tables, all applied via `ensure_recipe_schema()` (not a migrations tool — editing the schema means editing this file, which must stay idempotent `CREATE TABLE IF NOT EXISTS`):
|
||||
`recipes` (1 row per recipe) → `recipe_translations`, `recipe_tags`, `recipe_ingredients`, `recipe_utensils`, `recipe_steps` (all keyed by `recipe_slug` + `lang`, cascade-deleted with the parent recipe) → `site_settings` (imprint/privacy copy, keyed by `section` + `lang` + `setting_key`).
|
||||
|
||||
### Auth
|
||||
`admin.php` guards itself with a single shared secret (`FLIXCOOKS_ADMIN_KEY` env var, compared via `hash_equals`) rather than user accounts; success sets `$_SESSION['fc_admin'] = true`. Admin POST handlers additionally check a CSRF token (`$_SESSION['csrf_token']` vs `$_POST['token']`). If `FLIXCOOKS_ADMIN_KEY` is unset, `admin.php` renders an "unavailable" page instead of a login form.
|
||||
|
||||
### Frontend conventions
|
||||
- No JS build step — GSAP, ScrollTrigger, and Lenis are loaded from CDN in `partials/head.php`; page-specific behavior lives in inline `<script>` blocks at the bottom of each `.php` file.
|
||||
- `assets/fc-local.js` is the only standalone JS file — client-side `localStorage` for favorites and dietary-goal personalization (no backend user accounts on the public site).
|
||||
- Scroll-reveal: elements tagged `.reveal-target` are animated in via a shared `IntersectionObserver` pattern repeated per-page (see bottom of `index.php` / `recipe.php`).
|
||||
- Lenis smooth scroll must be explicitly stopped/started around fullscreen overlays: `window.lenis.stop()` on open, `window.lenis.start()` on close (see Cooking Mode in `recipe.php`).
|
||||
- Styling is one large `assets/style.css` using CSS custom properties, `clamp()`-based fluid spacing/typography, and a 24-column asymmetric grid (`FloemaLayoutGrid`). Full visual language (palette options, motion specs, named components like `CapitoliumRevealButton`, `LiquidOverlayMenu`, `AuraMarbleBackground`) is documented in `.agents/DESIGN_GUIDE.md` — consult it before styling new UI so new work matches the established aesthetic vocabulary.
|
||||
- Assets are cache-busted via `filemtime()` query strings (`$assetVersion()` in `partials/head.php`), not filename hashing.
|
||||
|
||||
### Deployment
|
||||
Single production `Dockerfile` (`php:8.3-apache-bookworm`), pushed to Coolify. `docker/entrypoint.sh` waits for the DB (`scripts/db-check.php` polling loop) and applies schema before starting Apache. `health.php` is the container `HEALTHCHECK` target. Details in `docs/COOLIFY.md`.
|
||||
|
||||
## Project workflow rules (from `.agents/rules/`)
|
||||
|
||||
- **Never commit directly to `main`.** All work happens on feature branches (`feature/...` or `issue-#...`), merged via PR. Pin GitHub Actions to specific version tags, not `@latest`.
|
||||
- Merge-conflict resolution on a feature branch is the responsibility of that branch's author — merge `main` in, resolve manually, never force-push over others' work.
|
||||
- The `close_feature` skill (`.agents/skills/close_feature.json`) encodes the "merge feature → main, verify, push, delete branch" flow.
|
||||
- Track development progress in `.agents/TODO.md`; record new architectural/aesthetic/workflow knowledge in `.agents/brain.md` (both are living documents future agents rely on — update them as you learn things, don't just read them).
|
||||
- Non-code-affecting markdown docs can be committed straight to `main`.
|
||||
@@ -19,7 +19,7 @@ The project is architected to remain extremely lightweight and fast, intentional
|
||||
- `helpers.php`: Core PHP utilities and the PostgreSQL data access layer for recipes and site settings.
|
||||
- `config.php`: Environment-independent configuration loader which reads runtime secrets from `.env`.
|
||||
- `assets/fc-local.js`: Browser-side storage for favorites and dietary goals.
|
||||
- `scripts/seed-data.php`: One-time seed data for recipes, imprint, and privacy settings.
|
||||
- `scripts/db-seed.php`: One-time import of `data/recipes.json` into Postgres.
|
||||
- `scripts/schema.sql`: Relational table definitions for recipes and site settings.
|
||||
|
||||
---
|
||||
@@ -154,11 +154,11 @@ Schema: `scripts/schema.sql`. PHP baut daraus dieselben Arrays wie früher (`i18
|
||||
|
||||
1. `DATABASE_URL` in `.env` → Verbindung über `config.php`.
|
||||
2. Beim ersten Request: Tabellen anlegen (`ensure_recipe_schema()`).
|
||||
3. `php scripts/db-seed.php` einmalig ausführen → Rezepte und Site-Daten werden in Postgres geschrieben.
|
||||
3. `php scripts/db-seed.php` einmalig ausführen → Rezepte aus `data/recipes.json` werden in Postgres geschrieben.
|
||||
4. `load_recipes()` und `load_site_settings()` lesen per SQL; ohne DB → HTTP 503 (`maintenance/db-unavailable.php`).
|
||||
5. Admin: `save_recipe()`, `delete_recipe()` und `save_site_settings()` schreiben direkt in die Tabellen.
|
||||
|
||||
**Einmalig Daten laden:** `php scripts/db-seed.php` (aus `scripts/seed-data.php`, ohne JSON-Dateien).
|
||||
**Einmalig Daten laden:** `php scripts/db-seed.php` (importiert `data/recipes.json`).
|
||||
|
||||
Für **Staging/Production** muss eine externe Postgres-Datenbank vorhanden sein. Setze nur `DATABASE_URL` in der Hosting-Umgebung und mische nie Production-Daten in die lokale Dev-DB.
|
||||
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
[
|
||||
{
|
||||
"slug": "frische-tagliatelle-mit-cremiger-tomatensauce",
|
||||
"hero": "/assets/pasta-tomato.jpg",
|
||||
"prep_time": 50,
|
||||
"cook_time": 10,
|
||||
"total_time": 60,
|
||||
"servings": 2,
|
||||
"featured": false,
|
||||
"coming_soon": false,
|
||||
"nutrition": {
|
||||
"calories": 0,
|
||||
"protein": 0,
|
||||
"carbs": 0,
|
||||
"fat": 0
|
||||
},
|
||||
"i18n": {
|
||||
"en": {
|
||||
"title": "Tagliatelle with tomato sauce",
|
||||
"description": "Simple but tasty, short cooking time but self made. The root dish to start in a cozy evening and satisfy the carbs cravings without any hidden additives. Noodles with Tomato Sauce, more hearty isnt possible.",
|
||||
"category": "Pasta",
|
||||
"difficulty": "Easy",
|
||||
"tags": [
|
||||
"Pasta",
|
||||
"Fast",
|
||||
"Vegetarian"
|
||||
],
|
||||
"ingredients": [
|
||||
"300 g flour",
|
||||
"3 eggs",
|
||||
"10 cherry tomatoes",
|
||||
"1/2 clove garlic",
|
||||
"1 tbsp olive oil",
|
||||
"80 ml cream",
|
||||
"Fresh basil",
|
||||
"Parmesan",
|
||||
"Salt and pepper"
|
||||
],
|
||||
"utensils": [
|
||||
"Rolling pin",
|
||||
"Optional: Pasta machine",
|
||||
"Optional: Blender"
|
||||
],
|
||||
"steps": [
|
||||
"Pile the flour on your counter, press a well in the center, crack in the eggs. Gradually pull flour into the eggs, then knead 5–10 minutes until smooth.",
|
||||
"Roll the dough to 1–2 mm thickness (rolling pin or pasta machine) and cut into tagliatelle.",
|
||||
"Bring salted water to a boil. Meanwhile, halve tomatoes and sear in olive oil with a pinch of salt until lightly charred.",
|
||||
"Toast the garlic briefly, then blend tomatoes with cream (or crush in the pan) and simmer to thicken slightly.",
|
||||
"Boil tagliatelle for 2–3 minutes, drain, toss with the sauce, and finish with basil and Parmesan."
|
||||
]
|
||||
},
|
||||
"de": {
|
||||
"title": "Tagliatelle mit Tomatensauce",
|
||||
"description": "Einfach, aber richtig lecker; kurze Kochzeit und trotzdem hausgemacht. Das Basisgericht für einen gemütlichen Abend, stillt den Kohlenhydrat-Hunger ohne versteckte Zusätze. Nudeln mit Tomatensauce – herzhafter geht es kaum.",
|
||||
"category": "Pasta",
|
||||
"difficulty": "Einfach",
|
||||
"tags": [
|
||||
"Pasta",
|
||||
"Schnell",
|
||||
"Vegetarisch"
|
||||
],
|
||||
"ingredients": [
|
||||
"300 g Mehl",
|
||||
"3 Eier",
|
||||
"10 Cherry-Tomaten",
|
||||
"1/2 Knoblauchzehe",
|
||||
"1 EL Olivenöl",
|
||||
"80 ml Sahne",
|
||||
"Frischer Basilikum",
|
||||
"Parmesan",
|
||||
"Salz und Pfeffer"
|
||||
],
|
||||
"utensils": [
|
||||
"Nudelholz",
|
||||
"Optional: Nudelmaschine",
|
||||
"Optional: Mixer"
|
||||
],
|
||||
"steps": [
|
||||
"Mehl auf der Arbeitsfläche anhäufen, eine Kuhle drücken, Eier hineingeben. Mehl nach und nach einarbeiten, dann 5–10 Minuten kneten, bis der Teig glatt ist.",
|
||||
"Teig auf 1–2 mm ausrollen (mit Nudelholz oder Nudelmaschine) und in Tagliatelle schneiden.",
|
||||
"Gesalzenes Wasser aufsetzen; währenddessen Tomaten halbieren und mit Olivenöl und einer Prise Salz in der Pfanne anrösten, bis Röstnoten entstehen.",
|
||||
"Knoblauch kurz mitrösten, dann Tomaten mit Sahne pürieren (Mixer) oder in der Pfanne zerdrücken und kurz einkochen lassen.",
|
||||
"Tagliatelle 2–3 Minuten kochen, abgießen und mit der Sauce vermengen. Mit Basilikum und Parmesan anrichten."
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"slug": "oat-pancakes",
|
||||
"hero": "/assets/pancakes.jpg",
|
||||
"prep_time": 0,
|
||||
"cook_time": 0,
|
||||
"total_time": 0,
|
||||
"servings": 2,
|
||||
"featured": false,
|
||||
"coming_soon": true,
|
||||
"nutrition": {
|
||||
"calories": 0,
|
||||
"protein": 0,
|
||||
"carbs": 0,
|
||||
"fat": 0
|
||||
},
|
||||
"i18n": {
|
||||
"en": {
|
||||
"title": "Oat-Pancakes",
|
||||
"description": "",
|
||||
"category": "",
|
||||
"difficulty": "Easy",
|
||||
"tags": [],
|
||||
"ingredients": [],
|
||||
"utensils": [],
|
||||
"steps": []
|
||||
},
|
||||
"de": {
|
||||
"title": "Hafer-Pfannkuchen",
|
||||
"description": "",
|
||||
"category": "",
|
||||
"difficulty": "Einfach",
|
||||
"tags": [],
|
||||
"ingredients": [],
|
||||
"utensils": [],
|
||||
"steps": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"slug": "steak-with-onion-jam",
|
||||
"hero": "/assets/steak.JPEG",
|
||||
"prep_time": 15,
|
||||
"cook_time": 15,
|
||||
"total_time": 30,
|
||||
"servings": 2,
|
||||
"featured": true,
|
||||
"coming_soon": false,
|
||||
"nutrition": {
|
||||
"calories": 0,
|
||||
"protein": 0,
|
||||
"carbs": 0,
|
||||
"fat": 0
|
||||
},
|
||||
"i18n": {
|
||||
"en": {
|
||||
"title": "Steak with onion jam",
|
||||
"description": "A hearty meal after a long day, best enjoyed with a glass of red wine and in good company—because the onion jam still isn’t satisfied on its own.",
|
||||
"category": "",
|
||||
"difficulty": "Easy",
|
||||
"tags": [
|
||||
"meat",
|
||||
"dinner"
|
||||
],
|
||||
"ingredients": [
|
||||
"2 x 250g rump steak",
|
||||
"300g potatoes",
|
||||
"1 large red onion",
|
||||
"1/2 clove garlic",
|
||||
"50ml cream",
|
||||
"50ml red wine",
|
||||
"4 carrots (yellow and purple)",
|
||||
"parmesan",
|
||||
"Sicilian orange salt",
|
||||
"thyme",
|
||||
"1 tbsp butter"
|
||||
],
|
||||
"utensils": [
|
||||
"knife",
|
||||
"cutting board",
|
||||
"pan",
|
||||
"pot",
|
||||
"oven"
|
||||
],
|
||||
"steps": [
|
||||
"Take the steak out 30 minutes before cooking",
|
||||
"Halve the potatoes",
|
||||
"Halve the carrots",
|
||||
"Dice the onion",
|
||||
"Cook the potatoes and carrots until al dente",
|
||||
"Sear the steak with butter",
|
||||
"Once both sides are golden brown, bake for 6 minutes at 160°C (medium); for medium rare, let it rest after searing both sides for 3 minutes",
|
||||
"Sauté the onion in the steak butter, deglaze with red wine, and add cream once the alcohol has evaporated",
|
||||
"Plate and serve!"
|
||||
]
|
||||
},
|
||||
"de": {
|
||||
"title": "Steak mit Zwiebelmarmelade",
|
||||
"description": "Eine schwere Mahlzeit nach einem langen Tag, am besten zu einem Schluck Rotwein und in Gesellschaft genießen, da die Zwiebelmarmelade noch nicht genug davon hat.",
|
||||
"category": "",
|
||||
"difficulty": "Einfach",
|
||||
"tags": [
|
||||
"Fleisch",
|
||||
"Abendessen"
|
||||
],
|
||||
"ingredients": [
|
||||
"2 x 250g Rumpsteak",
|
||||
"300gr Kartoffeln",
|
||||
"1 Große Rote Zwiebel",
|
||||
"1/2 Knoblauch-Zehe",
|
||||
"50ml Sahne",
|
||||
"50ml Rotwein",
|
||||
"4 Karotten (Geld und Lila)",
|
||||
"Parmesan",
|
||||
"Sizilianisches Orangensalz",
|
||||
"Thymian",
|
||||
"1 EL Butter"
|
||||
],
|
||||
"utensils": [
|
||||
"Messer",
|
||||
"Brett",
|
||||
"Pfanne",
|
||||
"Topf",
|
||||
"Ofen"
|
||||
],
|
||||
"steps": [
|
||||
"Steak 30 Minuten vor dem anbraten raus legen",
|
||||
"Kartoffeln halbieren",
|
||||
"Karotten halbieren",
|
||||
"Zwiebel würfeln",
|
||||
"Kartoffeln und Karotten al dente kochen",
|
||||
"Steak anbraten mit Butter",
|
||||
"sobald beide seiten goldbraun sind für 6 Minuten bei 160grad backen (Medium) für Medium Rare nur ruhen lassen nachdem es von beiden Seiten für 3 Minuten angebraten wurde",
|
||||
"Zwiebel in Steak-Butter anschwitzen, mit Rotwein ablöschen und Sahne dazu geben nachdem er Alkohol verdünstet ist",
|
||||
"Anrichten!"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"slug": "oat-cake",
|
||||
"hero": "/assets/haferkuchen.JPEG",
|
||||
"prep_time": 0,
|
||||
"cook_time": 0,
|
||||
"total_time": 0,
|
||||
"servings": 2,
|
||||
"featured": false,
|
||||
"coming_soon": true,
|
||||
"nutrition": {
|
||||
"calories": 0,
|
||||
"protein": 0,
|
||||
"carbs": 0,
|
||||
"fat": 0
|
||||
},
|
||||
"i18n": {
|
||||
"en": {
|
||||
"title": "Oat cake",
|
||||
"description": "",
|
||||
"category": "",
|
||||
"difficulty": "Easy",
|
||||
"tags": [],
|
||||
"ingredients": [],
|
||||
"utensils": [],
|
||||
"steps": []
|
||||
},
|
||||
"de": {
|
||||
"title": "Haferkuchen",
|
||||
"description": "",
|
||||
"category": "",
|
||||
"difficulty": "Einfach",
|
||||
"tags": [],
|
||||
"ingredients": [],
|
||||
"utensils": [],
|
||||
"steps": []
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
+2
-2
@@ -2,14 +2,14 @@
|
||||
#
|
||||
# docker compose build
|
||||
# docker compose up -d
|
||||
# curl http://127.0.0.1:8080/health.php
|
||||
# curl http://127.0.0.1:${APP_Port}/health.php
|
||||
|
||||
services:
|
||||
web:
|
||||
build: .
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8080:80"
|
||||
- "${APP_Port:-8080}:80"
|
||||
environment:
|
||||
DATABASE_URL: postgresql://flixcooks:flixcooks_prod@postgres:5432/flixcooks
|
||||
FLIXCOOKS_ADMIN_KEY: ${FLIXCOOKS_ADMIN_KEY:-change-me-in-production}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
/**
|
||||
* Einmaliger Import aus data/recipes.json in die relationale DB.
|
||||
* Die Website liest zur Laufzeit nur noch aus Postgres.
|
||||
*
|
||||
* php scripts/db-seed.php
|
||||
*/
|
||||
require_once dirname(__DIR__) . '/helpers.php';
|
||||
|
||||
$seedPath = dirname(__DIR__) . '/data/recipes.json';
|
||||
if (!file_exists($seedPath)) {
|
||||
fwrite(STDERR, "Seed-Datei fehlt: data/recipes.json\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$raw = file_get_contents($seedPath);
|
||||
$recipes = json_decode($raw, true);
|
||||
if (!is_array($recipes)) {
|
||||
fwrite(STDERR, "Ungültiges JSON in data/recipes.json\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo = require_database();
|
||||
} catch (DatabaseUnavailableException $e) {
|
||||
fwrite(STDERR, $e->getMessage() . "\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
foreach ($recipes as $recipe) {
|
||||
if (!is_array($recipe) || empty($recipe['slug'])) {
|
||||
continue;
|
||||
}
|
||||
if (save_recipe($recipe, $pdo)) {
|
||||
$count++;
|
||||
echo " + {$recipe['slug']}\n";
|
||||
} else {
|
||||
fwrite(STDERR, " ! Fehler bei {$recipe['slug']}\n");
|
||||
}
|
||||
}
|
||||
|
||||
echo "\nImport abgeschlossen: {$count} Rezepte.\n";
|
||||
Reference in New Issue
Block a user