- races table: name, date, description, is_active
- stations table: ordered checkpoints with GPS per race
- New /api/races and /api/races/{id}/stations endpoints
- Upload now requires race + station selection; uses station GPS
so images without GPS EXIF are accepted
- passages filtered by active race throughout
- RacePage: create races, manage stations (add/edit/delete checkpoints)
- Navbar shows active race name
- Start and finish stations created automatically per race
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,19 +6,20 @@ function fmtTs(ts) {
|
||||
return new Date(ts).toLocaleString('no-NO', { timeZone: 'UTC' })
|
||||
}
|
||||
|
||||
export default function PassagesPage() {
|
||||
export default function PassagesPage({ activeRace }) {
|
||||
const [passages, setPassages] = useState([])
|
||||
const [filter, setFilter] = useState({ station: '', needs_review: '' })
|
||||
|
||||
const load = async () => {
|
||||
const params = {}
|
||||
if (activeRace?.race_id) params.race_id = activeRace.race_id
|
||||
if (filter.station) params.station = filter.station
|
||||
if (filter.needs_review !== '') params.needs_review = filter.needs_review === 'true'
|
||||
const data = await api.getPassages(params)
|
||||
setPassages(data)
|
||||
}
|
||||
|
||||
useEffect(() => { load() }, [filter])
|
||||
useEffect(() => { load() }, [filter, activeRace])
|
||||
|
||||
const handleDelete = async (id) => {
|
||||
if (!confirm('Slett passering?')) return
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { api } from '../api.js'
|
||||
|
||||
function StationRow({ station, raceId, onUpdated, onDeleted }) {
|
||||
const [editing, setEditing] = useState(false)
|
||||
const [form, setForm] = useState({
|
||||
display_name: station.display_name,
|
||||
gps_lat: station.gps_lat ?? '',
|
||||
gps_lon: station.gps_lon ?? '',
|
||||
gps_alt: station.gps_alt ?? '',
|
||||
})
|
||||
|
||||
const isFixed = station.name === 'start' || station.name === 'finish'
|
||||
|
||||
const handleSave = async () => {
|
||||
await api.updateStation(raceId, station.station_id, {
|
||||
name: station.name,
|
||||
display_name: form.display_name,
|
||||
station_order: station.station_order,
|
||||
gps_lat: form.gps_lat !== '' ? Number(form.gps_lat) : null,
|
||||
gps_lon: form.gps_lon !== '' ? Number(form.gps_lon) : null,
|
||||
gps_alt: form.gps_alt !== '' ? Number(form.gps_alt) : null,
|
||||
})
|
||||
setEditing(false)
|
||||
onUpdated()
|
||||
}
|
||||
|
||||
if (editing) {
|
||||
return (
|
||||
<tr>
|
||||
<td>{station.name}</td>
|
||||
<td><input type="text" value={form.display_name} onChange={e => setForm(f => ({ ...f, display_name: e.target.value }))} style={{ width: 130 }} /></td>
|
||||
<td><input type="number" step="any" placeholder="Lat" value={form.gps_lat} onChange={e => setForm(f => ({ ...f, gps_lat: e.target.value }))} style={{ width: 100 }} /></td>
|
||||
<td><input type="number" step="any" placeholder="Lon" value={form.gps_lon} onChange={e => setForm(f => ({ ...f, gps_lon: e.target.value }))} style={{ width: 100 }} /></td>
|
||||
<td><input type="number" step="any" placeholder="Alt" value={form.gps_alt} onChange={e => setForm(f => ({ ...f, gps_alt: e.target.value }))} style={{ width: 70 }} /></td>
|
||||
<td style={{ whiteSpace: 'nowrap' }}>
|
||||
<button className="btn-success btn-sm" onClick={handleSave} style={{ marginRight: 4 }}>Lagre</button>
|
||||
<button className="btn-sm" style={{ background: '#eee' }} onClick={() => setEditing(false)}>Avbryt</button>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td><code>{station.name}</code></td>
|
||||
<td>{station.display_name}</td>
|
||||
<td>{station.gps_lat ?? '—'}</td>
|
||||
<td>{station.gps_lon ?? '—'}</td>
|
||||
<td>{station.gps_alt ?? '—'}</td>
|
||||
<td style={{ whiteSpace: 'nowrap' }}>
|
||||
<button className="btn-sm btn-primary" onClick={() => setEditing(true)} style={{ marginRight: 4 }}>Rediger</button>
|
||||
{!isFixed && (
|
||||
<button className="btn-sm btn-danger" onClick={onDeleted}>Slett</button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
function RaceCard({ race, isActive, onActivated, onDeleted, onUpdated }) {
|
||||
const [stations, setStations] = useState([])
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const [newStation, setNewStation] = useState({ display_name: '', name: '' })
|
||||
const [adding, setAdding] = useState(false)
|
||||
|
||||
const loadStations = async () => {
|
||||
const s = await api.getStations(race.race_id)
|
||||
setStations(s)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (expanded) loadStations()
|
||||
}, [expanded])
|
||||
|
||||
const handleAddStation = async () => {
|
||||
if (!newStation.name || !newStation.display_name) return
|
||||
const maxOrder = stations.reduce((m, s) => Math.max(m, s.station_order), 0)
|
||||
await api.createStation(race.race_id, {
|
||||
name: newStation.name,
|
||||
display_name: newStation.display_name,
|
||||
station_order: maxOrder + 100,
|
||||
})
|
||||
setNewStation({ display_name: '', name: '' })
|
||||
setAdding(false)
|
||||
loadStations()
|
||||
}
|
||||
|
||||
const handleDeleteStation = async (stationId) => {
|
||||
await api.deleteStation(race.race_id, stationId)
|
||||
loadStations()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="card" style={{ borderLeft: isActive ? '4px solid #2ecc71' : '4px solid transparent' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div>
|
||||
<strong style={{ fontSize: '1.05rem' }}>{race.name}</strong>
|
||||
{race.date && <span style={{ marginLeft: 8, color: '#888', fontSize: '0.85rem' }}>{race.date}</span>}
|
||||
{isActive && <span className="badge badge-success" style={{ marginLeft: 8 }}>Aktivt</span>}
|
||||
{race.description && <p style={{ fontSize: '0.85rem', color: '#666', marginTop: 4 }}>{race.description}</p>}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 6 }}>
|
||||
{!isActive && (
|
||||
<button className="btn-success btn-sm" onClick={onActivated}>Velg</button>
|
||||
)}
|
||||
<button className="btn-sm btn-primary" onClick={() => setExpanded(e => !e)}>
|
||||
Stasjoner {expanded ? '▲' : '▼'}
|
||||
</button>
|
||||
<button className="btn-danger btn-sm" onClick={onDeleted}>Slett</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{expanded && (
|
||||
<div style={{ marginTop: '1rem' }}>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Navn</th>
|
||||
<th>Lat</th>
|
||||
<th>Lon</th>
|
||||
<th>Alt (m)</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{stations.map(s => (
|
||||
<StationRow
|
||||
key={s.station_id}
|
||||
station={s}
|
||||
raceId={race.race_id}
|
||||
onUpdated={loadStations}
|
||||
onDeleted={() => handleDeleteStation(s.station_id)}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{adding ? (
|
||||
<div className="form-row" style={{ marginTop: '0.75rem' }}>
|
||||
<div className="form-group">
|
||||
<label>ID (f.eks. cp1)</label>
|
||||
<input type="text" style={{ width: 100 }} value={newStation.name}
|
||||
onChange={e => setNewStation(s => ({ ...s, name: e.target.value }))} />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Visningsnavn</label>
|
||||
<input type="text" style={{ width: 150 }} value={newStation.display_name}
|
||||
onChange={e => setNewStation(s => ({ ...s, display_name: e.target.value }))} />
|
||||
</div>
|
||||
<button className="btn-success btn-sm" onClick={handleAddStation}>Legg til</button>
|
||||
<button className="btn-sm" style={{ background: '#eee' }} onClick={() => setAdding(false)}>Avbryt</button>
|
||||
</div>
|
||||
) : (
|
||||
<button className="btn-sm btn-primary" style={{ marginTop: '0.75rem' }} onClick={() => setAdding(true)}>
|
||||
+ Ny mellomtid
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function RacePage() {
|
||||
const [races, setRaces] = useState([])
|
||||
const [activeId, setActiveId] = useState(null)
|
||||
const [form, setForm] = useState({ name: '', date: '', description: '' })
|
||||
const [creating, setCreating] = useState(false)
|
||||
|
||||
const load = async () => {
|
||||
const [r, a] = await Promise.all([api.getRaces(), api.getActiveRace()])
|
||||
setRaces(r)
|
||||
setActiveId(a?.race_id || null)
|
||||
}
|
||||
|
||||
useEffect(() => { load() }, [])
|
||||
|
||||
const handleCreate = async (e) => {
|
||||
e.preventDefault()
|
||||
await api.createRace({ name: form.name, date: form.date || null, description: form.description || null })
|
||||
setForm({ name: '', date: '', description: '' })
|
||||
setCreating(false)
|
||||
load()
|
||||
}
|
||||
|
||||
const handleActivate = async (id) => {
|
||||
await api.activateRace(id)
|
||||
setActiveId(id)
|
||||
}
|
||||
|
||||
const handleDelete = async (id) => {
|
||||
if (!confirm('Slette løpet og alle tilknyttede stasjoner?')) return
|
||||
await api.deleteRace(id)
|
||||
load()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1>Løp</h1>
|
||||
|
||||
<div className="card">
|
||||
{creating ? (
|
||||
<form onSubmit={handleCreate}>
|
||||
<h2>Nytt løp</h2>
|
||||
<div className="form-row">
|
||||
<div className="form-group" style={{ flex: 2 }}>
|
||||
<label>Navn *</label>
|
||||
<input type="text" required value={form.name} onChange={e => setForm(f => ({ ...f, name: e.target.value }))} placeholder="f.eks. Skogsløpet 2026" />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Dato</label>
|
||||
<input type="date" value={form.date} onChange={e => setForm(f => ({ ...f, date: e.target.value }))} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group" style={{ marginBottom: '0.75rem' }}>
|
||||
<label>Beskrivelse</label>
|
||||
<input type="text" value={form.description} onChange={e => setForm(f => ({ ...f, description: e.target.value }))} placeholder="Valgfri beskrivelse" />
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<button type="submit" className="btn-primary">Opprett</button>
|
||||
<button type="button" className="btn-sm" style={{ background: '#eee' }} onClick={() => setCreating(false)}>Avbryt</button>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<button className="btn-primary" onClick={() => setCreating(true)}>+ Nytt løp</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{races.length === 0 ? (
|
||||
<div className="card"><p className="empty-state">Ingen løp registrert ennå.</p></div>
|
||||
) : (
|
||||
races.map(r => (
|
||||
<RaceCard
|
||||
key={r.race_id}
|
||||
race={r}
|
||||
isActive={r.race_id === activeId}
|
||||
onActivated={() => handleActivate(r.race_id)}
|
||||
onDeleted={() => handleDelete(r.race_id)}
|
||||
onUpdated={load}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -6,17 +6,17 @@ function fmtTs(ts) {
|
||||
return new Date(ts).toLocaleTimeString('no-NO', { timeZone: 'UTC', hour12: false })
|
||||
}
|
||||
|
||||
export default function ResultsPage() {
|
||||
export default function ResultsPage({ activeRace }) {
|
||||
const [results, setResults] = useState([])
|
||||
const [expanded, setExpanded] = useState(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
api.getResults().then(data => {
|
||||
api.getResults(activeRace?.race_id).then(data => {
|
||||
setResults(data)
|
||||
setLoading(false)
|
||||
})
|
||||
}, [])
|
||||
}, [activeRace])
|
||||
|
||||
const toggleExpand = (id) => setExpanded(e => e === id ? null : id)
|
||||
|
||||
|
||||
@@ -149,14 +149,17 @@ function ReviewCard({ passage, athletes, onResolved, onDeleted }) {
|
||||
)
|
||||
}
|
||||
|
||||
export default function ReviewPage() {
|
||||
export default function ReviewPage({ activeRace }) {
|
||||
const [queue, setQueue] = useState([])
|
||||
const [athletes, setAthletes] = useState([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
const load = async () => {
|
||||
setLoading(true)
|
||||
const [q, a] = await Promise.all([api.getReviewQueue(), api.getAthletes()])
|
||||
const [q, a] = await Promise.all([
|
||||
api.getReviewQueue(activeRace?.race_id),
|
||||
api.getAthletes(),
|
||||
])
|
||||
setQueue(q)
|
||||
setAthletes(a)
|
||||
setLoading(false)
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import { useRef, useState } from 'react'
|
||||
|
||||
const ACCEPTED = '.jpg,.jpeg,.png'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { api } from '../api.js'
|
||||
|
||||
function FileRow({ file, result }) {
|
||||
const status = result
|
||||
? result.ok
|
||||
? <span className="badge badge-success">OK — {result.saved_as}</span>
|
||||
? <span className="badge badge-success">OK</span>
|
||||
: <span className="badge badge-danger">Feil: {result.error}</span>
|
||||
: <span className="badge badge-warning">Venter...</span>
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td>{file.name}</td>
|
||||
@@ -19,6 +17,10 @@ function FileRow({ file, result }) {
|
||||
}
|
||||
|
||||
export default function UploadPage() {
|
||||
const [races, setRaces] = useState([])
|
||||
const [stations, setStations] = useState([])
|
||||
const [selectedRace, setSelectedRace] = useState('')
|
||||
const [selectedStation, setSelectedStation] = useState('')
|
||||
const [files, setFiles] = useState([])
|
||||
const [results, setResults] = useState({})
|
||||
const [uploading, setUploading] = useState(false)
|
||||
@@ -26,6 +28,24 @@ export default function UploadPage() {
|
||||
const inputRef = useRef()
|
||||
const dropRef = useRef()
|
||||
|
||||
useEffect(() => {
|
||||
api.getRaces().then(r => {
|
||||
setRaces(r)
|
||||
const active = r.find(x => x.is_active)
|
||||
if (active) setSelectedRace(active.race_id)
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedRace) { setStations([]); return }
|
||||
api.getStations(selectedRace).then(s => {
|
||||
setStations(s)
|
||||
// Velg start som default
|
||||
const start = s.find(x => x.name === 'start')
|
||||
setSelectedStation(start?.station_id || s[0]?.station_id || '')
|
||||
})
|
||||
}, [selectedRace])
|
||||
|
||||
const addFiles = (newFiles) => {
|
||||
const valid = Array.from(newFiles).filter(f =>
|
||||
f.type === 'image/jpeg' || f.type === 'image/png'
|
||||
@@ -48,21 +68,18 @@ export default function UploadPage() {
|
||||
setResults({})
|
||||
setSummary(null)
|
||||
|
||||
// Send i bolker på 10 for å unngå for store requests
|
||||
const BATCH = 10
|
||||
const allResults = {}
|
||||
|
||||
for (let i = 0; i < files.length; i += BATCH) {
|
||||
const batch = files.slice(i, i + BATCH)
|
||||
const form = new FormData()
|
||||
batch.forEach(f => form.append('files', f))
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/upload', { method: 'POST', body: form })
|
||||
const data = await res.json()
|
||||
data.results.forEach((r, j) => {
|
||||
allResults[batch[j].name] = r
|
||||
})
|
||||
const data = await api.uploadImages(
|
||||
batch,
|
||||
selectedRace || null,
|
||||
selectedStation || null,
|
||||
)
|
||||
data.results.forEach((r, j) => { allResults[batch[j].name] = r })
|
||||
setResults({ ...allResults })
|
||||
} catch (err) {
|
||||
batch.forEach(f => { allResults[f.name] = { ok: false, error: err.message } })
|
||||
@@ -82,17 +99,45 @@ export default function UploadPage() {
|
||||
inputRef.current.value = ''
|
||||
}
|
||||
|
||||
const selectedStationObj = stations.find(s => s.station_id === selectedStation)
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1>Last opp bilder</h1>
|
||||
|
||||
<div className="card">
|
||||
<h2>Velg bilder</h2>
|
||||
<p style={{ fontSize: '0.85rem', color: '#666', marginBottom: '0.75rem' }}>
|
||||
JPEG og PNG støttes. Bildene legges i depot og behandles automatisk (EXIF valideres, OCR kjøres).
|
||||
</p>
|
||||
<h2>Løp og stasjon</h2>
|
||||
<div className="form-row">
|
||||
<div className="form-group" style={{ flex: 1 }}>
|
||||
<label>Løp</label>
|
||||
<select value={selectedRace} onChange={e => setSelectedRace(e.target.value)}>
|
||||
<option value="">— Velg løp —</option>
|
||||
{races.map(r => (
|
||||
<option key={r.race_id} value={r.race_id}>
|
||||
{r.name}{r.date ? ` (${r.date})` : ''}{r.is_active ? ' ★' : ''}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-group" style={{ flex: 1 }}>
|
||||
<label>Stasjon</label>
|
||||
<select value={selectedStation} onChange={e => setSelectedStation(e.target.value)} disabled={!selectedRace}>
|
||||
<option value="">— Velg stasjon —</option>
|
||||
{stations.map(s => (
|
||||
<option key={s.station_id} value={s.station_id}>{s.display_name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{selectedStationObj && (selectedStationObj.gps_lat == null) && (
|
||||
<div className="alert alert-info" style={{ marginTop: 0 }}>
|
||||
Ingen GPS satt for denne stasjonen — du kan sette det under Løp → Stasjoner.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Dra-og-slipp-sone */}
|
||||
<div className="card">
|
||||
<h2>Velg bilder</h2>
|
||||
<div
|
||||
ref={dropRef}
|
||||
onDrop={handleDrop}
|
||||
@@ -100,47 +145,40 @@ export default function UploadPage() {
|
||||
onDragLeave={() => dropRef.current.classList.remove('drag-over')}
|
||||
onClick={() => inputRef.current.click()}
|
||||
style={{
|
||||
border: '2px dashed #ddd',
|
||||
borderRadius: 8,
|
||||
padding: '2rem',
|
||||
textAlign: 'center',
|
||||
cursor: 'pointer',
|
||||
color: '#aaa',
|
||||
marginBottom: '0.75rem',
|
||||
transition: 'border-color 0.15s, background 0.15s',
|
||||
border: '2px dashed #ddd', borderRadius: 8, padding: '2rem',
|
||||
textAlign: 'center', cursor: 'pointer', color: '#aaa',
|
||||
marginBottom: '0.75rem', transition: 'border-color 0.15s, background 0.15s',
|
||||
}}
|
||||
>
|
||||
Dra og slipp bilder hit, eller klikk for å velge
|
||||
</div>
|
||||
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="file"
|
||||
accept={ACCEPTED}
|
||||
multiple
|
||||
style={{ display: 'none' }}
|
||||
onChange={e => addFiles(e.target.files)}
|
||||
/>
|
||||
<input ref={inputRef} type="file" accept=".jpg,.jpeg,.png" multiple
|
||||
style={{ display: 'none' }} onChange={e => addFiles(e.target.files)} />
|
||||
|
||||
{files.length > 0 && (
|
||||
<div className="form-row" style={{ marginTop: '0.5rem' }}>
|
||||
<button
|
||||
className="btn-primary"
|
||||
onClick={handleUpload}
|
||||
disabled={uploading}
|
||||
>
|
||||
{uploading ? `Laster opp... (${Object.keys(results).length}/${files.length})` : `Last opp ${files.length} bilde(r)`}
|
||||
<button className="btn-primary" onClick={handleUpload}
|
||||
disabled={uploading || !selectedRace || !selectedStation}>
|
||||
{uploading
|
||||
? `Laster opp... (${Object.keys(results).length}/${files.length})`
|
||||
: `Last opp ${files.length} bilde(r)`}
|
||||
</button>
|
||||
<button className="btn-danger btn-sm" onClick={handleClear} disabled={uploading}>
|
||||
Nullstill
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{files.length > 0 && (!selectedRace || !selectedStation) && (
|
||||
<p style={{ color: '#e67e22', fontSize: '0.85rem', marginTop: 4 }}>
|
||||
Velg løp og stasjon for å laste opp.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{summary && (
|
||||
<div className={`alert alert-${summary.ok === summary.total ? 'success' : 'info'}`}>
|
||||
{summary.ok} av {summary.total} bilder lastet opp. Behandles nå i bakgrunnen.
|
||||
{summary.ok} av {summary.total} bilder lastet opp og behandles nå.
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -148,28 +186,13 @@ export default function UploadPage() {
|
||||
<div className="card">
|
||||
<h2>Filer ({files.length})</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Filnavn</th>
|
||||
<th>Størrelse</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{files.map(f => (
|
||||
<FileRow key={f.name} file={f} result={results[f.name]} />
|
||||
))}
|
||||
</tbody>
|
||||
<thead><tr><th>Filnavn</th><th>Størrelse</th><th>Status</th></tr></thead>
|
||||
<tbody>{files.map(f => <FileRow key={f.name} file={f} result={results[f.name]} />)}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<style>{`
|
||||
.drag-over {
|
||||
border-color: #1a1a2e !important;
|
||||
background: #f0f0f8;
|
||||
}
|
||||
`}</style>
|
||||
<style>{`.drag-over { border-color: #1a1a2e !important; background: #f0f0f8; }`}</style>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user