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,69 @@
|
||||
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Routes, Route, NavLink } from 'react-router-dom';
|
||||
import { Sun, Moon, Settings } from 'lucide-react';
|
||||
import DashboardPage from './pages/DashboardPage';
|
||||
import MatchesPage from './pages/MatchesPage';
|
||||
import LeaderboardPage from './pages/LeaderboardPage';
|
||||
import ProfilePage from './pages/ProfilePage';
|
||||
import AdminPage from './pages/AdminPage';
|
||||
import BottomNav from './components/BottomNav';
|
||||
import styles from './App.module.css';
|
||||
const IS_DEV = import.meta.env.DEV || import.meta.env.VITE_TEST_MODE === 'true';
|
||||
// Lazy-load DevPanel in Development/Test-Mode
|
||||
let DevPanel = null;
|
||||
// VITE_TEST_MODE wird erst zur Laufzeit geprüft, daher Import immer einbinden
|
||||
import('./components/DevPanel').then(m => { DevPanel = m.default; }).catch(() => { });
|
||||
function getInitialTheme() {
|
||||
try {
|
||||
const stored = localStorage.getItem('theme');
|
||||
if (stored === 'light' || stored === 'dark')
|
||||
return stored;
|
||||
}
|
||||
catch { }
|
||||
return 'dark';
|
||||
}
|
||||
export default function App() {
|
||||
const [theme, setTheme] = useState(getInitialTheme);
|
||||
const [devUser, setDevUser] = useState(1);
|
||||
const [devMatches, setDevMatches] = useState([]);
|
||||
const [refreshKey, setRefreshKey] = useState(0);
|
||||
// Theme auf <html> setzen und in localStorage speichern
|
||||
useEffect(() => {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
try {
|
||||
localStorage.setItem('theme', theme);
|
||||
}
|
||||
catch { }
|
||||
}, [theme]);
|
||||
function toggleTheme() {
|
||||
setTheme(t => t === 'dark' ? 'light' : 'dark');
|
||||
}
|
||||
// DevUser als Query-Parameter im API-Fetch setzen
|
||||
useEffect(() => {
|
||||
if (!IS_DEV)
|
||||
return;
|
||||
// Patch fetch für Dev-Mode: devUser Query-Param anhängen
|
||||
const origFetch = window.fetch;
|
||||
window._devUser = devUser;
|
||||
window.fetch = (input, init) => {
|
||||
if (typeof input === 'string' && input.startsWith('/api')) {
|
||||
const url = new URL(input, window.location.origin);
|
||||
url.searchParams.set('devUser', String(window._devUser ?? 1));
|
||||
return origFetch(url.toString(), init);
|
||||
}
|
||||
return origFetch(input, init);
|
||||
};
|
||||
return () => { window.fetch = origFetch; };
|
||||
}, [devUser]);
|
||||
// Matches für DevPanel laden
|
||||
useEffect(() => {
|
||||
if (!IS_DEV)
|
||||
return;
|
||||
fetch('/api/matches').then(r => r.json()).then(d => setDevMatches(d.matches ?? [])).catch(() => { });
|
||||
}, [refreshKey, devUser]);
|
||||
function handleDevRefresh() {
|
||||
setRefreshKey(k => k + 1);
|
||||
}
|
||||
return (_jsxs("div", { className: styles.app, children: [_jsx("header", { className: styles.header, children: _jsxs("div", { className: styles.headerInner, children: [_jsxs("div", { className: styles.logo, children: [_jsx("span", { className: styles.logoFlag, children: "\uD83C\uDFC6" }), _jsx("span", { className: styles.logoText, children: "WM 2026 Tippspiel" }), IS_DEV && (_jsxs("span", { className: styles.devBadge, children: ["DEV \u00B7 User ", devUser] }))] }), _jsxs("nav", { className: styles.nav, children: [_jsx(NavLink, { to: "/spiele", className: ({ isActive }) => isActive ? styles.navLinkActive : styles.navLink, children: "Spielplan" }), _jsx(NavLink, { to: "/rangliste", className: ({ isActive }) => isActive ? styles.navLinkActive : styles.navLink, children: "Rangliste" }), _jsx(NavLink, { to: "/profil", className: ({ isActive }) => isActive ? styles.navLinkActive : styles.navLink, children: "Mein Profil" }), _jsx(NavLink, { to: "/admin", className: styles.adminLink, title: "Admin", children: _jsx(Settings, { size: 16 }) }), _jsx("button", { className: styles.themeToggle, onClick: toggleTheme, title: theme === 'dark' ? 'Light Mode aktivieren' : 'Dark Mode aktivieren', "aria-label": "Theme wechseln", children: theme === 'dark' ? _jsx(Sun, { size: 16 }) : _jsx(Moon, { size: 16 }) })] })] }) }), _jsx("main", { className: styles.main, children: _jsxs(Routes, { children: [_jsx(Route, { path: "/", element: _jsx(DashboardPage, { devUser: devUser }, refreshKey) }), _jsx(Route, { path: "/spiele", element: _jsx(MatchesPage, { devUser: devUser }, refreshKey) }), _jsx(Route, { path: "/rangliste", element: _jsx(LeaderboardPage, {}, refreshKey) }), _jsx(Route, { path: "/profil", element: _jsx(ProfilePage, {}, refreshKey) }), _jsx(Route, { path: "/admin", element: _jsx(AdminPage, {}) })] }) }), _jsx(BottomNav, {}), IS_DEV && DevPanel && (_jsx(DevPanel, { currentUser: devUser, onUserChange: (u) => { setDevUser(u); setRefreshKey(k => k + 1); }, matches: devMatches, onRefresh: handleDevRefresh }))] }));
|
||||
}
|
||||
Reference in New Issue
Block a user