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'
|
2026-03-17 09:54:25 +00:00
|
|
|
|
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-17 09:54:25 +00:00
|
|
|
|
import { ROUTES_ENUM } from '@/routes/route.constant'
|
|
|
|
|
|
import { useStoreState } from '@/store'
|
|
|
|
|
|
import { useStoreActions } from '@/store'
|
|
|
|
|
|
import { useNavigate } from 'react-router-dom'
|
|
|
|
|
|
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
|
|
|
|
|
|
useCounter?: boolean
|
|
|
|
|
|
counterEnd?: string
|
|
|
|
|
|
counterSuffix?: string
|
|
|
|
|
|
counterDuration?: number
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface AboutDescriptionContent {
|
|
|
|
|
|
key: string
|
|
|
|
|
|
text: string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface AboutSectionContent {
|
|
|
|
|
|
title: string
|
|
|
|
|
|
description: string
|
|
|
|
|
|
titleKey: string
|
|
|
|
|
|
descriptionKey: string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface AboutContent {
|
|
|
|
|
|
heroTitle: string
|
|
|
|
|
|
heroTitleKey: string
|
|
|
|
|
|
heroSubtitle: string
|
|
|
|
|
|
heroSubtitleKey: string
|
|
|
|
|
|
heroImage: string
|
|
|
|
|
|
heroImageKey: 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'
|
|
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
|
heroSubtitle: resolveLocalizedValue(translate, ABOUT_HERO_SUBTITLE_KEY),
|
|
|
|
|
|
heroSubtitleKey: ABOUT_HERO_SUBTITLE_KEY,
|
|
|
|
|
|
heroImage: resolveLocalizedValue(translate, ABOUT_HERO_IMAGE_KEY, ABOUT_HERO_IMAGE),
|
|
|
|
|
|
heroImageKey: ABOUT_HERO_IMAGE_KEY,
|
|
|
|
|
|
stats:
|
|
|
|
|
|
about?.statsDto.map((stat) => ({
|
|
|
|
|
|
icon: stat.icon || '',
|
|
|
|
|
|
value: stat.value,
|
|
|
|
|
|
label: resolveLocalizedValue(translate, stat.labelKey, stat.labelKey),
|
|
|
|
|
|
labelKey:
|
|
|
|
|
|
(isLikelyLocalizationKey(stat.labelKey) ? stat.labelKey : undefined) ||
|
|
|
|
|
|
`Public.about.dynamic.stat.${stat.value}.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),
|
|
|
|
|
|
})) ?? [],
|
|
|
|
|
|
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-02-24 20:44:16 +00:00
|
|
|
|
|
|
|
|
|
|
const About: React.FC = () => {
|
|
|
|
|
|
const { translate } = useLocalization()
|
2026-03-17 09:54:25 +00:00
|
|
|
|
const navigate = useNavigate()
|
|
|
|
|
|
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)
|
2026-03-17 09:54:25 +00:00
|
|
|
|
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',
|
|
|
|
|
|
]
|
|
|
|
|
|
|
2026-03-17 09:54:25 +00:00
|
|
|
|
function getIconColor(index: number) {
|
|
|
|
|
|
return iconColors[index % iconColors.length]
|
2026-02-24 20:44:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-17 09:54:25 +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()
|
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
2026-03-17 09:54:25 +00:00
|
|
|
|
const updateContent = (updater: (current: AboutContent) => AboutContent) => {
|
|
|
|
|
|
setContent((current) => {
|
|
|
|
|
|
if (!current) {
|
|
|
|
|
|
return current
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return updater(current)
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleFieldChange = (fieldKey: string, value: string | string[]) => {
|
|
|
|
|
|
updateContent((current) => {
|
|
|
|
|
|
if (fieldKey === 'heroTitle' || fieldKey === 'heroSubtitle' || fieldKey === 'heroImage') {
|
|
|
|
|
|
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,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'heroSubtitle',
|
|
|
|
|
|
label: content.heroSubtitleKey,
|
|
|
|
|
|
type: 'textarea',
|
|
|
|
|
|
value: content.heroSubtitle,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'heroImage',
|
|
|
|
|
|
label: content.heroImageKey,
|
|
|
|
|
|
type: 'image',
|
|
|
|
|
|
value: content.heroImage,
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (selectedBlockId === 'descriptions') {
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: 'descriptions',
|
|
|
|
|
|
title: 'Public.about.description.*',
|
|
|
|
|
|
description: 'Orta bolumdeki aciklama metinlerini duzenleyin.',
|
|
|
|
|
|
fields: content.descriptions.map((item, index) => ({
|
|
|
|
|
|
key: `description-${index}`,
|
|
|
|
|
|
label: item.key || `Public.about.dynamic.description.${index + 1}`,
|
|
|
|
|
|
type: 'textarea',
|
|
|
|
|
|
value: item.text,
|
|
|
|
|
|
rows: index % 2 === 0 ? 4 : 3,
|
|
|
|
|
|
})),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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',
|
|
|
|
|
|
label: translate('::Public.designer.value'),
|
|
|
|
|
|
type: 'text',
|
|
|
|
|
|
value: stat.value
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'label',
|
|
|
|
|
|
label: translate('::' + stat.labelKey),
|
|
|
|
|
|
type: 'text',
|
|
|
|
|
|
value: stat.label,
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
|
})),
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
await getConfig(false)
|
|
|
|
|
|
setSelectedBlockId(null)
|
|
|
|
|
|
navigate(ROUTES_ENUM.public.about, { replace: true })
|
|
|
|
|
|
} 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>
|
|
|
|
|
|
|
2026-03-17 09:54:25 +00:00
|
|
|
|
<div className={`min-h-screen bg-gray-50 ${isDesignMode && isPanelVisible ? 'xl:pr-[420px]' : ''}`}>
|
|
|
|
|
|
{isDesignMode && !isPanelVisible && (
|
|
|
|
|
|
<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')}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2026-02-24 20:44:16 +00:00
|
|
|
|
{/* Hero Section */}
|
2026-03-17 09:54:25 +00:00
|
|
|
|
<SelectableBlock
|
|
|
|
|
|
id="hero"
|
|
|
|
|
|
isActive={selectedBlockId === 'hero'}
|
|
|
|
|
|
isDesignMode={isDesignMode}
|
|
|
|
|
|
onSelect={handleSelectBlock}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="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">
|
|
|
|
|
|
<h1 className="text-5xl font-bold ml-4 mt-3 mb-2 text-white">
|
|
|
|
|
|
{content?.heroTitle}
|
|
|
|
|
|
</h1>
|
|
|
|
|
|
<p className="text-xl max-w-3xl ml-4">{content?.heroSubtitle}</p>
|
|
|
|
|
|
</div>
|
2026-02-24 20:44:16 +00:00
|
|
|
|
</div>
|
2026-03-17 09:54:25 +00:00
|
|
|
|
</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">
|
2026-03-17 09:54:25 +00:00
|
|
|
|
{content?.stats.map((stat, index) => {
|
2026-02-24 20:44:16 +00:00
|
|
|
|
const IconComponent = navigationIcon[stat.icon || '']
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
2026-03-17 09:54:25 +00:00
|
|
|
|
<SelectableBlock
|
|
|
|
|
|
key={index}
|
|
|
|
|
|
id={`stat-${index}`}
|
|
|
|
|
|
isActive={selectedBlockId === `stat-${index}`}
|
|
|
|
|
|
isDesignMode={isDesignMode}
|
|
|
|
|
|
onSelect={handleSelectBlock}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="text-center rounded-xl px-4 py-6">
|
|
|
|
|
|
{IconComponent && (
|
|
|
|
|
|
<IconComponent
|
|
|
|
|
|
className={`w-12 h-12 mx-auto mb-4 ${getIconColor(index)}`}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<div className="text-4xl font-bold text-gray-900 mb-2">{stat.value}</div>
|
|
|
|
|
|
<div className="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">
|
2026-03-17 09:54:25 +00:00
|
|
|
|
<SelectableBlock
|
|
|
|
|
|
id="descriptions"
|
|
|
|
|
|
isActive={selectedBlockId === 'descriptions'}
|
|
|
|
|
|
isDesignMode={isDesignMode}
|
|
|
|
|
|
onSelect={handleSelectBlock}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="p-5 mx-auto mx-auto text-gray-800 text-lg leading-relaxed shadow-md bg-white border-l-4 border-blue-600">
|
|
|
|
|
|
<p>{content?.descriptions[0]?.text}</p>
|
|
|
|
|
|
<p className="text-center p-5 text-blue-800">{content?.descriptions[1]?.text}</p>
|
|
|
|
|
|
<p>{content?.descriptions[2]?.text}</p>
|
|
|
|
|
|
<p className="text-center p-5 text-blue-800">{content?.descriptions[3]?.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">
|
2026-03-17 09:54:25 +00:00
|
|
|
|
{content?.sections.map((section, index) => (
|
|
|
|
|
|
<SelectableBlock
|
|
|
|
|
|
key={index}
|
|
|
|
|
|
id={`section-${index}`}
|
|
|
|
|
|
isActive={selectedBlockId === `section-${index}`}
|
|
|
|
|
|
isDesignMode={isDesignMode}
|
|
|
|
|
|
onSelect={handleSelectBlock}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="bg-white p-8 rounded-xl shadow-lg">
|
|
|
|
|
|
<h3 className="text-2xl font-bold text-gray-900 mb-4">{section.title}</h3>
|
|
|
|
|
|
<p className="text-gray-700">{section.description}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</SelectableBlock>
|
2026-02-24 20:44:16 +00:00
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-03-17 09:54:25 +00:00
|
|
|
|
|
|
|
|
|
|
<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
|