feat(cms): rich text editor for group visits page, fix /kvytky → /payments links
- Change heroDescription, featureText, bottomText fields from textarea to richText (Lexical) in GroupVisitsPage global so admins can format paragraphs and bold text in admin UI - Render rich text fields with <RichText> component on the frontend - Migration: ALTER varchar → jsonb for the three fields; UPDATE header cta_href and birthday_page_pricing_packages cta_href from /kvytky to /payments Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8eda50b351
commit
00f8742814
5 changed files with 90 additions and 45 deletions
55
migrations/20260611_140000.ts
Normal file
55
migrations/20260611_140000.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
|
||||
|
||||
export async function up({ db }: MigrateUpArgs): Promise<void> {
|
||||
// Fix header CTA link: /kvytky → /payments
|
||||
await db.execute(sql`
|
||||
UPDATE "header"
|
||||
SET "cta_href" = '/payments'
|
||||
WHERE "cta_href" = '/kvytky';
|
||||
`)
|
||||
|
||||
// Fix birthday page pricing package CTA links
|
||||
await db.execute(sql`
|
||||
UPDATE "birthday_page_pricing_packages"
|
||||
SET "cta_href" = '/payments'
|
||||
WHERE "cta_href" = '/kvytky';
|
||||
`)
|
||||
|
||||
// Change hero_description, feature_text, bottom_text from varchar to jsonb
|
||||
// (existing text content cannot be auto-converted; fields reset to NULL — re-enter in admin)
|
||||
await db.execute(sql`
|
||||
ALTER TABLE "group_visits_page"
|
||||
ALTER COLUMN "hero_description" TYPE jsonb USING NULL,
|
||||
ALTER COLUMN "feature_text" TYPE jsonb USING NULL,
|
||||
ALTER COLUMN "bottom_text" TYPE jsonb USING NULL;
|
||||
`)
|
||||
|
||||
await db.execute(sql`
|
||||
ALTER TABLE "_group_visits_page_v"
|
||||
ALTER COLUMN "version_hero_description" TYPE jsonb USING NULL,
|
||||
ALTER COLUMN "version_feature_text" TYPE jsonb USING NULL,
|
||||
ALTER COLUMN "version_bottom_text" TYPE jsonb USING NULL;
|
||||
`)
|
||||
}
|
||||
|
||||
export async function down({ db }: MigrateDownArgs): Promise<void> {
|
||||
await db.execute(sql`
|
||||
UPDATE "header"
|
||||
SET "cta_href" = '/kvytky'
|
||||
WHERE "cta_href" = '/payments';
|
||||
`)
|
||||
|
||||
await db.execute(sql`
|
||||
ALTER TABLE "group_visits_page"
|
||||
ALTER COLUMN "hero_description" TYPE varchar USING NULL,
|
||||
ALTER COLUMN "feature_text" TYPE varchar USING NULL,
|
||||
ALTER COLUMN "bottom_text" TYPE varchar USING NULL;
|
||||
`)
|
||||
|
||||
await db.execute(sql`
|
||||
ALTER TABLE "_group_visits_page_v"
|
||||
ALTER COLUMN "version_hero_description" TYPE varchar USING NULL,
|
||||
ALTER COLUMN "version_feature_text" TYPE varchar USING NULL,
|
||||
ALTER COLUMN "version_bottom_text" TYPE varchar USING NULL;
|
||||
`)
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ import * as migration_20260515_162527 from './20260515_162527'
|
|||
import * as migration_20260518_104929 from './20260518_104929'
|
||||
import * as migration_20260518_115657 from './20260518_115657'
|
||||
import * as migration_20260610_140000 from './20260610_140000'
|
||||
import * as migration_20260611_140000 from './20260611_140000'
|
||||
|
||||
export const migrations = [
|
||||
{
|
||||
|
|
@ -30,4 +31,9 @@ export const migrations = [
|
|||
down: migration_20260610_140000.down,
|
||||
name: '20260610_140000',
|
||||
},
|
||||
{
|
||||
up: migration_20260611_140000.up,
|
||||
down: migration_20260611_140000.down,
|
||||
name: '20260611_140000',
|
||||
},
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import type { Metadata } from 'next'
|
||||
import { getPayload } from 'payload'
|
||||
import configPromise from '@payload-config'
|
||||
import { RichText } from '@payloadcms/richtext-lexical/react'
|
||||
import { GroupRequestForm } from '@/components/forms/GroupRequestForm'
|
||||
import { FormBlock, type FormData as FormBlockData } from '@/components/forms/FormBlock'
|
||||
import { RefreshRouteOnSave } from '@/components/cms/RefreshRouteOnSave'
|
||||
|
|
@ -70,14 +71,11 @@ export default async function GroupVisitsPage() {
|
|||
const heroTitleSize = (d?.heroTitleSize as number | undefined) ?? null
|
||||
const heroSubtitleSize = (d?.heroSubtitleSize as number | undefined) ?? null
|
||||
const heroFont = (d?.heroFont as string | undefined) ?? null
|
||||
const heroDescription =
|
||||
(d?.heroDescription as string) ??
|
||||
'Шукаєте ідеальне місце для групового виїзду класу чи садочка? Або яскраву локацію для фотосесії? Хочете, щоб дитячий випускний альбом був дійсно унікальним? Запрошуємо провести цей захопливий і незабутній день на казковій локації.'
|
||||
type RichTextData = Parameters<typeof RichText>[0]['data']
|
||||
const heroDescription = (d?.heroDescription ?? null) as RichTextData | null
|
||||
const heroCta = (d?.heroCta as string) ?? 'Забронювати пригоду'
|
||||
|
||||
const featureText =
|
||||
(d?.featureText as string) ??
|
||||
'На дітлахів чекає подорож ДиноПарком та ДивоЛісом. Це активне дозвілля на свіжому повітрі та справжні казкові пригоди, де кожен стане героєм власної історії.'
|
||||
const featureText = (d?.featureText ?? null) as RichTextData | null
|
||||
const featureImages: string[] = ((d?.featureImages as { image: unknown }[]) ?? [])
|
||||
.map((i) => mediaUrl(i.image as never))
|
||||
.filter((u): u is string => Boolean(u))
|
||||
|
|
@ -145,9 +143,7 @@ export default async function GroupVisitsPage() {
|
|||
(d?.priceDescription as string) ??
|
||||
'У вартість входить відвідування ДиноПарку та ДивоЛісу.\nЧас перебування на локаціях необмежений.'
|
||||
|
||||
const bottomText =
|
||||
(d?.bottomText as string) ??
|
||||
'Хочете перетворити візит на справжню маленьку експедицію з розкопками або замовити екскурсію з розповідями про динозаврів? Ми залюбки це організуємо! Телефонуйте нам — і ми все підготуємо та розрахуємо індивідуально для вашої групи.'
|
||||
const bottomText = (d?.bottomText ?? null) as RichTextData | null
|
||||
const bottomImages: string[] = ((d?.bottomImages as { image: unknown }[]) ?? [])
|
||||
.map((i) => mediaUrl(i.image as never))
|
||||
.filter((u): u is string => Boolean(u))
|
||||
|
|
@ -202,7 +198,9 @@ export default async function GroupVisitsPage() {
|
|||
{/* 2. Green description band */}
|
||||
<section className="wave-pattern relative overflow-hidden rounded-b-[20px] px-4 py-12 md:px-8 md:py-16">
|
||||
<div className="relative z-10 mx-auto flex max-w-[1200px] flex-col items-stretch gap-10">
|
||||
<p className="text-[18px] leading-[1.5] text-white md:text-[24px]">{heroDescription}</p>
|
||||
<div className="text-[18px] leading-[1.5] text-white md:text-[24px] [&_p]:mb-3 [&_strong]:font-bold">
|
||||
{heroDescription && <RichText data={heroDescription} />}
|
||||
</div>
|
||||
<a
|
||||
href="#order-form"
|
||||
className="inline-flex items-center justify-center self-center rounded-[64px] bg-[#f28b4a] px-[30px] py-[10px] text-[18px] font-bold text-white transition-opacity hover:opacity-90 md:text-[20px]"
|
||||
|
|
@ -215,9 +213,9 @@ export default async function GroupVisitsPage() {
|
|||
{/* 3. Feature banner — text + overlapping photos */}
|
||||
<section className="mx-auto max-w-[1204px] px-4 py-12 md:px-8 md:py-16">
|
||||
<div className="grid grid-cols-1 items-center gap-10 md:grid-cols-2">
|
||||
<p className="order-2 text-[18px] leading-[1.5] font-medium text-[#272727] md:order-1 md:text-[24px]">
|
||||
{featureText}
|
||||
</p>
|
||||
<div className="order-2 text-[18px] leading-[1.5] font-medium text-[#272727] md:order-1 md:text-[24px] [&_p]:mb-3 [&_strong]:font-bold">
|
||||
{featureText && <RichText data={featureText} />}
|
||||
</div>
|
||||
<div className="relative order-1 mx-auto h-[300px] w-full max-w-[460px] md:order-2 md:h-[400px]">
|
||||
<img
|
||||
src={featureA}
|
||||
|
|
@ -266,9 +264,9 @@ export default async function GroupVisitsPage() {
|
|||
{/* Bottom banner — text + overlapping photos */}
|
||||
<section className="mx-auto max-w-[1204px] px-4 py-12 md:px-8 md:py-16">
|
||||
<div className="grid grid-cols-1 items-center gap-10 md:grid-cols-2">
|
||||
<p className="order-2 text-[18px] leading-[1.5] font-medium text-[#272727] md:order-1 md:text-[24px]">
|
||||
{bottomText}
|
||||
</p>
|
||||
<div className="order-2 text-[18px] leading-[1.5] font-medium text-[#272727] md:order-1 md:text-[24px] [&_p]:mb-3 [&_strong]:font-bold">
|
||||
{bottomText && <RichText data={bottomText} />}
|
||||
</div>
|
||||
<div className="relative order-1 mx-auto h-[320px] w-full max-w-[460px] md:order-2 md:h-[420px]">
|
||||
<img
|
||||
src={bottomA}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { GlobalConfig } from 'payload'
|
||||
import { lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||
import { isAdminOrEditor } from '@/access/isAdminOrEditor'
|
||||
import { revalidateGlobalAfterChange } from '@/hooks/revalidatePath'
|
||||
|
||||
|
|
@ -61,10 +62,9 @@ export const GroupVisitsPage: GlobalConfig = {
|
|||
// Green band
|
||||
{
|
||||
name: 'heroDescription',
|
||||
type: 'textarea',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor({}),
|
||||
label: 'Текст зеленої смуги під hero',
|
||||
defaultValue:
|
||||
'Шукаєте ідеальне місце для групового виїзду класу чи садочка? Або яскраву локацію для фотосесії? Хочете, щоб дитячий випускний альбом був дійсно унікальним? Запрошуємо провести цей захопливий і незабутній день на казковій локації.',
|
||||
},
|
||||
{
|
||||
name: 'heroCta',
|
||||
|
|
@ -75,10 +75,9 @@ export const GroupVisitsPage: GlobalConfig = {
|
|||
// Feature two-col section
|
||||
{
|
||||
name: 'featureText',
|
||||
type: 'textarea',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor({}),
|
||||
label: 'Текст у секції з фото (ліворуч)',
|
||||
defaultValue:
|
||||
'На дітлах чекає подорож ДинопарКом та ДивоЛісом. Це активне дозвілля на свіжому повітрі та справжні казкові пригоди, де кожен стане героєм власної історії.',
|
||||
},
|
||||
{
|
||||
name: 'featureImages',
|
||||
|
|
@ -180,10 +179,9 @@ export const GroupVisitsPage: GlobalConfig = {
|
|||
// Bottom section
|
||||
{
|
||||
name: 'bottomText',
|
||||
type: 'textarea',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor({}),
|
||||
label: 'Текст нижньої секції (ліворуч)',
|
||||
defaultValue:
|
||||
'Хочете перетворити візит на справжню маленьку експедицію з розповідями або замовити екскурсію з розповідями про динозаврів? Ми залюбки це організуємо! Телефонуйте нам — і ми все підготуємо та розрахуємо індивідуально для вашої групи.',
|
||||
},
|
||||
{
|
||||
name: 'bottomImages',
|
||||
|
|
|
|||
|
|
@ -2889,13 +2889,9 @@ export const group_visits_page = pgTable(
|
|||
heroSubtitle: varchar('hero_subtitle').default(
|
||||
'Спеціальні умови для організованих груп. Мінімум 10 осіб — максимум вражень.'
|
||||
),
|
||||
heroDescription: varchar('hero_description').default(
|
||||
'Шукаєте ідеальне місце для групового виїзду класу чи садочка? Або яскраву локацію для фотосесії? Хочете, щоб дитячий випускний альбом був дійсно унікальним? Запрошуємо провести цей захопливий і незабутній день на казковій локації.'
|
||||
),
|
||||
heroDescription: jsonb('hero_description'),
|
||||
heroCta: varchar('hero_cta').default('Забронювати пригоду'),
|
||||
featureText: varchar('feature_text').default(
|
||||
'На дітлах чекає подорож ДинопарКом та ДивоЛісом. Це активне дозвілля на свіжому повітрі та справжні казкові пригоди, де кожен стане героєм власної історії.'
|
||||
),
|
||||
featureText: jsonb('feature_text'),
|
||||
amenitiesTitle: varchar('amenities_title').default('Ми подбали про затишок і комфорт'),
|
||||
workingHoursTitle: varchar('working_hours_title').default('Час роботи'),
|
||||
workingHours: varchar('working_hours').default("п'ятниця-субота-неділя з 11:00 до 20:00"),
|
||||
|
|
@ -2911,9 +2907,7 @@ export const group_visits_page = pgTable(
|
|||
priceDescription: varchar('price_description').default(
|
||||
'У вартість входить відвідування Динопарку та ДивоЛісу.\nЧас перебування на локаціях необмежений.'
|
||||
),
|
||||
bottomText: varchar('bottom_text').default(
|
||||
'Хочете перетворити візит на справжню маленьку експедицію з розповідями або замовити екскурсію з розповідями про динозаврів? Ми залюбки це організуємо! Телефонуйте нам — і ми все підготуємо та розрахуємо індивідуально для вашої групи.'
|
||||
),
|
||||
bottomText: jsonb('bottom_text'),
|
||||
formTitle: varchar('form_title').default('Подати заявку на групове відвідування'),
|
||||
formSubtitle: varchar('form_subtitle').default(
|
||||
'Вкажіть кількість учасників та бажану дату — менеджер зателефонує і погодить деталі.'
|
||||
|
|
@ -3021,13 +3015,9 @@ export const _group_visits_page_v = pgTable(
|
|||
version_heroSubtitle: varchar('version_hero_subtitle').default(
|
||||
'Спеціальні умови для організованих груп. Мінімум 10 осіб — максимум вражень.'
|
||||
),
|
||||
version_heroDescription: varchar('version_hero_description').default(
|
||||
'Шукаєте ідеальне місце для групового виїзду класу чи садочка? Або яскраву локацію для фотосесії? Хочете, щоб дитячий випускний альбом був дійсно унікальним? Запрошуємо провести цей захопливий і незабутній день на казковій локації.'
|
||||
),
|
||||
version_heroDescription: jsonb('version_hero_description'),
|
||||
version_heroCta: varchar('version_hero_cta').default('Забронювати пригоду'),
|
||||
version_featureText: varchar('version_feature_text').default(
|
||||
'На дітлах чекає подорож ДинопарКом та ДивоЛісом. Це активне дозвілля на свіжому повітрі та справжні казкові пригоди, де кожен стане героєм власної історії.'
|
||||
),
|
||||
version_featureText: jsonb('version_feature_text'),
|
||||
version_amenitiesTitle: varchar('version_amenities_title').default(
|
||||
'Ми подбали про затишок і комфорт'
|
||||
),
|
||||
|
|
@ -3049,9 +3039,7 @@ export const _group_visits_page_v = pgTable(
|
|||
version_priceDescription: varchar('version_price_description').default(
|
||||
'У вартість входить відвідування Динопарку та ДивоЛісу.\nЧас перебування на локаціях необмежений.'
|
||||
),
|
||||
version_bottomText: varchar('version_bottom_text').default(
|
||||
'Хочете перетворити візит на справжню маленьку експедицію з розповідями або замовити екскурсію з розповідями про динозаврів? Ми залюбки це організуємо! Телефонуйте нам — і ми все підготуємо та розрахуємо індивідуально для вашої групи.'
|
||||
),
|
||||
version_bottomText: jsonb('version_bottom_text'),
|
||||
version_formTitle: varchar('version_form_title').default(
|
||||
'Подати заявку на групове відвідування'
|
||||
),
|
||||
|
|
@ -3171,7 +3159,7 @@ export const birthday_page_pricing_packages = pgTable(
|
|||
price: varchar('price').notNull(),
|
||||
note: varchar('note'),
|
||||
ctaLabel: varchar('cta_label').default('Купити квиток'),
|
||||
ctaHref: varchar('cta_href').default('/kvytky'),
|
||||
ctaHref: varchar('cta_href').default('/payments'),
|
||||
},
|
||||
(columns) => [
|
||||
index('birthday_page_pricing_packages_order_idx').on(columns._order),
|
||||
|
|
@ -3359,7 +3347,7 @@ export const _birthday_page_v_version_pricing_packages = pgTable(
|
|||
price: varchar('price').notNull(),
|
||||
note: varchar('note'),
|
||||
ctaLabel: varchar('cta_label').default('Купити квиток'),
|
||||
ctaHref: varchar('cta_href').default('/kvytky'),
|
||||
ctaHref: varchar('cta_href').default('/payments'),
|
||||
_uuid: varchar('_uuid'),
|
||||
},
|
||||
(columns) => [
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue