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 ( -
+
+
-

Issue Submitted Successfully!

-

- Thank you for submitting your issue. You can track its progress using the link below. -

+

{t('successTitle')}

+

{t('successMessage')}

- +
- -
- - Track Your Issue + + {t('trackIssue')} -
@@ -179,194 +247,125 @@ export default function PublicIssueSubmit() { } return ( -
+
+
{/* Header */}
-

Submit an Issue

-

- Report issues, request corrections, or make suggestions. We'll track your submission and keep you updated. -

+

{t('pageTitle')}

+

{t('pageSubtitle')}

{/* Form */}
{/* Contact Information */}
-

Your Information

+

{t('yourInfo')}

- updateForm('name', e.target.value)} - placeholder="Your full name" - required - error={errors.name} - /> - updateForm('email', e.target.value)} - placeholder="your.email@example.com" - required - error={errors.email} - /> - updateForm('phone', e.target.value)} - placeholder="+966 5X XXX XXXX" - /> + updateForm('name', e.target.value)} + placeholder={t('namePlaceholder')} required error={errors.name} /> + updateForm('email', e.target.value)} + placeholder={t('emailPlaceholder')} required error={errors.email} /> + updateForm('phone', e.target.value)} + placeholder={t('phonePlaceholder')} />
{/* Team Selection */} {!teamParam && teams.length > 0 && (
-

Which team should handle your issue?

- updateForm('team_id', e.target.value)} + className="w-full px-3 py-2 text-sm border border-border rounded-lg bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-brand-primary/20 transition-colors"> + + {teams.map((team) => )}
)} {/* Issue Details */}
-

Issue Details

- +

{t('issueDetails')}

- updateForm('category', e.target.value)} - placeholder="e.g., Marketing, IT, Operations" - className="w-full px-3 py-2 text-sm border border-border rounded-lg bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-brand-primary/20 transition-colors" - /> + updateForm('category', e.target.value)} + placeholder={t('categoryPlaceholder')} + className="w-full px-3 py-2 text-sm border border-border rounded-lg bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-brand-primary/20 transition-colors" />
- updateForm('type', e.target.value)} + className="w-full px-3 py-2 text-sm border border-border rounded-lg bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-brand-primary/20 transition-colors"> + {TYPE_OPTIONS.map((opt) => )}
- updateForm('priority', e.target.value)} + className="w-full px-3 py-2 text-sm border border-border rounded-lg bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-brand-primary/20 transition-colors"> + {PRIORITY_OPTIONS.map((opt) => )}
- updateForm('title', e.target.value)} - placeholder="Brief summary of the issue" - required - error={errors.title} - /> - - updateForm('title', e.target.value)} + placeholder={t('titlePlaceholder')} required error={errors.title} /> + updateForm('description', e.target.value)} - placeholder="Provide detailed information about the issue..." - rows={6} - required - error={errors.description} - /> + placeholder={t('descriptionPlaceholder')} rows={6} required error={errors.description} />
{/* File Upload */}
-

Attachment (Optional)

+

{t('attachment')}

- {/* Submit Button */} -
- {/* Footer Note */} -

- You'll receive a tracking link to monitor the progress of your issue. -

+

