diff --git a/migrations/20260611_160000.ts b/migrations/20260611_160000.ts new file mode 100644 index 0000000..b119c78 --- /dev/null +++ b/migrations/20260611_160000.ts @@ -0,0 +1,503 @@ +import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres' + +export async function up({ db }: MigrateUpArgs): Promise { + // ── group_visits_page: price_description ────────────────────────────────── + await db.execute(sql` + DO $$ + BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'group_visits_page' + AND column_name = 'price_description' + AND data_type = 'character varying' + ) THEN + ALTER TABLE "group_visits_page" + ALTER COLUMN "price_description" DROP DEFAULT, + ALTER COLUMN "price_description" TYPE jsonb USING NULL; + END IF; + END $$; + `) + + await db.execute(sql` + DO $$ + BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = '_group_visits_page_v' + AND column_name = 'version_price_description' + AND data_type = 'character varying' + ) THEN + ALTER TABLE "_group_visits_page_v" + ALTER COLUMN "version_price_description" DROP DEFAULT, + ALTER COLUMN "version_price_description" TYPE jsonb USING NULL; + END IF; + END $$; + `) + + // ── dinosaur_page: hero_description, activities_description ─────────────── + await db.execute(sql` + DO $$ + BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'dinosaur_page' + AND column_name = 'hero_description' + AND data_type = 'character varying' + ) THEN + ALTER TABLE "dinosaur_page" + ALTER COLUMN "hero_description" DROP DEFAULT, + ALTER COLUMN "hero_description" TYPE jsonb USING NULL; + END IF; + END $$; + `) + + await db.execute(sql` + DO $$ + BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'dinosaur_page' + AND column_name = 'activities_description' + AND data_type = 'character varying' + ) THEN + ALTER TABLE "dinosaur_page" + ALTER COLUMN "activities_description" DROP DEFAULT, + ALTER COLUMN "activities_description" TYPE jsonb USING NULL; + END IF; + END $$; + `) + + // ── dyvolis_page: hero_description ──────────────────────────────────────── + await db.execute(sql` + DO $$ + BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'dyvolis_page' + AND column_name = 'hero_description' + AND data_type = 'character varying' + ) THEN + ALTER TABLE "dyvolis_page" + ALTER COLUMN "hero_description" DROP DEFAULT, + ALTER COLUMN "hero_description" TYPE jsonb USING NULL; + END IF; + END $$; + `) + + // ── tickets_page: benefits_footnote ─────────────────────────────────────── + await db.execute(sql` + DO $$ + BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'tickets_page' + AND column_name = 'benefits_footnote' + AND data_type = 'character varying' + ) THEN + ALTER TABLE "tickets_page" + ALTER COLUMN "benefits_footnote" DROP DEFAULT, + ALTER COLUMN "benefits_footnote" TYPE jsonb USING NULL; + END IF; + END $$; + `) + + await db.execute(sql` + DO $$ + BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = '_tickets_page_v' + AND column_name = 'version_benefits_footnote' + AND data_type = 'character varying' + ) THEN + ALTER TABLE "_tickets_page_v" + ALTER COLUMN "version_benefits_footnote" DROP DEFAULT, + ALTER COLUMN "version_benefits_footnote" TYPE jsonb USING NULL; + END IF; + END $$; + `) + + // ── home_page: hero_subtitle, birthday_intro_text, news_subtitle ────────── + await db.execute(sql` + DO $$ + BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'home_page' + AND column_name = 'hero_subtitle' + AND data_type = 'character varying' + ) THEN + ALTER TABLE "home_page" + ALTER COLUMN "hero_subtitle" DROP DEFAULT, + ALTER COLUMN "hero_subtitle" TYPE jsonb USING NULL; + END IF; + END $$; + `) + + await db.execute(sql` + DO $$ + BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'home_page' + AND column_name = 'birthday_intro_text' + AND data_type = 'character varying' + ) THEN + ALTER TABLE "home_page" + ALTER COLUMN "birthday_intro_text" DROP DEFAULT, + ALTER COLUMN "birthday_intro_text" TYPE jsonb USING NULL; + END IF; + END $$; + `) + + await db.execute(sql` + DO $$ + BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'home_page' + AND column_name = 'news_subtitle' + AND data_type = 'character varying' + ) THEN + ALTER TABLE "home_page" + ALTER COLUMN "news_subtitle" DROP DEFAULT, + ALTER COLUMN "news_subtitle" TYPE jsonb USING NULL; + END IF; + END $$; + `) + + await db.execute(sql` + DO $$ + BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = '_home_page_v' + AND column_name = 'version_hero_subtitle' + AND data_type = 'character varying' + ) THEN + ALTER TABLE "_home_page_v" + ALTER COLUMN "version_hero_subtitle" DROP DEFAULT, + ALTER COLUMN "version_hero_subtitle" TYPE jsonb USING NULL; + END IF; + END $$; + `) + + await db.execute(sql` + DO $$ + BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = '_home_page_v' + AND column_name = 'version_birthday_intro_text' + AND data_type = 'character varying' + ) THEN + ALTER TABLE "_home_page_v" + ALTER COLUMN "version_birthday_intro_text" DROP DEFAULT, + ALTER COLUMN "version_birthday_intro_text" TYPE jsonb USING NULL; + END IF; + END $$; + `) + + await db.execute(sql` + DO $$ + BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = '_home_page_v' + AND column_name = 'version_news_subtitle' + AND data_type = 'character varying' + ) THEN + ALTER TABLE "_home_page_v" + ALTER COLUMN "version_news_subtitle" DROP DEFAULT, + ALTER COLUMN "version_news_subtitle" TYPE jsonb USING NULL; + END IF; + END $$; + `) + + // ── home_page_why_parents_items: description ────────────────────────────── + await db.execute(sql` + DO $$ + BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'home_page_why_parents_items' + AND column_name = 'description' + AND data_type = 'character varying' + ) THEN + ALTER TABLE "home_page_why_parents_items" + ALTER COLUMN "description" DROP DEFAULT, + ALTER COLUMN "description" TYPE jsonb USING NULL; + END IF; + END $$; + `) + + await db.execute(sql` + DO $$ + BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = '_home_page_v_version_why_parents_items' + AND column_name = 'description' + AND data_type = 'character varying' + ) THEN + ALTER TABLE "_home_page_v_version_why_parents_items" + ALTER COLUMN "description" DROP DEFAULT, + ALTER COLUMN "description" TYPE jsonb USING NULL; + END IF; + END $$; + `) + + // ── dinosaur_page_activities: description ───────────────────────────────── + await db.execute(sql` + DO $$ + BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'dinosaur_page_activities' + AND column_name = 'description' + AND data_type = 'character varying' + ) THEN + ALTER TABLE "dinosaur_page_activities" + ALTER COLUMN "description" DROP DEFAULT, + ALTER COLUMN "description" TYPE jsonb USING NULL; + END IF; + END $$; + `) + + // ── dinosaur_page_why_visit_items: description ──────────────────────────── + await db.execute(sql` + DO $$ + BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'dinosaur_page_why_visit_items' + AND column_name = 'description' + AND data_type = 'character varying' + ) THEN + ALTER TABLE "dinosaur_page_why_visit_items" + ALTER COLUMN "description" DROP DEFAULT, + ALTER COLUMN "description" TYPE jsonb USING NULL; + END IF; + END $$; + `) + + // ── birthday_page_package_items: description ────────────────────────────── + await db.execute(sql` + DO $$ + BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'birthday_page_package_items' + AND column_name = 'description' + AND data_type = 'character varying' + ) THEN + ALTER TABLE "birthday_page_package_items" + ALTER COLUMN "description" DROP DEFAULT, + ALTER COLUMN "description" TYPE jsonb USING NULL; + END IF; + END $$; + `) + + await db.execute(sql` + DO $$ + BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = '_birthday_page_v_version_package_items' + AND column_name = 'description' + AND data_type = 'character varying' + ) THEN + ALTER TABLE "_birthday_page_v_version_package_items" + ALTER COLUMN "description" DROP DEFAULT, + ALTER COLUMN "description" TYPE jsonb USING NULL; + END IF; + END $$; + `) + + // ── birthday_page_why_items: description ────────────────────────────────── + await db.execute(sql` + DO $$ + BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'birthday_page_why_items' + AND column_name = 'description' + AND data_type = 'character varying' + ) THEN + ALTER TABLE "birthday_page_why_items" + ALTER COLUMN "description" DROP DEFAULT, + ALTER COLUMN "description" TYPE jsonb USING NULL; + END IF; + END $$; + `) + + await db.execute(sql` + DO $$ + BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = '_birthday_page_v_version_why_items' + AND column_name = 'description' + AND data_type = 'character varying' + ) THEN + ALTER TABLE "_birthday_page_v_version_why_items" + ALTER COLUMN "description" DROP DEFAULT, + ALTER COLUMN "description" TYPE jsonb USING NULL; + END IF; + END $$; + `) + + // ── dyvolis_page_why_visit_items: description ───────────────────────────── + await db.execute(sql` + DO $$ + BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'dyvolis_page_why_visit_items' + AND column_name = 'description' + AND data_type = 'character varying' + ) THEN + ALTER TABLE "dyvolis_page_why_visit_items" + ALTER COLUMN "description" DROP DEFAULT, + ALTER COLUMN "description" TYPE jsonb USING NULL; + END IF; + END $$; + `) + + // ── tickets_page_combo_cards: description ───────────────────────────────── + await db.execute(sql` + DO $$ + BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'tickets_page_combo_cards' + AND column_name = 'description' + AND data_type = 'character varying' + ) THEN + ALTER TABLE "tickets_page_combo_cards" + ALTER COLUMN "description" DROP DEFAULT, + ALTER COLUMN "description" TYPE jsonb USING NULL; + END IF; + END $$; + `) + + await db.execute(sql` + DO $$ + BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = '_tickets_page_v_version_combo_cards' + AND column_name = 'description' + AND data_type = 'character varying' + ) THEN + ALTER TABLE "_tickets_page_v_version_combo_cards" + ALTER COLUMN "description" DROP DEFAULT, + ALTER COLUMN "description" TYPE jsonb USING NULL; + END IF; + END $$; + `) + + // ── locations: short_desc ───────────────────────────────────────────────── + await db.execute(sql` + DO $$ + BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'locations' + AND column_name = 'short_desc' + AND data_type = 'character varying' + ) THEN + ALTER TABLE "locations" + ALTER COLUMN "short_desc" DROP DEFAULT, + ALTER COLUMN "short_desc" TYPE jsonb USING NULL; + END IF; + END $$; + `) + + // ── locations_why_visit_items: description ──────────────────────────────── + await db.execute(sql` + DO $$ + BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'locations_why_visit_items' + AND column_name = 'description' + AND data_type = 'character varying' + ) THEN + ALTER TABLE "locations_why_visit_items" + ALTER COLUMN "description" DROP DEFAULT, + ALTER COLUMN "description" TYPE jsonb USING NULL; + END IF; + END $$; + `) + + // ── home_page_hero_slides: subtitle (created after next drizzle push) ───── + await db.execute(sql` + DO $$ + BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.tables WHERE table_name = 'home_page_hero_slides' + ) AND EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'home_page_hero_slides' + AND column_name = 'subtitle' + AND data_type = 'character varying' + ) THEN + ALTER TABLE "home_page_hero_slides" + ALTER COLUMN "subtitle" DROP DEFAULT, + ALTER COLUMN "subtitle" TYPE jsonb USING NULL; + END IF; + END $$; + `) + + // ── home_page_faq_items: answer (created after next drizzle push) ───────── + await db.execute(sql` + DO $$ + BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.tables WHERE table_name = 'home_page_faq_items' + ) AND EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'home_page_faq_items' + AND column_name = 'answer' + AND data_type = 'character varying' + ) THEN + ALTER TABLE "home_page_faq_items" + ALTER COLUMN "answer" DROP DEFAULT, + ALTER COLUMN "answer" TYPE jsonb USING NULL; + END IF; + END $$; + `) +} + +export async function down({ db }: MigrateDownArgs): Promise { + // Revert all jsonb columns back to varchar (data will be lost/NULL) + await db.execute(sql` + ALTER TABLE "group_visits_page" ALTER COLUMN "price_description" TYPE varchar USING NULL; + ALTER TABLE "_group_visits_page_v" ALTER COLUMN "version_price_description" TYPE varchar USING NULL; + ALTER TABLE "dinosaur_page" ALTER COLUMN "hero_description" TYPE varchar USING NULL; + ALTER TABLE "dinosaur_page" ALTER COLUMN "activities_description" TYPE varchar USING NULL; + ALTER TABLE "dyvolis_page" ALTER COLUMN "hero_description" TYPE varchar USING NULL; + ALTER TABLE "tickets_page" ALTER COLUMN "benefits_footnote" TYPE varchar USING NULL; + ALTER TABLE "_tickets_page_v" ALTER COLUMN "version_benefits_footnote" TYPE varchar USING NULL; + ALTER TABLE "home_page" ALTER COLUMN "hero_subtitle" TYPE varchar USING NULL; + ALTER TABLE "home_page" ALTER COLUMN "birthday_intro_text" TYPE varchar USING NULL; + ALTER TABLE "home_page" ALTER COLUMN "news_subtitle" TYPE varchar USING NULL; + ALTER TABLE "_home_page_v" ALTER COLUMN "version_hero_subtitle" TYPE varchar USING NULL; + ALTER TABLE "_home_page_v" ALTER COLUMN "version_birthday_intro_text" TYPE varchar USING NULL; + ALTER TABLE "_home_page_v" ALTER COLUMN "version_news_subtitle" TYPE varchar USING NULL; + ALTER TABLE "home_page_why_parents_items" ALTER COLUMN "description" TYPE varchar USING NULL; + ALTER TABLE "_home_page_v_version_why_parents_items" ALTER COLUMN "description" TYPE varchar USING NULL; + ALTER TABLE "dinosaur_page_activities" ALTER COLUMN "description" TYPE varchar USING NULL; + ALTER TABLE "dinosaur_page_why_visit_items" ALTER COLUMN "description" TYPE varchar USING NULL; + ALTER TABLE "birthday_page_package_items" ALTER COLUMN "description" TYPE varchar USING NULL; + ALTER TABLE "_birthday_page_v_version_package_items" ALTER COLUMN "description" TYPE varchar USING NULL; + ALTER TABLE "birthday_page_why_items" ALTER COLUMN "description" TYPE varchar USING NULL; + ALTER TABLE "_birthday_page_v_version_why_items" ALTER COLUMN "description" TYPE varchar USING NULL; + ALTER TABLE "dyvolis_page_why_visit_items" ALTER COLUMN "description" TYPE varchar USING NULL; + ALTER TABLE "tickets_page_combo_cards" ALTER COLUMN "description" TYPE varchar USING NULL; + ALTER TABLE "_tickets_page_v_version_combo_cards" ALTER COLUMN "description" TYPE varchar USING NULL; + ALTER TABLE "locations" ALTER COLUMN "short_desc" TYPE varchar USING NULL; + ALTER TABLE "locations_why_visit_items" ALTER COLUMN "description" TYPE varchar USING NULL; + `) +} diff --git a/migrations/index.ts b/migrations/index.ts index 2b61c1a..51fac17 100644 --- a/migrations/index.ts +++ b/migrations/index.ts @@ -4,6 +4,7 @@ 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' +import * as migration_20260611_160000 from './20260611_160000' export const migrations = [ { @@ -36,4 +37,9 @@ export const migrations = [ down: migration_20260611_140000.down, name: '20260611_140000', }, + { + up: migration_20260611_160000.up, + down: migration_20260611_160000.down, + name: '20260611_160000', + }, ] diff --git a/src/app/(frontend)/dni-narodzhennia/page.tsx b/src/app/(frontend)/dni-narodzhennia/page.tsx index 1df8f98..124498b 100644 --- a/src/app/(frontend)/dni-narodzhennia/page.tsx +++ b/src/app/(frontend)/dni-narodzhennia/page.tsx @@ -1,6 +1,8 @@ import type { Metadata } from 'next' +import React from 'react' import { getPayload } from 'payload' import configPromise from '@payload-config' +import { RichText } from '@payloadcms/richtext-lexical/react' import { BirthdayBookingForm } from '@/components/forms/BirthdayBookingForm' import { FormBlock, type FormData as FormBlockData } from '@/components/forms/FormBlock' import { RefreshRouteOnSave } from '@/components/cms/RefreshRouteOnSave' @@ -9,6 +11,8 @@ import { JsonLd } from '@/components/seo/JsonLd' import { birthdayPageJsonLd } from '@/lib/structuredData' import type { Media } from '@/payload-types' +type RichTextData = Parameters[0]['data'] + export const revalidate = 60 const FONT_MONT = { fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' } @@ -99,7 +103,7 @@ export async function generateMetadata(): Promise { interface PackageCardData { title: string - description: string + description: React.ReactNode imageUrl: string | null ctaLabel: string ctaHref?: string @@ -154,9 +158,12 @@ function PackageCard({ item }: { item: PackageCardData }) { > {item.title} -

+

{item.description} -

+
): PackageCardData => ({ title: item.title as string, - description: item.description as string, + description: item.description ? ( + typeof item.description === 'string' ? ( + item.description + ) : ( + + ) + ) : ( + '' + ), imageUrl: typeof item.image === 'string' ? item.image : mediaUrl(item.image as Media | undefined), ctaLabel: (item.ctaLabel as string) ?? 'Замовити', @@ -223,8 +238,21 @@ export default async function BirthdayPage() { : FALLBACK_EXTRA_ITEMS.map(toCard) const whyTitle = (d?.whyTitle as string) ?? 'Чому варто святкувати у Шуміленді' - const whyItems = Array.isArray(d?.whyItems) - ? (d?.whyItems as typeof FALLBACK_WHY_ITEMS) + type WhyRawItem = { title: string; description?: RichTextData | string | null } + const rawWhyItems = Array.isArray(d?.whyItems) ? (d?.whyItems as WhyRawItem[]) : null + const whyItems = rawWhyItems + ? rawWhyItems.map((i) => ({ + title: i.title, + description: i.description ? ( + typeof i.description === 'string' ? ( + i.description + ) : ( + + ) + ) : ( + '' + ), + })) : FALLBACK_WHY_ITEMS const whyVideos = Array.isArray(d?.whyVideos) ? (d?.whyVideos as Array>) diff --git a/src/app/(frontend)/grupovi-vidviduvannia/page.tsx b/src/app/(frontend)/grupovi-vidviduvannia/page.tsx index 198cc63..dbccc4f 100644 --- a/src/app/(frontend)/grupovi-vidviduvannia/page.tsx +++ b/src/app/(frontend)/grupovi-vidviduvannia/page.tsx @@ -1,4 +1,5 @@ import type { Metadata } from 'next' +import React from 'react' import { getPayload } from 'payload' import configPromise from '@payload-config' import { RichText } from '@payloadcms/richtext-lexical/react' @@ -139,9 +140,16 @@ export default async function GroupVisitsPage() { (d?.priceNote as string) ?? 'Вхід для двох дорослих, що супроводжують дітей, безкоштовний.' const priceMinPeople = (d?.priceMinPeople as string) ?? 'Пропозиція діє для груп від 10 людей' const priceCta = (d?.priceCta as string) ?? 'Купити квиток' - const priceDescription = - (d?.priceDescription as string) ?? - 'У вартість входить відвідування ДиноПарку та ДивоЛісу.\nЧас перебування на локаціях необмежений.' + const priceDescriptionRaw = d?.priceDescription ?? null + const priceDescription = priceDescriptionRaw ? ( + + ) : ( + + { + 'У вартість входить відвідування ДиноПарку та ДивоЛісу.\nЧас перебування на локаціях необмежений.' + } + + ) const bottomText = (d?.bottomText ?? null) as RichTextData | null const bottomImages: string[] = ((d?.bottomImages as { image: unknown }[]) ?? []) diff --git a/src/app/(frontend)/legal/LegalPage.tsx b/src/app/(frontend)/legal/LegalPage.tsx index 8aa7eb2..66f33c2 100644 --- a/src/app/(frontend)/legal/LegalPage.tsx +++ b/src/app/(frontend)/legal/LegalPage.tsx @@ -1,11 +1,17 @@ +import { RichText } from '@payloadcms/richtext-lexical/react' + const FONT = { fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' } +type RichTextData = Parameters[0]['data'] + interface Props { title: string - content: string + content: string | RichTextData | null | undefined } export function LegalPageLayout({ title, content }: Props) { + const isRichText = content !== null && content !== undefined && typeof content === 'object' + return (
@@ -17,10 +23,14 @@ export function LegalPageLayout({ title, content }: Props) {
- {content} + {isRichText ? ( + + ) : ( + {content as string} + )}
diff --git a/src/app/(frontend)/lokatsii/[slug]/page.tsx b/src/app/(frontend)/lokatsii/[slug]/page.tsx index af912ad..bc0afef 100644 --- a/src/app/(frontend)/lokatsii/[slug]/page.tsx +++ b/src/app/(frontend)/lokatsii/[slug]/page.tsx @@ -2,6 +2,7 @@ import type { Metadata } from 'next' import { notFound } from 'next/navigation' import { getPayload } from 'payload' import configPromise from '@payload-config' +import { RichText } from '@payloadcms/richtext-lexical/react' import { DyvoLisHero } from '@/components/sections/DyvoLisHero' import { DyvoLisGallery } from '@/components/sections/DyvoLisGallery' import { DyvoLisWhyVisit } from '@/components/sections/DyvoLisWhyVisit' @@ -9,6 +10,21 @@ import { DyvoLisTickets } from '@/components/sections/DyvoLisTickets' import { RefreshRouteOnSave } from '@/components/cms/RefreshRouteOnSave' import type { Location, Media } from '@/payload-types' +type RichTextData = Parameters[0]['data'] + +function richTextToPlain(rt: RichTextData | null | undefined): string { + if (!rt) return '' + type LexNode = { text?: string; children?: LexNode[] } + const walk = (n: LexNode): string => { + if (typeof n.text === 'string') return n.text + if (Array.isArray(n.children)) return n.children.map(walk).join('') + return '' + } + const root = (rt as { root?: { children?: LexNode[] } }).root + if (!root?.children) return '' + return root.children.map(walk).join(' ').trim() +} + interface Props { params: Promise<{ slug: string }> } @@ -48,7 +64,10 @@ export async function generateMetadata({ params }: Props): Promise { if (!location || !location.showDetailPage) return { title: 'Не знайдено — Шуміленд' } return { title: location.meta?.title ?? `${location.name} — Шуміленд`, - description: location.meta?.description ?? location.shortDesc ?? '', + description: + location.meta?.description ?? + richTextToPlain(location.shortDesc as RichTextData | null) ?? + '', } } @@ -76,7 +95,11 @@ export default async function LocationDetailPage({ params }: Props) { const whyVisitItems = location.whyVisitItems?.map((item) => ({ title: item.title, - description: item.description, + description: item.description ? ( + + ) : ( + '' + ), })) const reviewVideos = location.reviewVideos?.map((v) => ({ @@ -89,7 +112,11 @@ export default async function LocationDetailPage({ params }: Props) {
+ ) : undefined + } stat={location.heroStat ?? undefined} statLabel={location.heroStatLabel ?? undefined} tips={heroTips} diff --git a/src/app/(frontend)/lokatsii/dynozavry/page.tsx b/src/app/(frontend)/lokatsii/dynozavry/page.tsx index 0ba268e..76261ea 100644 --- a/src/app/(frontend)/lokatsii/dynozavry/page.tsx +++ b/src/app/(frontend)/lokatsii/dynozavry/page.tsx @@ -1,15 +1,31 @@ import type { Metadata } from 'next' import { getPayload } from 'payload' import configPromise from '@payload-config' +import { RichText } from '@payloadcms/richtext-lexical/react' import { DinoPageContent } from '@/components/sections/DinoPageContent' import { RefreshRouteOnSave } from '@/components/cms/RefreshRouteOnSave' import type { Media, Location } from '@/payload-types' +type RichTextData = Parameters[0]['data'] + +function richTextToPlain(rt: RichTextData | null | undefined): string { + if (!rt) return '' + type LexNode = { text?: string; children?: LexNode[] } + const walk = (n: LexNode): string => { + if (typeof n.text === 'string') return n.text + if (Array.isArray(n.children)) return n.children.map(walk).join('') + return '' + } + const root = (rt as { root?: { children?: LexNode[] } }).root + if (!root?.children) return '' + return root.children.map(walk).join(' ').trim() +} + export const revalidate = 60 interface DinoPageData { heroTitle?: string - heroDescription?: string + heroDescription?: RichTextData | null heroStat?: string heroStatLabel?: string heroImage?: number | Media | null @@ -25,17 +41,17 @@ interface DinoPageData { }[] galleryImages?: { image?: number | Media | null; id?: string }[] activitiesTitle?: string - activitiesDescription?: string + activitiesDescription?: RichTextData | null activities?: { name: string price?: string | null - description?: string | null + description?: RichTextData | null image?: number | Media | null href?: string | null id?: string }[] whyVisitTitle?: string - whyVisitItems?: { title: string; description: string; id?: string }[] + whyVisitItems?: { title: string; description: RichTextData | null; id?: string }[] reviewVideos?: { src: string; poster?: string | null; label?: string | null; id?: string }[] workingHours?: string comboDescription?: string @@ -54,7 +70,8 @@ export async function generateMetadata(): Promise { const data = raw as unknown as DinoPageData return { title: data.meta?.title ?? 'Динопарк — Шуміленд', - description: data.meta?.description ?? data.heroDescription ?? 'Динопарк у Шуміленді', + description: + data.meta?.description ?? (richTextToPlain(data.heroDescription) || 'Динопарк у Шуміленді'), } } catch { return { title: 'Динопарк — Шуміленд' } @@ -104,14 +121,14 @@ export default async function DinosaurPage() { const activities = data.activities?.map((a) => ({ name: a.name, price: a.price ?? null, - description: a.description ?? null, + description: a.description ? : null, imageUrl: mediaUrl(a.image), href: a.href ?? '#tickets', })) const whyVisitItems = data.whyVisitItems?.map((w) => ({ title: w.title, - description: w.description, + description: w.description ? : '', })) const reviewVideos = data.reviewVideos?.map((v) => ({ @@ -123,7 +140,9 @@ export default async function DinosaurPage() { <> : undefined + } stat={data.heroStat} statLabel={data.heroStatLabel} features={features && features.length > 0 ? features : undefined} @@ -132,7 +151,9 @@ export default async function DinosaurPage() { dinos={dinos && dinos.length >= 12 ? dinos : undefined} galleryImages={galleryImages && galleryImages.length >= 4 ? galleryImages : undefined} activitiesTitle={data.activitiesTitle} - activitiesDescription={data.activitiesDescription} + activitiesDescription={ + data.activitiesDescription ? : undefined + } activities={activities && activities.length > 0 ? activities : undefined} whyVisitTitle={data.whyVisitTitle} whyVisitItems={whyVisitItems && whyVisitItems.length > 0 ? whyVisitItems : undefined} diff --git a/src/app/(frontend)/page.tsx b/src/app/(frontend)/page.tsx index 2e2ab85..d9d4a03 100644 --- a/src/app/(frontend)/page.tsx +++ b/src/app/(frontend)/page.tsx @@ -1,3 +1,5 @@ +import React from 'react' +import { RichText } from '@payloadcms/richtext-lexical/react' import { Hero } from '@/components/sections/Hero' import { Locations } from '@/components/sections/Locations' import { WhyParents } from '@/components/sections/WhyParents' @@ -14,6 +16,8 @@ import { JsonLd } from '@/components/seo/JsonLd' import { homePageJsonLd } from '@/lib/structuredData' import type { HomePageHero, HomePageSectionType } from '@/types/globals' +type RichTextData = Parameters[0]['data'] + const DEFAULT_SECTION_ORDER: HomePageSectionType[] = [ 'locations', 'whyParents', @@ -56,15 +60,28 @@ export default async function HomePage() { title={home?.sectionTitles?.locations ?? undefined} /> ) - case 'whyParents': + case 'whyParents': { + const rawWhyParentsItems = home?.whyParents?.items + const whyParentsItems = rawWhyParentsItems + ? rawWhyParentsItems.map((item) => ({ + ...item, + description: + item.description && typeof item.description !== 'string' ? ( + + ) : ( + item.description + ), + })) + : undefined return ( ) + } case 'video': return ( + ) : ( + ((birthdayIntroText as string | undefined) ?? undefined) + ) return ( 0 ? birthdayPackages : undefined} title={home?.sectionTitles?.birthday ?? undefined} - intro={home?.birthdayIntro?.text ?? undefined} + intro={birthdayIntro} patternGreen={pg && typeof pg === 'object' ? (pg.url ?? undefined) : (pg ?? undefined)} patternOrange={po && typeof po === 'object' ? (po.url ?? undefined) : (po ?? undefined)} /> @@ -104,23 +128,38 @@ export default async function HomePage() { googleReviewUrl={home?.sectionTitles?.googleReviewUrl} /> ) - case 'faq': - return ( - - ) - case 'news': + case 'faq': { + const rawFaqItems = home?.faq?.items + const faqItems = rawFaqItems + ? rawFaqItems.map((item) => ({ + ...item, + answer: + item.answer && typeof item.answer !== 'string' ? ( + + ) : ( + item.answer + ), + })) + : undefined + return + } + case 'news': { + const newsSubtitleRaw = home?.news?.subtitle + const newsSubtitle = + newsSubtitleRaw && typeof newsSubtitleRaw !== 'string' ? ( + + ) : ( + ((newsSubtitleRaw as string | undefined) ?? undefined) + ) return ( ) + } case 'map': return ( [0]['data'] + export const revalidate = 60 const FONT_MONT = { fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' } @@ -209,6 +212,19 @@ const COMBO_CARDS_STATIC: ComboCardData[] = [ const parsePrice = (s: string): number => Number(String(s).replace(/[^\d]/g, '')) || 0 +type LexicalNode = { text?: string; children?: LexicalNode[] } +function richTextToPlain(rt: RichTextData | null | undefined): string { + if (!rt) return '' + const walk = (n: LexicalNode): string => { + if (typeof n.text === 'string') return n.text + if (Array.isArray(n.children)) return n.children.map(walk).join('') + return '' + } + const root = (rt as { root?: { children?: LexicalNode[] } }).root + if (!root?.children) return '' + return root.children.map(walk).join(' ').trim() +} + function buildComboCards(pageData: TicketsPage | null, tariffs: Tariff[]): ComboCardData[] { const apiCombos = tariffs .filter((t) => t.categoryTag === 'combo') @@ -248,7 +264,7 @@ function buildComboCards(pageData: TicketsPage | null, tariffs: Tariff[]): Combo name: c.name, subtitle: c.subtitle ?? null, price: c.price, - description: c.description ?? '', + description: richTextToPlain(c.description as RichTextData | null | undefined), featured: !!c.featured, badge: c.badge ?? null, locations: (c.locations ?? []).map((l) => l.text).filter(Boolean) as string[], @@ -385,13 +401,16 @@ function BenefitsSection({ pageData }: { pageData: TicketsPage | null }) { {/* Footnote */}
-

- {pageData?.benefitsFootnote ?? - '*Знижки та пільги поширюються виключно на індивідуальне відвідування 3 основних локацій (Динопарк, Зона топіарних фігур, Дзеркальний лабіринт) та не сумуються з тарифами категорії «КОМБО».'} -

+ {pageData?.benefitsFootnote ? ( + + ) : ( + '*Знижки та пільги поширюються виключно на індивідуальне відвідування 3 основних локацій (Динопарк, Зона топіарних фігур, Дзеркальний лабіринт) та не сумуються з тарифами категорії «КОМБО».' + )} +
diff --git a/src/collections/Locations.ts b/src/collections/Locations.ts index b32eb61..613130a 100644 --- a/src/collections/Locations.ts +++ b/src/collections/Locations.ts @@ -35,7 +35,7 @@ export const Locations: CollectionConfig = { { name: 'name', type: 'text', required: true }, { name: 'slug', type: 'text', required: true, unique: true }, { name: 'tagline', type: 'text' }, - { name: 'shortDesc', type: 'textarea' }, + { name: 'shortDesc', type: 'richText', editor: standardEditor }, { name: 'description', type: 'richText', editor: standardEditor }, { name: 'image', type: 'upload', relationTo: 'media' }, { @@ -91,7 +91,7 @@ export const Locations: CollectionConfig = { label: 'Чому варто відвідати', fields: [ { name: 'title', type: 'text', required: true }, - { name: 'description', type: 'textarea', required: true }, + { name: 'description', type: 'richText', editor: standardEditor, required: true }, ], }, { diff --git a/src/components/sections/BirthdayPricing.tsx b/src/components/sections/BirthdayPricing.tsx index b2c0579..6860f9a 100644 --- a/src/components/sections/BirthdayPricing.tsx +++ b/src/components/sections/BirthdayPricing.tsx @@ -1,4 +1,5 @@ /* eslint-disable @next/next/no-img-element */ +import React from 'react' import Link from 'next/link' import type { BirthdayPackageCMS } from '@/types/globals' @@ -9,7 +10,7 @@ const DEFAULT_PATTERN_ORANGE = '/images/figma/card-pattern-dark.webp' interface BirthdayPricingProps { packages?: BirthdayPackageCMS[] title?: string - intro?: string + intro?: React.ReactNode patternGreen?: string patternOrange?: string } diff --git a/src/components/sections/DinoPageContent.tsx b/src/components/sections/DinoPageContent.tsx index 1ca67d7..b019ffe 100644 --- a/src/components/sections/DinoPageContent.tsx +++ b/src/components/sections/DinoPageContent.tsx @@ -1,7 +1,7 @@ 'use client' /* eslint-disable @next/next/no-img-element */ -import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useCart } from '@/context/CartContext' import { ImageLightbox } from '@/components/ui/ImageLightbox' @@ -18,14 +18,14 @@ interface DinoSpec { interface ActivityItem { name: string price?: string | null - description?: string | null + description?: React.ReactNode imageUrl?: string | null href?: string | null } interface WhyVisitItem { title: string - description: string + description: React.ReactNode } interface VideoItem { @@ -45,7 +45,7 @@ interface Tariff { export interface DinoPageContentProps { heroTitle?: string - heroDescription?: string + heroDescription?: React.ReactNode stat?: string statLabel?: string features?: string[] @@ -54,7 +54,7 @@ export interface DinoPageContentProps { dinos?: DinoSpec[] galleryImages?: string[] activitiesTitle?: string - activitiesDescription?: string + activitiesDescription?: React.ReactNode activities?: ActivityItem[] whyVisitTitle?: string whyVisitItems?: WhyVisitItem[] diff --git a/src/components/sections/DyvoLisHero.tsx b/src/components/sections/DyvoLisHero.tsx index 35c998a..3690554 100644 --- a/src/components/sections/DyvoLisHero.tsx +++ b/src/components/sections/DyvoLisHero.tsx @@ -1,4 +1,5 @@ /* eslint-disable @next/next/no-img-element */ +import React from 'react' import { BtnPrimary } from '@/components/ui/BtnPrimary' const HERO_IMG = '/images/dyvolis/hero-cat.webp' @@ -11,7 +12,7 @@ const DEFAULT_TIPS = [ interface DyvoLisHeroProps { title?: string - description?: string + description?: React.ReactNode stat?: string statLabel?: string tips?: string[] diff --git a/src/components/sections/DyvoLisWhyVisit.tsx b/src/components/sections/DyvoLisWhyVisit.tsx index 74b6ccd..94ca148 100644 --- a/src/components/sections/DyvoLisWhyVisit.tsx +++ b/src/components/sections/DyvoLisWhyVisit.tsx @@ -1,6 +1,6 @@ 'use client' -import { useState, useRef, useEffect } from 'react' +import React, { useState, useRef, useEffect } from 'react' const FONT_MONT = { fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' } const IMG_WAVE_TILE = '/images/figma/wave-tile.svg' @@ -41,7 +41,7 @@ interface ReviewVideo { interface DyvoLisWhyVisitProps { title?: string - items?: Array<{ title: string; description: string }> + items?: Array<{ title: string; description: React.ReactNode }> reviewVideos?: ReviewVideo[] } diff --git a/src/components/sections/FAQ.tsx b/src/components/sections/FAQ.tsx index dfd413e..b2cc311 100644 --- a/src/components/sections/FAQ.tsx +++ b/src/components/sections/FAQ.tsx @@ -1,6 +1,6 @@ 'use client' -import { useState } from 'react' +import React, { useState } from 'react' import type { HomePageFaqItem } from '@/types/globals' const STATIC_ITEMS: HomePageFaqItem[] = [ diff --git a/src/components/sections/Hero.tsx b/src/components/sections/Hero.tsx index 0474951..21cccc7 100644 --- a/src/components/sections/Hero.tsx +++ b/src/components/sections/Hero.tsx @@ -1,8 +1,11 @@ import { getPayload } from 'payload' import configPromise from '@payload-config' +import { RichText } from '@payloadcms/richtext-lexical/react' import { HeroSlider } from './HeroSlider' import type { SlideData } from './HeroSlider' +type RichTextData = Parameters[0]['data'] + interface HeroProps { hero?: unknown } @@ -17,7 +20,7 @@ async function getHeroSlides(): Promise { backgroundImageUrl?: string | null type?: string | null title?: string | null - subtitle?: string | null + subtitle?: unknown ctaLabel?: string | null ctaHref?: string | null }> @@ -30,7 +33,12 @@ async function getHeroSlides(): Promise { img: s.backgroundImage?.url ?? s.backgroundImageUrl ?? '', type: (s.type === 'brand' ? 'brand' : 'location') as 'brand' | 'location', title: s.title ?? '', - subtitle: s.subtitle ?? '', + subtitle: + s.subtitle && typeof s.subtitle !== 'string' ? ( + + ) : ( + ((s.subtitle as string) ?? '') + ), ctaLabel: s.ctaLabel ?? 'Купити квиток', ctaHref: s.ctaHref ?? '/payments', })) diff --git a/src/components/sections/HeroSlider.tsx b/src/components/sections/HeroSlider.tsx index 737dac7..78f97fb 100644 --- a/src/components/sections/HeroSlider.tsx +++ b/src/components/sections/HeroSlider.tsx @@ -1,6 +1,6 @@ 'use client' -import { useState, useEffect, useCallback, useRef } from 'react' +import React, { useState, useEffect, useCallback, useRef } from 'react' import { BtnPrimary } from '@/components/ui/BtnPrimary' type SlideType = 'brand' | 'location' @@ -9,7 +9,7 @@ export interface SlideData { img: string type: SlideType title: string - subtitle: string + subtitle: React.ReactNode ctaLabel: string ctaHref: string } diff --git a/src/components/sections/News.tsx b/src/components/sections/News.tsx index 278b675..ad2b86f 100644 --- a/src/components/sections/News.tsx +++ b/src/components/sections/News.tsx @@ -1,4 +1,5 @@ /* eslint-disable @next/next/no-img-element */ +import React from 'react' import { getPayload } from 'payload' import configPromise from '@payload-config' import { BtnDetails } from '@/components/ui/BtnDetails' @@ -65,7 +66,7 @@ async function getLatestPosts(limit = 3): Promise { interface NewsProps { title?: string | null - subtitle?: string | null + subtitle?: React.ReactNode limit?: number | null } diff --git a/src/globals/BirthdayPage.ts b/src/globals/BirthdayPage.ts index a987677..7d56562 100644 --- a/src/globals/BirthdayPage.ts +++ b/src/globals/BirthdayPage.ts @@ -1,4 +1,5 @@ import type { GlobalConfig } from 'payload' +import { standardEditor } from '@/fields/richText' import { isAdminOrEditor } from '@/access/isAdminOrEditor' import { revalidateGlobalAfterChange } from '@/hooks/revalidatePath' @@ -40,7 +41,7 @@ export const BirthdayPage: GlobalConfig = { label: 'Що входить у пакет (картки з фото)', fields: [ { name: 'title', type: 'text', required: true }, - { name: 'description', type: 'textarea', required: true }, + { name: 'description', type: 'richText', editor: standardEditor, required: true }, { name: 'image', type: 'upload', @@ -51,36 +52,12 @@ export const BirthdayPage: GlobalConfig = { { name: 'ctaHref', type: 'text', admin: { description: 'Посилання кнопки (опційно)' } }, ], defaultValue: [ - { - title: 'ДиноПарк', - description: 'Справжні динозаври в натуральну величину', - ctaLabel: 'Замовити', - }, - { - title: 'ДивоЛіс', - description: 'Казкові топіарні фігури улюблених персонажів', - ctaLabel: 'Замовити', - }, - { - title: 'Дзеркальний Лабіринт', - description: 'Весела гра для дітей та дорослих', - ctaLabel: 'Замовити', - }, - { - title: 'Костюмованих ведучих', - description: 'Аніматори в яскравих костюмах проведуть свято', - ctaLabel: 'Замовити', - }, - { - title: 'Аквагрим', - description: 'Конкурси, ігри та розваги для всіх гостей', - ctaLabel: 'Замовити', - }, - { - title: 'Затишну альтанку', - description: 'Власна зона відпочинку для вашої родини', - ctaLabel: 'Замовити', - }, + { title: 'ДиноПарк', ctaLabel: 'Замовити' }, + { title: 'ДивоЛіс', ctaLabel: 'Замовити' }, + { title: 'Дзеркальний Лабіринт', ctaLabel: 'Замовити' }, + { title: 'Костюмованих ведучих', ctaLabel: 'Замовити' }, + { title: 'Аквагрим', ctaLabel: 'Замовити' }, + { title: 'Затишну альтанку', ctaLabel: 'Замовити' }, ], }, // Why section @@ -95,24 +72,12 @@ export const BirthdayPage: GlobalConfig = { label: 'Переваги (акордеон)', fields: [ { name: 'title', type: 'text', required: true }, - { name: 'description', type: 'textarea', required: true }, + { name: 'description', type: 'richText', editor: standardEditor, required: true }, ], defaultValue: [ - { - title: 'Свято під ключ', - description: - 'Ми беремо на себе всі деталі: аніматорів, конкурси, прикраси та окрему зону для вашої родини. Вам залишається лише насолоджуватись.', - }, - { - title: 'Простір для дітей і дорослих', - description: - 'Шуміленд — це 7 локацій, де кожен знайде щось для себе: від динозаврів до казкових лісів, від лабіринтів до фотозон.', - }, - { - title: 'Незабутні фото та спогади', - description: - 'Унікальні декорації, яскраві персонажі та щира радість дітей — ідеальний фон для фотографій, які хочеться переглядати знову і знову.', - }, + { title: 'Свято під ключ' }, + { title: 'Простір для дітей і дорослих' }, + { title: 'Незабутні фото та спогади' }, ], }, { diff --git a/src/globals/DinosaurPage.ts b/src/globals/DinosaurPage.ts index 57da0f9..f68a8e8 100644 --- a/src/globals/DinosaurPage.ts +++ b/src/globals/DinosaurPage.ts @@ -1,4 +1,5 @@ import type { GlobalConfig } from 'payload' +import { standardEditor } from '@/fields/richText' import { isAdminOrEditor } from '@/access/isAdminOrEditor' import { revalidateGlobalAfterChange } from '@/hooks/revalidatePath' @@ -17,9 +18,8 @@ export const DinosaurPage: GlobalConfig = { }, { name: 'heroDescription', - type: 'textarea', - defaultValue: - 'Великі динозаври, що рухаються та гарчать, справжнє роздоволлє, цікаві екскурсії та динородео — тут є все, щоб ваша дитина не нудьгувала.', + type: 'richText', + editor: standardEditor, }, { name: 'heroStat', @@ -99,9 +99,8 @@ export const DinosaurPage: GlobalConfig = { }, { name: 'activitiesDescription', - type: 'textarea', - defaultValue: - 'Хочете дізнатись ще більше про динозаврів? Замовте екскурсію з гідом, поринь у світ палеонтологічних розкопок або підкорюй справжнього динозавра!', + type: 'richText', + editor: standardEditor, }, { name: 'activities', @@ -110,7 +109,7 @@ export const DinosaurPage: GlobalConfig = { fields: [ { name: 'name', type: 'text', required: true }, { name: 'price', type: 'text', admin: { description: 'Наприклад: 150 грн' } }, - { name: 'description', type: 'textarea' }, + { name: 'description', type: 'richText', editor: standardEditor }, { name: 'image', type: 'upload', relationTo: 'media' }, { name: 'href', type: 'text', defaultValue: '#tickets' }, ], @@ -131,24 +130,12 @@ export const DinosaurPage: GlobalConfig = { type: 'array', fields: [ { name: 'title', type: 'text', required: true }, - { name: 'description', type: 'textarea', required: true }, + { name: 'description', type: 'richText', editor: standardEditor, required: true }, ], defaultValue: [ - { - title: 'Навчання через гру', - description: - 'Дітки дізнаються про стародавніх тварин через захопливі ігри та інтерактивні вправи з гідом.', - }, - { - title: 'Дитячі очі, що палають захватом', - description: - 'Реалістичні рухи та звуки динозаврів створюють ефект повного занурення — дитина точно не забуде цього дня.', - }, - { - title: 'Неймовірні фотографії', - description: - 'Сфотографуйтесь поруч із улюбленим динозавром або зробіть фото з екскурсоводом — тепла згадка для всієї родини.', - }, + { title: 'Навчання через гру' }, + { title: 'Дитячі очі, що палають захватом' }, + { title: 'Неймовірні фотографії' }, ], }, { diff --git a/src/globals/DyvoLisPage.ts b/src/globals/DyvoLisPage.ts index 6941798..b4b9d33 100644 --- a/src/globals/DyvoLisPage.ts +++ b/src/globals/DyvoLisPage.ts @@ -1,4 +1,5 @@ import type { GlobalConfig } from 'payload' +import { standardEditor } from '@/fields/richText' import { isAdminOrEditor } from '@/access/isAdminOrEditor' import { revalidateGlobalAfterChange } from '@/hooks/revalidatePath' @@ -23,9 +24,8 @@ export const DyvoLisPage: GlobalConfig = { }, { name: 'heroDescription', - type: 'textarea', - defaultValue: - 'Топіарні фігури зроблені з урахуванням важливих деталей, тому ви одразу впізнаєте в них улюблених казкових героїв. Тут можна бігати, стрибати, лазити по фігурках і ставати героями власної казки.', + type: 'richText', + editor: standardEditor, }, { name: 'heroStat', @@ -79,24 +79,12 @@ export const DyvoLisPage: GlobalConfig = { type: 'array', fields: [ { name: 'title', type: 'text', required: true }, - { name: 'description', type: 'textarea', required: true }, + { name: 'description', type: 'richText', editor: standardEditor, required: true }, ], defaultValue: [ - { - title: 'Простір для спільної фантазії', - description: - 'Вигадуйте казки та пригоди разом із дітьми — кожна топіарна фігурка стає новою сторінкою вашої власної чарівної історії.', - }, - { - title: 'Казковий ліс у справжньому лісі', - description: - 'Ми створили локацію, в якій гармонійно поєднуються казкові фігури та жива природа. Прогулянка лісом ще не була такою захопливою.', - }, - { - title: 'Магічні кадри для сімейного альбому', - description: - 'Унікальні топіарні декорації та яскраві персонажі — ідеальний фон для незабутніх сімейних фотографій, які захочеться переглядати знову і знову.', - }, + { title: 'Простір для спільної фантазії' }, + { title: 'Казковий ліс у справжньому лісі' }, + { title: 'Магічні кадри для сімейного альбому' }, ], }, // Video reviews carousel (right column of "Why visit" section) diff --git a/src/globals/GroupVisitsPage.ts b/src/globals/GroupVisitsPage.ts index 0ad79d7..79cd162 100644 --- a/src/globals/GroupVisitsPage.ts +++ b/src/globals/GroupVisitsPage.ts @@ -171,10 +171,9 @@ export const GroupVisitsPage: GlobalConfig = { }, { name: 'priceDescription', - type: 'textarea', + type: 'richText', + editor: standardEditor, label: 'Текст під ціновим блоком', - defaultValue: - 'У вартість входить відвідування Динопарку та ДивоЛісу.\nЧас перебування на локаціях необмежений.', }, // Bottom section { diff --git a/src/globals/HomePage.ts b/src/globals/HomePage.ts index 51ec89c..8f4052a 100644 --- a/src/globals/HomePage.ts +++ b/src/globals/HomePage.ts @@ -1,4 +1,5 @@ import type { GlobalConfig } from 'payload' +import { standardEditor } from '@/fields/richText' import { isAdminOrEditor } from '@/access/isAdminOrEditor' import { revalidateGlobalAfterChange } from '@/hooks/revalidatePath' @@ -45,7 +46,12 @@ export const HomePage: GlobalConfig = { ], }, { name: 'title', type: 'text', required: true, label: 'Заголовок' }, - { name: 'subtitle', type: 'textarea', label: 'Підзаголовок / опис' }, + { + name: 'subtitle', + type: 'richText', + editor: standardEditor, + label: 'Підзаголовок / опис', + }, { name: 'ctaLabel', type: 'text', defaultValue: 'Купити квиток', label: 'Кнопка' }, { name: 'ctaHref', type: 'text', defaultValue: '/payments', label: 'Посилання кнопки' }, ], @@ -55,7 +61,7 @@ export const HomePage: GlobalConfig = { type: 'group', fields: [ { name: 'title', type: 'text' }, - { name: 'subtitle', type: 'textarea' }, + { name: 'subtitle', type: 'richText', editor: standardEditor }, { name: 'ctaLabel', type: 'text' }, { name: 'ctaHref', type: 'text' }, { name: 'backgroundVideo', type: 'text' }, @@ -154,7 +160,7 @@ export const HomePage: GlobalConfig = { type: 'array', fields: [ { name: 'title', type: 'text' }, - { name: 'description', type: 'textarea' }, + { name: 'description', type: 'richText', editor: standardEditor }, ], }, { @@ -193,7 +199,7 @@ export const HomePage: GlobalConfig = { name: 'birthdayIntro', type: 'group', fields: [ - { name: 'text', type: 'textarea' }, + { name: 'text', type: 'richText', editor: standardEditor }, { name: 'patternGreen', type: 'upload', @@ -217,7 +223,8 @@ export const HomePage: GlobalConfig = { { name: 'title', type: 'text' }, { name: 'subtitle', - type: 'textarea', + type: 'richText', + editor: standardEditor, admin: { description: 'Підзаголовок секції новин на головній.' }, }, { name: 'limit', type: 'number', defaultValue: 3, min: 1, max: 12 }, @@ -240,7 +247,13 @@ export const HomePage: GlobalConfig = { label: 'Запитання та відповіді', fields: [ { name: 'question', type: 'text', required: true, label: 'Запитання' }, - { name: 'answer', type: 'textarea', required: true, label: 'Відповідь' }, + { + name: 'answer', + type: 'richText', + editor: standardEditor, + required: true, + label: 'Відповідь', + }, ], }, ], diff --git a/src/globals/LegalPages.ts b/src/globals/LegalPages.ts index e304a07..e7f719a 100644 --- a/src/globals/LegalPages.ts +++ b/src/globals/LegalPages.ts @@ -1,4 +1,5 @@ import type { GlobalConfig } from 'payload' +import { standardEditor } from '@/fields/richText' import { isAdminOrEditor } from '@/access/isAdminOrEditor' import { revalidateGlobalAfterChange } from '@/hooks/revalidatePath' @@ -15,7 +16,7 @@ export const LegalPages: GlobalConfig = { label: 'Політика конфіденційності', fields: [ { name: 'title', type: 'text', defaultValue: 'Політика конфіденційності' }, - { name: 'content', type: 'textarea', label: 'Текст сторінки', admin: { rows: 20 } }, + { name: 'content', type: 'richText', editor: standardEditor, label: 'Текст сторінки' }, ], }, { @@ -24,7 +25,7 @@ export const LegalPages: GlobalConfig = { label: 'Умови використання', fields: [ { name: 'title', type: 'text', defaultValue: 'Умови використання' }, - { name: 'content', type: 'textarea', label: 'Текст сторінки', admin: { rows: 20 } }, + { name: 'content', type: 'richText', editor: standardEditor, label: 'Текст сторінки' }, ], }, { @@ -33,7 +34,7 @@ export const LegalPages: GlobalConfig = { label: 'Публічна оферта', fields: [ { name: 'title', type: 'text', defaultValue: 'Публічна оферта' }, - { name: 'content', type: 'textarea', label: 'Текст сторінки', admin: { rows: 20 } }, + { name: 'content', type: 'richText', editor: standardEditor, label: 'Текст сторінки' }, ], }, { @@ -42,7 +43,7 @@ export const LegalPages: GlobalConfig = { label: 'Обробка персональних даних', fields: [ { name: 'title', type: 'text', defaultValue: 'Обробка персональних даних' }, - { name: 'content', type: 'textarea', label: 'Текст сторінки', admin: { rows: 20 } }, + { name: 'content', type: 'richText', editor: standardEditor, label: 'Текст сторінки' }, ], }, ], diff --git a/src/globals/TicketsPage.ts b/src/globals/TicketsPage.ts index 45b9330..646cf50 100644 --- a/src/globals/TicketsPage.ts +++ b/src/globals/TicketsPage.ts @@ -1,4 +1,5 @@ import type { GlobalConfig } from 'payload' +import { standardEditor } from '@/fields/richText' import { isAdminOrEditor } from '@/access/isAdminOrEditor' import { revalidateGlobalAfterChange } from '@/hooks/revalidatePath' @@ -56,7 +57,7 @@ export const TicketsPage: GlobalConfig = { { name: 'name', type: 'text', required: true }, { name: 'subtitle', type: 'text' }, { name: 'price', type: 'text', required: true, admin: { description: 'Напр. «600 ₴»' } }, - { name: 'description', type: 'textarea' }, + { name: 'description', type: 'richText', editor: standardEditor }, { name: 'featured', type: 'checkbox', label: 'Виділена (помаранчева)' }, { name: 'badge', type: 'text', admin: { description: 'Напр. «Найпопулярніший»' } }, { @@ -66,18 +67,11 @@ export const TicketsPage: GlobalConfig = { }, ], defaultValue: [ - { - name: 'Комбо 1', - price: '600 ₴', - description: 'Індивідуальний квиток для повного занурення в атмосферу парку.', - featured: false, - locations: COMBO_LOCATIONS, - }, + { name: 'Комбо 1', price: '600 ₴', featured: false, locations: COMBO_LOCATIONS }, { name: 'Комбо Сімейний', subtitle: '(3 ос.)', price: '1500 ₴', - description: 'Оптимальний вибір для батьків та дитини (віком до 14 років).', featured: true, badge: 'Найпопулярніший', locations: COMBO_LOCATIONS, @@ -86,7 +80,6 @@ export const TicketsPage: GlobalConfig = { name: 'Комбо Сімейний', subtitle: '(4 ос.)', price: '1800 ₴', - description: 'Універсальний сімейний пакет розваг для компанії з 4 людей.', featured: false, locations: COMBO_LOCATIONS, }, @@ -94,7 +87,6 @@ export const TicketsPage: GlobalConfig = { name: 'Комбо Сімейний', subtitle: '(5 ос.)', price: '2000 ₴', - description: 'Максимальний та найвигідніший пакет для великої родини.', featured: false, locations: COMBO_LOCATIONS, }, @@ -152,9 +144,8 @@ export const TicketsPage: GlobalConfig = { }, { name: 'benefitsFootnote', - type: 'textarea', - defaultValue: - '*Знижки та пільги поширюються виключно на індивідуальне відвідування 3 основних локацій (Динопарк, Зона топіарних фігур, Дзеркальний лабіринт) та не сумуються з тарифами категорії «КОМБО».', + type: 'richText', + editor: standardEditor, }, ], } diff --git a/src/types/globals.ts b/src/types/globals.ts index 69bf151..b70c1e4 100644 --- a/src/types/globals.ts +++ b/src/types/globals.ts @@ -1,3 +1,5 @@ +import type React from 'react' + export interface Media { id?: string url?: string | null @@ -93,7 +95,7 @@ export interface HomePageSectionTitles { export interface HomePageWhyParentsItem { title?: string | null - description?: string | null + description?: React.ReactNode } export interface HomePageWhyParents { @@ -116,14 +118,14 @@ export interface HomePageVideo { } export interface HomePageBirthdayIntro { - text?: string | null + text?: React.ReactNode patternGreen?: Media | string | null patternOrange?: Media | string | null } export interface HomePageNews { title?: string | null - subtitle?: string | null + subtitle?: React.ReactNode limit?: number | null } @@ -136,7 +138,7 @@ export interface HomePageMap { export interface HomePageFaqItem { question?: string | null - answer?: string | null + answer?: React.ReactNode } export interface HomePageFaq {