steelforesight/src/pages/Contact/Contact.tsx

444 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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',
}
}