feat: editor language menu, title field, header actions, publish dialogs, read-only rendering and draft recovery
This commit is contained in:
1 parent
a671b81f80
commit
339e646eb8
85 files changed
+6369
-624
No files matched your search
@@ -0,0 +1,130 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user