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:
Ronny
2026-04-11 19:08:39 +02:00
parent 1ed64078b4
commit 89046a2e29
17 changed files with 746 additions and 2 deletions
+50
View File
@@ -0,0 +1,50 @@
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import { useState, useEffect } from 'react';
import { api } from '../api/client';
import styles from './ProfilePage.module.css';
function initials(name) {
return name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2);
}
export default function ProfilePage() {
const [stats, setStats] = useState(null);
const [loading, setLoading] = useState(true);
const [teamEdit, setTeamEdit] = useState(false);
const [teamValue, setTeamValue] = useState('');
const [teamSaving, setTeamSaving] = useState(false);
const [teamMsg, setTeamMsg] = useState(null);
useEffect(() => {
api.getMyStats().then(s => {
setStats(s);
setTeamValue(s.team ?? '');
}).finally(() => setLoading(false));
}, []);
const saveTeam = async () => {
if (!teamValue.trim())
return;
setTeamSaving(true);
setTeamMsg(null);
try {
const res = await api.updateTeam(teamValue);
setStats(prev => prev ? { ...prev, team: res.team } : prev);
setTeamValue(res.team);
setTeamEdit(false);
setTeamMsg({ ok: true, text: 'Team gespeichert' });
}
catch (e) {
setTeamMsg({ ok: false, text: e.message });
}
finally {
setTeamSaving(false);
}
};
if (loading)
return _jsx("div", { className: styles.loading, children: _jsx("div", { className: styles.spinner }) });
if (!stats)
return _jsx("div", { className: styles.empty, children: "Profil nicht verf\u00FCgbar." });
const evaluated = stats.exactCount + stats.tendencyCount + stats.wrongCount;
return (_jsxs("div", { className: styles.page, children: [_jsxs("div", { className: `card ${styles.heroCard}`, children: [_jsx("div", { className: styles.avatar, children: initials(stats.fullName) }), _jsxs("div", { className: styles.heroInfo, children: [_jsx("h1", { className: `font-display ${styles.name}`, children: stats.fullName }), stats.rank && _jsxs("div", { className: styles.rankBadge, children: ["\uD83C\uDFC6 Platz ", stats.rank] }), _jsx("div", { className: styles.teamRow, children: teamEdit ? (_jsxs("div", { className: styles.teamEditRow, children: [_jsx("input", { className: styles.teamInput, value: teamValue, onChange: e => setTeamValue(e.target.value), placeholder: "z. B. Vertrieb S\u00FCd", maxLength: 80, autoFocus: true, onKeyDown: e => { if (e.key === 'Enter')
saveTeam(); if (e.key === 'Escape')
setTeamEdit(false); } }), _jsx("button", { className: styles.teamSaveBtn, onClick: saveTeam, disabled: teamSaving, children: teamSaving ? _jsx("span", { className: styles.spinnerSm }) : '✓' }), _jsx("button", { className: styles.teamCancelBtn, onClick: () => { setTeamEdit(false); setTeamValue(stats.team ?? ''); }, children: "\u2715" })] })) : (_jsx("button", { className: styles.teamBtn, onClick: () => setTeamEdit(true), children: stats.team
? _jsxs(_Fragment, { children: [_jsx("span", { className: styles.teamName, children: stats.team }), _jsx("span", { className: styles.teamEditHint, children: "bearbeiten" })] })
: _jsx("span", { className: styles.teamPlaceholder, children: "+ Team hinzuf\u00FCgen" }) })) }), teamMsg && (_jsx("div", { className: `${styles.teamMsg} ${teamMsg.ok ? styles.teamMsgOk : styles.teamMsgErr}`, children: teamMsg.text }))] }), _jsxs("div", { className: styles.heroPoints, children: [_jsx("span", { className: `font-display ${styles.pointsVal}`, children: stats.totalPoints }), _jsx("span", { className: styles.pointsLbl, children: "Punkte" })] })] }), _jsxs("div", { className: styles.statsGrid, children: [_jsxs("div", { className: `card ${styles.statCard}`, children: [_jsx("span", { className: `font-display ${styles.statVal} text-gold`, children: stats.exactCount }), _jsx("span", { className: styles.statLbl, children: "\uD83C\uDFAF Exakt" })] }), _jsxs("div", { className: `card ${styles.statCard}`, children: [_jsx("span", { className: `font-display ${styles.statVal} text-primary`, children: stats.tendencyCount }), _jsx("span", { className: styles.statLbl, children: "\u2713 Tendenz" })] }), _jsxs("div", { className: `card ${styles.statCard}`, children: [_jsx("span", { className: `font-display ${styles.statVal}`, style: { color: 'var(--error)' }, children: stats.wrongCount }), _jsx("span", { className: styles.statLbl, children: "\u2717 Falsch" })] }), _jsxs("div", { className: `card ${styles.statCard}`, children: [_jsx("span", { className: `font-display ${styles.statVal}`, children: stats.tipsCount }), _jsx("span", { className: styles.statLbl, children: "Tipps gesamt" })] })] }), evaluated > 0 && (_jsxs("div", { className: `card ${styles.accuracyCard}`, children: [_jsxs("div", { className: styles.accuracyHeader, children: [_jsx("span", { className: styles.accuracyLabel, children: "Trefferquote" }), _jsxs("span", { className: `font-display ${styles.accuracyVal}`, children: [stats.accuracy, "%"] })] }), _jsxs("div", { className: styles.bar, children: [_jsx("div", { className: `${styles.barFill} ${styles.exact}`, style: { width: `${(stats.exactCount / evaluated) * 100}%` } }), _jsx("div", { className: `${styles.barFill} ${styles.tendency}`, style: { width: `${(stats.tendencyCount / evaluated) * 100}%` } })] }), _jsxs("div", { className: styles.barLegend, children: [_jsxs("span", { children: [_jsx("span", { className: styles.dot, style: { background: 'var(--gold)' } }), " Exakt"] }), _jsxs("span", { children: [_jsx("span", { className: styles.dot, style: { background: 'var(--primary)' } }), " Tendenz"] }), _jsxs("span", { children: [_jsx("span", { className: styles.dot, style: { background: 'var(--surface-high)' } }), " Falsch"] })] })] }))] }));
}