sozsoft-platform/ui/src/views/public/Contact.tsx

707 lines
24 KiB
TypeScript
Raw Normal View History

2026-02-24 20:44:16 +00:00
import React, { useEffect, useState } from 'react'
import {
FaPhone,
FaBuilding,
FaCalendarAlt,
FaCalendarCheck,
FaRegComment,
FaMapMarkerAlt,
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'
2026-02-24 20:44:16 +00:00
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'
interface ContactContent {
heroTitle: string
heroTitleKey: string
heroSubtitle: string
heroSubtitleKey: string
heroImage: string
heroImageKey: string
contactInfoTitle: string
contactInfoTitleKey: string
address: string
addressKey: string
phoneNumber: string
email: string
location: string
taxNumber: string
bankTitle: string
bankTitleKey: string
bankAccountHolder: string
bankBranch: string
bankAccountNumber: string
bankIban: string
workHoursTitle: string
workHoursTitleKey: string
workWeekday: string
workWeekdayKey: string
workWeekend: string
workWeekendKey: string
workWhatsapp: string
workWhatsappKey: string
mapTitle: string
mapTitleKey: string
mapSrc: string
mapWidth: string
mapHeight: string
mapAllowFullScreen: string
mapLoading: string
mapReferrerPolicy: string
}
const CONTACT_HERO_TITLE_KEY = 'App.Contact'
const CONTACT_HERO_SUBTITLE_KEY = 'Public.contact.subtitle'
const CONTACT_HERO_IMAGE_KEY = 'Public.contact.heroImage'
const CONTACT_HERO_IMAGE_DEFAULT =
'https://images.pexels.com/photos/3183171/pexels-photo-3183171.jpeg?auto=compress&cs=tinysrgb&w=1920'
const CONTACT_INFO_TITLE_KEY = 'Abp.Identity.User.UserInformation.ContactInformation'
const CONTACT_ADDRESS_KEY = 'Public.contact.address.full'
const CONTACT_BANK_TITLE_KEY = 'Public.contact.bank.title'
const CONTACT_WORK_HOURS_TITLE_KEY = 'App.Definitions.WorkHour'
const CONTACT_WORK_WEEKDAY_KEY = 'Public.contact.workHours.weekday'
const CONTACT_WORK_WEEKEND_KEY = 'Public.contact.workHours.weekend'
const CONTACT_WORK_WHATSAPP_KEY = 'Public.contact.workHours.whatsapp'
const CONTACT_MAP_TITLE_KEY = 'Public.contact.location'
function isLikelyLocalizationKey(value?: string) {
return Boolean(value && /^[A-Za-z0-9_.-]+$/.test(value) && value.includes('.'))
}
function resolveLocalizedValue(
translate: (key: string) => string,
keyOrValue: string | undefined,
fallback = '',
) {
if (!keyOrValue) {
return fallback
}
if (!isLikelyLocalizationKey(keyOrValue)) {
return keyOrValue
}
const translatedValue = translate('::' + keyOrValue)
return translatedValue === keyOrValue ? fallback || keyOrValue : translatedValue
}
function buildContactContent(
contact: ContactDto | undefined,
translate: (key: string) => string,
): ContactContent {
return {
heroTitle: resolveLocalizedValue(translate, CONTACT_HERO_TITLE_KEY, 'Contact'),
heroTitleKey: CONTACT_HERO_TITLE_KEY,
heroSubtitle: resolveLocalizedValue(translate, CONTACT_HERO_SUBTITLE_KEY),
heroSubtitleKey: CONTACT_HERO_SUBTITLE_KEY,
heroImage: resolveLocalizedValue(translate, CONTACT_HERO_IMAGE_KEY, CONTACT_HERO_IMAGE_DEFAULT),
heroImageKey: CONTACT_HERO_IMAGE_KEY,
contactInfoTitle: resolveLocalizedValue(translate, CONTACT_INFO_TITLE_KEY),
contactInfoTitleKey: CONTACT_INFO_TITLE_KEY,
address: resolveLocalizedValue(translate, contact?.address || CONTACT_ADDRESS_KEY),
addressKey: contact?.address || CONTACT_ADDRESS_KEY,
phoneNumber: contact?.phoneNumber || '',
email: contact?.email || '',
location: contact?.location || '',
taxNumber: contact?.taxNumber || '',
bankTitle: resolveLocalizedValue(translate, CONTACT_BANK_TITLE_KEY),
bankTitleKey: CONTACT_BANK_TITLE_KEY,
bankAccountHolder: contact?.bankDto?.accountHolder || '',
bankBranch: contact?.bankDto?.branch || '',
bankAccountNumber: contact?.bankDto?.accountNumber || '',
bankIban: contact?.bankDto?.iban || '',
workHoursTitle: resolveLocalizedValue(translate, CONTACT_WORK_HOURS_TITLE_KEY),
workHoursTitleKey: CONTACT_WORK_HOURS_TITLE_KEY,
workWeekday: resolveLocalizedValue(translate, contact?.workHoursDto?.weekday || CONTACT_WORK_WEEKDAY_KEY),
workWeekdayKey: contact?.workHoursDto?.weekday || CONTACT_WORK_WEEKDAY_KEY,
workWeekend: resolveLocalizedValue(translate, contact?.workHoursDto?.weekend || CONTACT_WORK_WEEKEND_KEY),
workWeekendKey: contact?.workHoursDto?.weekend || CONTACT_WORK_WEEKEND_KEY,
workWhatsapp: resolveLocalizedValue(
translate,
contact?.workHoursDto?.whatsapp || CONTACT_WORK_WHATSAPP_KEY,
),
workWhatsappKey: contact?.workHoursDto?.whatsapp || CONTACT_WORK_WHATSAPP_KEY,
mapTitle: resolveLocalizedValue(translate, contact?.mapDto?.title || CONTACT_MAP_TITLE_KEY),
mapTitleKey: contact?.mapDto?.title || CONTACT_MAP_TITLE_KEY,
mapSrc: contact?.mapDto?.src || '',
mapWidth: contact?.mapDto?.width || '100%',
mapHeight: contact?.mapDto?.height || '700',
mapAllowFullScreen: String(contact?.mapDto?.allowFullScreen ?? true),
mapLoading: contact?.mapDto?.loading || 'lazy',
mapReferrerPolicy: contact?.mapDto?.referrerPolicy || 'no-referrer-when-downgrade',
}
}
2026-02-24 20:44:16 +00:00
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)),
)
2026-02-24 20:44:16 +00:00
const [loading, setLoading] = useState(true)
const [isSaving, setIsSaving] = useState(false)
const [isPanelVisible, setIsPanelVisible] = useState(true)
2026-02-24 20:44:16 +00:00
const [contact, setContact] = useState<ContactDto>()
const initialContent = !loading ? buildContactContent(contact, translate) : null
const {
content,
isDesignMode,
selectedBlockId,
selectedLanguage,
supportedLanguages,
setContent,
setSelectedBlockId,
resetContent,
} = useDesignerState<ContactContent>('contact', initialContent, {
currentLanguage,
supportedLanguages: editorLanguages,
})
2026-02-24 20:44:16 +00:00
useEffect(() => {
setLoading(true)
const fetchContact = async () => {
2026-02-24 20:44:16 +00:00
try {
const result = await getContact()
setContact(result.data)
} catch (error) {
console.error('Contact alinirken hata olustu:', error)
2026-02-24 20:44:16 +00:00
} finally {
setLoading(false)
}
}
fetchContact()
2026-02-24 20:44:16 +00:00
}, [])
const updateContent = (updater: (current: ContactContent) => ContactContent) => {
setContent((current) => {
if (!current) {
return current
}
return updater(current)
})
}
const handleFieldChange = (fieldKey: string, value: string | string[]) => {
updateContent((current) => ({
...current,
[fieldKey]: value as string,
}))
}
const selectedSelection: DesignerSelection | null = React.useMemo(() => {
if (!content || !selectedBlockId) {
return null
}
if (selectedBlockId === 'hero') {
return {
id: 'hero',
title: 'Public.contact.hero.*',
description: 'Hero basligi, alt basligi ve arka plan gorselini duzenleyin.',
fields: [
{
key: 'heroTitle',
label: content.heroTitleKey,
type: 'text',
value: content.heroTitle,
},
{
key: 'heroSubtitle',
label: content.heroSubtitleKey,
type: 'textarea',
value: content.heroSubtitle,
},
{
key: 'heroImage',
label: content.heroImageKey,
type: 'image',
value: content.heroImage,
},
],
}
}
if (selectedBlockId === 'contact-info') {
return {
id: selectedBlockId,
title: content.contactInfoTitleKey,
description: 'Iletisim bilgilerini duzenleyin.',
fields: [
{
key: 'contactInfoTitle',
label: content.contactInfoTitleKey,
type: 'text',
value: content.contactInfoTitle,
},
{
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,
},
],
}
}
if (selectedBlockId === 'bank') {
return {
id: selectedBlockId,
title: content.bankTitleKey,
description: '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,
},
],
}
}
if (selectedBlockId === 'work-hours') {
return {
id: selectedBlockId,
title: content.workHoursTitleKey,
description: 'Calisma saatleri metinlerini 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,
},
],
}
}
if (selectedBlockId === 'map') {
return {
id: selectedBlockId,
title: content.mapTitleKey,
description: 'Harita basligi ve iframe 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,
},
],
}
}
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,
workHoursTitleKey: content.workHoursTitleKey,
workHoursTitleValue: content.workHoursTitle,
workWeekdayKey: content.workWeekdayKey,
workWeekdayValue: content.workWeekday,
workWeekendKey: content.workWeekendKey,
workWeekendValue: content.workWeekend,
workWhatsappKey: content.workWhatsappKey,
workWhatsappValue: content.workWhatsapp,
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,
})
await getConfig(false)
setSelectedBlockId(null)
} 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)
}
}
2026-02-24 20:44:16 +00:00
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen bg-gray-50">
<div className="text-center">
<Loading loading={loading} />
</div>
</div>
)
}
return (
<div className={`min-h-screen bg-gray-50 ${isDesignMode && isPanelVisible ? 'xl:pr-[420px]' : ''}`}>
2026-02-24 20:44:16 +00:00
<Helmet
titleTemplate={`%s | ${APP_NAME}`}
title={translate('::App.Contact')}
defaultTitle={APP_NAME}
></Helmet>
{isDesignMode && (
<div className="fixed bottom-6 left-6 z-40 rounded-full bg-slate-900 px-4 py-2 text-sm font-medium text-white shadow-xl">
Contact designer aktif
2026-02-24 20:44:16 +00:00
</div>
)}
{isDesignMode && !isPanelVisible && (
<button
type="button"
onClick={() => setIsPanelVisible(true)}
className="fixed right-4 top-1/2 z-40 -translate-y-1/2 rounded-full bg-slate-900 px-4 py-2 text-sm font-semibold text-white shadow-xl"
>
{translate('::Public.designer.showPanel')}
</button>
)}
<SelectableBlock
id="hero"
isActive={selectedBlockId === 'hero'}
isDesignMode={isDesignMode}
onSelect={handleSelectBlock}
>
<div className="relative bg-blue-900 py-12 text-white">
<div
className="absolute inset-0 opacity-20"
style={{
backgroundImage: `url("${content?.heroImage || CONTACT_HERO_IMAGE_DEFAULT}")`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}}
></div>
<div className="container relative mx-auto pt-20">
<h1 className="ml-4 mb-2 mt-3 text-5xl font-bold text-white">{content?.heroTitle}</h1>
<p className="ml-4 max-w-3xl text-xl">{content?.heroSubtitle}</p>
</div>
</div>
</SelectableBlock>
2026-02-24 20:44:16 +00:00
<div className="py-16">
<div className="container mx-auto px-4">
<div className="grid grid-cols-1 gap-12 lg:grid-cols-2">
2026-02-24 20:44:16 +00:00
<div className="space-y-4">
<SelectableBlock
id="contact-info"
isActive={selectedBlockId === 'contact-info'}
isDesignMode={isDesignMode}
onSelect={handleSelectBlock}
>
<div className="rounded-xl bg-white p-8 shadow-lg">
<h2 className="mb-6 text-2xl font-bold text-gray-900">{content?.contactInfoTitle}</h2>
<div className="space-y-4">
<div className="flex items-start space-x-2">
<FaMapMarkerAlt className="mt-1 h-5 w-5 flex-shrink-0 text-blue-600" />
<div>
<p className="text-gray-600">{content?.address}</p>
</div>
2026-02-24 20:44:16 +00:00
</div>
<div className="flex items-start space-x-2">
<FaPhone className="h-5 w-5 flex-shrink-0 text-blue-600" />
<div>
<p className="text-gray-600">{content?.phoneNumber}</p>
</div>
2026-02-24 20:44:16 +00:00
</div>
<div className="flex items-start space-x-2">
<FaEnvelope className="h-5 w-5 flex-shrink-0 text-blue-600" />
<div>
<p className="text-gray-600">
<a href={`mailto:${content?.email}`} className="text-blue-600 hover:underline">
{content?.email}
</a>
</p>
</div>
2026-02-24 20:44:16 +00:00
</div>
<div className="flex items-start space-x-2">
<FaBuilding className="h-5 w-5 flex-shrink-0 text-blue-600" />
<div>
<p className="text-gray-600">{content?.location}</p>
</div>
2026-02-24 20:44:16 +00:00
</div>
<div className="flex items-start space-x-2">
<FaIdCard className="h-5 w-5 flex-shrink-0 text-blue-600" />
<div>
<p className="text-gray-600">{content?.taxNumber}</p>
</div>
2026-02-24 20:44:16 +00:00
</div>
</div>
</div>
</SelectableBlock>
2026-02-24 20:44:16 +00:00
<SelectableBlock
id="bank"
isActive={selectedBlockId === 'bank'}
isDesignMode={isDesignMode}
onSelect={handleSelectBlock}
>
<div className="rounded-xl bg-white p-8 shadow-lg">
<h2 className="mb-6 text-2xl font-bold text-gray-900">{content?.bankTitle}</h2>
<div className="mb-2">
<img
src="/img/enpara.svg"
alt="Enpara Logo"
className="mt-1 w-24 flex-shrink-0 object-contain"
/>
<div>
<h3 className="mb-1 font-semibold text-gray-900">{content?.bankAccountHolder}</h3>
<p className="mb-1 ml-1 text-gray-600">{content?.bankBranch}</p>
<p className="mb-1 ml-1 text-gray-600">{content?.bankAccountNumber}</p>
<p className="mb-1 ml-1 text-gray-600">{content?.bankIban}</p>
</div>
2026-02-24 20:44:16 +00:00
</div>
</div>
</SelectableBlock>
2026-02-24 20:44:16 +00:00
<SelectableBlock
id="work-hours"
isActive={selectedBlockId === 'work-hours'}
isDesignMode={isDesignMode}
onSelect={handleSelectBlock}
>
<div className="rounded-xl bg-white p-8 shadow-lg">
<h2 className="mb-6 text-2xl font-bold text-gray-900">{content?.workHoursTitle}</h2>
<div className="space-y-2">
<div className="flex items-center space-x-2">
<FaCalendarAlt className="h-5 w-5 text-blue-500" />
<p className="text-gray-600">{content?.workWeekday}</p>
</div>
<div className="flex items-center space-x-2">
<FaCalendarCheck className="h-5 w-5 text-blue-500" />
<p className="text-gray-600">{content?.workWeekend}</p>
</div>
<div className="flex items-center space-x-2">
<FaRegComment className="h-5 w-5 text-green-500" />
<p className="text-gray-600">{content?.workWhatsapp}</p>
</div>
2026-02-24 20:44:16 +00:00
</div>
</div>
</SelectableBlock>
2026-02-24 20:44:16 +00:00
</div>
<SelectableBlock
id="map"
isActive={selectedBlockId === 'map'}
isDesignMode={isDesignMode}
onSelect={handleSelectBlock}
>
<div className="rounded-xl bg-white p-8 shadow-lg">
<h2 className="mb-2 text-center text-2xl font-bold text-gray-900">{content?.mapTitle}</h2>
<div className="aspect-w-16 aspect-h-9 overflow-hidden rounded-xl bg-gray-200">
<iframe
src={content?.mapSrc}
width={content?.mapWidth}
style={{ border: 0 }}
height={content?.mapHeight}
allowFullScreen={(content?.mapAllowFullScreen || '').toLowerCase() === 'true'}
loading={content?.mapLoading as 'lazy' | 'eager' | undefined}
referrerPolicy={content?.mapReferrerPolicy as React.HTMLAttributeReferrerPolicy | undefined}
></iframe>
</div>
2026-02-24 20:44:16 +00:00
</div>
</SelectableBlock>
2026-02-24 20:44:16 +00:00
</div>
</div>
</div>
<DesignerDrawer
isOpen={isDesignMode && isPanelVisible}
selection={selectedSelection}
pageTitle="Contact"
selectedLanguage={selectedLanguage}
languages={
languageOptions.length > 0
? languageOptions
: supportedLanguages.map((language) => ({
key: language,
cultureName: language,
displayName: language.toUpperCase(),
}))
}
onClose={() => setIsPanelVisible(false)}
onSave={handleSaveAndExit}
onLanguageChange={handleLanguageChange}
onReset={resetContent}
onFieldChange={handleFieldChange}
/>
2026-02-24 20:44:16 +00:00
</div>
)
}
export default Contact