admin panel done

This commit is contained in:
Naveen Kumar 2025-03-21 01:03:21 +05:30
commit 4726582b89
27 changed files with 21324 additions and 0 deletions

9
.gitignore vendored Normal file
View 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

File diff suppressed because it is too large Load Diff

46
package.json Normal file
View 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
View 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
View 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
View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

48
src/App.js Normal file
View 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;

View File

View 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;

View File

View File

View 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;

View 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;

View 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;

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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;