shinkan-jinkendo/frontend/src/pages/OnboardingPage.jsx

209 lines
7.7 KiB
React
Raw Normal View History

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