+
+
{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.' + 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}
-
-
-
- {content?.bankAccountHolder}
- {content?.bankBranch}
- {content?.bankAccountNumber}
- {content?.bankIban}
-
-
+
+
+ {content?.bankTitle}
+
+
+
+ {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 && (
+
+ )}
@@ -824,26 +584,7 @@ const Contact: React.FC = () => {
- 0
- ? languageOptions
- : supportedLanguages.map((language) => ({
- key: language,
- cultureName: language,
- displayName: language.toUpperCase(),
- }))
- }
- onClose={() => setIsPanelVisible(false)}
- onSave={handleSaveAndExit}
- onLanguageChange={handleLanguageChange}
- onReset={resetContent}
- onFieldChange={handleFieldChange}
- />
+
)
}
diff --git a/ui/src/views/public/Demo.tsx b/ui/src/views/public/Demo.tsx
index 7e684ded..717353a7 100644
--- a/ui/src/views/public/Demo.tsx
+++ b/ui/src/views/public/Demo.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useState } from "react";
+import React, { useEffect, useRef, useState } from 'react'
import {
FaBuilding,
FaUser,
@@ -8,176 +8,231 @@ import {
FaUsers,
FaRegComment,
FaPaperPlane,
- FaCheckCircle
-} from 'react-icons/fa';
-import { useLocalization } from "@/utils/hooks/useLocalization";
-import { createDemoAsync } from "@/services/demo.service";
-import { DemoDto } from "@/proxy/demo/models";
-import { Button } from "@/components/ui";
+ FaCheckCircle,
+} from 'react-icons/fa'
+import { useLocalization } from '@/utils/hooks/useLocalization'
+import { createDemoAsync } from '@/services/demo.service'
+import { DemoDto } from '@/proxy/demo/models'
+import { Button, Notification, toast } from '@/components/ui'
+import { translateLabel } from './designer'
interface DemoModalProps {
- isOpen: boolean;
- onClose: () => void;
+ isOpen: boolean
+ onClose: () => void
}
+type DemoErrors = Partial>
+
+const NUMERIC_FIELDS: Array = ['numberOfBranches', 'numberOfUsers']
+
+function createEmptyForm(): DemoDto {
+ return {
+ id: crypto.randomUUID(),
+ organizationName: '',
+ name: '',
+ email: '',
+ phoneNumber: '',
+ address: '',
+ numberOfBranches: 0,
+ numberOfUsers: 0,
+ message: '',
+ }
+}
+
+const inputClass = (hasError: boolean) =>
+ `w-full rounded-xl border bg-gray-50 py-2.5 pl-11 pr-4 text-gray-800 placeholder-gray-500 transition-all duration-300 focus:outline-none dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-400 ${
+ hasError
+ ? 'border-red-500 focus:border-red-500 focus:ring-red-500/20'
+ : 'border-gray-200 focus:border-blue-500 focus:ring-blue-500/20 dark:border-gray-700'
+ }`
+
+interface FieldProps {
+ label: string
+ error?: string
+ icon: React.ReactNode
+ children: React.ReactNode
+}
+
+const Field: React.FC = ({ label, error, icon, children }) => (
+
+
+
+ {icon}
+ {children}
+
+ {error && {error}
}
+
+)
const Demo: React.FC = ({ isOpen, onClose }) => {
const { translate } = useLocalization()
-
- const [formData, setFormData] = useState({
- id: crypto.randomUUID(),
- organizationName: "",
- name: "",
- email: "",
- phoneNumber: "",
- address: "",
- numberOfBranches: 0,
- numberOfUsers: 0,
- message: "",
- });
-
- const [errors, setErrors] = useState>({});
- const [isSubmitted, setIsSubmitted] = useState(false);
-
- const handleInputChange = (
- e: React.ChangeEvent
- ) => {
- const { name, value } = e.target;
- setFormData((prev) => ({
- ...prev,
- [name]: value,
- }));
-
- if (errors[name as keyof DemoDto]) {
- setErrors((prev) => ({
- ...prev,
- [name]: "",
- }));
- }
- };
-
- const validateForm = (): boolean => {
- const newErrors: Partial = {};
-
- if (!formData.organizationName.trim())
- newErrors.organizationName = "Organization name is required";
- if (!formData.name.trim()) newErrors.name = "Full name is required";
- if (!formData.email.trim()) {
- newErrors.email = "Email is required";
- } else if (!/\S+@\S+\.\S+/.test(formData.email)) {
- newErrors.email = "Please enter a valid email";
- }
- if (!formData.phoneNumber.trim()) newErrors.phoneNumber = "Phone number is required";
- if (!formData.address.trim()) newErrors.address = "Address is required";
- if (!formData.message.trim()) newErrors.message = "Message is required";
-
- setErrors(newErrors);
- return Object.keys(newErrors).length === 0;
- };
-
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault();
-
- if (!validateForm()) return;
-
- try {
- await createDemoAsync(formData);
- setIsSubmitted(true);
- onClose(); // modal'ı otomatik kapat
- } catch (error) {
- console.error("Gönderim hatası:", error);
- alert("Sunucuya ulaşılamıyor.");
- }
- };
+ const [formData, setFormData] = useState(createEmptyForm)
+ const [errors, setErrors] = useState({})
+ const [isSubmitting, setIsSubmitting] = useState(false)
+ const [isSubmitted, setIsSubmitted] = useState(false)
+ const firstFieldRef = useRef(null)
useEffect(() => {
- const handleKeyDown = (e: KeyboardEvent) => {
- if (e.key === "Escape") {
- onClose();
- }
- };
-
- if (isOpen) {
- window.addEventListener("keydown", handleKeyDown);
+ if (!isOpen) {
+ return
}
- return () => {
- window.removeEventListener("keydown", handleKeyDown);
- };
- }, [isOpen, onClose]);
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (event.key === 'Escape') {
+ onClose()
+ }
+ }
+
+ window.addEventListener('keydown', handleKeyDown)
+ firstFieldRef.current?.focus()
+
+ return () => window.removeEventListener('keydown', handleKeyDown)
+ }, [isOpen, onClose])
+
+ const requiredMessage = translateLabel(
+ translate,
+ 'App.Validation.Required',
+ 'Bu alan zorunludur.',
+ )
+
+ const handleInputChange = (
+ event: React.ChangeEvent,
+ ) => {
+ const { name, value } = event.target
+ const key = name as keyof DemoDto
+
+ setFormData((previous) => ({
+ ...previous,
+ // Sayisal alanlar string olarak saklanirsa sunucu tarafinda tip hatasi olusuyor.
+ [key]: NUMERIC_FIELDS.includes(key) ? Number(value) || 0 : value,
+ }))
+
+ setErrors((previous) => (previous[key] ? { ...previous, [key]: undefined } : previous))
+ }
+
+ const validateForm = () => {
+ const nextErrors: DemoErrors = {}
+
+ if (!formData.organizationName.trim()) nextErrors.organizationName = requiredMessage
+ if (!formData.name.trim()) nextErrors.name = requiredMessage
+ if (!formData.email.trim()) {
+ nextErrors.email = requiredMessage
+ } else if (!/\S+@\S+\.\S+/.test(formData.email)) {
+ nextErrors.email = translateLabel(
+ translate,
+ 'App.Validation.InvalidEmail',
+ 'Gecerli bir e-posta adresi girin.',
+ )
+ }
+ if (!formData.phoneNumber.trim()) nextErrors.phoneNumber = requiredMessage
+ if (!formData.address.trim()) nextErrors.address = requiredMessage
+ if (!formData.message.trim()) nextErrors.message = requiredMessage
+
+ setErrors(nextErrors)
+
+ return Object.keys(nextErrors).length === 0
+ }
+
+ const handleSubmit = async (event: React.FormEvent) => {
+ event.preventDefault()
+
+ if (isSubmitting || !validateForm()) {
+ return
+ }
+
+ setIsSubmitting(true)
+
+ try {
+ await createDemoAsync(formData)
+ setIsSubmitted(true)
+ } catch (error) {
+ console.error('Demo talebi gonderilemedi:', error)
+
+ toast.push(
+
+ {translateLabel(translate, 'App.Errors.ServerUnreachable', 'Sunucuya ulasilamiyor.')}
+ ,
+ { placement: 'bottom-end' },
+ )
+ } finally {
+ setIsSubmitting(false)
+ }
+ }
+
+ const resetForm = () => {
+ setIsSubmitted(false)
+ setErrors({})
+ setFormData(createEmptyForm())
+ }
+
+ if (!isOpen && !isSubmitted) {
+ return null
+ }
- // 🎉 Gönderim sonrası teşekkür ekranı
if (isSubmitted) {
return (
-
-
-
- {/* Kapat Butonu */}
+
+
+
-
-
-
+
+
+
-
+
{translate('::Public.demo.thankYou')}
-
+
{translate('::Public.demo.resultMessage')}
-
- );
+ )
}
- // Modal kapalıysa render etme
- if (!isOpen) return null;
-
- // 🎯 Normal form ekranı
return (
-
-
+
+
×
{translate('::Public.' + post.title)}
-+
{translate('::Public.' + post.summary)}
- {/* Tags */} {post.tags.length > 0 && ( -
+
{translate('::Public.blog.subscribe')}
-
+
{translate('::Public.blog.subscribe.desc')}
-{children}
+Error: {error}
-{translate('::Public.blog.notFound')}
-- {translate('::Public.' + blogPost.title)} -
-{title}
+ +{children}
+{content?.heroTitle}
-{content?.heroSubtitle}
++ {content?.heroTitle} +
++ {content?.heroSubtitle} +
{content?.contactInfoTitle}
++ {content?.contactInfoTitle} +
{content?.address}
-{content?.phoneNumber}
-{content?.location}
-{content?.taxNumber}
-{content?.bankTitle}
-{content?.bankAccountHolder}
-{content?.bankBranch}
-{content?.bankAccountNumber}
-{content?.bankIban}
-+ {content?.bankTitle} +
++ {content?.bankAccountHolder} +
+{content?.bankBranch}
++ {content?.bankAccountNumber} +
+{content?.bankIban}
{content?.workHoursTitle}
++ {content?.workHoursTitle} +
{content?.mapTitle}
-+ {content?.mapTitle} +
+{error}
} +
+
{translate('::Public.demo.thankYou')}
-
+
{translate('::Public.demo.resultMessage')}
-