shinkan-jinkendo/frontend/src/components/ReportContentModal.jsx
Lars 34e93101f1
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
feat(P-13): Workflow-Management, Fehleranzeige, Badge-Update, Wieder-öffnen
- 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>
2026-05-11 21:32:44 +02:00

201 lines
7.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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>
)
}