From 3132fcb8eb838a76aa3db30e705d85ce663d6e41 Mon Sep 17 00:00:00 2001 From: Do Siki Date: Thu, 10 Sep 2026 03:41:03 +0200 Subject: [PATCH] feat(cms): Home/About/Services/Contact/Common Globals + JSON migration (MITHOME-87) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Payload Global configs mirroring proto/src/content/types.ts: - src/globals/{Home,About,Services,Contact,Common}.ts - src/globals/fields/stringArray.ts — shared helper: Payload has no native string[] field, so every plain string array from the old content types (trustBullets, paragraphs, features, spec items, …) becomes an array of one-field { value } objects. - home.partners is intentionally NOT included — that becomes its own Partners collection (logo -> Media upload) in MITHOME-89, to avoid two disagreeing sources for the same data. - contact.form.fields.gdpr.label stays a plain textarea (not lexical richText): the JSON source is a hand-written HTML string with an tag; richText's node-tree serialization would need its own migration/render logic, out of scope for this bootstrap pass. scripts/migrate-content-to-payload.ts: one-shot, idempotent Local API migration reading the existing JSON files and calling updateGlobal — does not touch or delete the JSON files. Run via the new `npm run migrate:content` script. Registered the five Globals in payload.config.ts. Also included here (belongs with the previous "resolve double-root- layout conflict" commit but didn't actually get staged there — verified only now by diffing HEAD against the working tree): (frontend)/layout.tsx's relative imports corrected to ../../ instead of ../ (one directory deeper than the original src/app/layout.tsx). Verified: - npm run build / lint, tsc --noEmit, npm test (58 passed) all clean - npm run migrate:content against the real dev MongoDB container, then read back via payload.findGlobal() — hero.title, trustBullets, services.items[0].features, footer.address, faq.items.length all match the JSON source - Real browser: logged into /admin, opened the Home global editor — Hero/Trust bullets group renders and shows the migrated Hungarian content correctly (see screenshot shared in chat) Co-Authored-By: Claude Sonnet 5 --- proto/scripts/migrate-content-to-payload.ts | 104 ++++++++++++++ proto/src/app/(frontend)/layout.tsx | 10 +- proto/src/app/(payload)/admin/importMap.js | 10 +- proto/src/globals/About.ts | 73 ++++++++++ proto/src/globals/Common.ts | 73 ++++++++++ proto/src/globals/Contact.ts | 142 ++++++++++++++++++++ proto/src/globals/Home.ts | 105 +++++++++++++++ proto/src/globals/Services.ts | 82 +++++++++++ proto/src/globals/fields/stringArray.ts | 24 ++++ proto/src/payload.config.ts | 15 ++- 10 files changed, 624 insertions(+), 14 deletions(-) create mode 100644 proto/scripts/migrate-content-to-payload.ts create mode 100644 proto/src/globals/About.ts create mode 100644 proto/src/globals/Common.ts create mode 100644 proto/src/globals/Contact.ts create mode 100644 proto/src/globals/Home.ts create mode 100644 proto/src/globals/Services.ts create mode 100644 proto/src/globals/fields/stringArray.ts diff --git a/proto/scripts/migrate-content-to-payload.ts b/proto/scripts/migrate-content-to-payload.ts new file mode 100644 index 0000000..5c2bdab --- /dev/null +++ b/proto/scripts/migrate-content-to-payload.ts @@ -0,0 +1,104 @@ +/** + * MITHOME-87: egyszeri migrációs script — a proto/src/content/pages/*.json + * + common.json tartalmát átemeli a Payload Globals-ekbe (Home, About, + * Services, Contact, Common) a Local API-n keresztül. + * + * Futtatás (proto/ mappából, futó MongoDB-vel és beállított env-ekkel): + * node --env-file=.env.local --import tsx scripts/migrate-content-to-payload.ts + * + * Idempotens: updateGlobal-t hív, tetszőlegesen többször futtatható — + * mindig a JSON az aktuális "forrás igazság", felülírja a Payload-ban + * lévő korábbi állapotot. NEM törli/nem érinti a JSON fájlokat. + */ +import { getPayload } from 'payload' +import config from '../src/payload.config' + +import homeJson from '../src/content/pages/home.json' +import aboutJson from '../src/content/pages/about.json' +import servicesJson from '../src/content/pages/services.json' +import contactJson from '../src/content/pages/contact.json' +import commonJson from '../src/content/common.json' + +/** string[] -> [{ value: string }] — lásd src/globals/fields/stringArray.ts */ +function toStringArray(items: readonly string[]): { value: string }[] { + return items.map((value) => ({ value })) +} + +function buildHomeData(json: typeof homeJson) { + const { hero, about, services, cta, serviceFeatures } = json + return { + hero: { + ...hero, + trustBullets: toStringArray(hero.trustBullets), + }, + about, + services: { + ...services, + items: services.items.map((item) => ({ + ...item, + features: toStringArray(item.features), + })), + }, + cta, + serviceFeatures, + } +} + +function buildAboutData(json: typeof aboutJson) { + const { meta, hero, story, mission, team, cta } = json + return { + meta, + hero, + story: { ...story, paragraphs: toStringArray(story.paragraphs) }, + mission, + team: { ...team, paragraphs: toStringArray(team.paragraphs) }, + cta, + } +} + +function buildServicesData(json: typeof servicesJson) { + const { meta, hero, details, support, cta } = json + return { + meta, + hero, + details: { + ...details, + services: details.services.map((service) => ({ + ...service, + specs: { + ...service.specs, + items: toStringArray(service.specs.items), + }, + })), + }, + support, + cta, + } +} + +async function run() { + const payload = await getPayload({ config }) + + await payload.updateGlobal({ slug: 'home', data: buildHomeData(homeJson) }) + payload.logger.info('Home global migrálva') + + await payload.updateGlobal({ slug: 'about', data: buildAboutData(aboutJson) }) + payload.logger.info('About global migrálva') + + await payload.updateGlobal({ slug: 'services', data: buildServicesData(servicesJson) }) + payload.logger.info('Services global migrálva') + + await payload.updateGlobal({ slug: 'contact', data: contactJson }) + payload.logger.info('Contact global migrálva') + + await payload.updateGlobal({ slug: 'common', data: commonJson }) + payload.logger.info('Common global migrálva') + + payload.logger.info('MITHOME-87 migráció kész.') + process.exit(0) +} + +run().catch((error) => { + console.error('Migráció sikertelen:', error) + process.exit(1) +}) diff --git a/proto/src/app/(frontend)/layout.tsx b/proto/src/app/(frontend)/layout.tsx index fc621b5..e0096da 100755 --- a/proto/src/app/(frontend)/layout.tsx +++ b/proto/src/app/(frontend)/layout.tsx @@ -2,11 +2,11 @@ import type { Metadata, Viewport } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; -import Header from "../components/Header"; -import Footer from "../components/Footer"; -import { ThemeProvider } from "../components/ThemeProvider"; -import { siteConfig } from "../config/site"; -import { common } from "../content"; +import Header from "../../components/Header"; +import Footer from "../../components/Footer"; +import { ThemeProvider } from "../../components/ThemeProvider"; +import { siteConfig } from "../../config/site"; +import { common } from "../../content"; const geistSans = Geist({ variable: "--font-geist-sans", diff --git a/proto/src/app/(payload)/admin/importMap.js b/proto/src/app/(payload)/admin/importMap.js index 3ed9bc6..af86423 100644 --- a/proto/src/app/(payload)/admin/importMap.js +++ b/proto/src/app/(payload)/admin/importMap.js @@ -1,4 +1,6 @@ -// Auto-generated by Payload (`npx payload generate:importmap`). -// Kept empty until a collection/global config references a custom admin -// component — regenerate with the CLI command whenever one does. -export const importMap = {} +import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from '@payloadcms/next/rsc' + +/** @type import('payload').ImportMap */ +export const importMap = { + "@payloadcms/next/rsc#CollectionCards": CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 +} diff --git a/proto/src/globals/About.ts b/proto/src/globals/About.ts new file mode 100644 index 0000000..eaa8ceb --- /dev/null +++ b/proto/src/globals/About.ts @@ -0,0 +1,73 @@ +import type { GlobalConfig } from 'payload' + +import { stringArrayField } from './fields/stringArray' + +/** Mirrors AboutPageContent (proto/src/content/types.ts) — MITHOME-87. */ +export const About: GlobalConfig = { + slug: 'about', + admin: { + group: 'Oldalak', + }, + fields: [ + { + name: 'meta', + type: 'group', + fields: [ + { name: 'title', type: 'text', required: true }, + { name: 'description', type: 'textarea', required: true }, + { name: 'ogDescription', type: 'textarea', required: true }, + ], + }, + { + name: 'hero', + type: 'group', + fields: [ + { name: 'title', type: 'text', required: true }, + { name: 'subtitle', type: 'text', required: true }, + ], + }, + { + name: 'story', + type: 'group', + fields: [ + { name: 'title', type: 'text', required: true }, + stringArrayField('paragraphs', 'Paragraphs'), + ], + }, + { + name: 'mission', + type: 'group', + fields: [ + { name: 'title', type: 'text', required: true }, + { name: 'subtitle', type: 'text', required: true }, + { + name: 'values', + type: 'array', + fields: [ + { name: 'title', type: 'text', required: true }, + { name: 'description', type: 'textarea', required: true }, + { name: 'icon', type: 'text', required: true }, + ], + }, + ], + }, + { + name: 'team', + type: 'group', + fields: [ + { name: 'title', type: 'text', required: true }, + { name: 'subtitle', type: 'text', required: true }, + stringArrayField('paragraphs', 'Paragraphs'), + ], + }, + { + name: 'cta', + type: 'group', + fields: [ + { name: 'title', type: 'text', required: true }, + { name: 'subtitle', type: 'textarea', required: true }, + { name: 'button', type: 'text', required: true }, + ], + }, + ], +} diff --git a/proto/src/globals/Common.ts b/proto/src/globals/Common.ts new file mode 100644 index 0000000..93b0674 --- /dev/null +++ b/proto/src/globals/Common.ts @@ -0,0 +1,73 @@ +import type { GlobalConfig } from 'payload' + +/** Mirrors CommonContent (proto/src/content/types.ts) — MITHOME-87. */ +export const Common: GlobalConfig = { + slug: 'common', + admin: { + group: 'Oldalak', + }, + fields: [ + { + name: 'buttons', + type: 'group', + fields: [ + { name: 'contact', type: 'text', required: true }, + { name: 'learnMore', type: 'text', required: true }, + { name: 'webmail', type: 'text', required: true }, + { name: 'sendMessage', type: 'text', required: true }, + ], + }, + { + name: 'labels', + type: 'group', + fields: [ + { name: 'required', type: 'text', required: true }, + { name: 'features', type: 'text', required: true }, + ], + }, + { + name: 'validation', + type: 'group', + fields: [ + { name: 'required', type: 'text', required: true }, + { name: 'invalidEmail', type: 'text', required: true }, + { + name: 'minLength', + type: 'text', + required: true, + admin: { + description: 'A {min} placeholder futásidőben cserélődik ki.', + }, + }, + ], + }, + { + name: 'staging', + type: 'group', + fields: [{ name: 'banner', type: 'text', required: true }], + }, + { + name: 'footer', + type: 'group', + fields: [ + { + name: 'copyright', + type: 'text', + required: true, + admin: { + description: 'A {year} placeholder futásidőben cserélődik ki.', + }, + }, + { name: 'address', type: 'text', required: true }, + ], + }, + { + name: 'a11y', + type: 'group', + fields: [ + { name: 'openMenu', type: 'text', required: true }, + { name: 'closeMenu', type: 'text', required: true }, + ], + }, + ], +} diff --git a/proto/src/globals/Contact.ts b/proto/src/globals/Contact.ts new file mode 100644 index 0000000..7ae951d --- /dev/null +++ b/proto/src/globals/Contact.ts @@ -0,0 +1,142 @@ +import type { GlobalConfig } from 'payload' + +/** + * Mirrors ContactPageContent (proto/src/content/types.ts) — MITHOME-87. + * + * WHY `gdpr.label` textarea és nem richText: a JSON forrás egy kézzel + * beírt taget tartalmazó HTML string (lásd contact.json). A lexical + * richText mező más (JSON node-fa) szerializációt használna, ami extra + * migrációs/render logikát igényelne — ezt itt, bootstrap szinten nem + * bontjuk ki, marad egyszerű (HTML-t tartalmazó) szöveg mező. + */ +export const Contact: GlobalConfig = { + slug: 'contact', + admin: { + group: 'Oldalak', + }, + fields: [ + { + name: 'meta', + type: 'group', + fields: [ + { name: 'title', type: 'text', required: true }, + { name: 'description', type: 'textarea', required: true }, + ], + }, + { + name: 'hero', + type: 'group', + fields: [ + { name: 'title', type: 'text', required: true }, + { name: 'subtitle', type: 'text', required: true }, + ], + }, + { + name: 'form', + type: 'group', + fields: [ + { name: 'title', type: 'text', required: true }, + { name: 'successMessage', type: 'text', required: true }, + { name: 'errorMessage', type: 'text', required: true }, + { + name: 'fields', + type: 'group', + fields: [ + { + name: 'name', + type: 'group', + fields: [ + { name: 'label', type: 'text', required: true }, + { name: 'placeholder', type: 'text', required: true }, + { name: 'error', type: 'text', required: true }, + ], + }, + { + name: 'email', + type: 'group', + fields: [ + { name: 'label', type: 'text', required: true }, + { name: 'placeholder', type: 'text', required: true }, + { name: 'errorRequired', type: 'text', required: true }, + { name: 'errorInvalid', type: 'text', required: true }, + ], + }, + { + name: 'subject', + type: 'group', + fields: [ + { name: 'label', type: 'text', required: true }, + { name: 'placeholder', type: 'text', required: true }, + { name: 'error', type: 'text', required: true }, + ], + }, + { + name: 'message', + type: 'group', + fields: [ + { name: 'label', type: 'text', required: true }, + { name: 'placeholder', type: 'text', required: true }, + { name: 'errorRequired', type: 'text', required: true }, + { name: 'errorMinLength', type: 'text', required: true }, + ], + }, + { + name: 'gdpr', + type: 'group', + fields: [ + { name: 'label', type: 'textarea', required: true }, + { name: 'error', type: 'text', required: true }, + ], + }, + ], + }, + { name: 'submitButton', type: 'text', required: true }, + { name: 'submittingButton', type: 'text', required: true }, + ], + }, + { + name: 'info', + type: 'group', + fields: [ + { name: 'title', type: 'text', required: true }, + { + name: 'email', + type: 'group', + fields: [ + { name: 'title', type: 'text', required: true }, + { name: 'responseTime', type: 'text', required: true }, + ], + }, + { + name: 'company', + type: 'group', + fields: [{ name: 'title', type: 'text', required: true }], + }, + { + name: 'webmail', + type: 'group', + fields: [ + { name: 'title', type: 'text', required: true }, + { name: 'linkText', type: 'text', required: true }, + { name: 'subtitle', type: 'text', required: true }, + ], + }, + ], + }, + { + name: 'faq', + type: 'group', + fields: [ + { name: 'title', type: 'text', required: true }, + { + name: 'items', + type: 'array', + fields: [ + { name: 'question', type: 'text', required: true }, + { name: 'answer', type: 'textarea', required: true }, + ], + }, + ], + }, + ], +} diff --git a/proto/src/globals/Home.ts b/proto/src/globals/Home.ts new file mode 100644 index 0000000..05cffb1 --- /dev/null +++ b/proto/src/globals/Home.ts @@ -0,0 +1,105 @@ +import type { GlobalConfig } from 'payload' + +import { stringArrayField } from './fields/stringArray' + +/** + * Mirrors HomePageContent (proto/src/content/types.ts) — MITHOME-87. + * + * WHY nincs `partners` mező: a home.partners.items a JSON-ban ma egy + * beágyazott lista, de a Payload oldalon önálló `Partners` collection lesz + * (logó = Media upload reference) — lásd MITHOME-89. Itt szándékosan + * kihagyjuk, nehogy két, egymásnak ellentmondó forrás legyen. + */ +export const Home: GlobalConfig = { + slug: 'home', + admin: { + group: 'Oldalak', + }, + fields: [ + { + name: 'hero', + type: 'group', + fields: [ + { name: 'title', type: 'text', required: true }, + { name: 'subtitle', type: 'text', required: true }, + { name: 'description', type: 'textarea', required: true }, + stringArrayField('trustBullets', 'Trust bullets'), + { + name: 'cta', + type: 'group', + fields: [ + { + name: 'primary', + type: 'group', + fields: [ + { name: 'text', type: 'text', required: true }, + { name: 'href', type: 'text', required: true }, + { name: 'external', type: 'checkbox', defaultValue: false }, + ], + }, + { + name: 'secondary', + type: 'group', + fields: [ + { name: 'text', type: 'text', required: true }, + { name: 'href', type: 'text', required: true }, + { name: 'external', type: 'checkbox', defaultValue: false }, + ], + }, + ], + }, + ], + }, + { + name: 'about', + type: 'group', + fields: [ + { name: 'title', type: 'text', required: true }, + { + name: 'usps', + type: 'array', + fields: [ + { name: 'id', type: 'text', required: true }, + { name: 'title', type: 'text', required: true }, + { name: 'description', type: 'textarea', required: true }, + { name: 'icon', type: 'text', required: true }, + ], + }, + ], + }, + { + name: 'services', + type: 'group', + fields: [ + { name: 'title', type: 'text', required: true }, + { name: 'subtitle', type: 'text', required: true }, + { + name: 'items', + type: 'array', + fields: [ + { name: 'id', type: 'text', required: true }, + { name: 'title', type: 'text', required: true }, + { name: 'description', type: 'textarea', required: true }, + { name: 'icon', type: 'text', required: true }, + stringArrayField('features', 'Features'), + { name: 'ctaText', type: 'text', required: true }, + ], + }, + ], + }, + { + name: 'cta', + type: 'group', + fields: [ + { name: 'title', type: 'text', required: true }, + { name: 'subtitle', type: 'textarea', required: true }, + { name: 'button', type: 'text', required: true }, + ], + }, + { + name: 'serviceFeatures', + type: 'group', + fields: [{ name: 'title', type: 'text', required: true }], + }, + ], +} diff --git a/proto/src/globals/Services.ts b/proto/src/globals/Services.ts new file mode 100644 index 0000000..3fccecd --- /dev/null +++ b/proto/src/globals/Services.ts @@ -0,0 +1,82 @@ +import type { GlobalConfig } from 'payload' + +import { stringArrayField } from './fields/stringArray' + +/** Mirrors ServicesPageContent (proto/src/content/types.ts) — MITHOME-87. */ +export const Services: GlobalConfig = { + slug: 'services', + admin: { + group: 'Oldalak', + }, + fields: [ + { + name: 'meta', + type: 'group', + fields: [ + { name: 'title', type: 'text', required: true }, + { name: 'description', type: 'textarea', required: true }, + { name: 'ogDescription', type: 'textarea', required: true }, + ], + }, + { + name: 'hero', + type: 'group', + fields: [ + { name: 'title', type: 'text', required: true }, + { name: 'subtitle', type: 'text', required: true }, + ], + }, + { + name: 'details', + type: 'group', + fields: [ + { name: 'title', type: 'text', required: true }, + { name: 'subtitle', type: 'text', required: true }, + { + name: 'services', + type: 'array', + fields: [ + { name: 'icon', type: 'text', required: true }, + { name: 'title', type: 'text', required: true }, + { name: 'description', type: 'textarea', required: true }, + { + name: 'specs', + type: 'group', + fields: [ + { name: 'title', type: 'text', required: true }, + stringArrayField('items', 'Items'), + ], + }, + ], + }, + ], + }, + { + name: 'support', + type: 'group', + fields: [ + { name: 'title', type: 'text', required: true }, + { name: 'subtitle', type: 'text', required: true }, + { + name: 'channels', + type: 'array', + fields: [ + { name: 'icon', type: 'text', required: true }, + { name: 'title', type: 'text', required: true }, + { name: 'description', type: 'text', required: true }, + ], + }, + ], + }, + { + name: 'cta', + type: 'group', + fields: [ + { name: 'title', type: 'text', required: true }, + { name: 'subtitle', type: 'textarea', required: true }, + { name: 'primaryButton', type: 'text', required: true }, + { name: 'secondaryButton', type: 'text', required: true }, + ], + }, + ], +} diff --git a/proto/src/globals/fields/stringArray.ts b/proto/src/globals/fields/stringArray.ts new file mode 100644 index 0000000..a2f57a3 --- /dev/null +++ b/proto/src/globals/fields/stringArray.ts @@ -0,0 +1,24 @@ +import type { ArrayField } from 'payload' + +/** + * Payload has no native "array of plain strings" field — the closest + * built-in shape is an array of one-field objects. Used for every + * string[] in the old content/types.ts (trustBullets, paragraphs, + * features, spec items, …) so the JSON -> Payload migration script has a + * single, consistent shape to transform into. + */ +export function stringArrayField(name: string, label?: string, required = false): ArrayField { + return { + name, + type: 'array', + label, + fields: [ + { + name: 'value', + type: 'text', + required: true, + }, + ], + required, + } +} diff --git a/proto/src/payload.config.ts b/proto/src/payload.config.ts index c92a231..60d4517 100644 --- a/proto/src/payload.config.ts +++ b/proto/src/payload.config.ts @@ -5,11 +5,9 @@ * (proto/src/lib/mongodb.ts) — Payload a `mongooseAdapter`-en keresztül * ugyanabba az adatbázisba ír, nincs szükség külön DB-re/szerverre. * - * Ez a fájl jelenleg csak a bootstrap-hez szükséges minimumot tartalmazza - * (Users collection az admin bejelentkezéshez — Payload nem tud admin - * felületet renderelni auth collection nélkül). A tényleges tartalom- - * collectionök/globalok (Home, About, Services, Contact, Common, LegalPages, - * Partners, Media) a MITHOME-87..90 ticketekben kerülnek be. + * MITHOME-87: Home/About/Services/Contact/Common Globals hozzáadva — ezek + * tükrözik a proto/src/content/pages/*.json + common.json struktúráját. + * A LegalPages/Partners/Media collectionök a MITHOME-88..89-ben kerülnek be. */ import path from 'path' import { fileURLToPath } from 'url' @@ -19,6 +17,11 @@ import { lexicalEditor } from '@payloadcms/richtext-lexical' import sharp from 'sharp' import { Users } from './collections/Users' +import { Home } from './globals/Home' +import { About } from './globals/About' +import { Services } from './globals/Services' +import { Contact } from './globals/Contact' +import { Common } from './globals/Common' const filename = fileURLToPath(import.meta.url) const dirname = path.dirname(filename) @@ -34,6 +37,8 @@ export default buildConfig({ collections: [Users], + globals: [Home, About, Services, Contact, Common], + secret: process.env.PAYLOAD_SECRET || '', typescript: {