feat(offline): hoist sync UI to homepage, slow auto-sync to weekly
Move OfflineSyncIndicator (logo pip) and OfflineSyncBanner from the [recipeLang] layout/page to (main)/+layout.svelte and (main)/+page.svelte. Sync is an app-wide concern, not recipe-specific, and surfacing it on the homepage gives the entry point users actually see when they install the PWA. Indicator pulls language from languageStore since (main) doesn't have data.lang from a recipe-scoped load. Drop the now-unused .banner-wrap CSS and OfflineSyncIndicator/Banner imports from the recipe routes. Auto-sync cadence: - AUTO_SYNC_INTERVAL 30 min -> 1 week. Recipes don't change often enough to justify a half-hourly background download (the user explicitly wanted this dialed back). - Internal poll tick 5 min -> 1 hour. Polling 12x an hour for a weekly event is wasted work; hourly is fine and still responsive when the weekly window opens. Bump 1.65.3 -> 1.66.0.
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "homepage",
|
"name": "homepage",
|
||||||
"version": "1.65.3",
|
"version": "1.66.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { browser } from '$app/environment';
|
|||||||
import { isOfflineDataAvailable, getLastSync, clearOfflineData } from '$lib/offline/db';
|
import { isOfflineDataAvailable, getLastSync, clearOfflineData } from '$lib/offline/db';
|
||||||
import { downloadAllRecipes, type SyncResult, type SyncProgress } from '$lib/offline/sync';
|
import { downloadAllRecipes, type SyncResult, type SyncProgress } from '$lib/offline/sync';
|
||||||
|
|
||||||
const AUTO_SYNC_INTERVAL = 30 * 60 * 1000; // 30 minutes
|
const AUTO_SYNC_INTERVAL = 7 * 24 * 60 * 60 * 1000; // 1 week
|
||||||
const LAST_SYNC_KEY = 'bocken-last-sync-time';
|
const LAST_SYNC_KEY = 'bocken-last-sync-time';
|
||||||
|
|
||||||
type PWAState = {
|
type PWAState = {
|
||||||
@@ -152,12 +152,13 @@ function createPWAStore() {
|
|||||||
startAutoSync() {
|
startAutoSync() {
|
||||||
if (autoSyncInterval) return; // Already running
|
if (autoSyncInterval) return; // Already running
|
||||||
|
|
||||||
// Check every 5 minutes if we should sync
|
// Check hourly if we should sync — actual sync only fires once
|
||||||
|
// AUTO_SYNC_INTERVAL has elapsed since the last sync.
|
||||||
autoSyncInterval = setInterval(() => {
|
autoSyncInterval = setInterval(() => {
|
||||||
autoSync();
|
autoSync();
|
||||||
}, 5 * 60 * 1000); // Check every 5 minutes
|
}, 60 * 60 * 1000);
|
||||||
|
|
||||||
console.log('[PWA] Auto-sync enabled (every 30 minutes)');
|
console.log('[PWA] Auto-sync enabled (weekly)');
|
||||||
},
|
},
|
||||||
|
|
||||||
stopAutoSync() {
|
stopAutoSync() {
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
import Header from '$lib/components/Header.svelte'
|
import Header from '$lib/components/Header.svelte'
|
||||||
import UserHeader from '$lib/components/UserHeader.svelte';
|
import UserHeader from '$lib/components/UserHeader.svelte';
|
||||||
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
|
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
|
||||||
|
import OfflineSyncIndicator from '$lib/components/OfflineSyncIndicator.svelte';
|
||||||
|
import { languageStore } from '$lib/stores/language.svelte';
|
||||||
let { data, children } = $props();
|
let { data, children } = $props();
|
||||||
|
|
||||||
let user = $derived(data.session?.user);
|
let user = $derived(data.session?.user);
|
||||||
@@ -16,9 +18,25 @@ let user = $derived(data.session?.user);
|
|||||||
<LanguageSelector />
|
<LanguageSelector />
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
|
{#snippet logo_overlay()}
|
||||||
|
<div class="logo-pip">
|
||||||
|
<OfflineSyncIndicator lang={languageStore.value} />
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
{#snippet right_side()}
|
{#snippet right_side()}
|
||||||
<UserHeader {user}></UserHeader>
|
<UserHeader {user}></UserHeader>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</Header>
|
</Header>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:global(.logo-pip) {
|
||||||
|
position: absolute;
|
||||||
|
top: -8px;
|
||||||
|
right: -7px;
|
||||||
|
z-index: 2;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { resolve } from '$app/paths';
|
import { resolve } from '$app/paths';
|
||||||
import LinksGrid from "$lib/components/LinksGrid.svelte";
|
import LinksGrid from "$lib/components/LinksGrid.svelte";
|
||||||
import Seo from '$lib/components/Seo.svelte';
|
import Seo from '$lib/components/Seo.svelte';
|
||||||
|
import OfflineSyncBanner from '$lib/components/OfflineSyncBanner.svelte';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
|
|
||||||
@@ -143,6 +144,8 @@ section h2{
|
|||||||
</section>
|
</section>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<OfflineSyncBanner {lang} />
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
|
|
||||||
<h2>{labels.pages}</h2>
|
<h2>{labels.pages}</h2>
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ onNavigate((navigation) => {
|
|||||||
});
|
});
|
||||||
import UserHeader from '$lib/components/UserHeader.svelte';
|
import UserHeader from '$lib/components/UserHeader.svelte';
|
||||||
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
|
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
|
||||||
import OfflineSyncIndicator from '$lib/components/OfflineSyncIndicator.svelte';
|
|
||||||
import BookOpen from '@lucide/svelte/icons/book-open';
|
import BookOpen from '@lucide/svelte/icons/book-open';
|
||||||
import Heart from '@lucide/svelte/icons/heart';
|
import Heart from '@lucide/svelte/icons/heart';
|
||||||
import Leaf from '@lucide/svelte/icons/leaf';
|
import Leaf from '@lucide/svelte/icons/leaf';
|
||||||
@@ -134,12 +133,6 @@ const recipeCanonicalPath = $derived(recipeAltPath(page.url.pathname, /** @type
|
|||||||
<LanguageSelector lang={data.lang} />
|
<LanguageSelector lang={data.lang} />
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
{#snippet logo_overlay()}
|
|
||||||
<div class="logo-pip">
|
|
||||||
<OfflineSyncIndicator lang={data.lang} />
|
|
||||||
</div>
|
|
||||||
{/snippet}
|
|
||||||
|
|
||||||
{#snippet right_side()}
|
{#snippet right_side()}
|
||||||
<UserHeader {user} recipeLang={data.recipeLang} lang={data.lang}></UserHeader>
|
<UserHeader {user} recipeLang={data.recipeLang} lang={data.lang}></UserHeader>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
@@ -147,12 +140,3 @@ const recipeCanonicalPath = $derived(recipeAltPath(page.url.pathname, /** @type
|
|||||||
{@render children()}
|
{@render children()}
|
||||||
</Header>
|
</Header>
|
||||||
|
|
||||||
<style>
|
|
||||||
:global(.logo-pip) {
|
|
||||||
position: absolute;
|
|
||||||
top: -8px;
|
|
||||||
right: -7px;
|
|
||||||
z-index: 2;
|
|
||||||
pointer-events: auto;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
import AddButton from '$lib/components/AddButton.svelte';
|
import AddButton from '$lib/components/AddButton.svelte';
|
||||||
import Seo from '$lib/components/Seo.svelte';
|
import Seo from '$lib/components/Seo.svelte';
|
||||||
import CompactCard from '$lib/components/recipes/CompactCard.svelte';
|
import CompactCard from '$lib/components/recipes/CompactCard.svelte';
|
||||||
import OfflineSyncBanner from '$lib/components/OfflineSyncBanner.svelte';
|
|
||||||
import Search from '$lib/components/recipes/Search.svelte';
|
import Search from '$lib/components/recipes/Search.svelte';
|
||||||
import { getCategories } from '$lib/js/categories';
|
import { getCategories } from '$lib/js/categories';
|
||||||
import { m, type RecipesLang } from '$lib/js/recipesI18n';
|
import { m, type RecipesLang } from '$lib/js/recipesI18n';
|
||||||
@@ -315,19 +314,6 @@
|
|||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ─── Offline sync banner — between search and recipe grid ─── */
|
|
||||||
.banner-wrap {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0 2em;
|
|
||||||
position: relative;
|
|
||||||
z-index: 9;
|
|
||||||
}
|
|
||||||
.banner-wrap.fallback {
|
|
||||||
padding: 0 2em;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sentinel {
|
.sentinel {
|
||||||
height: 1px;
|
height: 1px;
|
||||||
}
|
}
|
||||||
@@ -442,9 +428,6 @@
|
|||||||
<div class="hero-search-wrap">
|
<div class="hero-search-wrap">
|
||||||
<Search lang={data.lang} recipes={data.all_brief} isLoggedIn={!!data.session?.user} onSearchResults={handleSearchResults}></Search>
|
<Search lang={data.lang} recipes={data.all_brief} isLoggedIn={!!data.session?.user} onSearchResults={handleSearchResults}></Search>
|
||||||
</div>
|
</div>
|
||||||
<div class="banner-wrap">
|
|
||||||
<OfflineSyncBanner lang={data.lang} />
|
|
||||||
</div>
|
|
||||||
<div class="recipe-grid">
|
<div class="recipe-grid">
|
||||||
{#each visibleRecipes as recipe, i (recipe._id)}
|
{#each visibleRecipes as recipe, i (recipe._id)}
|
||||||
<CompactCard
|
<CompactCard
|
||||||
@@ -472,9 +455,6 @@
|
|||||||
<h1>{labels.title}</h1>
|
<h1>{labels.title}</h1>
|
||||||
<p class="subheading">{labels.subheading}</p>
|
<p class="subheading">{labels.subheading}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="banner-wrap fallback">
|
|
||||||
<OfflineSyncBanner lang={data.lang} />
|
|
||||||
</div>
|
|
||||||
<Search lang={data.lang} recipes={data.all_brief} isLoggedIn={!!data.session?.user} onSearchResults={handleSearchResults}></Search>
|
<Search lang={data.lang} recipes={data.all_brief} isLoggedIn={!!data.session?.user} onSearchResults={handleSearchResults}></Search>
|
||||||
|
|
||||||
<div class="recipe-grid">
|
<div class="recipe-grid">
|
||||||
|
|||||||
Reference in New Issue
Block a user