feat: entry editor at /content/:key/new and /content/:key/:id

This commit is contained in:
Florian Sylvain committed 2026-10-21 00:12:34 +02:00
1 parent 91d916fb74
commit fba20f83c5
94 files changed
+7116 -423

No files matched your search

+155
View File
@@ -0,0 +1,155 @@
import { t } from '@/i18n'
import type { BuiltinField } from '@/lib/contentTypes'
import { formatDayValue } from '@/lib/entryValue'
export type EntryRule = 'required' | 'type' | 'min' | 'max' | 'options' | 'unique'
function bound(field: BuiltinField, side: 'min' | 'max') {
return field[side] ?? ''
}
function choices(field: BuiltinField) {
const options = (field.options ?? []).map(String)
if (options.length < 2) {
return options.join('')
}
return t('{0} or {1}', [options.slice(0, -1).join(', '), options[options.length - 1]!])
}
function requiredMessage(field: BuiltinField) {
switch (field.type) {
case 'date':
return t('Pick a date for {0}.', [field.label])
case 'select':
return t('Choose an option for {0}.', [field.label])
case 'boolean':
return t('Choose yes or no for {0}.', [field.label])
default:
return t('Fill in {0}.', [field.label])
}
}
function typeMessage(field: BuiltinField) {
switch (field.type) {
case 'number':
return t('{0} must be a number.', [field.label])
case 'boolean':
return t('{0} must be yes or no.', [field.label])
case 'date':
return t('{0} must be a date, like 2026-11-21.', [field.label])
case 'select':
return t('{0} must be one of the choices.', [field.label])
default:
return t('{0} must be text.', [field.label])
}
}
function minMessage(field: BuiltinField) {
const limit = bound(field, 'min')
if (field.type === 'number') {
return t('{0} must be at least {1}.', [field.label, String(limit)])
}
if (field.type === 'date') {
return t("{0} can't be before {1}.", [field.label, formatDayValue(String(limit))])
}
return t('{0} needs at least {1} characters.', [field.label, String(limit)])
}
function maxMessage(field: BuiltinField) {
const limit = bound(field, 'max')
if (field.type === 'number') {
return t('{0} must be at most {1}.', [field.label, String(limit)])
}
if (field.type === 'date') {
return t("{0} can't be after {1}.", [field.label, formatDayValue(String(limit))])
}
return t('{0} can be up to {1} characters.', [field.label, String(limit)])
}
function uniqueMessage(field: BuiltinField) {
switch (field.type) {
case 'select':
return t('Another entry already uses this choice for {0}.', [field.label])
case 'date':
return t('Another entry already uses this date for {0}.', [field.label])
default:
return t('Another entry already uses this {0}. Enter a different one.', [field.label])
}
}
export function ruleMessage(rule: EntryRule, field: BuiltinField) {
switch (rule) {
case 'required':
return requiredMessage(field)
case 'type':
return typeMessage(field)
case 'min':
return minMessage(field)
case 'max':
return maxMessage(field)
case 'options':
return t('Choose one of: {0}.', [choices(field)])
default:
return uniqueMessage(field)
}
}
export function uniqueAlert(field: BuiltinField) {
switch (field.type) {
case 'select':
return t('Another entry already uses this choice. Change it and save again.')
case 'date':
return t('Another entry already uses this date. Change it and save again.')
default:
return t('Another entry already uses this {0}. Change it and save again.', [
field.label.toLowerCase(),
])
}
}
export function uniqueHelp(field: BuiltinField) {
return t('Each entry needs a different {0}.', [field.label.toLowerCase()])
}
export function rangeHelp(field: BuiltinField) {
const min = field.min
const max = field.max
if (field.type === 'date') {
const from = min === null || min === undefined ? '' : formatDayValue(String(min))
const until = max === null || max === undefined ? '' : formatDayValue(String(max))
if (from && until) {
return t('From {0} until {1}.', [from, until])
}
if (from) {
return t('From {0}.', [from])
}
return until ? t('Until {0}.', [until]) : ''
}
if (typeof min === 'number' && typeof max === 'number') {
return t('Between {0} and {1}.', [String(min), String(max)])
}
if (typeof min === 'number') {
return t('At least {0}.', [String(min)])
}
return typeof max === 'number' ? t('At most {0}.', [String(max)]) : ''
}
export function schemaChangedLine(key: string) {
const name = key.replace(/_/g, ' ')
return t(
"A field was removed and a new required field, {0}, was added after you opened this form. Reload the form to continue. What you typed here can't be saved against the old fields.",
[name.charAt(0).toUpperCase() + name.slice(1)],
)
}
export function entryLoadErrorLine(path: string) {
return t('GET {0} failed. Check your connection and try again.', [path])
}
export function typeLoadErrorLine(key: string) {
return t('GET /v1/content-type/{0} failed. Check your connection and try again.', [key])
}
export function entriesLoadErrorLine(key: string) {
return t('GET /v1/content-type/{0}/entry failed. Check your connection and try again.', [key])
}