import { list } from "@keystone-6/core"; import { allowAll } from "@keystone-6/core/access"; import { text, password, timestamp, image, relationship, select, integer, checkbox } from "@keystone-6/core/fields"; import { document } from '@keystone-6/fields-document'; import { type Lists } from ".keystone/types"; export const lists: Lists = { // πŸ‘€ ΠŸΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ User: list({ access: allowAll, fields: { name: text({ validation: { isRequired: true } }), email: text({ validation: { isRequired: true }, isIndexed: "unique" }), password: password({ validation: { isRequired: true } }), createdAt: timestamp({ defaultValue: { kind: "now" } }), }, }), System: list({ access: allowAll, ui: { label: 'System settings', description: 'Π“Π»ΠΎΠ±Π°Π»ΡŒΠ½Ρ‹Π΅ настройки сайта', listView: { initialColumns: ['siteTitle', 'updatedAt'] }, }, fields: { siteTitle: text({ validation: { isRequired: true }, ui: { description: 'Title сайта (metatitle / ΠΎΡ‚ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ Π² шапкС ΠΈ Ρ‚.ΠΏ.)' }, }), linedUrl: text({ validation: { isRequired: true }, ui: { description: 'LinedIn url' }, }), headerLogo: image({ storage: 'local_images', ui: { description: 'Π›ΠΎΠ³ΠΎΡ‚ΠΈΠΏ для Ρ…Π΅Π΄Π΅Ρ€Π° (свСтлый/Ρ‚Π΅ΠΌΠ½Ρ‹ΠΉ Π²Π°Ρ€ΠΈΠ°Π½Ρ‚ ΠΏΠΎ ΠΌΠ°ΠΊΠ΅Ρ‚Ρƒ)' }, }), footerLogo: image({ storage: 'local_images', ui: { description: 'Π›ΠΎΠ³ΠΎΡ‚ΠΈΠΏ для Ρ„ΡƒΡ‚Π΅Ρ€Π°' }, }), footerDisclaimer: document({ ui: { description: 'ДисклСймСр Π² Ρ„ΡƒΡ‚Π΅Ρ€Π΅ (rich-text)' }, formatting: { headingLevels: [1, 2, 3, 4], inlineMarks: { bold: true, italic: true, underline: true, strikethrough: true, code: true, superscript: true, subscript: true, }, listTypes: { ordered: true, unordered: true }, alignment: { center: true, end: true }, blockTypes: { blockquote: true, code: true }, softBreaks: true, }, links: true, dividers: true, }), updatedAt: timestamp({ defaultValue: { kind: 'now' }, ui: { itemView: { fieldMode: 'read' } }, }), }, hooks: { async beforeOperation({ operation, context, listKey }) { // Π Π°Π·Ρ€Π΅ΡˆΠ°Π΅ΠΌ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ ΠžΠ”ΠΠ£ запись if (operation === 'create') { const count = await context.db[listKey].count({}); if (count > 0) throw new Error('МоТно ΡΠΎΠ·Π΄Π°Ρ‚ΡŒ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ ΠΎΠ΄Π½Ρƒ запись System'); } // Π—Π°ΠΏΡ€Π΅Ρ‚ удалСния (Ρ‡Ρ‚ΠΎΠ±Ρ‹ случайно Π½Π΅ ΠΏΠΎΡ‚Π΅Ρ€ΡΡ‚ΡŒ Π³Π»ΠΎΠ±Π°Π»ΡŒΠ½Ρ‹Π΅ настройки) if (operation === 'delete') { throw new Error('НСльзя ΡƒΠ΄Π°Π»ΠΈΡ‚ΡŒ System; ΠΈΠ·ΠΌΠ΅Π½ΠΈΡ‚Π΅ ΡΡƒΡ‰Π΅ΡΡ‚Π²ΡƒΡŽΡ‰ΡƒΡŽ запись.'); } }, async resolveInput({ operation, resolvedData }) { if (operation === 'update') { resolvedData.updatedAt = new Date().toISOString(); } return resolvedData; }, }, }), NavItem: list({ access: allowAll, ui: { label: 'Navigation', labelField: 'label', listView: { initialColumns: ['label', 'href', 'order', 'parent'] }, }, fields: { label: text({ validation: { isRequired: true } }), href: text({ validation: { isRequired: true }, ui: { description: 'НапримСр: /about, #contact, https://example.com' }, }), order: integer({ defaultValue: 0 }), isExternal: checkbox({ defaultValue: false, ui: { description: 'ΠžΡ‚ΠΊΡ€Ρ‹Π²Π°Ρ‚ΡŒ Π² Π½ΠΎΠ²ΠΎΠΉ Π²ΠΊΠ»Π°Π΄ΠΊΠ΅ (target=_blank)' }, }), enabled: checkbox({ defaultValue: true }), // дрСвовидная структура (подмСню) parent: relationship({ ref: 'NavItem.children' }), children: relationship({ ref: 'NavItem.parent', many: true, ui: { displayMode: 'cards', cardFields: ['label', 'href', 'order', 'isExternal', 'enabled'], inlineCreate: { fields: ['label', 'href', 'order', 'isExternal', 'enabled'] }, inlineEdit: { fields: ['label', 'href', 'order', 'isExternal', 'enabled'] }, }, }), }, }), // 🟩 Hero (ΠΊΠ°ΠΊ Π±Ρ‹Π»ΠΎ) HeroSection: list({ access: allowAll, ui: { label: "Hero Section", listView: { initialColumns: ["title"] } }, fields: { title: text({ validation: { isRequired: true }, ui: { description: "Π“Π»Π°Π²Π½Ρ‹ΠΉ Π·Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ сСкции" } }), description: text({ ui: { displayMode: "textarea", description: "Подзаголовок / описаниС" } }), backgroundImage: image({ storage: "local_images", ui: { description: "Ѐоновая ΠΊΠ°Ρ€Ρ‚ΠΈΠ½ΠΊΠ° (1360x640)" } }), buttonText: text({ ui: { description: "ВСкст ΠΊΠ½ΠΎΠΏΠΊΠΈ" } }), buttonLink: text({ ui: { description: "Бсылка ΠΊΠ½ΠΎΠΏΠΊΠΈ (Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€: /contact)" } }), }, hooks: { async beforeOperation({ operation, listKey, context }) { if (operation === "create") { const existing = await context.db[listKey].findMany({ take: 1 }); if (existing.length > 0) throw new Error("МоТно ΡΠΎΠ·Π΄Π°Ρ‚ΡŒ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ ΠΎΠ΄ΠΈΠ½ HeroSection"); } }, }, }), // 🟨 Π£Π½ΠΈΠ²Π΅Ρ€ΡΠ°Π»ΡŒΠ½Π°Ρ ΠΏΠ»ΠΈΡ‚ΠΊΠ° сСкции (ΠΈ «статистика», ΠΈ «финансы») FinanceRow: list({ access: allowAll, ui: { label: "Finance Row", listView: { initialColumns: ["order", "value", "label", "trend"] }, }, fields: { value: text({ validation: { isRequired: true } }), // Π½Π°ΠΏΡ€. "$2,05 bln" ΠΈΠ»ΠΈ "5 mln" label: text({ validation: { isRequired: true } }), // подпись ΠΏΠΎΠ΄ Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅ΠΌ trend: select({ type: "enum", options: [ { label: "НСт стрСлки", value: "none" }, { label: "Π’Π²Π΅Ρ€Ρ…", value: "up" }, { label: "Π’Π½ΠΈΠ·", value: "down" }, ], defaultValue: "none", ui: { displayMode: "segmented-control" }, }), order: integer({ defaultValue: 0 }), // для сортировки ΠΈ Β«Π±ΠΈΡ‚ΡŒ ΠΏΠΎ 3Β» section: relationship({ ref: "ScreenSecondSection.rows" }), }, }), // 🟦 Вторая сСкция (About/Overview) β€” singleton ScreenSecondSection: list({ access: allowAll, ui: { label: "Screen Second", listView: { initialColumns: ["title"] } }, fields: { title: text({ validation: { isRequired: true }, ui: { description: "Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ сСкции (About Freedom Holding Corp)" }, }), description: text({ ui: { displayMode: "textarea", description: "ОписаниС ΠΏΠΎΠ΄ Π·Π°Π³ΠΎΠ»ΠΎΠ²ΠΊΠΎΠΌ" }, }), logo: image({ storage: "local_images", ui: { description: "Π›ΠΎΠ³ΠΎΡ‚ΠΈΠΏ справа" } }), rows: relationship({ ref: "FinanceRow.section", many: true, ui: { displayMode: "cards", cardFields: ["value", "label", "trend", "order"], inlineCreate: { fields: ["value", "label", "trend", "order"] }, inlineEdit: { fields: ["value", "label", "trend", "order"] }, linkToItem: true, }, }), footnote: text({ ui: { description: "Бноска (Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€: * for the year ended ...)" } }), }, hooks: { async beforeOperation({ operation, listKey, context }) { if (operation === "create") { const existing = await context.db[listKey].findMany({ take: 1 }); if (existing.length > 0) throw new Error("МоТно ΡΠΎΠ·Π΄Π°Ρ‚ΡŒ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ ΠΎΠ΄ΠΈΠ½ ScreenSecondSection"); } }, }, }), ScreenThreeStatCard: list({ access: allowAll, ui: { label: 'Screen3 β€’ Stat Card', listView: { initialColumns: ['order', 'value', 'label', 'color'] }, }, fields: { value: text({ validation: { isRequired: true } }), // Π½Π°ΠΏΡ€. "USD ~ 10.8 bn*" label: text({ validation: { isRequired: true } }), // "Current market capitalization" color: text({ ui: { description: 'HEX Ρ†Π²Π΅Ρ‚ значСния (Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€ #16a34a). МоТно ΠΎΡΡ‚Π°Π²ΠΈΡ‚ΡŒ пустым.' }, }), order: integer({ defaultValue: 0 }), section: relationship({ ref: 'ScreenThreeSection.statCards' }), }, }), /* 🟨 Π­Π»Π΅ΠΌΠ΅Π½Ρ‚ списка индСксов (bullet) */ ScreenThreeIndex: list({ access: allowAll, ui: { label: 'Screen3 β€’ Index Item', listView: { initialColumns: ['order', 'text'] }, }, fields: { text: text({ validation: { isRequired: true } }), // Π½Π°ΠΏΡ€. "MSCI U.S. Small Cap 1750" order: integer({ defaultValue: 0 }), section: relationship({ ref: 'ScreenThreeSection.indexes' }), }, }), /* 🟦 БСкция Financial performance (singleton) */ ScreenThreeSection: list({ access: allowAll, ui: { label: 'Screen Three (Financial performance)', listView: { initialColumns: ['title'] }, }, fields: { title: text({ validation: { isRequired: true }, ui: { description: 'Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ сСкции (Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€: Financial performance)' }, }), description: text({ ui: { displayMode: 'textarea', description: 'ОписаниС ΠΏΠΎΠ΄ Π·Π°Π³ΠΎΠ»ΠΎΠ²ΠΊΠΎΠΌ' }, }), // ΠšΠ°Ρ€Ρ‚ΠΎΡ‡ΠΊΠΈ справа (Π΄Π²Π΅ Ρƒ тСбя Π² ΠΌΠ°ΠΊΠ΅Ρ‚Π΅, Π½ΠΎ ΠΌΠΎΠΆΠ½ΠΎ большС/мСньшС) statCards: relationship({ ref: 'ScreenThreeStatCard.section', many: true, ui: { displayMode: 'cards', cardFields: ['value', 'label', 'color', 'order'], inlineCreate: { fields: ['value', 'label', 'color', 'order'] }, inlineEdit: { fields: ['value', 'label', 'color', 'order'] }, linkToItem: true, }, }), // Бписок индСксов (Π±ΡƒΠ»Π»ΠΈΡ‚Ρ‹) indexes: relationship({ ref: 'ScreenThreeIndex.section', many: true, ui: { displayMode: 'cards', cardFields: ['text', 'order'], inlineCreate: { fields: ['text', 'order'] }, inlineEdit: { fields: ['text', 'order'] }, linkToItem: true, }, }), footnote: text({ ui: { description: 'Бноска ΠΏΠΎΠ΄ сСкциСй (Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€: * Based on the price ...)' }, }), }, hooks: { // singleton: Ρ€Π°Π·Ρ€Π΅ΡˆΠ°Π΅ΠΌ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ ΠΎΠ΄Π½Ρƒ запись async beforeOperation({ operation, listKey, context }) { if (operation === 'create') { const existing = await context.db[listKey].findMany({ take: 1 }); if (existing.length > 0) { throw new Error('МоТно ΡΠΎΠ·Π΄Π°Ρ‚ΡŒ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ ΠΎΠ΄Π½Ρƒ ScreenThreeSection'); } } }, }, }), KeyBusinessSegmentsSection: list({ access: allowAll, ui: { label: "Key Business Segments (Section)", listView: { initialColumns: ["title"] } }, fields: { title: text({ validation: { isRequired: true } }), segments: relationship({ ref: "KeySegment.section", many: true, ui: { displayMode: "cards", cardFields: ["name", "key", "order", "logo"], inlineCreate: { fields: ["name", "key", "logo", "order"] }, inlineEdit: { fields: ["name", "key", "logo", "order"] }, linkToItem: true, }, }), }, hooks: { async beforeOperation({ operation, listKey, context }) { if (operation === "create") { const exists = await context.db[listKey].findMany({ take: 1 }); if (exists.length) throw new Error("МоТно ΡΠΎΠ·Π΄Π°Ρ‚ΡŒ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ ΠΎΠ΄ΠΈΠ½ KeyBusinessSegmentsSection"); } }, }, }), /* Segment */ KeySegment: list({ access: allowAll, ui: { label: "Key Segment", listView: { initialColumns: ["order", "name", "key"] } }, fields: { key: text({ isIndexed: 'unique', ui: { description: "Код: broker/bank/telecom…" } }), name: text({ validation: { isRequired: true } }), order: integer({ defaultValue: 0 }), logo: image({ storage: 'local_images' }), section: relationship({ ref: "KeyBusinessSegmentsSection.segments" }), info: relationship({ ref: "KeySegmentInfo.segment", many: true, ui: { displayMode: "cards", cardFields: ["title", "order"], inlineCreate: { fields: ["title", "order", "brandLink", "brandLogo"] }, inlineEdit: { fields: ["title", "order", "brandLink", "brandLogo"] }, linkToItem: true, }, }), }, }), /* Info card (column block) */ KeySegmentInfo: list({ access: allowAll, ui: { label: "Key Segment β€’ Info Block", listView: { initialColumns: ["order", "title"] } }, fields: { title: text({ validation: { isRequired: true } }), order: integer({ defaultValue: 0 }), segment: relationship({ ref: "KeySegment.info" }), // Π±Ρ€Π΅Π½Π΄ ΠΏΡ€ΠΈΠ½Π°Π΄Π»Π΅ΠΆΠΈΡ‚ Ρ†Π΅Π»ΠΎΠΌΡƒ Π±Π»ΠΎΠΊΡƒ brandLink: text({ ui: { description: "Бсылка Π±Ρ€Π΅Π½Π΄Π° (ΠΎΠΏΡ†.)" } }), brandLogo: image({ storage: 'local_images', ui: { description: "Π›ΠΎΠ³ΠΎ Π±Ρ€Π΅Π½Π΄Π° (ΠΎΠΏΡ†.)" } }), items: relationship({ ref: "KeySegmentInfoItem.info", many: true, ui: { displayMode: "cards", cardFields: ["text", "order", "infoLogo", "colSpan"], inlineCreate: { fields: ["text", "infoLogo", "colSpan", "order"] }, inlineEdit: { fields: ["text", "infoLogo", "colSpan", "order"] }, linkToItem: true, }, }), }, }), /* Info list item (Ρ‚ΠΎΠ»ΡŒΠΊΠΎ тСкст + Π»ΠΎΠ³ΠΎ) */ KeySegmentInfoItem: list({ access: allowAll, ui: { label: "Key Segment β€’ Info Item", listView: { initialColumns: ["order", "text", "infoLogo", "colSpan"] } }, fields: { text: text({ validation: { isRequired: true } }), infoLogo: image({ storage: 'local_images', ui: { description: "Π›ΠΎΠ³ΠΎ Π² Π·Π΅Π»Ρ‘Π½ΠΎΠΌ ΠΊΠ²Π°Π΄Ρ€Π°Ρ‚ΠΈΠΊΠ΅" } }), colSpan: select({ type: 'enum', options: [ { label: 'Normal (1 column)', value: 'ONE' }, { label: 'Wide (2 columns)', value: 'TWO' }, ], defaultValue: 'ONE', ui: { displayMode: 'segmented-control' }, }), order: integer({ defaultValue: 0 }), info: relationship({ ref: "KeySegmentInfo.items" }), }, }), WhyChooseUsItem: list({ access: allowAll, ui: { label: 'Why Choose Us – Item', listView: { initialColumns: ['text', 'order', 'section'] }, }, fields: { text: text({ validation: { isRequired: true }, ui: { displayMode: 'textarea' }, }), order: integer({ defaultValue: 0, ui: { description: 'Π‘ΠΎΡ€Ρ‚ΠΈΡ€ΠΎΠ²ΠΊΠ° ΠΏΠΎ Π²ΠΎΠ·Ρ€Π°ΡΡ‚Π°Π½ΠΈΡŽ' }, }), section: relationship({ ref: 'WhyChooseUsSection.items' }), createdAt: timestamp({ defaultValue: { kind: 'now' } }), }, }), WhyChooseUsSection: list({ access: allowAll, ui: { label: 'Why Choose Us (section)', listView: { initialColumns: ['title'] }, }, fields: { // Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ сСкции ("Why Choose Us") title: text({ defaultValue: 'Why Choose Us' }), // ЛСвая ΠΊΠ°Ρ€Ρ‚ΠΎΡ‡ΠΊΠ° leftTitle: text({ validation: { isRequired: true }, ui: { description: 'ВСкст Π² Π»Π΅Π²ΠΎΠΉ ΠΊΠ°Ρ€Ρ‚ΠΎΡ‡ΠΊΠ΅' }, }), leftBackground: image({ storage: 'local_images', ui: { description: 'Π€ΠΎΠ½ для Π»Π΅Π²ΠΎΠΉ ΠΊΠ°Ρ€Ρ‚ΠΎΡ‡ΠΊΠΈ (Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€ Backgroung_2.png)' }, }), ctaText: text({ defaultValue: 'Partner with us' }), ctaLink: text({ ui: { description: 'Бсылка для ΠΊΠ½ΠΎΠΏΠΊΠΈ (ΠΎΠΏΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½ΠΎ)' } }), // ΠŸΡ€Π°Π²Π°Ρ ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ°: ΠΏΡƒΠ½ΠΊΡ‚Ρ‹ (ΠΌΠ½ΠΎΠ³ΠΎ) items: relationship({ ref: 'WhyChooseUsItem.section', many: true, ui: { displayMode: 'cards', cardFields: ['text', 'order'], inlineCreate: { fields: ['text', 'order'] }, inlineEdit: { fields: ['text', 'order'] }, linkToItem: true, }, }), } }), InnovationSection: list({ access: allowAll, ui: { label: 'Innovation (section)', listView: { initialColumns: ['title', 'updatedAt'], }, }, fields: { // Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ сСкции title: text({ defaultValue: 'Innovation at the intersection of telecom and fintech', ui: { description: 'Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ Π½Π°Π΄ сСткой ΠΊΠ°Ρ€Ρ‚ΠΎΡ‡Π΅ΠΊ' }, }), // Π’Π•Π Π₯: лСвая ΠΊΠ°Ρ€Ρ‚ΠΎΡ‡ΠΊΠ° (тСкст + ΠΊΠ°Ρ€Ρ‚ΠΈΠ½ΠΊΠ° справа) topLeftTitle: text({ validation: { isRequired: true }, ui: { description: 'Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ Π»Π΅Π²ΠΎΠΉ Π²Π΅Ρ€Ρ…Π½Π΅ΠΉ ΠΊΠ°Ρ€Ρ‚ΠΎΡ‡ΠΊΠΈ' }, }), topLeftText: text({ ui: { displayMode: 'textarea', description: 'ВСкст Π»Π΅Π²ΠΎΠΉ Π²Π΅Ρ€Ρ…Π½Π΅ΠΉ ΠΊΠ°Ρ€Ρ‚ΠΎΡ‡ΠΊΠΈ' }, }), topLeftImage: image({ storage: 'local_images', ui: { description: 'ΠšΠ°Ρ€Ρ‚ΠΈΠ½ΠΊΠ° Π² ΠΏΡ€Π°Π²ΠΎΠΉ части Π»Π΅Π²ΠΎΠΉ Π²Π΅Ρ€Ρ…Π½Π΅ΠΉ ΠΊΠ°Ρ€Ρ‚ΠΎΡ‡ΠΊΠΈ' }, }), // Π’Π•Π Π₯: правая зСлёная ΠΊΠ°Ρ€Ρ‚ΠΎΡ‡ΠΊΠ° topRightTitle: text({ validation: { isRequired: true }, ui: { description: 'Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ ΠΏΡ€Π°Π²ΠΎΠΉ Π²Π΅Ρ€Ρ…Π½Π΅ΠΉ Π·Π΅Π»Ρ‘Π½ΠΎΠΉ ΠΊΠ°Ρ€Ρ‚ΠΎΡ‡ΠΊΠΈ' }, }), topRightText: text({ ui: { displayMode: 'textarea', description: 'ВСкст ΠΏΡ€Π°Π²ΠΎΠΉ Π²Π΅Ρ€Ρ…Π½Π΅ΠΉ ΠΊΠ°Ρ€Ρ‚ΠΎΡ‡ΠΊΠΈ' }, }), // ΠΠ˜Π—: Π΄Π²Π΅ ΠΊΠ°Ρ€Ρ‚ΠΎΡ‡ΠΊΠΈ bottom1Title: text({ ui: { description: 'Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ Π»Π΅Π²ΠΎΠΉ Π½ΠΈΠΆΠ½Π΅ΠΉ ΠΊΠ°Ρ€Ρ‚ΠΎΡ‡ΠΊΠΈ' } }), bottom1Text: text({ ui: { displayMode: 'textarea', description: 'ВСкст Π»Π΅Π²ΠΎΠΉ Π½ΠΈΠΆΠ½Π΅ΠΉ ΠΊΠ°Ρ€Ρ‚ΠΎΡ‡ΠΊΠΈ' }, }), bottom2Title: text({ ui: { description: 'Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ ΠΏΡ€Π°Π²ΠΎΠΉ Π½ΠΈΠΆΠ½Π΅ΠΉ ΠΊΠ°Ρ€Ρ‚ΠΎΡ‡ΠΊΠΈ' } }), bottom2Text: text({ ui: { displayMode: 'textarea', description: 'ВСкст ΠΏΡ€Π°Π²ΠΎΠΉ Π½ΠΈΠΆΠ½Π΅ΠΉ ΠΊΠ°Ρ€Ρ‚ΠΎΡ‡ΠΊΠΈ' }, }), // Ρ‚Π΅Ρ….поля createdAt: timestamp({ defaultValue: { kind: 'now' } }), updatedAt: timestamp({ db: { updatedAt: true }, }), }, // Singleton: Ρ€Π°Π·Ρ€Π΅ΡˆΠ°Π΅ΠΌ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ ΠΎΠ΄Π½Ρƒ запись hooks: { async beforeOperation({ operation, listKey, context }) { if (operation === 'create') { const existing = await context.db[listKey].findMany({ where: {} }); if (existing.length > 0) { throw new Error('МоТно ΡΠΎΠ·Π΄Π°Ρ‚ΡŒ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ ΠΎΠ΄ΠΈΠ½ InnovationSection'); } } }, }, }), SolutionsSection : list({ access: allowAll, ui: { label: 'Solutions (Section)', listView: { initialColumns: ['title'] }, }, fields: { title: text({ ui: { description: 'Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ сСкции (Π½Π° Ρ„Ρ€ΠΎΠ½Ρ‚Π΅ "Solutions")' }, defaultValue: 'Solutions', }), buttonText: text({ ui: { description: 'ВСкст ΠΊΠ½ΠΎΠΏΠΊΠΈ Π²Π½ΠΈΠ·Ρƒ (CTA)' }, defaultValue: 'Partner with us', }), buttonHref: text({ validation: { isRequired: false }, ui: { description: "ΠšΡƒΠ΄Π° Π²Π΅Π΄Ρ‘Ρ‚ ΠΊΠ½ΠΎΠΏΠΊΠ° 'Partner with us'" }, }), // массив ΠΊΠ°Ρ€Ρ‚ΠΎΡ‡Π΅ΠΊ items: relationship({ ref: 'SolutionItem.section', many: true, ui: { displayMode: 'cards', cardFields: ['title', 'order', 'image'], inlineCreate: { fields: ['title', 'text', 'href', 'image', 'order'] }, inlineEdit: { fields: ['title', 'text', 'href', 'image', 'order'] }, linkToItem: true, inlineConnect: true, }, }), }, hooks: { // ΠžΠ³Ρ€Π°Π½ΠΈΡ‡ΠΈΠ²Π°Π΅ΠΌ Π΄ΠΎ ΠΎΠ΄Π½ΠΎΠΉ записи async beforeOperation({ operation, listKey, context }) { if (operation === 'create') { const existing = await context.db[listKey].findMany({ take: 1 }); if (existing.length) throw new Error('МоТно ΡΠΎΠ·Π΄Π°Ρ‚ΡŒ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ ΠΎΠ΄ΠΈΠ½ SolutionsSection'); } }, }, }), SolutionItem: list({ access: allowAll, ui: { label: 'Solutions β†’ Item', listView: { initialColumns: ['title', 'order'] }, }, fields: { title: text({ validation: { isRequired: true } }), text: text({ ui: { displayMode: 'textarea' } }), href: text({ ui: { description: 'ΠžΠΏΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½Π°Ρ ссылка "Learn more"' } }), image: image({ storage: 'local_images', ui: { description: 'Ѐоновая ΠΊΠ°Ρ€Ρ‚ΠΈΠ½ΠΊΠ° ΠΊΠ°Ρ€Ρ‚ΠΎΡ‡ΠΊΠΈ' }, }), order: integer({ defaultValue: 0, ui: { description: 'ΠŸΠΎΡ€ΡΠ΄ΠΎΠΊ сортировки (Ρ‡Π΅ΠΌ мСньшС, Ρ‚Π΅ΠΌ Π²Ρ‹ΡˆΠ΅)' }, }), // обратная связь ΠΊ сСкции section: relationship({ ref: 'SolutionsSection.items', ui: { hideCreate: true }, }), }, }), CountriesMapSection: list({ access: allowAll, ui: { label: 'Countries Map (Section)', listView: { initialColumns: ['title'] }, }, fields: { title: text({ ui: { description: 'ΠžΠΏΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½Ρ‹ΠΉ Π·Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ сСкции' }, }), countries: relationship({ ref: 'MapCountry.section', many: true, ui: { displayMode: 'cards', cardFields: ['code', 'name', 'isEnabled', 'flag'], inlineCreate: { fields: ['code', 'name', 'isEnabled', 'flag'] }, inlineEdit: { fields: ['name', 'isEnabled', 'flag'] }, linkToItem: true, }, }), }, hooks: { async beforeOperation({ operation, listKey, context }) { if (operation === 'create') { const existing = await context.db[listKey].findMany({ where: {} }); if (existing.length > 0) { throw new Error('МоТно ΡΠΎΠ·Π΄Π°Ρ‚ΡŒ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ ΠΎΠ΄Π½Ρƒ CountriesMapSection'); } } }, }, }), /* === Π‘Ρ‚Ρ€Π°Π½Π° Π½Π° ΠΊΠ°Ρ€Ρ‚Π΅ === */ MapCountry: list({ access: allowAll, ui: { label: 'Countries', listView: { initialColumns: ['code', 'name', 'isEnabled'] }, }, fields: { section: relationship({ ref: 'CountriesMapSection.countries', ui: { hideCreate: true }, }), code: select({ type: 'enum', // value β€” Ρ‚ΠΎ, Ρ‡Ρ‚ΠΎ ΡƒΠ²ΠΈΠ΄ΠΈΡ‚ Ρ„Ρ€ΠΎΠ½Ρ‚; label β€” ΡƒΠ΄ΠΎΠ±ΠΎΡ‡ΠΈΡ‚Π°Π΅ΠΌΠΎΠ΅ имя Π² Π°Π΄ΠΌΠΈΠ½ΠΊΠ΅ options: [ { label: 'United States of America', value: 'usa' }, { label: 'Kazakhstan', value: 'kaz' }, { label: 'Uzbekistan', value: 'uzb' }, { label: 'Kyrgyzstan', value: 'kgz' }, { label: 'Tajikistan', value: 'tjk' }, { label: 'Turkey', value: 'tur' }, { label: 'Cyprus', value: 'cyp' }, { label: 'Armenia', value: 'arm' }, { label: 'United Arab Emirates', value: 'uae' }, ], validation: { isRequired: true }, defaultValue: 'kaz', ui: { description: 'Код страны (фиксированный Π½Π°Π±ΠΎΡ€ для Ρ„Ρ€ΠΎΠ½Ρ‚Π°)' }, isIndexed: 'unique', // ΠΎΠ΄Π½Π° запись Π½Π° ΠΊΠΎΠ΄ }), name: text({ validation: { isRequired: true }, ui: { description: 'Подпись Π½Π° ΠΊΠ°Ρ€Ρ‚ΠΎΡ‡ΠΊΠ΅ (ΠΌΠΎΠΆΠ½ΠΎ Π»ΠΎΠΊΠ°Π»ΠΈΠ·ΠΎΠ²Π°Ρ‚ΡŒ)' }, }), isEnabled: checkbox({ defaultValue: true, ui: { description: 'ΠŸΠΎΠΊΠ°Π·Ρ‹Π²Π°Ρ‚ΡŒ страну Π½Π° ΠΊΠ°Ρ€Ρ‚Π΅' }, }), flag: image({ storage: 'local_images', ui: { description: 'Иконка Ρ„Π»Π°Π³Π° (SVG/PNG), Π±ΡƒΠ΄Π΅Ρ‚ вмСсто Π·Π΅Π»Ρ‘Π½ΠΎΠΉ Ρ‚ΠΎΡ‡ΠΊΠΈ' }, }), items: relationship({ ref: 'MapCountryItem.country', many: true, ui: { displayMode: 'cards', cardFields: ['text', 'order'], inlineCreate: { fields: ['text', 'order'] }, inlineEdit: { fields: ['text', 'order'] }, linkToItem: false, }, }), order: integer({ defaultValue: 0, ui: { description: 'ΠŸΡ€ΠΎΠΈΠ·Π²ΠΎΠ»ΡŒΠ½Π°Ρ сортировка стран (мСньшС β€” Ρ€Π°Π½ΡŒΡˆΠ΅)' }, }), }, }), /* === ΠŸΡƒΠ½ΠΊΡ‚Ρ‹ Π²Π½ΡƒΡ‚Ρ€ΠΈ ΠΊΠ°Ρ€Ρ‚ΠΎΡ‡ΠΊΠΈ страны === */ MapCountryItem: list({ access: allowAll, ui: { label: 'Country Items', isHidden: false, listView: { initialColumns: ['country', 'text', 'order'] }, }, fields: { country: relationship({ ref: 'MapCountry.items', ui: { hideCreate: true }, }), text: text({ validation: { isRequired: true }, ui: { description: 'Π‘Ρ‚Ρ€ΠΎΠΊΠ° Π² спискС (bullet)' }, }), order: integer({ defaultValue: 0, ui: { description: 'ΠŸΠΎΡ€ΡΠ΄ΠΎΠΊ Π²Π½ΡƒΡ‚Ρ€ΠΈ ΠΊΠ°Ρ€Ρ‚ΠΎΡ‡ΠΊΠΈ' }, }), }, }), ContactRequest: list({ access: { // Π‘ΠΎΠ·Π΄Π°Π²Π°Ρ‚ΡŒ ΠΌΠΎΠΆΠ΅Ρ‚ ΠΊΡ‚ΠΎ ΡƒΠ³ΠΎΠ΄Π½ΠΎ, Ρ‡ΠΈΡ‚Π°Ρ‚ΡŒ/ΠΏΡ€Π°Π²ΠΈΡ‚ΡŒ β€” Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Π°Π²Ρ‚ΠΎΡ€ΠΈΠ·ΠΎΠ²Π°Π½Π½Ρ‹Π΅ (Π°Π΄ΠΌΠΈΠ½ΠΊΠ°) operation: { query: ({ session }) => !!session, create: allowAll, update: ({ session }) => !!session, delete: ({ session }) => !!session, }, }, ui: { label: 'Contact requests', listView: { initialColumns: ['createdAt', 'name', 'email', 'company', 'message', 'countryCode'], initialSort: { field: 'createdAt', direction: 'DESC' }, }, }, fields: { name: text({ validation: { isRequired: true } }), company: text(), message: text(), position: text(), email: text({ validation: { isRequired: true } }), phone: text(), countryCode: text({ ui: { description: 'ISO-ΠΊΠΎΠ΄ страны (Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€, US)' } }), createdAt: timestamp({ defaultValue: { kind: 'now' } }), }, hooks: { // послС ΡƒΡΠΏΠ΅ΡˆΠ½ΠΎΠ³ΠΎ создания β€” отправляСм письмо afterOperation: async ({ operation, item }) => { if (operation !== 'create' || !item) return; const { SMTP_HOST, SMTP_PORT = '587', SMTP_USER, SMTP_PASS, SMTP_SECURE = 'false', MAIL_FROM = 'no-reply@example.com', CONTACT_TO = MAIL_FROM, } = process.env; if (!SMTP_HOST || !CONTACT_TO) { console.warn('[ContactRequest] SMTP not configured β€” skip email.'); return; } const transporter = nodemailer.createTransport({ host: SMTP_HOST, port: Number(SMTP_PORT), secure: SMTP_SECURE === 'true', auth: SMTP_USER ? { user: SMTP_USER, pass: SMTP_PASS } : undefined, }); const html = `

New contact request

`; try { await transporter.sendMail({ from: MAIL_FROM, to: CONTACT_TO, subject: 'New contact request', html, }); } catch (err) { console.error('[ContactRequest] Email send failed:', err); } }, }, }), Privacy : list({ access: allowAll, ui: { label: "Privacy Policy", isHidden: false, }, graphql: { singular: "privacy", plural: "privacys", // ΠΌΠΎΠΆΠ½ΠΎ ΠΈ "privacyList", Ссли Ρ…ΠΎΡ‡Π΅ΡˆΡŒ ΠΏΠΎ-Π΄Ρ€ΡƒΠ³ΠΎΠΌΡƒ }, fields: { title: text({ validation: { isRequired: true }, defaultValue: "Privacy Policy", }), content: document({ label: "Privacy Policy Content", formatting: { inlineMarks: { bold: true, italic: true, underline: true, strikethrough: true, }, listTypes: true, headingLevels: [1, 2, 3], alignment: true, }, links: true, layouts: [[1], [1, 1]], }), }, }) };