Files
RenewCMS/web/src/lib/storedDraft.ts
T

131 lines
3.1 KiB
TypeScript

import type { Byline } from '@/lib/articleTypes'
import { SEO_KEYS, type ArticleSeo, type SeoKey } from '@/lib/articleSeo'
export interface StoredDraft {
title?: string
body?: string
meta?: { site_id: number | null; authors: Byline[] }
seo?: ArticleSeo
mirrored_at: string
}
const DRAFT_PREFIX = 'renewcms:article-draft:'
const NULLABLE_NUMBER_KEYS: SeoKey[] = ['cover_image_id', 'social_image_id']
const BOOLEAN_KEYS: SeoKey[] = ['noindex']
type UnknownRecord = Record<string, unknown>
function isRecord(value: unknown): value is UnknownRecord {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
function isOptionalString(value: unknown) {
return value === undefined || typeof value === 'string'
}
function isNullableNumber(value: unknown) {
return value === null || typeof value === 'number'
}
function isByline(value: unknown) {
return (
isRecord(value) &&
typeof value.id === 'number' &&
typeof value.name === 'string' &&
typeof value.slug === 'string'
)
}
function isMeta(value: unknown) {
return (
value === undefined ||
(isRecord(value) &&
isNullableNumber(value.site_id) &&
Array.isArray(value.authors) &&
value.authors.every(isByline))
)
}
function isSeoValue(key: SeoKey, value: unknown) {
if (NULLABLE_NUMBER_KEYS.includes(key)) {
return isNullableNumber(value)
}
return BOOLEAN_KEYS.includes(key) ? typeof value === 'boolean' : typeof value === 'string'
}
function isSeo(value: unknown) {
return (
value === undefined ||
(isRecord(value) &&
SEO_KEYS.every((key) => value[key] === undefined || isSeoValue(key, value[key])))
)
}
function isMirrorTime(value: unknown) {
return typeof value === 'string' && !Number.isNaN(new Date(value).getTime())
}
function isStoredDraft(value: unknown): value is StoredDraft {
return (
isRecord(value) &&
isOptionalString(value.title) &&
isOptionalString(value.body) &&
isMeta(value.meta) &&
isSeo(value.seo) &&
isMirrorTime(value.mirrored_at)
)
}
function keyOf(userId: number, articleId: number) {
return `${DRAFT_PREFIX}${userId}:${articleId}`
}
export function readStoredDraft(userId: number | null, articleId: number): StoredDraft | null {
if (userId === null) {
return null
}
try {
const raw = sessionStorage.getItem(keyOf(userId, articleId))
const parsed: unknown = raw === null ? null : JSON.parse(raw)
return isStoredDraft(parsed) ? parsed : null
} catch {
return null
}
}
export function writeStoredDraft(userId: number | null, articleId: number, draft: StoredDraft) {
if (userId === null) {
return
}
try {
sessionStorage.setItem(keyOf(userId, articleId), JSON.stringify(draft))
} catch {
return
}
}
export function removeStoredDraft(userId: number | null, articleId: number) {
if (userId === null) {
return
}
try {
sessionStorage.removeItem(keyOf(userId, articleId))
} catch {
return
}
}
export function removeStoredDraftsOf(userId: number | undefined) {
if (userId === undefined) {
return
}
try {
const prefix = `${DRAFT_PREFIX}${userId}:`
const owned = Object.keys(sessionStorage).filter((key) => key.startsWith(prefix))
owned.forEach((key) => sessionStorage.removeItem(key))
} catch {
return
}
}