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

590 lines
18 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'
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()
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)
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) => {
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>
<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 */}
<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>
</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}
>
<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">
<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">
{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>
<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