Release 202506020353
This commit is contained in:
77
web/src/lib/localstorage.ts
Normal file
77
web/src/lib/localstorage.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { z } from 'astro:schema'
|
||||
|
||||
import { typedObjectEntries } from './objects'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
||||
interface JSONObject {
|
||||
[k: string]: JSONValue
|
||||
}
|
||||
type JSONList = JSONValue[]
|
||||
type JSONPrimitive = boolean | number | string | null
|
||||
type JSONValue = Date | JSONList | JSONObject | JSONPrimitive
|
||||
|
||||
function makeTypedLocalStorage<
|
||||
Schemas extends Record<string, z.ZodType<JSONValue>>,
|
||||
T extends {
|
||||
[K in keyof Schemas]: {
|
||||
schema: Schemas[K]
|
||||
default?: z.output<Schemas[K]> | undefined
|
||||
key?: string
|
||||
}
|
||||
},
|
||||
>(options: T) {
|
||||
return Object.fromEntries(
|
||||
typedObjectEntries(options).map(([originalKey, option]) => {
|
||||
const key = option.key ?? originalKey
|
||||
|
||||
return [
|
||||
key,
|
||||
{
|
||||
get: () => {
|
||||
const stringValue = localStorage.getItem(key)
|
||||
if (!stringValue) return option.default
|
||||
|
||||
let jsonValue: z.output<typeof option.schema> | undefined = option.default
|
||||
try {
|
||||
jsonValue = JSON.parse(stringValue)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return option.default
|
||||
}
|
||||
|
||||
const parsedValue = option.schema.safeParse(jsonValue)
|
||||
if (!parsedValue.success) {
|
||||
console.error(parsedValue.error)
|
||||
return option.default
|
||||
}
|
||||
|
||||
return parsedValue.data
|
||||
},
|
||||
|
||||
set: (value: z.input<typeof option.schema>) => {
|
||||
localStorage.setItem(key, JSON.stringify(value))
|
||||
},
|
||||
|
||||
remove: () => {
|
||||
localStorage.removeItem(key)
|
||||
},
|
||||
|
||||
default: option.default,
|
||||
},
|
||||
]
|
||||
})
|
||||
) as {
|
||||
[K in keyof T]: {
|
||||
get: () => z.output<T[K]['schema']> | (T[K] extends { default: infer D } ? D : undefined)
|
||||
set: (value: z.input<T[K]['schema']>) => void
|
||||
remove: () => void
|
||||
default: z.output<T[K]['schema']> | undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const typedLocalStorage = makeTypedLocalStorage({
|
||||
pushNotificationsBannerDismissedAt: {
|
||||
schema: z.coerce.date(),
|
||||
},
|
||||
})
|
||||
@@ -162,3 +162,16 @@ export function areEqualObjectsWithoutOrder<T extends Record<string, unknown>>(
|
||||
return undefined
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as {@link Object.entries}, but with proper typing.
|
||||
* @example
|
||||
* typedObjectEntries({ a: 1, b: 2 }) // [['a', 1], ['b', 2]]
|
||||
*/
|
||||
export function typedObjectEntries<T extends Record<string, unknown>>(obj: T) {
|
||||
return Object.entries(obj) as Prettify<
|
||||
{
|
||||
[K in Extract<keyof T, string>]: [K, T[K]]
|
||||
}[Extract<keyof T, string>]
|
||||
>[]
|
||||
}
|
||||
|
||||
52
web/src/lib/webPush.ts
Normal file
52
web/src/lib/webPush.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
/* eslint-disable import/no-named-as-default-member */
|
||||
import { VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, VAPID_SUBJECT } from 'astro:env/server'
|
||||
import webpush, { WebPushError } from 'web-push'
|
||||
|
||||
// Configure VAPID keys
|
||||
webpush.setVapidDetails(VAPID_SUBJECT, VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY)
|
||||
|
||||
export { webpush }
|
||||
|
||||
export async function sendPushNotification(
|
||||
subscription: {
|
||||
endpoint: string
|
||||
keys: {
|
||||
p256dh: string
|
||||
auth: string
|
||||
}
|
||||
},
|
||||
data: {
|
||||
title: string
|
||||
body?: string
|
||||
icon?: string
|
||||
badge?: string
|
||||
url?: string
|
||||
}
|
||||
) {
|
||||
try {
|
||||
const result = await webpush.sendNotification(
|
||||
subscription,
|
||||
JSON.stringify({
|
||||
title: data.title,
|
||||
options: {
|
||||
body: data.body,
|
||||
icon: data.icon ?? '/favicon.svg',
|
||||
badge: data.badge ?? '/favicon.svg',
|
||||
data: {
|
||||
url: data.url,
|
||||
},
|
||||
},
|
||||
}),
|
||||
{
|
||||
TTL: 24 * 60 * 60, // 24 hours
|
||||
}
|
||||
)
|
||||
return { success: true, result } as const
|
||||
} catch (error) {
|
||||
console.error('Error sending push notification:', error)
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof WebPushError ? error : undefined,
|
||||
} as const
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,11 @@ const stringToArrayFactory = (delimiter: RegExp | string = ',') => {
|
||||
.filter((item) => item !== '')
|
||||
}
|
||||
|
||||
export const stringListOfSlugsSchemaRequired = z.preprocess(
|
||||
stringToArrayFactory(/[\s,\n]+/),
|
||||
z.array(z.string().regex(/^[a-z0-9-_A-Z]+$/)).min(1)
|
||||
)
|
||||
|
||||
export const stringListOfUrlsSchema = z.preprocess(
|
||||
stringToArrayFactory(/[\s,\n]+/),
|
||||
z.array(zodUrlOptionalProtocol).default([])
|
||||
|
||||
Reference in New Issue
Block a user