691 lines
24 KiB
TypeScript
691 lines
24 KiB
TypeScript
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
import type { FC } from 'react'
|
|
import PageTitle from '@/components/shared/PageTitle'
|
|
import { AboutDto } from '@/proxy/about/models'
|
|
import { getAbout, saveAboutPage } from '@/services/about'
|
|
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
|
|
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
|
|
styleClassKey: string
|
|
styleClass: string
|
|
}
|
|
|
|
interface AboutSectionContent {
|
|
title: string
|
|
description: string
|
|
titleKey: string
|
|
descriptionKey: string
|
|
cardStyleClassKey: string
|
|
cardStyleClass: string
|
|
titleStyleClassKey: string
|
|
titleStyleClass: string
|
|
descriptionStyleClassKey: string
|
|
descriptionStyleClass: string
|
|
}
|
|
|
|
interface AboutContent {
|
|
heroTitle: string
|
|
heroTitleKey: string
|
|
heroTitleStyleClassKey: string
|
|
heroTitleStyleClass: string
|
|
heroSubtitle: string
|
|
heroSubtitleKey: string
|
|
heroSubtitleStyleClassKey: string
|
|
heroSubtitleStyleClass: string
|
|
heroImage: string
|
|
heroImageKey: string
|
|
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'
|
|
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'
|
|
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+)?/)
|
|
|
|
if (!numericMatch) {
|
|
return null
|
|
}
|
|
|
|
const endValue = Number(numericMatch[0])
|
|
|
|
if (!Number.isFinite(endValue)) {
|
|
return null
|
|
}
|
|
|
|
const matchIndex = numericMatch.index ?? 0
|
|
|
|
return {
|
|
endValue,
|
|
prefix: value.slice(0, matchIndex),
|
|
suffix: suffix || value.slice(matchIndex + numericMatch[0].length),
|
|
hasDecimals: numericMatch[0].includes('.'),
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sayac animasyonu yalnizca deger ekrana girdiginde bir kez calisir;
|
|
* boylece sayfanin altindaki istatistikler kullanici oraya gelmeden bitmis olmaz.
|
|
*/
|
|
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])
|
|
|
|
useEffect(() => {
|
|
if (!stat.useCounter || !counterParts || !isVisible) {
|
|
return
|
|
}
|
|
|
|
const duration = Math.max(stat.counterDuration || 2000, 0)
|
|
let animationFrameId = 0
|
|
let startTime: number | null = null
|
|
|
|
const animate = (timestamp: number) => {
|
|
startTime ??= timestamp
|
|
|
|
const progress = duration === 0 ? 1 : Math.min((timestamp - startTime) / duration, 1)
|
|
setCurrentValue(counterParts.endValue * (1 - Math.pow(1 - progress, 3)))
|
|
|
|
if (progress < 1) {
|
|
animationFrameId = requestAnimationFrame(animate)
|
|
}
|
|
}
|
|
|
|
setCurrentValue(0)
|
|
animationFrameId = requestAnimationFrame(animate)
|
|
|
|
return () => cancelAnimationFrame(animationFrameId)
|
|
}, [counterParts, isVisible, stat.counterDuration, stat.useCounter])
|
|
|
|
if (!stat.useCounter || !counterParts) {
|
|
return <>{stat.value}</>
|
|
}
|
|
|
|
return (
|
|
<span ref={containerRef}>
|
|
{counterParts.prefix}
|
|
{counterParts.hasDecimals ? currentValue.toFixed(1) : Math.floor(currentValue)}
|
|
{counterParts.suffix}
|
|
</span>
|
|
)
|
|
}
|
|
|
|
function buildAboutContent(about: AboutDto | undefined, translate: TranslateFn): AboutContent {
|
|
return {
|
|
heroTitle: resolveLocalizedValue(translate, ABOUT_HERO_TITLE_KEY, 'About'),
|
|
heroTitleKey: ABOUT_HERO_TITLE_KEY,
|
|
heroTitleStyleClassKey: ABOUT_HERO_TITLE_STYLE_KEY,
|
|
heroTitleStyleClass: resolveLocalizedValue(
|
|
translate,
|
|
ABOUT_HERO_TITLE_STYLE_KEY,
|
|
HERO_TITLE_STYLE_DEFAULT,
|
|
),
|
|
heroSubtitle: resolveLocalizedValue(translate, ABOUT_HERO_SUBTITLE_KEY),
|
|
heroSubtitleKey: ABOUT_HERO_SUBTITLE_KEY,
|
|
heroSubtitleStyleClassKey: ABOUT_HERO_SUBTITLE_STYLE_KEY,
|
|
heroSubtitleStyleClass: resolveLocalizedValue(
|
|
translate,
|
|
ABOUT_HERO_SUBTITLE_STYLE_KEY,
|
|
HERO_SUBTITLE_STYLE_DEFAULT,
|
|
),
|
|
heroImage: resolveLocalizedValue(translate, ABOUT_HERO_IMAGE_KEY, ABOUT_HERO_IMAGE),
|
|
heroImageKey: ABOUT_HERO_IMAGE_KEY,
|
|
heroSectionStyleClassKey: ABOUT_HERO_SECTION_STYLE_KEY,
|
|
heroSectionStyleClass: resolveLocalizedValue(
|
|
translate,
|
|
ABOUT_HERO_SECTION_STYLE_KEY,
|
|
HERO_SECTION_STYLE_DEFAULT,
|
|
),
|
|
descriptionsContainerStyleClassKey: ABOUT_DESCRIPTIONS_CONTAINER_STYLE_KEY,
|
|
descriptionsContainerStyleClass: resolveLocalizedValue(
|
|
translate,
|
|
ABOUT_DESCRIPTIONS_CONTAINER_STYLE_KEY,
|
|
DESCRIPTIONS_CONTAINER_STYLE_DEFAULT,
|
|
),
|
|
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),
|
|
labelKey:
|
|
(isLikelyLocalizationKey(stat.labelKey) ? stat.labelKey : '') || `${prefix}.label`,
|
|
styleClassKey: `${prefix}.styleClass`,
|
|
styleClass: resolveLocalizedValue(translate, `${prefix}.styleClass`, STAT_STYLE_DEFAULT),
|
|
valueStyleClassKey: `${prefix}.valueStyleClass`,
|
|
valueStyleClass: resolveLocalizedValue(
|
|
translate,
|
|
`${prefix}.valueStyleClass`,
|
|
STAT_VALUE_STYLE_DEFAULT,
|
|
),
|
|
labelStyleClassKey: `${prefix}.labelStyleClass`,
|
|
labelStyleClass: resolveLocalizedValue(
|
|
translate,
|
|
`${prefix}.labelStyleClass`,
|
|
STAT_LABEL_STYLE_DEFAULT,
|
|
),
|
|
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`,
|
|
styleClass: resolveLocalizedValue(
|
|
translate,
|
|
`${prefix}.styleClass`,
|
|
index % 2 === 0 ? '' : 'text-center p-5 text-blue-800 dark:text-white',
|
|
),
|
|
}
|
|
}),
|
|
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:
|
|
(isLikelyLocalizationKey(section.descKey) ? section.descKey : '') ||
|
|
`${prefix}.description`,
|
|
cardStyleClassKey: `${prefix}.cardStyleClass`,
|
|
cardStyleClass: resolveLocalizedValue(
|
|
translate,
|
|
`${prefix}.cardStyleClass`,
|
|
SECTION_CARD_STYLE_DEFAULT,
|
|
),
|
|
titleStyleClassKey: `${prefix}.titleStyleClass`,
|
|
titleStyleClass: resolveLocalizedValue(
|
|
translate,
|
|
`${prefix}.titleStyleClass`,
|
|
SECTION_TITLE_STYLE_DEFAULT,
|
|
),
|
|
descriptionStyleClassKey: `${prefix}.descriptionStyleClass`,
|
|
descriptionStyleClass: resolveLocalizedValue(
|
|
translate,
|
|
`${prefix}.descriptionStyleClass`,
|
|
SECTION_DESCRIPTION_STYLE_DEFAULT,
|
|
),
|
|
}
|
|
}),
|
|
}
|
|
}
|
|
|
|
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 },
|
|
]),
|
|
],
|
|
}
|
|
}
|
|
|
|
const About: FC = () => {
|
|
const { translate } = useLocalization()
|
|
const [loading, setLoading] = useState(true)
|
|
const [about, setAbout] = useState<AboutDto>()
|
|
|
|
useEffect(() => {
|
|
const fetchAbout = async () => {
|
|
setLoading(true)
|
|
|
|
try {
|
|
const result = await getAbout()
|
|
setAbout(result.data)
|
|
} catch (error) {
|
|
console.error('About alinirken hata olustu:', error)
|
|
} 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' }),
|
|
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.*',
|
|
description: translateLabel(translate, 'Public.designer.descriptionsHint'),
|
|
fields: [
|
|
...content.descriptions.map((item, index) =>
|
|
textAreaField(`descriptions.${index}.text`, item.key, item.text, {
|
|
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),
|
|
),
|
|
],
|
|
}
|
|
}
|
|
|
|
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',
|
|
placeholder: translateLabel(translate, 'Public.designer.useCounter'),
|
|
}),
|
|
textField(`stats.${index}.counterEnd`, 'counterEnd', stat.counterEnd, {
|
|
group: 'advanced',
|
|
helpText: translateLabel(translate, 'Public.designer.counterEndHint'),
|
|
}),
|
|
textField(`stats.${index}.counterSuffix`, 'counterSuffix', stat.counterSuffix, {
|
|
group: 'advanced',
|
|
}),
|
|
numberField(
|
|
`stats.${index}.counterDuration`,
|
|
'counterDuration (ms)',
|
|
stat.counterDuration,
|
|
{
|
|
group: 'advanced',
|
|
},
|
|
),
|
|
styleField(`stats.${index}.styleClass`, stat.styleClassKey, stat.styleClass),
|
|
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,
|
|
description: translateLabel(translate, 'Public.designer.sectionHint'),
|
|
fields: [
|
|
textField(`sections.${index}.title`, section.titleKey, section.title),
|
|
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])
|
|
|
|
if (loading) {
|
|
return <PublicPageLoader />
|
|
}
|
|
|
|
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
|
|
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>
|
|
</div>
|
|
</SelectableBlock>
|
|
|
|
{/* Stats */}
|
|
<div className="bg-white py-10 dark:bg-gray-900">
|
|
<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>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Descriptions + sections */}
|
|
<div className="py-6">
|
|
<div className="container mx-auto px-4">
|
|
<div className="mb-6">
|
|
<SelectableBlock
|
|
id="descriptions"
|
|
label={translateLabel(translate, 'Public.designer.group.content')}
|
|
isActive={selectedBlockId === 'descriptions'}
|
|
isDesignMode={isDesignMode}
|
|
onSelect={selectBlock}
|
|
>
|
|
<div
|
|
className={
|
|
content?.descriptionsContainerStyleClass || DESCRIPTIONS_CONTAINER_STYLE_DEFAULT
|
|
}
|
|
>
|
|
{content?.descriptions.map((item, index) => (
|
|
<p key={item.key || `description-${index}`} className={item.styleClass}>
|
|
{item.text}
|
|
</p>
|
|
))}
|
|
</div>
|
|
</SelectableBlock>
|
|
</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>
|
|
<p
|
|
className={section.descriptionStyleClass || SECTION_DESCRIPTION_STYLE_DEFAULT}
|
|
>
|
|
{section.description}
|
|
</p>
|
|
</div>
|
|
</SelectableBlock>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<DesignerLayer pageTitle="About" selection={selection} designer={designer} />
|
|
</div>
|
|
</>
|
|
)
|
|
}
|
|
|
|
export default About
|