sozsoft-platform/ui/src/views/public/Demo.tsx
2026-09-08 18:00:23 +03:00

389 lines
13 KiB
TypeScript

import React, { useEffect, useRef, useState } from 'react'
import {
FaBuilding,
FaUser,
FaEnvelope,
FaPhone,
FaMapPin,
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, Notification, toast } from '@/components/ui'
import { translateLabel } from './localization'
import Input from '@/components/ui/Input'
interface DemoModalProps {
isOpen: boolean
onClose: () => void
}
type DemoErrors = Partial<Record<keyof DemoDto, string>>
const NUMERIC_FIELDS: Array<keyof DemoDto> = ['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<FieldProps> = ({ label, error, icon, children }) => (
<div>
<label className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-200">
{label} *
</label>
<div className="relative">
<span className="absolute left-3 top-3 h-5 w-5 text-gray-400">{icon}</span>
{children}
</div>
{error && <p className="mt-1 text-xs text-red-600 dark:text-red-400">{error}</p>}
</div>
)
const Demo: React.FC<DemoModalProps> = ({ isOpen, onClose }) => {
const { translate } = useLocalization()
const [formData, setFormData] = useState<DemoDto>(createEmptyForm)
const [errors, setErrors] = useState<DemoErrors>({})
const [isSubmitting, setIsSubmitting] = useState(false)
const [isSubmitted, setIsSubmitted] = useState(false)
const firstFieldRef = useRef<HTMLInputElement>(null)
useEffect(() => {
if (!isOpen) {
return
}
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')
const handleInputChange = (
event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
) => {
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')
}
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(
<Notification type="danger" duration={4000}>
{translateLabel(translate, 'App.Errors.ServerUnreachable')}
</Notification>,
{ placement: 'bottom-end' },
)
} finally {
setIsSubmitting(false)
}
}
const resetForm = () => {
setIsSubmitted(false)
setErrors({})
setFormData(createEmptyForm())
}
if (!isOpen && !isSubmitted) {
return null
}
if (isSubmitted) {
return (
<div
role="dialog"
aria-modal="true"
className="fixed inset-0 z-50 overflow-y-auto bg-gray-600/50 py-5 dark:bg-gray-950/70"
>
<div className="relative mx-auto w-full max-w-md rounded-xl bg-white shadow-lg dark:bg-gray-900">
<div className="w-full rounded-3xl border border-blue-200 bg-gradient-to-br from-blue-50 to-indigo-100 p-8 text-center shadow-xl dark:border-gray-700 dark:from-gray-900 dark:to-gray-800">
<Button
variant="plain"
shape="circle"
aria-label={translate('::App.Platform.Close')}
className="absolute right-4 top-4 !h-8 !w-8 !px-0 text-2xl text-gray-500 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-100"
onClick={() => {
resetForm()
onClose()
}}
>
&times;
</Button>
<div className="mx-auto mb-6 flex h-16 w-16 items-center justify-center rounded-full bg-green-500">
<FaCheckCircle className="h-8 w-8 text-white" />
</div>
<h2 className="mb-4 text-2xl font-bold text-gray-800 dark:text-gray-100">
{translate('::App.PublicDemo.ThankYou')}
</h2>
<p className="mb-6 text-gray-600 dark:text-gray-300">
{translate('::App.PublicDemo.ResultMessage')}
</p>
<Button variant="solid" onClick={resetForm}>
{translate('::App.PublicDemo.NewDemo')}
</Button>
</div>
</div>
</div>
)
}
return (
<div
role="dialog"
aria-modal="true"
aria-label={translate('::App.PublicDemo.DemoTitle')}
className="fixed inset-0 z-50 overflow-y-auto bg-gray-600/50 py-5 dark:bg-gray-950/70"
>
<div className="relative mx-auto w-full max-w-2xl rounded-xl bg-white shadow-lg dark:bg-gray-900">
<Button
aria-label={translate('::App.Platform.Close')}
variant="default"
shape="circle"
className="absolute right-4 top-4 z-10 !h-8 !w-8 !px-0"
onClick={onClose}
>
&times;
</Button>
<form
noValidate
className="rounded-3xl border border-gray-100 bg-white p-6 shadow-xl dark:border-gray-700 dark:bg-gray-900 lg:p-8"
onSubmit={handleSubmit}
>
<div className="mb-6 pr-10">
<h2 className="text-2xl font-bold text-gray-900 dark:text-gray-100">
{translate('::App.PublicDemo.DemoTitle')}
</h2>
<p className="mt-2 text-sm text-gray-500 dark:text-gray-400">
{translate('::App.PublicDemo.Subtitle')}
</p>
</div>
<div className="space-y-3">
<Field
label={translate('::App.Platform.Company')}
error={errors.organizationName}
icon={<FaBuilding className="h-5 w-5" />}
>
<Input
unstyle
ref={firstFieldRef}
type="text"
name="organizationName"
value={formData.organizationName}
className={inputClass(Boolean(errors.organizationName))}
placeholder={translate('::App.PublicDemo.OrganizationName')}
onChange={handleInputChange}
/>
</Field>
<Field
label={translate('::App.PublicCommon.FullName')}
error={errors.name}
icon={<FaUser className="h-5 w-5" />}
>
<Input
unstyle
type="text"
name="name"
value={formData.name}
className={inputClass(Boolean(errors.name))}
placeholder={translate('::App.PublicDemo.FullName')}
onChange={handleInputChange}
/>
</Field>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<Field
label={translate('::App.Listform.ListformField.Email')}
error={errors.email}
icon={<FaEnvelope className="h-5 w-5" />}
>
<Input
unstyle
type="email"
name="email"
value={formData.email}
className={inputClass(Boolean(errors.email))}
placeholder={translate('::App.PublicDemo.Email')}
onChange={handleInputChange}
/>
</Field>
<Field
label={translate('::Abp.Identity.User.UserInformation.PhoneNumber')}
error={errors.phoneNumber}
icon={<FaPhone className="h-5 w-5" />}
>
<Input
unstyle
type="tel"
name="phoneNumber"
value={formData.phoneNumber}
className={inputClass(Boolean(errors.phoneNumber))}
placeholder={translate('::App.PublicDemo.PhoneNumber')}
onChange={handleInputChange}
/>
</Field>
</div>
<Field
label={translate('::App.Listform.ListformField.Address')}
error={errors.address}
icon={<FaMapPin className="h-5 w-5" />}
>
<textarea
name="address"
rows={3}
value={formData.address}
className={inputClass(Boolean(errors.address))}
placeholder={translate('::App.PublicDemo.Address')}
onChange={handleInputChange}
/>
</Field>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<Field
label={translate('::App.PublicCommon.BranchCount')}
error={errors.numberOfBranches}
icon={<FaBuilding className="h-5 w-5" />}
>
<Input
unstyle
type="number"
min={0}
name="numberOfBranches"
value={formData.numberOfBranches}
className={inputClass(Boolean(errors.numberOfBranches))}
placeholder={translate('::App.PublicDemo.Branches')}
onChange={handleInputChange}
/>
</Field>
<Field
label={translate('::App.PublicCommon.UserCount')}
error={errors.numberOfUsers}
icon={<FaUsers className="h-5 w-5" />}
>
<Input
unstyle
type="number"
min={0}
name="numberOfUsers"
value={formData.numberOfUsers}
className={inputClass(Boolean(errors.numberOfUsers))}
placeholder={translate('::App.PublicDemo.Users')}
onChange={handleInputChange}
/>
</Field>
</div>
<Field
label={translate('::App.Listform.ListformField.Message')}
error={errors.message}
icon={<FaRegComment className="h-5 w-5" />}
>
<textarea
name="message"
rows={3}
value={formData.message}
className={`${inputClass(Boolean(errors.message))} resize-none`}
placeholder={translate('::App.PublicDemo.DemoMessage')}
onChange={handleInputChange}
/>
</Field>
<Button
type="submit"
variant="plain"
loading={isSubmitting}
icon={<FaPaperPlane className="h-5 w-5" />}
className="w-full !h-auto !justify-center !rounded-xl bg-gradient-to-r from-blue-600 to-purple-600 !px-6 !py-4 font-semibold text-white transition-all duration-300 hover:scale-[1.02] hover:from-blue-700 hover:to-purple-700 hover:shadow-lg"
>
{translate('::App.PublicDemo.Send')}
</Button>
</div>
</form>
</div>
</div>
)
}
export default Demo