209 lines
7.7 KiB
React
209 lines
7.7 KiB
React
|
|
import { useEffect, useState } from 'react'
|
||
|
|
import { Link } from 'react-router-dom'
|
||
|
|
import api from '../utils/api'
|
||
|
|
import { useAuth } from '../context/AuthContext'
|
||
|
|
import EmailVerificationBanner from '../components/EmailVerificationBanner'
|
||
|
|
import { resolveAccountState } from '../utils/accountState'
|
||
|
|
|
||
|
|
const joinStatusLabel = (s) =>
|
||
|
|
({
|
||
|
|
pending: 'ausstehend',
|
||
|
|
accepted: 'angenommen',
|
||
|
|
rejected: 'abgelehnt',
|
||
|
|
withdrawn: 'zurückgezogen',
|
||
|
|
})[s] || s
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Onboarding für Nutzer ohne aktive Vereinsmitgliedschaft (Phase A).
|
||
|
|
*/
|
||
|
|
export default function OnboardingPage() {
|
||
|
|
const { user, checkAuth } = useAuth()
|
||
|
|
const [publicClubs, setPublicClubs] = useState([])
|
||
|
|
const [myJoinRequests, setMyJoinRequests] = useState([])
|
||
|
|
const [joinClubId, setJoinClubId] = useState('')
|
||
|
|
const [joinMessage, setJoinMessage] = useState('')
|
||
|
|
const [joinBusy, setJoinBusy] = useState(false)
|
||
|
|
const [error, setError] = useState('')
|
||
|
|
const [ok, setOk] = useState('')
|
||
|
|
|
||
|
|
const accountState = resolveAccountState(user)
|
||
|
|
const emailOk = accountState !== 'unverified'
|
||
|
|
|
||
|
|
const refreshJoinRequests = () => {
|
||
|
|
if (!emailOk) return
|
||
|
|
api.getMyClubJoinRequests().then(setMyJoinRequests).catch(() => {})
|
||
|
|
}
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
api.listPublicClubsDirectory().then(setPublicClubs).catch(() => {})
|
||
|
|
refreshJoinRequests()
|
||
|
|
}, [user?.id, emailOk])
|
||
|
|
|
||
|
|
const memberClubIds = new Set((user?.clubs || []).map((c) => c.id))
|
||
|
|
const pendingClubIds = new Set(
|
||
|
|
myJoinRequests.filter((r) => r.status === 'pending').map((r) => r.club_id)
|
||
|
|
)
|
||
|
|
const joinClubChoices = publicClubs.filter(
|
||
|
|
(c) => !memberClubIds.has(c.id) && !pendingClubIds.has(c.id)
|
||
|
|
)
|
||
|
|
|
||
|
|
const handleJoin = async (e) => {
|
||
|
|
e.preventDefault()
|
||
|
|
setError('')
|
||
|
|
setOk('')
|
||
|
|
if (!joinClubId) {
|
||
|
|
setError('Bitte einen Verein auswählen.')
|
||
|
|
return
|
||
|
|
}
|
||
|
|
setJoinBusy(true)
|
||
|
|
try {
|
||
|
|
await api.createClubJoinRequest({
|
||
|
|
club_id: parseInt(joinClubId, 10),
|
||
|
|
message: (joinMessage || '').trim() || undefined,
|
||
|
|
})
|
||
|
|
setJoinMessage('')
|
||
|
|
setJoinClubId('')
|
||
|
|
refreshJoinRequests()
|
||
|
|
await checkAuth()
|
||
|
|
setOk('Antrag gesendet. Der Vereinsadmin kann ihn unter Vereinsverwaltung annehmen.')
|
||
|
|
} catch (err) {
|
||
|
|
setError(err.message || 'Antrag fehlgeschlagen.')
|
||
|
|
} finally {
|
||
|
|
setJoinBusy(false)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="page-padding app-page" style={{ padding: '1rem', maxWidth: '40rem' }}>
|
||
|
|
<h1 style={{ marginTop: 0, fontSize: '1.5rem' }}>Willkommen bei Shinkan</h1>
|
||
|
|
<p style={{ color: 'var(--text2)', lineHeight: 1.5, marginBottom: '1.25rem' }}>
|
||
|
|
Shinkan ist die Trainingsplanungs-Plattform für Vereine. Um Übungen, Planung und Medien zu nutzen,
|
||
|
|
brauchst du eine Mitgliedschaft in einem Verein — oder du beantragst die Gründung eines neuen Vereins.
|
||
|
|
</p>
|
||
|
|
|
||
|
|
<EmailVerificationBanner profile={user} />
|
||
|
|
|
||
|
|
{!emailOk ? (
|
||
|
|
<div className="card">
|
||
|
|
<p style={{ margin: 0, color: 'var(--text2)', lineHeight: 1.5 }}>
|
||
|
|
Bitte bestätige zuerst deine E-Mail-Adresse. Danach kannst du einen Beitrittsantrag stellen.
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
) : (
|
||
|
|
<>
|
||
|
|
{ok ? (
|
||
|
|
<p role="status" style={{ color: 'var(--accent-dark)', marginBottom: '1rem' }}>
|
||
|
|
{ok}
|
||
|
|
</p>
|
||
|
|
) : null}
|
||
|
|
{error ? (
|
||
|
|
<p role="alert" style={{ color: 'var(--danger)', marginBottom: '1rem' }}>
|
||
|
|
{error}
|
||
|
|
</p>
|
||
|
|
) : null}
|
||
|
|
|
||
|
|
<div className="card" style={{ marginBottom: '1rem' }}>
|
||
|
|
<h2 style={{ margin: '0 0 0.75rem', fontSize: '1.1rem' }}>Bestehendem Verein beitreten</h2>
|
||
|
|
<p style={{ color: 'var(--text2)', fontSize: '0.875rem', marginBottom: '1rem', lineHeight: 1.5 }}>
|
||
|
|
Wähle einen Verein und sende einen Beitrittsantrag. Nach Freigabe durch den Vereinsadmin
|
||
|
|
stehen dir alle Funktionen zur Verfügung.
|
||
|
|
</p>
|
||
|
|
|
||
|
|
{myJoinRequests.length > 0 ? (
|
||
|
|
<div style={{ marginBottom: '1rem' }}>
|
||
|
|
<strong style={{ fontSize: '0.9rem' }}>Meine Anträge</strong>
|
||
|
|
<ul
|
||
|
|
style={{
|
||
|
|
margin: '0.5rem 0 0',
|
||
|
|
paddingLeft: '1.25rem',
|
||
|
|
color: 'var(--text2)',
|
||
|
|
fontSize: '0.9rem',
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{myJoinRequests.map((r) => (
|
||
|
|
<li key={r.id} style={{ marginBottom: '0.35rem' }}>
|
||
|
|
{r.club_name || `Verein #${r.club_id}`} — {joinStatusLabel(r.status)}
|
||
|
|
{r.status === 'pending' ? (
|
||
|
|
<>
|
||
|
|
{' '}
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
className="btn btn-secondary"
|
||
|
|
style={{ fontSize: '0.75rem', padding: '0.15rem 0.45rem' }}
|
||
|
|
onClick={async () => {
|
||
|
|
if (!confirm('Antrag wirklich zurückziehen?')) return
|
||
|
|
try {
|
||
|
|
await api.withdrawClubJoinRequest(r.id)
|
||
|
|
refreshJoinRequests()
|
||
|
|
} catch (err) {
|
||
|
|
setError(err.message || 'Zurückziehen fehlgeschlagen.')
|
||
|
|
}
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
zurückziehen
|
||
|
|
</button>
|
||
|
|
</>
|
||
|
|
) : null}
|
||
|
|
</li>
|
||
|
|
))}
|
||
|
|
</ul>
|
||
|
|
</div>
|
||
|
|
) : null}
|
||
|
|
|
||
|
|
<form onSubmit={handleJoin}>
|
||
|
|
<label className="form-label" htmlFor="onb-join-club">
|
||
|
|
Verein
|
||
|
|
</label>
|
||
|
|
<select
|
||
|
|
id="onb-join-club"
|
||
|
|
className="form-input"
|
||
|
|
value={joinClubId}
|
||
|
|
onChange={(e) => setJoinClubId(e.target.value)}
|
||
|
|
>
|
||
|
|
<option value="">—</option>
|
||
|
|
{joinClubChoices.map((c) => (
|
||
|
|
<option key={c.id} value={String(c.id)}>
|
||
|
|
{c.name}
|
||
|
|
{c.abbreviation ? ` (${c.abbreviation})` : ''}
|
||
|
|
</option>
|
||
|
|
))}
|
||
|
|
</select>
|
||
|
|
<label className="form-label" htmlFor="onb-join-msg" style={{ marginTop: '0.75rem' }}>
|
||
|
|
Nachricht (optional)
|
||
|
|
</label>
|
||
|
|
<textarea
|
||
|
|
id="onb-join-msg"
|
||
|
|
className="form-input"
|
||
|
|
rows={2}
|
||
|
|
value={joinMessage}
|
||
|
|
onChange={(e) => setJoinMessage(e.target.value)}
|
||
|
|
/>
|
||
|
|
<button
|
||
|
|
type="submit"
|
||
|
|
className="btn btn-primary"
|
||
|
|
disabled={joinBusy}
|
||
|
|
style={{ marginTop: '0.85rem' }}
|
||
|
|
>
|
||
|
|
{joinBusy ? 'Senden…' : 'Beitritt beantragen'}
|
||
|
|
</button>
|
||
|
|
</form>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="card">
|
||
|
|
<h2 style={{ margin: '0 0 0.75rem', fontSize: '1.1rem' }}>Neuen Verein gründen</h2>
|
||
|
|
<p style={{ color: 'var(--text2)', fontSize: '0.875rem', margin: 0, lineHeight: 1.5 }}>
|
||
|
|
Die Beantragung einer neuen Vereinsgründung wird als Nächstes freigeschaltet (Freigabe durch
|
||
|
|
den Plattform-Administrator). Bis dahin wende dich an{' '}
|
||
|
|
<a href="mailto:support@jinkendo.de">support@jinkendo.de</a> oder tritt einem bestehenden Verein bei.
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
</>
|
||
|
|
)}
|
||
|
|
|
||
|
|
<p style={{ marginTop: '1.25rem', fontSize: '0.875rem' }}>
|
||
|
|
<Link to="/settings">Einstellungen</Link> (Passwort, Profil)
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
}
|