| 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> |
| ); |
| } |
|
|