admin panel done
This commit is contained in:
commit
4726582b89
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
node_modules/
|
||||
.env
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.DS_Store
|
||||
.idea/
|
||||
dist/
|
||||
coverage/
|
||||
19751
package-lock.json
generated
Normal file
19751
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
46
package.json
Normal file
46
package.json
Normal file
@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "bazar3-adminpanel",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"axios": "^1.4.0",
|
||||
"bootstrap": "^5.3.0",
|
||||
"framer-motion": "^10.18.0",
|
||||
"react": "^18.2.0",
|
||||
"react-bootstrap": "^2.8.0",
|
||||
"react-datepicker": "^4.16.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-icons": "^4.10.1",
|
||||
"react-router-dom": "^6.14.1",
|
||||
"react-scripts": "5.0.1",
|
||||
"react-toastify": "^9.1.3",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
20
public/index.html
Normal file
20
public/index.html
Normal file
@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Kings Admin Panel - Manage teams and results"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<title>Kings Admin Panel</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
25
public/manifest.json
Normal file
25
public/manifest.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"short_name": "Kings Admin",
|
||||
"name": "Kings Admin Panel",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
3
public/robots.txt
Normal file
3
public/robots.txt
Normal file
@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
48
src/App.js
Normal file
48
src/App.js
Normal file
@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
import { Routes, Route, Navigate } from 'react-router-dom';
|
||||
import Login from './pages/Login';
|
||||
import Dashboard from './pages/Dashboard';
|
||||
import Teams from './pages/Teams';
|
||||
import Results from './pages/Results';
|
||||
import PrivateRoute from './utils/PrivateRoute';
|
||||
import Layout from './components/Layout/Layout';
|
||||
import AnimatedRoute from './components/AnimatedRoute';
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<AnimatePresence mode="wait">
|
||||
<Routes>
|
||||
<Route path="/login" element={
|
||||
<AnimatedRoute>
|
||||
<Login />
|
||||
</AnimatedRoute>
|
||||
} />
|
||||
|
||||
<Route path="/" element={<PrivateRoute />}>
|
||||
<Route path="/" element={<Layout />}>
|
||||
<Route index element={
|
||||
<AnimatedRoute>
|
||||
<Dashboard />
|
||||
</AnimatedRoute>
|
||||
} />
|
||||
<Route path="teams" element={
|
||||
<AnimatedRoute>
|
||||
<Teams />
|
||||
</AnimatedRoute>
|
||||
} />
|
||||
<Route path="results" element={
|
||||
<AnimatedRoute>
|
||||
<Results />
|
||||
</AnimatedRoute>
|
||||
} />
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
0
src/components/AnimatedCard.js
Normal file
0
src/components/AnimatedCard.js
Normal file
41
src/components/AnimatedRoute.js
Normal file
41
src/components/AnimatedRoute.js
Normal file
@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
// Variants for page transitions
|
||||
const pageVariants = {
|
||||
initial: {
|
||||
opacity: 0,
|
||||
y: 20
|
||||
},
|
||||
in: {
|
||||
opacity: 1,
|
||||
y: 0
|
||||
},
|
||||
out: {
|
||||
opacity: 0,
|
||||
y: -20
|
||||
}
|
||||
};
|
||||
|
||||
// Transition timing
|
||||
const pageTransition = {
|
||||
type: 'tween',
|
||||
ease: 'anticipate',
|
||||
duration: 0.4
|
||||
};
|
||||
|
||||
const AnimatedRoute = ({ children }) => {
|
||||
return (
|
||||
<motion.div
|
||||
initial="initial"
|
||||
animate="in"
|
||||
exit="out"
|
||||
variants={pageVariants}
|
||||
transition={pageTransition}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AnimatedRoute;
|
||||
0
src/components/AnimatedTable.js
Normal file
0
src/components/AnimatedTable.js
Normal file
0
src/components/FadeInComponent.js
Normal file
0
src/components/FadeInComponent.js
Normal file
25
src/components/Layout/Layout.js
Normal file
25
src/components/Layout/Layout.js
Normal file
@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { Container, Row, Col } from 'react-bootstrap';
|
||||
import Sidebar from './Sidebar';
|
||||
import Navbar from './Navbar';
|
||||
|
||||
const Layout = () => {
|
||||
return (
|
||||
<div className="admin-layout">
|
||||
<Row className="g-0">
|
||||
<Col md={2} className="sidebar">
|
||||
<Sidebar />
|
||||
</Col>
|
||||
<Col md={10}>
|
||||
<Navbar />
|
||||
<Container fluid className="content-area">
|
||||
<Outlet />
|
||||
</Container>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
27
src/components/Layout/Navbar.js
Normal file
27
src/components/Layout/Navbar.js
Normal file
@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import { Navbar, Container, Button } from 'react-bootstrap';
|
||||
import { useAuth } from '../../context/AuthContext';
|
||||
import { FiLogOut } from 'react-icons/fi';
|
||||
|
||||
const AdminNavbar = () => {
|
||||
const { logout } = useAuth();
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
};
|
||||
|
||||
return (
|
||||
<Navbar className="admin-navbar py-2" expand="lg">
|
||||
<Container fluid>
|
||||
<Navbar.Brand>Kings Admin Panel</Navbar.Brand>
|
||||
<div className="ms-auto">
|
||||
<Button variant="outline-secondary" onClick={handleLogout}>
|
||||
<FiLogOut className="me-1" /> Logout
|
||||
</Button>
|
||||
</div>
|
||||
</Container>
|
||||
</Navbar>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminNavbar;
|
||||
34
src/components/Layout/Sidebar.js
Normal file
34
src/components/Layout/Sidebar.js
Normal file
@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { FiHome, FiUsers, FiList } from 'react-icons/fi';
|
||||
|
||||
const Sidebar = () => {
|
||||
return (
|
||||
<div className="sidebar-content">
|
||||
<div className="text-center mb-4">
|
||||
<h4 className="text-white">Kings Admin</h4>
|
||||
</div>
|
||||
<div className="sidebar-links">
|
||||
<NavLink to="/" className={({isActive}) =>
|
||||
`sidebar-link ${isActive ? 'active' : ''}`
|
||||
}>
|
||||
<FiHome className="me-2" /> Dashboard
|
||||
</NavLink>
|
||||
|
||||
<NavLink to="/teams" className={({isActive}) =>
|
||||
`sidebar-link ${isActive ? 'active' : ''}`
|
||||
}>
|
||||
<FiUsers className="me-2" /> Teams
|
||||
</NavLink>
|
||||
|
||||
<NavLink to="/results" className={({isActive}) =>
|
||||
`sidebar-link ${isActive ? 'active' : ''}`
|
||||
}>
|
||||
<FiList className="me-2" /> Results
|
||||
</NavLink>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
44
src/context/AuthContext.js
Normal file
44
src/context/AuthContext.js
Normal file
@ -0,0 +1,44 @@
|
||||
import React, { createContext, useState, useContext, useEffect } from 'react';
|
||||
|
||||
const TOKEN_KEY = process.env.REACT_APP_TOKEN_KEY;
|
||||
const AuthContext = createContext();
|
||||
|
||||
export const useAuth = () => useContext(AuthContext);
|
||||
|
||||
export const AuthProvider = ({ children }) => {
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const checkAuth = () => {
|
||||
const token = localStorage.getItem(TOKEN_KEY);
|
||||
setIsAuthenticated(!!token);
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
checkAuth();
|
||||
}, []);
|
||||
|
||||
const login = (token) => {
|
||||
localStorage.setItem(TOKEN_KEY, token);
|
||||
setIsAuthenticated(true);
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
setIsAuthenticated(false);
|
||||
};
|
||||
|
||||
const value = {
|
||||
isAuthenticated,
|
||||
isLoading,
|
||||
login,
|
||||
logout
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={value}>
|
||||
{!isLoading && children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
21
src/index.js
Normal file
21
src/index.js
Normal file
@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import './styles.css';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { AuthProvider } from './context/AuthContext';
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<BrowserRouter>
|
||||
<AuthProvider>
|
||||
<App />
|
||||
<ToastContainer position="top-right" autoClose={3000} />
|
||||
</AuthProvider>
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>
|
||||
);
|
||||
117
src/pages/Dashboard.js
Normal file
117
src/pages/Dashboard.js
Normal file
@ -0,0 +1,117 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, Row, Col, Alert, Spinner } from 'react-bootstrap';
|
||||
import resultsService from '../services/results';
|
||||
import teamsService from '../services/teams';
|
||||
import { formatDateTimeDisplay } from '../utils/dateUtils';
|
||||
|
||||
const Dashboard = () => {
|
||||
const [todayResults, setTodayResults] = useState([]);
|
||||
const [teamCount, setTeamCount] = useState(0);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const fetchDashboardData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// Get today's results
|
||||
const results = await resultsService.getTodayResults();
|
||||
setTodayResults(results);
|
||||
|
||||
// Get team count
|
||||
const teams = await teamsService.getAll();
|
||||
setTeamCount(teams.length);
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error fetching dashboard data:', err);
|
||||
setError('Failed to load dashboard data. Please try again.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchDashboardData();
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="text-center my-5">
|
||||
<Spinner animation="border" variant="primary" />
|
||||
<p className="mt-2">Loading dashboard data...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="dashboard-container">
|
||||
<h2 className="mb-4">Dashboard</h2>
|
||||
|
||||
{error && <Alert variant="danger">{error}</Alert>}
|
||||
|
||||
<Row className="mb-4">
|
||||
<Col md={6}>
|
||||
<Card className="h-100">
|
||||
<Card.Body>
|
||||
<Card.Title>Team Statistics</Card.Title>
|
||||
<div className="d-flex align-items-center justify-content-center" style={{ height: "150px" }}>
|
||||
<div className="text-center">
|
||||
<h1 className="display-1 text-primary">{teamCount}</h1>
|
||||
<p className="lead">Total Teams</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col md={6}>
|
||||
<Card className="h-100">
|
||||
<Card.Body>
|
||||
<Card.Title>Today's Results</Card.Title>
|
||||
<div className="d-flex align-items-center justify-content-center" style={{ height: "150px" }}>
|
||||
<div className="text-center">
|
||||
<h1 className="display-1 text-primary">{todayResults.length}</h1>
|
||||
<p className="lead">Results Published Today</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Card>
|
||||
<Card.Body>
|
||||
<Card.Title>Today's Results</Card.Title>
|
||||
|
||||
{todayResults.length === 0 ? (
|
||||
<Alert variant="info">No results available for today.</Alert>
|
||||
) : (
|
||||
<Row className="g-3">
|
||||
{todayResults.map((result) => (
|
||||
<Col key={result.id} md={4}>
|
||||
<Card className="result-card">
|
||||
<Card.Body>
|
||||
<h5 className="card-title">{result.team}</h5>
|
||||
<p className="mb-1">
|
||||
<strong>Result:</strong>{' '}
|
||||
{result.visible_result === '-1'
|
||||
? 'Pending'
|
||||
: <span className="text-success fw-bold">{result.visible_result}</span>
|
||||
}
|
||||
</p>
|
||||
<p className="mb-0 text-muted small">
|
||||
<strong>Time:</strong> {formatDateTimeDisplay(result.result_time)}
|
||||
</p>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
)}
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
104
src/pages/Login.js
Normal file
104
src/pages/Login.js
Normal file
@ -0,0 +1,104 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, Form, Button, Alert, Spinner } from 'react-bootstrap';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import authService from '../services/auth';
|
||||
|
||||
const Login = () => {
|
||||
const [accessKey, setAccessKey] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const { login, isAuthenticated } = useAuth();
|
||||
|
||||
// Redirect if already authenticated
|
||||
useEffect(() => {
|
||||
if (isAuthenticated) {
|
||||
navigate('/');
|
||||
}
|
||||
}, [isAuthenticated, navigate]);
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!accessKey.trim() || !password.trim()) {
|
||||
setError('Access key and password are required');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
|
||||
const { token } = await authService.login(accessKey, password);
|
||||
login(token);
|
||||
|
||||
navigate('/');
|
||||
} catch (err) {
|
||||
setError(err.response?.data?.error || 'Failed to login. Please check your credentials.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="auth-page">
|
||||
<Card className="auth-card">
|
||||
<Card.Header className="bg-primary text-white text-center">
|
||||
<h4>Kings Admin Login</h4>
|
||||
</Card.Header>
|
||||
<Card.Body className="p-4">
|
||||
{error && <Alert variant="danger">{error}</Alert>}
|
||||
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label>Access Key</Form.Label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
placeholder="Enter your access key"
|
||||
value={accessKey}
|
||||
onChange={(e) => setAccessKey(e.target.value)}
|
||||
disabled={loading}
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group className="mb-4">
|
||||
<Form.Label>Password</Form.Label>
|
||||
<Form.Control
|
||||
type="password"
|
||||
placeholder="Enter your password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
disabled={loading}
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
className="w-100"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<Spinner
|
||||
as="span"
|
||||
animation="border"
|
||||
size="sm"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
className="me-2"
|
||||
/>
|
||||
Logging in...
|
||||
</>
|
||||
) : 'Login'}
|
||||
</Button>
|
||||
</Form>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
404
src/pages/Results.js
Normal file
404
src/pages/Results.js
Normal file
@ -0,0 +1,404 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Card, Table, Button, Form, Row, Col, Modal, Spinner, Alert } from 'react-bootstrap';
|
||||
import DatePicker from 'react-datepicker';
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
import resultsService from '../services/results';
|
||||
import teamsService from '../services/teams';
|
||||
import { FiEdit, FiTrash2, FiPlus, FiFilter } from 'react-icons/fi';
|
||||
import { toast } from 'react-toastify';
|
||||
import { formatMySQLDateTime, formatDateTimeDisplay } from '../utils/dateUtils';
|
||||
|
||||
const Results = () => {
|
||||
// Main data state
|
||||
const [results, setResults] = useState([]);
|
||||
const [teams, setTeams] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
// Filter state
|
||||
const [selectedTeam, setSelectedTeam] = useState('');
|
||||
|
||||
// Form state
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [formData, setFormData] = useState({
|
||||
team: '',
|
||||
result: '',
|
||||
result_time: new Date()
|
||||
});
|
||||
const [editingResultId, setEditingResultId] = useState(null);
|
||||
const [formLoading, setFormLoading] = useState(false);
|
||||
|
||||
// Delete confirmation
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
const [deletingResultId, setDeletingResultId] = useState(null);
|
||||
|
||||
const fetchTeams = async () => {
|
||||
try {
|
||||
const data = await teamsService.getAll();
|
||||
setTeams(data);
|
||||
} catch (err) {
|
||||
console.error('Error fetching teams:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchResults = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
let data;
|
||||
|
||||
if (selectedTeam) {
|
||||
data = await resultsService.getByTeam(selectedTeam);
|
||||
} else {
|
||||
data = await resultsService.getTodayResults();
|
||||
}
|
||||
|
||||
setResults(data);
|
||||
setError('');
|
||||
} catch (err) {
|
||||
console.error('Error fetching results:', err);
|
||||
setError('Failed to load results. Please try again.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const initData = async () => {
|
||||
await fetchTeams();
|
||||
await fetchResults();
|
||||
};
|
||||
|
||||
initData();
|
||||
}, [selectedTeam]); // Refetch when filter changes
|
||||
|
||||
const handleOpenCreateModal = () => {
|
||||
setEditingResultId(null);
|
||||
setFormData({
|
||||
team: '',
|
||||
result: '',
|
||||
result_time: new Date()
|
||||
});
|
||||
setShowModal(true);
|
||||
};
|
||||
|
||||
const handleOpenEditModal = (result) => {
|
||||
const team = teams.find(t => t.name === result.team || t.name === result.team_name);
|
||||
setEditingResultId(result.id);
|
||||
setFormData({
|
||||
team: team ? team.name : '',
|
||||
result: result.result || result.visible_result,
|
||||
result_time: new Date(result.result_time)
|
||||
});
|
||||
setShowModal(true);
|
||||
};
|
||||
|
||||
const handleOpenDeleteModal = (result) => {
|
||||
setDeletingResultId(result.id);
|
||||
setShowDeleteModal(true);
|
||||
};
|
||||
|
||||
const handleInputChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
const handleDateChange = (date) => {
|
||||
setFormData(prev => ({ ...prev, result_time: date }));
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
setFormLoading(true);
|
||||
|
||||
// Format date for MySQL
|
||||
const formattedData = {
|
||||
...formData,
|
||||
result_time: formatMySQLDateTime(formData.result_time)
|
||||
};
|
||||
|
||||
// Validate the date format
|
||||
if (!/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(formattedData.result_time)) {
|
||||
toast.error('Invalid date format. Should be YYYY-MM-DD HH:MM:SS');
|
||||
setFormLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (editingResultId) {
|
||||
// Update existing result
|
||||
await resultsService.update(editingResultId, formattedData);
|
||||
toast.success('Result updated successfully');
|
||||
} else {
|
||||
// Create new result
|
||||
await resultsService.publish(formattedData);
|
||||
toast.success('Result created successfully');
|
||||
}
|
||||
|
||||
// Reset form and refresh results
|
||||
setShowModal(false);
|
||||
setFormData({
|
||||
team: '',
|
||||
result: '',
|
||||
result_time: new Date()
|
||||
});
|
||||
setEditingResultId(null);
|
||||
await fetchResults();
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error saving result:', err);
|
||||
toast.error(err.response?.data?.error || 'Failed to save result');
|
||||
} finally {
|
||||
setFormLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
setFormLoading(true);
|
||||
await resultsService.delete(deletingResultId);
|
||||
|
||||
toast.success('Result deleted successfully');
|
||||
setShowDeleteModal(false);
|
||||
await fetchResults();
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error deleting result:', err);
|
||||
toast.error(err.response?.data?.error || 'Failed to delete result');
|
||||
} finally {
|
||||
setFormLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFilterChange = useCallback((e) => {
|
||||
setSelectedTeam(e.target.value);
|
||||
}, []);
|
||||
|
||||
if (loading && results.length === 0) {
|
||||
return (
|
||||
<div className="text-center my-5">
|
||||
<Spinner animation="border" variant="primary" />
|
||||
<p className="mt-2">Loading results...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="results-container">
|
||||
<div className="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2>Results Management</h2>
|
||||
<Button variant="primary" onClick={handleOpenCreateModal}>
|
||||
<FiPlus className="me-1" /> Add Result
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{error && <Alert variant="danger">{error}</Alert>}
|
||||
|
||||
<Card className="mb-4">
|
||||
<Card.Body>
|
||||
<Form>
|
||||
<Row>
|
||||
<Col md={6}>
|
||||
<Form.Group>
|
||||
<Form.Label><FiFilter className="me-1" /> Filter by Team</Form.Label>
|
||||
<Form.Select
|
||||
value={selectedTeam}
|
||||
onChange={handleFilterChange}
|
||||
>
|
||||
<option value="">All Teams</option>
|
||||
{teams.map(team => (
|
||||
<option key={team.id} value={team.name}>
|
||||
{team.name}
|
||||
</option>
|
||||
))}
|
||||
</Form.Select>
|
||||
</Form.Group>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
|
||||
<Card className="results-table">
|
||||
<Card.Body>
|
||||
{results.length === 0 ? (
|
||||
<Alert variant="info">No results found with the current filter.</Alert>
|
||||
) : (
|
||||
<Table responsive hover>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Team</th>
|
||||
<th>Result</th>
|
||||
<th>Result Time</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{results.map((result, index) => (
|
||||
<tr key={result.id}>
|
||||
<td>{index + 1}</td>
|
||||
<td>{result.team || result.team_name}</td>
|
||||
<td>
|
||||
{(result.visible_result === '-1' || result.result === '-1') ? (
|
||||
<span className="text-muted">Pending</span>
|
||||
) : (
|
||||
<span className="text-success fw-bold">
|
||||
{result.visible_result || result.result}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td>{formatDateTimeDisplay(result.result_time)}</td>
|
||||
<td>
|
||||
{new Date(result.result_time) > new Date() ? (
|
||||
<span className="badge bg-warning">Scheduled</span>
|
||||
) : (
|
||||
<span className="badge bg-success">Published</span>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
size="sm"
|
||||
className="me-2"
|
||||
onClick={() => handleOpenEditModal(result)}
|
||||
>
|
||||
<FiEdit />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline-danger"
|
||||
size="sm"
|
||||
onClick={() => handleOpenDeleteModal(result)}
|
||||
>
|
||||
<FiTrash2 />
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
)}
|
||||
</Card.Body>
|
||||
</Card>
|
||||
|
||||
{/* Create/Edit Modal */}
|
||||
<Modal show={showModal} onHide={() => setShowModal(false)}>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>{editingResultId ? 'Edit Result' : 'Create New Result'}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Modal.Body>
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label>Team</Form.Label>
|
||||
<Form.Select
|
||||
name="team"
|
||||
value={formData.team}
|
||||
onChange={handleInputChange}
|
||||
disabled={formLoading}
|
||||
required
|
||||
>
|
||||
<option value="">Select a team</option>
|
||||
{teams.map(team => (
|
||||
<option key={team.id} value={team.name}>
|
||||
{team.name}
|
||||
</option>
|
||||
))}
|
||||
</Form.Select>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label>Result</Form.Label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
name="result"
|
||||
placeholder="Enter result"
|
||||
value={formData.result}
|
||||
onChange={handleInputChange}
|
||||
disabled={formLoading}
|
||||
required
|
||||
pattern="[0-9]+"
|
||||
/>
|
||||
<Form.Text className="text-muted">
|
||||
Enter numeric result (e.g., 45).
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label>Result Time</Form.Label>
|
||||
<DatePicker
|
||||
selected={formData.result_time}
|
||||
onChange={handleDateChange}
|
||||
showTimeSelect
|
||||
timeFormat="HH:mm"
|
||||
timeIntervals={1} // Changed from 15 to 1 to allow any minute
|
||||
timeCaption="Time"
|
||||
dateFormat="yyyy-MM-dd HH:mm:ss" // Updated format to match required MySQL format
|
||||
className="form-control"
|
||||
disabled={formLoading}
|
||||
required
|
||||
/>
|
||||
<Form.Text className="text-muted">
|
||||
Date and time when the result should be published (Format: YYYY-MM-DD HH:MM:SS, Indian Standard Time).
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="secondary" onClick={() => setShowModal(false)} disabled={formLoading}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" type="submit" disabled={formLoading}>
|
||||
{formLoading ? (
|
||||
<>
|
||||
<Spinner
|
||||
as="span"
|
||||
animation="border"
|
||||
size="sm"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
className="me-1"
|
||||
/>
|
||||
{editingResultId ? 'Updating...' : 'Creating...'}
|
||||
</>
|
||||
) : (
|
||||
editingResultId ? 'Update Result' : 'Create Result'
|
||||
)}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{/* Delete Confirmation Modal */}
|
||||
<Modal show={showDeleteModal} onHide={() => setShowDeleteModal(false)}>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>Confirm Delete</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
Are you sure you want to delete this result? This action cannot be undone.
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="secondary" onClick={() => setShowDeleteModal(false)} disabled={formLoading}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="danger" onClick={handleDelete} disabled={formLoading}>
|
||||
{formLoading ? (
|
||||
<>
|
||||
<Spinner
|
||||
as="span"
|
||||
animation="border"
|
||||
size="sm"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
className="me-1"
|
||||
/>
|
||||
Deleting...
|
||||
</>
|
||||
) : 'Delete Result'}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Results;
|
||||
253
src/pages/Teams.js
Normal file
253
src/pages/Teams.js
Normal file
@ -0,0 +1,253 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, Table, Button, Form, Modal, Spinner, Alert } from 'react-bootstrap';
|
||||
import teamsService from '../services/teams';
|
||||
import { FiEdit, FiTrash2, FiPlus } from 'react-icons/fi';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
const Teams = () => {
|
||||
const [teams, setTeams] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
// Form state
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [teamName, setTeamName] = useState('');
|
||||
const [editingTeamId, setEditingTeamId] = useState(null);
|
||||
const [formLoading, setFormLoading] = useState(false);
|
||||
|
||||
// Delete confirmation
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
const [deletingTeamId, setDeletingTeamId] = useState(null);
|
||||
const [deletingTeamName, setDeletingTeamName] = useState('');
|
||||
|
||||
const fetchTeams = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await teamsService.getAll();
|
||||
setTeams(data);
|
||||
setError('');
|
||||
} catch (err) {
|
||||
console.error('Error fetching teams:', err);
|
||||
setError('Failed to load teams. Please try again.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchTeams();
|
||||
}, []);
|
||||
|
||||
const handleOpenCreateModal = () => {
|
||||
setEditingTeamId(null);
|
||||
setTeamName('');
|
||||
setShowModal(true);
|
||||
};
|
||||
|
||||
const handleOpenEditModal = (team) => {
|
||||
setEditingTeamId(team.id);
|
||||
setTeamName(team.name);
|
||||
setShowModal(true);
|
||||
};
|
||||
|
||||
const handleOpenDeleteModal = (team) => {
|
||||
setDeletingTeamId(team.id);
|
||||
setDeletingTeamName(team.name);
|
||||
setShowDeleteModal(true);
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
if (!teamName.trim()) {
|
||||
toast.error('Team name is required');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setFormLoading(true);
|
||||
|
||||
if (editingTeamId) {
|
||||
// Update existing team
|
||||
await teamsService.update(editingTeamId, { name: teamName });
|
||||
toast.success('Team updated successfully');
|
||||
} else {
|
||||
// Create new team
|
||||
await teamsService.create({ name: teamName });
|
||||
toast.success('Team created successfully');
|
||||
}
|
||||
|
||||
// Reset form and refresh teams
|
||||
setShowModal(false);
|
||||
setTeamName('');
|
||||
setEditingTeamId(null);
|
||||
await fetchTeams();
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error saving team:', err);
|
||||
toast.error(err.response?.data?.error || 'Failed to save team');
|
||||
} finally {
|
||||
setFormLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
setFormLoading(true);
|
||||
await teamsService.delete(deletingTeamId);
|
||||
|
||||
toast.success('Team deleted successfully');
|
||||
setShowDeleteModal(false);
|
||||
await fetchTeams();
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error deleting team:', err);
|
||||
toast.error(err.response?.data?.error || 'Failed to delete team');
|
||||
} finally {
|
||||
setFormLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="text-center my-5">
|
||||
<Spinner animation="border" variant="primary" />
|
||||
<p className="mt-2">Loading teams...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="teams-container">
|
||||
<div className="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2>Teams Management</h2>
|
||||
<Button variant="primary" onClick={handleOpenCreateModal}>
|
||||
<FiPlus className="me-1" /> Add Team
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{error && <Alert variant="danger">{error}</Alert>}
|
||||
|
||||
<Card className="teams-table">
|
||||
<Card.Body>
|
||||
{teams.length === 0 ? (
|
||||
<Alert variant="info">No teams found. Create a new team to get started.</Alert>
|
||||
) : (
|
||||
<Table responsive hover>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Team Name</th>
|
||||
<th>Created At</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{teams.map((team, index) => (
|
||||
<tr key={team.id}>
|
||||
<td>{index + 1}</td>
|
||||
<td>{team.name}</td>
|
||||
<td>{new Date(team.created_at).toLocaleString('en-IN', {timeZone: 'Asia/Kolkata'})}</td>
|
||||
<td>
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
size="sm"
|
||||
className="me-2"
|
||||
onClick={() => handleOpenEditModal(team)}
|
||||
>
|
||||
<FiEdit />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline-danger"
|
||||
size="sm"
|
||||
onClick={() => handleOpenDeleteModal(team)}
|
||||
>
|
||||
<FiTrash2 />
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
)}
|
||||
</Card.Body>
|
||||
</Card>
|
||||
|
||||
{/* Create/Edit Modal */}
|
||||
<Modal show={showModal} onHide={() => setShowModal(false)}>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>{editingTeamId ? 'Edit Team' : 'Create New Team'}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Modal.Body>
|
||||
<Form.Group>
|
||||
<Form.Label>Team Name</Form.Label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
placeholder="Enter team name"
|
||||
value={teamName}
|
||||
onChange={(e) => setTeamName(e.target.value)}
|
||||
disabled={formLoading}
|
||||
required
|
||||
/>
|
||||
</Form.Group>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="secondary" onClick={() => setShowModal(false)} disabled={formLoading}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" type="submit" disabled={formLoading}>
|
||||
{formLoading ? (
|
||||
<>
|
||||
<Spinner
|
||||
as="span"
|
||||
animation="border"
|
||||
size="sm"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
className="me-1"
|
||||
/>
|
||||
{editingTeamId ? 'Updating...' : 'Creating...'}
|
||||
</>
|
||||
) : (
|
||||
editingTeamId ? 'Update Team' : 'Create Team'
|
||||
)}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{/* Delete Confirmation Modal */}
|
||||
<Modal show={showDeleteModal} onHide={() => setShowDeleteModal(false)}>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>Confirm Delete</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
Are you sure you want to delete the team <strong>{deletingTeamName}</strong>?
|
||||
This action cannot be undone.
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="secondary" onClick={() => setShowDeleteModal(false)} disabled={formLoading}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="danger" onClick={handleDelete} disabled={formLoading}>
|
||||
{formLoading ? (
|
||||
<>
|
||||
<Spinner
|
||||
as="span"
|
||||
animation="border"
|
||||
size="sm"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
className="me-1"
|
||||
/>
|
||||
Deleting...
|
||||
</>
|
||||
) : 'Delete Team'}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Teams;
|
||||
10
src/services/auth.js
Normal file
10
src/services/auth.js
Normal file
@ -0,0 +1,10 @@
|
||||
import http from '../utils/http';
|
||||
|
||||
const authService = {
|
||||
login: async (accessKey, password) => {
|
||||
const response = await http.post('/admin/login', { accessKey, password });
|
||||
return response.data;
|
||||
}
|
||||
};
|
||||
|
||||
export default authService;
|
||||
30
src/services/results.js
Normal file
30
src/services/results.js
Normal file
@ -0,0 +1,30 @@
|
||||
import http from '../utils/http';
|
||||
|
||||
const resultsService = {
|
||||
getByTeam: async (team) => {
|
||||
const response = await http.get(`/admin/results?team=${team}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
publish: async (resultData) => {
|
||||
const response = await http.post('/admin/results', resultData);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
update: async (id, resultData) => {
|
||||
const response = await http.put(`/admin/results/${id}`, resultData);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
delete: async (id) => {
|
||||
const response = await http.delete(`/admin/results/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getTodayResults: async () => {
|
||||
const response = await http.get('/api/today');
|
||||
return response.data;
|
||||
}
|
||||
};
|
||||
|
||||
export default resultsService;
|
||||
25
src/services/teams.js
Normal file
25
src/services/teams.js
Normal file
@ -0,0 +1,25 @@
|
||||
import http from '../utils/http';
|
||||
|
||||
const teamsService = {
|
||||
getAll: async () => {
|
||||
const response = await http.get('/api/teams');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
create: async (teamData) => {
|
||||
const response = await http.post('/admin/teams', teamData);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
update: async (id, teamData) => {
|
||||
const response = await http.put(`/admin/teams/${id}`, teamData);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
delete: async (id) => {
|
||||
const response = await http.delete(`/admin/teams/${id}`);
|
||||
return response.data;
|
||||
}
|
||||
};
|
||||
|
||||
export default teamsService;
|
||||
171
src/styles.css
Normal file
171
src/styles.css
Normal file
@ -0,0 +1,171 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background-color: #f5f8fa;
|
||||
}
|
||||
|
||||
/* Add a CSS animation for page transitions */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.content-area {
|
||||
padding: 20px;
|
||||
animation: fadeIn 0.4s ease-out;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
min-height: 100vh;
|
||||
background-color: #343a40;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.sidebar-link {
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
text-decoration: none;
|
||||
padding: 10px 15px;
|
||||
display: block;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.sidebar-link:hover, .sidebar-link.active {
|
||||
color: white;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
.admin-navbar {
|
||||
background-color: #fff;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.auth-page {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.auth-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.spinner-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
.result-card {
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.result-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.teams-table, .results-table {
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* Button animations */
|
||||
.btn {
|
||||
transition: all 0.2s ease !important;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
/* Animate form controls */
|
||||
.form-control, .form-select {
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
/* Modal animations */
|
||||
.modal {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
transform: scale(0.8);
|
||||
opacity: 0;
|
||||
transition: transform 0.3s ease, opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.modal.show .modal-content {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Table row hover effect */
|
||||
.table tbody tr {
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.table tbody tr:hover {
|
||||
background-color: rgba(0, 123, 255, 0.05);
|
||||
}
|
||||
|
||||
/* Badge animations */
|
||||
.badge {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.badge:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* Animated loading spinner */
|
||||
@keyframes pulse {
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(1.1); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
.spinner-border {
|
||||
animation: spinner-border 0.75s linear infinite, pulse 2s ease infinite !important;
|
||||
}
|
||||
|
||||
/* Dashboard stats animation */
|
||||
.display-1.text-primary {
|
||||
transition: all 0.5s ease;
|
||||
animation: fadeIn 0.8s ease-out;
|
||||
}
|
||||
|
||||
.display-1.text-primary:hover {
|
||||
transform: scale(1.05);
|
||||
text-shadow: 0 0 10px rgba(0, 123, 255, 0.5);
|
||||
}
|
||||
11
src/utils/PrivateRoute.js
Normal file
11
src/utils/PrivateRoute.js
Normal file
@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import { Navigate, Outlet } from 'react-router-dom';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
|
||||
const PrivateRoute = () => {
|
||||
const { isAuthenticated } = useAuth();
|
||||
|
||||
return isAuthenticated ? <Outlet /> : <Navigate to="/login" />;
|
||||
};
|
||||
|
||||
export default PrivateRoute;
|
||||
68
src/utils/dateUtils.js
Normal file
68
src/utils/dateUtils.js
Normal file
@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Date utility functions for the admin panel
|
||||
* All functions consider Indian Standard Time (IST/UTC+5:30)
|
||||
*/
|
||||
|
||||
// Format a date to YYYY-MM-DD format
|
||||
export const formatDate = (date) => {
|
||||
const options = { timeZone: 'Asia/Kolkata' };
|
||||
const dateObj = date ? new Date(date) : new Date();
|
||||
const year = dateObj.toLocaleString('en-US', { year: 'numeric', ...options });
|
||||
const month = dateObj.toLocaleString('en-US', { month: '2-digit', ...options });
|
||||
const day = dateObj.toLocaleString('en-US', { day: '2-digit', ...options });
|
||||
return `${year}-${month}-${day}`;
|
||||
};
|
||||
|
||||
// Format date and time to display friendly format
|
||||
export const formatDateTimeDisplay = (dateTimeStr) => {
|
||||
const options = {
|
||||
timeZone: 'Asia/Kolkata',
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
};
|
||||
|
||||
const date = new Date(dateTimeStr);
|
||||
return date.toLocaleString('en-US', options);
|
||||
};
|
||||
|
||||
// Update format to ensure proper MySQL datetime format (YYYY-MM-DD HH:MM:SS)
|
||||
export const formatMySQLDateTime = (date) => {
|
||||
// Create a date object in IST timezone
|
||||
const dateObj = date ? new Date(date) : new Date();
|
||||
|
||||
// Get the India time string from the date
|
||||
const options = { timeZone: 'Asia/Kolkata' };
|
||||
const indianTime = dateObj.toLocaleString('en-US', options);
|
||||
|
||||
// Parse the Indian time string into a new Date object
|
||||
const istDate = new Date(indianTime);
|
||||
|
||||
// Format with leading zeros
|
||||
const year = istDate.getFullYear();
|
||||
const month = String(istDate.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(istDate.getDate()).padStart(2, '0');
|
||||
const hours = String(istDate.getHours()).padStart(2, '0');
|
||||
const minutes = String(istDate.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(istDate.getSeconds()).padStart(2, '0');
|
||||
|
||||
// Return in MySQL format
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
};
|
||||
|
||||
// Get current date in IST
|
||||
export const getCurrentIndianDate = () => {
|
||||
return formatDate();
|
||||
};
|
||||
|
||||
// Check if a given time is in the future (IST)
|
||||
export const isTimeInFuture = (dateTime) => {
|
||||
const now = new Date();
|
||||
const options = { timeZone: 'Asia/Kolkata' };
|
||||
const nowInIST = new Date(now.toLocaleString('en-US', options));
|
||||
const checkTime = new Date(dateTime);
|
||||
|
||||
return checkTime > nowInIST;
|
||||
};
|
||||
37
src/utils/http.js
Normal file
37
src/utils/http.js
Normal file
@ -0,0 +1,37 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const API_URL = process.env.REACT_APP_API_URL;
|
||||
const TOKEN_KEY = process.env.REACT_APP_TOKEN_KEY;
|
||||
|
||||
const http = axios.create({
|
||||
baseURL: API_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
// Add token to requests if available
|
||||
http.interceptors.request.use(
|
||||
config => {
|
||||
const token = localStorage.getItem(TOKEN_KEY);
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
error => Promise.reject(error)
|
||||
);
|
||||
|
||||
// Handle 401 responses
|
||||
http.interceptors.response.use(
|
||||
response => response,
|
||||
error => {
|
||||
if (error.response && error.response.status === 401) {
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
window.location = '/login';
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default http;
|
||||
Loading…
x
Reference in New Issue
Block a user