fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
591
klausur-service/frontend/src/components/EHUploadWizard.tsx
Normal file
591
klausur-service/frontend/src/components/EHUploadWizard.tsx
Normal file
@@ -0,0 +1,591 @@
|
||||
/**
|
||||
* BYOEH Upload Wizard Component
|
||||
*
|
||||
* 5-step wizard for uploading Erwartungshorizonte with client-side encryption:
|
||||
* 1. File Selection - Choose PDF file
|
||||
* 2. Metadata - Title, Subject, Niveau, Year
|
||||
* 3. Rights Confirmation - Legal acknowledgment (required)
|
||||
* 4. Encryption - Set passphrase (2x confirmation)
|
||||
* 5. Summary & Upload - Review and confirm
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import {
|
||||
encryptFile,
|
||||
generateSalt,
|
||||
isEncryptionSupported
|
||||
} from '../services/encryption'
|
||||
import { ehApi } from '../services/api'
|
||||
|
||||
interface EHMetadata {
|
||||
title: string
|
||||
subject: string
|
||||
niveau: 'eA' | 'gA'
|
||||
year: number
|
||||
aufgaben_nummer?: string
|
||||
}
|
||||
|
||||
interface EHUploadWizardProps {
|
||||
onClose: () => void
|
||||
onComplete?: (ehId: string) => void
|
||||
onSuccess?: (ehId: string) => void // Legacy alias for onComplete
|
||||
klausurSubject?: string
|
||||
klausurYear?: number
|
||||
defaultSubject?: string // Alias for klausurSubject
|
||||
defaultYear?: number // Alias for klausurYear
|
||||
klausurId?: string // If provided, automatically link EH to this Klausur
|
||||
}
|
||||
|
||||
type WizardStep = 'file' | 'metadata' | 'rights' | 'encryption' | 'summary'
|
||||
|
||||
const WIZARD_STEPS: WizardStep[] = ['file', 'metadata', 'rights', 'encryption', 'summary']
|
||||
|
||||
const STEP_LABELS: Record<WizardStep, string> = {
|
||||
file: 'Datei',
|
||||
metadata: 'Metadaten',
|
||||
rights: 'Rechte',
|
||||
encryption: 'Verschluesselung',
|
||||
summary: 'Zusammenfassung'
|
||||
}
|
||||
|
||||
const SUBJECTS = [
|
||||
'deutsch', 'englisch', 'mathematik', 'physik', 'chemie', 'biologie',
|
||||
'geschichte', 'politik', 'erdkunde', 'kunst', 'musik', 'sport',
|
||||
'informatik', 'latein', 'franzoesisch', 'spanisch'
|
||||
]
|
||||
|
||||
const RIGHTS_TEXT = `Ich bestaetige hiermit, dass:
|
||||
|
||||
1. Ich das Urheberrecht oder die notwendigen Nutzungsrechte an diesem
|
||||
Erwartungshorizont besitze.
|
||||
|
||||
2. Breakpilot diesen Erwartungshorizont NICHT fuer KI-Training verwendet,
|
||||
sondern ausschliesslich fuer RAG-gestuetzte Korrekturvorschlaege
|
||||
in meinem persoenlichen Arbeitsbereich.
|
||||
|
||||
3. Der Inhalt verschluesselt gespeichert wird und Breakpilot-Mitarbeiter
|
||||
keinen Zugriff auf den Klartext haben.
|
||||
|
||||
4. Ich diesen Erwartungshorizont jederzeit loeschen kann.`
|
||||
|
||||
function EHUploadWizard({
|
||||
onClose,
|
||||
onComplete,
|
||||
onSuccess,
|
||||
klausurSubject,
|
||||
klausurYear,
|
||||
defaultSubject,
|
||||
defaultYear,
|
||||
klausurId
|
||||
}: EHUploadWizardProps) {
|
||||
// Resolve aliases
|
||||
const effectiveSubject = klausurSubject || defaultSubject || 'deutsch'
|
||||
const effectiveYear = klausurYear || defaultYear || new Date().getFullYear()
|
||||
const handleComplete = onComplete || onSuccess || (() => {})
|
||||
|
||||
// Step state
|
||||
const [currentStep, setCurrentStep] = useState<WizardStep>('file')
|
||||
|
||||
// File step
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
||||
const [fileError, setFileError] = useState<string | null>(null)
|
||||
|
||||
// Metadata step
|
||||
const [metadata, setMetadata] = useState<EHMetadata>({
|
||||
title: '',
|
||||
subject: effectiveSubject,
|
||||
niveau: 'eA',
|
||||
year: effectiveYear,
|
||||
aufgaben_nummer: ''
|
||||
})
|
||||
|
||||
// Rights step
|
||||
const [rightsConfirmed, setRightsConfirmed] = useState(false)
|
||||
|
||||
// Encryption step
|
||||
const [passphrase, setPassphrase] = useState('')
|
||||
const [passphraseConfirm, setPassphraseConfirm] = useState('')
|
||||
const [showPassphrase, setShowPassphrase] = useState(false)
|
||||
const [passphraseStrength, setPassphraseStrength] = useState<'weak' | 'medium' | 'strong'>('weak')
|
||||
|
||||
// Upload state
|
||||
const [uploading, setUploading] = useState(false)
|
||||
const [uploadProgress, setUploadProgress] = useState(0)
|
||||
const [uploadError, setUploadError] = useState<string | null>(null)
|
||||
|
||||
// Check encryption support
|
||||
const encryptionSupported = isEncryptionSupported()
|
||||
|
||||
// Calculate passphrase strength
|
||||
useEffect(() => {
|
||||
if (passphrase.length < 8) {
|
||||
setPassphraseStrength('weak')
|
||||
} else if (passphrase.length < 12 || !/\d/.test(passphrase) || !/[A-Z]/.test(passphrase)) {
|
||||
setPassphraseStrength('medium')
|
||||
} else {
|
||||
setPassphraseStrength('strong')
|
||||
}
|
||||
}, [passphrase])
|
||||
|
||||
// Step navigation
|
||||
const currentStepIndex = WIZARD_STEPS.indexOf(currentStep)
|
||||
const isFirstStep = currentStepIndex === 0
|
||||
const isLastStep = currentStepIndex === WIZARD_STEPS.length - 1
|
||||
|
||||
const goNext = useCallback(() => {
|
||||
if (!isLastStep) {
|
||||
setCurrentStep(WIZARD_STEPS[currentStepIndex + 1])
|
||||
}
|
||||
}, [currentStepIndex, isLastStep])
|
||||
|
||||
const goBack = useCallback(() => {
|
||||
if (!isFirstStep) {
|
||||
setCurrentStep(WIZARD_STEPS[currentStepIndex - 1])
|
||||
}
|
||||
}, [currentStepIndex, isFirstStep])
|
||||
|
||||
// Validation
|
||||
const isStepValid = useCallback((step: WizardStep): boolean => {
|
||||
switch (step) {
|
||||
case 'file':
|
||||
return selectedFile !== null && fileError === null
|
||||
case 'metadata':
|
||||
return metadata.title.trim().length > 0 && metadata.subject.length > 0
|
||||
case 'rights':
|
||||
return rightsConfirmed
|
||||
case 'encryption':
|
||||
return passphrase.length >= 8 && passphrase === passphraseConfirm
|
||||
case 'summary':
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}, [selectedFile, fileError, metadata, rightsConfirmed, passphrase, passphraseConfirm])
|
||||
|
||||
// File handling
|
||||
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (file) {
|
||||
if (file.type !== 'application/pdf') {
|
||||
setFileError('Nur PDF-Dateien sind erlaubt')
|
||||
setSelectedFile(null)
|
||||
return
|
||||
}
|
||||
if (file.size > 50 * 1024 * 1024) { // 50MB limit
|
||||
setFileError('Datei ist zu gross (max. 50MB)')
|
||||
setSelectedFile(null)
|
||||
return
|
||||
}
|
||||
setFileError(null)
|
||||
setSelectedFile(file)
|
||||
// Auto-fill title from filename
|
||||
if (!metadata.title) {
|
||||
const name = file.name.replace(/\.pdf$/i, '').replace(/[_-]/g, ' ')
|
||||
setMetadata(prev => ({ ...prev, title: name }))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Upload handler
|
||||
const handleUpload = async () => {
|
||||
if (!selectedFile || !encryptionSupported) return
|
||||
|
||||
setUploading(true)
|
||||
setUploadProgress(10)
|
||||
setUploadError(null)
|
||||
|
||||
try {
|
||||
// Step 1: Generate salt (used via encrypted.salt below)
|
||||
generateSalt()
|
||||
setUploadProgress(20)
|
||||
|
||||
// Step 2: Encrypt file client-side
|
||||
const encrypted = await encryptFile(selectedFile, passphrase)
|
||||
setUploadProgress(50)
|
||||
|
||||
// Step 3: Create form data
|
||||
const formData = new FormData()
|
||||
const encryptedBlob = new Blob([encrypted.encryptedData], { type: 'application/octet-stream' })
|
||||
formData.append('file', encryptedBlob, 'encrypted.bin')
|
||||
|
||||
const metadataJson = JSON.stringify({
|
||||
metadata: {
|
||||
title: metadata.title,
|
||||
subject: metadata.subject,
|
||||
niveau: metadata.niveau,
|
||||
year: metadata.year,
|
||||
aufgaben_nummer: metadata.aufgaben_nummer || null
|
||||
},
|
||||
encryption_key_hash: encrypted.keyHash,
|
||||
salt: encrypted.salt,
|
||||
rights_confirmed: true,
|
||||
original_filename: selectedFile.name
|
||||
})
|
||||
formData.append('metadata_json', metadataJson)
|
||||
|
||||
setUploadProgress(70)
|
||||
|
||||
// Step 4: Upload to server
|
||||
const response = await ehApi.uploadEH(formData)
|
||||
setUploadProgress(90)
|
||||
|
||||
// Step 5: Link to Klausur if klausurId provided
|
||||
if (klausurId && response.id) {
|
||||
try {
|
||||
await ehApi.linkToKlausur(response.id, klausurId)
|
||||
} catch (linkError) {
|
||||
console.warn('Failed to auto-link EH to Klausur:', linkError)
|
||||
// Don't fail the whole upload if linking fails
|
||||
}
|
||||
}
|
||||
|
||||
setUploadProgress(100)
|
||||
|
||||
// Success!
|
||||
handleComplete(response.id)
|
||||
} catch (error) {
|
||||
console.error('Upload failed:', error)
|
||||
setUploadError(error instanceof Error ? error.message : 'Upload fehlgeschlagen')
|
||||
} finally {
|
||||
setUploading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Render step content
|
||||
const renderStepContent = () => {
|
||||
switch (currentStep) {
|
||||
case 'file':
|
||||
return (
|
||||
<div className="eh-wizard-step">
|
||||
<h3>Erwartungshorizont hochladen</h3>
|
||||
<p className="eh-wizard-description">
|
||||
Waehlen Sie die PDF-Datei Ihres Erwartungshorizonts aus.
|
||||
Die Datei wird verschluesselt und kann nur von Ihnen entschluesselt werden.
|
||||
</p>
|
||||
|
||||
<div className="eh-file-drop">
|
||||
<input
|
||||
type="file"
|
||||
accept=".pdf"
|
||||
onChange={handleFileSelect}
|
||||
id="eh-file-input"
|
||||
/>
|
||||
<label htmlFor="eh-file-input" className="eh-file-label">
|
||||
{selectedFile ? (
|
||||
<>
|
||||
<span className="eh-file-icon">📄</span>
|
||||
<span className="eh-file-name">{selectedFile.name}</span>
|
||||
<span className="eh-file-size">
|
||||
({(selectedFile.size / 1024 / 1024).toFixed(2)} MB)
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="eh-file-icon">📂</span>
|
||||
<span>PDF-Datei hier ablegen oder klicken</span>
|
||||
</>
|
||||
)}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{fileError && <p className="eh-error">{fileError}</p>}
|
||||
|
||||
{!encryptionSupported && (
|
||||
<p className="eh-warning">
|
||||
Ihr Browser unterstuetzt keine Verschluesselung.
|
||||
Bitte verwenden Sie einen modernen Browser (Chrome, Firefox, Safari, Edge).
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
case 'metadata':
|
||||
return (
|
||||
<div className="eh-wizard-step">
|
||||
<h3>Metadaten</h3>
|
||||
<p className="eh-wizard-description">
|
||||
Geben Sie Informationen zum Erwartungshorizont ein.
|
||||
</p>
|
||||
|
||||
<div className="eh-form-group">
|
||||
<label htmlFor="eh-title">Titel *</label>
|
||||
<input
|
||||
id="eh-title"
|
||||
type="text"
|
||||
value={metadata.title}
|
||||
onChange={e => setMetadata(prev => ({ ...prev, title: e.target.value }))}
|
||||
placeholder="z.B. Deutsch Abitur 2024 Aufgabe 1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="eh-form-row">
|
||||
<div className="eh-form-group">
|
||||
<label htmlFor="eh-subject">Fach *</label>
|
||||
<select
|
||||
id="eh-subject"
|
||||
value={metadata.subject}
|
||||
onChange={e => setMetadata(prev => ({ ...prev, subject: e.target.value }))}
|
||||
>
|
||||
{SUBJECTS.map(s => (
|
||||
<option key={s} value={s}>
|
||||
{s.charAt(0).toUpperCase() + s.slice(1)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="eh-form-group">
|
||||
<label htmlFor="eh-niveau">Niveau *</label>
|
||||
<select
|
||||
id="eh-niveau"
|
||||
value={metadata.niveau}
|
||||
onChange={e => setMetadata(prev => ({ ...prev, niveau: e.target.value as 'eA' | 'gA' }))}
|
||||
>
|
||||
<option value="eA">Erhoehtes Anforderungsniveau (eA)</option>
|
||||
<option value="gA">Grundlegendes Anforderungsniveau (gA)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="eh-form-row">
|
||||
<div className="eh-form-group">
|
||||
<label htmlFor="eh-year">Jahr *</label>
|
||||
<input
|
||||
id="eh-year"
|
||||
type="number"
|
||||
min={2000}
|
||||
max={2050}
|
||||
value={metadata.year}
|
||||
onChange={e => setMetadata(prev => ({ ...prev, year: parseInt(e.target.value) }))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="eh-form-group">
|
||||
<label htmlFor="eh-aufgabe">Aufgabennummer</label>
|
||||
<input
|
||||
id="eh-aufgabe"
|
||||
type="text"
|
||||
value={metadata.aufgaben_nummer || ''}
|
||||
onChange={e => setMetadata(prev => ({ ...prev, aufgaben_nummer: e.target.value }))}
|
||||
placeholder="z.B. 1a, 2.1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
case 'rights':
|
||||
return (
|
||||
<div className="eh-wizard-step">
|
||||
<h3>Rechte-Bestaetigung</h3>
|
||||
<p className="eh-wizard-description">
|
||||
Bitte lesen und bestaetigen Sie die folgenden Bedingungen.
|
||||
</p>
|
||||
|
||||
<div className="eh-rights-box">
|
||||
<pre>{RIGHTS_TEXT}</pre>
|
||||
</div>
|
||||
|
||||
<div className="eh-checkbox-group">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="eh-rights-confirm"
|
||||
checked={rightsConfirmed}
|
||||
onChange={e => setRightsConfirmed(e.target.checked)}
|
||||
/>
|
||||
<label htmlFor="eh-rights-confirm">
|
||||
Ich habe die Bedingungen gelesen und stimme ihnen zu.
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="eh-info-box">
|
||||
<strong>Wichtig:</strong> Ihr Erwartungshorizont wird niemals fuer
|
||||
KI-Training verwendet. Er dient ausschliesslich als Referenz fuer
|
||||
Ihre persoenlichen Korrekturvorschlaege.
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
case 'encryption':
|
||||
return (
|
||||
<div className="eh-wizard-step">
|
||||
<h3>Verschluesselung</h3>
|
||||
<p className="eh-wizard-description">
|
||||
Waehlen Sie ein sicheres Passwort fuer Ihren Erwartungshorizont.
|
||||
Dieses Passwort wird <strong>niemals</strong> an den Server gesendet.
|
||||
</p>
|
||||
|
||||
<div className="eh-form-group">
|
||||
<label htmlFor="eh-passphrase">Passwort *</label>
|
||||
<div className="eh-password-input">
|
||||
<input
|
||||
id="eh-passphrase"
|
||||
type={showPassphrase ? 'text' : 'password'}
|
||||
value={passphrase}
|
||||
onChange={e => setPassphrase(e.target.value)}
|
||||
placeholder="Mindestens 8 Zeichen"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="eh-toggle-password"
|
||||
onClick={() => setShowPassphrase(!showPassphrase)}
|
||||
>
|
||||
{showPassphrase ? '👁' : '👀'}
|
||||
</button>
|
||||
</div>
|
||||
<div className={`eh-password-strength eh-strength-${passphraseStrength}`}>
|
||||
Staerke: {passphraseStrength === 'weak' ? 'Schwach' : passphraseStrength === 'medium' ? 'Mittel' : 'Stark'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="eh-form-group">
|
||||
<label htmlFor="eh-passphrase-confirm">Passwort bestaetigen *</label>
|
||||
<input
|
||||
id="eh-passphrase-confirm"
|
||||
type={showPassphrase ? 'text' : 'password'}
|
||||
value={passphraseConfirm}
|
||||
onChange={e => setPassphraseConfirm(e.target.value)}
|
||||
placeholder="Passwort wiederholen"
|
||||
/>
|
||||
{passphraseConfirm && passphrase !== passphraseConfirm && (
|
||||
<p className="eh-error">Passwoerter stimmen nicht ueberein</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="eh-warning-box">
|
||||
<strong>Achtung:</strong> Merken Sie sich dieses Passwort gut!
|
||||
Ohne das Passwort kann der Erwartungshorizont nicht fuer
|
||||
Korrekturvorschlaege verwendet werden. Breakpilot kann Ihr
|
||||
Passwort nicht wiederherstellen.
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
case 'summary':
|
||||
return (
|
||||
<div className="eh-wizard-step">
|
||||
<h3>Zusammenfassung</h3>
|
||||
<p className="eh-wizard-description">
|
||||
Pruefen Sie Ihre Eingaben und starten Sie den Upload.
|
||||
</p>
|
||||
|
||||
<div className="eh-summary-table">
|
||||
<div className="eh-summary-row">
|
||||
<span className="eh-summary-label">Datei:</span>
|
||||
<span className="eh-summary-value">{selectedFile?.name}</span>
|
||||
</div>
|
||||
<div className="eh-summary-row">
|
||||
<span className="eh-summary-label">Titel:</span>
|
||||
<span className="eh-summary-value">{metadata.title}</span>
|
||||
</div>
|
||||
<div className="eh-summary-row">
|
||||
<span className="eh-summary-label">Fach:</span>
|
||||
<span className="eh-summary-value">
|
||||
{metadata.subject.charAt(0).toUpperCase() + metadata.subject.slice(1)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="eh-summary-row">
|
||||
<span className="eh-summary-label">Niveau:</span>
|
||||
<span className="eh-summary-value">{metadata.niveau}</span>
|
||||
</div>
|
||||
<div className="eh-summary-row">
|
||||
<span className="eh-summary-label">Jahr:</span>
|
||||
<span className="eh-summary-value">{metadata.year}</span>
|
||||
</div>
|
||||
<div className="eh-summary-row">
|
||||
<span className="eh-summary-label">Verschluesselung:</span>
|
||||
<span className="eh-summary-value">AES-256-GCM</span>
|
||||
</div>
|
||||
<div className="eh-summary-row">
|
||||
<span className="eh-summary-label">Rechte bestaetigt:</span>
|
||||
<span className="eh-summary-value">Ja</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{uploading && (
|
||||
<div className="eh-upload-progress">
|
||||
<div
|
||||
className="eh-progress-bar"
|
||||
style={{ width: `${uploadProgress}%` }}
|
||||
/>
|
||||
<span>{uploadProgress}%</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{uploadError && (
|
||||
<p className="eh-error">{uploadError}</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="eh-wizard-overlay">
|
||||
<div className="eh-wizard-modal">
|
||||
{/* Header */}
|
||||
<div className="eh-wizard-header">
|
||||
<h2>Erwartungshorizont hochladen</h2>
|
||||
<button className="eh-wizard-close" onClick={onClose}>×</button>
|
||||
</div>
|
||||
|
||||
{/* Progress */}
|
||||
<div className="eh-wizard-progress">
|
||||
{WIZARD_STEPS.map((step, index) => (
|
||||
<div
|
||||
key={step}
|
||||
className={`eh-progress-step ${
|
||||
index < currentStepIndex ? 'completed' :
|
||||
index === currentStepIndex ? 'active' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="eh-progress-dot">
|
||||
{index < currentStepIndex ? '\u2713' : index + 1}
|
||||
</div>
|
||||
<span className="eh-progress-label">{STEP_LABELS[step]}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="eh-wizard-content">
|
||||
{renderStepContent()}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="eh-wizard-footer">
|
||||
<button
|
||||
className="eh-btn eh-btn-secondary"
|
||||
onClick={isFirstStep ? onClose : goBack}
|
||||
disabled={uploading}
|
||||
>
|
||||
{isFirstStep ? 'Abbrechen' : 'Zurueck'}
|
||||
</button>
|
||||
|
||||
{isLastStep ? (
|
||||
<button
|
||||
className="eh-btn eh-btn-primary"
|
||||
onClick={handleUpload}
|
||||
disabled={uploading || !isStepValid(currentStep)}
|
||||
>
|
||||
{uploading ? 'Wird hochgeladen...' : 'Hochladen'}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="eh-btn eh-btn-primary"
|
||||
onClick={goNext}
|
||||
disabled={!isStepValid(currentStep)}
|
||||
>
|
||||
Weiter
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default EHUploadWizard
|
||||
Reference in New Issue
Block a user