2026-03-17 11:27:30 +00:00
|
|
|
import React, { useEffect, useState } from 'react'
|
2026-02-24 20:44:16 +00:00
|
|
|
import { Link } from 'react-router-dom'
|
|
|
|
|
import {
|
|
|
|
|
FaArrowRight,
|
|
|
|
|
FaChevronLeft,
|
|
|
|
|
FaChevronRight,
|
|
|
|
|
} from 'react-icons/fa'
|
|
|
|
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
|
|
|
|
import { ROUTES_ENUM } from '@/routes/route.constant'
|
|
|
|
|
import { Helmet } from 'react-helmet'
|
|
|
|
|
import { APP_NAME } from '@/constants/app.constant'
|
2026-03-17 11:27:30 +00:00
|
|
|
import { useStoreActions, useStoreState } from '@/store'
|
|
|
|
|
import Loading from '@/components/shared/Loading'
|
|
|
|
|
import navigationIcon from '@/proxy/menus/navigation-icon.config'
|
|
|
|
|
import DesignerDrawer from './designer/DesignerDrawer'
|
|
|
|
|
import SelectableBlock from './designer/SelectableBlock'
|
|
|
|
|
import { DesignerSelection } from './designer/types'
|
|
|
|
|
import { useDesignerState } from './designer/useDesignerState'
|
|
|
|
|
import { getHome, HomeDto, saveHomePage } from '@/services/home.service'
|
2026-03-17 11:46:20 +00:00
|
|
|
import { Notification, toast } from '@/components/ui'
|
2026-02-24 20:44:16 +00:00
|
|
|
|
2026-03-17 11:27:30 +00:00
|
|
|
interface HomeSlideServiceContent {
|
|
|
|
|
icon: string
|
|
|
|
|
title: string
|
|
|
|
|
titleKey: string
|
|
|
|
|
description: string
|
|
|
|
|
descriptionKey: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface HomeSlideContent {
|
|
|
|
|
title: string
|
|
|
|
|
titleKey: string
|
|
|
|
|
subtitle: string
|
|
|
|
|
subtitleKey: string
|
|
|
|
|
services: HomeSlideServiceContent[]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface HomeFeatureContent {
|
|
|
|
|
icon: string
|
|
|
|
|
title: string
|
|
|
|
|
titleKey: string
|
|
|
|
|
description: string
|
|
|
|
|
descriptionKey: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface HomeSolutionContent {
|
|
|
|
|
icon: string
|
|
|
|
|
colorClass: string
|
|
|
|
|
title: string
|
|
|
|
|
titleKey: string
|
|
|
|
|
description: string
|
|
|
|
|
descriptionKey: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface HomeContent {
|
|
|
|
|
heroBackgroundImage: string
|
|
|
|
|
heroBackgroundImageKey: string
|
|
|
|
|
heroPrimaryCtaLabel: string
|
|
|
|
|
heroPrimaryCtaKey: string
|
|
|
|
|
heroSecondaryCtaLabel: string
|
|
|
|
|
heroSecondaryCtaKey: string
|
|
|
|
|
featuresTitle: string
|
|
|
|
|
featuresTitleKey: string
|
|
|
|
|
featuresSubtitle: string
|
|
|
|
|
featuresSubtitleKey: string
|
|
|
|
|
solutionsTitle: string
|
|
|
|
|
solutionsTitleKey: string
|
|
|
|
|
solutionsSubtitle: string
|
|
|
|
|
solutionsSubtitleKey: string
|
|
|
|
|
ctaTitle: string
|
|
|
|
|
ctaTitleKey: string
|
|
|
|
|
ctaSubtitle: string
|
|
|
|
|
ctaSubtitleKey: string
|
|
|
|
|
ctaButtonLabel: string
|
|
|
|
|
ctaButtonLabelKey: string
|
|
|
|
|
slides: HomeSlideContent[]
|
|
|
|
|
features: HomeFeatureContent[]
|
|
|
|
|
solutions: HomeSolutionContent[]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const HOME_HERO_BACKGROUND_KEY = 'Public.home.hero.backgroundImage'
|
|
|
|
|
const HOME_HERO_DEFAULT_IMAGE =
|
|
|
|
|
'https://images.pexels.com/photos/3183150/pexels-photo-3183150.jpeg?auto=compress&cs=tinysrgb&w=1920'
|
|
|
|
|
|
|
|
|
|
const HOME_HERO_PRIMARY_CTA_KEY = 'Public.hero.cta.consultation'
|
|
|
|
|
const HOME_HERO_SECONDARY_CTA_KEY = 'Public.hero.cta.discover'
|
|
|
|
|
const HOME_FEATURES_TITLE_KEY = 'Public.features.title'
|
|
|
|
|
const HOME_FEATURES_SUBTITLE_KEY = 'Public.features.subtitle'
|
|
|
|
|
const HOME_SOLUTIONS_TITLE_KEY = 'Public.solutions.title'
|
|
|
|
|
const HOME_SOLUTIONS_SUBTITLE_KEY = 'Public.solutions.subtitle'
|
|
|
|
|
const HOME_CTA_TITLE_KEY = 'Public.common.getStarted'
|
|
|
|
|
const HOME_CTA_SUBTITLE_KEY = 'Public.common.contact'
|
|
|
|
|
const HOME_CTA_BUTTON_KEY = 'Public.common.learnMore'
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
2026-02-24 20:44:16 +00:00
|
|
|
|
2026-03-17 11:27:30 +00:00
|
|
|
if (!isLikelyLocalizationKey(keyOrValue)) {
|
|
|
|
|
return keyOrValue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const translatedValue = translate('::' + keyOrValue)
|
|
|
|
|
return translatedValue === keyOrValue ? fallback || keyOrValue : translatedValue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function defaultSlides() {
|
|
|
|
|
return [
|
2026-02-24 20:44:16 +00:00
|
|
|
{
|
2026-03-17 11:27:30 +00:00
|
|
|
titleKey: 'Public.hero.slide1.title',
|
|
|
|
|
subtitleKey: 'Public.hero.slide1.subtitle',
|
2026-02-24 20:44:16 +00:00
|
|
|
services: [
|
|
|
|
|
{
|
2026-03-17 11:27:30 +00:00
|
|
|
icon: 'FaCalendarAlt',
|
|
|
|
|
titleKey: 'Public.hero.slide1.service1.title',
|
|
|
|
|
descriptionKey: 'Public.hero.slide1.service1.desc',
|
2026-02-24 20:44:16 +00:00
|
|
|
},
|
|
|
|
|
{
|
2026-03-17 11:27:30 +00:00
|
|
|
icon: 'FaUsers',
|
|
|
|
|
titleKey: 'Public.hero.slide1.service2.title',
|
|
|
|
|
descriptionKey: 'Public.hero.slide1.service2.desc',
|
2026-02-24 20:44:16 +00:00
|
|
|
},
|
|
|
|
|
{
|
2026-03-17 11:27:30 +00:00
|
|
|
icon: 'FaShieldAlt',
|
|
|
|
|
titleKey: 'Public.hero.slide1.service3.title',
|
|
|
|
|
descriptionKey: 'Public.hero.slide1.service3.desc',
|
2026-02-24 20:44:16 +00:00
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{
|
2026-03-17 11:27:30 +00:00
|
|
|
titleKey: 'Public.hero.slide2.title',
|
|
|
|
|
subtitleKey: 'Public.hero.slide2.subtitle',
|
2026-02-24 20:44:16 +00:00
|
|
|
services: [
|
|
|
|
|
{
|
2026-03-17 11:27:30 +00:00
|
|
|
icon: 'FaChartBar',
|
|
|
|
|
titleKey: 'Public.hero.slide2.service1.title',
|
|
|
|
|
descriptionKey: 'Public.hero.slide2.service1.desc',
|
2026-02-24 20:44:16 +00:00
|
|
|
},
|
|
|
|
|
{
|
2026-03-17 11:27:30 +00:00
|
|
|
icon: 'FaCreditCard',
|
|
|
|
|
titleKey: 'Public.hero.slide2.service2.title',
|
|
|
|
|
descriptionKey: 'Public.hero.slide2.service2.desc',
|
2026-02-24 20:44:16 +00:00
|
|
|
},
|
|
|
|
|
{
|
2026-03-17 11:27:30 +00:00
|
|
|
icon: 'FaDatabase',
|
|
|
|
|
titleKey: 'Public.hero.slide2.service3.title',
|
|
|
|
|
descriptionKey: 'Public.hero.slide2.service3.desc',
|
2026-02-24 20:44:16 +00:00
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{
|
2026-03-17 11:27:30 +00:00
|
|
|
titleKey: 'Public.hero.slide3.title',
|
|
|
|
|
subtitleKey: 'Public.hero.slide3.subtitle',
|
2026-02-24 20:44:16 +00:00
|
|
|
services: [
|
|
|
|
|
{
|
2026-03-17 11:27:30 +00:00
|
|
|
icon: 'FaDesktop',
|
|
|
|
|
titleKey: 'Public.hero.slide3.service1.title',
|
|
|
|
|
descriptionKey: 'Public.hero.slide3.service1.desc',
|
2026-02-24 20:44:16 +00:00
|
|
|
},
|
|
|
|
|
{
|
2026-03-17 11:27:30 +00:00
|
|
|
icon: 'FaServer',
|
|
|
|
|
titleKey: 'Public.hero.slide3.service2.title',
|
|
|
|
|
descriptionKey: 'Public.hero.slide3.service2.desc',
|
2026-02-24 20:44:16 +00:00
|
|
|
},
|
|
|
|
|
{
|
2026-03-17 11:27:30 +00:00
|
|
|
icon: 'FaMobileAlt',
|
|
|
|
|
titleKey: 'Public.hero.slide3.service3.title',
|
|
|
|
|
descriptionKey: 'Public.hero.slide3.service3.desc',
|
2026-02-24 20:44:16 +00:00
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
]
|
2026-03-17 11:27:30 +00:00
|
|
|
}
|
2026-02-24 20:44:16 +00:00
|
|
|
|
2026-03-17 11:27:30 +00:00
|
|
|
function defaultFeatures() {
|
|
|
|
|
return [
|
|
|
|
|
{ icon: 'FaUsers', titleKey: 'Public.features.reliable', descriptionKey: 'Public.features.reliable.desc' },
|
2026-02-24 20:44:16 +00:00
|
|
|
{
|
2026-03-17 11:27:30 +00:00
|
|
|
icon: 'FaCalendarAlt',
|
|
|
|
|
titleKey: 'App.Coordinator.Classroom.Planning',
|
|
|
|
|
descriptionKey: 'Public.features.rapid.desc',
|
2026-02-24 20:44:16 +00:00
|
|
|
},
|
2026-03-17 11:27:30 +00:00
|
|
|
{ icon: 'FaBookOpen', titleKey: 'Public.features.expert', descriptionKey: 'Public.features.expert.desc' },
|
2026-02-24 20:44:16 +00:00
|
|
|
{
|
2026-03-17 11:27:30 +00:00
|
|
|
icon: 'FaCreditCard',
|
|
|
|
|
titleKey: 'Public.features.muhasebe',
|
|
|
|
|
descriptionKey: 'Public.features.muhasebe.desc',
|
2026-02-24 20:44:16 +00:00
|
|
|
},
|
|
|
|
|
{
|
2026-03-17 11:27:30 +00:00
|
|
|
icon: 'FaRegComment',
|
|
|
|
|
titleKey: 'Public.features.iletisim',
|
|
|
|
|
descriptionKey: 'Public.features.iletisim.desc',
|
2026-02-24 20:44:16 +00:00
|
|
|
},
|
2026-03-17 11:27:30 +00:00
|
|
|
{ icon: 'FaPhone', titleKey: 'Public.features.mobil', descriptionKey: 'Public.features.mobil.desc' },
|
|
|
|
|
{ icon: 'FaChartBar', titleKey: 'Public.features.scalable', descriptionKey: 'Public.features.scalable.desc' },
|
2026-02-24 20:44:16 +00:00
|
|
|
{
|
2026-03-17 11:27:30 +00:00
|
|
|
icon: 'FaShieldAlt',
|
|
|
|
|
titleKey: 'Public.features.guvenlik',
|
|
|
|
|
descriptionKey: 'Public.features.guvenlik.desc',
|
2026-02-24 20:44:16 +00:00
|
|
|
},
|
|
|
|
|
]
|
2026-03-17 11:27:30 +00:00
|
|
|
}
|
2026-02-24 20:44:16 +00:00
|
|
|
|
2026-03-17 11:27:30 +00:00
|
|
|
function defaultSolutions() {
|
|
|
|
|
return [
|
2026-02-24 20:44:16 +00:00
|
|
|
{
|
2026-03-17 11:27:30 +00:00
|
|
|
icon: 'FaDesktop',
|
|
|
|
|
colorClass: 'bg-blue-600',
|
|
|
|
|
titleKey: 'Public.services.web.title',
|
|
|
|
|
descriptionKey: 'Public.solutions.web.desc',
|
2026-02-24 20:44:16 +00:00
|
|
|
},
|
|
|
|
|
{
|
2026-03-17 11:27:30 +00:00
|
|
|
icon: 'FaMobileAlt',
|
|
|
|
|
colorClass: 'bg-purple-600',
|
|
|
|
|
titleKey: 'Public.services.mobile.title',
|
|
|
|
|
descriptionKey: 'Public.solutions.mobile.desc',
|
2026-02-24 20:44:16 +00:00
|
|
|
},
|
|
|
|
|
{
|
2026-03-17 11:27:30 +00:00
|
|
|
icon: 'FaServer',
|
|
|
|
|
colorClass: 'bg-green-600',
|
|
|
|
|
titleKey: 'Public.solutions.custom.title',
|
|
|
|
|
descriptionKey: 'Public.solutions.custom.desc',
|
2026-02-24 20:44:16 +00:00
|
|
|
},
|
|
|
|
|
{
|
2026-03-17 11:27:30 +00:00
|
|
|
icon: 'FaDatabase',
|
|
|
|
|
colorClass: 'bg-red-600',
|
|
|
|
|
titleKey: 'Public.solutions.database.title',
|
|
|
|
|
descriptionKey: 'Public.solutions.database.desc',
|
2026-02-24 20:44:16 +00:00
|
|
|
},
|
|
|
|
|
]
|
2026-03-17 11:27:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildHomeContent(home: HomeDto | undefined, translate: (key: string) => string): HomeContent {
|
|
|
|
|
const slideItems = home?.slidesDto?.length ? home.slidesDto : defaultSlides()
|
|
|
|
|
const featureItems = home?.featuresDto?.length ? home.featuresDto : defaultFeatures()
|
|
|
|
|
const solutionItems = home?.solutionsDto?.length ? home.solutionsDto : defaultSolutions()
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
heroBackgroundImage: resolveLocalizedValue(
|
|
|
|
|
translate,
|
|
|
|
|
home?.heroBackgroundImageKey || HOME_HERO_BACKGROUND_KEY,
|
|
|
|
|
HOME_HERO_DEFAULT_IMAGE,
|
|
|
|
|
),
|
|
|
|
|
heroBackgroundImageKey: home?.heroBackgroundImageKey || HOME_HERO_BACKGROUND_KEY,
|
|
|
|
|
heroPrimaryCtaLabel: resolveLocalizedValue(
|
|
|
|
|
translate,
|
|
|
|
|
home?.heroPrimaryCtaKey || HOME_HERO_PRIMARY_CTA_KEY,
|
|
|
|
|
),
|
|
|
|
|
heroPrimaryCtaKey: home?.heroPrimaryCtaKey || HOME_HERO_PRIMARY_CTA_KEY,
|
|
|
|
|
heroSecondaryCtaLabel: resolveLocalizedValue(
|
|
|
|
|
translate,
|
|
|
|
|
home?.heroSecondaryCtaKey || HOME_HERO_SECONDARY_CTA_KEY,
|
|
|
|
|
),
|
|
|
|
|
heroSecondaryCtaKey: home?.heroSecondaryCtaKey || HOME_HERO_SECONDARY_CTA_KEY,
|
|
|
|
|
featuresTitle: resolveLocalizedValue(translate, home?.featuresTitleKey || HOME_FEATURES_TITLE_KEY),
|
|
|
|
|
featuresTitleKey: home?.featuresTitleKey || HOME_FEATURES_TITLE_KEY,
|
|
|
|
|
featuresSubtitle: resolveLocalizedValue(
|
|
|
|
|
translate,
|
|
|
|
|
home?.featuresSubtitleKey || HOME_FEATURES_SUBTITLE_KEY,
|
|
|
|
|
),
|
|
|
|
|
featuresSubtitleKey: home?.featuresSubtitleKey || HOME_FEATURES_SUBTITLE_KEY,
|
|
|
|
|
solutionsTitle: resolveLocalizedValue(translate, home?.solutionsTitleKey || HOME_SOLUTIONS_TITLE_KEY),
|
|
|
|
|
solutionsTitleKey: home?.solutionsTitleKey || HOME_SOLUTIONS_TITLE_KEY,
|
|
|
|
|
solutionsSubtitle: resolveLocalizedValue(
|
|
|
|
|
translate,
|
|
|
|
|
home?.solutionsSubtitleKey || HOME_SOLUTIONS_SUBTITLE_KEY,
|
|
|
|
|
),
|
|
|
|
|
solutionsSubtitleKey: home?.solutionsSubtitleKey || HOME_SOLUTIONS_SUBTITLE_KEY,
|
|
|
|
|
ctaTitle: resolveLocalizedValue(translate, home?.ctaTitleKey || HOME_CTA_TITLE_KEY),
|
|
|
|
|
ctaTitleKey: home?.ctaTitleKey || HOME_CTA_TITLE_KEY,
|
|
|
|
|
ctaSubtitle: resolveLocalizedValue(translate, home?.ctaSubtitleKey || HOME_CTA_SUBTITLE_KEY),
|
|
|
|
|
ctaSubtitleKey: home?.ctaSubtitleKey || HOME_CTA_SUBTITLE_KEY,
|
|
|
|
|
ctaButtonLabel: resolveLocalizedValue(translate, home?.ctaButtonLabelKey || HOME_CTA_BUTTON_KEY),
|
|
|
|
|
ctaButtonLabelKey: home?.ctaButtonLabelKey || HOME_CTA_BUTTON_KEY,
|
|
|
|
|
slides: slideItems.map((slide, slideIndex) => ({
|
|
|
|
|
title: resolveLocalizedValue(translate, slide.titleKey, slide.titleKey),
|
|
|
|
|
titleKey: slide.titleKey || `Public.home.dynamic.slide.${slideIndex + 1}.title`,
|
|
|
|
|
subtitle: resolveLocalizedValue(translate, slide.subtitleKey, slide.subtitleKey),
|
|
|
|
|
subtitleKey: slide.subtitleKey || `Public.home.dynamic.slide.${slideIndex + 1}.subtitle`,
|
|
|
|
|
services: (slide.services || []).map((service, serviceIndex) => ({
|
|
|
|
|
icon: service.icon || 'FaCircle',
|
|
|
|
|
title: resolveLocalizedValue(translate, service.titleKey, service.titleKey),
|
|
|
|
|
titleKey:
|
|
|
|
|
service.titleKey ||
|
|
|
|
|
`Public.home.dynamic.slide.${slideIndex + 1}.service.${serviceIndex + 1}.title`,
|
|
|
|
|
description: resolveLocalizedValue(translate, service.descriptionKey, service.descriptionKey),
|
|
|
|
|
descriptionKey:
|
|
|
|
|
service.descriptionKey ||
|
|
|
|
|
`Public.home.dynamic.slide.${slideIndex + 1}.service.${serviceIndex + 1}.description`,
|
|
|
|
|
})),
|
|
|
|
|
})),
|
|
|
|
|
features: featureItems.map((feature, index) => ({
|
|
|
|
|
icon: feature.icon || 'FaCircle',
|
|
|
|
|
title: resolveLocalizedValue(translate, feature.titleKey, feature.titleKey),
|
|
|
|
|
titleKey: feature.titleKey || `Public.home.dynamic.feature.${index + 1}.title`,
|
|
|
|
|
description: resolveLocalizedValue(translate, feature.descriptionKey, feature.descriptionKey),
|
|
|
|
|
descriptionKey: feature.descriptionKey || `Public.home.dynamic.feature.${index + 1}.description`,
|
|
|
|
|
})),
|
|
|
|
|
solutions: solutionItems.map((solution, index) => ({
|
|
|
|
|
icon: solution.icon || 'FaCircle',
|
|
|
|
|
colorClass: solution.colorClass || 'bg-blue-600',
|
|
|
|
|
title: resolveLocalizedValue(translate, solution.titleKey, solution.titleKey),
|
|
|
|
|
titleKey: solution.titleKey || `Public.home.dynamic.solution.${index + 1}.title`,
|
|
|
|
|
description: resolveLocalizedValue(translate, solution.descriptionKey, solution.descriptionKey),
|
|
|
|
|
descriptionKey:
|
|
|
|
|
solution.descriptionKey || `Public.home.dynamic.solution.${index + 1}.description`,
|
|
|
|
|
})),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const Home: 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)),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const [home, setHome] = useState<HomeDto>()
|
|
|
|
|
const [loading, setLoading] = useState(true)
|
|
|
|
|
const [isSaving, setIsSaving] = useState(false)
|
|
|
|
|
const [isPanelVisible, setIsPanelVisible] = useState(true)
|
|
|
|
|
const [currentSlide, setCurrentSlide] = useState(0)
|
|
|
|
|
|
|
|
|
|
const initialContent = !loading ? buildHomeContent(home, translate) : null
|
|
|
|
|
const {
|
|
|
|
|
content,
|
|
|
|
|
isDesignMode,
|
|
|
|
|
selectedBlockId,
|
|
|
|
|
selectedLanguage,
|
|
|
|
|
supportedLanguages,
|
|
|
|
|
setContent,
|
|
|
|
|
setSelectedBlockId,
|
|
|
|
|
resetContent,
|
|
|
|
|
} = useDesignerState<HomeContent>('home', initialContent, {
|
|
|
|
|
currentLanguage,
|
|
|
|
|
supportedLanguages: editorLanguages,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
setLoading(true)
|
|
|
|
|
const fetchHome = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const result = await getHome()
|
|
|
|
|
setHome(result.data)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Home alinirken hata olustu:', error)
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fetchHome()
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!content?.slides?.length) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (currentSlide >= content.slides.length) {
|
|
|
|
|
setCurrentSlide(0)
|
|
|
|
|
}
|
|
|
|
|
}, [content?.slides.length, currentSlide])
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!content?.slides?.length) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const timer = setInterval(() => {
|
|
|
|
|
setCurrentSlide((prev) => (prev + 1) % content.slides.length)
|
|
|
|
|
}, 10000)
|
|
|
|
|
|
|
|
|
|
return () => clearInterval(timer)
|
|
|
|
|
}, [content?.slides.length])
|
|
|
|
|
|
|
|
|
|
const nextSlide = () => {
|
|
|
|
|
const size = content?.slides.length || 1
|
|
|
|
|
setCurrentSlide((prev) => (prev + 1) % size)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const prevSlide = () => {
|
|
|
|
|
const size = content?.slides.length || 1
|
|
|
|
|
setCurrentSlide((prev) => (prev - 1 + size) % size)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const updateContent = (updater: (current: HomeContent) => HomeContent) => {
|
|
|
|
|
setContent((current) => {
|
|
|
|
|
if (!current) {
|
|
|
|
|
return current
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return updater(current)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleFieldChange = (fieldKey: string, value: string | string[]) => {
|
|
|
|
|
const nextValue = value as string
|
|
|
|
|
|
|
|
|
|
updateContent((current) => {
|
|
|
|
|
if (
|
|
|
|
|
fieldKey === 'heroBackgroundImage' ||
|
|
|
|
|
fieldKey === 'heroPrimaryCtaLabel' ||
|
|
|
|
|
fieldKey === 'heroSecondaryCtaLabel' ||
|
|
|
|
|
fieldKey === 'featuresTitle' ||
|
|
|
|
|
fieldKey === 'featuresSubtitle' ||
|
|
|
|
|
fieldKey === 'solutionsTitle' ||
|
|
|
|
|
fieldKey === 'solutionsSubtitle' ||
|
|
|
|
|
fieldKey === 'ctaTitle' ||
|
|
|
|
|
fieldKey === 'ctaSubtitle' ||
|
|
|
|
|
fieldKey === 'ctaButtonLabel'
|
|
|
|
|
) {
|
|
|
|
|
return {
|
|
|
|
|
...current,
|
|
|
|
|
[fieldKey]: nextValue,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (selectedBlockId?.startsWith('slide-')) {
|
|
|
|
|
const index = Number(selectedBlockId.replace('slide-', ''))
|
|
|
|
|
if (Number.isNaN(index)) {
|
|
|
|
|
return current
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const slides = [...current.slides]
|
|
|
|
|
const target = { ...slides[index] }
|
|
|
|
|
|
|
|
|
|
if (fieldKey === 'title' || fieldKey === 'subtitle') {
|
|
|
|
|
target[fieldKey] = nextValue
|
|
|
|
|
} else if (fieldKey.startsWith('service-')) {
|
|
|
|
|
const parts = fieldKey.split('-')
|
|
|
|
|
const serviceIndex = Number(parts[1])
|
|
|
|
|
const serviceField = parts[2] as 'icon' | 'title' | 'description'
|
|
|
|
|
const services = [...target.services]
|
|
|
|
|
const service = { ...services[serviceIndex] }
|
|
|
|
|
service[serviceField] = nextValue
|
|
|
|
|
services[serviceIndex] = service
|
|
|
|
|
target.services = services
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
slides[index] = target
|
|
|
|
|
return {
|
|
|
|
|
...current,
|
|
|
|
|
slides,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (selectedBlockId?.startsWith('feature-')) {
|
|
|
|
|
const index = Number(selectedBlockId.replace('feature-', ''))
|
|
|
|
|
if (Number.isNaN(index)) {
|
|
|
|
|
return current
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const features = [...current.features]
|
|
|
|
|
const feature = { ...features[index] }
|
|
|
|
|
const key = fieldKey as 'icon' | 'title' | 'description'
|
|
|
|
|
feature[key] = nextValue
|
|
|
|
|
features[index] = feature
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...current,
|
|
|
|
|
features,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (selectedBlockId?.startsWith('solution-')) {
|
|
|
|
|
const index = Number(selectedBlockId.replace('solution-', ''))
|
|
|
|
|
if (Number.isNaN(index)) {
|
|
|
|
|
return current
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const solutions = [...current.solutions]
|
|
|
|
|
const solution = { ...solutions[index] }
|
|
|
|
|
const key = fieldKey as 'icon' | 'title' | 'description' | 'colorClass'
|
|
|
|
|
solution[key] = nextValue
|
|
|
|
|
solutions[index] = solution
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...current,
|
|
|
|
|
solutions,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return current
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const selectedSelection: DesignerSelection | null = React.useMemo(() => {
|
|
|
|
|
if (!content || !selectedBlockId) {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (selectedBlockId === 'hero') {
|
|
|
|
|
return {
|
|
|
|
|
id: 'hero',
|
|
|
|
|
title: 'Public.home.hero.*',
|
|
|
|
|
description: 'Hero arka planini ve CTA metinlerini duzenleyin.',
|
|
|
|
|
fields: [
|
|
|
|
|
{
|
|
|
|
|
key: 'heroBackgroundImage',
|
|
|
|
|
label: content.heroBackgroundImageKey,
|
|
|
|
|
type: 'image',
|
|
|
|
|
value: content.heroBackgroundImage,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'heroPrimaryCtaLabel',
|
|
|
|
|
label: content.heroPrimaryCtaKey,
|
|
|
|
|
type: 'text',
|
|
|
|
|
value: content.heroPrimaryCtaLabel,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'heroSecondaryCtaLabel',
|
|
|
|
|
label: content.heroSecondaryCtaKey,
|
|
|
|
|
type: 'text',
|
|
|
|
|
value: content.heroSecondaryCtaLabel,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (selectedBlockId.startsWith('slide-')) {
|
|
|
|
|
const index = Number(selectedBlockId.replace('slide-', ''))
|
|
|
|
|
const slide = content.slides[index]
|
|
|
|
|
|
|
|
|
|
if (!slide) {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
id: selectedBlockId,
|
|
|
|
|
title: slide.titleKey,
|
|
|
|
|
description: 'Slayt baslik, alt baslik ve servis kartlarini duzenleyin.',
|
|
|
|
|
fields: [
|
|
|
|
|
{
|
|
|
|
|
key: 'title',
|
|
|
|
|
label: slide.titleKey,
|
|
|
|
|
type: 'text',
|
|
|
|
|
value: slide.title,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'subtitle',
|
|
|
|
|
label: slide.subtitleKey,
|
|
|
|
|
type: 'textarea',
|
|
|
|
|
value: slide.subtitle,
|
|
|
|
|
},
|
|
|
|
|
...slide.services.flatMap((service, serviceIndex) => [
|
|
|
|
|
{
|
|
|
|
|
key: `service-${serviceIndex}-icon`,
|
|
|
|
|
label: `Slide${index + 1}.Service${serviceIndex + 1}.Icon`,
|
|
|
|
|
type: 'icon' as const,
|
|
|
|
|
value: service.icon,
|
|
|
|
|
placeholder: 'Ornek: FaUsers',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: `service-${serviceIndex}-title`,
|
|
|
|
|
label: service.titleKey,
|
|
|
|
|
type: 'text' as const,
|
|
|
|
|
value: service.title,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: `service-${serviceIndex}-description`,
|
|
|
|
|
label: service.descriptionKey,
|
|
|
|
|
type: 'textarea' as const,
|
|
|
|
|
value: service.description,
|
|
|
|
|
},
|
|
|
|
|
]),
|
|
|
|
|
],
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (selectedBlockId === 'features-heading') {
|
|
|
|
|
return {
|
|
|
|
|
id: selectedBlockId,
|
|
|
|
|
title: content.featuresTitleKey,
|
|
|
|
|
description: 'Features bolum basliklarini duzenleyin.',
|
|
|
|
|
fields: [
|
|
|
|
|
{
|
|
|
|
|
key: 'featuresTitle',
|
|
|
|
|
label: content.featuresTitleKey,
|
|
|
|
|
type: 'text',
|
|
|
|
|
value: content.featuresTitle,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'featuresSubtitle',
|
|
|
|
|
label: content.featuresSubtitleKey,
|
|
|
|
|
type: 'textarea',
|
|
|
|
|
value: content.featuresSubtitle,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (selectedBlockId.startsWith('feature-')) {
|
|
|
|
|
const index = Number(selectedBlockId.replace('feature-', ''))
|
|
|
|
|
const item = content.features[index]
|
|
|
|
|
|
|
|
|
|
if (!item) {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
id: selectedBlockId,
|
|
|
|
|
title: item.titleKey,
|
|
|
|
|
description: 'Feature ikon ve metinlerini duzenleyin.',
|
|
|
|
|
fields: [
|
|
|
|
|
{
|
|
|
|
|
key: 'icon',
|
|
|
|
|
label: `Feature${index + 1}.Icon`,
|
|
|
|
|
type: 'icon',
|
|
|
|
|
value: item.icon,
|
|
|
|
|
placeholder: 'Ornek: FaChartBar',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'title',
|
|
|
|
|
label: item.titleKey,
|
|
|
|
|
type: 'text',
|
|
|
|
|
value: item.title,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'description',
|
|
|
|
|
label: item.descriptionKey,
|
|
|
|
|
type: 'textarea',
|
|
|
|
|
value: item.description,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (selectedBlockId === 'solutions-heading') {
|
|
|
|
|
return {
|
|
|
|
|
id: selectedBlockId,
|
|
|
|
|
title: content.solutionsTitleKey,
|
|
|
|
|
description: 'Solutions bolum basliklarini duzenleyin.',
|
|
|
|
|
fields: [
|
|
|
|
|
{
|
|
|
|
|
key: 'solutionsTitle',
|
|
|
|
|
label: content.solutionsTitleKey,
|
|
|
|
|
type: 'text',
|
|
|
|
|
value: content.solutionsTitle,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'solutionsSubtitle',
|
|
|
|
|
label: content.solutionsSubtitleKey,
|
|
|
|
|
type: 'textarea',
|
|
|
|
|
value: content.solutionsSubtitle,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (selectedBlockId.startsWith('solution-')) {
|
|
|
|
|
const index = Number(selectedBlockId.replace('solution-', ''))
|
|
|
|
|
const item = content.solutions[index]
|
|
|
|
|
|
|
|
|
|
if (!item) {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
id: selectedBlockId,
|
|
|
|
|
title: item.titleKey,
|
|
|
|
|
description: 'Solution kartini tamamen duzenleyin.',
|
|
|
|
|
fields: [
|
|
|
|
|
{
|
|
|
|
|
key: 'colorClass',
|
|
|
|
|
label: `Solution${index + 1}.ColorClass`,
|
|
|
|
|
type: 'text',
|
|
|
|
|
value: item.colorClass,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'icon',
|
|
|
|
|
label: `Solution${index + 1}.Icon`,
|
|
|
|
|
type: 'icon',
|
|
|
|
|
value: item.icon,
|
|
|
|
|
placeholder: 'Ornek: FaDesktop',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'title',
|
|
|
|
|
label: item.titleKey,
|
|
|
|
|
type: 'text',
|
|
|
|
|
value: item.title,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'description',
|
|
|
|
|
label: item.descriptionKey,
|
|
|
|
|
type: 'textarea',
|
|
|
|
|
value: item.description,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (selectedBlockId === 'cta') {
|
|
|
|
|
return {
|
|
|
|
|
id: selectedBlockId,
|
|
|
|
|
title: content.ctaTitleKey,
|
|
|
|
|
description: 'Sayfa alti cta metinlerini duzenleyin.',
|
|
|
|
|
fields: [
|
|
|
|
|
{
|
|
|
|
|
key: 'ctaTitle',
|
|
|
|
|
label: content.ctaTitleKey,
|
|
|
|
|
type: 'text',
|
|
|
|
|
value: content.ctaTitle,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'ctaSubtitle',
|
|
|
|
|
label: content.ctaSubtitleKey,
|
|
|
|
|
type: 'textarea',
|
|
|
|
|
value: content.ctaSubtitle,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'ctaButtonLabel',
|
|
|
|
|
label: content.ctaButtonLabelKey,
|
|
|
|
|
type: 'text',
|
|
|
|
|
value: content.ctaButtonLabel,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null
|
|
|
|
|
}, [content, selectedBlockId])
|
|
|
|
|
|
|
|
|
|
const handleSaveAndExit = async () => {
|
|
|
|
|
if (!content || isSaving) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setIsSaving(true)
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await saveHomePage({
|
|
|
|
|
cultureName: selectedLanguage,
|
|
|
|
|
heroBackgroundImageKey: content.heroBackgroundImageKey,
|
|
|
|
|
heroBackgroundImageValue: content.heroBackgroundImage,
|
|
|
|
|
heroPrimaryCtaKey: content.heroPrimaryCtaKey,
|
|
|
|
|
heroPrimaryCtaValue: content.heroPrimaryCtaLabel,
|
|
|
|
|
heroSecondaryCtaKey: content.heroSecondaryCtaKey,
|
|
|
|
|
heroSecondaryCtaValue: content.heroSecondaryCtaLabel,
|
|
|
|
|
featuresTitleKey: content.featuresTitleKey,
|
|
|
|
|
featuresTitleValue: content.featuresTitle,
|
|
|
|
|
featuresSubtitleKey: content.featuresSubtitleKey,
|
|
|
|
|
featuresSubtitleValue: content.featuresSubtitle,
|
|
|
|
|
solutionsTitleKey: content.solutionsTitleKey,
|
|
|
|
|
solutionsTitleValue: content.solutionsTitle,
|
|
|
|
|
solutionsSubtitleKey: content.solutionsSubtitleKey,
|
|
|
|
|
solutionsSubtitleValue: content.solutionsSubtitle,
|
|
|
|
|
ctaTitleKey: content.ctaTitleKey,
|
|
|
|
|
ctaTitleValue: content.ctaTitle,
|
|
|
|
|
ctaSubtitleKey: content.ctaSubtitleKey,
|
|
|
|
|
ctaSubtitleValue: content.ctaSubtitle,
|
|
|
|
|
ctaButtonLabelKey: content.ctaButtonLabelKey,
|
|
|
|
|
ctaButtonLabelValue: content.ctaButtonLabel,
|
|
|
|
|
slides: content.slides.map((slide, slideIndex) => ({
|
|
|
|
|
titleKey: slide.titleKey || `Public.home.dynamic.slide.${slideIndex + 1}.title`,
|
|
|
|
|
titleValue: slide.title,
|
|
|
|
|
subtitleKey: slide.subtitleKey || `Public.home.dynamic.slide.${slideIndex + 1}.subtitle`,
|
|
|
|
|
subtitleValue: slide.subtitle,
|
|
|
|
|
services: slide.services.map((service, serviceIndex) => ({
|
|
|
|
|
icon: service.icon,
|
|
|
|
|
titleKey:
|
|
|
|
|
service.titleKey ||
|
|
|
|
|
`Public.home.dynamic.slide.${slideIndex + 1}.service.${serviceIndex + 1}.title`,
|
|
|
|
|
titleValue: service.title,
|
|
|
|
|
descriptionKey:
|
|
|
|
|
service.descriptionKey ||
|
|
|
|
|
`Public.home.dynamic.slide.${slideIndex + 1}.service.${serviceIndex + 1}.description`,
|
|
|
|
|
descriptionValue: service.description,
|
|
|
|
|
})),
|
|
|
|
|
})),
|
|
|
|
|
features: content.features.map((feature, index) => ({
|
|
|
|
|
icon: feature.icon,
|
|
|
|
|
titleKey: feature.titleKey || `Public.home.dynamic.feature.${index + 1}.title`,
|
|
|
|
|
titleValue: feature.title,
|
|
|
|
|
descriptionKey: feature.descriptionKey || `Public.home.dynamic.feature.${index + 1}.description`,
|
|
|
|
|
descriptionValue: feature.description,
|
|
|
|
|
})),
|
|
|
|
|
solutions: content.solutions.map((solution, index) => ({
|
|
|
|
|
icon: solution.icon,
|
|
|
|
|
colorClass: solution.colorClass,
|
|
|
|
|
titleKey: solution.titleKey || `Public.home.dynamic.solution.${index + 1}.title`,
|
|
|
|
|
titleValue: solution.title,
|
|
|
|
|
descriptionKey:
|
|
|
|
|
solution.descriptionKey || `Public.home.dynamic.solution.${index + 1}.description`,
|
|
|
|
|
descriptionValue: solution.description,
|
|
|
|
|
})),
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
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',
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
2026-03-17 11:27:30 +00:00
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Home tasarimi kaydedilemedi:', error)
|
|
|
|
|
} finally {
|
|
|
|
|
setIsSaving(false)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleLanguageChange = (language: string) => {
|
|
|
|
|
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>
|
|
|
|
|
)
|
|
|
|
|
}
|
2026-02-24 20:44:16 +00:00
|
|
|
|
|
|
|
|
return (
|
2026-03-17 11:27:30 +00:00
|
|
|
<div className={`min-h-screen ${isDesignMode && isPanelVisible ? 'xl:pr-[420px]' : ''}`}>
|
2026-02-24 20:44:16 +00:00
|
|
|
<Helmet
|
|
|
|
|
titleTemplate={`%s | ${APP_NAME}`}
|
|
|
|
|
title={translate('::' + 'App.Home')}
|
|
|
|
|
defaultTitle={APP_NAME}
|
|
|
|
|
></Helmet>
|
|
|
|
|
|
2026-03-17 11:27:30 +00:00
|
|
|
{isDesignMode && (
|
|
|
|
|
<div className="fixed bottom-6 left-6 z-40 rounded-full bg-slate-900 px-4 py-2 text-sm font-medium text-white shadow-xl">
|
|
|
|
|
Home designer aktif
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{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 Carousel */}
|
2026-03-17 11:27:30 +00:00
|
|
|
<SelectableBlock
|
|
|
|
|
id="hero"
|
|
|
|
|
isActive={selectedBlockId === 'hero'}
|
|
|
|
|
isDesignMode={isDesignMode}
|
|
|
|
|
onSelect={handleSelectBlock}
|
|
|
|
|
>
|
2026-02-24 20:44:16 +00:00
|
|
|
<div className="relative min-h-screen overflow-hidden">
|
|
|
|
|
<div className="absolute inset-0 bg-gradient-to-br from-blue-900 via-indigo-900 to-purple-900"></div>
|
|
|
|
|
<div
|
|
|
|
|
className="absolute inset-0 opacity-20"
|
|
|
|
|
style={{
|
2026-03-17 11:27:30 +00:00
|
|
|
backgroundImage: `url("${content?.heroBackgroundImage || HOME_HERO_DEFAULT_IMAGE}")`,
|
2026-02-24 20:44:16 +00:00
|
|
|
backgroundSize: 'cover',
|
|
|
|
|
backgroundPosition: 'center',
|
|
|
|
|
}}
|
|
|
|
|
></div>
|
|
|
|
|
|
|
|
|
|
{/* Carousel Content */}
|
|
|
|
|
<div className="relative container mx-auto px-4 pt-32 pb-16 h-screen flex items-center">
|
2026-03-17 11:27:30 +00:00
|
|
|
{(content?.slides || []).map((slide, index) => (
|
2026-02-24 20:44:16 +00:00
|
|
|
<div
|
|
|
|
|
key={index}
|
|
|
|
|
className={`absolute inset-0 transition-all duration-700 ease-in-out ${
|
|
|
|
|
index === currentSlide
|
|
|
|
|
? 'opacity-100 translate-x-0'
|
|
|
|
|
: index < currentSlide
|
|
|
|
|
? 'opacity-0 -translate-x-full'
|
|
|
|
|
: 'opacity-0 translate-x-full'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
2026-03-17 11:27:30 +00:00
|
|
|
<SelectableBlock
|
|
|
|
|
id={`slide-${index}`}
|
|
|
|
|
isActive={selectedBlockId === `slide-${index}`}
|
|
|
|
|
isDesignMode={isDesignMode}
|
|
|
|
|
onSelect={(id) => {
|
|
|
|
|
setCurrentSlide(index)
|
|
|
|
|
handleSelectBlock(id)
|
|
|
|
|
}}
|
|
|
|
|
className="h-full"
|
|
|
|
|
>
|
2026-02-24 20:44:16 +00:00
|
|
|
<div className="container mx-auto px-4 pt-32">
|
|
|
|
|
<div className="max-w-4xl mx-auto text-center">
|
|
|
|
|
<h1 className="text-3xl md:text-6xl font-bold mb-6 text-white animate-fade-in">
|
|
|
|
|
{slide.title}
|
|
|
|
|
</h1>
|
|
|
|
|
<p className="text-xl md:text-2xl text-gray-300 mb-12 animate-fade-in-delay">
|
|
|
|
|
{slide.subtitle}
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
<div className="flex flex-col md:flex-row justify-center gap-6 mb-16">
|
|
|
|
|
<Link
|
|
|
|
|
to={ROUTES_ENUM.public.contact}
|
|
|
|
|
className="inline-flex items-center justify-center px-8 py-4 bg-gradient-to-r from-blue-500 to-purple-500 hover:from-blue-600 hover:to-purple-600 text-white rounded-lg font-semibold transition-all transform hover:scale-105"
|
|
|
|
|
>
|
2026-03-17 11:27:30 +00:00
|
|
|
{content?.heroPrimaryCtaLabel}{' '}
|
2026-02-24 20:44:16 +00:00
|
|
|
<FaArrowRight className="ml-2" size={20} />
|
|
|
|
|
</Link>
|
|
|
|
|
<Link
|
|
|
|
|
to={ROUTES_ENUM.public.products}
|
|
|
|
|
className="inline-flex items-center justify-center px-8 py-4 bg-white/10 hover:bg-white/20 text-white rounded-lg font-semibold backdrop-blur-sm transition-all transform hover:scale-105"
|
|
|
|
|
>
|
2026-03-17 11:27:30 +00:00
|
|
|
{content?.heroSecondaryCtaLabel}
|
2026-02-24 20:44:16 +00:00
|
|
|
</Link>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 max-w-4xl mx-auto">
|
|
|
|
|
{slide.services.map((service, i) => (
|
|
|
|
|
<div
|
|
|
|
|
key={i}
|
|
|
|
|
className="bg-white/5 backdrop-blur-sm rounded-2xl p-8 text-center hover:scale-105 hover:bg-white/10 transition-all"
|
|
|
|
|
>
|
2026-03-17 11:27:30 +00:00
|
|
|
{(() => {
|
|
|
|
|
const IconComponent = navigationIcon[service.icon || '']
|
|
|
|
|
return IconComponent ? (
|
|
|
|
|
<IconComponent className="mx-auto mb-4 text-blue-300" size={40} />
|
|
|
|
|
) : null
|
|
|
|
|
})()}
|
2026-02-24 20:44:16 +00:00
|
|
|
<h3 className="text-xl font-semibold mb-3 text-white">{service.title}</h3>
|
2026-03-17 11:27:30 +00:00
|
|
|
<p className="text-gray-300">{service.description}</p>
|
2026-02-24 20:44:16 +00:00
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-03-17 11:27:30 +00:00
|
|
|
</SelectableBlock>
|
2026-02-24 20:44:16 +00:00
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Navigation Buttons */}
|
|
|
|
|
<button
|
|
|
|
|
onClick={prevSlide}
|
|
|
|
|
className="absolute left-4 top-1/2 -translate-y-1/2 bg-white/10 hover:bg-white/20 backdrop-blur-sm text-white p-4 rounded-full transition-all z-10"
|
|
|
|
|
aria-label="Previous slide"
|
|
|
|
|
>
|
|
|
|
|
<FaChevronLeft size={24} />
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={nextSlide}
|
|
|
|
|
className="absolute right-4 top-1/2 -translate-y-1/2 bg-white/10 hover:bg-white/20 backdrop-blur-sm text-white p-4 rounded-full transition-all z-10"
|
|
|
|
|
aria-label="Next slide"
|
|
|
|
|
>
|
|
|
|
|
<FaChevronRight size={24} />
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
{/* Slide Indicators */}
|
|
|
|
|
<div className="absolute bottom-8 left-1/2 -translate-x-1/2 flex gap-3 z-10">
|
2026-03-17 11:27:30 +00:00
|
|
|
{(content?.slides || []).map((_, index) => (
|
2026-02-24 20:44:16 +00:00
|
|
|
<button
|
|
|
|
|
key={index}
|
|
|
|
|
onClick={() => setCurrentSlide(index)}
|
|
|
|
|
className={`w-3 h-3 rounded-full transition-all ${
|
|
|
|
|
index === currentSlide ? 'bg-white w-8' : 'bg-white/50 hover:bg-white/70'
|
|
|
|
|
}`}
|
|
|
|
|
aria-label={`Go to slide ${index + 1}`}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
2026-03-17 11:27:30 +00:00
|
|
|
|
|
|
|
|
{isDesignMode && (
|
|
|
|
|
<div className="absolute bottom-24 left-1/2 z-20 -translate-x-1/2 flex gap-2 rounded-full bg-black/30 px-3 py-2 backdrop-blur">
|
|
|
|
|
{(content?.slides || []).map((_, index) => (
|
|
|
|
|
<button
|
|
|
|
|
key={`editor-slide-${index}`}
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setCurrentSlide(index)
|
|
|
|
|
handleSelectBlock(`slide-${index}`)
|
|
|
|
|
}}
|
|
|
|
|
className={`rounded-full px-3 py-1 text-xs font-semibold text-white transition ${
|
|
|
|
|
selectedBlockId === `slide-${index}` ? 'bg-sky-500' : 'bg-white/20 hover:bg-white/30'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
Slide {index + 1}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-02-24 20:44:16 +00:00
|
|
|
</div>
|
2026-03-17 11:27:30 +00:00
|
|
|
</SelectableBlock>
|
2026-02-24 20:44:16 +00:00
|
|
|
|
|
|
|
|
{/* Features */}
|
|
|
|
|
<section className="py-20 bg-white">
|
|
|
|
|
<div className="container mx-auto px-4">
|
2026-03-17 11:27:30 +00:00
|
|
|
<SelectableBlock
|
|
|
|
|
id="features-heading"
|
|
|
|
|
isActive={selectedBlockId === 'features-heading'}
|
|
|
|
|
isDesignMode={isDesignMode}
|
|
|
|
|
onSelect={handleSelectBlock}
|
|
|
|
|
>
|
2026-02-24 20:44:16 +00:00
|
|
|
<div className="text-center mb-16">
|
|
|
|
|
<h2 className="text-4xl font-bold text-gray-900 mb-4">
|
2026-03-17 11:27:30 +00:00
|
|
|
{content?.featuresTitle}
|
2026-02-24 20:44:16 +00:00
|
|
|
</h2>
|
|
|
|
|
<p className="text-xl text-gray-600 max-w-2xl mx-auto">
|
2026-03-17 11:27:30 +00:00
|
|
|
{content?.featuresSubtitle}
|
2026-02-24 20:44:16 +00:00
|
|
|
</p>
|
|
|
|
|
</div>
|
2026-03-17 11:27:30 +00:00
|
|
|
</SelectableBlock>
|
2026-02-24 20:44:16 +00:00
|
|
|
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
|
2026-03-17 11:27:30 +00:00
|
|
|
{(content?.features || []).map((feature, i) => (
|
|
|
|
|
<SelectableBlock
|
2026-02-24 20:44:16 +00:00
|
|
|
key={i}
|
2026-03-17 11:27:30 +00:00
|
|
|
id={`feature-${i}`}
|
|
|
|
|
isActive={selectedBlockId === `feature-${i}`}
|
|
|
|
|
isDesignMode={isDesignMode}
|
|
|
|
|
onSelect={handleSelectBlock}
|
2026-02-24 20:44:16 +00:00
|
|
|
>
|
2026-03-17 11:27:30 +00:00
|
|
|
<div className="p-8 bg-white rounded-xl shadow-lg hover:shadow-xl transition-shadow">
|
|
|
|
|
<div className="mb-6">
|
|
|
|
|
{(() => {
|
|
|
|
|
const IconComponent = navigationIcon[feature.icon || '']
|
|
|
|
|
return IconComponent ? <IconComponent className="w-12 h-12 text-blue-500" /> : null
|
|
|
|
|
})()}
|
|
|
|
|
</div>
|
2026-02-24 20:44:16 +00:00
|
|
|
<h2 className="text-2xl font-bold text-gray-900">{feature.title}</h2>
|
|
|
|
|
<p className="text-gray-600">{feature.description}</p>
|
|
|
|
|
</div>
|
2026-03-17 11:27:30 +00:00
|
|
|
</SelectableBlock>
|
2026-02-24 20:44:16 +00:00
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
{/* Solutions */}
|
|
|
|
|
<section className="py-20 bg-gray-50">
|
|
|
|
|
<div className="container mx-auto px-4">
|
2026-03-17 11:27:30 +00:00
|
|
|
<SelectableBlock
|
|
|
|
|
id="solutions-heading"
|
|
|
|
|
isActive={selectedBlockId === 'solutions-heading'}
|
|
|
|
|
isDesignMode={isDesignMode}
|
|
|
|
|
onSelect={handleSelectBlock}
|
|
|
|
|
>
|
2026-02-24 20:44:16 +00:00
|
|
|
<div className="text-center mb-16">
|
|
|
|
|
<h2 className="text-4xl font-bold text-gray-900 mb-4">
|
2026-03-17 11:27:30 +00:00
|
|
|
{content?.solutionsTitle}
|
2026-02-24 20:44:16 +00:00
|
|
|
</h2>
|
|
|
|
|
<p className="text-xl text-gray-600 max-w-2xl mx-auto">
|
2026-03-17 11:27:30 +00:00
|
|
|
{content?.solutionsSubtitle}
|
2026-02-24 20:44:16 +00:00
|
|
|
</p>
|
|
|
|
|
</div>
|
2026-03-17 11:27:30 +00:00
|
|
|
</SelectableBlock>
|
2026-02-24 20:44:16 +00:00
|
|
|
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
|
2026-03-17 11:27:30 +00:00
|
|
|
{(content?.solutions || []).map((s, i) => (
|
|
|
|
|
<SelectableBlock
|
2026-02-24 20:44:16 +00:00
|
|
|
key={i}
|
2026-03-17 11:27:30 +00:00
|
|
|
id={`solution-${i}`}
|
|
|
|
|
isActive={selectedBlockId === `solution-${i}`}
|
|
|
|
|
isDesignMode={isDesignMode}
|
|
|
|
|
onSelect={handleSelectBlock}
|
2026-02-24 20:44:16 +00:00
|
|
|
>
|
2026-03-17 11:27:30 +00:00
|
|
|
<div className={`${s.colorClass} p-8 h-full rounded-2xl`}>
|
|
|
|
|
<div className="mb-6">
|
|
|
|
|
{(() => {
|
|
|
|
|
const IconComponent = navigationIcon[s.icon || '']
|
|
|
|
|
return IconComponent ? <IconComponent className="w-16 h-16 text-white" /> : null
|
|
|
|
|
})()}
|
|
|
|
|
</div>
|
2026-02-24 20:44:16 +00:00
|
|
|
<h3 className="text-2xl font-semibold text-white mb-4">{s.title}</h3>
|
|
|
|
|
<p className="text-white/90">{s.description}</p>
|
|
|
|
|
</div>
|
2026-03-17 11:27:30 +00:00
|
|
|
</SelectableBlock>
|
2026-02-24 20:44:16 +00:00
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
{/* Call to Action */}
|
2026-03-17 11:27:30 +00:00
|
|
|
<SelectableBlock
|
|
|
|
|
id="cta"
|
|
|
|
|
isActive={selectedBlockId === 'cta'}
|
|
|
|
|
isDesignMode={isDesignMode}
|
|
|
|
|
onSelect={handleSelectBlock}
|
|
|
|
|
>
|
2026-02-24 20:44:16 +00:00
|
|
|
<section className="bg-blue-600 py-16">
|
|
|
|
|
<div className="container mx-auto px-4 text-center">
|
2026-03-17 11:27:30 +00:00
|
|
|
<h2 className="text-3xl font-bold text-white mb-4">{content?.ctaTitle}</h2>
|
|
|
|
|
<p className="text-white text-lg mb-8">{content?.ctaSubtitle}</p>
|
2026-02-24 20:44:16 +00:00
|
|
|
<Link
|
|
|
|
|
to={ROUTES_ENUM.public.contact}
|
|
|
|
|
className="bg-white text-blue-600 px-8 py-3 rounded-lg font-semibold hover:bg-blue-50 transition-colors"
|
|
|
|
|
>
|
2026-03-17 11:27:30 +00:00
|
|
|
{content?.ctaButtonLabel}
|
2026-02-24 20:44:16 +00:00
|
|
|
</Link>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
2026-03-17 11:27:30 +00:00
|
|
|
</SelectableBlock>
|
|
|
|
|
|
|
|
|
|
<DesignerDrawer
|
|
|
|
|
isOpen={isDesignMode && isPanelVisible}
|
|
|
|
|
selection={selectedSelection}
|
|
|
|
|
pageTitle="Home"
|
|
|
|
|
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 Home
|