sozsoft-platform/ui/src/views/public/About.tsx

692 lines
24 KiB
TypeScript
Raw Normal View History

import { useEffect, useMemo, useRef, useState } from 'react'
2026-05-26 08:56:05 +00:00
import type { FC } from 'react'
import PageTitle from '@/components/shared/PageTitle'
2026-02-24 20:44:16 +00:00
import { AboutDto } from '@/proxy/about/models'
import { getAbout, saveAboutPage } from '@/services/about'
2026-02-24 20:44:16 +00:00
import { useLocalization } from '@/utils/hooks/useLocalization'
import { PUBLIC_PAGE_DESIGN } from '@/constants/permission.constant'
import {
DesignerLayer,
DesignerSelection,
SelectableBlock,
TranslateFn,
iconField,
imageField,
isLikelyLocalizationKey,
numberField,
booleanField,
resolveLocalizedValue,
styleField,
textAreaField,
textField,
translateLabel,
usePageDesigner,
} from './designer'
import { designerPageClass, getIconColor } from './pageStyles'
import { PageIcon, PublicPageLoader } from './shared'
interface AboutStatContent {
icon: string
value: string
label: string
labelKey: string
2026-03-17 13:35:58 +00:00
styleClassKey: string
styleClass: string
valueStyleClassKey: string
valueStyleClass: string
labelStyleClassKey: string
labelStyleClass: string
useCounter: boolean
counterEnd: string
counterSuffix: string
counterDuration: number
}
interface AboutDescriptionContent {
key: string
text: string
2026-03-17 13:35:58 +00:00
styleClassKey: string
styleClass: string
}
interface AboutSectionContent {
title: string
description: string
titleKey: string
descriptionKey: string
2026-03-17 13:35:58 +00:00
cardStyleClassKey: string
cardStyleClass: string
titleStyleClassKey: string
titleStyleClass: string
descriptionStyleClassKey: string
descriptionStyleClass: string
}
interface AboutContent {
heroTitle: string
heroTitleKey: string
2026-03-17 13:35:58 +00:00
heroTitleStyleClassKey: string
heroTitleStyleClass: string
heroSubtitle: string
heroSubtitleKey: string
2026-03-17 13:35:58 +00:00
heroSubtitleStyleClassKey: string
heroSubtitleStyleClass: string
heroImage: string
heroImageKey: string
2026-03-17 13:35:58 +00:00
heroSectionStyleClassKey: string
heroSectionStyleClass: string
descriptionsContainerStyleClassKey: string
descriptionsContainerStyleClass: string
stats: AboutStatContent[]
descriptions: AboutDescriptionContent[]
sections: AboutSectionContent[]
}
const ABOUT_HERO_IMAGE =
'https://images.pexels.com/photos/3183183/pexels-photo-3183183.jpeg?auto=compress&cs=tinysrgb&w=1920'
const ABOUT_HERO_TITLE_KEY = 'App.About'
const ABOUT_HERO_SUBTITLE_KEY = 'App.PublicAbout.Subtitle'
const ABOUT_HERO_IMAGE_KEY = 'Public.about.heroImage'
2026-03-17 13:35:58 +00:00
const ABOUT_HERO_SECTION_STYLE_KEY = 'Public.about.hero.sectionStyleClass'
const ABOUT_HERO_TITLE_STYLE_KEY = 'Public.about.hero.titleStyleClass'
const ABOUT_HERO_SUBTITLE_STYLE_KEY = 'Public.about.hero.subtitleStyleClass'
const ABOUT_DESCRIPTIONS_CONTAINER_STYLE_KEY = 'Public.about.descriptions.containerStyleClass'
const HERO_SECTION_STYLE_DEFAULT = 'relative bg-blue-900 text-white py-12'
const HERO_TITLE_STYLE_DEFAULT = 'text-5xl font-bold ml-4 mt-3 mb-2 text-white'
const HERO_SUBTITLE_STYLE_DEFAULT = 'text-xl max-w-3xl ml-4'
const DESCRIPTIONS_CONTAINER_STYLE_DEFAULT =
'p-5 mx-auto text-gray-800 dark:text-gray-200 text-lg leading-relaxed shadow-md bg-white dark:bg-gray-900 border-l-4 border-blue-600'
const STAT_STYLE_DEFAULT = 'text-center rounded-xl px-4 py-6'
const STAT_VALUE_STYLE_DEFAULT = 'text-4xl font-bold text-gray-900 dark:text-gray-100 mb-2'
const STAT_LABEL_STYLE_DEFAULT = 'text-gray-600 dark:text-gray-300'
2026-08-14 11:32:31 +00:00
const SECTION_CARD_STYLE_DEFAULT =
'bg-white dark:bg-gray-900 p-8 rounded-xl shadow-lg dark:shadow-gray-950/40'
const SECTION_TITLE_STYLE_DEFAULT = 'text-2xl font-bold text-gray-900 dark:text-gray-100 mb-4'
const SECTION_DESCRIPTION_STYLE_DEFAULT = 'text-gray-700 dark:text-gray-300'
function getCounterParts(value: string, suffix: string) {
const numericMatch = (value || '0').replace(',', '.').match(/\d+(?:\.\d+)?/)
2026-05-26 08:56:05 +00:00
if (!numericMatch) {
return null
}
const endValue = Number(numericMatch[0])
if (!Number.isFinite(endValue)) {
return null
}
const matchIndex = numericMatch.index ?? 0
2026-05-26 08:56:05 +00:00
return {
endValue,
prefix: value.slice(0, matchIndex),
suffix: suffix || value.slice(matchIndex + numericMatch[0].length),
2026-05-26 08:56:05 +00:00
hasDecimals: numericMatch[0].includes('.'),
}
}
/**
* Sayac animasyonu yalnizca deger ekrana girdiginde bir kez calisir;
* boylece sayfanin altindaki istatistikler kullanici oraya gelmeden bitmis olmaz.
*/
2026-05-26 08:56:05 +00:00
const AnimatedStatValue: FC<{ stat: AboutStatContent }> = ({ stat }) => {
const counterParts = useMemo(
() => getCounterParts(stat.counterEnd || stat.value, stat.counterSuffix),
[stat.counterEnd, stat.counterSuffix, stat.value],
)
const [currentValue, setCurrentValue] = useState(0)
const [isVisible, setIsVisible] = useState(false)
const containerRef = useRef<HTMLSpanElement>(null)
useEffect(() => {
const element = containerRef.current
if (!stat.useCounter || !element || typeof IntersectionObserver === 'undefined') {
setIsVisible(true)
return
}
const observer = new IntersectionObserver(
(entries) => {
if (entries.some((entry) => entry.isIntersecting)) {
setIsVisible(true)
observer.disconnect()
}
},
{ threshold: 0.3 },
)
observer.observe(element)
return () => observer.disconnect()
}, [stat.useCounter])
2026-05-26 08:56:05 +00:00
useEffect(() => {
if (!stat.useCounter || !counterParts || !isVisible) {
2026-05-26 08:56:05 +00:00
return
}
const duration = Math.max(stat.counterDuration || 2000, 0)
2026-05-26 08:56:05 +00:00
let animationFrameId = 0
let startTime: number | null = null
const animate = (timestamp: number) => {
startTime ??= timestamp
2026-05-26 08:56:05 +00:00
const progress = duration === 0 ? 1 : Math.min((timestamp - startTime) / duration, 1)
setCurrentValue(counterParts.endValue * (1 - Math.pow(1 - progress, 3)))
2026-05-26 08:56:05 +00:00
if (progress < 1) {
animationFrameId = requestAnimationFrame(animate)
}
}
setCurrentValue(0)
animationFrameId = requestAnimationFrame(animate)
return () => cancelAnimationFrame(animationFrameId)
}, [counterParts, isVisible, stat.counterDuration, stat.useCounter])
2026-05-26 08:56:05 +00:00
if (!stat.useCounter || !counterParts) {
return <>{stat.value}</>
}
return (
<span ref={containerRef}>
2026-05-26 08:56:05 +00:00
{counterParts.prefix}
{counterParts.hasDecimals ? currentValue.toFixed(1) : Math.floor(currentValue)}
2026-05-26 08:56:05 +00:00
{counterParts.suffix}
</span>
2026-05-26 08:56:05 +00:00
)
}
function buildAboutContent(about: AboutDto | undefined, translate: TranslateFn): AboutContent {
return {
heroTitle: resolveLocalizedValue(translate, ABOUT_HERO_TITLE_KEY, 'About'),
heroTitleKey: ABOUT_HERO_TITLE_KEY,
2026-03-17 13:35:58 +00:00
heroTitleStyleClassKey: ABOUT_HERO_TITLE_STYLE_KEY,
heroTitleStyleClass: resolveLocalizedValue(
translate,
ABOUT_HERO_TITLE_STYLE_KEY,
HERO_TITLE_STYLE_DEFAULT,
2026-03-17 13:35:58 +00:00
),
heroSubtitle: resolveLocalizedValue(translate, ABOUT_HERO_SUBTITLE_KEY),
heroSubtitleKey: ABOUT_HERO_SUBTITLE_KEY,
2026-03-17 13:35:58 +00:00
heroSubtitleStyleClassKey: ABOUT_HERO_SUBTITLE_STYLE_KEY,
heroSubtitleStyleClass: resolveLocalizedValue(
translate,
ABOUT_HERO_SUBTITLE_STYLE_KEY,
HERO_SUBTITLE_STYLE_DEFAULT,
2026-03-17 13:35:58 +00:00
),
heroImage: resolveLocalizedValue(translate, ABOUT_HERO_IMAGE_KEY, ABOUT_HERO_IMAGE),
heroImageKey: ABOUT_HERO_IMAGE_KEY,
2026-03-17 13:35:58 +00:00
heroSectionStyleClassKey: ABOUT_HERO_SECTION_STYLE_KEY,
heroSectionStyleClass: resolveLocalizedValue(
translate,
ABOUT_HERO_SECTION_STYLE_KEY,
HERO_SECTION_STYLE_DEFAULT,
2026-03-17 13:35:58 +00:00
),
descriptionsContainerStyleClassKey: ABOUT_DESCRIPTIONS_CONTAINER_STYLE_KEY,
descriptionsContainerStyleClass: resolveLocalizedValue(
translate,
ABOUT_DESCRIPTIONS_CONTAINER_STYLE_KEY,
DESCRIPTIONS_CONTAINER_STYLE_DEFAULT,
2026-03-17 13:35:58 +00:00
),
stats: (about?.statsDto ?? []).map((stat, index) => {
const prefix = `Public.about.dynamic.stat.${index + 1}`
return {
icon: stat.icon || '',
value: stat.value,
label: resolveLocalizedValue(translate, stat.labelKey, stat.labelKey),
2026-08-14 11:32:31 +00:00
labelKey:
(isLikelyLocalizationKey(stat.labelKey) ? stat.labelKey : '') || `${prefix}.label`,
styleClassKey: `${prefix}.styleClass`,
styleClass: resolveLocalizedValue(translate, `${prefix}.styleClass`, STAT_STYLE_DEFAULT),
valueStyleClassKey: `${prefix}.valueStyleClass`,
2026-03-17 13:35:58 +00:00
valueStyleClass: resolveLocalizedValue(
translate,
`${prefix}.valueStyleClass`,
STAT_VALUE_STYLE_DEFAULT,
2026-03-17 13:35:58 +00:00
),
labelStyleClassKey: `${prefix}.labelStyleClass`,
2026-03-17 13:35:58 +00:00
labelStyleClass: resolveLocalizedValue(
translate,
`${prefix}.labelStyleClass`,
STAT_LABEL_STYLE_DEFAULT,
2026-03-17 13:35:58 +00:00
),
useCounter: stat.useCounter ?? false,
counterEnd: stat.counterEnd ?? '',
counterSuffix: stat.counterSuffix ?? '',
counterDuration: stat.counterDuration ?? 2000,
}
}),
descriptions: (about?.descriptionsDto ?? []).map((item, index) => {
const prefix = `Public.about.dynamic.description.${index + 1}`
return {
key: (isLikelyLocalizationKey(item) ? item : '') || prefix,
text: resolveLocalizedValue(translate, item, item),
styleClassKey: `${prefix}.styleClass`,
2026-03-17 13:35:58 +00:00
styleClass: resolveLocalizedValue(
translate,
`${prefix}.styleClass`,
2026-06-04 14:13:55 +00:00
index % 2 === 0 ? '' : 'text-center p-5 text-blue-800 dark:text-white',
2026-03-17 13:35:58 +00:00
),
}
}),
sections: (about?.sectionsDto ?? []).map((section) => {
const prefix = `Public.about.dynamic.section.${section.key}`
return {
title: resolveLocalizedValue(translate, section.key, section.key),
description: resolveLocalizedValue(translate, section.descKey, section.descKey),
titleKey: (isLikelyLocalizationKey(section.key) ? section.key : '') || `${prefix}.title`,
descriptionKey:
2026-08-14 11:32:31 +00:00
(isLikelyLocalizationKey(section.descKey) ? section.descKey : '') ||
`${prefix}.description`,
cardStyleClassKey: `${prefix}.cardStyleClass`,
2026-03-17 13:35:58 +00:00
cardStyleClass: resolveLocalizedValue(
translate,
`${prefix}.cardStyleClass`,
SECTION_CARD_STYLE_DEFAULT,
2026-03-17 13:35:58 +00:00
),
titleStyleClassKey: `${prefix}.titleStyleClass`,
2026-03-17 13:35:58 +00:00
titleStyleClass: resolveLocalizedValue(
translate,
`${prefix}.titleStyleClass`,
SECTION_TITLE_STYLE_DEFAULT,
2026-03-17 13:35:58 +00:00
),
descriptionStyleClassKey: `${prefix}.descriptionStyleClass`,
2026-03-17 13:35:58 +00:00
descriptionStyleClass: resolveLocalizedValue(
translate,
`${prefix}.descriptionStyleClass`,
SECTION_DESCRIPTION_STYLE_DEFAULT,
2026-03-17 13:35:58 +00:00
),
}
}),
}
}
function toSaveInput(content: AboutContent, cultureName: string) {
return {
cultureName,
heroTitleKey: content.heroTitleKey,
heroTitleValue: content.heroTitle,
heroSubtitleKey: content.heroSubtitleKey,
heroSubtitleValue: content.heroSubtitle,
heroImageKey: content.heroImageKey,
heroImageValue: content.heroImage,
stats: content.stats.map((stat, index) => ({
icon: stat.icon,
value: stat.value,
labelKey: stat.labelKey || `Public.about.dynamic.stat.${index + 1}.label`,
labelValue: stat.label,
useCounter: stat.useCounter,
counterEnd: stat.counterEnd,
counterSuffix: stat.counterSuffix,
counterDuration: stat.counterDuration,
})),
descriptions: content.descriptions.map((item, index) => ({
key: item.key || `Public.about.dynamic.description.${index + 1}`,
value: item.text,
})),
sections: content.sections.map((section, index) => ({
titleKey: section.titleKey || `Public.about.dynamic.section.${index + 1}.title`,
titleValue: section.title,
descriptionKey:
section.descriptionKey || `Public.about.dynamic.section.${index + 1}.description`,
descriptionValue: section.description,
})),
styleTexts: [
{ key: content.heroSectionStyleClassKey, value: content.heroSectionStyleClass },
{ key: content.heroTitleStyleClassKey, value: content.heroTitleStyleClass },
{ key: content.heroSubtitleStyleClassKey, value: content.heroSubtitleStyleClass },
{
key: content.descriptionsContainerStyleClassKey,
value: content.descriptionsContainerStyleClass,
},
...content.stats.flatMap((stat) => [
{ key: stat.styleClassKey, value: stat.styleClass },
{ key: stat.valueStyleClassKey, value: stat.valueStyleClass },
{ key: stat.labelStyleClassKey, value: stat.labelStyleClass },
]),
...content.descriptions.map((item) => ({
key: item.styleClassKey,
value: item.styleClass,
})),
...content.sections.flatMap((section) => [
{ key: section.cardStyleClassKey, value: section.cardStyleClass },
{ key: section.titleStyleClassKey, value: section.titleStyleClass },
{ key: section.descriptionStyleClassKey, value: section.descriptionStyleClass },
]),
],
}
}
2026-02-24 20:44:16 +00:00
2026-05-26 08:56:05 +00:00
const About: FC = () => {
2026-02-24 20:44:16 +00:00
const { translate } = useLocalization()
const [loading, setLoading] = useState(true)
const [about, setAbout] = useState<AboutDto>()
useEffect(() => {
const fetchAbout = async () => {
setLoading(true)
2026-02-24 20:44:16 +00:00
try {
const result = await getAbout()
setAbout(result.data)
} catch (error) {
console.error('About alinirken hata olustu:', error)
2026-02-24 20:44:16 +00:00
} finally {
setLoading(false)
}
}
fetchAbout()
}, [])
// Referansi kararli tutmak icin memoize edilir; aksi halde tasarim state'i her render'da yeniden tohumlanir.
const initialContent = useMemo(
() => (loading ? null : buildAboutContent(about, translate)),
[about, loading, translate],
)
const designer = usePageDesigner<AboutContent>({
pageKey: 'About',
permission: PUBLIC_PAGE_DESIGN.ABOUT,
initialContent,
onSave: (content, cultureName) => saveAboutPage(toSaveInput(content, cultureName)),
})
const { content, isDesignMode, isPanelVisible, selectedBlockId, selectBlock } = designer
const selection: DesignerSelection | null = useMemo(() => {
if (!content || !selectedBlockId) {
return null
}
if (selectedBlockId === 'hero') {
return {
id: 'hero',
title: content.heroTitleKey,
description: translateLabel(translate, 'App.PublicDesigner.DesignerDesc4'),
fields: [
textField('heroTitle', content.heroTitleKey, content.heroTitle),
textAreaField('heroSubtitle', content.heroSubtitleKey, content.heroSubtitle),
imageField('heroImage', content.heroImageKey, content.heroImage, { group: 'media' }),
2026-08-14 11:32:31 +00:00
styleField(
'heroTitleStyleClass',
content.heroTitleStyleClassKey,
content.heroTitleStyleClass,
),
styleField(
'heroSubtitleStyleClass',
content.heroSubtitleStyleClassKey,
content.heroSubtitleStyleClass,
),
styleField(
'heroSectionStyleClass',
content.heroSectionStyleClassKey,
content.heroSectionStyleClass,
),
],
}
}
if (selectedBlockId === 'descriptions') {
return {
id: 'descriptions',
title: 'Public.about.description.*',
2026-08-14 11:32:31 +00:00
description: translateLabel(translate, 'Public.designer.descriptionsHint'),
2026-03-17 13:35:58 +00:00
fields: [
...content.descriptions.map((item, index) =>
textAreaField(`descriptions.${index}.text`, item.key, item.text, {
2026-03-17 13:35:58 +00:00
rows: index % 2 === 0 ? 4 : 3,
}),
),
styleField(
'descriptionsContainerStyleClass',
content.descriptionsContainerStyleClassKey,
content.descriptionsContainerStyleClass,
),
...content.descriptions.map((item, index) =>
styleField(`descriptions.${index}.styleClass`, item.styleClassKey, item.styleClass),
),
2026-03-17 13:35:58 +00:00
],
}
}
if (selectedBlockId.startsWith('stat-')) {
const index = Number(selectedBlockId.replace('stat-', ''))
const stat = content.stats[index]
if (!stat) {
return null
}
return {
id: selectedBlockId,
title: stat.labelKey,
description: translateLabel(translate, 'App.PublicDesigner.DesignerDesc1'),
fields: [
iconField(
`stats.${index}.icon`,
translateLabel(translate, 'App.PublicDesigner.IkonAnahtari'),
stat.icon,
{ placeholder: translate('::App.PublicDesigner.IconPlaceholder', { icon: 'FaUsers' }) },
),
textField(
`stats.${index}.value`,
translate('::App.Listform.ListformField.Value'),
stat.value,
),
textField(`stats.${index}.label`, stat.labelKey, stat.label),
booleanField(`stats.${index}.useCounter`, 'useCounter', stat.useCounter, {
group: 'advanced',
2026-08-14 11:32:31 +00:00
placeholder: translateLabel(translate, 'Public.designer.useCounter'),
}),
textField(`stats.${index}.counterEnd`, 'counterEnd', stat.counterEnd, {
group: 'advanced',
2026-08-14 11:32:31 +00:00
helpText: translateLabel(translate, 'Public.designer.counterEndHint'),
}),
textField(`stats.${index}.counterSuffix`, 'counterSuffix', stat.counterSuffix, {
group: 'advanced',
}),
2026-08-14 11:32:31 +00:00
numberField(
`stats.${index}.counterDuration`,
'counterDuration (ms)',
stat.counterDuration,
{
group: 'advanced',
},
),
styleField(`stats.${index}.styleClass`, stat.styleClassKey, stat.styleClass),
2026-08-14 11:32:31 +00:00
styleField(
`stats.${index}.valueStyleClass`,
stat.valueStyleClassKey,
stat.valueStyleClass,
),
styleField(
`stats.${index}.labelStyleClass`,
stat.labelStyleClassKey,
stat.labelStyleClass,
),
],
}
}
if (selectedBlockId.startsWith('section-')) {
const index = Number(selectedBlockId.replace('section-', ''))
const section = content.sections[index]
if (!section) {
return null
}
return {
id: selectedBlockId,
title: section.titleKey,
2026-08-14 11:32:31 +00:00
description: translateLabel(translate, 'Public.designer.sectionHint'),
fields: [
textField(`sections.${index}.title`, section.titleKey, section.title),
2026-08-14 11:32:31 +00:00
textAreaField(
`sections.${index}.description`,
section.descriptionKey,
section.description,
),
styleField(
`sections.${index}.cardStyleClass`,
section.cardStyleClassKey,
section.cardStyleClass,
),
styleField(
`sections.${index}.titleStyleClass`,
section.titleStyleClassKey,
section.titleStyleClass,
),
styleField(
`sections.${index}.descriptionStyleClass`,
section.descriptionStyleClassKey,
section.descriptionStyleClass,
),
],
}
}
return null
}, [content, selectedBlockId, translate])
2026-02-24 20:44:16 +00:00
if (loading) {
return <PublicPageLoader />
2026-02-24 20:44:16 +00:00
}
return (
<>
<PageTitle title={translate('::App.About')} />
<div className={designerPageClass(isDesignMode, isPanelVisible)}>
{/* Hero */}
<SelectableBlock
id="hero"
label="Hero"
isActive={selectedBlockId === 'hero'}
isDesignMode={isDesignMode}
onSelect={selectBlock}
>
<div
className={`${content?.heroSectionStyleClass || HERO_SECTION_STYLE_DEFAULT} dark:bg-gray-950`}
>
<div
2026-05-26 16:09:26 +00:00
className="absolute inset-0 opacity-20 dark:opacity-35"
style={{
backgroundImage: `url("${content?.heroImage || ABOUT_HERO_IMAGE}")`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}}
/>
<div className="absolute inset-0 bg-blue-950/25 dark:bg-gray-950/45" />
<div className="container relative mx-auto pt-20">
<h1 className={content?.heroTitleStyleClass || HERO_TITLE_STYLE_DEFAULT}>
{content?.heroTitle}
</h1>
<p className={content?.heroSubtitleStyleClass || HERO_SUBTITLE_STYLE_DEFAULT}>
{content?.heroSubtitle}
</p>
</div>
2026-02-24 20:44:16 +00:00
</div>
</SelectableBlock>
2026-02-24 20:44:16 +00:00
{/* Stats */}
<div className="bg-white py-10 dark:bg-gray-900">
2026-02-24 20:44:16 +00:00
<div className="container mx-auto px-4">
<div className="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-5">
{content?.stats.map((stat, index) => (
<SelectableBlock
key={`${stat.labelKey}-${index}`}
id={`stat-${index}`}
label={`Stat ${index + 1}`}
isActive={selectedBlockId === `stat-${index}`}
isDesignMode={isDesignMode}
onSelect={selectBlock}
>
<div className={stat.styleClass || STAT_STYLE_DEFAULT}>
<PageIcon
name={stat.icon}
className={`mx-auto mb-4 h-12 w-12 ${getIconColor(index)}`}
/>
<div className={stat.valueStyleClass || STAT_VALUE_STYLE_DEFAULT}>
<AnimatedStatValue stat={stat} />
</div>
<div className={stat.labelStyleClass || STAT_LABEL_STYLE_DEFAULT}>
{stat.label}
</div>
</div>
</SelectableBlock>
))}
2026-02-24 20:44:16 +00:00
</div>
</div>
</div>
{/* Descriptions + sections */}
2026-02-24 20:44:16 +00:00
<div className="py-6">
<div className="container mx-auto px-4">
<div className="mb-6">
<SelectableBlock
id="descriptions"
2026-08-14 11:32:31 +00:00
label={translateLabel(translate, 'Public.designer.group.content')}
isActive={selectedBlockId === 'descriptions'}
isDesignMode={isDesignMode}
onSelect={selectBlock}
>
<div
className={
content?.descriptionsContainerStyleClass || DESCRIPTIONS_CONTAINER_STYLE_DEFAULT
}
>
2026-03-17 13:35:58 +00:00
{content?.descriptions.map((item, index) => (
<p key={item.key || `description-${index}`} className={item.styleClass}>
2026-03-17 13:35:58 +00:00
{item.text}
</p>
))}
</div>
</SelectableBlock>
2026-02-24 20:44:16 +00:00
</div>
<div className="grid grid-cols-1 gap-12 lg:grid-cols-2">
{content?.sections.map((section, index) => (
<SelectableBlock
key={`${section.titleKey}-${index}`}
id={`section-${index}`}
label={`Section ${index + 1}`}
isActive={selectedBlockId === `section-${index}`}
isDesignMode={isDesignMode}
onSelect={selectBlock}
>
<div className={section.cardStyleClass || SECTION_CARD_STYLE_DEFAULT}>
<h3 className={section.titleStyleClass || SECTION_TITLE_STYLE_DEFAULT}>
{section.title}
</h3>
2026-08-14 11:32:31 +00:00
<p
className={section.descriptionStyleClass || SECTION_DESCRIPTION_STYLE_DEFAULT}
>
{section.description}
</p>
</div>
</SelectableBlock>
2026-02-24 20:44:16 +00:00
))}
</div>
</div>
</div>
<DesignerLayer pageTitle="About" selection={selection} designer={designer} />
2026-02-24 20:44:16 +00:00
</div>
</>
)
}
export default About