Download apps/admin/src/pages/TrackFormPage.tsx from safetrack/edtech: direct link, hf CLI and curl.
- Browser
- Download file 5.63 kB
-
https://huggingface.co/spaces/safetrack/edtech/resolve/main/apps/admin/src/pages/TrackFormPage.tsx
- Command line
-
hf download hf://spaces/safetrack/edtech/apps/admin/src/pages/TrackFormPage.tsx
-
curl -L -o TrackFormPage.tsx https://huggingface.co/spaces/safetrack/edtech/resolve/main/apps/admin/src/pages/TrackFormPage.tsx
5.63 kB
| import React, { useEffect, useState } from 'react'; | |
| import { useTranslation } from 'react-i18next'; | |
| import { useParams, useNavigate } from 'react-router-dom'; | |
| import { ArrowLeft, 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 TrackFormPage() { | |
| const { t } = useTranslation(); | |
| const { token } = useAuth(); | |
| const { selectedOrgId } = useTenant(); | |
| const { id } = useParams<{ id: string }>(); | |
| const navigate = useNavigate(); | |
| const isNew = id === 'new'; | |
| const [form, setForm] = useState({ | |
| title: '', description: '', duration: 7, language: 'FR', | |
| isPremium: false, priceAmount: 0 | |
| }); | |
| const [saving, setSaving] = useState(false); | |
| useEffect(() => { | |
| if (!isNew && token && selectedOrgId) { | |
| api.get(`/v1/admin/tracks/${id}`, token, selectedOrgId) | |
| .then(t => setForm({ | |
| title: t.title, description: t.description || '', duration: t.duration, | |
| language: t.language, isPremium: t.isPremium, priceAmount: t.priceAmount || 0 | |
| })); | |
| } | |
| }, [id, token, selectedOrgId, isNew]); | |
| 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"; | |
| const handleSubmit = async (e: React.FormEvent) => { | |
| e.preventDefault(); | |
| if (!token || !selectedOrgId) return; | |
| setSaving(true); | |
| const payload = { | |
| ...form, | |
| priceAmount: form.priceAmount || undefined | |
| }; | |
| try { | |
| if (isNew) { | |
| await api.post('/v1/admin/tracks', payload, token, selectedOrgId); | |
| } else { | |
| await api.put(`/v1/admin/tracks/${id}`, payload, token, selectedOrgId); | |
| } | |
| navigate('/content'); | |
| } catch (err) { | |
| logError(err); | |
| } finally { | |
| setSaving(false); | |
| } | |
| }; | |
| return ( | |
| <div className="p-8 max-w-xl"> | |
| <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> | |
| <h1 className="text-2xl font-bold text-slate-800">{isNew ? t('tracks.new') : t('common.edit')}</h1> | |
| </div> | |
| <form onSubmit={handleSubmit} className="bg-white rounded-2xl border border-slate-100 p-6 space-y-4 shadow-sm"> | |
| <div><label className="text-sm font-medium text-slate-700 mb-1 block">{t('tracks.form_title_label')} *</label> | |
| <input required className={inp} value={form.title} onChange={e => setForm(f => ({ ...f, title: e.target.value }))} /></div> | |
| <div><label className="text-sm font-medium text-slate-700 mb-1 block">{t('tracks.form_description')}</label> | |
| <textarea className={inp} rows={3} value={form.description} onChange={e => setForm(f => ({ ...f, description: e.target.value }))} /></div> | |
| <div className="grid grid-cols-2 gap-4"> | |
| <div><label className="text-sm font-medium text-slate-700 mb-1 block">{t('tracks.form_duration')}</label> | |
| <input type="number" min={1} required className={inp} value={form.duration} onChange={e => setForm(f => ({ ...f, duration: parseInt(e.target.value) }))} /></div> | |
| <div><label className="text-sm font-medium text-slate-700 mb-1 block">{t('tracks.form_language')}</label> | |
| <select className={inp} value={form.language} onChange={e => setForm(f => ({ ...f, language: e.target.value }))}> | |
| <option value="FR">{t('tracks.form_lang_fr')}</option><option value="WOLOF">{t('tracks.form_lang_wolof')}</option> | |
| </select></div> | |
| </div> | |
| <label className="flex items-center gap-3 p-3 bg-amber-50 rounded-xl cursor-pointer"> | |
| <input type="checkbox" checked={form.isPremium} onChange={e => setForm(f => ({ ...f, isPremium: e.target.checked }))} className="w-4 h-4" /> | |
| <span className="text-sm font-medium text-amber-800">{t('tracks.form_premium')}</span> | |
| </label> | |
| {form.isPremium && <div> | |
| <label className="text-sm font-medium text-slate-700 mb-1 block">{t('tracks.form_price')}</label> | |
| <input type="number" className={inp} value={form.priceAmount} onChange={e => setForm(f => ({ ...f, priceAmount: parseInt(e.target.value) }))} /> | |
| </div>} | |
| <div className="flex gap-3 pt-2"> | |
| <button type="button" onClick={() => navigate('/content')} 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 || (isNew && !selectedOrgId)} | |
| className="flex-[2] 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') : (!selectedOrgId && isNew ? t('common.select_org') : t('common.save'))} | |
| </button> | |
| </div> | |
| </form> | |
| </div> | |
| ); | |
| } | |