edtech / apps /admin /src /pages /TrackDaysPage.tsx
CognxSafeTrack
feat(i18n): complete admin app internationalization across all pages
d80fec4
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams, useNavigate } from 'react-router-dom';
import { Plus, Edit2, Trash2, ArrowLeft, X, Save } from 'lucide-react';
import { useAuth } from '../lib/auth';
import { useTenant } from '../lib/tenant';
import { api } from '../lib/api';
import { logError } from '../lib/logger';
export default function TrackDaysPage() {
const { t } = useTranslation();
const { token } = useAuth();
const { selectedOrgId } = useTenant();
const { trackId } = useParams<{ trackId: string }>();
const navigate = useNavigate();
const [days, setDays] = useState<any[]>([]);
const [track, setTrack] = useState<any>(null);
const [editing, setEditing] = useState<any>(null);
const [saving, setSaving] = useState(false);
const load = async (): Promise<void> => {
if (!token || !selectedOrgId) return;
try {
const [trackData, daysData] = await Promise.all([
api.get(`/v1/admin/tracks/${trackId}`, token, selectedOrgId),
api.get(`/v1/admin/tracks/${trackId}/days`, token, selectedOrgId)
]);
setTrack(trackData);
setDays(daysData);
} catch (err) {
logError(err);
}
};
useEffect(() => { load(); }, [token, selectedOrgId]);
const emptyDay = { dayNumber: (days.length || 0) + 1, title: '', lessonText: '', audioUrl: '', exerciseType: 'TEXT', exercisePrompt: '', validationKeyword: '' };
const saveDay = async (e: React.FormEvent) => {
e.preventDefault();
if (!token || !selectedOrgId) return;
setSaving(true);
try {
if (editing.id) {
await api.put(`/v1/admin/tracks/${trackId}/days/${editing.id}`, editing, token, selectedOrgId);
} else {
await api.post(`/v1/admin/tracks/${trackId}/days`, editing, token, selectedOrgId);
}
await load();
setEditing(null);
} catch (err) {
logError(err);
} finally {
setSaving(false);
}
};
const del = async (dayId: string) => {
if (!confirm(t('tracks.confirm_delete'))) return;
try {
await api.delete(`/v1/admin/tracks/${trackId}/days/${dayId}`, token, selectedOrgId);
load();
} catch (err) {
logError(err);
}
};
const inp = "w-full border border-slate-200 rounded-xl px-4 py-2.5 text-sm outline-none focus:ring-2 focus:ring-slate-300";
return (
<div className="p-8">
<div className="flex items-center gap-3 mb-6">
<button onClick={() => navigate('/content')} className="p-2 hover:bg-slate-100 rounded-lg"><ArrowLeft className="w-4 h-4" /></button>
<div><h1 className="text-2xl font-bold text-slate-800">{track?.title}</h1>
<p className="text-sm text-slate-500">{days.length} {t('tracks.days')}</p></div>
<button onClick={() => setEditing(emptyDay)} className="ml-auto flex items-center gap-2 bg-slate-900 text-white px-4 py-2 rounded-xl text-sm font-bold hover:bg-slate-700">
<Plus className="w-4 h-4" /> {t('tracks.new')}
</button>
</div>
{editing && (
<div className="fixed inset-0 bg-black/40 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-lg max-h-[90vh] overflow-y-auto">
<div className="flex items-center justify-between p-5 border-b">
<h2 className="font-bold text-slate-800">{editing.id ? `${t('tracks.edit_day')} ${editing.dayNumber}` : t('tracks.new_day')}</h2>
<button onClick={() => setEditing(null)}><X className="w-5 h-5 text-slate-400" /></button>
</div>
<form onSubmit={saveDay} className="p-5 space-y-4">
<div className="grid grid-cols-2 gap-3">
<div><label className="text-xs font-medium text-slate-600 mb-1 block">{t('tracks.day_number')}</label>
<input type="number" min={1} required className={inp} value={editing.dayNumber} onChange={e => setEditing((d: any) => ({ ...d, dayNumber: parseInt(e.target.value) }))} /></div>
<div><label className="text-xs font-medium text-slate-600 mb-1 block">{t('tracks.day_title')}</label>
<input className={inp} value={editing.title || ''} onChange={e => setEditing((d: any) => ({ ...d, title: e.target.value }))} /></div>
</div>
<div><label className="text-xs font-medium text-slate-600 mb-1 block">{t('tracks.lesson_text')}</label>
<textarea className={inp} rows={5} value={editing.lessonText || ''} onChange={e => setEditing((d: any) => ({ ...d, lessonText: e.target.value }))} placeholder={t('tracks.lesson_placeholder')} /></div>
<div><label className="text-xs font-medium text-slate-600 mb-1 block">{t('tracks.audio_url')}</label>
<input className={inp} value={editing.audioUrl || ''} onChange={e => setEditing((d: any) => ({ ...d, audioUrl: e.target.value }))} placeholder="https://..." /></div>
<div className="grid grid-cols-2 gap-3">
<div><label className="text-xs font-medium text-slate-600 mb-1 block">{t('tracks.exercise_type')}</label>
<select className={inp} value={editing.exerciseType} onChange={e => setEditing((d: any) => ({ ...d, exerciseType: e.target.value }))}>
<option value="TEXT">{t('tracks.exercise_type_text')}</option><option value="AUDIO">{t('tracks.exercise_type_audio')}</option><option value="BUTTON">{t('tracks.exercise_type_button')}</option>
</select></div>
<div><label className="text-xs font-medium text-slate-600 mb-1 block">{t('tracks.validation_keyword')}</label>
<input className={inp} value={editing.validationKeyword || ''} onChange={e => setEditing((d: any) => ({ ...d, validationKeyword: e.target.value }))} /></div>
</div>
<div><label className="text-xs font-medium text-slate-600 mb-1 block">{t('tracks.exercise_prompt')}</label>
<textarea className={inp} rows={2} value={editing.exercisePrompt || ''} onChange={e => setEditing((d: any) => ({ ...d, exercisePrompt: e.target.value }))} placeholder={t('tracks.exercise_prompt_placeholder')} /></div>
<div className="flex gap-3">
<button type="button" onClick={() => setEditing(null)} className="flex-1 border border-slate-200 text-slate-600 py-2.5 rounded-xl text-sm hover:bg-slate-50">{t('common.cancel')}</button>
<button type="submit" disabled={saving} className="flex-1 bg-slate-900 text-white py-2.5 rounded-xl text-sm font-bold flex items-center justify-center gap-2 hover:bg-slate-700 disabled:opacity-50">
<Save className="w-4 h-4" />{saving ? t('common.loading') : t('common.save')}
</button>
</div>
</form>
</div>
</div>
)}
<div className="grid gap-3">
{days.map((d: any) => (
<div key={d.id} className="bg-white rounded-xl border border-slate-100 p-4 flex items-start justify-between shadow-sm">
<div className="flex gap-4">
<div className="bg-slate-900 text-white w-9 h-9 rounded-lg flex items-center justify-center text-sm font-bold shrink-0">{d.dayNumber}</div>
<div>
<p className="font-medium text-slate-800">{d.title || `${t('common.day')} ${d.dayNumber}`}</p>
<p className="text-xs text-slate-500 mt-0.5 line-clamp-1">{d.lessonText?.substring(0, 100) || t('tracks.no_lesson_text')}</p>
<div className="flex gap-2 mt-1.5">
<span className="text-xs bg-slate-100 text-slate-600 px-2 py-0.5 rounded-full">{d.exerciseType}</span>
{d.audioUrl && <span className="text-xs bg-green-100 text-green-700 px-2 py-0.5 rounded-full">🎵 Audio</span>}
{d.exercisePrompt && <span className="text-xs bg-blue-100 text-blue-700 px-2 py-0.5 rounded-full">🎯 Exercice</span>}
</div>
</div>
</div>
<div className="flex gap-1 shrink-0 ml-4">
<button onClick={() => setEditing(d)} className="p-2 text-slate-400 hover:text-blue-600 hover:bg-blue-50 rounded-lg"><Edit2 className="w-4 h-4" /></button>
<button onClick={() => del(d.id)} className="p-2 text-slate-400 hover:text-red-500 hover:bg-red-50 rounded-lg"><Trash2 className="w-4 h-4" /></button>
</div>
</div>
))}
</div>
</div>
);
}