389 lines
13 KiB
TypeScript
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()
|
|
}}
|
|
>
|
|
×
|
|
</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}
|
|
>
|
|
×
|
|
</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
|