Some checks failed
Deploy Development / deploy (push) Successful in 40s
Test Suite / pytest-backend (push) Failing after 41s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 11s
Test Suite / playwright-tests (push) Successful in 56s
- InboxPage: Workflow-Balken (Eingegangen > In Bearbeitung > Abgeschlossen) - InboxPage: Meldungen können nach Abschluss wieder geöffnet werden (PATCH status=submitted) - InboxPage: Bearbeitungskommentar separat speicherbar; Reviewer + Datum sichtbar - InboxPage: Fehler beim Laden von Meldungen wird angezeigt statt leerem Bereich - OrgInboxContext: contentReportsError State exposed - ReportContentModal: onSuccess Callback -> Badge in Medienbibliothek sofort aktuell - content_reports PATCH: Reviewer-Felder werden beim Wieder-öffnen zurückgesetzt - content_reports PATCH: Kommentar-Änderungen ohne Statuswechsel werden im Audit-Log protokolliert version: 0.8.92 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
201 lines
7.6 KiB
JavaScript
201 lines
7.6 KiB
JavaScript
/**
|
||
* Wiederverwendbares Melde-Modal (P-13).
|
||
* Props:
|
||
* targetType – 'media_asset' | 'exercise'
|
||
* targetId – ID des Zielobjekts
|
||
* targetLabel – Anzeigename für den Header (z.B. Dateiname oder Übungstitel)
|
||
* onClose – Callback zum Schließen
|
||
*/
|
||
import React, { useState } from 'react'
|
||
import { useAuth } from '../context/AuthContext'
|
||
import api from '../utils/api'
|
||
|
||
const REASON_OPTIONS = [
|
||
{ value: '', label: '— Grund auswählen —' },
|
||
{ value: 'copyright', label: 'Urheberrechtsverletzung' },
|
||
{ value: 'image_rights', label: 'Bildrechtsverletzung / Recht am eigenen Bild' },
|
||
{ value: 'privacy', label: 'Datenschutz / Persönlichkeitsrecht' },
|
||
{ value: 'minors', label: 'Darstellung Minderjähriger' },
|
||
{ value: 'illegal_content', label: 'Rechtswidriger Inhalt' },
|
||
{ value: 'youth_protection', label: 'Jugendschutz' },
|
||
{ value: 'offensive_content', label: 'Beleidigender / anstößiger Inhalt' },
|
||
{ value: 'other', label: 'Sonstiges' },
|
||
]
|
||
|
||
export default function ReportContentModal({ targetType, targetId, targetLabel, onClose, onSuccess }) {
|
||
const { user } = useAuth()
|
||
|
||
const [reason, setReason] = useState('')
|
||
const [description, setDescription] = useState('')
|
||
const [name, setName] = useState(user?.name || '')
|
||
const [email, setEmail] = useState(user?.email || '')
|
||
const [confirmed, setConfirmed] = useState(false)
|
||
const [saving, setSaving] = useState(false)
|
||
const [error, setError] = useState(null)
|
||
const [success, setSuccess] = useState(false)
|
||
|
||
async function handleSubmit(e) {
|
||
e.preventDefault()
|
||
if (!reason) { setError('Bitte einen Meldegrund auswählen.'); return }
|
||
if (description.trim().length < 10) { setError('Beschreibung muss mindestens 10 Zeichen haben.'); return }
|
||
if (!name.trim()) { setError('Bitte deinen Namen eingeben.'); return }
|
||
if (!email.trim()) { setError('Bitte deine E-Mail-Adresse eingeben.'); return }
|
||
if (!confirmed) { setError('Bitte bestätige die Gutglaubenserklärung.'); return }
|
||
|
||
setSaving(true)
|
||
setError(null)
|
||
try {
|
||
await api.submitContentReport({
|
||
target_type: targetType,
|
||
target_id: targetId,
|
||
report_reason: reason,
|
||
report_description: description.trim(),
|
||
reporter_name: name.trim(),
|
||
reporter_email: email.trim(),
|
||
good_faith_confirmed: true,
|
||
})
|
||
setSuccess(true)
|
||
if (onSuccess) onSuccess()
|
||
} catch (err) {
|
||
setError(err.message || String(err))
|
||
} finally {
|
||
setSaving(false)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div
|
||
style={{
|
||
position: 'fixed',
|
||
inset: 0,
|
||
background: 'rgba(0,0,0,0.55)',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
zIndex: 1200,
|
||
padding: '1rem',
|
||
overflowY: 'auto',
|
||
}}
|
||
onClick={(e) => { if (e.target === e.currentTarget) onClose() }}
|
||
>
|
||
<div
|
||
style={{
|
||
background: 'var(--surface)',
|
||
borderRadius: '12px',
|
||
padding: '1.5rem',
|
||
maxWidth: '480px',
|
||
width: '100%',
|
||
marginTop: '2rem',
|
||
marginBottom: '2rem',
|
||
}}
|
||
>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
|
||
<h2 style={{ margin: 0, fontSize: '1.1rem' }}>Inhalt melden</h2>
|
||
<button type="button" className="btn btn-secondary" style={{ padding: '4px 10px' }} onClick={onClose}>✕</button>
|
||
</div>
|
||
|
||
{targetLabel && (
|
||
<p className="muted" style={{ fontSize: '0.85rem', marginBottom: '1rem', marginTop: 0 }}>
|
||
{targetType === 'media_asset' ? 'Medium' : 'Übung'}: <strong>{targetLabel}</strong>
|
||
</p>
|
||
)}
|
||
|
||
{success ? (
|
||
<div>
|
||
<p style={{ color: 'var(--accent)', fontWeight: 600 }}>Meldung eingegangen.</p>
|
||
<p className="muted" style={{ fontSize: '0.9rem' }}>
|
||
Deine Meldung wurde gespeichert und wird von unseren Moderatoren geprüft. Vielen Dank.
|
||
</p>
|
||
<button type="button" className="btn btn-primary btn-full" onClick={onClose}>
|
||
Schließen
|
||
</button>
|
||
</div>
|
||
) : (
|
||
<form onSubmit={handleSubmit}>
|
||
<div className="form-row">
|
||
<label className="form-label">Meldegrund *</label>
|
||
<select
|
||
className="form-input"
|
||
value={reason}
|
||
onChange={(e) => setReason(e.target.value)}
|
||
required
|
||
>
|
||
{REASON_OPTIONS.map((o) => (
|
||
<option key={o.value} value={o.value}>{o.label}</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
|
||
<div className="form-row">
|
||
<label className="form-label">Beschreibung * (mind. 10 Zeichen)</label>
|
||
<textarea
|
||
className="form-input"
|
||
rows={3}
|
||
value={description}
|
||
onChange={(e) => setDescription(e.target.value)}
|
||
placeholder="Bitte erläutere kurz, warum du diesen Inhalt meldest."
|
||
required
|
||
/>
|
||
</div>
|
||
|
||
<div className="form-row">
|
||
<label className="form-label">Dein Name *</label>
|
||
<input
|
||
type="text"
|
||
className="form-input"
|
||
value={name}
|
||
onChange={user?.name ? undefined : (e) => setName(e.target.value)}
|
||
readOnly={!!user?.name}
|
||
style={user?.name ? { background: 'var(--surface2)', color: 'var(--text2)' } : undefined}
|
||
required
|
||
/>
|
||
</div>
|
||
|
||
<div className="form-row">
|
||
<label className="form-label">Deine E-Mail-Adresse *</label>
|
||
<input
|
||
type="email"
|
||
className="form-input"
|
||
value={email}
|
||
onChange={user?.email ? undefined : (e) => setEmail(e.target.value)}
|
||
readOnly={!!user?.email}
|
||
style={user?.email ? { background: 'var(--surface2)', color: 'var(--text2)' } : undefined}
|
||
required
|
||
/>
|
||
</div>
|
||
|
||
<div className="form-row">
|
||
<label style={{ display: 'flex', gap: '0.6rem', alignItems: 'flex-start', cursor: 'pointer' }}>
|
||
<input
|
||
type="checkbox"
|
||
checked={confirmed}
|
||
onChange={(e) => setConfirmed(e.target.checked)}
|
||
style={{ marginTop: '3px', flexShrink: 0 }}
|
||
/>
|
||
<span style={{ fontSize: '0.85rem', color: 'var(--text2)' }}>
|
||
Ich melde diesen Inhalt nach bestem Wissen und Gewissen. Ich bestätige, dass meine Meldung
|
||
nicht missbräuchlich ist und ich der Überzeugung bin, dass der gemeldete Inhalt rechtswidrig
|
||
ist oder gegen die Nutzungsbedingungen verstößt.
|
||
</span>
|
||
</label>
|
||
</div>
|
||
|
||
{error && (
|
||
<p style={{ color: 'var(--danger)', fontSize: '0.88rem', margin: '0.5rem 0' }}>{error}</p>
|
||
)}
|
||
|
||
<div style={{ display: 'flex', gap: '0.5rem', marginTop: '1rem' }}>
|
||
<button type="submit" className="btn btn-primary" style={{ flex: 1 }} disabled={saving}>
|
||
{saving ? 'Wird gesendet…' : 'Meldung absenden'}
|
||
</button>
|
||
<button type="button" className="btn btn-secondary" onClick={onClose} disabled={saving}>
|
||
Abbrechen
|
||
</button>
|
||
</div>
|
||
</form>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|