feat: extend PWA offline support to all recipe routes and glaube pages

- Add offline support for category, tag, icon list pages
- Add offline support for favorites page (stores locally for offline)
- Add offline support for season list page
- Cache root page and glaube pages for offline access
- Dynamically discover glaube routes at build time using Vite glob
- Add db functions for getAllCategories, getAllTags, getAllIcons
- Pre-cache __data.json for all category, tag, icon, season subroutes
- Update service worker to cache glaube and root page responses
This commit is contained in:
2026-01-29 09:54:26 +01:00
parent 9ff30b28cd
commit c86a734da0
8 changed files with 371 additions and 29 deletions

View File

@@ -1,10 +1,43 @@
import type { PageLoad } from "./$types";
import { browser } from '$app/environment';
import { isOffline, canUseOfflineData } from '$lib/offline/helpers';
import { getAllCategories, isOfflineDataAvailable } from '$lib/offline/db';
export async function load({ fetch, params}) {
export const load: PageLoad = async ({ fetch, params }) => {
const isEnglish = params.recipeLang === 'recipes';
const apiBase = isEnglish ? '/api/recipes' : '/api/rezepte';
const res = await fetch(`${apiBase}/items/category`);
const categories= await res.json();
return {categories}
// Check if we should use offline data
if (browser && isOffline() && canUseOfflineData()) {
try {
const hasOfflineData = await isOfflineDataAvailable();
if (hasOfflineData) {
const categories = await getAllCategories();
return { categories, isOffline: true };
}
} catch (error) {
console.error('Failed to load offline categories:', error);
}
}
// Online mode - fetch from API
try {
const res = await fetch(`${apiBase}/items/category`);
const categories = await res.json();
return { categories, isOffline: false };
} catch (error) {
// Network error - try offline fallback
if (browser && canUseOfflineData()) {
try {
const hasOfflineData = await isOfflineDataAvailable();
if (hasOfflineData) {
const categories = await getAllCategories();
return { categories, isOffline: true };
}
} catch (offlineError) {
console.error('Failed to load offline categories:', offlineError);
}
}
throw error;
}
};

View File

@@ -0,0 +1,78 @@
import type { PageLoad } from "./$types";
import { browser } from '$app/environment';
import { isOffline, canUseOfflineData } from '$lib/offline/helpers';
import { getAllBriefRecipes, isOfflineDataAvailable } from '$lib/offline/db';
// Store favorites in localStorage for offline access
const FAVORITES_STORAGE_KEY = 'bocken-favorites';
function getStoredFavorites(): string[] {
if (!browser) return [];
try {
const stored = localStorage.getItem(FAVORITES_STORAGE_KEY);
return stored ? JSON.parse(stored) : [];
} catch {
return [];
}
}
function storeFavorites(favoriteIds: string[]): void {
if (!browser) return;
try {
localStorage.setItem(FAVORITES_STORAGE_KEY, JSON.stringify(favoriteIds));
} catch {
// Storage full or unavailable
}
}
export const load: PageLoad = async ({ data, params }) => {
const isEnglish = params.recipeLang === 'recipes';
// If we have server data, store the favorite IDs for offline use
if (data?.favorites && Array.isArray(data.favorites) && data.favorites.length > 0) {
const favoriteIds = data.favorites.map((r: any) => r.short_name);
storeFavorites(favoriteIds);
}
// Check if we should use offline data
const shouldUseOffline = browser && (isOffline() || data?.isOffline) && canUseOfflineData();
if (shouldUseOffline) {
try {
const hasOfflineData = await isOfflineDataAvailable();
if (hasOfflineData) {
const storedFavoriteIds = getStoredFavorites();
if (storedFavoriteIds.length === 0) {
return {
...data,
favorites: [],
isOffline: true,
offlineMessage: isEnglish
? 'Favorites are not available offline. Please sync while online first.'
: 'Favoriten sind offline nicht verfügbar. Bitte zuerst online synchronisieren.'
};
}
const allRecipes = await getAllBriefRecipes();
const favorites = allRecipes
.filter(recipe => storedFavoriteIds.includes(recipe.short_name))
.map(recipe => ({ ...recipe, isFavorite: true }));
return {
...data,
favorites,
isOffline: true
};
}
} catch (error) {
console.error('Failed to load offline favorites:', error);
}
}
// Return server data as-is
return {
...data,
isOffline: false
};
};

View File

@@ -1,10 +1,40 @@
import type { PageLoad } from "./$types";
import { browser } from '$app/environment';
import { isOffline, canUseOfflineData } from '$lib/offline/helpers';
import { getAllIcons, isOfflineDataAvailable } from '$lib/offline/db';
export async function load({ fetch }) {
let current_month = new Date().getMonth() + 1
const res_icons = await fetch(`/api/rezepte/items/icon`);
const item = await res_icons.json();
return {
icons: item,
};
export const load: PageLoad = async ({ fetch }) => {
// Check if we should use offline data
if (browser && isOffline() && canUseOfflineData()) {
try {
const hasOfflineData = await isOfflineDataAvailable();
if (hasOfflineData) {
const icons = await getAllIcons();
return { icons, isOffline: true };
}
} catch (error) {
console.error('Failed to load offline icons:', error);
}
}
// Online mode - fetch from API
try {
const res_icons = await fetch(`/api/rezepte/items/icon`);
const icons = await res_icons.json();
return { icons, isOffline: false };
} catch (error) {
// Network error - try offline fallback
if (browser && canUseOfflineData()) {
try {
const hasOfflineData = await isOfflineDataAvailable();
if (hasOfflineData) {
const icons = await getAllIcons();
return { icons, isOffline: true };
}
} catch (offlineError) {
console.error('Failed to load offline icons:', offlineError);
}
}
throw error;
}
};

View File

@@ -0,0 +1,41 @@
import { browser } from '$app/environment';
import { isOffline, canUseOfflineData } from '$lib/offline/helpers';
import { getBriefRecipesBySeason, isOfflineDataAvailable } from '$lib/offline/db';
import { rand_array } from '$lib/js/randomize';
export async function load({ data }) {
// On the server, just pass through the server data unchanged
if (!browser) {
return {
...data,
isOffline: false
};
}
// On the client, check if we need to load from IndexedDB
const shouldUseOfflineData = (isOffline() || data?.isOffline || !data?.season?.length) && canUseOfflineData();
if (shouldUseOfflineData) {
try {
const hasOfflineData = await isOfflineDataAvailable();
if (hasOfflineData) {
const currentMonth = new Date().getMonth() + 1;
const recipes = await getBriefRecipesBySeason(currentMonth);
return {
...data,
season: rand_array(recipes),
isOffline: true
};
}
} catch (error) {
console.error('Failed to load offline season data:', error);
}
}
// Return server data as-is
return {
...data,
isOffline: false
};
}

View File

@@ -1,10 +1,43 @@
import type { PageLoad } from "./$types";
import { browser } from '$app/environment';
import { isOffline, canUseOfflineData } from '$lib/offline/helpers';
import { getAllTags, isOfflineDataAvailable } from '$lib/offline/db';
export async function load({ fetch, params}) {
export const load: PageLoad = async ({ fetch, params }) => {
const isEnglish = params.recipeLang === 'recipes';
const apiBase = isEnglish ? '/api/recipes' : '/api/rezepte';
const res = await fetch(`${apiBase}/items/tag`);
const tags = await res.json();
return {tags}
// Check if we should use offline data
if (browser && isOffline() && canUseOfflineData()) {
try {
const hasOfflineData = await isOfflineDataAvailable();
if (hasOfflineData) {
const tags = await getAllTags();
return { tags, isOffline: true };
}
} catch (error) {
console.error('Failed to load offline tags:', error);
}
}
// Online mode - fetch from API
try {
const res = await fetch(`${apiBase}/items/tag`);
const tags = await res.json();
return { tags, isOffline: false };
} catch (error) {
// Network error - try offline fallback
if (browser && canUseOfflineData()) {
try {
const hasOfflineData = await isOfflineDataAvailable();
if (hasOfflineData) {
const tags = await getAllTags();
return { tags, isOffline: true };
}
} catch (offlineError) {
console.error('Failed to load offline tags:', offlineError);
}
}
throw error;
}
};