444 lines
18 KiB
TypeScript
444 lines
18 KiB
TypeScript
import { useState, useRef } from 'react'
|
||
import { useLang } from '@/context/LangContext'
|
||
import { MapPin, Phone, Mail, ChevronDown, Paperclip, Send, X } from 'lucide-react'
|
||
|
||
const NAVY = '#032340'
|
||
const GOLD = '#CD9E53'
|
||
|
||
const tr = {
|
||
fa: {
|
||
breadcrumbHome: 'خانه',
|
||
breadcrumbContact: 'تماس با ما',
|
||
title: 'تماس با ما',
|
||
address: 'پارک علم و فناوری دانشگاه تهران',
|
||
phone: '۰۲۱-۱۲۳۴۵۶۷',
|
||
email: 'info@steelforesight.com',
|
||
addressLabel: 'آدرس',
|
||
phoneLabel: 'تلفن',
|
||
emailLabel: 'ایمیل',
|
||
description:
|
||
'اندیشکده فولاد آینده، بستری برای تبادل دانش و همافزایی است. ما با نگاهی تحلیلی به دیدگاهها و پرسشهای شما مینگریم و مشتاقانه آمادهایم تا در مسیر خلق ارزشهای راهبردی برای صنعت فولاد کشور، همراه و پاسخگوی شما باشیم.',
|
||
nameLabel: 'نام و نام خانوادگی',
|
||
namePlaceholder: 'علیرضا اکبریان',
|
||
subjectLabel: 'موضوع',
|
||
subjectPlaceholder: 'موضوع خود را انتخاب کنید',
|
||
subjects: ['همکاری پژوهشی', 'درخواست مشاوره', 'عضویت', 'گزارش فنی', 'سایر'],
|
||
emailLabel2: 'ایمیل',
|
||
emailPlaceholder: 'M.jalali@gamil.com',
|
||
phoneLabel2: 'شماره تماس',
|
||
phonePlaceholder: '۰۹۱۲۳۴۵۶۷۸۹',
|
||
messageLabel: 'متن پیام',
|
||
messagePlaceholder: 'پیام خود را وارد کنید...',
|
||
fileHint: 'حداکثر ۵ تصویر حداکثر یک مگابایت، یک ویدیو MP4 حداکثر ۵۰ مگابایت',
|
||
addFile: 'افزودن فایل',
|
||
submit: 'ثبت و ارسال',
|
||
required: 'این فیلد الزامی است',
|
||
subjectRequired: 'لطفاً توضیحات خود را وارد کنید.',
|
||
},
|
||
en: {
|
||
breadcrumbHome: 'Home',
|
||
breadcrumbContact: 'Contact Us',
|
||
title: 'Contact Us',
|
||
address: 'Tehran, North Kargar Street',
|
||
phone: '021-1234567',
|
||
email: 'info@steelforesight.com',
|
||
addressLabel: 'Address',
|
||
phoneLabel: 'Phone',
|
||
emailLabel: 'Email',
|
||
description:
|
||
'The Steel Futures Institute is a platform for knowledge exchange and synergy. We approach your ideas and questions with an analytical lens, and are eager to accompany you in creating strategic value.',
|
||
nameLabel: 'Full Name',
|
||
namePlaceholder: 'Alireza Akbarian',
|
||
subjectLabel: 'Subject',
|
||
subjectPlaceholder: 'Select a subject',
|
||
subjects: ['Research Collaboration', 'Consulting Request', 'Membership', 'Technical Report', 'Other'],
|
||
emailLabel2: 'Email',
|
||
emailPlaceholder: 'M.jalali@email.com',
|
||
phoneLabel2: 'Phone Number',
|
||
phonePlaceholder: '+98 912 345 6789',
|
||
messageLabel: 'Message',
|
||
messagePlaceholder: 'Write your message here...',
|
||
fileHint: 'Max 5 images up to 1MB each, one MP4 video up to 50MB',
|
||
addFile: 'Add File',
|
||
submit: 'Submit',
|
||
required: 'This field is required',
|
||
subjectRequired: 'Please enter your details.',
|
||
},
|
||
}
|
||
|
||
type FormState = {
|
||
name: string
|
||
subject: string
|
||
email: string
|
||
phone: string
|
||
message: string
|
||
files: File[]
|
||
}
|
||
type Errors = Partial<Record<keyof FormState, string>>
|
||
|
||
export default function Contact() {
|
||
const { lang } = useLang()
|
||
const t = tr[lang]
|
||
const isRtl = lang === 'fa'
|
||
const dir = isRtl ? 'rtl' : 'ltr'
|
||
|
||
const [form, setForm] = useState<FormState>({ name: '', subject: '', email: '', phone: '', message: '', files: [] })
|
||
const [errors, setErrors] = useState<Errors>({})
|
||
const [subjectOpen, setSubjectOpen] = useState(false)
|
||
const fileRef = useRef<HTMLInputElement>(null)
|
||
|
||
const set = (field: keyof FormState, value: string) => {
|
||
setForm(f => ({ ...f, [field]: value }))
|
||
if (errors[field]) setErrors(e => ({ ...e, [field]: undefined }))
|
||
}
|
||
|
||
const validate = () => {
|
||
const e: Errors = {}
|
||
if (!form.name.trim()) e.name = t.required
|
||
if (!form.subject) e.subject = t.subjectRequired
|
||
if (!form.email.trim()) e.email = t.required
|
||
if (!form.phone.trim()) e.phone = t.required
|
||
setErrors(e)
|
||
return Object.keys(e).length === 0
|
||
}
|
||
|
||
const handleSubmit = (ev: React.FormEvent) => {
|
||
ev.preventDefault()
|
||
if (validate()) alert(isRtl ? 'پیام شما ارسال شد' : 'Your message has been sent')
|
||
}
|
||
|
||
const handleFiles = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||
const picked = Array.from(ev.target.files ?? [])
|
||
setForm(f => ({ ...f, files: [...f.files, ...picked].slice(0, 6) }))
|
||
ev.target.value = ''
|
||
}
|
||
|
||
const removeFile = (i: number) =>
|
||
setForm(f => ({ ...f, files: f.files.filter((_, idx) => idx !== i) }))
|
||
|
||
return (
|
||
<div dir={dir} style={{ background: 'var(--paper)', minHeight: '100vh' }}>
|
||
|
||
{/* ── breadcrumb ── */}
|
||
<div style={{
|
||
borderBottom: '1px solid rgba(3,35,64,0.08)',
|
||
padding: '14px 40px',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: 8,
|
||
fontSize: 13,
|
||
color: 'rgba(3,35,64,0.5)',
|
||
}}>
|
||
<span style={{ color: NAVY, fontWeight: 700, fontSize: 16 }}>|</span>
|
||
<span style={{ color: NAVY, fontWeight: 700, fontSize: 15 }}>{t.breadcrumbHome}</span>
|
||
<span>{isRtl ? '›' : '›'}</span>
|
||
<span style={{ color: GOLD, fontWeight: 700 }}>{t.breadcrumbContact}</span>
|
||
</div>
|
||
|
||
{/* ── breathing pin keyframes ── */}
|
||
<style>{`
|
||
@keyframes pin-pulse {
|
||
0%, 100% { transform: translate(-50%, -50%) scale(1); opacity: 1; }
|
||
50% { transform: translate(-50%, -50%) scale(1.18); opacity: 0.75; }
|
||
}
|
||
@keyframes ring-expand {
|
||
0% { transform: translate(-50%, -50%) scale(0.8); opacity: 0.6; }
|
||
100% { transform: translate(-50%, -50%) scale(2.6); opacity: 0; }
|
||
}
|
||
.map-pin-wrap { position: absolute; top: 50%; left: 50%; pointer-events: none; z-index: 10; }
|
||
.map-pin-dot { position: absolute; top: 50%; left: 50%;
|
||
width: 20px; height: 20px; background: #e53e3e; border: 3px solid #fff;
|
||
border-radius: 50%; box-shadow: 0 2px 10px rgba(0,0,0,0.35);
|
||
animation: pin-pulse 2s ease-in-out infinite; }
|
||
.map-pin-ring { position: absolute; top: 50%; left: 50%; width: 20px; height: 20px;
|
||
border: 2px solid rgba(229,62,62,0.55); border-radius: 50%;
|
||
animation: ring-expand 2s ease-out infinite; }
|
||
.map-pin-ring2 { animation-delay: 1s; }
|
||
`}</style>
|
||
|
||
{/* ── map + contact card ── */}
|
||
<div className="max-w-7xl mx-auto px-12 max-md:px-5" style={{ paddingTop: 32 }}>
|
||
{/* wrapper: relative so card can overflow downward */}
|
||
<div style={{ position: 'relative', paddingBottom: 80 }}>
|
||
|
||
{/* map */}
|
||
<div style={{ borderRadius: 16, overflow: 'hidden', boxShadow: '0 4px 32px rgba(3,35,64,0.12)', height: 380, position: 'relative' }} className="max-md:!h-[260px]">
|
||
<iframe
|
||
title="map"
|
||
src="https://maps.google.com/maps?q=35.7325709,51.3894991&z=17&hl=fa&output=embed"
|
||
style={{ width: '100%', height: '100%', border: 'none', display: 'block', filter: 'grayscale(10%)' }}
|
||
allowFullScreen
|
||
loading="lazy"
|
||
referrerPolicy="no-referrer-when-downgrade"
|
||
/>
|
||
</div>
|
||
|
||
{/* contact card — half inside map, half below */}
|
||
<div style={{
|
||
position: 'absolute',
|
||
bottom: 0,
|
||
right: 32,
|
||
background: '#fff',
|
||
borderRadius: 16,
|
||
padding: '28px 32px',
|
||
boxShadow: '0 8px 40px rgba(3,35,64,0.14)',
|
||
minWidth: 300,
|
||
direction: dir,
|
||
zIndex: 10,
|
||
}} className="max-md:!position-static max-md:!right-0 max-md:!min-w-0">
|
||
<h2 style={{ fontSize: 17, fontWeight: 900, color: NAVY, margin: '0 0 16px' }}>{t.title}</h2>
|
||
{[
|
||
{ icon: <MapPin size={15} color={GOLD} />, label: t.addressLabel, value: t.address },
|
||
{ icon: <Phone size={15} color={GOLD} />, label: t.phoneLabel, value: t.phone },
|
||
{ icon: <Mail size={15} color={GOLD} />, label: t.emailLabel, value: t.email },
|
||
].map(({ icon, label, value }) => (
|
||
<div key={label} style={{ display: 'flex', alignItems: 'flex-start', gap: 10, marginBottom: 12 }}>
|
||
<span style={{ marginTop: 2, flexShrink: 0 }}>{icon}</span>
|
||
<div style={{ fontSize: 13, color: NAVY, lineHeight: 1.6 }}>
|
||
<span style={{ color: 'rgba(3,35,64,0.45)', marginInlineEnd: 4 }}>{label}:</span>
|
||
{value}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
{/* ── form section ── */}
|
||
<div className="max-w-7xl mx-auto px-12 max-md:px-5" style={{ paddingTop: 48, paddingBottom: 80 }}>
|
||
|
||
<p style={{ fontSize: 15, fontWeight: 700, color: NAVY, lineHeight: 2, marginBottom: 40 }}>
|
||
{t.description}
|
||
</p>
|
||
|
||
<form onSubmit={handleSubmit} noValidate>
|
||
|
||
{/* row 1: name + subject */}
|
||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 20, marginBottom: 20 }} className="max-md:!grid-cols-1">
|
||
<Field label={t.nameLabel} required error={errors.name} rtl={isRtl}>
|
||
<input
|
||
type="text"
|
||
placeholder={t.namePlaceholder}
|
||
value={form.name}
|
||
onChange={e => set('name', e.target.value)}
|
||
style={inputStyle(!!errors.name)}
|
||
/>
|
||
</Field>
|
||
|
||
<Field label={t.subjectLabel} required error={errors.subject} rtl={isRtl} noErrorMsg>
|
||
<div style={{ position: 'relative' }}>
|
||
<button
|
||
type="button"
|
||
onClick={() => setSubjectOpen(o => !o)}
|
||
style={{
|
||
...inputStyle(!!errors.subject),
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'space-between',
|
||
cursor: 'pointer',
|
||
background: '#fff',
|
||
width: '100%',
|
||
color: form.subject ? NAVY : 'rgba(3,35,64,0.35)',
|
||
textAlign: isRtl ? 'right' : 'left',
|
||
}}
|
||
>
|
||
<span style={{ flex: 1, textAlign: isRtl ? 'right' : 'left' }}>
|
||
{form.subject || t.subjectPlaceholder}
|
||
</span>
|
||
<ChevronDown size={16} style={{ flexShrink: 0 }} />
|
||
</button>
|
||
{subjectOpen && (
|
||
<ul style={{
|
||
position: 'absolute',
|
||
top: '100%',
|
||
left: 0, right: 0,
|
||
background: '#fff',
|
||
border: '1px solid rgba(3,35,64,0.15)',
|
||
borderRadius: 10,
|
||
marginTop: 4,
|
||
zIndex: 50,
|
||
overflow: 'hidden',
|
||
boxShadow: '0 8px 24px rgba(3,35,64,0.10)',
|
||
listStyle: 'none',
|
||
padding: 0,
|
||
margin: '4px 0 0',
|
||
}}>
|
||
{t.subjects.map(s => (
|
||
<li key={s}>
|
||
<button
|
||
type="button"
|
||
onClick={() => { set('subject', s); setSubjectOpen(false) }}
|
||
style={{
|
||
width: '100%',
|
||
padding: '10px 16px',
|
||
textAlign: isRtl ? 'right' : 'left',
|
||
background: form.subject === s ? 'rgba(205,158,83,0.08)' : 'transparent',
|
||
color: form.subject === s ? GOLD : NAVY,
|
||
fontSize: 14,
|
||
cursor: 'pointer',
|
||
border: 'none',
|
||
display: 'block',
|
||
fontWeight: form.subject === s ? 700 : 400,
|
||
fontFamily: 'inherit',
|
||
}}
|
||
>
|
||
{s}
|
||
</button>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
)}
|
||
</div>
|
||
{errors.subject && (
|
||
<p style={{ color: '#e53e3e', fontSize: 12, marginTop: 4 }}>{errors.subject}</p>
|
||
)}
|
||
</Field>
|
||
</div>
|
||
|
||
{/* row 2: email + phone */}
|
||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 20, marginBottom: 20 }} className="max-md:!grid-cols-1">
|
||
<Field label={t.emailLabel2} required error={errors.email} rtl={isRtl}>
|
||
<input
|
||
type="email"
|
||
placeholder={t.emailPlaceholder}
|
||
value={form.email}
|
||
onChange={e => set('email', e.target.value)}
|
||
style={{ ...inputStyle(!!errors.email), direction: 'ltr', textAlign: isRtl ? 'right' : 'left' }}
|
||
/>
|
||
</Field>
|
||
<Field label={t.phoneLabel2} required error={errors.phone} rtl={isRtl}>
|
||
<input
|
||
type="tel"
|
||
placeholder={t.phonePlaceholder}
|
||
value={form.phone}
|
||
onChange={e => set('phone', e.target.value)}
|
||
style={{ ...inputStyle(!!errors.phone), direction: 'ltr', textAlign: isRtl ? 'right' : 'left' }}
|
||
/>
|
||
</Field>
|
||
</div>
|
||
|
||
{/* message */}
|
||
<div style={{ marginBottom: 20 }}>
|
||
<Field label={t.messageLabel} rtl={isRtl}>
|
||
<textarea
|
||
rows={5}
|
||
placeholder={t.messagePlaceholder}
|
||
value={form.message}
|
||
onChange={e => set('message', e.target.value)}
|
||
style={{ ...inputStyle(false), resize: 'vertical', minHeight: 120 }}
|
||
/>
|
||
</Field>
|
||
</div>
|
||
|
||
{/* file upload */}
|
||
<div style={{
|
||
border: '1.5px dashed rgba(3,35,64,0.2)',
|
||
borderRadius: 12,
|
||
padding: '24px 20px',
|
||
marginBottom: 32,
|
||
textAlign: 'center',
|
||
background: 'rgba(3,35,64,0.015)',
|
||
}}>
|
||
<p style={{ fontSize: 13, color: 'rgba(3,35,64,0.5)', marginBottom: 14 }}>{t.fileHint}</p>
|
||
{form.files.length > 0 && (
|
||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, justifyContent: 'center', marginBottom: 14 }}>
|
||
{form.files.map((f, i) => (
|
||
<div key={i} style={{
|
||
display: 'flex', alignItems: 'center', gap: 6,
|
||
background: 'rgba(3,35,64,0.07)', borderRadius: 8,
|
||
padding: '4px 10px', fontSize: 12, color: NAVY, maxWidth: 180,
|
||
}}>
|
||
<Paperclip size={12} />
|
||
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{f.name}</span>
|
||
<button type="button" onClick={() => removeFile(i)}
|
||
style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'rgba(3,35,64,0.4)', padding: 0, lineHeight: 1 }}>
|
||
<X size={12} />
|
||
</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
<input ref={fileRef} type="file" multiple accept="image/*,video/mp4" onChange={handleFiles} style={{ display: 'none' }} />
|
||
<button
|
||
type="button"
|
||
onClick={() => fileRef.current?.click()}
|
||
style={{
|
||
display: 'inline-flex', alignItems: 'center', gap: 8,
|
||
border: `1.5px solid ${GOLD}`, borderRadius: 8,
|
||
padding: '8px 20px', background: 'transparent',
|
||
color: GOLD, fontSize: 14, fontWeight: 700, cursor: 'pointer', fontFamily: 'inherit',
|
||
}}
|
||
>
|
||
<Paperclip size={15} />
|
||
{t.addFile}
|
||
</button>
|
||
</div>
|
||
|
||
{/* submit — always left-aligned, icon on the left */}
|
||
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||
<button
|
||
type="submit"
|
||
style={{
|
||
display: 'inline-flex', alignItems: 'center', flexDirection: 'row-reverse', gap: 10,
|
||
background: '#2d6a2d', color: '#fff', border: 'none',
|
||
borderRadius: 10, padding: '13px 36px',
|
||
fontSize: 15, fontWeight: 800, cursor: 'pointer',
|
||
letterSpacing: 0.5, fontFamily: 'inherit',
|
||
}}
|
||
>
|
||
<Send size={16} style={{ transform: 'scaleX(-1)' }} />
|
||
{t.submit}
|
||
</button>
|
||
</div>
|
||
|
||
</form>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function Field({
|
||
label, required, error, children, noErrorMsg, style: extraStyle,
|
||
}: {
|
||
label: string
|
||
required?: boolean
|
||
error?: string
|
||
rtl?: boolean
|
||
children: React.ReactNode
|
||
noErrorMsg?: boolean
|
||
style?: React.CSSProperties
|
||
}) {
|
||
return (
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, ...extraStyle }}>
|
||
<label style={{
|
||
fontSize: 13, fontWeight: 700, color: NAVY,
|
||
display: 'flex', alignItems: 'center', gap: 4,
|
||
}}>
|
||
{required && <span style={{ color: '#e53e3e' }}>*</span>}
|
||
{label}
|
||
</label>
|
||
{children}
|
||
{!noErrorMsg && error && (
|
||
<p style={{ color: '#e53e3e', fontSize: 12, margin: 0 }}>{error}</p>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function inputStyle(hasError: boolean): React.CSSProperties {
|
||
return {
|
||
width: '100%',
|
||
padding: '11px 14px',
|
||
border: `1.5px solid ${hasError ? '#e53e3e' : 'rgba(3,35,64,0.15)'}`,
|
||
borderRadius: 10,
|
||
fontSize: 14,
|
||
color: NAVY,
|
||
background: '#fff',
|
||
outline: 'none',
|
||
fontFamily: 'inherit',
|
||
boxSizing: 'border-box',
|
||
transition: 'border-color 150ms',
|
||
}
|
||
}
|