diff --git a/listify-client/public/manifest.webmanifest b/listify-client/public/manifest.webmanifest new file mode 100644 index 0000000..16c6b57 --- /dev/null +++ b/listify-client/public/manifest.webmanifest @@ -0,0 +1,18 @@ +{ + "name": "Listify", + "short_name": "Listify", + "description": "Listen, Templates und Aufgaben organisieren.", + "start_url": "/lists", + "scope": "/", + "display": "standalone", + "background_color": "#fffbfe", + "theme_color": "#6750a4", + "icons": [ + { + "src": "/pwa-icon.svg", + "sizes": "any", + "type": "image/svg+xml", + "purpose": "any maskable" + } + ] +} diff --git a/listify-client/public/pwa-icon.svg b/listify-client/public/pwa-icon.svg new file mode 100644 index 0000000..ce6e812 --- /dev/null +++ b/listify-client/public/pwa-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/listify-client/public/sw.js b/listify-client/public/sw.js new file mode 100644 index 0000000..4cb0722 --- /dev/null +++ b/listify-client/public/sw.js @@ -0,0 +1,65 @@ +const CACHE_NAME = 'listify-shell-v1'; +const SHELL_ASSETS = ['/', '/index.html', '/manifest.webmanifest', '/pwa-icon.svg']; + +self.addEventListener('install', (event) => { + event.waitUntil( + caches + .open(CACHE_NAME) + .then((cache) => cache.addAll(SHELL_ASSETS)) + .then(() => self.skipWaiting()), + ); +}); + +self.addEventListener('activate', (event) => { + event.waitUntil( + caches + .keys() + .then((keys) => + Promise.all( + keys + .filter((key) => key !== CACHE_NAME) + .map((key) => caches.delete(key)), + ), + ) + .then(() => self.clients.claim()), + ); +}); + +self.addEventListener('fetch', (event) => { + const request = event.request; + const url = new URL(request.url); + + if (url.origin !== self.location.origin || url.pathname.startsWith('/api/')) { + return; + } + + if (request.mode === 'navigate') { + event.respondWith( + fetch(request) + .then((response) => { + const responseClone = response.clone(); + caches.open(CACHE_NAME).then((cache) => cache.put('/index.html', responseClone)); + return response; + }) + .catch(() => caches.match('/index.html')), + ); + return; + } + + event.respondWith( + caches.match(request).then((cached) => { + if (cached) { + return cached; + } + + return fetch(request).then((response) => { + if (request.method === 'GET' && response.ok) { + const responseClone = response.clone(); + caches.open(CACHE_NAME).then((cache) => cache.put(request, responseClone)); + } + + return response; + }); + }), + ); +}); diff --git a/listify-client/src/app/app.html b/listify-client/src/app/app.html index cb6935e..127d37c 100644 --- a/listify-client/src/app/app.html +++ b/listify-client/src/app/app.html @@ -19,6 +19,18 @@ @if (auth.isAuthenticated()) { + + + @if (offlineSync.pendingCount() > 0) { + {{ offlineSync.pendingCount() }} + } + {{ auth.user()?.name || auth.user()?.email }} } @else { state.matches)), @@ -43,6 +47,8 @@ export class App implements OnInit { if (this.auth.isAuthenticated()) { this.auth.loadCurrentUser().subscribe({ error: () => undefined }); } + + void this.offlineSync.syncNow(); } protected toggleSidebar(): void { diff --git a/listify-client/src/app/assistant/assistant-chat.component.html b/listify-client/src/app/assistant/assistant-chat.component.html index 4384e8e..6e4d034 100644 --- a/listify-client/src/app/assistant/assistant-chat.component.html +++ b/listify-client/src/app/assistant/assistant-chat.component.html @@ -2,9 +2,9 @@

Assistent

-

Listen planen und erstellen

+

{{ onlineStatus.online() ? 'Listen planen und erstellen' : 'Nur online verfuegbar' }}

- +
@@ -50,6 +50,7 @@ matInput rows="3" [value]="draft()" + [disabled]="!onlineStatus.online()" (input)="draft.set($any($event.target).value)" (keydown.enter)="handleEnter($event)" > diff --git a/listify-client/src/app/assistant/assistant-chat.component.ts b/listify-client/src/app/assistant/assistant-chat.component.ts index 2e293ca..433dc9b 100644 --- a/listify-client/src/app/assistant/assistant-chat.component.ts +++ b/listify-client/src/app/assistant/assistant-chat.component.ts @@ -15,6 +15,7 @@ import { } from './assistant.models'; import { AssistantService } from './assistant.service'; import { ListsService } from '../lists/lists.service'; +import { OnlineStatusService } from '../offline/online-status.service'; import { TemplatesService } from '../templates/templates.service'; @Component({ @@ -33,6 +34,7 @@ import { TemplatesService } from '../templates/templates.service'; export class AssistantChatComponent { private readonly assistantService = inject(AssistantService); private readonly listsService = inject(ListsService); + protected readonly onlineStatus = inject(OnlineStatusService); private readonly router = inject(Router); private readonly templatesService = inject(TemplatesService); @@ -46,13 +48,20 @@ export class AssistantChatComponent { protected readonly sending = signal(false); protected readonly errorMessage = signal(null); protected readonly canSend = computed( - () => this.draft().trim().length > 0 && !this.sending(), + () => + this.draft().trim().length > 0 && + !this.sending() && + this.onlineStatus.online(), ); protected send(): void { const content = this.draft().trim(); - if (!content || this.sending()) { + if (!content || this.sending() || !this.onlineStatus.online()) { + if (!this.onlineStatus.online()) { + this.errorMessage.set('Der Assistent ist nur online verfuegbar.'); + } + return; } diff --git a/listify-client/src/app/lists/list-detail/list-detail.component.html b/listify-client/src/app/lists/list-detail/list-detail.component.html index 2041350..3a89c84 100644 --- a/listify-client/src/app/lists/list-detail/list-detail.component.html +++ b/listify-client/src/app/lists/list-detail/list-detail.component.html @@ -259,7 +259,7 @@