diff --git a/client/src/pages/PublicIssueSubmit.jsx b/client/src/pages/PublicIssueSubmit.jsx index 39ad224..17745ce 100644 --- a/client/src/pages/PublicIssueSubmit.jsx +++ b/client/src/pages/PublicIssueSubmit.jsx @@ -1,41 +1,149 @@ import { useState, useEffect } from 'react' -import { AlertCircle, Send, CheckCircle2, Upload, X } from 'lucide-react' +import { AlertCircle, Send, CheckCircle2, Upload, X, Globe } from 'lucide-react' import { api } from '../utils/api' import FormInput from '../components/FormInput' import { useToast } from '../components/ToastContainer' -const TYPE_OPTIONS = [ - { value: 'request', label: 'Request' }, - { value: 'correction', label: 'Correction' }, - { value: 'complaint', label: 'Complaint' }, - { value: 'suggestion', label: 'Suggestion' }, - { value: 'other', label: 'Other' }, -] +// ─── Bilingual translations ──────────────────────────────────── +const T = { + en: { + pageTitle: 'Submit an Issue', + pageSubtitle: 'Report issues, request corrections, or make suggestions. We\'ll track your submission and keep you updated.', + yourInfo: 'Your Information', + name: 'Name', + namePlaceholder: 'Your full name', + nameRequired: 'Name is required', + email: 'Email', + emailPlaceholder: 'your.email@example.com', + emailRequired: 'Email is required', + emailInvalid: 'Invalid email address', + phone: 'Phone (Optional)', + phonePlaceholder: '+966 5X XXX XXXX', + teamQuestion: 'Which team should handle your issue?', + selectTeam: 'Select a team', + issueDetails: 'Issue Details', + category: 'Category', + categoryPlaceholder: 'e.g., Marketing, IT, Operations', + type: 'Type', + priority: 'Priority', + title: 'Title', + titlePlaceholder: 'Brief summary of the issue', + titleRequired: 'Title is required', + description: 'Description', + descriptionPlaceholder: 'Provide detailed information about the issue...', + descriptionRequired: 'Description is required', + attachment: 'Attachment (Optional)', + uploadPrompt: 'Click to upload a file (screenshots, documents, etc.)', + submit: 'Submit Issue', + submitting: 'Submitting...', + submitFailed: 'Failed to submit issue. Please try again.', + footerNote: 'You\'ll receive a tracking link to monitor the progress of your issue.', + // Success page + successTitle: 'Issue Submitted Successfully!', + successMessage: 'Thank you for submitting your issue. You can track its progress using the link below.', + trackingLink: 'Your Tracking Link', + copy: 'Copy', + copied: 'Copied to clipboard!', + trackIssue: 'Track Your Issue', + submitAnother: 'Submit Another Issue', + // Options + request: 'Request', correction: 'Correction', complaint: 'Complaint', suggestion: 'Suggestion', other: 'Other', + low: 'Low', medium: 'Medium', high: 'High', urgent: 'Urgent', + }, + ar: { + pageTitle: 'تقديم مشكلة', + pageSubtitle: 'أبلغ عن مشاكل، اطلب تصحيحات، أو قدّم اقتراحات. سنتابع طلبك ونبقيك على اطلاع.', + yourInfo: 'معلوماتك', + name: 'الاسم', + namePlaceholder: 'الاسم الكامل', + nameRequired: 'الاسم مطلوب', + email: 'البريد الإلكتروني', + emailPlaceholder: 'your.email@example.com', + emailRequired: 'البريد الإلكتروني مطلوب', + emailInvalid: 'بريد إلكتروني غير صالح', + phone: 'الهاتف (اختياري)', + phonePlaceholder: '+966 5X XXX XXXX', + teamQuestion: 'أي فريق يجب أن يتعامل مع مشكلتك؟', + selectTeam: 'اختر فريقاً', + issueDetails: 'تفاصيل المشكلة', + category: 'الفئة', + categoryPlaceholder: 'مثال: تسويق، تقنية، عمليات', + type: 'النوع', + priority: 'الأولوية', + title: 'العنوان', + titlePlaceholder: 'ملخص موجز للمشكلة', + titleRequired: 'العنوان مطلوب', + description: 'الوصف', + descriptionPlaceholder: 'قدّم معلومات مفصلة عن المشكلة...', + descriptionRequired: 'الوصف مطلوب', + attachment: 'مرفق (اختياري)', + uploadPrompt: 'اضغط لرفع ملف (لقطات شاشة، مستندات، إلخ)', + submit: 'تقديم المشكلة', + submitting: 'جارٍ التقديم...', + submitFailed: 'فشل في تقديم المشكلة. يرجى المحاولة مرة أخرى.', + footerNote: 'ستتلقى رابط تتبع لمراقبة تقدم مشكلتك.', + successTitle: 'تم تقديم المشكلة بنجاح!', + successMessage: 'شكراً لتقديم مشكلتك. يمكنك تتبع تقدمها من خلال الرابط أدناه.', + trackingLink: 'رابط التتبع', + copy: 'نسخ', + copied: 'تم النسخ!', + trackIssue: 'تتبع مشكلتك', + submitAnother: 'تقديم مشكلة أخرى', + request: 'طلب', correction: 'تصحيح', complaint: 'شكوى', suggestion: 'اقتراح', other: 'أخرى', + low: 'منخفضة', medium: 'متوسطة', high: 'عالية', urgent: 'عاجلة', + }, +} -const PRIORITY_OPTIONS = [ - { value: 'low', label: 'Low' }, - { value: 'medium', label: 'Medium' }, - { value: 'high', label: 'High' }, - { value: 'urgent', label: 'Urgent' }, -] +function detectLang() { + const nav = navigator.language || navigator.userLanguage || '' + return nav.startsWith('ar') ? 'ar' : 'en' +} + +function LangToggle({ lang, setLang }) { + return ( + + ) +} export default function PublicIssueSubmit() { const toast = useToast() + const [lang, setLang] = useState(detectLang) + const t = (key) => T[lang]?.[key] || T.en[key] || key + const dir = lang === 'ar' ? 'rtl' : 'ltr' + + useEffect(() => { + document.documentElement.dir = dir + document.documentElement.lang = lang + return () => { document.documentElement.dir = 'ltr'; document.documentElement.lang = 'en' } + }, [lang, dir]) + + const TYPE_OPTIONS = [ + { value: 'request', label: t('request') }, + { value: 'correction', label: t('correction') }, + { value: 'complaint', label: t('complaint') }, + { value: 'suggestion', label: t('suggestion') }, + { value: 'other', label: t('other') }, + ] + + const PRIORITY_OPTIONS = [ + { value: 'low', label: t('low') }, + { value: 'medium', label: t('medium') }, + { value: 'high', label: t('high') }, + { value: 'urgent', label: t('urgent') }, + ] - // Team pre-selection from URL const urlParams = new URLSearchParams(window.location.search) const teamParam = urlParams.get('team') const [form, setForm] = useState({ - name: '', - email: '', - phone: '', - category: 'Marketing', - type: 'request', - priority: 'medium', - title: '', - description: '', - team_id: teamParam || '', + name: '', email: '', phone: '', category: 'Marketing', + type: 'request', priority: 'medium', title: '', description: '', team_id: teamParam || '', }) const [file, setFile] = useState(null) const [submitting, setSubmitting] = useState(false) @@ -52,25 +160,16 @@ export default function PublicIssueSubmit() { const updateForm = (field, value) => { setForm((f) => ({ ...f, [field]: value })) - if (errors[field]) { - setErrors((e) => ({ ...e, [field]: '' })) - } - } - - const handleFileChange = (e) => { - const selectedFile = e.target.files?.[0] - if (selectedFile) { - setFile(selectedFile) - } + if (errors[field]) setErrors((e) => ({ ...e, [field]: '' })) } const validate = () => { const newErrors = {} - if (!form.name.trim()) newErrors.name = 'Name is required' - if (!form.email.trim()) newErrors.email = 'Email is required' - else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email)) newErrors.email = 'Invalid email address' - if (!form.title.trim()) newErrors.title = 'Title is required' - if (!form.description.trim()) newErrors.description = 'Description is required' + if (!form.name.trim()) newErrors.name = t('nameRequired') + if (!form.email.trim()) newErrors.email = t('emailRequired') + else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email)) newErrors.email = t('emailInvalid') + if (!form.title.trim()) newErrors.title = t('titleRequired') + if (!form.description.trim()) newErrors.description = t('descriptionRequired') setErrors(newErrors) return Object.keys(newErrors).length === 0 } @@ -78,7 +177,6 @@ export default function PublicIssueSubmit() { const handleSubmit = async (e) => { e.preventDefault() if (!validate() || submitting) return - try { setSubmitting(true) const formData = new FormData() @@ -90,19 +188,14 @@ export default function PublicIssueSubmit() { formData.append('priority', form.priority) formData.append('title', form.title) formData.append('description', form.description) - if (form.team_id) { - formData.append('team_id', form.team_id) - } - if (file) { - formData.append('file', file) - } - + if (form.team_id) formData.append('team_id', form.team_id) + if (file) formData.append('file', file) const result = await api.upload('/public/issues', formData) setTrackingToken(result.token) setSubmitted(true) } catch (err) { console.error('Submit error:', err) - toast.error('Failed to submit issue. Please try again.') + toast.error(t('submitFailed')) } finally { setSubmitting(false) } @@ -111,65 +204,40 @@ export default function PublicIssueSubmit() { if (submitted) { const trackingUrl = `${window.location.origin}/track/${trackingToken}` return ( -
- Thank you for submitting your issue. You can track its progress using the link below. -
+{t('successMessage')}
- Report issues, request corrections, or make suggestions. We'll track your submission and keep you updated. -
+{t('pageSubtitle')}
- You'll receive a tracking link to monitor the progress of your issue. -
+{t('footerNote')}
- The tracking link you used is invalid or the issue has been removed. -
- - Submit a New Issue +{t('notFoundMessage')}
+ + {t('submitNew')}{issue.description}
{issue.resolution_summary}
{issue.resolved_at && (- {formatDate(issue.resolved_at)} + {dateFmt(issue.resolved_at)}
)}No updates yet. We'll post updates here as we work on your issue.
+{t('noUpdates')}
{update.message}
{att.original_name}
-- {formatFileSize(att.size)} • {att.uploaded_by} -
+{fileSize(att.size)} • {att.uploaded_by}