131 lines
3.1 KiB
TypeScript
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
|
|
}
|
|
}
|