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

832 lines
27 KiB
TypeScript
Raw Normal View History

2026-02-24 20:44:16 +00:00
import React, { useEffect, useState } from 'react'
import { Helmet } from 'react-helmet'
import navigationIcon from '@/proxy/menus/navigation-icon.config'
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 Loading from '@/components/shared/Loading'
import { APP_NAME } from '@/constants/app.constant'
2026-03-30 20:40:20 +00:00
import { Button, Notification, toast } from '@/components/ui'
import { useStoreState } from '@/store'
import { useStoreActions } from '@/store'
import DesignerDrawer from './designer/DesignerDrawer'
import SelectableBlock from './designer/SelectableBlock'
import { DesignerSelection } from './designer/types'
import { useDesignerState } from './designer/useDesignerState'
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 = 'Public.about.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'
function isLikelyLocalizationKey(value?: string) {
return Boolean(value && /^[A-Za-z0-9_.-]+$/.test(value) && value.includes('.'))
}
function resolveLocalizedValue(
translate: (key: string) => string,
keyOrValue: string | undefined,
fallback = '',
) {
if (!keyOrValue) {
return fallback
}
if (!isLikelyLocalizationKey(keyOrValue)) {
return keyOrValue
}
const translatedValue = translate('::' + keyOrValue)
return translatedValue === keyOrValue ? fallback || keyOrValue : translatedValue
}
function buildAboutContent(
about: AboutDto | undefined,
translate: (key: string) => string,
): 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,
'text-5xl font-bold ml-4 mt-3 mb-2 text-white',
),
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,
'text-xl max-w-3xl ml-4',
),
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,
'relative bg-blue-900 text-white py-12',
),
descriptionsContainerStyleClassKey: ABOUT_DESCRIPTIONS_CONTAINER_STYLE_KEY,
descriptionsContainerStyleClass: resolveLocalizedValue(
translate,
ABOUT_DESCRIPTIONS_CONTAINER_STYLE_KEY,
'p-5 mx-auto text-gray-800 text-lg leading-relaxed shadow-md bg-white border-l-4 border-blue-600',
),
stats:
2026-03-17 13:35:58 +00:00
about?.statsDto.map((stat, index) => ({
styleClassKey: `Public.about.dynamic.stat.${index + 1}.styleClass`,
styleClass: resolveLocalizedValue(
translate,
`Public.about.dynamic.stat.${index + 1}.styleClass`,
'text-center rounded-xl px-4 py-6',
),
valueStyleClassKey: `Public.about.dynamic.stat.${index + 1}.valueStyleClass`,
valueStyleClass: resolveLocalizedValue(
translate,
`Public.about.dynamic.stat.${index + 1}.valueStyleClass`,
'text-4xl font-bold text-gray-900 mb-2',
),
labelStyleClassKey: `Public.about.dynamic.stat.${index + 1}.labelStyleClass`,
labelStyleClass: resolveLocalizedValue(
translate,
`Public.about.dynamic.stat.${index + 1}.labelStyleClass`,
'text-gray-600',
),
icon: stat.icon || '',
value: stat.value,
label: resolveLocalizedValue(translate, stat.labelKey, stat.labelKey),
labelKey:
(isLikelyLocalizationKey(stat.labelKey) ? stat.labelKey : undefined) ||
2026-03-17 13:35:58 +00:00
`Public.about.dynamic.stat.${index + 1}.label`,
useCounter: stat.useCounter,
counterEnd: stat.counterEnd,
counterSuffix: stat.counterSuffix,
counterDuration: stat.counterDuration,
})) ?? [],
descriptions:
about?.descriptionsDto.map((item, index) => ({
key:
(isLikelyLocalizationKey(item) ? item : undefined) ||
`Public.about.dynamic.description.${index + 1}`,
text: resolveLocalizedValue(translate, item, item),
2026-03-17 13:35:58 +00:00
styleClassKey: `Public.about.dynamic.description.${index + 1}.styleClass`,
styleClass: resolveLocalizedValue(
translate,
`Public.about.dynamic.description.${index + 1}.styleClass`,
index % 2 === 0 ? '' : 'text-center p-5 text-blue-800',
),
})) ?? [],
sections:
about?.sectionsDto.map((section) => ({
title: resolveLocalizedValue(translate, section.key, section.key),
description: resolveLocalizedValue(translate, section.descKey, section.descKey),
titleKey:
(isLikelyLocalizationKey(section.key) ? section.key : undefined) ||
`Public.about.dynamic.section.${section.key}.title`,
descriptionKey:
(isLikelyLocalizationKey(section.descKey) ? section.descKey : undefined) ||
`Public.about.dynamic.section.${section.key}.description`,
2026-03-17 13:35:58 +00:00
cardStyleClassKey: `Public.about.dynamic.section.${section.key}.cardStyleClass`,
cardStyleClass: resolveLocalizedValue(
translate,
`Public.about.dynamic.section.${section.key}.cardStyleClass`,
'bg-white p-8 rounded-xl shadow-lg',
),
titleStyleClassKey: `Public.about.dynamic.section.${section.key}.titleStyleClass`,
titleStyleClass: resolveLocalizedValue(
translate,
`Public.about.dynamic.section.${section.key}.titleStyleClass`,
'text-2xl font-bold text-gray-900 mb-4',
),
descriptionStyleClassKey: `Public.about.dynamic.section.${section.key}.descriptionStyleClass`,
descriptionStyleClass: resolveLocalizedValue(
translate,
`Public.about.dynamic.section.${section.key}.descriptionStyleClass`,
'text-gray-700',
),
})) ?? [],
}
}
2026-02-24 20:44:16 +00:00
const About: React.FC = () => {
const { translate } = useLocalization()
const { setLang } = useStoreActions((actions) => actions.locale)
const { getConfig } = useStoreActions((actions) => actions.abpConfig)
const configCultureName = useStoreState(
(state) => state.abpConfig.config?.localization.currentCulture.cultureName,
)
const localeCurrentLang = useStoreState((state) => state.locale?.currentLang)
const currentLanguage = configCultureName || localeCurrentLang || 'tr'
const abpLanguages = useStoreState((state) => state.abpConfig.config?.localization.languages) || []
const languageOptions = abpLanguages
.filter((language) => Boolean(language.cultureName))
.map((language) => {
const cultureName = language.cultureName || 'tr'
return {
key: cultureName.toLowerCase().split('-')[0],
cultureName,
displayName: language.displayName || cultureName,
}
})
const languagesFromConfig = languageOptions.map((language) => language.key)
const editorLanguages = Array.from(
new Set((languagesFromConfig.length > 0 ? languagesFromConfig : [currentLanguage]).filter(Boolean)),
)
2026-02-24 20:44:16 +00:00
const [loading, setLoading] = useState(true)
const [isSaving, setIsSaving] = useState(false)
const [isPanelVisible, setIsPanelVisible] = useState(true)
2026-02-24 20:44:16 +00:00
const [about, setAbout] = useState<AboutDto>()
const iconColors = [
'text-blue-600',
'text-red-600',
'text-green-600',
'text-purple-600',
'text-yellow-600',
'text-indigo-600',
]
function getIconColor(index: number) {
return iconColors[index % iconColors.length]
2026-02-24 20:44:16 +00:00
}
const initialContent = !loading ? buildAboutContent(about, translate) : null
const {
content,
isDesignMode,
selectedBlockId,
selectedLanguage,
supportedLanguages,
setContent,
setSelectedBlockId,
resetContent,
} = useDesignerState<AboutContent>('about', initialContent, {
currentLanguage,
supportedLanguages: editorLanguages,
})
2026-02-24 20:44:16 +00:00
useEffect(() => {
setLoading(true)
const fetchServices = async () => {
try {
const result = await getAbout()
setAbout(result.data)
} catch (error) {
console.error('About alınırken hata oluştu:', error)
} finally {
setLoading(false)
}
}
fetchServices()
}, [])
const updateContent = (updater: (current: AboutContent) => AboutContent) => {
setContent((current) => {
if (!current) {
return current
}
return updater(current)
})
}
const handleFieldChange = (fieldKey: string, value: string | string[]) => {
updateContent((current) => {
2026-03-17 13:35:58 +00:00
if (
fieldKey === 'heroTitle' ||
fieldKey === 'heroSubtitle' ||
fieldKey === 'heroImage' ||
fieldKey === 'heroSectionStyleClass' ||
fieldKey === 'heroTitleStyleClass' ||
fieldKey === 'heroSubtitleStyleClass' ||
fieldKey === 'descriptionsContainerStyleClass'
) {
return {
...current,
[fieldKey]: value as string,
}
}
if (fieldKey.startsWith('description-')) {
const index = Number(fieldKey.replace('description-', ''))
const descriptions = [...current.descriptions]
descriptions[index] = {
...descriptions[index],
text: value as string,
}
return {
...current,
descriptions,
}
}
2026-03-17 13:35:58 +00:00
if (fieldKey.startsWith('descriptionStyle-')) {
const index = Number(fieldKey.replace('descriptionStyle-', ''))
const descriptions = [...current.descriptions]
descriptions[index] = {
...descriptions[index],
styleClass: value as string,
}
return {
...current,
descriptions,
}
}
if (selectedBlockId?.startsWith('stat-')) {
const index = Number(selectedBlockId.replace('stat-', ''))
const stats = [...current.stats]
stats[index] = {
...stats[index],
[fieldKey]: value as string,
}
return {
...current,
stats,
}
}
if (selectedBlockId?.startsWith('section-')) {
const index = Number(selectedBlockId.replace('section-', ''))
const sections = [...current.sections]
sections[index] = {
...sections[index],
[fieldKey]: value as string,
}
return {
...current,
sections,
}
}
return current
})
}
const selectedSelection: DesignerSelection | null = React.useMemo(() => {
if (!content || !selectedBlockId) {
return null
}
if (selectedBlockId === 'hero') {
return {
id: 'hero',
title: content.heroTitleKey,
description: 'Baslik, alt baslik ve arka plan gorselini guncelleyin.',
fields: [
{
key: 'heroTitle',
label: content.heroTitleKey,
type: 'text',
value: content.heroTitle,
},
2026-03-17 13:35:58 +00:00
{
key: 'heroTitleStyleClass',
label: content.heroTitleStyleClassKey,
type: 'text',
value: content.heroTitleStyleClass,
},
{
key: 'heroSubtitle',
label: content.heroSubtitleKey,
type: 'textarea',
value: content.heroSubtitle,
},
2026-03-17 13:35:58 +00:00
{
key: 'heroSubtitleStyleClass',
label: content.heroSubtitleStyleClassKey,
type: 'text',
value: content.heroSubtitleStyleClass,
},
{
key: 'heroImage',
label: content.heroImageKey,
type: 'image',
value: content.heroImage,
},
2026-03-17 13:35:58 +00:00
{
key: 'heroSectionStyleClass',
label: content.heroSectionStyleClassKey,
type: 'text',
value: content.heroSectionStyleClass,
},
],
}
}
if (selectedBlockId === 'descriptions') {
return {
id: 'descriptions',
title: 'Public.about.description.*',
description: 'Orta bolumdeki aciklama metinlerini duzenleyin.',
2026-03-17 13:35:58 +00:00
fields: [
{
key: 'descriptionsContainerStyleClass',
label: content.descriptionsContainerStyleClassKey,
type: 'text',
value: content.descriptionsContainerStyleClass,
},
...content.descriptions.flatMap((item, index) => [
{
key: `description-${index}`,
label: item.key || `Public.about.dynamic.description.${index + 1}`,
type: 'textarea' as const,
value: item.text,
rows: index % 2 === 0 ? 4 : 3,
},
{
key: `descriptionStyle-${index}`,
label: item.styleClassKey,
type: 'text' as const,
value: item.styleClass,
},
]),
],
}
}
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: translate('::Public.designer.desc1'),
fields: [
{
key: 'icon',
label: translate('::Public.designer.ikonAnahtari'),
type: 'icon',
value: stat.icon,
placeholder: 'Ornek: FaUsers',
},
{
key: 'value',
2026-03-29 08:59:07 +00:00
label: translate('::App.Listform.ListformField.Value'),
type: 'text',
value: stat.value
},
{
key: 'label',
label: translate('::' + stat.labelKey),
type: 'text',
value: stat.label,
},
2026-03-17 13:35:58 +00:00
{
key: 'styleClass',
label: stat.styleClassKey,
type: 'text',
value: stat.styleClass,
},
{
key: 'valueStyleClass',
label: stat.valueStyleClassKey,
type: 'text',
value: stat.valueStyleClass,
},
{
key: 'labelStyleClass',
label: stat.labelStyleClassKey,
type: 'text',
value: 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,
description: 'Kart basligi ve aciklama metnini duzenleyin.',
fields: [
{
key: 'title',
label: section.titleKey,
type: 'text',
value: section.title,
},
{
key: 'description',
label: section.descriptionKey,
type: 'textarea',
value: section.description,
},
2026-03-17 13:35:58 +00:00
{
key: 'cardStyleClass',
label: section.cardStyleClassKey,
type: 'text',
value: section.cardStyleClass,
},
{
key: 'titleStyleClass',
label: section.titleStyleClassKey,
type: 'text',
value: section.titleStyleClass,
},
{
key: 'descriptionStyleClass',
label: section.descriptionStyleClassKey,
type: 'text',
value: section.descriptionStyleClass,
},
],
}
}
return null
}, [content, selectedBlockId])
const handleSaveAndExit = async () => {
if (!content || isSaving) {
return
}
setIsSaving(true)
try {
await saveAboutPage({
cultureName: selectedLanguage,
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,
})),
2026-03-17 13:35:58 +00:00
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,
},
]),
],
})
await getConfig(false)
setSelectedBlockId(null)
2026-03-17 11:46:20 +00:00
toast.push(
<Notification type="success" duration={2000}>
{translate('::ListForms.FormBilgileriKaydedildi')}
</Notification>,
{
placement: 'top-end',
},
)
} catch (error) {
console.error('About tasarimi kaydedilemedi:', error)
} finally {
setIsSaving(false)
}
}
const handleLanguageChange = (language: string) => {
// Global locale changes asynchronously fetch fresh localization texts.
// Keep designer language synced from store after that refresh.
setLang(language)
}
const handleSelectBlock = (blockId: string) => {
setSelectedBlockId(blockId)
if (!isPanelVisible) {
setIsPanelVisible(true)
}
}
2026-02-24 20:44:16 +00:00
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen bg-gray-50">
<div className="text-center">
<Loading loading={loading} />
</div>
</div>
)
}
return (
<>
<Helmet
titleTemplate={`%s | ${APP_NAME}`}
title={translate('::App.About')}
defaultTitle={APP_NAME}
></Helmet>
<div className={`min-h-screen bg-gray-50 ${isDesignMode && isPanelVisible ? 'xl:pr-[420px]' : ''}`}>
{isDesignMode && !isPanelVisible && (
2026-03-30 20:40:20 +00:00
<Button
type="button"
onClick={() => setIsPanelVisible(true)}
className="fixed right-4 top-1/2 z-40 -translate-y-1/2 rounded-full bg-slate-900 px-4 py-2 text-sm font-semibold text-white shadow-xl"
>
{translate('::Public.designer.showPanel')}
2026-03-30 20:40:20 +00:00
</Button>
)}
2026-02-24 20:44:16 +00:00
{/* Hero Section */}
<SelectableBlock
id="hero"
isActive={selectedBlockId === 'hero'}
isDesignMode={isDesignMode}
onSelect={handleSelectBlock}
>
2026-03-17 13:35:58 +00:00
<div className={content?.heroSectionStyleClass || 'relative bg-blue-900 text-white py-12'}>
<div
className="absolute inset-0 opacity-20"
style={{
backgroundImage: `url("${content?.heroImage ?? ABOUT_HERO_IMAGE}")`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}}
></div>
<div className="container mx-auto pt-20 relative">
2026-03-17 13:35:58 +00:00
<h1 className={content?.heroTitleStyleClass || 'text-5xl font-bold ml-4 mt-3 mb-2 text-white'}>
{content?.heroTitle}
</h1>
2026-03-17 13:35:58 +00:00
<p className={content?.heroSubtitleStyleClass || 'text-xl max-w-3xl ml-4'}>{content?.heroSubtitle}</p>
</div>
2026-02-24 20:44:16 +00:00
</div>
</SelectableBlock>
2026-02-24 20:44:16 +00:00
{/* Stats Section */}
<div className="py-10 bg-white">
<div className="container mx-auto px-4">
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
{content?.stats.map((stat, index) => {
2026-02-24 20:44:16 +00:00
const IconComponent = navigationIcon[stat.icon || '']
return (
<SelectableBlock
key={index}
id={`stat-${index}`}
isActive={selectedBlockId === `stat-${index}`}
isDesignMode={isDesignMode}
onSelect={handleSelectBlock}
>
2026-03-17 13:35:58 +00:00
<div className={stat.styleClass || 'text-center rounded-xl px-4 py-6'}>
{IconComponent && (
<IconComponent
className={`w-12 h-12 mx-auto mb-4 ${getIconColor(index)}`}
/>
)}
2026-03-17 13:35:58 +00:00
<div className={stat.valueStyleClass || 'text-4xl font-bold text-gray-900 mb-2'}>{stat.value}</div>
<div className={stat.labelStyleClass || 'text-gray-600'}>{stat.label}</div>
</div>
</SelectableBlock>
2026-02-24 20:44:16 +00:00
)
})}
</div>
</div>
</div>
{/* Main Content */}
<div className="py-6">
<div className="container mx-auto px-4">
<div className="mb-6">
<SelectableBlock
id="descriptions"
isActive={selectedBlockId === 'descriptions'}
isDesignMode={isDesignMode}
onSelect={handleSelectBlock}
>
2026-03-17 13:35:58 +00:00
<div className={content?.descriptionsContainerStyleClass || 'p-5 mx-auto text-gray-800 text-lg leading-relaxed shadow-md bg-white border-l-4 border-blue-600'}>
{content?.descriptions.map((item, index) => (
<p key={item.key || `description-${index}`} className={item.styleClass || ''}>
{item.text}
</p>
))}
</div>
</SelectableBlock>
2026-02-24 20:44:16 +00:00
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
{content?.sections.map((section, index) => (
<SelectableBlock
key={index}
id={`section-${index}`}
isActive={selectedBlockId === `section-${index}`}
isDesignMode={isDesignMode}
onSelect={handleSelectBlock}
>
2026-03-17 13:35:58 +00:00
<div className={section.cardStyleClass || 'bg-white p-8 rounded-xl shadow-lg'}>
<h3 className={section.titleStyleClass || 'text-2xl font-bold text-gray-900 mb-4'}>{section.title}</h3>
<p className={section.descriptionStyleClass || 'text-gray-700'}>{section.description}</p>
</div>
</SelectableBlock>
2026-02-24 20:44:16 +00:00
))}
</div>
</div>
</div>
<DesignerDrawer
isOpen={isDesignMode && isPanelVisible}
selection={selectedSelection}
pageTitle="About"
selectedLanguage={selectedLanguage}
languages={
languageOptions.length > 0
? languageOptions
: supportedLanguages.map((language) => ({
key: language,
cultureName: language,
displayName: language.toUpperCase(),
}))
}
onClose={() => setIsPanelVisible(false)}
onSave={handleSaveAndExit}
onLanguageChange={handleLanguageChange}
onReset={resetContent}
onFieldChange={handleFieldChange}
/>
2026-02-24 20:44:16 +00:00
</div>
</>
)
}
export default About