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>
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
||||
import { useState } from 'react';
|
||||
import { api } from '../api/client';
|
||||
import styles from './TipModal.module.css';
|
||||
function getTendency(home, away) {
|
||||
if (home > away)
|
||||
return 'home';
|
||||
if (away > home)
|
||||
return 'away';
|
||||
return 'draw';
|
||||
}
|
||||
export default function TipModal({ match, onClose, onSaved }) {
|
||||
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(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.message);
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
return (_jsx("div", { className: styles.overlay, onClick: onClose, children: _jsxs("div", { className: styles.sheet, onClick: e => e.stopPropagation(), children: [_jsx("div", { className: styles.handle }), _jsxs("div", { className: styles.teamsRow, children: [_jsxs("div", { className: styles.teamBlock, children: [_jsx("div", { className: styles.flagLarge, children: match.homeTeam.crest
|
||||
? _jsx("img", { src: match.homeTeam.crest, alt: match.homeTeam.name, className: styles.flagImg })
|
||||
: _jsx("span", { className: styles.flagEmoji, children: "\uD83C\uDFF3\uFE0F" }) }), _jsx("span", { className: styles.teamName, children: match.homeTeam.name })] }), _jsx("div", { className: styles.vsBlock }), _jsxs("div", { className: styles.teamBlock, children: [_jsx("div", { className: styles.flagLarge, children: match.awayTeam.crest
|
||||
? _jsx("img", { src: match.awayTeam.crest, alt: match.awayTeam.name, className: styles.flagImg })
|
||||
: _jsx("span", { className: styles.flagEmoji, children: "\uD83C\uDFF3\uFE0F" }) }), _jsx("span", { className: styles.teamName, children: match.awayTeam.name })] })] }), _jsxs("div", { className: styles.pickerSection, children: [_jsx("p", { className: styles.pickerLabel, children: "Dein Tipp" }), _jsxs("div", { className: styles.pickerRow, children: [_jsx(ScorePicker, { value: home, onChange: setHome }), _jsx("div", { className: styles.pickerColon, children: ":" }), _jsx(ScorePicker, { value: away, onChange: setAway })] })] }), _jsxs("div", { className: styles.tendencyBar, style: { '--tendency-color': tendencyColor }, children: [_jsx("span", { className: styles.tendencyIcon, children: tendency === 'draw' ? '🤝' : tendency === 'home' ? '🏠' : '✈️' }), _jsxs("span", { className: styles.tendencyText, children: ["Tendenz: ", _jsx("strong", { children: tendencyLabel })] })] }), error && _jsx("div", { className: styles.error, children: error }), showSuccess && (_jsxs("div", { className: styles.successOverlay, children: [_jsx("div", { className: styles.successCheck, children: "\u2713" }), _jsx("div", { className: styles.successText, children: "Dein Tipp ist drin! \uD83C\uDFAF" })] })), _jsx("button", { className: `btn-primary ${styles.saveBtn}`, onClick: handleSave, disabled: saving, children: saving ? '⏳ Wird gespeichert…' : '✓ Tipp bestätigen' }), _jsx("button", { className: styles.cancelBtn, onClick: onClose, children: "Abbrechen" })] }) }));
|
||||
}
|
||||
function ScorePicker({ value, onChange }) {
|
||||
return (_jsxs("div", { className: styles.picker, children: [_jsx("button", { className: styles.pickerBtn, onClick: () => onChange(Math.min(20, value + 1)), "aria-label": "Erh\u00F6hen", children: "+" }), _jsx("span", { className: styles.pickerValue, children: value }), _jsx("button", { className: styles.pickerBtn, onClick: () => onChange(Math.max(0, value - 1)), "aria-label": "Verringern", children: "\u2212" })] }));
|
||||
}
|
||||
Reference in New Issue
Block a user