diff --git a/src/core/i18n.ts b/src/core/i18n.ts
index ee94972..21339a8 100644
--- a/src/core/i18n.ts
+++ b/src/core/i18n.ts
@@ -12,6 +12,10 @@ export interface Translations {
error: string;
notAvailable: string;
+ // App update
+ updateAvailable: string;
+ updateReload: string;
+
// Navigation
nav: {
calendar: string;
@@ -548,6 +552,9 @@ const translations: Record = {
error: 'Error',
notAvailable: 'Not available',
+ updateAvailable: 'New version available',
+ updateReload: 'Update',
+
nav: {
calendar: '📅 Calendar',
service: '📖 Service',
@@ -1116,6 +1123,9 @@ const translations: Record = {
error: 'Ошибка',
notAvailable: 'Недоступно',
+ updateAvailable: 'Достꙋ́пна новаѧ вєрсі́ѧ',
+ updateReload: 'Обнови́ти',
+
nav: {
calendar: '📅 Календарь',
service: '📖 Устав',
@@ -1684,6 +1694,9 @@ const translations: Record = {
error: 'Ошибка',
notAvailable: 'Недоступно',
+ updateAvailable: 'Доступна новая версия',
+ updateReload: 'Обновить',
+
nav: {
calendar: '📅 Календарь',
service: '📖 Уставъ',
diff --git a/src/main.ts b/src/main.ts
index 0820842..1df3a6a 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,5 +1,32 @@
import './styles/main.css';
+import { registerSW } from 'virtual:pwa-register';
import { App } from './ui/app';
+import { getTranslations, type LanguageCode } from './core/i18n';
+import { loadSettings } from './ui/settings-view';
+
+const updateSW = registerSW({
+ immediate: true,
+ onNeedRefresh: () => showUpdateToast(),
+});
+
+function showUpdateToast() {
+ if (document.getElementById('pwa-update-toast')) return;
+ const t = getTranslations(loadSettings().language as LanguageCode);
+ const toast = document.createElement('div');
+ toast.id = 'pwa-update-toast';
+ toast.className = 'fixed bottom-4 left-1/2 -translate-x-1/2 z-[60] bg-surface border border-gold/40 rounded-lg shadow-xl px-4 py-3 flex items-center gap-3 max-w-[calc(100vw-2rem)]';
+ toast.innerHTML = `
+ ${t.updateAvailable}
+
+
+ `;
+ document.body.appendChild(toast);
+ toast.querySelector('#pwa-update-btn')?.addEventListener('click', () => {
+ (toast.querySelector('#pwa-update-btn') as HTMLButtonElement).disabled = true;
+ updateSW(true);
+ });
+ toast.querySelector('#pwa-update-dismiss')?.addEventListener('click', () => toast.remove());
+}
const app = new App(document.getElementById('app')!);
app.init();
diff --git a/src/ui/settings-view.ts b/src/ui/settings-view.ts
index 8f47687..61843b9 100644
--- a/src/ui/settings-view.ts
+++ b/src/ui/settings-view.ts
@@ -319,6 +319,7 @@ export class SettingsView {
${t.settings.aboutLicense}
+ build ${__BUILD_DATE__}
`;
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
index 11f02fe..9860a3a 100644
--- a/src/vite-env.d.ts
+++ b/src/vite-env.d.ts
@@ -1 +1,4 @@
///
+///
+
+declare const __BUILD_DATE__: string;
diff --git a/vite.config.ts b/vite.config.ts
index 1a5afbc..6f593f6 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -6,11 +6,10 @@ import { VitePWA } from 'vite-plugin-pwa';
export default defineConfig({
plugins: [
tailwindcss(),
- VitePWA({
- registerType: 'autoUpdate',
+ VitePWA({ registerType: 'prompt',
workbox: {
- skipWaiting: true,
- clientsClaim: true,
+ skipWaiting: false,
+ clientsClaim: false,
globPatterns: ['**/*.{js,css,html,woff,png}'],
globIgnores: ['**/data/**'],
maximumFileSizeToCacheInBytes: 20 * 1024 * 1024,
@@ -66,5 +65,8 @@ export default defineConfig({
outDir: 'dist',
sourcemap: true,
},
+ define: {
+ __BUILD_DATE__: JSON.stringify(new Date().toISOString().slice(0, 10)),
+ },
publicDir: 'static',
});
\ No newline at end of file