Files
veilix.app/src/payload/collections/Sites.ts
T
andreypr207andClaude Opus 5.5 7e331c2c9d Этап 1a: фундамент
- Next 16.4 и Payload 3.91 в одном приложении, PostgreSQL с UUID,
  TypeScript 6, ESLint 9, Prettier, Vitest
- Payload: администраторы с ролями, multi-tenant по сайтам, локализация
  en/ru/de/fr/es, интерфейс админки ru/en, языки сайта (sites.locales)
- Коллекции sites, domains, currencies, countries, locations, tags,
  tariffs, payment-systems (manual), media и начальные данные (pnpm seed)
- Proxy: хост → домен, язык и валюта, внутренние маршруты по хосту;
  /admin и /api закрыты на доменах брендов
- Токены и темы брендов, оболочки сайта и кабинета, переключатели языка,
  валюты и темы, заглушки страниц
- /svc/health, юнит systemd veilix-dev.service
- Документация: решения D-022…D-024, команды в CLAUDE.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 05:08:05 +02:00

124 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type {
CollectionConfig,
SelectFieldManyValidation,
TextFieldSingleValidation,
} from 'payload'
import { select } from 'payload/shared'
import { DEFAULT_LOCALE, LOCALES } from '@/i18n/locales'
import { anyRole, managers, owners } from '../access/roles'
import { localeOptions } from '../fields/locales'
import { groups, label, message } from '../i18n'
/**
* Языки сайта — языки его контента. Домен показывает любую их часть.
* Английский обязателен: на него откатывается непереведённый контент.
*/
const validateLocales: SelectFieldManyValidation = async (value, args) => {
const base = select(value, args)
if (base !== true) return base
const locales = value ?? []
if (!locales.includes(DEFAULT_LOCALE)) {
return message(args.req, {
en: 'English is required: untranslated content falls back to it',
ru: 'Английский обязателен: на него откатывается непереведённый контент',
})
}
if (args.id) {
const { docs } = await args.req.payload.find({
collection: 'domains',
where: { site: { equals: args.id } },
depth: 0,
pagination: false,
overrideAccess: true,
req: args.req,
})
const used = new Set(docs.flatMap((domain) => domain.locales ?? []))
const missing = [...used].filter((locale) => !locales.includes(locale))
if (missing.length > 0) {
return message(args.req, {
en: `These languages are enabled on the site's domains, remove them there first: ${missing.join(', ')}`,
ru: `Эти языки включены на доменах сайта, сначала уберите их там: ${missing.join(', ')}`,
})
}
}
return true
}
const validateOrderPrefix: TextFieldSingleValidation = (value, { req }) =>
typeof value === 'string' && /^[A-Z]{2,4}$/.test(value)
? true
: message(req, {
en: '2–4 capital Latin letters, e.g. VX',
ru: '2–4 заглавные латинские буквы, например VX',
})
export const Sites: CollectionConfig = {
slug: 'sites',
labels: {
singular: label('Site', 'Сайт'),
plural: label('Sites', 'Сайты'),
},
admin: {
group: groups.settings,
useAsTitle: 'name',
defaultColumns: ['name', 'code', 'orderPrefix', 'locales'],
},
access: {
read: anyRole,
create: managers,
update: managers,
// вместе с сайтом плагин удаляет все его документы
delete: owners,
},
fields: [
{
name: 'code',
type: 'select',
required: true,
unique: true,
options: [
{ value: 'dedicated', label: label('Veilix (dedicated)', 'Вейликс (dedicated)') },
{ value: 'mass', label: label('VeilixVPN (mass)', 'ВейликсВПН (mass)') },
],
label: label('Brand', 'Бренд'),
// по коду выбираются тема и страницы бренда, после создания не меняется
access: { update: () => false },
},
{
name: 'name',
type: 'text',
required: true,
localized: true,
label: label('Name', 'Название'),
},
{
name: 'orderPrefix',
type: 'text',
required: true,
unique: true,
label: label('Order number prefix', 'Префикс номеров'),
validate: validateOrderPrefix,
},
{
name: 'locales',
type: 'select',
hasMany: true,
required: true,
options: localeOptions,
defaultValue: [...LOCALES],
label: label('Content languages', 'Языки контента'),
validate: validateLocales,
admin: {
description: label(
'Each domain of the site shows some of these languages.',
'Каждый домен сайта показывает часть этих языков.',
),
},
},
],
}