A simple to-do app focused on tasks that can be completed within a specific time span.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

lints

benni043 2d44cccd bd856ad2

+138 -126
+47 -47
app/pages/index.vue
··· 26 26 const { isMobile } = useDevice(); 27 27 28 28 onMounted(async () => { 29 - if ("serviceWorker" in navigator) { 30 - try { 31 - const reg = await navigator.serviceWorker.register("/sw.js"); 29 + if ("serviceWorker" in navigator) { 30 + try { 31 + const reg = await navigator.serviceWorker.register("/sw.js"); 32 32 33 - await navigator.serviceWorker.ready; 33 + await navigator.serviceWorker.ready; 34 34 35 - console.log("SW ist bereit und aktiv:", reg.active); 36 - } catch (err) { 37 - console.error("SW registration failed", err); 38 - } 39 - } 40 - }); 35 + console.log("SW ist bereit und aktiv:", reg.active); 36 + } catch (err) { 37 + console.error("SW registration failed", err); 38 + } 39 + } 40 + }); 41 41 42 - async function send() { 43 - const permission = await Notification.requestPermission(); 42 + async function send() { 43 + const permission = await Notification.requestPermission(); 44 44 45 - if (permission !== "granted") return; 45 + if (permission !== "granted") return; 46 46 47 - const reg = await navigator.serviceWorker.ready; 47 + const reg = await navigator.serviceWorker.ready; 48 48 49 - const sub = await reg.pushManager.subscribe({ 50 - userVisibleOnly: true, 51 - applicationServerKey: 52 - "BKkvpMKOQ3wvNUpoohpuZmTUCNe8rH4bZwCbTeLW16F1ZeUm9DDEavdpXOfXIR6PWZpPswiCYte1KMveWMFvslY", 53 - }); 49 + const sub = await reg.pushManager.subscribe({ 50 + userVisibleOnly: true, 51 + applicationServerKey: 52 + "BKkvpMKOQ3wvNUpoohpuZmTUCNe8rH4bZwCbTeLW16F1ZeUm9DDEavdpXOfXIR6PWZpPswiCYte1KMveWMFvslY", 53 + }); 54 54 55 - const fetch = useRequestFetch(); 55 + const fetch = useRequestFetch(); 56 56 57 - const response = await fetch(`/api/subscription/`, { 58 - method: "POST", 59 - body: sub.toJSON(), 60 - ...useFetchOptions(), 61 - }).catch(async (err) => { 62 - //todo - show in toast 63 - console.warn(err); 64 - // await this.fetch(); 65 - }); 57 + const response = await fetch(`/api/subscription/`, { 58 + method: "POST", 59 + body: sub.toJSON(), 60 + ...useFetchOptions(), 61 + }).catch(async (err) => { 62 + //todo - show in toast 63 + console.warn(err); 64 + // await this.fetch(); 65 + }); 66 66 67 - console.log(response) 68 - } 67 + console.log(response); 68 + } 69 69 70 - async function msg() { 71 - const fetch = useRequestFetch(); 70 + async function msg() { 71 + const fetch = useRequestFetch(); 72 72 73 - await fetch(`/api/subscription/sendNotification`, { 74 - method: "POST", 75 - ...useFetchOptions(), 76 - }).catch(async (err) => { 77 - //todo - show in toast 78 - console.warn(err); 79 - // await this.fetch(); 80 - }); 81 - } 73 + await fetch(`/api/subscription/sendNotification`, { 74 + method: "POST", 75 + ...useFetchOptions(), 76 + }).catch(async (err) => { 77 + //todo - show in toast 78 + console.warn(err); 79 + // await this.fetch(); 80 + }); 81 + } 82 82 </script> 83 83 <template> 84 84 <div> ··· 88 88 <NewTodoSheet v-model:is-open="isNewSheetOpen" /> 89 89 90 90 <div> 91 - <h1>testing begin</h1> 91 + <h1>testing begin</h1> 92 92 93 - <button @click="send()">send</button> 94 - <br></br> 95 - <button @click="msg()">message</button> 93 + <button type="button" @click="send()">send</button> 94 + <br> 95 + <button type="button" @click="msg()">message</button> 96 96 97 - <h1>testing end</h1> 97 + <h1>testing end</h1> 98 98 </div> 99 99 100 100 <div v-if="!isMobile">
+9 -9
nuxt.config.ts
··· 53 53 { code: "de", name: "German", file: "de.json" }, 54 54 ], 55 55 defaultLocale: "en", 56 - }, 57 - nitro: { 58 - experimental: { 59 - tasks: true, 60 - }, 61 - scheduledTasks: { 62 - "* * * * *": ["notify:push"], 63 - }, 64 - }, 56 + }, 57 + nitro: { 58 + experimental: { 59 + tasks: true, 60 + }, 61 + scheduledTasks: { 62 + "* * * * *": ["notify:push"], 63 + }, 64 + }, 65 65 });
+1
package.json
··· 47 47 "@biomejs/biome": "^2.4.10", 48 48 "@nuxt/test-utils": "^4.0.0", 49 49 "@playwright/test": "^1.58.0", 50 + "@types/web-push": "^3.6.4", 50 51 "@vue/test-utils": "^2.4.6", 51 52 "happy-dom": "^20.8.9", 52 53 "typescript": "^6.0.2",
+10
pnpm-lock.yaml
··· 90 90 '@playwright/test': 91 91 specifier: ^1.58.0 92 92 version: 1.58.2 93 + '@types/web-push': 94 + specifier: ^3.6.4 95 + version: 3.6.4 93 96 '@vue/test-utils': 94 97 specifier: ^2.4.6 95 98 version: 2.4.6 ··· 2250 2253 2251 2254 '@types/web-bluetooth@0.0.21': 2252 2255 resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} 2256 + 2257 + '@types/web-push@3.6.4': 2258 + resolution: {integrity: sha512-GnJmSr40H3RAnj0s34FNTcJi1hmWFV5KXugE0mYWnYhgTAHLJ/dJKAwDmvPJYMke0RplY2XE9LnM4hqSqKIjhQ==} 2253 2259 2254 2260 '@types/webidl-conversions@7.0.3': 2255 2261 resolution: {integrity: sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==} ··· 7073 7079 '@types/web-bluetooth@0.0.20': {} 7074 7080 7075 7081 '@types/web-bluetooth@0.0.21': {} 7082 + 7083 + '@types/web-push@3.6.4': 7084 + dependencies: 7085 + '@types/node': 25.5.0 7076 7086 7077 7087 '@types/webidl-conversions@7.0.3': 7078 7088 optional: true
+7 -7
public/sw.js
··· 1 - self.addEventListener("push", function (event) { 2 - const data = event.data.json(); 1 + self.addEventListener("push", (event) => { 2 + const data = event.data.json(); 3 3 4 - event.waitUntil( 5 - self.registration.showNotification(data.title, { 6 - body: data.body, 7 - }), 8 - ); 4 + event.waitUntil( 5 + self.registration.showNotification(data.title, { 6 + body: data.body, 7 + }), 8 + ); 9 9 });
+1 -4
server/api/subscription/.post.ts
··· 5 5 async (event, userId): Promise<PushSubscriptionJSON> => { 6 6 const body = await readValidatedBody(event, PushSubscriptionJSON.parse); 7 7 8 - const subscription = await Subscriptions.updateOrAdd( 9 - userId, 10 - body, 11 - ); 8 + const subscription = await Subscriptions.updateOrAdd(userId, body); 12 9 if (subscription instanceof Error) throw subscription; 13 10 14 11 SubscriptionEventStream.sendUpdate(userId);
+3 -5
server/api/subscription/sendNotification.post.ts
··· 1 - export default defineAuthenticatedEventHandler( 2 - async (event, userId) => { 3 - await Subscriptions.sendNotification(userId); 4 - }, 5 - ); 1 + export default defineAuthenticatedEventHandler(async (_event, userId) => { 2 + await Subscriptions.sendNotification(userId); 3 + });
+2 -2
server/plugins/storage.ts
··· 51 51 fsDriver({ 52 52 base: ".data/categories", 53 53 }), 54 - ); 54 + ); 55 55 storage.mount( 56 56 "subscriptions", 57 57 fsDriver({ ··· 86 86 token: useRuntimeConfig().upstash.token, 87 87 base: "categories", 88 88 }), 89 - ); 89 + ); 90 90 storage.mount( 91 91 "subscriptions", 92 92 upstashDriver({
+7 -7
server/tasks/notify/push.ts
··· 1 1 export default defineTask({ 2 - async run(event) { 3 - console.log("Task running"); 2 + async run(_event) { 3 + console.log("Task running"); 4 4 5 - const result = { 6 - sent: 10, 7 - }; 5 + const result = { 6 + sent: 10, 7 + }; 8 8 9 - return { result }; 10 - }, 9 + return { result }; 10 + }, 11 11 });
-1
server/types/web-push.d.ts
··· 1 - declare module "web-push";
+40 -37
server/utils/db/subscription.ts
··· 1 1 import type { NuxtError } from "nuxt/app"; 2 - import type { PushSubscriptionJSON } from "~~/shared/types"; 3 2 import webpush from "web-push"; 3 + import type { PushSubscriptionJSON } from "~~/shared/types"; 4 4 5 5 function getKey(userId: string): string { 6 6 return `subscriptions:${userId}`; 7 7 } 8 8 9 9 webpush.setVapidDetails( 10 - "mailto:you@example.com", 11 - "BKkvpMKOQ3wvNUpoohpuZmTUCNe8rH4bZwCbTeLW16F1ZeUm9DDEavdpXOfXIR6PWZpPswiCYte1KMveWMFvslY", 12 - "wGiryLM_sL1fRZfWhcYeJ_ZgSucqtOn1S_A60lBThnc", 10 + "mailto:you@example.com", 11 + "BKkvpMKOQ3wvNUpoohpuZmTUCNe8rH4bZwCbTeLW16F1ZeUm9DDEavdpXOfXIR6PWZpPswiCYte1KMveWMFvslY", 12 + "wGiryLM_sL1fRZfWhcYeJ_ZgSucqtOn1S_A60lBThnc", 13 13 ); 14 14 15 15 export const Subscriptions = { 16 - async getAll(userId: string): Promise<PushSubscriptionJSON[]> { 16 + async getAll(userId: string): Promise<PushSubscriptionJSON[]> { 17 17 const storage = useStorage(); 18 18 return (await storage.get<PushSubscriptionJSON[]>(getKey(userId))) ?? []; 19 - }, 20 - async updateOrAdd( 19 + }, 20 + async updateOrAdd( 21 21 userId: string, 22 22 subscription: PushSubscriptionJSON, 23 23 ): Promise<PushSubscriptionJSON | NuxtError> { 24 24 const storage = useStorage(); 25 - const subscriptions = await Subscriptions.getAll(userId); 25 + const subscriptions = await Subscriptions.getAll(userId); 26 26 27 - const exists = subscriptions.find((s) => 28 - s.endpoint === subscription.endpoint || 29 - (s.keys.p256dh === subscription.keys.p256dh && 30 - s.keys.auth === subscription.keys.auth) 31 - ); 27 + const exists = subscriptions.find( 28 + (s) => 29 + s.endpoint === subscription.endpoint || 30 + (s.keys.p256dh === subscription.keys.p256dh && 31 + s.keys.auth === subscription.keys.auth), 32 + ); 32 33 33 - if (!exists) { 34 - subscriptions.push(subscription); 35 - await storage.set(getKey(userId), subscriptions); 36 - return subscription; 37 - } 34 + if (!exists) { 35 + subscriptions.push(subscription); 36 + await storage.set(getKey(userId), subscriptions); 37 + return subscription; 38 + } 38 39 39 - return createError({ 40 - status: 409, 41 - statusMessage: "Conflict", 42 - message: `Subscription already exists`, 43 - }); 44 - }, 45 - async sendNotification(userId: string) { 46 - const subscriptions = await Subscriptions.getAll(userId); 40 + return createError({ 41 + status: 409, 42 + statusMessage: "Conflict", 43 + message: `Subscription already exists`, 44 + }); 45 + }, 46 + async sendNotification(userId: string) { 47 + const subscriptions = await Subscriptions.getAll(userId); 47 48 48 - const payload = JSON.stringify({ title: "Erinnerung", body: "Zeit für XYZ" }); 49 - 50 - for (const sub of subscriptions) { 51 - try { 52 - await webpush.sendNotification(sub, payload); 53 - } catch (err) { 54 - console.error("Push failed", err); 55 - } 56 - } 49 + const payload = JSON.stringify({ 50 + title: "Erinnerung", 51 + body: "Zeit für XYZ", 52 + }); 57 53 58 - } 59 - } 54 + for (const sub of subscriptions) { 55 + try { 56 + await webpush.sendNotification(sub, payload); 57 + } catch (err) { 58 + console.error("Push failed", err); 59 + } 60 + } 61 + }, 62 + };
+5 -1
server/utils/sse.ts
··· 36 36 addEventStream(userId, subscriptionsEventStreams, eventStream); 37 37 }, 38 38 async sendUpdate(userId: string) { 39 - await publish(userId, subscriptionsEventStreams, await Subscriptions.getAll(userId)); 39 + await publish( 40 + userId, 41 + subscriptionsEventStreams, 42 + await Subscriptions.getAll(userId), 43 + ); 40 44 }, 41 45 }; 42 46
+6 -6
shared/types.ts
··· 44 44 export type Time = z.infer<typeof Time>; 45 45 46 46 export const PushSubscriptionJSON = z.object({ 47 - endpoint: z.string(), 48 - expirationTime: z.number().nullable().optional(), 49 - keys: z.object({ 50 - auth: z.string(), 51 - p256dh: z.string(), 52 - }) 47 + endpoint: z.string(), 48 + expirationTime: z.number().nullable().optional(), 49 + keys: z.object({ 50 + auth: z.string(), 51 + p256dh: z.string(), 52 + }), 53 53 }); 54 54 export type PushSubscriptionJSON = z.infer<typeof PushSubscriptionJSON>; 55 55