@@ -28,12 +28,6 @@ Dieses Dokument enthält den aktuellen Entwicklungsstand und detaillierte Aufgab
|
||||
- Touch-Swipe-Karussell direkt auf index.php (unter Landing-Page)
|
||||
- Filteralgorithmus nach Benutzer-Ernährungszielen
|
||||
- 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
|
||||
- `[ ]` Noch nicht begonnen
|
||||
- `[/]` In Bearbeitung
|
||||
|
||||
+1
-5
@@ -32,10 +32,6 @@ This document summarizes the architectural knowledge, conventions, and learnings
|
||||
## 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 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
|
||||
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; }
|
||||
.pill { background: rgba(255,255,255,0.06); color: var(--ink); }
|
||||
</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>
|
||||
<body>
|
||||
<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?view=settings" class="<?php echo $view === 'settings' ? 'active' : ''; ?>">Site settings</a>
|
||||
</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>
|
||||
<?php if ($editing): ?>
|
||||
<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>
|
||||
</form>
|
||||
<?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>
|
||||
</html>
|
||||
|
||||
+1
-88
@@ -7,46 +7,7 @@ if (session_status() === PHP_SESSION_NONE) {
|
||||
|
||||
require_once __DIR__ . '/config.php';
|
||||
|
||||
function decode_firestore_value($value) {
|
||||
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 {
|
||||
function load_recipes(): array {
|
||||
$path = __DIR__ . '/data/recipes.json';
|
||||
if (!file_exists($path)) {
|
||||
return [];
|
||||
@@ -67,54 +28,6 @@ function load_recipes_local(): array {
|
||||
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 {
|
||||
$path = __DIR__ . '/data/recipes.json';
|
||||
$json = json_encode($recipes, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@ $t = $copy[$lang];
|
||||
|
||||
$allRecipes = load_recipes();
|
||||
$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;
|
||||
$allRecipesLocal = localize_recipes($allRecipes, $lang);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user