This repository has been archived on 2026-05-06. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
tippspiel/frontend/src/components/TipModal.tsx
T
Ronny 950f51c61b feat: tip confirmation animation with haptic feedback
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>
2026-04-11 19:08:39 +02:00

154 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
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>
);
}