950f51c61b
Success overlay with animated checkmark and 'Dein Tipp ist drin! 🎯' message. Haptic vibration on mobile. Auto-closes after 1.2s. - Add showSuccess state to TipModal - Trigger vibration feedback on successful submit - Display success overlay with popIn animation for checkmark - Auto-close modal after success animation completes - Add CSS animations (fadeIn, popIn) to TipModal.module.css Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
154 lines
4.8 KiB
TypeScript
154 lines
4.8 KiB
TypeScript
import { useState } from 'react';
|
||
import { Match, api } from '../api/client';
|
||
import styles from './TipModal.module.css';
|
||
|
||
interface Props {
|
||
match: Match;
|
||
onClose: () => void;
|
||
onSaved: (matchId: number, home: number, away: number) => void;
|
||
}
|
||
|
||
type Tendency = 'home' | 'draw' | 'away';
|
||
|
||
function getTendency(home: number, away: number): Tendency {
|
||
if (home > away) return 'home';
|
||
if (away > home) return 'away';
|
||
return 'draw';
|
||
}
|
||
|
||
export default function TipModal({ match, onClose, onSaved }: Props) {
|
||
const existing = match.userTip;
|
||
const [home, setHome] = useState(existing?.home ?? 0);
|
||
const [away, setAway] = useState(existing?.away ?? 0);
|
||
const [saving, setSaving] = useState(false);
|
||
const [showSuccess, setShowSuccess] = useState(false);
|
||
const [error, setError] = useState<string | null>(null);
|
||
|
||
const tendency = getTendency(home, away);
|
||
const tendencyLabel =
|
||
tendency === 'home' ? match.homeTeam.shortName || match.homeTeam.name :
|
||
tendency === 'away' ? match.awayTeam.shortName || match.awayTeam.name :
|
||
'Unentschieden';
|
||
const tendencyColor =
|
||
tendency === 'home' ? 'var(--primary)' :
|
||
tendency === 'away' ? 'var(--cyan)' :
|
||
'var(--gold)';
|
||
|
||
const handleSave = async () => {
|
||
setSaving(true);
|
||
setError(null);
|
||
try {
|
||
await api.submitTip(match.id, home, away);
|
||
setShowSuccess(true);
|
||
if (navigator.vibrate) navigator.vibrate(50); // haptic feedback
|
||
setTimeout(() => {
|
||
setShowSuccess(false);
|
||
onSaved(match.id, home, away);
|
||
onClose();
|
||
}, 1200);
|
||
} catch (e) {
|
||
setError((e as Error).message);
|
||
setSaving(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className={styles.overlay} onClick={onClose}>
|
||
<div className={styles.sheet} onClick={e => e.stopPropagation()}>
|
||
|
||
{/* Drag handle */}
|
||
<div className={styles.handle} />
|
||
|
||
{/* Teams mit Flaggen */}
|
||
<div className={styles.teamsRow}>
|
||
<div className={styles.teamBlock}>
|
||
<div className={styles.flagLarge}>
|
||
{match.homeTeam.crest
|
||
? <img src={match.homeTeam.crest} alt={match.homeTeam.name} className={styles.flagImg} />
|
||
: <span className={styles.flagEmoji}>🏳️</span>
|
||
}
|
||
</div>
|
||
<span className={styles.teamName}>{match.homeTeam.name}</span>
|
||
</div>
|
||
|
||
<div className={styles.vsBlock} />
|
||
|
||
<div className={styles.teamBlock}>
|
||
<div className={styles.flagLarge}>
|
||
{match.awayTeam.crest
|
||
? <img src={match.awayTeam.crest} alt={match.awayTeam.name} className={styles.flagImg} />
|
||
: <span className={styles.flagEmoji}>🏳️</span>
|
||
}
|
||
</div>
|
||
<span className={styles.teamName}>{match.awayTeam.name}</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Score Picker */}
|
||
<div className={styles.pickerSection}>
|
||
<p className={styles.pickerLabel}>Dein Tipp</p>
|
||
<div className={styles.pickerRow}>
|
||
<ScorePicker value={home} onChange={setHome} />
|
||
<div className={styles.pickerColon}>:</div>
|
||
<ScorePicker value={away} onChange={setAway} />
|
||
</div>
|
||
</div>
|
||
|
||
{/* Tendenz */}
|
||
<div className={styles.tendencyBar} style={{ '--tendency-color': tendencyColor } as React.CSSProperties}>
|
||
<span className={styles.tendencyIcon}>
|
||
{tendency === 'draw' ? '🤝' : tendency === 'home' ? '🏠' : '✈️'}
|
||
</span>
|
||
<span className={styles.tendencyText}>
|
||
Tendenz: <strong>{tendencyLabel}</strong>
|
||
</span>
|
||
</div>
|
||
|
||
{error && <div className={styles.error}>{error}</div>}
|
||
|
||
{showSuccess && (
|
||
<div className={styles.successOverlay}>
|
||
<div className={styles.successCheck}>✓</div>
|
||
<div className={styles.successText}>Dein Tipp ist drin! 🎯</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* CTA */}
|
||
<button
|
||
className={`btn-primary ${styles.saveBtn}`}
|
||
onClick={handleSave}
|
||
disabled={saving}
|
||
>
|
||
{saving ? '⏳ Wird gespeichert…' : '✓ Tipp bestätigen'}
|
||
</button>
|
||
|
||
<button className={styles.cancelBtn} onClick={onClose}>
|
||
Abbrechen
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function ScorePicker({ value, onChange }: { value: number; onChange: (v: number) => void }) {
|
||
return (
|
||
<div className={styles.picker}>
|
||
<button
|
||
className={styles.pickerBtn}
|
||
onClick={() => onChange(Math.min(20, value + 1))}
|
||
aria-label="Erhöhen"
|
||
>
|
||
+
|
||
</button>
|
||
<span className={styles.pickerValue}>{value}</span>
|
||
<button
|
||
className={styles.pickerBtn}
|
||
onClick={() => onChange(Math.max(0, value - 1))}
|
||
aria-label="Verringern"
|
||
>
|
||
−
|
||
</button>
|
||
</div>
|
||
);
|
||
}
|