diff --git a/api/src/Sozsoft.Platform.Application/Public/PublicAppService.cs b/api/src/Sozsoft.Platform.Application/Public/PublicAppService.cs index b72979aa..57e33cc5 100644 --- a/api/src/Sozsoft.Platform.Application/Public/PublicAppService.cs +++ b/api/src/Sozsoft.Platform.Application/Public/PublicAppService.cs @@ -17,9 +17,16 @@ using System.Text.Json; using Volo.Abp.Identity; using Sozsoft.Languages; using Sozsoft.Languages.Entities; +using Microsoft.AspNetCore.Authorization; +using static Sozsoft.Platform.Data.Seeds.SeedConsts; namespace Sozsoft.Platform.Public; +/// +/// Public site uc noktalari. Okuma metotlari anonim erisime aciktir; sayfa tasarimini +/// degistiren Save*PageAsync metotlari +/// yetkileriyle korunur (yetkiler PermissionsData.json icinde tanimli, seeder ile yonetilir). +/// public class PublicAppService : PlatformAppService { private readonly IRepository _serviceRepository; @@ -96,6 +103,7 @@ public class PublicAppService : PlatformAppService return ObjectMapper.Map, List>(entity); } + [Authorize(AppCodes.WebSiteDesign.About)] public async Task SaveAboutPageAsync(SaveAboutPageInput input) { var entity = await _aboutRepository.FirstOrDefaultAsync() ?? throw new EntityNotFoundException(typeof(About)); @@ -148,6 +156,7 @@ public class PublicAppService : PlatformAppService await _languageTextAppService.ClearRedisCacheAsync(); } + [Authorize(AppCodes.WebSiteDesign.Services)] public async Task SaveServicesPageAsync(SaveServicesPageInput input) { var existingEntities = await _serviceRepository.GetListAsync(); @@ -207,6 +216,7 @@ public class PublicAppService : PlatformAppService return ObjectMapper.Map(entity); } + [Authorize(AppCodes.WebSiteDesign.Home)] public async Task SaveHomePageAsync(SaveHomePageInput input) { var entity = await _homeRepository.FirstOrDefaultAsync() ?? throw new EntityNotFoundException(typeof(Home)); @@ -956,6 +966,7 @@ public class PublicAppService : PlatformAppService .ToList(); } + [Authorize(AppCodes.WebSiteDesign.Contact)] public async Task SaveContactPageAsync(SaveContactPageInput input) { var entity = await _contactRepository.FirstOrDefaultAsync() ?? throw new EntityNotFoundException(typeof(Contact)); diff --git a/api/src/Sozsoft.Platform.DbMigrator/Seeds/PermissionsData.json b/api/src/Sozsoft.Platform.DbMigrator/Seeds/PermissionsData.json index 3d62bc45..5459dd7e 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Seeds/PermissionsData.json +++ b/api/src/Sozsoft.Platform.DbMigrator/Seeds/PermissionsData.json @@ -1603,6 +1603,15 @@ "MultiTenancySide": 2, "MenuGroup": "Erp|Kurs" }, + { + "GroupName": "App.Saas", + "Name": "App.Home.Design", + "ParentName": "App.Home", + "DisplayName": "Design", + "IsEnabled": true, + "MultiTenancySide": 2, + "MenuGroup": "Erp|Kurs" + }, { "GroupName": "App.Saas", "Name": "App.About", @@ -1612,6 +1621,15 @@ "MultiTenancySide": 2, "MenuGroup": "Erp|Kurs" }, + { + "GroupName": "App.Saas", + "Name": "App.About.Design", + "ParentName": "App.About", + "DisplayName": "Design", + "IsEnabled": true, + "MultiTenancySide": 2, + "MenuGroup": "Erp|Kurs" + }, { "GroupName": "App.Saas", "Name": "App.Services", @@ -1621,6 +1639,15 @@ "MultiTenancySide": 2, "MenuGroup": "Erp|Kurs" }, + { + "GroupName": "App.Saas", + "Name": "App.Services.Design", + "ParentName": "App.Services", + "DisplayName": "Design", + "IsEnabled": true, + "MultiTenancySide": 2, + "MenuGroup": "Erp|Kurs" + }, { "GroupName": "App.Saas", "Name": "App.Orders.Products", @@ -2080,6 +2107,15 @@ "MultiTenancySide": 2, "MenuGroup": "Erp|Kurs" }, + { + "GroupName": "App.Saas", + "Name": "App.Contact.Design", + "ParentName": "App.Contact", + "DisplayName": "Design", + "IsEnabled": true, + "MultiTenancySide": 2, + "MenuGroup": "Erp|Kurs" + }, { "GroupName": "App.Saas", "Name": "App.Menus.Routes", diff --git a/api/src/Sozsoft.Platform.Domain.Shared/PlatformConsts.cs b/api/src/Sozsoft.Platform.Domain.Shared/PlatformConsts.cs index 2628b3f2..b5aeb378 100644 --- a/api/src/Sozsoft.Platform.Domain.Shared/PlatformConsts.cs +++ b/api/src/Sozsoft.Platform.Domain.Shared/PlatformConsts.cs @@ -526,6 +526,18 @@ public static class PlatformConsts public const string Demos = Prefix.App + ".Demos"; public const string Contact = Prefix.App + ".Contact"; + /// + /// Public sitedeki sayfa tasarim modu (`?design=1`) yetkileri. + /// Yetkiler Seeds/PermissionsData.json icinde tanimlanir ve seeder ile yonetilir. + /// + public static class WebSiteDesign + { + public const string Home = AppCodes.Home + ".Design"; + public const string About = AppCodes.About + ".Design"; + public const string Services = AppCodes.Services + ".Design"; + public const string Contact = AppCodes.Contact + ".Design"; + } + //Administration public const string Administration = Prefix.App + ".Administration"; diff --git a/api/src/Sozsoft.Platform.Domain/Data/SeedConsts.cs b/api/src/Sozsoft.Platform.Domain/Data/SeedConsts.cs index de05f9e5..9207220e 100644 --- a/api/src/Sozsoft.Platform.Domain/Data/SeedConsts.cs +++ b/api/src/Sozsoft.Platform.Domain/Data/SeedConsts.cs @@ -443,6 +443,18 @@ public static class SeedConsts public const string Demos = Prefix.App + ".Demos"; public const string Contact = Prefix.App + ".Contact"; + /// + /// Public sitedeki sayfa tasarim modu (`?design=1`) yetkileri. + /// Yetkiler Seeds/PermissionsData.json icinde tanimlanir ve seeder ile yonetilir. + /// + public static class WebSiteDesign + { + public const string Home = AppCodes.Home + ".Design"; + public const string About = AppCodes.About + ".Design"; + public const string Services = AppCodes.Services + ".Design"; + public const string Contact = AppCodes.Contact + ".Design"; + } + public static class Menus { public const string Default = Prefix.App + ".Menus"; diff --git a/ui/src/components/orders/BillingControls.tsx b/ui/src/components/orders/BillingControls.tsx index 2e5552a1..00b19f16 100644 --- a/ui/src/components/orders/BillingControls.tsx +++ b/ui/src/components/orders/BillingControls.tsx @@ -33,7 +33,7 @@ export const BillingControls: React.FC = ({ return (
diff --git a/ui/src/constants/permission.constant.ts b/ui/src/constants/permission.constant.ts index ce676a96..a5acc93c 100644 --- a/ui/src/constants/permission.constant.ts +++ b/ui/src/constants/permission.constant.ts @@ -3,3 +3,15 @@ export const AI_ASSISTANT = 'App.Definitions.AiBot.Asistant' /** Veritabanı migration + seed tetikleme yetkisi (host tarafı). */ export const DB_MIGRATE = 'App.Setup.Migrate' + +/** + * Public site sayfalarının tasarım modu yetkileri. + * Sunucuda `PermissionsData.json` içinde tanımlıdır ve seeder ile yönetilir; + * asıl kontrol `PublicAppService.Save*PageAsync` üzerindedir. + */ +export const PUBLIC_PAGE_DESIGN = { + HOME: 'App.Home.Design', + ABOUT: 'App.About.Design', + SERVICES: 'App.Services.Design', + CONTACT: 'App.Contact.Design', +} as const diff --git a/ui/src/views/public/About.tsx b/ui/src/views/public/About.tsx index 0dbec22d..e716e0f8 100644 --- a/ui/src/views/public/About.tsx +++ b/ui/src/views/public/About.tsx @@ -1,18 +1,30 @@ -import { useEffect, useMemo, useState } from 'react' +import { useEffect, useMemo, useRef, useState } from 'react' import type { FC } 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 { Button, Notification, toast } from '@/components/ui' -import { useStoreState, useStoreActions } from '@/store' -import DesignerDrawer from './designer/DesignerDrawer' -import SelectableBlock from './designer/SelectableBlock' -import { DesignerSelection } from './designer/types' -import { useDesignerState } from './designer/useDesignerState' +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 @@ -25,10 +37,10 @@ interface AboutStatContent { valueStyleClass: string labelStyleClassKey: string labelStyleClass: string - useCounter?: boolean - counterEnd?: string - counterSuffix?: string - counterDuration?: number + useCounter: boolean + counterEnd: string + counterSuffix: string + counterDuration: number } interface AboutDescriptionContent { @@ -81,30 +93,20 @@ 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' -function isLikelyLocalizationKey(value?: string) { - return Boolean(value && /^[A-Za-z0-9_.-]+$/.test(value) && value.includes('.')) -} +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 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 getCounterParts(value?: string, suffix?: string) { - const sourceValue = value || '0' - const numericMatch = sourceValue.replace(',', '.').match(/\d+(?:\.\d+)?/) +function getCounterParts(value: string, suffix: string) { + const numericMatch = (value || '0').replace(',', '.').match(/\d+(?:\.\d+)?/) if (!numericMatch) { return null @@ -116,43 +118,66 @@ function getCounterParts(value?: string, suffix?: string) { return null } - const prefix = sourceValue.slice(0, numericMatch.index) - const detectedSuffix = sourceValue.slice((numericMatch.index || 0) + numericMatch[0].length) + const matchIndex = numericMatch.index ?? 0 return { endValue, - prefix, - suffix: suffix ?? detectedSuffix, + 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(null) useEffect(() => { - if (!stat.useCounter || !counterParts) { + 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 duration = Math.max(stat.counterDuration || 2000, 0) const animate = (timestamp: number) => { - if (startTime === null) { - startTime = timestamp - } + startTime ??= timestamp - const elapsed = timestamp - startTime - const progress = duration === 0 ? 1 : Math.min(elapsed / duration, 1) - const easedProgress = 1 - Math.pow(1 - progress, 3) - - setCurrentValue(counterParts.endValue * easedProgress) + 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) @@ -162,32 +187,23 @@ const AnimatedStatValue: FC<{ stat: AboutStatContent }> = ({ stat }) => { setCurrentValue(0) animationFrameId = requestAnimationFrame(animate) - return () => { - cancelAnimationFrame(animationFrameId) - } - }, [counterParts, stat.counterDuration, stat.useCounter]) + return () => cancelAnimationFrame(animationFrameId) + }, [counterParts, isVisible, stat.counterDuration, stat.useCounter]) if (!stat.useCounter || !counterParts) { return <>{stat.value} } - const formattedValue = counterParts.hasDecimals - ? currentValue.toFixed(1) - : Math.floor(currentValue).toString() - return ( - <> + {counterParts.prefix} - {formattedValue} + {counterParts.hasDecimals ? currentValue.toFixed(1) : Math.floor(currentValue)} {counterParts.suffix} - + ) } -function buildAboutContent( - about: AboutDto | undefined, - translate: (key: string) => string, -): AboutContent { +function buildAboutContent(about: AboutDto | undefined, translate: TranslateFn): AboutContent { return { heroTitle: resolveLocalizedValue(translate, ABOUT_HERO_TITLE_KEY, 'About'), heroTitleKey: ABOUT_HERO_TITLE_KEY, @@ -195,7 +211,7 @@ function buildAboutContent( heroTitleStyleClass: resolveLocalizedValue( translate, ABOUT_HERO_TITLE_STYLE_KEY, - 'text-5xl font-bold ml-4 mt-3 mb-2 text-white', + HERO_TITLE_STYLE_DEFAULT, ), heroSubtitle: resolveLocalizedValue(translate, ABOUT_HERO_SUBTITLE_KEY), heroSubtitleKey: ABOUT_HERO_SUBTITLE_KEY, @@ -203,7 +219,7 @@ function buildAboutContent( heroSubtitleStyleClass: resolveLocalizedValue( translate, ABOUT_HERO_SUBTITLE_STYLE_KEY, - 'text-xl max-w-3xl ml-4', + HERO_SUBTITLE_STYLE_DEFAULT, ), heroImage: resolveLocalizedValue(translate, ABOUT_HERO_IMAGE_KEY, ABOUT_HERO_IMAGE), heroImageKey: ABOUT_HERO_IMAGE_KEY, @@ -211,250 +227,182 @@ function buildAboutContent( heroSectionStyleClass: resolveLocalizedValue( translate, ABOUT_HERO_SECTION_STYLE_KEY, - 'relative bg-blue-900 text-white py-12', + HERO_SECTION_STYLE_DEFAULT, ), descriptionsContainerStyleClassKey: ABOUT_DESCRIPTIONS_CONTAINER_STYLE_KEY, descriptionsContainerStyleClass: resolveLocalizedValue( translate, ABOUT_DESCRIPTIONS_CONTAINER_STYLE_KEY, - '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', + DESCRIPTIONS_CONTAINER_STYLE_DEFAULT, ), - stats: - about?.statsDto.map((stat, index) => ({ - styleClassKey: `Public.about.dynamic.stat.${index + 1}.styleClass`, - styleClass: resolveLocalizedValue( - translate, - `Public.about.dynamic.stat.${index + 1}.styleClass`, - 'text-center rounded-xl px-4 py-6', - ), - valueStyleClassKey: `Public.about.dynamic.stat.${index + 1}.valueStyleClass`, - valueStyleClass: resolveLocalizedValue( - translate, - `Public.about.dynamic.stat.${index + 1}.valueStyleClass`, - 'text-4xl font-bold text-gray-900 dark:text-gray-100 mb-2', - ), - labelStyleClassKey: `Public.about.dynamic.stat.${index + 1}.labelStyleClass`, - labelStyleClass: resolveLocalizedValue( - translate, - `Public.about.dynamic.stat.${index + 1}.labelStyleClass`, - 'text-gray-600 dark:text-gray-300', - ), + 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 : undefined) || - `Public.about.dynamic.stat.${index + 1}.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}`, + 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: `Public.about.dynamic.description.${index + 1}.styleClass`, + styleClassKey: `${prefix}.styleClass`, styleClass: resolveLocalizedValue( translate, - `Public.about.dynamic.description.${index + 1}.styleClass`, + `${prefix}.styleClass`, index % 2 === 0 ? '' : 'text-center p-5 text-blue-800 dark:text-white', ), - })) ?? [], - sections: - about?.sectionsDto.map((section) => ({ + } + }), + 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 : undefined) || - `Public.about.dynamic.section.${section.key}.title`, + titleKey: (isLikelyLocalizationKey(section.key) ? section.key : '') || `${prefix}.title`, descriptionKey: - (isLikelyLocalizationKey(section.descKey) ? section.descKey : undefined) || - `Public.about.dynamic.section.${section.key}.description`, - cardStyleClassKey: `Public.about.dynamic.section.${section.key}.cardStyleClass`, + (isLikelyLocalizationKey(section.descKey) ? section.descKey : '') || `${prefix}.description`, + cardStyleClassKey: `${prefix}.cardStyleClass`, cardStyleClass: resolveLocalizedValue( translate, - `Public.about.dynamic.section.${section.key}.cardStyleClass`, - 'bg-white dark:bg-gray-900 p-8 rounded-xl shadow-lg dark:shadow-gray-950/40', + `${prefix}.cardStyleClass`, + SECTION_CARD_STYLE_DEFAULT, ), - titleStyleClassKey: `Public.about.dynamic.section.${section.key}.titleStyleClass`, + titleStyleClassKey: `${prefix}.titleStyleClass`, titleStyleClass: resolveLocalizedValue( translate, - `Public.about.dynamic.section.${section.key}.titleStyleClass`, - 'text-2xl font-bold text-gray-900 dark:text-gray-100 mb-4', + `${prefix}.titleStyleClass`, + SECTION_TITLE_STYLE_DEFAULT, ), - descriptionStyleClassKey: `Public.about.dynamic.section.${section.key}.descriptionStyleClass`, + descriptionStyleClassKey: `${prefix}.descriptionStyleClass`, descriptionStyleClass: resolveLocalizedValue( translate, - `Public.about.dynamic.section.${section.key}.descriptionStyleClass`, - 'text-gray-700 dark:text-gray-300', + `${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 { 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() - 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('about', initialContent, { - currentLanguage, - supportedLanguages: editorLanguages, - }) - useEffect(() => { - setLoading(true) - const fetchServices = async () => { + const fetchAbout = async () => { + setLoading(true) + try { const result = await getAbout() setAbout(result.data) } catch (error) { - console.error('About alınırken hata oluştu:', error) + console.error('About alinirken hata olustu:', error) } finally { setLoading(false) } } - fetchServices() + + fetchAbout() }, []) - const updateContent = (updater: (current: AboutContent) => AboutContent) => { - setContent((current) => { - if (!current) { - return current - } + // 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], + ) - return updater(current) - }) - } + const designer = usePageDesigner({ + pageKey: 'About', + permission: PUBLIC_PAGE_DESIGN.ABOUT, + initialContent, + onSave: (content, cultureName) => saveAboutPage(toSaveInput(content, cultureName)), + }) - const handleFieldChange = (fieldKey: string, value: string | string[]) => { - updateContent((current) => { - if ( - fieldKey === 'heroTitle' || - fieldKey === 'heroSubtitle' || - fieldKey === 'heroImage' || - fieldKey === 'heroSectionStyleClass' || - fieldKey === 'heroTitleStyleClass' || - fieldKey === 'heroSubtitleStyleClass' || - fieldKey === 'descriptionsContainerStyleClass' - ) { - return { - ...current, - [fieldKey]: value as string, - } - } + const { content, isDesignMode, isPanelVisible, selectedBlockId, selectBlock } = designer - 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 (fieldKey.startsWith('descriptionStyle-')) { - const index = Number(fieldKey.replace('descriptionStyle-', '')) - const descriptions = [...current.descriptions] - descriptions[index] = { - ...descriptions[index], - styleClass: 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 = useMemo(() => { + const selection: DesignerSelection | null = useMemo(() => { if (!content || !selectedBlockId) { return null } @@ -463,44 +411,26 @@ const About: FC = () => { return { id: 'hero', title: content.heroTitleKey, - description: 'Baslik, alt baslik ve arka plan gorselini guncelleyin.', + description: translateLabel( + translate, + 'Public.designer.desc4', + 'Baslik, alt baslik ve arka plan gorselini guncelleyin.', + ), fields: [ - { - key: 'heroTitle', - label: content.heroTitleKey, - type: 'text', - value: content.heroTitle, - }, - { - key: 'heroTitleStyleClass', - label: content.heroTitleStyleClassKey, - type: 'text', - value: content.heroTitleStyleClass, - }, - { - key: 'heroSubtitle', - label: content.heroSubtitleKey, - type: 'textarea', - value: content.heroSubtitle, - }, - { - key: 'heroSubtitleStyleClass', - label: content.heroSubtitleStyleClassKey, - type: 'text', - value: content.heroSubtitleStyleClass, - }, - { - key: 'heroImage', - label: content.heroImageKey, - type: 'image', - value: content.heroImage, - }, - { - key: 'heroSectionStyleClass', - label: content.heroSectionStyleClassKey, - type: 'text', - value: content.heroSectionStyleClass, - }, + 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, + ), ], } } @@ -509,29 +439,25 @@ const About: FC = () => { return { id: 'descriptions', title: 'Public.about.description.*', - description: 'Orta bolumdeki aciklama metinlerini duzenleyin.', + description: translateLabel( + translate, + 'Public.designer.descriptionsHint', + 'Orta bolumdeki aciklama metinlerini duzenleyin.', + ), fields: [ - { - key: 'descriptionsContainerStyleClass', - label: content.descriptionsContainerStyleClassKey, - type: 'text', - value: content.descriptionsContainerStyleClass, - }, - ...content.descriptions.flatMap((item, index) => [ - { - key: `description-${index}`, - label: item.key || `Public.about.dynamic.description.${index + 1}`, - type: 'textarea' as const, - value: item.text, + ...content.descriptions.map((item, index) => + textAreaField(`descriptions.${index}.text`, item.key, item.text, { rows: index % 2 === 0 ? 4 : 3, - }, - { - key: `descriptionStyle-${index}`, - label: item.styleClassKey, - type: 'text' as const, - value: item.styleClass, - }, - ]), + }), + ), + styleField( + 'descriptionsContainerStyleClass', + content.descriptionsContainerStyleClassKey, + content.descriptionsContainerStyleClass, + ), + ...content.descriptions.map((item, index) => + styleField(`descriptions.${index}.styleClass`, item.styleClassKey, item.styleClass), + ), ], } } @@ -547,45 +473,45 @@ const About: FC = () => { return { id: selectedBlockId, title: stat.labelKey, - description: translate('::Public.designer.desc1'), + description: translateLabel(translate, 'Public.designer.desc1', 'Kart icerigini duzenleyin.'), fields: [ - { - key: 'icon', - label: translate('::Public.designer.ikonAnahtari'), - type: 'icon', - value: stat.icon, - placeholder: 'Ornek: FaUsers', - }, - { - key: 'value', - label: translate('::App.Listform.ListformField.Value'), - type: 'text', - value: stat.value - }, - { - key: 'label', - label: translate('::' + stat.labelKey), - type: 'text', - value: stat.label, - }, - { - key: 'styleClass', - label: stat.styleClassKey, - type: 'text', - value: stat.styleClass, - }, - { - key: 'valueStyleClass', - label: stat.valueStyleClassKey, - type: 'text', - value: stat.valueStyleClass, - }, - { - key: 'labelStyleClass', - label: stat.labelStyleClassKey, - type: 'text', - value: stat.labelStyleClass, - }, + iconField( + `stats.${index}.icon`, + translateLabel(translate, 'Public.designer.ikonAnahtari', 'Ikon anahtari'), + stat.icon, + { placeholder: 'Ornek: 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', + 'Sayac animasyonu kullan', + ), + }), + textField(`stats.${index}.counterEnd`, 'counterEnd', stat.counterEnd, { + group: 'advanced', + helpText: translateLabel( + translate, + 'Public.designer.counterEndHint', + 'Bos birakilirsa deger alanindaki sayi kullanilir.', + ), + }), + 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), ], } } @@ -601,174 +527,34 @@ const About: FC = () => { return { id: selectedBlockId, title: section.titleKey, - description: 'Kart basligi ve aciklama metnini duzenleyin.', + description: translateLabel( + translate, + 'Public.designer.sectionHint', + '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, - }, - { - key: 'cardStyleClass', - label: section.cardStyleClassKey, - type: 'text', - value: section.cardStyleClass, - }, - { - key: 'titleStyleClass', - label: section.titleStyleClassKey, - type: 'text', - value: section.titleStyleClass, - }, - { - key: 'descriptionStyleClass', - label: section.descriptionStyleClassKey, - type: 'text', - value: section.descriptionStyleClass, - }, + 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]) - - 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, - })), - 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, - }, - ]), - ], - }) - - await getConfig(false) - setSelectedBlockId(null) - - toast.push( - - {translate('::ListForms.FormBilgileriKaydedildi')} - , - { - placement: 'bottom-end', - }, - ) - - } 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) - } - } + }, [content, selectedBlockId, translate]) if (loading) { - return ( -
-
- -
-
- ) + return } return ( @@ -777,110 +563,64 @@ const About: FC = () => { titleTemplate={`%s | ${APP_NAME}`} title={translate('::App.About')} defaultTitle={APP_NAME} - > + /> -
- {isDesignMode && !isPanelVisible && ( - - )} - - {/* Hero Section */} +
+ {/* Hero */} -
+
-
-
-

+ /> +
+
+

{content?.heroTitle}

-

{content?.heroSubtitle}

+

+ {content?.heroSubtitle} +

- {/* Stats Section */} -
+ {/* Stats */} +
-
- {content?.stats.map((stat, index) => { - const IconComponent = navigationIcon[stat.icon || ''] - - return ( - -
- {IconComponent && ( - - )} -
- -
-
{stat.label}
-
-
- ) - })} -
-
-
- - {/* Main Content */} -
-
-
- -
- {content?.descriptions.map((item, index) => ( -

- {item.text} -

- ))} -
-
-
- -
- {content?.sections.map((section, index) => ( +
+ {content?.stats.map((stat, index) => ( -
-

{section.title}

-

{section.description}

+
+ +
+ +
+
+ {stat.label} +
))} @@ -888,26 +628,56 @@ const About: FC = () => {
- 0 - ? languageOptions - : supportedLanguages.map((language) => ({ - key: language, - cultureName: language, - displayName: language.toUpperCase(), - })) - } - onClose={() => setIsPanelVisible(false)} - onSave={handleSaveAndExit} - onLanguageChange={handleLanguageChange} - onReset={resetContent} - onFieldChange={handleFieldChange} - /> + {/* Descriptions + sections */} +
+
+
+ +
+ {content?.descriptions.map((item, index) => ( +

+ {item.text} +

+ ))} +
+
+
+ +
+ {content?.sections.map((section, index) => ( + +
+

+ {section.title} +

+

+ {section.description} +

+
+
+ ))} +
+
+
+ +
) diff --git a/ui/src/views/public/Blog.tsx b/ui/src/views/public/Blog.tsx index fc22e73d..665f855a 100644 --- a/ui/src/views/public/Blog.tsx +++ b/ui/src/views/public/Blog.tsx @@ -1,63 +1,90 @@ -import React, { useEffect, useState } from 'react' +import React, { useCallback, useEffect, useMemo, useState } from 'react' import { Link } from 'react-router-dom' import { FaCalendarAlt, FaUser, FaTag, FaSearch } from 'react-icons/fa' +import { Helmet } from 'react-helmet' import { showDbDateAsIs } from '@/utils/dateUtils' import { BlogCategory, BlogPost } from '@/proxy/blog/blog' import { blogService } from '@/services/blog.service' import { useLocalization } from '@/utils/hooks/useLocalization' -import { Helmet } from 'react-helmet' -import { Loading } from '@/components/shared' import { APP_NAME } from '@/constants/app.constant' import { Button } from '@/components/ui' +import { translateLabel } from './designer' +import { PublicPageLoader } from './shared' -const Blog = () => { +const PAGE_SIZE = 10 +const PAGINATION_WINDOW = 2 + +const BLOG_HERO_IMAGE = + 'https://images.pexels.com/photos/3183164/pexels-photo-3183164.jpeg?auto=compress&cs=tinysrgb&w=1920' + +function buildPageNumbers(currentPage: number, totalPages: number) { + const pages: number[] = [] + + for (let page = 1; page <= totalPages; page += 1) { + if ( + page === 1 || + page === totalPages || + Math.abs(page - currentPage) <= PAGINATION_WINDOW + ) { + pages.push(page) + } + } + + return pages +} + +const Blog: React.FC = () => { const { translate } = useLocalization() const [posts, setPosts] = useState([]) const [categories, setCategories] = useState([]) const [loading, setLoading] = useState(true) - const [selectedCategory, setSelectedCategory] = useState('') + const [selectedCategory, setSelectedCategory] = useState('') const [searchQuery, setSearchQuery] = useState('') + // Arama yalnizca form gonderildiginde uygulanir; her tusa basista istek atilmaz. + const [appliedSearch, setAppliedSearch] = useState('') const [currentPage, setCurrentPage] = useState(1) - const [totalPages, setTotalPages] = useState(1) + const [totalCount, setTotalCount] = useState(0) - useEffect(() => { - loadBlogData() - }, [currentPage, selectedCategory]) + const loadBlogData = useCallback(async () => { + setLoading(true) - const loadBlogData = async () => { try { - setLoading(true) - - const postsData = await blogService.getPosts({ + const result = await blogService.getPosts({ page: currentPage, - pageSize: 10, + pageSize: PAGE_SIZE, categoryId: selectedCategory, - search: searchQuery, + search: appliedSearch, }) - if ( - postsData.posts && - postsData.posts.items && - postsData.posts.totalCount && - postsData.categories - ) { - setPosts(postsData.posts.items.filter((a) => a.isPublished)) - setTotalPages(postsData.posts.totalCount / 10) - setCategories(postsData.categories.filter((a) => a.isActive)) - } + // Sonuc bos donse de liste sifirlanmali; aksi halde onceki sayfanin yazilari ekranda kalir. + setPosts((result?.posts?.items ?? []).filter((post) => post.isPublished)) + setTotalCount(result?.posts?.totalCount ?? 0) + setCategories((result?.categories ?? []).filter((category) => category.isActive)) } catch (error) { - console.error('Blog verileri yüklenemedi:', error) + console.error('Blog verileri yuklenemedi:', error) setPosts([]) + setTotalCount(0) } finally { setLoading(false) } - } + }, [appliedSearch, currentPage, selectedCategory]) - const handleSearch = (e: React.FormEvent) => { - e.preventDefault() - setCurrentPage(1) + useEffect(() => { loadBlogData() + }, [loadBlogData]) + + // Kesirli sayfa sayisi `Array(totalPages)` cagrisini patlatiyordu; yukari yuvarlanir. + const totalPages = Math.max(1, Math.ceil(totalCount / PAGE_SIZE)) + const pageNumbers = useMemo( + () => buildPageNumbers(currentPage, totalPages), + [currentPage, totalPages], + ) + + const handleSearch = (event: React.FormEvent) => { + event.preventDefault() + setCurrentPage(1) + setAppliedSearch(searchQuery.trim()) } const handleCategoryChange = (categoryId: string) => { @@ -65,147 +92,137 @@ const Blog = () => { setCurrentPage(1) } + const categoryButtonClass = (isActive: boolean) => + `px-4 py-2 text-sm font-medium rounded-lg transition-colors ${ + isActive + ? 'bg-blue-600 text-white hover:bg-blue-700 dark:bg-blue-900 dark:hover:bg-blue-800' + : 'bg-gray-200 text-gray-700 hover:bg-gray-300 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700' + }` + if (loading && posts.length === 0) { - return ( -
-
- -
-
- ) + return } return (
- {/* Hero Section */} -
+ /> + + {/* Hero */} +
-
-
-

+ /> +
+
+

{translate('::App.BlogManagement')}

-

{translate('::Public.blog.subtitle')}

+

{translate('::Public.blog.subtitle')}

- - - {/* Blog Posts Grid */}
+ {/* Arama ve kategori filtresi */} +
+
+
+
+
+ setSearchQuery(event.target.value)} + /> + +
+
- {/* Search and Filter Section */} -
-
-
- {/* Search */} -
-
- setSearchQuery(e.target.value)} - placeholder="Blog yazılarında ara..." - className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg bg-white text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-400" - /> - +
+ {categories.map((category) => ( + + ))}
- - - {/* Category Filter */} -
- - {categories.map((category) => ( - - ))}
-
- {!Array.isArray(posts) || posts.length === 0 ? ( -
-

Henüz blog yazısı bulunmuyor.

+ {posts.length === 0 ? ( +
+

+ {translateLabel(translate, 'Public.blog.empty', 'Henuz blog yazisi bulunmuyor.')} +

) : ( -
+
{posts.map((post) => ( - -
-
- {post.title} -
+ +
+
+ {post.coverImage && ( + {post.title} + )} +
{translate('::Public.' + post.category.name)}
-
-

+
+

{translate('::Public.' + post.title)}

-

+

{translate('::Public.' + post.summary)}

- {/* Tags */} {post.tags.length > 0 && ( -
- {post.tags.slice(0, 3).map((tag, index) => ( +
+ {post.tags.slice(0, 3).map((tag) => ( - + {tag} ))}
)} -
-
+
+ {post.author} -
-
+ + {showDbDateAsIs(post.publishedAt || post.creationTime)} -
+

@@ -214,62 +231,65 @@ const Blog = () => {
)} - {/* Pagination */} {totalPages > 1 && ( -
-
+ + )}
- {/* Newsletter Section */} + {/* Newsletter */}
-

+

{translate('::Public.blog.subscribe')}

-

+

{translate('::Public.blog.subscribe.desc')}

-
+
- +
diff --git a/ui/src/views/public/BlogDetail.tsx b/ui/src/views/public/BlogDetail.tsx index d49e2835..222b92c5 100644 --- a/ui/src/views/public/BlogDetail.tsx +++ b/ui/src/views/public/BlogDetail.tsx @@ -1,19 +1,21 @@ -import React, { useState, useEffect } from 'react' +import React, { useEffect, useMemo, useState } from 'react' import { Link, useParams } from 'react-router-dom' +import { Helmet } from 'react-helmet' import { showDbDateAsIs } from '@/utils/dateUtils' import { BlogPost } from '@/proxy/blog/blog' import { blogService } from '@/services/blog.service' import { useLocalization } from '@/utils/hooks/useLocalization' import { useStoreState } from '@/store/store' import { ROUTES_ENUM } from '@/routes/route.constant' -import { Helmet } from 'react-helmet' -import { Loading } from '@/components/shared' import { APP_NAME } from '@/constants/app.constant' +import { translateLabel } from './designer' +import { PublicPageLoader } from './shared' -interface PostData { - image?: string - author?: string -} +const CenteredMessage: React.FC<{ children: React.ReactNode }> = ({ children }) => ( +
+

{children}

+
+) const BlogDetail: React.FC = () => { const { id } = useParams<{ id: string }>() @@ -21,7 +23,6 @@ const BlogDetail: React.FC = () => { const { currentLang } = useStoreState((state) => state.locale) const [blogPost, setBlogPost] = useState(null) - const [postData, setPostData] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) @@ -29,94 +30,88 @@ const BlogDetail: React.FC = () => { const fetchBlogPost = async () => { setLoading(true) setError(null) + + if (!id) { + setError(translateLabel(translate, 'Public.blog.notFound', 'Blog yazisi bulunamadi.')) + setLoading(false) + return + } + try { - if (id) { - const response = await blogService.getPostBySlug(id) - setBlogPost(response) - setPostData({ - image: response.coverImage, - author: response.author, - }) - } else { - setError('Blog post ID is missing.') - } - } catch (error: any) { - setError(error.message || 'Failed to fetch blog post.') + setBlogPost(await blogService.getPostBySlug(id)) + } catch (fetchError) { + console.error('Blog yazisi alinamadi:', fetchError) + setError(translateLabel(translate, 'Public.blog.notFound', 'Blog yazisi bulunamadi.')) } finally { setLoading(false) } } fetchBlogPost() + // `translate` yalnizca hata metni icin kullanilir; dil degisiminde yeniden istek atilmaz. + // eslint-disable-next-line react-hooks/exhaustive-deps }, [id]) + const title = blogPost ? translate('::Public.' + blogPost.title) : '' + + // Icerik anahtari yoksa `'::' + undefined` cevirisi "undefined" basiyordu. + const content = useMemo(() => { + if (!blogPost) { + return '' + } + + const contentKey = currentLang === 'tr' ? blogPost.contentTr : blogPost.contentEn + + return contentKey ? translate('::' + contentKey) : '' + }, [blogPost, currentLang, translate]) + if (loading) { - return ( -
-
- -
-
- ) + return } if (error) { - return ( -
-

Error: {error}

-
- ) + return {error} } - if (!blogPost || !postData) { - return ( -
-

{translate('::Public.blog.notFound')}

-
- ) + if (!blogPost) { + return {translate('::Public.blog.notFound')} } return (
-
+ /> +
+
← {translate('::App.BlogManagement')} - {postData.image && ( + + {blogPost.coverImage && ( {translate('::Public.' )} -

- {translate('::Public.' + blogPost.title)} -

-
-
- {postData.author} -
-
- {blogPost.publishedAt && showDbDateAsIs(blogPost.publishedAt)} -
+ +

{title}

+ +
+ {blogPost.author} + {showDbDateAsIs(blogPost.publishedAt || blogPost.creationTime)}
+
diff --git a/ui/src/views/public/Checkout.tsx b/ui/src/views/public/Checkout.tsx index c3bad70b..793934bc 100644 --- a/ui/src/views/public/Checkout.tsx +++ b/ui/src/views/public/Checkout.tsx @@ -64,15 +64,7 @@ const Checkout: React.FC = () => { > {/* Hero Section */} -
-
-
+
diff --git a/ui/src/views/public/Contact.tsx b/ui/src/views/public/Contact.tsx index b6f36219..0a6ae0c1 100644 --- a/ui/src/views/public/Contact.tsx +++ b/ui/src/views/public/Contact.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react' +import React, { useEffect, useMemo, useState } from 'react' import { FaPhone, FaBuilding, @@ -9,18 +9,28 @@ import { FaEnvelope, FaIdCard, } from 'react-icons/fa' -import { useLocalization } from '@/utils/hooks/useLocalization' import { Helmet } from 'react-helmet' import { ContactDto } from '@/proxy/contact/models' -import Loading from '@/components/shared/Loading' import { getContact, saveContactPage } from '@/services/contact.service' +import { useLocalization } from '@/utils/hooks/useLocalization' import { APP_NAME } from '@/constants/app.constant' -import { useStoreActions, useStoreState } from '@/store' -import DesignerDrawer from './designer/DesignerDrawer' -import SelectableBlock from './designer/SelectableBlock' -import { DesignerSelection } from './designer/types' -import { useDesignerState } from './designer/useDesignerState' -import { Button, Notification, toast } from '@/components/ui' +import { PUBLIC_PAGE_DESIGN } from '@/constants/permission.constant' +import { + DesignerLayer, + DesignerSelection, + SelectableBlock, + TranslateFn, + booleanField, + imageField, + resolveLocalizedValue, + styleField, + textAreaField, + textField, + translateLabel, + usePageDesigner, +} from './designer' +import { designerPageClass } from './pageStyles' +import { PublicPageLoader } from './shared' interface ContactContent { heroTitle: string @@ -68,7 +78,7 @@ interface ContactContent { mapSrc: string mapWidth: string mapHeight: string - mapAllowFullScreen: string + mapAllowFullScreen: boolean mapLoading: string mapReferrerPolicy: string mapContainerStyleClass: string @@ -89,44 +99,28 @@ const CONTACT_HERO_SUBTITLE_STYLE_KEY = 'Public.contact.hero.subtitleStyleClass' const CONTACT_INFO_CARD_STYLE_KEY = 'Public.contact.info.cardStyleClass' const CONTACT_INFO_TITLE_STYLE_KEY = 'Public.contact.info.titleStyleClass' -function isLikelyLocalizationKey(value?: string) { - return Boolean(value && /^[A-Za-z0-9_.-]+$/.test(value) && value.includes('.')) -} +const HERO_SECTION_STYLE_DEFAULT = 'relative bg-blue-900 py-12 text-white dark:bg-gray-950' +const HERO_TITLE_STYLE_DEFAULT = 'ml-4 mb-2 mt-3 text-5xl font-bold text-white' +const HERO_SUBTITLE_STYLE_DEFAULT = 'ml-4 max-w-3xl text-xl' +const CARD_STYLE_DEFAULT = 'rounded-xl bg-white p-8 shadow-lg dark:bg-gray-900 dark:shadow-gray-950/40' +const INFO_TITLE_STYLE_DEFAULT = 'mb-6 text-2xl font-bold text-gray-900 dark:text-gray-100' +const MAP_FRAME_STYLE_DEFAULT = + 'aspect-w-16 aspect-h-9 overflow-hidden rounded-xl bg-gray-200 dark:bg-gray-800' -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 buildContactContent( - contact: ContactDto | undefined, - translate: (key: string) => string, -): ContactContent { +function buildContactContent(contact: ContactDto | undefined, translate: TranslateFn): ContactContent { return { heroTitle: resolveLocalizedValue(translate, CONTACT_HERO_TITLE_KEY, 'Contact'), heroTitleKey: CONTACT_HERO_TITLE_KEY, heroSectionStyleClass: resolveLocalizedValue( translate, CONTACT_HERO_SECTION_STYLE_KEY, - 'relative bg-blue-900 py-12 text-white dark:bg-gray-950', + HERO_SECTION_STYLE_DEFAULT, ), heroSectionStyleClassKey: CONTACT_HERO_SECTION_STYLE_KEY, heroTitleStyleClass: resolveLocalizedValue( translate, CONTACT_HERO_TITLE_STYLE_KEY, - 'ml-4 mb-2 mt-3 text-5xl font-bold text-white', + HERO_TITLE_STYLE_DEFAULT, ), heroTitleStyleClassKey: CONTACT_HERO_TITLE_STYLE_KEY, heroSubtitle: resolveLocalizedValue(translate, CONTACT_HERO_SUBTITLE_KEY), @@ -134,7 +128,7 @@ function buildContactContent( heroSubtitleStyleClass: resolveLocalizedValue( translate, CONTACT_HERO_SUBTITLE_STYLE_KEY, - 'ml-4 max-w-3xl text-xl', + HERO_SUBTITLE_STYLE_DEFAULT, ), heroSubtitleStyleClassKey: CONTACT_HERO_SUBTITLE_STYLE_KEY, heroImage: resolveLocalizedValue(translate, CONTACT_HERO_IMAGE_KEY, CONTACT_HERO_IMAGE_DEFAULT), @@ -144,13 +138,13 @@ function buildContactContent( contactInfoCardStyleClass: resolveLocalizedValue( translate, CONTACT_INFO_CARD_STYLE_KEY, - 'rounded-xl bg-white p-8 shadow-lg dark:bg-gray-900 dark:shadow-gray-950/40', + CARD_STYLE_DEFAULT, ), contactInfoCardStyleClassKey: CONTACT_INFO_CARD_STYLE_KEY, contactInfoTitleStyleClass: resolveLocalizedValue( translate, CONTACT_INFO_TITLE_STYLE_KEY, - 'mb-6 text-2xl font-bold text-gray-900 dark:text-gray-100', + INFO_TITLE_STYLE_DEFAULT, ), contactInfoTitleStyleClassKey: CONTACT_INFO_TITLE_STYLE_KEY, address: resolveLocalizedValue(translate, contact?.address, ''), @@ -165,9 +159,7 @@ function buildContactContent( bankBranch: contact?.bankDto?.branch || '', bankAccountNumber: contact?.bankDto?.accountNumber || '', bankIban: contact?.bankDto?.iban || '', - bankStyleClass: - contact?.bankDto?.styleClass || - 'rounded-xl bg-white p-8 shadow-lg dark:bg-gray-900 dark:shadow-gray-950/40', + bankStyleClass: contact?.bankDto?.styleClass || CARD_STYLE_DEFAULT, workHoursTitle: resolveLocalizedValue(translate, CONTACT_WORK_HOURS_TITLE_KEY), workHoursTitleKey: CONTACT_WORK_HOURS_TITLE_KEY, workWeekday: resolveLocalizedValue(translate, contact?.workHoursDto?.weekday, ''), @@ -176,74 +168,92 @@ function buildContactContent( workWeekendKey: contact?.workHoursDto?.weekend || '', workWhatsapp: resolveLocalizedValue(translate, contact?.workHoursDto?.whatsapp, ''), workWhatsappKey: contact?.workHoursDto?.whatsapp || '', - workHoursStyleClass: - contact?.workHoursDto?.styleClass || - 'rounded-xl bg-white p-8 shadow-lg dark:bg-gray-900 dark:shadow-gray-950/40', + workHoursStyleClass: contact?.workHoursDto?.styleClass || CARD_STYLE_DEFAULT, mapTitle: resolveLocalizedValue(translate, contact?.mapDto?.title, ''), mapTitleKey: contact?.mapDto?.title || '', mapSrc: contact?.mapDto?.src || '', mapWidth: contact?.mapDto?.width || '100%', mapHeight: contact?.mapDto?.height || '700', - mapAllowFullScreen: String(contact?.mapDto?.allowFullScreen ?? true), + mapAllowFullScreen: contact?.mapDto?.allowFullScreen ?? true, mapLoading: contact?.mapDto?.loading || 'lazy', mapReferrerPolicy: contact?.mapDto?.referrerPolicy || 'no-referrer-when-downgrade', - mapContainerStyleClass: - contact?.mapDto?.containerStyleClass || - 'rounded-xl bg-white p-8 shadow-lg dark:bg-gray-900 dark:shadow-gray-950/40', - mapFrameStyleClass: - contact?.mapDto?.frameStyleClass || - 'aspect-w-16 aspect-h-9 overflow-hidden rounded-xl bg-gray-200 dark:bg-gray-800', + mapContainerStyleClass: contact?.mapDto?.containerStyleClass || CARD_STYLE_DEFAULT, + mapFrameStyleClass: contact?.mapDto?.frameStyleClass || MAP_FRAME_STYLE_DEFAULT, } } +function toSaveInput(content: ContactContent, cultureName: string) { + return { + cultureName, + heroTitleKey: content.heroTitleKey, + heroTitleValue: content.heroTitle, + heroSubtitleKey: content.heroSubtitleKey, + heroSubtitleValue: content.heroSubtitle, + heroImageKey: content.heroImageKey, + heroImageValue: content.heroImage, + contactInfoTitleKey: content.contactInfoTitleKey, + contactInfoTitleValue: content.contactInfoTitle, + addressKey: content.addressKey, + addressValue: content.address, + phoneNumber: content.phoneNumber, + email: content.email, + location: content.location, + taxNumber: content.taxNumber, + bankTitleKey: content.bankTitleKey, + bankTitleValue: content.bankTitle, + bankAccountHolder: content.bankAccountHolder, + bankBranch: content.bankBranch, + bankAccountNumber: content.bankAccountNumber, + bankIban: content.bankIban, + bankStyleClass: content.bankStyleClass, + workHoursTitleKey: content.workHoursTitleKey, + workHoursTitleValue: content.workHoursTitle, + workWeekdayKey: content.workWeekdayKey, + workWeekdayValue: content.workWeekday, + workWeekendKey: content.workWeekendKey, + workWeekendValue: content.workWeekend, + workWhatsappKey: content.workWhatsappKey, + workWhatsappValue: content.workWhatsapp, + workHoursStyleClass: content.workHoursStyleClass, + mapTitleKey: content.mapTitleKey, + mapTitleValue: content.mapTitle, + mapSrc: content.mapSrc, + mapWidth: content.mapWidth, + mapHeight: content.mapHeight, + mapAllowFullScreen: content.mapAllowFullScreen, + mapLoading: content.mapLoading, + mapReferrerPolicy: content.mapReferrerPolicy, + mapContainerStyleClass: content.mapContainerStyleClass, + mapFrameStyleClass: content.mapFrameStyleClass, + styleTexts: [ + { key: content.heroSectionStyleClassKey, value: content.heroSectionStyleClass }, + { key: content.heroTitleStyleClassKey, value: content.heroTitleStyleClass }, + { key: content.heroSubtitleStyleClassKey, value: content.heroSubtitleStyleClass }, + { key: content.contactInfoCardStyleClassKey, value: content.contactInfoCardStyleClass }, + { key: content.contactInfoTitleStyleClassKey, value: content.contactInfoTitleStyleClass }, + ], + } +} + +const InfoRow: React.FC<{ icon: React.ReactNode; children: React.ReactNode }> = ({ + icon, + children, +}) => ( +
+ {icon} +

{children}

+
+) + const Contact: 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 [loading, setLoading] = useState(true) - const [isSaving, setIsSaving] = useState(false) - const [isPanelVisible, setIsPanelVisible] = useState(true) const [contact, setContact] = useState() - const initialContent = !loading ? buildContactContent(contact, translate) : null - const { - content, - isDesignMode, - selectedBlockId, - selectedLanguage, - supportedLanguages, - setContent, - setSelectedBlockId, - resetContent, - } = useDesignerState('contact', initialContent, { - currentLanguage, - supportedLanguages: editorLanguages, - }) - useEffect(() => { - setLoading(true) const fetchContact = async () => { + setLoading(true) + try { const result = await getContact() setContact(result.data) @@ -257,24 +267,21 @@ const Contact: React.FC = () => { fetchContact() }, []) - const updateContent = (updater: (current: ContactContent) => ContactContent) => { - setContent((current) => { - if (!current) { - return current - } + const initialContent = useMemo( + () => (loading ? null : buildContactContent(contact, translate)), + [contact, loading, translate], + ) - return updater(current) - }) - } + const designer = usePageDesigner({ + pageKey: 'Contact', + permission: PUBLIC_PAGE_DESIGN.CONTACT, + initialContent, + onSave: (content, cultureName) => saveContactPage(toSaveInput(content, cultureName)), + }) - const handleFieldChange = (fieldKey: string, value: string | string[]) => { - updateContent((current) => ({ - ...current, - [fieldKey]: value as string, - })) - } + const { content, isDesignMode, isPanelVisible, selectedBlockId, selectBlock } = designer - const selectedSelection: DesignerSelection | null = React.useMemo(() => { + const selection: DesignerSelection | null = useMemo(() => { if (!content || !selectedBlockId) { return null } @@ -283,44 +290,26 @@ const Contact: React.FC = () => { return { id: 'hero', title: 'Public.contact.hero.*', - description: translate('::Public.designer.desc4'), + description: translateLabel( + translate, + 'Public.designer.desc4', + 'Baslik, alt baslik ve arka plan gorselini guncelleyin.', + ), fields: [ - { - key: 'heroTitle', - label: content.heroTitleKey, - type: 'text', - value: content.heroTitle, - }, - { - key: 'heroTitleStyleClass', - label: content.heroTitleStyleClassKey, - type: 'text', - value: content.heroTitleStyleClass, - }, - { - key: 'heroSubtitle', - label: content.heroSubtitleKey, - type: 'textarea', - value: content.heroSubtitle, - }, - { - key: 'heroSubtitleStyleClass', - label: content.heroSubtitleStyleClassKey, - type: 'text', - value: content.heroSubtitleStyleClass, - }, - { - key: 'heroImage', - label: content.heroImageKey, - type: 'image', - value: content.heroImage, - }, - { - key: 'heroSectionStyleClass', - label: content.heroSectionStyleClassKey, - type: 'text', - value: content.heroSectionStyleClass, - }, + 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, + ), ], } } @@ -329,56 +318,28 @@ const Contact: React.FC = () => { return { id: selectedBlockId, title: content.contactInfoTitleKey, - description: translate('::Public.designer.desc5'), + description: translateLabel( + translate, + 'Public.designer.desc5', + 'Iletisim bilgilerini duzenleyin.', + ), fields: [ - { - key: 'contactInfoTitle', - label: content.contactInfoTitleKey, - type: 'text', - value: content.contactInfoTitle, - }, - { - key: 'contactInfoTitleStyleClass', - label: content.contactInfoTitleStyleClassKey, - type: 'text', - value: content.contactInfoTitleStyleClass, - }, - { - key: 'contactInfoCardStyleClass', - label: content.contactInfoCardStyleClassKey, - type: 'text', - value: content.contactInfoCardStyleClass, - }, - { - key: 'address', - label: content.addressKey, - type: 'textarea', - value: content.address, - }, - { - key: 'phoneNumber', - label: 'Public.contact.phone', - type: 'text', - value: content.phoneNumber, - }, - { - key: 'email', - label: 'Public.contact.email', - type: 'text', - value: content.email, - }, - { - key: 'location', - label: 'Public.contact.location.plain', - type: 'text', - value: content.location, - }, - { - key: 'taxNumber', - label: 'Public.contact.taxNumber', - type: 'text', - value: content.taxNumber, - }, + textField('contactInfoTitle', content.contactInfoTitleKey, content.contactInfoTitle), + textAreaField('address', content.addressKey || 'Public.contact.address', content.address), + textField('phoneNumber', 'Public.contact.phone', content.phoneNumber), + textField('email', 'Public.contact.email', content.email), + textField('location', 'Public.contact.location.plain', content.location), + textField('taxNumber', 'Public.contact.taxNumber', content.taxNumber), + styleField( + 'contactInfoTitleStyleClass', + content.contactInfoTitleStyleClassKey, + content.contactInfoTitleStyleClass, + ), + styleField( + 'contactInfoCardStyleClass', + content.contactInfoCardStyleClassKey, + content.contactInfoCardStyleClass, + ), ], } } @@ -387,44 +348,14 @@ const Contact: React.FC = () => { return { id: selectedBlockId, title: content.bankTitleKey, - description: translate('::Public.designer.desc6'), + description: translateLabel(translate, 'Public.designer.desc6', 'Banka bilgilerini duzenleyin.'), fields: [ - { - key: 'bankTitle', - label: content.bankTitleKey, - type: 'text', - value: content.bankTitle, - }, - { - key: 'bankAccountHolder', - label: 'Public.contact.bank.accountHolder', - type: 'text', - value: content.bankAccountHolder, - }, - { - key: 'bankBranch', - label: 'Public.contact.bank.branch', - type: 'text', - value: content.bankBranch, - }, - { - key: 'bankAccountNumber', - label: 'Public.contact.bank.accountNumber', - type: 'text', - value: content.bankAccountNumber, - }, - { - key: 'bankIban', - label: 'Public.contact.bank.iban', - type: 'text', - value: content.bankIban, - }, - { - key: 'bankStyleClass', - label: 'Public.contact.bank.styleClass', - type: 'text', - value: content.bankStyleClass, - }, + textField('bankTitle', content.bankTitleKey, content.bankTitle), + textField('bankAccountHolder', 'Public.contact.bank.accountHolder', content.bankAccountHolder), + textField('bankBranch', 'Public.contact.bank.branch', content.bankBranch), + textField('bankAccountNumber', 'Public.contact.bank.accountNumber', content.bankAccountNumber), + textField('bankIban', 'Public.contact.bank.iban', content.bankIban), + styleField('bankStyleClass', 'Public.contact.bank.styleClass', content.bankStyleClass), ], } } @@ -433,38 +364,21 @@ const Contact: React.FC = () => { return { id: selectedBlockId, title: content.workHoursTitleKey, - description: translate('::Public.designer.desc7'), + description: translateLabel( + translate, + 'Public.designer.desc7', + 'Calisma saatlerini duzenleyin.', + ), fields: [ - { - key: 'workHoursTitle', - label: content.workHoursTitleKey, - type: 'text', - value: content.workHoursTitle, - }, - { - key: 'workWeekday', - label: content.workWeekdayKey, - type: 'text', - value: content.workWeekday, - }, - { - key: 'workWeekend', - label: content.workWeekendKey, - type: 'text', - value: content.workWeekend, - }, - { - key: 'workWhatsapp', - label: content.workWhatsappKey, - type: 'text', - value: content.workWhatsapp, - }, - { - key: 'workHoursStyleClass', - label: 'Public.contact.workHours.styleClass', - type: 'text', - value: content.workHoursStyleClass, - }, + textField('workHoursTitle', content.workHoursTitleKey, content.workHoursTitle), + textField('workWeekday', content.workWeekdayKey || 'weekday', content.workWeekday), + textField('workWeekend', content.workWeekendKey || 'weekend', content.workWeekend), + textField('workWhatsapp', content.workWhatsappKey || 'whatsapp', content.workWhatsapp), + styleField( + 'workHoursStyleClass', + 'Public.contact.workHours.styleClass', + content.workHoursStyleClass, + ), ], } } @@ -472,216 +386,63 @@ const Contact: React.FC = () => { if (selectedBlockId === 'map') { return { id: selectedBlockId, - title: content.mapTitleKey, - description: translate('::Public.designer.desc8'), + title: content.mapTitleKey || 'Public.contact.map', + description: translateLabel(translate, 'Public.designer.desc8', 'Harita ayarlarini duzenleyin.'), fields: [ - { - key: 'mapTitle', - label: content.mapTitleKey, - type: 'text', - value: content.mapTitle, - }, - { - key: 'mapSrc', - label: 'Public.contact.map.src', - type: 'textarea', - value: content.mapSrc, - }, - { - key: 'mapWidth', - label: 'Public.contact.map.width', - type: 'text', - value: content.mapWidth, - }, - { - key: 'mapHeight', - label: 'Public.contact.map.height', - type: 'text', - value: content.mapHeight, - }, - { - key: 'mapAllowFullScreen', - label: 'Public.contact.map.allowFullScreen', - type: 'text', - value: content.mapAllowFullScreen, - placeholder: 'true veya false', - }, - { - key: 'mapLoading', - label: 'Public.contact.map.loading', - type: 'text', - value: content.mapLoading, - }, - { - key: 'mapReferrerPolicy', - label: 'Public.contact.map.referrerPolicy', - type: 'text', - value: content.mapReferrerPolicy, - }, - { - key: 'mapContainerStyleClass', - label: 'Public.contact.map.containerStyleClass', - type: 'text', - value: content.mapContainerStyleClass, - }, - { - key: 'mapFrameStyleClass', - label: 'Public.contact.map.frameStyleClass', - type: 'text', - value: content.mapFrameStyleClass, - }, + textField('mapTitle', content.mapTitleKey || 'Public.contact.map.title', content.mapTitle), + textAreaField('mapSrc', 'Public.contact.map.src', content.mapSrc, { group: 'media' }), + textField('mapWidth', 'Public.contact.map.width', content.mapWidth, { group: 'advanced' }), + textField('mapHeight', 'Public.contact.map.height', content.mapHeight, { group: 'advanced' }), + booleanField( + 'mapAllowFullScreen', + 'Public.contact.map.allowFullScreen', + content.mapAllowFullScreen, + { group: 'advanced' }, + ), + textField('mapLoading', 'Public.contact.map.loading', content.mapLoading, { + group: 'advanced', + placeholder: 'lazy | eager', + }), + textField('mapReferrerPolicy', 'Public.contact.map.referrerPolicy', content.mapReferrerPolicy, { + group: 'advanced', + }), + styleField( + 'mapContainerStyleClass', + 'Public.contact.map.containerStyleClass', + content.mapContainerStyleClass, + ), + styleField( + 'mapFrameStyleClass', + 'Public.contact.map.frameStyleClass', + content.mapFrameStyleClass, + ), ], } } return null - }, [content, selectedBlockId]) - - const handleSaveAndExit = async () => { - if (!content || isSaving) { - return - } - - setIsSaving(true) - - try { - await saveContactPage({ - cultureName: selectedLanguage, - heroTitleKey: content.heroTitleKey, - heroTitleValue: content.heroTitle, - heroSubtitleKey: content.heroSubtitleKey, - heroSubtitleValue: content.heroSubtitle, - heroImageKey: content.heroImageKey, - heroImageValue: content.heroImage, - contactInfoTitleKey: content.contactInfoTitleKey, - contactInfoTitleValue: content.contactInfoTitle, - addressKey: content.addressKey, - addressValue: content.address, - phoneNumber: content.phoneNumber, - email: content.email, - location: content.location, - taxNumber: content.taxNumber, - bankTitleKey: content.bankTitleKey, - bankTitleValue: content.bankTitle, - bankAccountHolder: content.bankAccountHolder, - bankBranch: content.bankBranch, - bankAccountNumber: content.bankAccountNumber, - bankIban: content.bankIban, - bankStyleClass: content.bankStyleClass, - workHoursTitleKey: content.workHoursTitleKey, - workHoursTitleValue: content.workHoursTitle, - workWeekdayKey: content.workWeekdayKey, - workWeekdayValue: content.workWeekday, - workWeekendKey: content.workWeekendKey, - workWeekendValue: content.workWeekend, - workWhatsappKey: content.workWhatsappKey, - workWhatsappValue: content.workWhatsapp, - workHoursStyleClass: content.workHoursStyleClass, - mapTitleKey: content.mapTitleKey, - mapTitleValue: content.mapTitle, - mapSrc: content.mapSrc, - mapWidth: content.mapWidth, - mapHeight: content.mapHeight, - mapAllowFullScreen: content.mapAllowFullScreen.toLowerCase() === 'true', - mapLoading: content.mapLoading, - mapReferrerPolicy: content.mapReferrerPolicy, - mapContainerStyleClass: content.mapContainerStyleClass, - mapFrameStyleClass: content.mapFrameStyleClass, - styleTexts: [ - { - key: content.heroSectionStyleClassKey, - value: content.heroSectionStyleClass, - }, - { - key: content.heroTitleStyleClassKey, - value: content.heroTitleStyleClass, - }, - { - key: content.heroSubtitleStyleClassKey, - value: content.heroSubtitleStyleClass, - }, - { - key: content.contactInfoCardStyleClassKey, - value: content.contactInfoCardStyleClass, - }, - { - key: content.contactInfoTitleStyleClassKey, - value: content.contactInfoTitleStyleClass, - }, - ], - }) - - await getConfig(false) - setSelectedBlockId(null) - - toast.push( - - {translate('::ListForms.FormBilgileriKaydedildi')} - , - { - placement: 'bottom-end', - }, - ) - - } catch (error) { - console.error('Contact tasarimi kaydedilemedi:', error) - } finally { - setIsSaving(false) - } - } - - const handleLanguageChange = (language: string) => { - setLang(language) - } - - const handleSelectBlock = (blockId: string) => { - setSelectedBlockId(blockId) - if (!isPanelVisible) { - setIsPanelVisible(true) - } - } + }, [content, selectedBlockId, translate]) if (loading) { - return ( -
-
- -
-
- ) + return } return ( -
+
- - {isDesignMode && ( -
- Contact designer aktif -
- )} - - {isDesignMode && !isPanelVisible && ( - - )} + /> -
+
{ backgroundSize: 'cover', backgroundPosition: 'center', }} - >
-
+ /> +
-

{content?.heroTitle}

-

{content?.heroSubtitle}

+

+ {content?.heroTitle} +

+

+ {content?.heroSubtitle} +

@@ -704,83 +469,70 @@ const Contact: React.FC = () => {
-
-

{content?.contactInfoTitle}

+
+

+ {content?.contactInfoTitle} +

-
- -
-

{content?.address}

-
-
-
- -
-

{content?.phoneNumber}

-
-
- -
- -
-

{content?.location}

-
-
-
- -
-

{content?.taxNumber}

-
-
+ }>{content?.address} + }>{content?.phoneNumber} + }> + + {content?.email} + + + }>{content?.location} + }>{content?.taxNumber}
-
-

{content?.bankTitle}

-
- Enpara Logo -
-

{content?.bankAccountHolder}

-

{content?.bankBranch}

-

{content?.bankAccountNumber}

-

{content?.bankIban}

-
-
+
+

+ {content?.bankTitle} +

+ {content?.bankAccountHolder +

+ {content?.bankAccountHolder} +

+

{content?.bankBranch}

+

+ {content?.bankAccountNumber} +

+

{content?.bankIban}

-
-

{content?.workHoursTitle}

+
+

+ {content?.workHoursTitle} +

@@ -801,22 +553,30 @@ const Contact: React.FC = () => { -
-

{content?.mapTitle}

-
- +
+

+ {content?.mapTitle} +

+
+ {content?.mapSrc && ( +