sozsoft-platform/ui/src/views/public/About.tsx
2026-03-17 12:54:25 +03:00

589 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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'
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`,
})) ?? [],
}
}
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)),
)
const [loading, setLoading] = useState(true)
const [isSaving, setIsSaving] = useState(false)
const [isPanelVisible, setIsPanelVisible] = useState(true)
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]
}
const initialContent = !loading ? buildAboutContent(about, translate) : null
const {
content,
isDesignMode,
selectedBlockId,
selectedLanguage,
supportedLanguages,
setContent,
setSelectedBlockId,
resetContent,
} = useDesignerState<AboutContent>('about', initialContent, {
currentLanguage,
supportedLanguages: editorLanguages,
})
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)
}
}
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>
)}
{/* 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>
</div>
</SelectableBlock>
{/* 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) => {
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>
)
})}
</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>
</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>
))}
</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}
/>
</div>
</>
)
}
export default About