{t('footerNote')}

) diff --git a/client/src/pages/PublicIssueTracker.jsx b/client/src/pages/PublicIssueTracker.jsx index 94f3e22..158bd24 100644 --- a/client/src/pages/PublicIssueTracker.jsx +++ b/client/src/pages/PublicIssueTracker.jsx @@ -1,56 +1,126 @@ import { useState, useEffect } from 'react' import { useParams } from 'react-router-dom' -import { AlertCircle, Clock, CheckCircle2, XCircle, MessageCircle, Upload, FileText, Send } from 'lucide-react' +import { AlertCircle, Clock, CheckCircle2, XCircle, MessageCircle, Upload, FileText, Send, Globe } from 'lucide-react' import { api } from '../utils/api' import { useToast } from '../components/ToastContainer' -const STATUS_CONFIG = { - new: { label: 'New', bg: 'bg-purple-100', text: 'text-purple-700', dot: 'bg-purple-500', icon: AlertCircle }, - acknowledged: { label: 'Acknowledged', bg: 'bg-blue-100', text: 'text-blue-700', dot: 'bg-blue-500', icon: CheckCircle2 }, - in_progress: { label: 'In Progress', bg: 'bg-amber-100', text: 'text-amber-700', dot: 'bg-amber-500', icon: Clock }, - resolved: { label: 'Resolved', bg: 'bg-emerald-100', text: 'text-emerald-700', dot: 'bg-emerald-500', icon: CheckCircle2 }, - declined: { label: 'Declined', bg: 'bg-gray-100', text: 'text-gray-700', dot: 'bg-gray-500', icon: XCircle }, +// ─── Bilingual translations ──────────────────────────────────── +const T = { + en: { + description: 'Description', + submitted: 'Submitted', + lastUpdated: 'Last Updated', + resolution: 'Resolution', + declined: 'Declined', + progressUpdates: 'Progress Updates', + noUpdates: 'No updates yet. We\'ll post updates here as we work on your issue.', + team: 'Team', you: 'You', + attachments: 'Attachments', + download: 'Download', + addComment: 'Add a Comment', + yourName: 'Your Name (Optional)', + yourNamePlaceholder: 'Your name', + message: 'Message', + messagePlaceholder: 'Add additional information or ask a question...', + sendComment: 'Send Comment', + sending: 'Sending...', + uploadFile: 'Upload File', + uploading: 'Uploading...', + bookmarkNote: 'Bookmark this page to check your issue status anytime.', + notFoundTitle: 'Issue Not Found', + notFoundMessage: 'The tracking link you used is invalid or the issue has been removed.', + submitNew: 'Submit a New Issue', + failedComment: 'Failed to add comment', + failedUpload: 'Failed to upload file', + priority: 'Priority', + // Status + new: 'New', acknowledged: 'Acknowledged', in_progress: 'In Progress', resolved: 'Resolved', declined_status: 'Declined', + // Priority + low: 'Low', medium: 'Medium', high: 'High', urgent: 'Urgent', + }, + ar: { + description: 'الوصف', + submitted: 'تم التقديم', + lastUpdated: 'آخر تحديث', + resolution: 'الحل', + declined: 'مرفوض', + progressUpdates: 'تحديثات التقدم', + noUpdates: 'لا توجد تحديثات بعد. سننشر التحديثات هنا أثناء العمل على مشكلتك.', + team: 'الفريق', you: 'أنت', + attachments: 'المرفقات', + download: 'تحميل', + addComment: 'إضافة تعليق', + yourName: 'اسمك (اختياري)', + yourNamePlaceholder: 'اسمك', + message: 'الرسالة', + messagePlaceholder: 'أضف معلومات إضافية أو اطرح سؤالاً...', + sendComment: 'إرسال التعليق', + sending: 'جارٍ الإرسال...', + uploadFile: 'رفع ملف', + uploading: 'جارٍ الرفع...', + bookmarkNote: 'احفظ هذه الصفحة للاطلاع على حالة مشكلتك في أي وقت.', + notFoundTitle: 'المشكلة غير موجودة', + notFoundMessage: 'رابط التتبع الذي استخدمته غير صالح أو تمت إزالة المشكلة.', + submitNew: 'تقديم مشكلة جديدة', + failedComment: 'فشل في إضافة التعليق', + failedUpload: 'فشل في رفع الملف', + priority: 'الأولوية', + new: 'جديد', acknowledged: 'تم الاستلام', in_progress: 'قيد التنفيذ', resolved: 'تم الحل', declined_status: 'مرفوض', + low: 'منخفضة', medium: 'متوسطة', high: 'عالية', urgent: 'عاجلة', + }, } -const PRIORITY_CONFIG = { - low: { label: 'Low', color: 'text-gray-700' }, - medium: { label: 'Medium', color: 'text-blue-700' }, - high: { label: 'High', color: 'text-orange-700' }, - urgent: { label: 'Urgent', color: 'text-red-700' }, +function detectLang() { + const nav = navigator.language || navigator.userLanguage || '' + return nav.startsWith('ar') ? 'ar' : 'en' +} + +function LangToggle({ lang, setLang }) { + return ( + + ) } export default function PublicIssueTracker() { const { token } = useParams() 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 [issue, setIssue] = useState(null) const [updates, setUpdates] = useState([]) const [attachments, setAttachments] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) - - // Comment form const [commentName, setCommentName] = useState('') const [commentMessage, setCommentMessage] = useState('') const [submittingComment, setSubmittingComment] = useState(false) - - // File upload const [uploadingFile, setUploadingFile] = useState(false) - useEffect(() => { - loadIssue() - }, [token]) + useEffect(() => { loadIssue() }, [token]) const loadIssue = async () => { try { - setLoading(true) - setError(null) + setLoading(true); setError(null) const data = await api.get(`/public/issues/${token}`) setIssue(data.issue) setUpdates(data.updates || []) setAttachments(data.attachments || []) } catch (err) { - console.error('Failed to load issue:', err) - setError(err.response?.status === 404 ? 'Issue not found' : 'Failed to load issue') + setError(err.response?.status === 404 ? 'notFound' : 'error') } finally { setLoading(false) } @@ -59,27 +129,18 @@ export default function PublicIssueTracker() { const handleAddComment = async (e) => { e.preventDefault() if (!commentMessage.trim() || submittingComment) return - try { setSubmittingComment(true) - await api.post(`/public/issues/${token}/comment`, { - name: commentName.trim() || 'Anonymous', - message: commentMessage, - }) + await api.post(`/public/issues/${token}/comment`, { name: commentName.trim() || 'Anonymous', message: commentMessage }) setCommentMessage('') await loadIssue() - } catch (err) { - console.error('Failed to add comment:', err) - toast.error('Failed to add comment') - } finally { - setSubmittingComment(false) - } + } catch { toast.error(t('failedComment')) } + finally { setSubmittingComment(false) } } const handleFileUpload = async (e) => { const file = e.target.files?.[0] if (!file) return - try { setUploadingFile(true) const formData = new FormData() @@ -87,52 +148,52 @@ export default function PublicIssueTracker() { formData.append('name', commentName.trim() || 'Anonymous') await api.upload(`/public/issues/${token}/attachments`, formData) await loadIssue() - e.target.value = '' // Reset input - } catch (err) { - console.error('Failed to upload file:', err) - toast.error('Failed to upload file') - } finally { - setUploadingFile(false) - } + e.target.value = '' + } catch { toast.error(t('failedUpload')) } + finally { setUploadingFile(false) } } - const formatDate = (dateStr) => { + const dateFmt = (dateStr) => { if (!dateStr) return '' - const date = new Date(dateStr) - return date.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' }) + return new Date(dateStr).toLocaleDateString(lang === 'ar' ? 'ar-SA' : 'en-US', { month: 'long', day: 'numeric', year: 'numeric' }) } - const formatDateTime = (dateStr) => { + const dateTimeFmt = (dateStr) => { if (!dateStr) return '' - const date = new Date(dateStr) - return date.toLocaleString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric', - hour: '2-digit', - minute: '2-digit', - }) + return new Date(dateStr).toLocaleString(lang === 'ar' ? 'ar-SA' : 'en-US', { month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit' }) } - const formatFileSize = (bytes) => { + const fileSize = (bytes) => { if (bytes < 1024) return bytes + ' B' if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB' return (bytes / (1024 * 1024)).toFixed(1) + ' MB' } + const STATUS_CONFIG = { + new: { label: t('new'), bg: 'bg-purple-100', text: 'text-purple-700', dot: 'bg-purple-500', icon: AlertCircle }, + acknowledged: { label: t('acknowledged'), bg: 'bg-blue-100', text: 'text-blue-700', dot: 'bg-blue-500', icon: CheckCircle2 }, + in_progress: { label: t('in_progress'), bg: 'bg-amber-100', text: 'text-amber-700', dot: 'bg-amber-500', icon: Clock }, + resolved: { label: t('resolved'), bg: 'bg-emerald-100', text: 'text-emerald-700', dot: 'bg-emerald-500', icon: CheckCircle2 }, + declined: { label: t('declined_status'), bg: 'bg-gray-100', text: 'text-gray-700', dot: 'bg-gray-500', icon: XCircle }, + } + + const PRIORITY_CONFIG = { + low: { label: t('low'), color: 'text-gray-700' }, + medium: { label: t('medium'), color: 'text-blue-700' }, + high: { label: t('high'), color: 'text-orange-700' }, + urgent: { label: t('urgent'), color: 'text-red-700' }, + } + if (loading) { return ( -
+
+
-
-
-
-
-
-
-
-
+
+
+
+
@@ -141,20 +202,16 @@ export default function PublicIssueTracker() { if (error) { return ( -
+
+
-

Issue Not Found

-

- The tracking link you used is invalid or the issue has been removed. -

- - Submit a New Issue +

{t('notFoundTitle')}

+

{t('notFoundMessage')}

+
+ {t('submitNew')}
@@ -164,22 +221,22 @@ export default function PublicIssueTracker() { if (!issue) return null const statusConfig = STATUS_CONFIG[issue.status] || STATUS_CONFIG.new - const StatusIcon = statusConfig.icon const priorityConfig = PRIORITY_CONFIG[issue.priority] || PRIORITY_CONFIG.medium return ( -
+
+
{/* Header */}
- + {statusConfig.label} - {priorityConfig.label} Priority + {priorityConfig.label} {t('priority')} {issue.type} @@ -188,18 +245,18 @@ export default function PublicIssueTracker() {
-

Description

+

{t('description')}

{issue.description}

- Submitted: - {formatDate(issue.created_at)} + {t('submitted')}: + {dateFmt(issue.created_at)}
- Last Updated: - {formatDate(issue.updated_at)} + {t('lastUpdated')}: + {dateFmt(issue.updated_at)}
@@ -208,21 +265,19 @@ export default function PublicIssueTracker() { {(issue.status === 'resolved' || issue.status === 'declined') && issue.resolution_summary && (
- {issue.status === 'resolved' ? ( - - ) : ( - - )} + {issue.status === 'resolved' + ? + : }

- {issue.status === 'resolved' ? 'Resolution' : 'Declined'} + {issue.status === 'resolved' ? t('resolution') : t('declined')}

{issue.resolution_summary}

{issue.resolved_at && (

- {formatDate(issue.resolved_at)} + {dateFmt(issue.resolved_at)}

)}
@@ -234,26 +289,25 @@ export default function PublicIssueTracker() {

- Progress Updates + {t('progressUpdates')}

- {updates.length === 0 ? (
-

No updates yet. We'll post updates here as we work on your issue.

+

{t('noUpdates')}

) : (
{updates.map((update, idx) => ( -
+
{update.author_name} - {update.author_type === 'staff' ? 'Team' : 'You'} + {update.author_type === 'staff' ? t('team') : t('you')}
- {formatDateTime(update.created_at)} + {dateTimeFmt(update.created_at)}

{update.message}

@@ -267,7 +321,7 @@ export default function PublicIssueTracker() {

- Attachments + {t('attachments')}

{attachments.map((att) => ( @@ -276,18 +330,12 @@ export default function PublicIssueTracker() {

{att.original_name}

-

- {formatFileSize(att.size)} • {att.uploaded_by} -

+

{fileSize(att.size)} • {att.uploaded_by}

- - Download + + {t('download')}
))} @@ -295,55 +343,39 @@ export default function PublicIssueTracker() {
)} - {/* Add Comment Section */} + {/* Add Comment */} {issue.status !== 'resolved' && issue.status !== 'declined' && (

- Add a Comment + {t('addComment')}

-
- - setCommentName(e.target.value)} - placeholder="Your name" - className="w-full px-4 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20" - /> + + setCommentName(e.target.value)} + placeholder={t('yourNamePlaceholder')} + className="w-full px-4 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20" />
-
-