@@ -28,12 +28,6 @@ Dieses Dokument enthält den aktuellen Entwicklungsstand und detaillierte Aufgab
|
|||||||
- Touch-Swipe-Karussell direkt auf index.php (unter Landing-Page)
|
- Touch-Swipe-Karussell direkt auf index.php (unter Landing-Page)
|
||||||
- Filteralgorithmus nach Benutzer-Ernährungszielen
|
- Filteralgorithmus nach Benutzer-Ernährungszielen
|
||||||
- Mikro-Animationen & Quick-Save Funktion
|
- Mikro-Animationen & Quick-Save Funktion
|
||||||
- [/] **Phase 6: Firebase Firestore Datenbank-Migration & Seeding**
|
|
||||||
- Firebase CLI-Login & Projekt-Initialisierung
|
|
||||||
- .env-Konfiguration mit Firebase App Credentials
|
|
||||||
- Client-seitiger "Seed Firestore"-Button in admin.php
|
|
||||||
- Firestore REST API Integration in helpers.php
|
|
||||||
- Echtzeit-Zwei-Wege-Sync bei Rezeptänderungen in admin.php
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -144,29 +138,6 @@ Ein hochgradig interaktives, touch-freundliches Karussell direkt auf der Startse
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 🔥 Phase 6: Firebase Firestore Datenbank-Migration & Seeding
|
|
||||||
Migration des lokalen JSON-Flachdateispeichers auf eine skalierbare, cloudbasierte Firestore-Struktur.
|
|
||||||
|
|
||||||
- [/] **Firebase-Projekt & CLI-Setup**
|
|
||||||
- [x] Firebase-Verzeichnis-Umgebung initialisieren und anbinden
|
|
||||||
- [/] Firebase CLI Login durchführen und aktiven Account verknüpfen
|
|
||||||
- [ ] Firebase Project & Web App ermitteln oder neu anlegen
|
|
||||||
- [ ] Lokale `.env`-Konfiguration mit Firebase SDK Parametern befüllen
|
|
||||||
- [ ] **Client-seitiges Daten-Seeding**
|
|
||||||
- [ ] "Seed Firestore"-Aktion im Admin-Bereich (`admin.php`) einbauen
|
|
||||||
- [ ] Batch-Dokumentenupload der `recipes.json`-Einträge in die Firestore Collection `recipes` über JS SDK
|
|
||||||
- [ ] Firestore Security Rules für den öffentlichen Lesezugriff anpassen
|
|
||||||
- [ ] **Firestore Server-seitiger Abruf (PHP REST API)**
|
|
||||||
- [ ] Firestore REST API Parser-Helfer in `helpers.php` implementieren (Transformation von Firestore Key-Maps in Standard-Arrays)
|
|
||||||
- [ ] `load_recipes()` auf REST API GET `/recipes` umstellen
|
|
||||||
- [ ] Neue Funktion `load_recipe_by_slug($slug)` für gezielten Einzelabruf implementieren und in `recipe.php` integrieren
|
|
||||||
- [ ] **Zwei-Wege-Echtzeit-Synchronisierung (Admin)**
|
|
||||||
- [ ] `save_recipes()` so anpassen, dass lokales JSON als Fallback-Backup erhalten bleibt
|
|
||||||
- [ ] Client-seitigen Trigger nach erfolgreichem PHP-Speichern in `admin.php` ausführen, um Änderungen sofort in Firestore zu spiegeln
|
|
||||||
- [ ] Funktionstest: Hinzufügen, Editieren und Löschen von Rezepten über die Admin-Konsole verifizieren
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📈 Status-Legende
|
## 📈 Status-Legende
|
||||||
- `[ ]` Noch nicht begonnen
|
- `[ ]` Noch nicht begonnen
|
||||||
- `[/]` In Bearbeitung
|
- `[/]` In Bearbeitung
|
||||||
|
|||||||
+1
-5
@@ -32,10 +32,6 @@ This document summarizes the architectural knowledge, conventions, and learnings
|
|||||||
## 5. Completed Milestones
|
## 5. Completed Milestones
|
||||||
- **Phase 3 (Nutrition):** Implemented. Recipes now store `calories`, `protein`, `carbs`, and `fat`. Admin panel handles inputs, and the UI displays them beautifully.
|
- **Phase 3 (Nutrition):** Implemented. Recipes now store `calories`, `protein`, `carbs`, and `fat`. Admin panel handles inputs, and the UI displays them beautifully.
|
||||||
- **Phase 4 (Interactive Cooking Mode):** Implemented. Recipes now support step-by-step looping background videos and interactive timers (`step_videos`, `step_timers`). The UI utilizes a fullscreen overlay slider with Vanilla JS logic.
|
- **Phase 4 (Interactive Cooking Mode):** Implemented. Recipes now support step-by-step looping background videos and interactive timers (`step_videos`, `step_timers`). The UI utilizes a fullscreen overlay slider with Vanilla JS logic.
|
||||||
- **Phase 6 (Firestore Database Migration):** Migrated recipes database from `data/recipes.json` to Firebase Firestore.
|
|
||||||
- *Zero-Dependency REST API Read:* Server-side read requests in `helpers.php` use native PHP cURL to query the Firestore REST API `/documents/recipes`. Complex Firestore nested type maps are dynamically parsed into clean standard associative arrays using custom decoders.
|
|
||||||
- *Dynamic Local Fallback:* In case of rate limits, network failures, or missing `.env` config, all lookup functions automatically fail back to the local `recipes.json` flat-file, guaranteeing 100% database availability and site resilience.
|
|
||||||
- *Browser-Driven Seeding & Auto-Sync:* Admin seeding and real-time updates are executed client-side in `admin.php` via the authenticated Firebase Client SDK, keeping the local file and Cloud Firestore perfectly in sync without server-side OAuth2 keys.
|
|
||||||
|
|
||||||
## 6. Next Steps
|
## 6. Next Steps
|
||||||
According to `TODO.md`, the next major feature block is completing the database seeding (by clicking "Seed Firestore" on `admin.php` in the browser) and verifying all recipe updates reflect in real-time. Afterwards, we can proceed to **Phase 5 (PWA & Offline Support)**.
|
According to `TODO.md`, the next major feature block is **Phase 5 (PWA & Offline Support)**, which involves service workers, manifest files, and enabling the app to be installable on mobile devices.
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"projects": {
|
|
||||||
"default": "flixcooks"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -367,18 +367,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delet
|
|||||||
.list-item { padding: 12px 0; border-bottom: 1px solid rgba(255,255,255,0.08); display: flex; justify-content: space-between; align-items: center; }
|
.list-item { padding: 12px 0; border-bottom: 1px solid rgba(255,255,255,0.08); display: flex; justify-content: space-between; align-items: center; }
|
||||||
.pill { background: rgba(255,255,255,0.06); color: var(--ink); }
|
.pill { background: rgba(255,255,255,0.06); color: var(--ink); }
|
||||||
</style>
|
</style>
|
||||||
<!-- Firebase v10 compat SDKs -->
|
|
||||||
<script src="https://www.gstatic.com/firebasejs/10.8.0/firebase-app-compat.js"></script>
|
|
||||||
<script src="https://www.gstatic.com/firebasejs/10.8.0/firebase-auth-compat.js"></script>
|
|
||||||
<script src="https://www.gstatic.com/firebasejs/10.8.0/firebase-firestore-compat.js"></script>
|
|
||||||
<script>
|
|
||||||
(function() {
|
|
||||||
var config = <?php echo json_encode(get_firebase_config()); ?>;
|
|
||||||
firebase.initializeApp(config);
|
|
||||||
window.auth = firebase.auth();
|
|
||||||
window.db = firebase.firestore();
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="admin-header">
|
<div class="admin-header">
|
||||||
@@ -388,9 +376,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delet
|
|||||||
<a href="/admin.php" class="<?php echo $view === 'recipes' ? 'active' : ''; ?>">Recipes</a>
|
<a href="/admin.php" class="<?php echo $view === 'recipes' ? 'active' : ''; ?>">Recipes</a>
|
||||||
<a href="/admin.php?view=settings" class="<?php echo $view === 'settings' ? 'active' : ''; ?>">Site settings</a>
|
<a href="/admin.php?view=settings" class="<?php echo $view === 'settings' ? 'active' : ''; ?>">Site settings</a>
|
||||||
</div>
|
</div>
|
||||||
<?php if ($view === 'recipes' && !$editing): ?>
|
|
||||||
<button id="btn-seed-firestore" class="button" style="background: var(--accent-gold, #c5a880); color: #000; font-weight: 700; border: none; padding: 10px 14px; border-radius: 10px; cursor: pointer;">Seed Firestore</button>
|
|
||||||
<?php endif; ?>
|
|
||||||
<a class="button" href="/index.php" style="padding:10px 14px;">View site</a>
|
<a class="button" href="/index.php" style="padding:10px 14px;">View site</a>
|
||||||
<?php if ($editing): ?>
|
<?php if ($editing): ?>
|
||||||
<a class="button" href="/admin.php" style="padding:10px 14px;">Back</a>
|
<a class="button" href="/admin.php" style="padding:10px 14px;">Back</a>
|
||||||
@@ -653,66 +638,5 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delet
|
|||||||
<button type="submit" class="button" style="width: fit-content;"><?php echo $editing ? 'Save changes' : 'Save recipe'; ?></button>
|
<button type="submit" class="button" style="width: fit-content;"><?php echo $editing ? 'Save changes' : 'Save recipe'; ?></button>
|
||||||
</form>
|
</form>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<script>
|
|
||||||
// Firestore Seeder Logic
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
const btnSeed = document.getElementById('btn-seed-firestore');
|
|
||||||
if (btnSeed) {
|
|
||||||
btnSeed.addEventListener('click', async () => {
|
|
||||||
if (!confirm('Bist du sicher, dass du alle Rezepte nach Firestore importieren möchtest? Bestehende Dokumente mit identischem Slug werden überschrieben.')) return;
|
|
||||||
btnSeed.disabled = true;
|
|
||||||
btnSeed.innerText = 'Seeding...';
|
|
||||||
try {
|
|
||||||
const recipes = <?php echo json_encode($allRecipes); ?>;
|
|
||||||
let count = 0;
|
|
||||||
for (const recipe of recipes) {
|
|
||||||
await window.db.collection('recipes').doc(recipe.slug).set(recipe);
|
|
||||||
console.log('Seeded to Firestore:', recipe.slug);
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
alert(`Erfolgreich ${count} Rezepte nach Firestore migriert!`);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Seeding Fehler:', err);
|
|
||||||
alert('Fehler beim Seeding: ' + err.message);
|
|
||||||
} finally {
|
|
||||||
btnSeed.disabled = false;
|
|
||||||
btnSeed.innerText = 'Seed Firestore';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Auto-Sync after PHP Save -->
|
|
||||||
<?php if (isset($record) && str_contains($message ?? '', 'Saved')): ?>
|
|
||||||
<script>
|
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
|
||||||
const recipe = <?php echo json_encode($record); ?>;
|
|
||||||
try {
|
|
||||||
await window.db.collection('recipes').doc(recipe.slug).set(recipe);
|
|
||||||
console.log('Successfully synced saved recipe to Firestore:', recipe.slug);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error syncing saved recipe to Firestore:', err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<!-- Auto-Sync after PHP Delete -->
|
|
||||||
<?php if (isset($slugDel) && ($message ?? '') === 'Recipe deleted.'): ?>
|
|
||||||
<script>
|
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
|
||||||
const slugDel = <?php echo json_encode($slugDel); ?>;
|
|
||||||
try {
|
|
||||||
await window.db.collection('recipes').doc(slugDel).delete();
|
|
||||||
console.log('Successfully deleted recipe from Firestore:', slugDel);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error deleting recipe from Firestore:', err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+1
-88
@@ -7,46 +7,7 @@ if (session_status() === PHP_SESSION_NONE) {
|
|||||||
|
|
||||||
require_once __DIR__ . '/config.php';
|
require_once __DIR__ . '/config.php';
|
||||||
|
|
||||||
function decode_firestore_value($value) {
|
function load_recipes(): array {
|
||||||
if (!is_array($value)) return $value;
|
|
||||||
if (isset($value['stringValue'])) return $value['stringValue'];
|
|
||||||
if (isset($value['integerValue'])) return (int)$value['integerValue'];
|
|
||||||
if (isset($value['doubleValue'])) return (float)$value['doubleValue'];
|
|
||||||
if (isset($value['booleanValue'])) return (bool)$value['booleanValue'];
|
|
||||||
if (isset($value['nullValue'])) return null;
|
|
||||||
if (isset($value['arrayValue']['values'])) {
|
|
||||||
return array_map('decode_firestore_value', $value['arrayValue']['values']);
|
|
||||||
}
|
|
||||||
if (isset($value['mapValue']['fields'])) {
|
|
||||||
return array_map('decode_firestore_value', $value['mapValue']['fields']);
|
|
||||||
}
|
|
||||||
return $value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function decode_firestore_document(array $doc): array {
|
|
||||||
if (!isset($doc['fields'])) return [];
|
|
||||||
return array_map('decode_firestore_value', $doc['fields']);
|
|
||||||
}
|
|
||||||
|
|
||||||
function firestore_get(string $url): ?array {
|
|
||||||
$ch = curl_init();
|
|
||||||
curl_setopt($ch, CURLOPT_URL, $url);
|
|
||||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
||||||
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
|
||||||
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
|
||||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
|
|
||||||
$response = curl_exec($ch);
|
|
||||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
||||||
curl_close($ch);
|
|
||||||
|
|
||||||
if ($httpCode !== 200 || !$response) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return json_decode($response, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
function load_recipes_local(): array {
|
|
||||||
$path = __DIR__ . '/data/recipes.json';
|
$path = __DIR__ . '/data/recipes.json';
|
||||||
if (!file_exists($path)) {
|
if (!file_exists($path)) {
|
||||||
return [];
|
return [];
|
||||||
@@ -67,54 +28,6 @@ function load_recipes_local(): array {
|
|||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
function load_recipes(): array {
|
|
||||||
$config = get_firebase_config();
|
|
||||||
$projectId = $config['projectId'] ?? '';
|
|
||||||
if (empty($projectId)) {
|
|
||||||
return load_recipes_local();
|
|
||||||
}
|
|
||||||
|
|
||||||
$url = "https://firestore.googleapis.com/v1/projects/{$projectId}/databases/(default)/documents/recipes?pageSize=100";
|
|
||||||
$res = firestore_get($url);
|
|
||||||
if (!$res || !isset($res['documents'])) {
|
|
||||||
return load_recipes_local();
|
|
||||||
}
|
|
||||||
|
|
||||||
$recipes = [];
|
|
||||||
foreach ($res['documents'] as $doc) {
|
|
||||||
$decoded = decode_firestore_document($doc);
|
|
||||||
if (!empty($decoded)) {
|
|
||||||
if (!empty($decoded['hero']) && is_string($decoded['hero'])) {
|
|
||||||
$decoded['hero'] = normalize_asset_path($decoded['hero']);
|
|
||||||
}
|
|
||||||
$recipes[] = $decoded;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $recipes;
|
|
||||||
}
|
|
||||||
|
|
||||||
function load_recipe_by_slug(string $slug): ?array {
|
|
||||||
$config = get_firebase_config();
|
|
||||||
$projectId = $config['projectId'] ?? '';
|
|
||||||
if (empty($projectId)) {
|
|
||||||
return find_recipe_by_slug(load_recipes_local(), $slug);
|
|
||||||
}
|
|
||||||
|
|
||||||
$url = "https://firestore.googleapis.com/v1/projects/{$projectId}/databases/(default)/documents/recipes/" . urlencode($slug);
|
|
||||||
$res = firestore_get($url);
|
|
||||||
if (!$res || isset($res['error'])) {
|
|
||||||
return find_recipe_by_slug(load_recipes_local(), $slug);
|
|
||||||
}
|
|
||||||
|
|
||||||
$decoded = decode_firestore_document($res);
|
|
||||||
if (!empty($decoded['hero']) && is_string($decoded['hero'])) {
|
|
||||||
$decoded['hero'] = normalize_asset_path($decoded['hero']);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $decoded;
|
|
||||||
}
|
|
||||||
|
|
||||||
function save_recipes(array $recipes): bool {
|
function save_recipes(array $recipes): bool {
|
||||||
$path = __DIR__ . '/data/recipes.json';
|
$path = __DIR__ . '/data/recipes.json';
|
||||||
$json = json_encode($recipes, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
$json = json_encode($recipes, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||||
|
|||||||
+1
-1
@@ -48,7 +48,7 @@ $t = $copy[$lang];
|
|||||||
|
|
||||||
$allRecipes = load_recipes();
|
$allRecipes = load_recipes();
|
||||||
$slug = isset($_GET['slug']) ? trim($_GET['slug']) : '';
|
$slug = isset($_GET['slug']) ? trim($_GET['slug']) : '';
|
||||||
$recipeRaw = $slug ? load_recipe_by_slug($slug) : null;
|
$recipeRaw = $slug ? find_recipe_by_slug($allRecipes, $slug) : null;
|
||||||
$recipe = $recipeRaw ? localize_recipe($recipeRaw, $lang) : null;
|
$recipe = $recipeRaw ? localize_recipe($recipeRaw, $lang) : null;
|
||||||
$allRecipesLocal = localize_recipes($allRecipes, $lang);
|
$allRecipesLocal = localize_recipes($allRecipes, $lang);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user