Initial commit — Dine360 Ads Frontend
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
a78b168b60
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
6
.htaccess
Normal file
6
.htaccess
Normal file
@ -0,0 +1,6 @@
|
||||
<IfModule mod_rewrite.c>
|
||||
Options -MultiViews
|
||||
RewriteEngine On
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteRule ^ index.html [QSA,L]
|
||||
</IfModule>
|
||||
12
README.md
Normal file
12
README.md
Normal file
@ -0,0 +1,12 @@
|
||||
# React + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend using TypeScript and enable type-aware lint rules. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
|
||||
33
eslint.config.js
Normal file
33
eslint.config.js
Normal file
@ -0,0 +1,33 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
|
||||
export default [
|
||||
{ ignores: ['dist'] },
|
||||
{
|
||||
files: ['**/*.{js,jsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
ecmaFeatures: { jsx: true },
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...js.configs.recommended.rules,
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
13
index.html
Normal file
13
index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/logo.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Dine 360 Ads</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
3816
package-lock.json
generated
Normal file
3816
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
40
package.json
Normal file
40
package.json
Normal file
@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"@tailwindcss/vite": "^4.0.14",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"axios": "^1.8.3",
|
||||
"framer-motion": "^12.9.4",
|
||||
"lucide-react": "^0.484.0",
|
||||
"postcss": "^8.5.3",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-router-dom": "^7.3.0",
|
||||
"react-select": "^5.10.1",
|
||||
"react-toastify": "^11.0.5",
|
||||
"sweetalert2": "^11.17.2",
|
||||
"tailwindcss": "^4.0.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.21.0",
|
||||
"@types/react": "^19.0.10",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"eslint": "^9.21.0",
|
||||
"eslint-plugin-react-hooks": "^5.1.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"globals": "^15.15.0",
|
||||
"vite": "^6.2.0"
|
||||
}
|
||||
}
|
||||
1765
public/canada_cities.json
Normal file
1765
public/canada_cities.json
Normal file
File diff suppressed because it is too large
Load Diff
BIN
public/logo.png
Normal file
BIN
public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
53
src/API/api.js
Normal file
53
src/API/api.js
Normal file
@ -0,0 +1,53 @@
|
||||
import axios from "axios";
|
||||
|
||||
var API_BASE_URL = "https://ads.dine360ads.com/api/api";
|
||||
//API_BASE_URL = "http://82.25.95.117:5000/api";
|
||||
//API_BASE_URL = "http://localhost:5000/api"; // Update as needed
|
||||
|
||||
export const api = {
|
||||
get: async (endpoint) => {
|
||||
// Show loading
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE_URL}${endpoint}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error("GET Error:", error);
|
||||
throw error;
|
||||
} finally {
|
||||
// Hide loading
|
||||
}
|
||||
},
|
||||
|
||||
post: async (endpoint, data) => {
|
||||
try {
|
||||
const response = await axios.post(`${API_BASE_URL}${endpoint}`, data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error("POST Error:", error);
|
||||
throw error;
|
||||
} finally {
|
||||
}
|
||||
},
|
||||
|
||||
put: async (endpoint, data) => {
|
||||
try {
|
||||
const response = await axios.put(`${API_BASE_URL}${endpoint}`, data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error("PUT Error:", error);
|
||||
throw error;
|
||||
} finally {
|
||||
}
|
||||
},
|
||||
|
||||
delete: async (endpoint) => {
|
||||
try {
|
||||
const response = await axios.delete(`${API_BASE_URL}${endpoint}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error("DELETE Error:", error);
|
||||
throw error;
|
||||
} finally {
|
||||
}
|
||||
},
|
||||
};
|
||||
0
src/App.css
Normal file
0
src/App.css
Normal file
105
src/App.jsx
Normal file
105
src/App.jsx
Normal file
@ -0,0 +1,105 @@
|
||||
import { BrowserRouter as Router, Routes, Route, useLocation, Navigate } from "react-router-dom";
|
||||
import NotFound from "./Pages/NotFound";
|
||||
import Login from "./Pages/Login";
|
||||
import AdminDashboard from "./Pages/AdminDashboard";
|
||||
import AdsPage from "./Pages/Ads";
|
||||
import PartnersPage from "./Pages/PartnersPage";
|
||||
import AddPartner from "./Pages/AddPartner";
|
||||
import ManagePartner from "./Pages/ManagePartner";
|
||||
import ClientsPage from "./Pages/ClientsPage";
|
||||
import AddClient from "./Pages/AddClient";
|
||||
import ManageClient from "./Pages/ManageClient";
|
||||
import ClientPartnerMapping from "./Pages/ClientPartnerMapping";
|
||||
import NewAdConfiguration from "./Pages/NewAdConfiguration";
|
||||
import Header from "./Components/Header";
|
||||
import ManageFilesOrder from "./Pages/ManageFilesOrderPage";
|
||||
import SettingsPage from "./Pages/SettingsPage";
|
||||
import YouTubePlayer from "./Pages/YT";
|
||||
|
||||
const Layout = () => {
|
||||
const location = useLocation();
|
||||
const getLoginState = () => {
|
||||
const storedLogin = JSON.parse(localStorage.getItem('loggedIn'));
|
||||
if (storedLogin && storedLogin.value) {
|
||||
const expirationTime = 1 * 60 * 60 * 1000; // 1 hour expiration
|
||||
if (Date.now() - storedLogin.timestamp < expirationTime) {
|
||||
return true;
|
||||
} else {
|
||||
localStorage.removeItem('loggedIn'); // Expired, remove
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const isLoggedIn = getLoginState();
|
||||
|
||||
const hideHeaderRoutes = ["/", "/login"];
|
||||
const isAdsPage = location.pathname.startsWith("/ads/");
|
||||
|
||||
|
||||
|
||||
// App.js
|
||||
const ProtectedRoute = ({ children }) => {
|
||||
const location = useLocation();
|
||||
const storedLogin = localStorage.getItem('loggedIn');
|
||||
let isLoggedIn = false; // Default to false
|
||||
|
||||
if (storedLogin) {
|
||||
try {
|
||||
const parsedLogin = JSON.parse(storedLogin);
|
||||
isLoggedIn = parsedLogin.value; // Access the 'value' property
|
||||
} catch (error) {
|
||||
console.error("Error parsing login data:", error);
|
||||
// Handle parsing error (e.g., clear localStorage)
|
||||
localStorage.removeItem('loggedIn');
|
||||
}
|
||||
}
|
||||
|
||||
console.log(isLoggedIn); // Log the boolean value
|
||||
|
||||
const isStaticFile = location.pathname.endsWith('.css') || location.pathname.endsWith('.js');
|
||||
|
||||
if (isStaticFile) {
|
||||
return children; // Allow access to static files
|
||||
}
|
||||
|
||||
return isLoggedIn ? children : <Navigate to="/login" />;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{!hideHeaderRoutes.includes(location.pathname) && !isAdsPage && <Header />}
|
||||
<Routes>
|
||||
<Route path="/" element={<Login />} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/yt" element={<YouTubePlayer />} />
|
||||
<Route path="/ads/:partnerid/:screenName" element={<AdsPage />} />
|
||||
|
||||
<Route path="/admin-dashboard" element={<ProtectedRoute><AdminDashboard /></ProtectedRoute>} />
|
||||
<Route path="/partners" element={<ProtectedRoute><PartnersPage /></ProtectedRoute>} />
|
||||
<Route path="/add-partner" element={<ProtectedRoute><AddPartner /></ProtectedRoute>} />
|
||||
<Route path="/manage-partner/:id" element={<ProtectedRoute><ManagePartner /></ProtectedRoute>} />
|
||||
<Route path="/clients" element={<ProtectedRoute><ClientsPage /></ProtectedRoute>} />
|
||||
<Route path="/add-client" element={<ProtectedRoute><AddClient /></ProtectedRoute>} />
|
||||
<Route path="/manage-client/:id" element={<ProtectedRoute><ManageClient /></ProtectedRoute>} />
|
||||
<Route path="/Client-Partner-Mapping" element={<ProtectedRoute><ClientPartnerMapping /></ProtectedRoute>} />
|
||||
<Route path="/new-ad-configuration" element={<ProtectedRoute><NewAdConfiguration /></ProtectedRoute>} />
|
||||
<Route path="/ads-order-configuration/:id" element={<ProtectedRoute><ManageFilesOrder /></ProtectedRoute>} />
|
||||
<Route path="/settings" element={<ProtectedRoute><SettingsPage /></ProtectedRoute>} />
|
||||
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Router>
|
||||
<Layout />
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
95
src/Components/AdminDashboardComponent.jsx
Normal file
95
src/Components/AdminDashboardComponent.jsx
Normal file
@ -0,0 +1,95 @@
|
||||
import React from 'react'
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { FaPlus, FaCircle } from "react-icons/fa";
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
export default function AdminDashboardComponent() {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
|
||||
<>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
|
||||
<motion.div
|
||||
className="bg-gray-800 p-6 rounded-lg shadow-lg text-center cursor-pointer hover:bg-gray-700 transition flex items-center justify-center"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={() => navigate("/Partners")}
|
||||
>
|
||||
<div
|
||||
className="bg-cover bg-center h-40 rounded-lg filter blur-sm"
|
||||
style={{
|
||||
backgroundImage: `url('///')`,
|
||||
}}
|
||||
></div>
|
||||
|
||||
<div>
|
||||
{/* <FaPlus className="text-5xl text-blue-500 mx-auto mb-4" /> */}
|
||||
<p className="text-lg font-semibold">Partners</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="bg-gray-800 p-6 rounded-lg shadow-lg text-center cursor-pointer hover:bg-gray-700 transition flex items-center justify-center"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={() => navigate("/Clients")}
|
||||
>
|
||||
<div
|
||||
className="bg-cover bg-center h-40 rounded-lg filter blur-sm"
|
||||
style={{
|
||||
backgroundImage: `url('///')`,
|
||||
}}
|
||||
></div>
|
||||
|
||||
|
||||
<div>
|
||||
{/* <FaPlus className="text-5xl text-blue-500 mx-auto mb-4" /> */}
|
||||
<p className="text-lg font-semibold">Clients</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="bg-gray-800 p-6 rounded-lg shadow-lg text-center cursor-pointer hover:bg-gray-700 transition flex items-center justify-center"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={() => navigate("/Client-Partner-Mapping")}
|
||||
>
|
||||
<div
|
||||
className="bg-cover bg-center h-40 rounded-lg filter blur-sm"
|
||||
style={{
|
||||
backgroundImage: `url('///')`,
|
||||
}}
|
||||
></div>
|
||||
|
||||
<div>
|
||||
{/* <FaPlus className="text-5xl text-blue-500 mx-auto mb-4" /> */}
|
||||
<p className="text-lg font-semibold">Ads Configuration</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="bg-gray-800 p-6 rounded-lg shadow-lg text-center cursor-pointer hover:bg-gray-700 transition flex items-center justify-center"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={() => navigate("/Settings")}
|
||||
>
|
||||
<div
|
||||
className="bg-cover bg-center h-40 rounded-lg filter blur-sm"
|
||||
style={{
|
||||
backgroundImage: `url('///')`,
|
||||
}}
|
||||
></div>
|
||||
|
||||
<div>
|
||||
{/* <FaPlus className="text-5xl text-blue-500 mx-auto mb-4" /> */}
|
||||
<p className="text-lg font-semibold">Configure General Settings</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
</div>
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
87
src/Components/ClientsPageCardComponent.jsx
Normal file
87
src/Components/ClientsPageCardComponent.jsx
Normal file
@ -0,0 +1,87 @@
|
||||
import React from 'react'
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { FaPlus, FaCircle } from "react-icons/fa";
|
||||
import { api } from "../API/api";
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useLoading } from '../Context/LoadingContext';
|
||||
|
||||
export default function ClientsCardComponent({ Data, setUpd }) {
|
||||
const navigate = useNavigate();
|
||||
const { setLoading } = useLoading();
|
||||
|
||||
|
||||
const handleDelete = async (clientid) => {
|
||||
console.log(clientid)
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await api.delete(`/admin/delete-client/${clientid}`);
|
||||
console.log(response);
|
||||
setUpd(clientid)
|
||||
} catch (error) {
|
||||
console.error("Error fetching files:", error);
|
||||
}
|
||||
setLoading(false)
|
||||
};
|
||||
|
||||
|
||||
|
||||
return (
|
||||
|
||||
<>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
|
||||
<motion.div
|
||||
className="bg-gray-800 p-6 rounded-lg shadow-lg text-center cursor-pointer hover:bg-gray-700 transition flex items-center justify-center"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={() => navigate("/add-client")}
|
||||
>
|
||||
<div>
|
||||
<FaPlus className="text-5xl text-blue-500 mx-auto mb-4" />
|
||||
<p className="text-lg font-semibold">Add New Client</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
{Data?.map((client) => (
|
||||
<motion.div
|
||||
key={client.transid}
|
||||
className="bg-gray-800 p-6 rounded-lg shadow-lg relative cursor-pointer hover:bg-gray-700 transition"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0 }}
|
||||
whileHover={{ scale: 1.03 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
onClick={() => navigate(`/manage-client/${client.transid}`)}
|
||||
>
|
||||
|
||||
{/* Red Delete Button at Top Right Corner */}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation(); // Prevents triggering the card's onClick event
|
||||
if (confirm("Are you sure you want to delete this client?")) {
|
||||
handleDelete(client.transid);
|
||||
}
|
||||
}}
|
||||
className="absolute top-2 right-2 text-red-500 hover:text-red-700 text-xl"
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
|
||||
<div
|
||||
className="bg-cover bg-center h-40 rounded-lg filter blur-sm"
|
||||
style={{
|
||||
backgroundImage: `url('///')`,
|
||||
}}
|
||||
></div>
|
||||
|
||||
<div className="mt-4">
|
||||
<h2 className="text-xl font-bold">{client.name}</h2>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
74
src/Components/Header.jsx
Normal file
74
src/Components/Header.jsx
Normal file
@ -0,0 +1,74 @@
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
const Header = () => {
|
||||
return (
|
||||
<header className="bg-gradient-to-r from-gray-900 via-gray-800 to-gray-700 text-white shadow-md sticky top-0 z-50">
|
||||
<div className="container mx-auto flex items-center justify-between px-6 py-3">
|
||||
|
||||
{/* Logo and Title */}
|
||||
<div className="flex items-center space-x-3">
|
||||
<img src="/logo.png" alt="Logo" className="h-10 w-10 rounded-full" />
|
||||
<h1 className="text-xl font-bold tracking-wide text-gray-100">
|
||||
Dine 360 Ads
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* Navigation Menu */}
|
||||
<nav>
|
||||
<ul className="flex space-x-6">
|
||||
<li>
|
||||
<Link
|
||||
to="/admin-dashboard"
|
||||
className="text-gray-300 hover:text-white transition duration-300"
|
||||
>
|
||||
Dashboard
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
to="/partners"
|
||||
className="text-gray-300 hover:text-white transition duration-300"
|
||||
>
|
||||
Partners
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
to="/clients"
|
||||
className="text-gray-300 hover:text-white transition duration-300"
|
||||
>
|
||||
Clients
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
to="Client-Partner-Mapping"
|
||||
className="text-gray-300 hover:text-white transition duration-300"
|
||||
>
|
||||
Ads Configuration
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
to="Settings"
|
||||
className="text-gray-300 hover:text-white transition duration-300"
|
||||
>
|
||||
General Settings
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
{/* Logout Button Styled as Link */}
|
||||
<Link
|
||||
to="/login"
|
||||
className="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-lg transition duration-300 text-center"
|
||||
>
|
||||
Logout
|
||||
</Link>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
57
src/Components/HoveringCardComponent.jsx
Normal file
57
src/Components/HoveringCardComponent.jsx
Normal file
@ -0,0 +1,57 @@
|
||||
import React from 'react'
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { FaPlus, FaCircle } from "react-icons/fa";
|
||||
import { api } from "../API/api";
|
||||
|
||||
export default function HoveringCardComponent({ Data }) {
|
||||
return (
|
||||
|
||||
<>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
|
||||
<motion.div
|
||||
className="bg-gray-800 p-6 rounded-lg shadow-lg text-center cursor-pointer hover:bg-gray-700 transition flex items-center justify-center"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={() => navigate("/add-client")}
|
||||
>
|
||||
<div>
|
||||
<FaPlus className="text-5xl text-blue-500 mx-auto mb-4" />
|
||||
<p className="text-lg font-semibold">Add New Client</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
{Data?.map((client) => (
|
||||
<motion.div
|
||||
key={client.id}
|
||||
className="bg-gray-800 p-6 rounded-lg shadow-lg relative cursor-pointer hover:bg-gray-700 transition"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0 }}
|
||||
whileHover={{ scale: 1.03 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
onClick={() => navigate(`/manage-client/${client.id}`)}
|
||||
>
|
||||
<div
|
||||
className="bg-cover bg-center h-40 rounded-lg filter blur-sm"
|
||||
style={{
|
||||
backgroundImage: `url('///')`,
|
||||
}}
|
||||
></div>
|
||||
|
||||
<div className="mt-4">
|
||||
<h2 className="text-xl font-bold">{client.name}</h2>
|
||||
<p className="text-gray-400">🕒 {client.open_time} - {client.close_time}</p>
|
||||
|
||||
<div className="mt-2 ml-1 flex items-center">
|
||||
<FaCircle className={`mr-2 ${client.is_active ? "text-green-400" : "text-red-400"}`} />
|
||||
<span> {client.is_active ? "Active" : "Inactive"}</span>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
20
src/Components/LoadingScreen.jsx
Normal file
20
src/Components/LoadingScreen.jsx
Normal file
@ -0,0 +1,20 @@
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
const LoadingScreen = () => {
|
||||
return (
|
||||
<motion.div
|
||||
className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
>
|
||||
<motion.div
|
||||
className="w-20 h-20 border-t-4 border-white border-solid rounded-full animate-spin"
|
||||
animate={{ rotate: 360 }}
|
||||
transition={{ repeat: Infinity, duration: 1 }}
|
||||
/>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoadingScreen;
|
||||
87
src/Components/MappingComponent.jsx
Normal file
87
src/Components/MappingComponent.jsx
Normal file
@ -0,0 +1,87 @@
|
||||
import React from 'react'
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { FaPlus, FaCircle } from "react-icons/fa";
|
||||
import { api } from "../API/api";
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useLoading } from '../Context/LoadingContext';
|
||||
|
||||
export default function MappingComponent({ Data, setUpd }) {
|
||||
const navigate = useNavigate();
|
||||
const { setLoading } = useLoading();
|
||||
|
||||
|
||||
const handleDelete = async (clientid) => {
|
||||
console.log(clientid)
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await api.delete(`/admin/delete-client/${clientid}`);
|
||||
console.log(response);
|
||||
setUpd(clientid)
|
||||
} catch (error) {
|
||||
console.error("Error fetching files:", error);
|
||||
}
|
||||
setLoading(false)
|
||||
};
|
||||
|
||||
|
||||
|
||||
return (
|
||||
|
||||
<>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
|
||||
<motion.div
|
||||
className="bg-gray-800 p-6 rounded-lg shadow-lg text-center cursor-pointer hover:bg-gray-700 transition flex items-center justify-center"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={() => navigate("/new-ad-configuration")}
|
||||
>
|
||||
<div>
|
||||
<FaPlus className="text-5xl text-blue-500 mx-auto mb-4" />
|
||||
<p className="text-lg font-semibold">Add / Edit Ad Configuration</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
{Data?.map((client) => (
|
||||
<motion.div
|
||||
key={client.transid}
|
||||
className="bg-gray-800 p-6 rounded-lg shadow-lg relative cursor-pointer hover:bg-gray-700 transition"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0 }}
|
||||
whileHover={{ scale: 1.03 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
onClick={() => navigate(`/ads-order-configuration/${client.transid}`)}
|
||||
>
|
||||
|
||||
|
||||
{/* <button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation(); // Prevents triggering the card's onClick event
|
||||
if (confirm("Are you sure you want to delete this client?")) {
|
||||
handleDelete(client.transid);
|
||||
}
|
||||
}}
|
||||
className="absolute top-2 right-2 text-red-500 hover:text-red-700 text-xl"
|
||||
>
|
||||
🗑️
|
||||
</button> */}
|
||||
|
||||
<div
|
||||
className="bg-cover bg-center h-40 rounded-lg filter blur-sm"
|
||||
style={{
|
||||
backgroundImage: `url('///')`,
|
||||
}}
|
||||
></div>
|
||||
|
||||
<div className="mt-4">
|
||||
<h2 className="text-xl font-bold">{client.name}</h2>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
74
src/Components/NewMappingCardComponent copy.jsx
Normal file
74
src/Components/NewMappingCardComponent copy.jsx
Normal file
@ -0,0 +1,74 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useLoading } from "../Context/LoadingContext";
|
||||
import NewMappingSubPartnersComponent from "./NewMappingSubPartnersComponent";
|
||||
import { api } from "../API/api"
|
||||
export default function NewMappingCardComponent({ Data, setUpd }) {
|
||||
const navigate = useNavigate();
|
||||
const { setLoading } = useLoading();
|
||||
const [activeCard, setActiveCard] = useState(null); // Track active sub-card
|
||||
const [partners, setPartners] = useState(null);
|
||||
|
||||
const toggleSubCard = (transid) => {
|
||||
setActiveCard(activeCard === transid ? null : transid); // Toggle logic
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const fetchClients = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await api.get("/admin/partners");
|
||||
setPartners(response);
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error fetching clients:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchClients();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-1 lg:grid-cols-1 gap-6">
|
||||
{Data?.map((client,i) => (
|
||||
<div key={client.transid} className="relative">
|
||||
<motion.div
|
||||
className="bg-gray-800 p-6 rounded-lg shadow-lg cursor-pointer hover:bg-gray-700 transition"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0 }}
|
||||
whileHover={{ scale: 1.03 }}
|
||||
// whileTap={{ scale: 0.98 }}
|
||||
onClick={() => toggleSubCard(client.transid)}
|
||||
>
|
||||
<div
|
||||
className="bg-cover bg-center h-40 rounded-lg filter blur-sm"
|
||||
style={{ backgroundImage: "url('///')" }}
|
||||
></div>
|
||||
<div className="mt-4">
|
||||
<h2 className="text-xl font-bold"> {client.name}</h2>
|
||||
</div>
|
||||
|
||||
{activeCard === client.transid && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -10 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="mt-2"
|
||||
>
|
||||
<NewMappingSubPartnersComponent partners={partners} client={client} setUpd={setUpd} />
|
||||
</motion.div>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
177
src/Components/NewMappingSubPartnersComponent.jsx
Normal file
177
src/Components/NewMappingSubPartnersComponent.jsx
Normal file
@ -0,0 +1,177 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import NewMappingSubScreensComponent from "./NewMappingSubScreensComponent";
|
||||
import { useLoading } from '../Context/LoadingContext';
|
||||
import { api } from "../API/api";
|
||||
|
||||
export default function NewMappingSubPartnersComponent({ client, partners, setUpd }) {
|
||||
const [expandedPartners, setExpandedPartners] = useState({});
|
||||
const { setLoading } = useLoading();
|
||||
const BACKEND = 'https://ads.dine360ads.com/api//';
|
||||
const [files, setFiles] = useState([]);
|
||||
const [selectedPartners, setSelectedPartners] = useState([]);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [showSelectedList, setShowSelectedList] = useState(false);
|
||||
const [showButtonText, setShowButtonText] = useState("Show Selected Partners");
|
||||
const [showButtonColor, setShowButtonColor] = useState("bg-blue-600");
|
||||
const [cityFilter, setCityFilter] = useState('');
|
||||
const [provinceFilter, setProvinceFilter] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
fetchFiles();
|
||||
}, []);
|
||||
|
||||
const fetchFiles = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await api.get(`/client/files/${client.transid}`);
|
||||
setFiles(response);
|
||||
} catch (error) {
|
||||
console.error("Error fetching files:", error);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const handlePartnerSelection = (partnerId) => {
|
||||
setSelectedPartners((prev) => {
|
||||
if (prev.includes(partnerId)) {
|
||||
return prev.filter((id) => id !== partnerId);
|
||||
} else {
|
||||
return [...prev, partnerId];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleShowSelected = () => {
|
||||
setShowSelectedList(!showSelectedList);
|
||||
setShowButtonText(showSelectedList ? "Show Selected Partners" : "Hide Selected Partners");
|
||||
setShowButtonColor(showSelectedList ? "bg-blue-600" : "bg-green-600");
|
||||
};
|
||||
|
||||
const filteredPartners = partners?.filter((partner) =>
|
||||
partner.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
const sortedPartners = filteredPartners?.slice().sort((a, b) => {
|
||||
const aSelected = selectedPartners.includes(a.transid);
|
||||
const bSelected = selectedPartners.includes(b.transid);
|
||||
if (aSelected && !bSelected) return -1;
|
||||
if (!aSelected && bSelected) return 1;
|
||||
return 0;
|
||||
}).filter(partner => {
|
||||
if (cityFilter && partner.city !== cityFilter) return false;
|
||||
if (provinceFilter && partner.state !== provinceFilter) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
const uniqueCities = [...new Set(partners?.map(partner => partner.city).filter(Boolean))];
|
||||
const uniqueProvinces = [...new Set(partners?.map(partner => partner.state).filter(Boolean))];
|
||||
|
||||
return (
|
||||
<div className="bg-blue-900 p-4 rounded-lg shadow-md text-white" onClick={(e) => e.stopPropagation()}>
|
||||
<p className="font-bold mb-3">Client Ads List</p>
|
||||
<ul className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4 p-4">
|
||||
{files.length > 0 ? (
|
||||
files.map((file, index) => {
|
||||
const fileName = file.file_name;
|
||||
const fileUrl = BACKEND + file.file_path;
|
||||
const isImage = fileUrl.match(/\.(jpeg|jpg|png|gif)$/i);
|
||||
const isVideo = fileUrl.match(/\.(mp4|webm|ogg|mkv)$/i);
|
||||
|
||||
return (
|
||||
<li key={index} className="bg-gray-800 p-4 rounded-lg shadow-lg flex flex-col items-center relative">
|
||||
<span className="text-gray-300 font-bold truncate w-full text-center">
|
||||
{fileName}
|
||||
</span>
|
||||
{isImage && (<img src={fileUrl} alt={fileName} className="w-full h-40 object-cover rounded-lg mt-2" />)}
|
||||
{isVideo && (<video controls className="w-full h-40 rounded-lg mt-2"><source src={fileUrl} type="video/mp4" />Your browser does not support the video tag.</video>)}
|
||||
</li>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<p className="text-gray-500 col-span-full text-center">No Ads uploaded yet.</p>
|
||||
)}
|
||||
</ul>
|
||||
|
||||
<p className="font-bold mb-3">Available Partners</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-2 mb-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search Partners..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => {
|
||||
e.stopPropagation();
|
||||
setSearchTerm(e.target.value);
|
||||
}}
|
||||
className="w-full bg-gray-700 p-2 rounded-lg text-white"
|
||||
/>
|
||||
<select
|
||||
value={cityFilter}
|
||||
onChange={(e) => setCityFilter(e.target.value)}
|
||||
className="w-full bg-gray-700 p-2 rounded-lg text-white"
|
||||
>
|
||||
<option value="">All Cities</option>
|
||||
{uniqueCities?.map(city => (
|
||||
<option key={city} value={city}>{city}</option>
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
value={provinceFilter}
|
||||
onChange={(e) => setProvinceFilter(e.target.value)}
|
||||
className="w-full bg-gray-700 p-2 rounded-lg text-white"
|
||||
>
|
||||
<option value="">All Provinces</option>
|
||||
{uniqueProvinces?.map(province => (
|
||||
<option key={province} value={province}>{province}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<ul className="bg-gray-800 p-4 rounded-lg space-y-2">
|
||||
{sortedPartners?.length > 0 ? (
|
||||
sortedPartners?.map((partner, index) => (
|
||||
<div key={index}>
|
||||
<label
|
||||
htmlFor={`partner-${partner.transid}`}
|
||||
className="text-gray-300 flex items-start border-b border-gray-700 pb-2 space-y-2 cursor-pointer"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={`partner-${partner.transid}`}
|
||||
checked={selectedPartners.includes(partner.transid)}
|
||||
onChange={() => handlePartnerSelection(partner.transid)}
|
||||
className="mr-2 mt-1"
|
||||
/>
|
||||
<span>{index + 1}. {partner.city} - {partner.name}</span>
|
||||
</label>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p className="text-gray-500">No Partners found.</p>
|
||||
)}
|
||||
</ul>
|
||||
|
||||
<button
|
||||
onClick={handleShowSelected}
|
||||
className={`mt-4 ${showButtonColor} hover:bg-blue-700 text-white px-6 py-2 rounded-lg font-bold block mx-auto`}
|
||||
>
|
||||
{showButtonText}
|
||||
</button>
|
||||
|
||||
{showSelectedList && selectedPartners.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<p className="font-bold mb-3">Sub Screens:</p>
|
||||
{selectedPartners.map((partnerId) => {
|
||||
const partner = partners.find((p) => p.transid === partnerId);
|
||||
return (
|
||||
<div key={partnerId} className="mb-4">
|
||||
<NewMappingSubScreensComponent partner={partner} client={client} files={files} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,96 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import NewMappingSubScreensComponent from "./NewMappingSubScreensComponent"; // Ensure this exists
|
||||
import { useLoading } from '../Context/LoadingContext';
|
||||
import { api } from "../API/api";
|
||||
|
||||
export default function NewMappingSubPartnersComponent({ client, partners, setUpd }) {
|
||||
const [expandedPartners, setExpandedPartners] = useState({});
|
||||
const { setLoading } = useLoading();
|
||||
const BACKEND = 'https://ads.dine360ads.com/api//';
|
||||
const toggleSubScreens = (event, partnerId) => {
|
||||
|
||||
event.stopPropagation(); // ⛔️ Prevents event from closing parent card!
|
||||
setExpandedPartners((prev) => ({
|
||||
...prev,
|
||||
[partnerId]: !prev[partnerId],
|
||||
}));
|
||||
};
|
||||
|
||||
const [files, setFiles] = useState([]);
|
||||
useEffect(() => {
|
||||
fetchFiles();
|
||||
}, []);
|
||||
|
||||
// Fetch Client Files
|
||||
const fetchFiles = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await api.get(`/client/files/${client.transid}`);
|
||||
setFiles(response);
|
||||
} catch (error) {
|
||||
console.error("Error fetching files:", error);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
return (
|
||||
<div className="bg-blue-900 p-4 rounded-lg shadow-md text-white" onClick={(e) => e.stopPropagation()}>
|
||||
|
||||
<p className="font-bold mb-3">Client Ads List</p>
|
||||
<ul className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4 p-4">
|
||||
{files.length > 0 ? (
|
||||
files.map((file, index) => {
|
||||
const fileName = file.file_name;
|
||||
const fileUrl = BACKEND + file.file_path;
|
||||
const isImage = fileUrl.match(/\.(jpeg|jpg|png|gif)$/i);
|
||||
const isVideo = fileUrl.match(/\.(mp4|webm|ogg|mkv)$/i);
|
||||
|
||||
return (
|
||||
<li key={index} className="bg-gray-800 p-4 rounded-lg shadow-lg flex flex-col items-center relative">
|
||||
<span className="text-gray-300 font-bold truncate w-full text-center">
|
||||
{fileName}
|
||||
</span>
|
||||
{isImage && (<img src={fileUrl} alt={fileName} className="w-full h-40 object-cover rounded-lg mt-2" />)}
|
||||
{isVideo && (<video controls className="w-full h-40 rounded-lg mt-2"><source src={fileUrl} type="video/mp4" />Your browser does not support the video tag.</video>)}
|
||||
</li>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<p className="text-gray-500 col-span-full text-center">No Ads uploaded yet.</p>
|
||||
)}
|
||||
</ul>
|
||||
|
||||
<p className="font-bold mb-3">Available Partners</p>
|
||||
<ul className="bg-gray-800 p-4 rounded-lg space-y-2">
|
||||
{partners?.length > 0 ? (
|
||||
partners?.map((partner, index) => (
|
||||
<div key={index}>
|
||||
{/* Clickable Partner Name */}
|
||||
<li
|
||||
className="text-gray-300 flex flex-col border-b border-gray-700 pb-2 space-y-2 cursor-pointer hover:text-white transition"
|
||||
onClick={(e) => toggleSubScreens(e, partner.transid)}
|
||||
>
|
||||
<span>{index + 1}. {partner.name}</span>
|
||||
</li>
|
||||
|
||||
{/* Conditionally Show SubScreens */}
|
||||
{expandedPartners[partner.transid] && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -10 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="mt-2 border-b border-gray-700 pb-2"
|
||||
>
|
||||
<NewMappingSubScreensComponent partner={partner} client={client} files={files} />
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p className="text-gray-500">No Partners are Registered Yet.</p>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
332
src/Components/NewMappingSubScreensComponent.jsx
Normal file
332
src/Components/NewMappingSubScreensComponent.jsx
Normal file
@ -0,0 +1,332 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useLoading } from '../Context/LoadingContext';
|
||||
import { api } from "../API/api";
|
||||
import { useMemo } from "react";
|
||||
|
||||
export default function NewMappingSubScreensComponent({ partner, client, files }) {
|
||||
const daysOfWeek = ["M", "T", "W", "Th", "F", "S", "Su"];
|
||||
const { setLoading } = useLoading();
|
||||
const [screens, setScreens] = useState([]);
|
||||
const BACKEND = 'https://ads.dine360ads.com/api//';
|
||||
const [selectedFiles, setSelectedFiles] = useState({});
|
||||
const [screenSelections, setScreenSelections] = useState({});
|
||||
const [FileDurations, setFileDurations] = useState({});
|
||||
const [selectAllFiles, setSelectAllFiles] = useState({});
|
||||
const [selectAllDays, setSelectAllDays] = useState({});
|
||||
const [databaseData, setDatabaseData] = useState([]);
|
||||
const [selectAllColumnFiles, setSelectAllColumnFiles] = useState(false);
|
||||
|
||||
const fetchScreens = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await api.get(`/admin/get-screen/${partner.transid}`);
|
||||
setScreens(response);
|
||||
} catch (error) {
|
||||
console.error("Error fetching files:", error);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const fetchMappingsFromDB = async (clientId, partnerId) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await api.get(`/admin/client-partner-mappings?clientid=${clientId}&partnerid=${partnerId}`);
|
||||
setDatabaseData(response);
|
||||
} catch (error) {
|
||||
console.error("Error fetching mappings:", error);
|
||||
alert("Error fetching mappings from the database.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (client && partner) {
|
||||
fetchMappingsFromDB(client.transid, partner.transid);
|
||||
}
|
||||
}, [client, partner]);
|
||||
|
||||
const handleFileSelection = (screenId, fileId) => {
|
||||
setSelectedFiles(prev => ({
|
||||
...prev,
|
||||
[screenId]: {
|
||||
...prev[screenId],
|
||||
[fileId]: !prev[screenId]?.[fileId],
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const handleScreenCheckboxChange = (screenId, fileId, day) => {
|
||||
setScreenSelections(prev => ({
|
||||
...prev,
|
||||
[screenId]: {
|
||||
...prev[screenId],
|
||||
[fileId]: {
|
||||
...prev[screenId]?.[fileId],
|
||||
[day]: !prev[screenId]?.[fileId]?.[day],
|
||||
},
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSelectAllFiles = (screenId, checked) => {
|
||||
setSelectAllFiles(prev => ({ ...prev, [screenId]: checked }));
|
||||
setSelectedFiles(prev => {
|
||||
const screenFiles = files.reduce((acc, file) => ({ ...acc, [file.transid]: checked }), {});
|
||||
return { ...prev, [screenId]: screenFiles };
|
||||
});
|
||||
};
|
||||
|
||||
const handleSelectAllDays = (screenId, fileId, checked) => {
|
||||
setSelectAllDays(prev => ({ ...prev, [screenId]: { ...prev[screenId], [fileId]: checked } }));
|
||||
setScreenSelections(prev => {
|
||||
const screenDays = daysOfWeek.reduce((acc, day) => ({ ...acc, [day]: checked }), {});
|
||||
return { ...prev, [screenId]: { ...prev[screenId], [fileId]: screenDays } };
|
||||
});
|
||||
};
|
||||
const areAllFilesSelected = () =>
|
||||
screens.length > 0 &&
|
||||
screens.every(screen =>
|
||||
files.every(file =>
|
||||
selectedFiles[screen.transid]?.[file.transid] === true
|
||||
)
|
||||
);const allFilesSelected = useMemo(
|
||||
() => areAllFilesSelected(),
|
||||
[screens, files, selectedFiles]
|
||||
);
|
||||
|
||||
const handleSelectAllColumnFiles = (checked) => {
|
||||
setSelectAllColumnFiles(checked);
|
||||
setSelectedFiles(prev => {
|
||||
const updatedSelectedFiles = {};
|
||||
screens.forEach(screen => {
|
||||
updatedSelectedFiles[screen.transid] = {};
|
||||
files.forEach(file => {
|
||||
updatedSelectedFiles[screen.transid][file.transid] = checked;
|
||||
});
|
||||
});
|
||||
return updatedSelectedFiles;
|
||||
});
|
||||
};
|
||||
|
||||
const handleDurationChange = (screenId, fileId, duration) => {
|
||||
setFileDurations(prev => ({
|
||||
...prev,
|
||||
[screenId]: {
|
||||
...prev[screenId],
|
||||
[fileId]: duration,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
|
||||
let finalData = [];
|
||||
screens.forEach(screen => {
|
||||
const filesSelectedForScreen = selectedFiles[screen.transid] || {};
|
||||
|
||||
Object.keys(filesSelectedForScreen).forEach(fileId => {
|
||||
if (filesSelectedForScreen[fileId]) {
|
||||
const file = files.find(f => f.transid === fileId);
|
||||
if (!file) return;
|
||||
const daysSelected = screenSelections[screen.transid]?.[fileId] || {};
|
||||
const imageduration = FileDurations[screen.transid]?.[fileId] || 10;
|
||||
finalData.push({
|
||||
clientid: client.transid,
|
||||
partnerid: partner.transid,
|
||||
screenid: screen.transid,
|
||||
fileid: file.transid,
|
||||
file_path: file.file_path,
|
||||
ismonday: daysSelected["M"] ? 1 : 0,
|
||||
istuesday: daysSelected["T"] ? 1 : 0,
|
||||
iswednesday: daysSelected["W"] ? 1 : 0,
|
||||
isthursday: daysSelected["Th"] ? 1 : 0,
|
||||
isfriday: daysSelected["F"] ? 1 : 0,
|
||||
issaturday: daysSelected["S"] ? 1 : 0,
|
||||
issunday: daysSelected["Su"] ? 1 : 0,
|
||||
imageduration: imageduration
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
console.log("Final Data to Insert:", finalData);
|
||||
|
||||
try {
|
||||
await api.post("/admin/add-client-partner-mapping", finalData);
|
||||
alert("Client Partner Mapping data added successfully!");
|
||||
fetchMappingsFromDB(client.transid, partner.transid);
|
||||
} catch (error) {
|
||||
console.error("Error adding client partner mapping:", error);
|
||||
alert("Error adding client partner mapping. Please check console for details.");
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchScreens();
|
||||
|
||||
const initialSelectedFiles = {};
|
||||
const initialScreenSelections = {};
|
||||
const initialFileDurations = {};
|
||||
databaseData?.forEach(item => {
|
||||
if (!initialSelectedFiles[item.screenid]) {
|
||||
initialSelectedFiles[item.screenid] = {};
|
||||
}
|
||||
initialSelectedFiles[item.screenid][item.fileid] = true;
|
||||
|
||||
if (!initialScreenSelections[item.screenid]) {
|
||||
initialScreenSelections[item.screenid] = {};
|
||||
}
|
||||
if (!initialScreenSelections[item.screenid][item.fileid]) {
|
||||
initialScreenSelections[item.screenid][item.fileid] = {};
|
||||
}
|
||||
initialScreenSelections[item.screenid][item.fileid]["M"] = item.ismonday === 1;
|
||||
initialScreenSelections[item.screenid][item.fileid]["T"] = item.istuesday === 1;
|
||||
initialScreenSelections[item.screenid][item.fileid]["W"] = item.iswednesday === 1;
|
||||
initialScreenSelections[item.screenid][item.fileid]["Th"] = item.isthursday === 1;
|
||||
initialScreenSelections[item.screenid][item.fileid]["F"] = item.isfriday === 1;
|
||||
initialScreenSelections[item.screenid][item.fileid]["S"] = item.issaturday === 1;
|
||||
initialScreenSelections[item.screenid][item.fileid]["Su"] = item.issunday === 1;
|
||||
if (!initialFileDurations[item.screenid]) {
|
||||
initialFileDurations[item.screenid] = {};
|
||||
}
|
||||
if (!initialFileDurations[item.screenid][item.fileid]) {
|
||||
initialFileDurations[item.screenid][item.fileid] = {};
|
||||
}
|
||||
initialFileDurations[item.screenid][item.fileid] = item.imageduration;
|
||||
});
|
||||
setFileDurations(initialFileDurations)
|
||||
console.log(initialFileDurations)
|
||||
setSelectedFiles(initialSelectedFiles);
|
||||
setScreenSelections(initialScreenSelections);
|
||||
}, [databaseData]);
|
||||
|
||||
|
||||
const areAllDaysChecked = (screenId, fileId) =>
|
||||
daysOfWeek.every(day =>
|
||||
screenSelections[screenId]?.[fileId]?.[day] === true
|
||||
);
|
||||
|
||||
const handleDeleteClient = async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
|
||||
if (client && partner) {
|
||||
fetchMappingsFromDB(client.transid, partner.transid);
|
||||
} else {
|
||||
alert("Client / Partner data Not Loaded Yet...");
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await api.delete(`/admin/delete-client-partner-mapping/${partner.transid}/${client.transid}`);
|
||||
|
||||
alert("Client has been removed from the partner successfully!");
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error removing client partner mapping:", error);
|
||||
alert("Error removing client partner mapping. Please check console for details.");
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div className="bg-gray-600 p-4 rounded-lg shadow-md text-white">
|
||||
<p className="font-bold mb-3">{partner.name} - Assign Screens</p>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full border border-gray-500">
|
||||
<thead>
|
||||
<tr className="bg-gray-700">
|
||||
<th className="p-2 border">Screen Name</th>
|
||||
<th className="p-2 border">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mr-2"
|
||||
//checked={selectAllColumnFiles}
|
||||
checked={areAllFilesSelected()}
|
||||
onChange={(e) => handleSelectAllColumnFiles(e.target.checked)}
|
||||
/>
|
||||
Ad Name
|
||||
</label>
|
||||
</th>
|
||||
{daysOfWeek.map(day => (
|
||||
<th key={day} className="p-2 border">{day}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{screens.map(screen => (
|
||||
files.map(file => (
|
||||
<tr key={`${screen.transid}-${file.transid}`} className="border">
|
||||
<td className="p-2 border">{screen.name}</td>
|
||||
<td className="p-2 border align-top">
|
||||
<div className="flex justify-between items-start gap-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mr-2"
|
||||
checked={selectedFiles[screen.transid]?.[file.transid] || false}
|
||||
onChange={() => handleFileSelection(screen.transid, file.transid)}
|
||||
/>
|
||||
<span className="text-sm truncate">{file.file_name}</span>
|
||||
</label>
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mr-2"
|
||||
//checked={selectAllDays[screen.transid]?.[file.transid] || false}
|
||||
checked={areAllDaysChecked(screen.transid, file.transid)}
|
||||
onChange={(e) => handleSelectAllDays(screen.transid, file.transid, e.target.checked)}
|
||||
/>
|
||||
<span className="text-sm">All Days</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
value={FileDurations[screen.transid]?.[file.transid] || '10'}
|
||||
onChange={(e) => handleDurationChange(screen.transid, file.transid, e.target.value)}
|
||||
maxLength={3}
|
||||
pattern="\d{1,3}"
|
||||
inputMode="numeric"
|
||||
placeholder="000"
|
||||
title="Enter up to 3 digits only"
|
||||
className="w-16 p-2 text-center text-sm border border-gray-300 rounded-md shadow-inner"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{daysOfWeek.map(day => (
|
||||
<td key={day} className="p-2 border text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={screenSelections[screen.transid]?.[file.transid]?.[day] || false}
|
||||
onChange={() => handleScreenCheckboxChange(screen.transid, file.transid, day)}
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<br />
|
||||
<div>
|
||||
|
||||
{files.length > 0 && (
|
||||
<button onClick={handleSubmit} className="mt-4 bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg font-bold block mx-auto">Save Ad Configuration</button>
|
||||
)}
|
||||
{files.length > 0 && (
|
||||
<button onClick={handleDeleteClient} className="mt-4 bg-red-600 hover:bg-red-700 text-white px-6 py-2 rounded-lg font-bold block mx-auto">Delete Client Ad Configuration</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
304
src/Components/NewMappingSubScreensComponent_29_03_25_Bak.jsx
Normal file
304
src/Components/NewMappingSubScreensComponent_29_03_25_Bak.jsx
Normal file
@ -0,0 +1,304 @@
|
||||
// Part 1: Component Logic and State Management
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useLoading } from '../../../Context/LoadingContext';
|
||||
import { api } from "../../../API/api";
|
||||
|
||||
export default function NewMappingSubScreensComponent({ partner, client, files }) {
|
||||
const daysOfWeek = ["M", "T", "W", "Th", "F", "S", "Su"];
|
||||
const { setLoading } = useLoading();
|
||||
const [screens, setScreens] = useState([]);
|
||||
const BACKEND = 'https://ads.dine360ads.com/api//';
|
||||
const [selectedFiles, setSelectedFiles] = useState({});
|
||||
const [screenSelections, setScreenSelections] = useState({});
|
||||
const [selectAllFiles, setSelectAllFiles] = useState({});
|
||||
const [selectAllDays, setSelectAllDays] = useState({});
|
||||
const [databaseData, setDatabaseData] = useState([]);
|
||||
|
||||
|
||||
|
||||
const fetchScreens = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await api.get(`/admin/get-screen/${partner.transid}`);
|
||||
setScreens(response);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error fetching files:", error);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
|
||||
|
||||
const fetchMappingsFromDB = async (clientId, partnerId) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await api.get(`/admin/client-partner-mappings?clientid=${clientId}&partnerid=${partnerId}`); // Use the correct API endpoint
|
||||
setDatabaseData(response); // Assuming you have a state variable called databaseData and a setter function called setDatabaseData
|
||||
} catch (error) {
|
||||
console.error("Error fetching mappings:", error);
|
||||
alert("Error fetching mappings from the database.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Assuming client.transid and partner.transid are available in your component's props or state
|
||||
if (client && partner) {
|
||||
fetchMappingsFromDB(client.transid, partner.transid);
|
||||
}
|
||||
}, [client, partner]);
|
||||
|
||||
|
||||
const handleFileSelection = (screenId, fileId) => {
|
||||
setSelectedFiles(prev => ({
|
||||
...prev,
|
||||
[screenId]: {
|
||||
...prev[screenId],
|
||||
[fileId]: !prev[screenId]?.[fileId],
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const handleScreenCheckboxChange = (screenId, day) => {
|
||||
setScreenSelections(prev => ({
|
||||
...prev,
|
||||
[screenId]: {
|
||||
...prev[screenId],
|
||||
[day]: !prev[screenId]?.[day],
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSelectAllFiles = (screenId, checked) => {
|
||||
setSelectAllFiles(prev => ({ ...prev, [screenId]: checked }));
|
||||
setSelectedFiles(prev => {
|
||||
const screenFiles = files.reduce((acc, file) => ({ ...acc, [file.transid]: checked }), {});
|
||||
return { ...prev, [screenId]: screenFiles };
|
||||
});
|
||||
};
|
||||
|
||||
const handleSelectAllDays = (screenId, checked) => {
|
||||
setSelectAllDays(prev => ({ ...prev, [screenId]: checked }));
|
||||
setScreenSelections(prev => {
|
||||
const screenDays = daysOfWeek.reduce((acc, day) => ({ ...acc, [day]: checked }), {});
|
||||
return { ...prev, [screenId]: screenDays };
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setLoading(true);
|
||||
|
||||
let finalData = [];
|
||||
screens.forEach(screen => {
|
||||
const filesSelectedForScreen = selectedFiles[screen.transid] || {};
|
||||
const daysSelected = screenSelections[screen.transid] || {};
|
||||
|
||||
// Check if at least one file and one day are selected
|
||||
const isFileSelected = Object.values(filesSelectedForScreen).some(Boolean);
|
||||
const isDaySelected = Object.values(daysSelected).some(Boolean);
|
||||
|
||||
if (isFileSelected && isDaySelected) {
|
||||
Object.keys(filesSelectedForScreen).forEach(fileId => {
|
||||
if (filesSelectedForScreen[fileId]) {
|
||||
const file = files.find(f => f.transid === fileId);
|
||||
if (!file) return;
|
||||
finalData.push({
|
||||
clientid: client.transid,
|
||||
partnerid: partner.transid,
|
||||
screenid: screen.transid,
|
||||
fileid: file.transid,
|
||||
file_path: file.file_path,
|
||||
ismonday: daysSelected["M"] ? 1 : 0,
|
||||
istuesday: daysSelected["T"] ? 1 : 0,
|
||||
iswednesday: daysSelected["W"] ? 1 : 0,
|
||||
isthursday: daysSelected["Th"] ? 1 : 0,
|
||||
isfriday: daysSelected["F"] ? 1 : 0,
|
||||
issaturday: daysSelected["S"] ? 1 : 0,
|
||||
issunday: daysSelected["Su"] ? 1 : 0,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
//if nothing is selected for the screen, or only files or only days are selected then the screen is not added to the finalData.
|
||||
});
|
||||
console.log("Final Data to Insert:", finalData);
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
await api.post("/admin/add-client-partner-mapping", finalData); // Use your API helper and the correct endpoint
|
||||
alert("Client Partner Mapping data added successfully!");
|
||||
fetchMappingsFromDB()
|
||||
// Optionally, you might want to navigate or refresh data here.
|
||||
// navigate("/mappings"); // Example navigation
|
||||
} catch (error) {
|
||||
console.error("Error adding client partner mapping:", error);
|
||||
alert("Error adding client partner mapping. Please check console for details.");
|
||||
}
|
||||
setLoading(false);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
fetchScreens();
|
||||
|
||||
// Initialize state from databaseData
|
||||
const initialSelectedFiles = {};
|
||||
const initialScreenSelections = {};
|
||||
|
||||
databaseData?.forEach(item => {
|
||||
if (!initialSelectedFiles[item.screenid]) {
|
||||
initialSelectedFiles[item.screenid] = {};
|
||||
}
|
||||
if (!initialSelectedFiles[item.screenid][item.fileid]) {
|
||||
initialSelectedFiles[item.screenid][item.fileid] = true;
|
||||
}
|
||||
|
||||
if (!initialScreenSelections[item.screenid]) {
|
||||
initialScreenSelections[item.screenid] = {};
|
||||
}
|
||||
initialScreenSelections[item.screenid]["M"] = item.ismonday === 1;
|
||||
initialScreenSelections[item.screenid]["T"] = item.istuesday === 1;
|
||||
initialScreenSelections[item.screenid]["W"] = item.iswednesday === 1;
|
||||
initialScreenSelections[item.screenid]["Th"] = item.isthursday === 1;
|
||||
initialScreenSelections[item.screenid]["F"] = item.isfriday === 1;
|
||||
initialScreenSelections[item.screenid]["S"] = item.issaturday === 1;
|
||||
initialScreenSelections[item.screenid]["Su"] = item.issunday === 1;
|
||||
});
|
||||
|
||||
setSelectedFiles(initialSelectedFiles);
|
||||
setScreenSelections(initialScreenSelections);
|
||||
|
||||
}, [databaseData]);
|
||||
|
||||
// Part 2: JSX Rendering
|
||||
return (
|
||||
<div className="bg-gray-600 p-4 rounded-lg shadow-md text-white">
|
||||
<p className="font-bold mb-3">{partner.name} - Assign Screens</p>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full border border-gray-500">
|
||||
<thead>
|
||||
<tr className="bg-gray-700">
|
||||
<th className="p-2 border">Screen Name</th>
|
||||
<th className="p-2 border">Ads List</th>
|
||||
{daysOfWeek.map(day => (
|
||||
<th key={day} className="p-2 border">{day}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{screens.map(screen => (
|
||||
<tr key={screen.transid} className="border">
|
||||
<td className="p-2 border">
|
||||
{screen.name}
|
||||
<label className="flex items-center mt-2">
|
||||
<br></br>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mr-2"
|
||||
checked={selectAllDays[screen.transid] || false}
|
||||
onChange={(e) => handleSelectAllDays(screen.transid, e.target.checked)}
|
||||
/>
|
||||
All Days
|
||||
</label>
|
||||
</td>
|
||||
<td className="p-2 border">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-2">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mr-2"
|
||||
checked={selectAllFiles[screen.transid] || false}
|
||||
onChange={(e) => handleSelectAllFiles(screen.transid, e.target.checked)}
|
||||
/>
|
||||
Select All
|
||||
</label>
|
||||
{files.map(file => (
|
||||
<label key={file.transid} className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mr-2"
|
||||
checked={selectedFiles[screen.transid]?.[file.transid] || false}
|
||||
onChange={() => handleFileSelection(screen.transid, file.transid)}
|
||||
/>
|
||||
<span className="text-sm truncate">{file.file_name}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</td>
|
||||
{daysOfWeek.map(day => (
|
||||
<td key={day} className="p-2 border text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={screenSelections[screen.transid]?.[day] || false}
|
||||
onChange={() => handleScreenCheckboxChange(screen.transid, day)}
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<br />
|
||||
<div>
|
||||
<p className="font-bold mb-3">Client Ads List</p>
|
||||
<ul className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4 p-4">
|
||||
{files.length > 0 ? (
|
||||
files.map((file, index) => {
|
||||
const fileName = file.file_name;
|
||||
const fileUrl = BACKEND + file.file_path;
|
||||
const isImage = fileUrl.match(/\.(jpeg|jpg|png|gif)$/i);
|
||||
const isVideo = fileUrl.match(/\.(mp4|webm|ogg|mkv)$/i);
|
||||
|
||||
return (
|
||||
<li
|
||||
key={index}
|
||||
className="bg-gray-800 p-4 rounded-lg shadow-lg flex flex-col items-center relative"
|
||||
>
|
||||
<span className="text-gray-300 font-bold truncate w-full text-center">
|
||||
{fileName}
|
||||
</span>
|
||||
|
||||
{isImage && (
|
||||
<img
|
||||
src={fileUrl}
|
||||
alt={fileName}
|
||||
className="w-full h-40 object-cover rounded-lg mt-2"
|
||||
/>
|
||||
)}
|
||||
|
||||
{isVideo && (
|
||||
<video controls className="w-full h-40 rounded-lg mt-2">
|
||||
<source
|
||||
src={fileUrl} type="video/mp4" />
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<p className="text-gray-500 col-span-full text-center">
|
||||
No Ads uploaded yet.
|
||||
</p>
|
||||
)}
|
||||
</ul>
|
||||
{files.length > 0 && (
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
className="mt-4 bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg font-bold block mx-auto"
|
||||
>
|
||||
Save Ad Configuration
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
193
src/Components/NewMappingSubScreensComponent_Bak.jsx
Normal file
193
src/Components/NewMappingSubScreensComponent_Bak.jsx
Normal file
@ -0,0 +1,193 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useLoading } from '../Context/LoadingContext';
|
||||
import { api } from "../API/api";
|
||||
|
||||
export default function NewMappingSubScreensComponent({ partner, client, files }) {
|
||||
const daysOfWeek = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
|
||||
|
||||
const { setLoading } = useLoading();
|
||||
const [screens, setScreens] = useState([]);
|
||||
|
||||
const fetchScreens = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await api.get(`/admin/get-screen/${partner.transid}`);
|
||||
setScreens(response);
|
||||
} catch (error) {
|
||||
console.error("Error fetching files:", error);
|
||||
}
|
||||
setLoading(false)
|
||||
};
|
||||
const screens1 = [
|
||||
{ id: 1, name: "Screen A" },
|
||||
{ id: 2, name: "Screen B" },
|
||||
{ id: 3, name: "Screen C" },
|
||||
]; // Replace with actual screen names if dynamic
|
||||
|
||||
|
||||
const BACKEND = 'https://ads.dine360ads.com/api//';
|
||||
|
||||
const [selectedFiles, setSelectedFiles] = useState([]);
|
||||
const [screenSelections, setScreenSelections] = useState({}); // Stores selected checkboxes
|
||||
|
||||
|
||||
// Handle file checkbox selection
|
||||
const handleFileSelection = (fileId) => {
|
||||
setSelectedFiles((prev) =>
|
||||
prev.includes(fileId) ? prev.filter((id) => id !== fileId) : [...prev, fileId]
|
||||
);
|
||||
};
|
||||
|
||||
// Handle weekday checkbox selection for screens
|
||||
const handleScreenCheckboxChange = (screenId, day) => {
|
||||
setScreenSelections((prev) => ({
|
||||
...prev,
|
||||
[screenId]: {
|
||||
...prev[screenId],
|
||||
[day]: !prev[screenId]?.[day], // Toggle checkbox
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
// Handle Submit - Collect data to insert into database
|
||||
const handleSubmit = () => {
|
||||
if (selectedFiles.length === 0) {
|
||||
alert("No files selected!");
|
||||
return;
|
||||
}
|
||||
|
||||
let finalData = [];
|
||||
|
||||
selectedFiles.forEach((fileId) => {
|
||||
const file = files.find((f) => f.transid === fileId);
|
||||
if (!file) return;
|
||||
|
||||
screens.forEach((screen) => {
|
||||
const daysSelected = screenSelections[screen.transid] || {};
|
||||
|
||||
finalData.push({
|
||||
clientid: client.transid,
|
||||
partnerid: partner.transid,
|
||||
screenid: screen.transid,
|
||||
fileid: file.transid,
|
||||
file_path: file.file_path,
|
||||
ismonday: daysSelected["Monday"] ? 1 : 0,
|
||||
istuesday: daysSelected["Tuesday"] ? 1 : 0,
|
||||
iswednesday: daysSelected["Wednesday"] ? 1 : 0,
|
||||
isthursday: daysSelected["Thursday"] ? 1 : 0,
|
||||
isfriday: daysSelected["Friday"] ? 1 : 0,
|
||||
issaturday: daysSelected["Saturday"] ? 1 : 0,
|
||||
issunday: daysSelected["Sunday"] ? 1 : 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
console.log("Final Data to Insert:", finalData);
|
||||
alert("Data formatted successfully! Check console for details.");
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchScreens()
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="bg-gray-600 p-4 rounded-lg shadow-md text-white">
|
||||
<p className="font-bold mb-3">{partner.name} - Assign Screens</p>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full border border-gray-500">
|
||||
<thead>
|
||||
<tr className="bg-gray-700">
|
||||
<th className="p-2 border">Screen Name</th>
|
||||
<th className="p-2 border">Ads List</th>
|
||||
{daysOfWeek.map((day) => (
|
||||
<th key={day} className="p-2 border">{day}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{screens.map((screen) => (
|
||||
<tr key={screen.transid} className="border">
|
||||
<td className="p-2 border">{screen.name}</td>
|
||||
<td className="p-2 border">{screen.adslist}</td>
|
||||
|
||||
{daysOfWeek.map((day) => (
|
||||
<td key={day} className="p-2 border text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={screenSelections[screen.transid]?.[day] || false}
|
||||
onChange={() => handleScreenCheckboxChange(screen.transid, day)}
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<br />
|
||||
|
||||
{/* File Selection */}
|
||||
<div>
|
||||
<p className="font-bold mb-3">Client Ads List</p>
|
||||
|
||||
<ul className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 p-4">
|
||||
{files.length > 0 ? (
|
||||
files.map((file, index) => {
|
||||
const fileName = file.file_name;
|
||||
const fileUrl = BACKEND + file.file_path;
|
||||
const isImage = fileUrl.match(/\.(jpeg|jpg|png|gif)$/i);
|
||||
const isVideo = fileUrl.match(/\.(mp4|webm|ogg|mkv)$/i);
|
||||
|
||||
return (
|
||||
<li
|
||||
key={index}
|
||||
className="bg-gray-800 p-4 rounded-lg shadow-lg flex flex-col items-center relative"
|
||||
>
|
||||
{/* Checkbox at Top Left */}
|
||||
<input
|
||||
type="checkbox"
|
||||
className="absolute top-5 left-3 w-5 h-5"
|
||||
checked={selectedFiles.includes(file.transid)}
|
||||
onChange={() => handleFileSelection(file.transid)}
|
||||
/>
|
||||
|
||||
<span className="text-gray-300 font-bold truncate ml-10 pr-3 w-full text-right">
|
||||
{fileName}
|
||||
</span>
|
||||
|
||||
{isImage && (
|
||||
<img
|
||||
src={fileUrl}
|
||||
alt={fileName}
|
||||
className="w-full h-40 object-cover rounded-lg mt-2"
|
||||
/>
|
||||
)}
|
||||
|
||||
{isVideo && (
|
||||
<video controls className="w-full h-40 rounded-lg mt-2">
|
||||
<source src={fileUrl} type="video/mp4" />
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<p className="text-gray-500 col-span-full text-center">
|
||||
No Ads uploaded yet.
|
||||
</p>
|
||||
)}
|
||||
</ul>
|
||||
|
||||
{files.length > 0 && (
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
className="mt-4 bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg font-bold block mx-auto"
|
||||
>
|
||||
Save Ad Configuration
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
87
src/Components/PageCardComponent.jsx
Normal file
87
src/Components/PageCardComponent.jsx
Normal file
@ -0,0 +1,87 @@
|
||||
import React from 'react'
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { FaPlus, FaCircle } from "react-icons/fa";
|
||||
import { api } from "../API/api";
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useLoading } from '../Context/LoadingContext';
|
||||
|
||||
export default function ClientsCardComponent({ Data,setUpd }) {
|
||||
const navigate = useNavigate();
|
||||
const { setLoading } = useLoading();
|
||||
|
||||
|
||||
const handleDelete = async (clientid) => {
|
||||
console.log(clientid)
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await api.delete(`/admin/delete-client/${clientid}`);
|
||||
console.log(response);
|
||||
setUpd(clientid)
|
||||
} catch (error) {
|
||||
console.error("Error fetching files:", error);
|
||||
}
|
||||
setLoading(false)
|
||||
};
|
||||
|
||||
|
||||
|
||||
return (
|
||||
|
||||
<>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
|
||||
<motion.div
|
||||
className="bg-gray-800 p-6 rounded-lg shadow-lg text-center cursor-pointer hover:bg-gray-700 transition flex items-center justify-center"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={() => navigate("/add-client")}
|
||||
>
|
||||
<div>
|
||||
<FaPlus className="text-5xl text-blue-500 mx-auto mb-4" />
|
||||
<p className="text-lg font-semibold">Add New Client</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
{Data?.map((client) => (
|
||||
<motion.div
|
||||
key={client.transid}
|
||||
className="bg-gray-800 p-6 rounded-lg shadow-lg relative cursor-pointer hover:bg-gray-700 transition"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0 }}
|
||||
whileHover={{ scale: 1.03 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
onClick={() => navigate(`/manage-client/${client.transid}`)}
|
||||
>
|
||||
|
||||
{/* Red Delete Button at Top Right Corner */}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation(); // Prevents triggering the card's onClick event
|
||||
if (confirm("Are you sure you want to delete this client?")) {
|
||||
handleDelete(client.transid);
|
||||
}
|
||||
}}
|
||||
className="absolute top-2 right-2 text-red-500 hover:text-red-700 text-xl"
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
|
||||
<div
|
||||
className="bg-cover bg-center h-40 rounded-lg filter blur-sm"
|
||||
style={{
|
||||
backgroundImage: `url('///')`,
|
||||
}}
|
||||
></div>
|
||||
|
||||
<div className="mt-4">
|
||||
<h2 className="text-xl font-bold">{client.name}</h2>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
92
src/Components/PartnersPageCardComponent.jsx
Normal file
92
src/Components/PartnersPageCardComponent.jsx
Normal file
@ -0,0 +1,92 @@
|
||||
import React from 'react'
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { FaPlus, FaCircle } from "react-icons/fa";
|
||||
import { api } from "../API/api";
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useLoading } from '../Context/LoadingContext';
|
||||
|
||||
export default function PartnersCardComponent({ Data, setUpd }) {
|
||||
const navigate = useNavigate();
|
||||
const { setLoading } = useLoading();
|
||||
|
||||
|
||||
const handleDelete = async (partnerid) => {
|
||||
console.log(partnerid)
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await api.delete(`/admin/delete-partner/${partnerid}`);
|
||||
console.log(response);
|
||||
setUpd(partnerid)
|
||||
} catch (error) {
|
||||
console.error("Error fetching files:", error);
|
||||
}
|
||||
setLoading(false)
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
return (
|
||||
|
||||
<>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
|
||||
<motion.div
|
||||
className="bg-gray-800 p-6 rounded-lg shadow-lg text-center cursor-pointer hover:bg-gray-700 transition flex items-center justify-center"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={() => navigate("/add-partner")}
|
||||
>
|
||||
<div>
|
||||
<FaPlus className="text-5xl text-blue-500 mx-auto mb-4" />
|
||||
<p className="text-lg font-semibold">Add New Partner</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
{Data?.map((client) => (
|
||||
<motion.div
|
||||
key={client.transid}
|
||||
className="bg-gray-800 p-6 rounded-lg shadow-lg relative cursor-pointer hover:bg-gray-700 transition"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0 }}
|
||||
whileHover={{ scale: 1.03 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
onClick={() => navigate(`/manage-partner/${client.transid}`)}
|
||||
>
|
||||
|
||||
{/* Red Delete Button at Top Right Corner */}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation(); // Prevents triggering the card's onClick event
|
||||
if (confirm("Are you sure you want to delete this Partner?")) {
|
||||
handleDelete(client.transid);
|
||||
}
|
||||
}}
|
||||
className="absolute top-2 right-2 text-red-500 hover:text-red-700 text-xl"
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
<div
|
||||
className="bg-cover bg-center h-40 rounded-lg "
|
||||
style={{
|
||||
backgroundImage: `url('https://ads.dine360ads.com/api/${client.logo_url || ""}')`,
|
||||
}}
|
||||
></div>
|
||||
|
||||
<div className="mt-4">
|
||||
<h2 className="text-xl font-bold">{client.name}</h2>
|
||||
<p className="text-gray-400">🕒 {client.open_time} - {client.close_time}</p>
|
||||
|
||||
{/* <div className="mt-2 ml-1 flex items-center">
|
||||
<FaCircle className={`mr-2 ${client.is_active ? "text-green-400" : "text-red-400"}`} />
|
||||
<span> {client.is_active ? "Active" : "Inactive"}</span>
|
||||
</div> */}
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
131
src/Components/ViewPartnerAdsConfiguration.jsx
Normal file
131
src/Components/ViewPartnerAdsConfiguration.jsx
Normal file
@ -0,0 +1,131 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useLoading } from '../Context/LoadingContext';
|
||||
import { api } from "../API/api";
|
||||
|
||||
export default function ViewPartnerAdsConfiguration({ data }) {
|
||||
const daysOfWeek = ["M", "T", "W", "Th", "F", "S", "Su"];
|
||||
const { setLoading } = useLoading();
|
||||
const [screens, setScreens] = useState([]);
|
||||
const [selectedFiles, setSelectedFiles] = useState({});
|
||||
const [screenSelections, setScreenSelections] = useState({});
|
||||
const [selectAllFiles, setSelectAllFiles] = useState({});
|
||||
const [selectAllDays, setSelectAllDays] = useState({});
|
||||
const [selectAllColumnFiles, setSelectAllColumnFiles] = useState(false);
|
||||
|
||||
// Extract unique screens from the data prop
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
console.log(data)
|
||||
const uniqueScreens = [...new Map(data.map(item => [item.screenid, {
|
||||
transid: item.screenid,
|
||||
name: item.screenname || item.screenid // Use screenname if available, otherwise screenid
|
||||
}])).values()];
|
||||
console.log(uniqueScreens)
|
||||
setScreens(uniqueScreens);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
const initialSelectedFiles = {};
|
||||
const initialScreenSelections = {};
|
||||
|
||||
data.forEach(item => {
|
||||
if (!initialSelectedFiles[item.screenid]) {
|
||||
initialSelectedFiles[item.screenid] = {};
|
||||
}
|
||||
initialSelectedFiles[item.screenid][item.fileid] = true;
|
||||
|
||||
if (!initialScreenSelections[item.screenid]) {
|
||||
initialScreenSelections[item.screenid] = {};
|
||||
}
|
||||
if (!initialScreenSelections[item.screenid][item.fileid]) {
|
||||
initialScreenSelections[item.screenid][item.fileid] = {};
|
||||
}
|
||||
initialScreenSelections[item.screenid][item.fileid]["M"] = item.ismonday === 1;
|
||||
initialScreenSelections[item.screenid][item.fileid]["T"] = item.istuesday === 1;
|
||||
initialScreenSelections[item.screenid][item.fileid]["W"] = item.iswednesday === 1;
|
||||
initialScreenSelections[item.screenid][item.fileid]["Th"] = item.isthursday === 1;
|
||||
initialScreenSelections[item.screenid][item.fileid]["F"] = item.isfriday === 1;
|
||||
initialScreenSelections[item.screenid][item.fileid]["S"] = item.issaturday === 1;
|
||||
initialScreenSelections[item.screenid][item.fileid]["Su"] = item.issunday === 1;
|
||||
});
|
||||
|
||||
setSelectedFiles(initialSelectedFiles);
|
||||
setScreenSelections(initialScreenSelections);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return (
|
||||
|
||||
<div className="bg-gray-600 p-4 rounded-lg shadow-md text-white">
|
||||
{/* // <p className="font-bold text-2xl mb-3"> Ad Configurations</p> */}
|
||||
{screens.map(screen => {
|
||||
console.log(screen)
|
||||
const filesForScreen = data.filter(item => item.screenid === screen.transid);
|
||||
const uniqueFilesForScreen = [...new Set(filesForScreen.map(item => ({
|
||||
transid: item.fileid,
|
||||
file_name: item.file_path.split('/').pop() // Extract filename from path
|
||||
})))];
|
||||
|
||||
return (
|
||||
<div key={screen.transid} className="mb-6">
|
||||
<h3 className="text-lg font-semibold mb-2">{screen.name} </h3>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full border border-gray-500">
|
||||
<thead>
|
||||
<tr className="bg-gray-700">
|
||||
<th className="p-2 border">Ad Name</th>
|
||||
{daysOfWeek.map(day => (
|
||||
<th key={day} className="p-2 border">{day}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{uniqueFilesForScreen.map(file => {
|
||||
// Find the first occurrence of this file for this screen to get initial day states
|
||||
const fileData = filesForScreen.find(item => item.fileid === file.transid);
|
||||
if (!fileData) return null; // Should not happen if uniqueFilesForScreen is correct
|
||||
|
||||
return (
|
||||
<tr key={`${screen.transid}-${file.transid}`} className="border">
|
||||
<td className="p-2 border">
|
||||
<label className="flex items-center">
|
||||
{/* <input
|
||||
type="checkbox"
|
||||
className="mr-2"
|
||||
checked={selectedFiles[screen.transid]?.[file.transid] || false}
|
||||
onChange={() => null}
|
||||
/> */}
|
||||
<span className="text-sm truncate">[ {fileData.clientname} ] - {file.file_name.split("_")[1]}</span>
|
||||
</label>
|
||||
</td>
|
||||
{daysOfWeek.map(day => (
|
||||
<td key={day} className="p-2 border text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
|
||||
checked={screenSelections[screen.transid]?.[file.transid]?.[day] || (fileData[`is${day.toLowerCase()}`] === 1)}
|
||||
onChange={() => null}
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<br />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
17
src/Context/LoadingContext.jsx
Normal file
17
src/Context/LoadingContext.jsx
Normal file
@ -0,0 +1,17 @@
|
||||
import { createContext, useState, useContext } from "react";
|
||||
import LoadingScreen from "../Components/LoadingScreen";
|
||||
|
||||
const LoadingContext = createContext();
|
||||
|
||||
export const LoadingProvider = ({ children }) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
return (
|
||||
<LoadingContext.Provider value={{ loading, setLoading }}>
|
||||
{loading && <LoadingScreen />} {/* Show loading screen when loading is true */}
|
||||
{children}
|
||||
</LoadingContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useLoading = () => useContext(LoadingContext);
|
||||
129
src/Pages/AddClient.jsx
Normal file
129
src/Pages/AddClient.jsx
Normal file
@ -0,0 +1,129 @@
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { motion } from "framer-motion";
|
||||
import { FaArrowLeft } from "react-icons/fa";
|
||||
import { api } from "../API/api";
|
||||
import { useLoading } from "../Context/LoadingContext";
|
||||
|
||||
const AddClient = () => {
|
||||
const [formData, setFormData] = useState({ name: "", emailid: "", phoneno: "", address: "" });
|
||||
const { setLoading } = useLoading();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData({ ...formData, [name]: value });
|
||||
};
|
||||
|
||||
const handleFileChange = (e) => {
|
||||
setFormData({ ...formData, image: e.target.files[0] });
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
try {
|
||||
await api.post("/admin/add-client", formData);
|
||||
navigate("/clients");
|
||||
} catch (error) {
|
||||
console.error("Error adding Partner:", error);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white p-8">
|
||||
<motion.button
|
||||
className="mb-4 flex items-center text-gray-400 hover:text-white"
|
||||
whileHover={{ scale: 1.1 }}
|
||||
onClick={() => navigate("/clients")}
|
||||
>
|
||||
<FaArrowLeft className="mr-2" /> Back
|
||||
</motion.button>
|
||||
|
||||
<motion.h1 className="text-3xl font-bold mb-6">➕ Add New Client</motion.h1>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
|
||||
<div>
|
||||
|
||||
|
||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700">Client Name</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
id="name"
|
||||
placeholder="Client Name"
|
||||
onChange={handleChange}
|
||||
className="block w-full bg-gray-700 p-2 rounded-lg mb-2"
|
||||
required
|
||||
minLength="1" // Example: Add minimum length validation
|
||||
maxLength="255" //Example: add maximum length validation
|
||||
/>
|
||||
{/*
|
||||
<label htmlFor="logo_url" className="block text-sm font-medium text-gray-700">Logo</label>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
name="logo_url"
|
||||
id="logo_url"
|
||||
onChange={handleFileChange}
|
||||
className="block w-full bg-gray-700 p-2 rounded-lg mb-2"
|
||||
required
|
||||
/> */}
|
||||
|
||||
|
||||
|
||||
<label htmlFor="emailid" className="block text-sm font-medium text-gray-700">E-Mail ID</label>
|
||||
<input
|
||||
type="email"
|
||||
name="emailid"
|
||||
id="emailid"
|
||||
placeholder="E-Mail ID"
|
||||
onChange={handleChange}
|
||||
className="block w-full bg-gray-700 p-2 rounded-lg mb-2"
|
||||
required
|
||||
minLength="1" // Example: Add minimum length validation
|
||||
maxLength="255" //Example: add maximum length validation
|
||||
/>
|
||||
|
||||
<label htmlFor="phoneno" className="block text-sm font-medium text-gray-700">Phone Number</label>
|
||||
<input
|
||||
type="number"
|
||||
name="phoneno"
|
||||
id="phoneno"
|
||||
placeholder="Phone Number"
|
||||
onChange={handleChange}
|
||||
className="block w-full bg-gray-700 p-2 rounded-lg mb-2"
|
||||
required
|
||||
minLength="1" // Example: Add minimum length validation
|
||||
maxLength="255" //Example: add maximum length validation
|
||||
/>
|
||||
<label htmlFor="address" className="block text-sm font-medium text-gray-700">Address</label>
|
||||
<input
|
||||
type="text"
|
||||
name="address"
|
||||
id="address"
|
||||
placeholder="Address"
|
||||
onChange={handleChange}
|
||||
className="block w-full bg-gray-700 p-2 rounded-lg mb-2"
|
||||
required
|
||||
/>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
<motion.button
|
||||
type="submit"
|
||||
className="bg-blue-500 px-4 py-2 rounded-lg"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
>
|
||||
Submit
|
||||
</motion.button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddClient
|
||||
310
src/Pages/AddPartner.jsx
Normal file
310
src/Pages/AddPartner.jsx
Normal file
@ -0,0 +1,310 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { motion } from "framer-motion";
|
||||
import { FaArrowLeft } from "react-icons/fa";
|
||||
import { api } from "../API/api";
|
||||
import { useLoading } from "../Context/LoadingContext";
|
||||
import Select from 'react-select';
|
||||
import canada_cities from '../assets/canada_cities.json';
|
||||
|
||||
const AddPartner = () => {
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [formData, setFormData] = useState({
|
||||
name: "",
|
||||
open_time: "",
|
||||
close_time: "",
|
||||
image: null,
|
||||
address: "",
|
||||
city: "",
|
||||
state: "",
|
||||
pincode: "",
|
||||
screens: "",
|
||||
});
|
||||
const { setLoading } = useLoading();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData({ ...formData, [name]: value });
|
||||
};
|
||||
|
||||
|
||||
const handleFileUpload = async (file,id) => {
|
||||
if (!file) return;
|
||||
|
||||
setUploading(true);
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
formData.append("file_name", file.name);
|
||||
formData.append("client_id", id); // Attach client ID
|
||||
|
||||
try {
|
||||
console.log(formData)
|
||||
setLoading(true)
|
||||
await api.post("/files/update-partner-logo", formData, {
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
});
|
||||
navigate("/partners");
|
||||
|
||||
// Refresh file list after each successful upload
|
||||
//fetchFiles();
|
||||
} catch (error) {
|
||||
console.error(`File upload failed for ${file.name}:`, error);
|
||||
// Optionally, you could add some user feedback here for individual file failures
|
||||
} finally {
|
||||
setUploading(false); // Keep this false, as we're handling individual uploads
|
||||
setLoading(false)
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileChange = async (e) => {
|
||||
|
||||
setFormData({ ...formData, image: e.target.files[0] });
|
||||
};
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
console.log("iuytg")
|
||||
setLoading(true);
|
||||
var data = await api.post("/admin/add-partner", formData);
|
||||
console.log(data)
|
||||
console.log("iuytg")
|
||||
await handleFileUpload(formData.image,data.id);
|
||||
navigate("/Partners");
|
||||
} catch (error) {
|
||||
console.error("Error adding Partner:", error);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const [selectedProvince, setSelectedProvince] = useState(null);
|
||||
const [selectedCity, setSelectedCity] = useState(null);
|
||||
|
||||
const provinceOptions = useMemo(
|
||||
() =>
|
||||
Object.keys(canada_cities).map((province) => ({
|
||||
value: province,
|
||||
label: province,
|
||||
})),
|
||||
[canada_cities]
|
||||
);
|
||||
|
||||
const cityOptions = useMemo(
|
||||
() =>
|
||||
selectedProvince
|
||||
? canada_cities[selectedProvince.value].map((city) => ({
|
||||
value: city,
|
||||
label: city,
|
||||
}))
|
||||
: [],
|
||||
[selectedProvince, canada_cities]
|
||||
);
|
||||
|
||||
const handleProvinceChange = (selectedOption) => {
|
||||
setSelectedProvince(selectedOption);
|
||||
setSelectedCity(null);
|
||||
setFormData({ ...formData, state: selectedOption?.value || "" }); // Update state in formData
|
||||
};
|
||||
|
||||
const handleCityChange = (selectedOption) => {
|
||||
setSelectedCity(selectedOption);
|
||||
setFormData({ ...formData, city: selectedOption?.value || "" }); // Update city in formData
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white p-8">
|
||||
<motion.button
|
||||
className="mb-4 flex items-center text-gray-400 hover:text-white"
|
||||
whileHover={{ scale: 1.1 }}
|
||||
onClick={() => navigate("/admin-dashboard")}
|
||||
>
|
||||
<FaArrowLeft className="mr-2" /> Back
|
||||
</motion.button>
|
||||
|
||||
<motion.h1 className="text-3xl font-bold mb-6">➕ Add New Partner</motion.h1>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
|
||||
Partner Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
id="name"
|
||||
placeholder="Partner Name"
|
||||
onChange={handleChange}
|
||||
className="block w-full bg-gray-700 p-2 rounded-lg mb-2"
|
||||
required
|
||||
minLength="1"
|
||||
maxLength="255"
|
||||
/>
|
||||
|
||||
<label htmlFor="logo_url" className="block text-sm font-medium text-gray-700">
|
||||
Logo
|
||||
</label>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
name="logo_url"
|
||||
id="logo_url"
|
||||
onChange={handleFileChange}
|
||||
className="block w-full bg-gray-700 p-2 rounded-lg mb-2"
|
||||
/>
|
||||
|
||||
<label htmlFor="open_time" className="block text-sm font-medium text-gray-700">
|
||||
Open Time
|
||||
</label>
|
||||
<input
|
||||
type="time"
|
||||
name="open_time"
|
||||
id="open_time"
|
||||
onChange={handleChange}
|
||||
className="block w-full bg-gray-700 p-2 rounded-lg mb-2"
|
||||
required
|
||||
/>
|
||||
|
||||
<label htmlFor="close_time" className="block text-sm font-medium text-gray-700">
|
||||
Close Time
|
||||
</label>
|
||||
<input
|
||||
type="time"
|
||||
name="close_time"
|
||||
id="close_time"
|
||||
onChange={handleChange}
|
||||
className="block w-full bg-gray-700 p-2 rounded-lg mb-2"
|
||||
required
|
||||
/>
|
||||
|
||||
<label htmlFor="address" className="block text-sm font-medium text-gray-700">
|
||||
Address
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="address"
|
||||
id="address"
|
||||
placeholder="Address"
|
||||
onChange={handleChange}
|
||||
className="block w-full bg-gray-700 p-2 rounded-lg mb-2"
|
||||
required
|
||||
/>
|
||||
|
||||
<label htmlFor="state" className="block text-sm font-medium text-gray-700">
|
||||
Province
|
||||
</label>
|
||||
<Select
|
||||
options={provinceOptions}
|
||||
value={selectedProvince}
|
||||
onChange={handleProvinceChange}
|
||||
isSearchable
|
||||
className="block w-full mb-2"
|
||||
styles={{
|
||||
control: (provided) => ({
|
||||
...provided,
|
||||
backgroundColor: '#374151',
|
||||
color: 'white',
|
||||
borderRadius: '0.5rem',
|
||||
padding: '0.5rem',
|
||||
border: 'none',
|
||||
}),
|
||||
option: (provided, state) => ({
|
||||
...provided,
|
||||
backgroundColor: state.isSelected ? '#1f2937' : '#374151',
|
||||
color: 'white',
|
||||
}),
|
||||
singleValue: (provided) => ({
|
||||
...provided,
|
||||
color: 'white',
|
||||
}),
|
||||
input: (provided) => ({
|
||||
...provided,
|
||||
color: 'white',
|
||||
}),
|
||||
placeholder: (provided) => ({
|
||||
...provided,
|
||||
color: 'white',
|
||||
}),
|
||||
}}
|
||||
placeholder="Select Province"
|
||||
/>
|
||||
|
||||
<label htmlFor="city" className="block text-sm font-medium text-gray-700">
|
||||
City
|
||||
</label>
|
||||
<Select
|
||||
options={cityOptions}
|
||||
value={selectedCity}
|
||||
onChange={handleCityChange}
|
||||
isSearchable
|
||||
className="block w-full mb-2"
|
||||
styles={{
|
||||
control: (provided) => ({
|
||||
...provided,
|
||||
backgroundColor: '#374151',
|
||||
color: 'white',
|
||||
borderRadius: '0.5rem',
|
||||
padding: '0.5rem',
|
||||
border: 'none',
|
||||
}),
|
||||
option: (provided, state) => ({
|
||||
...provided,
|
||||
backgroundColor: state.isSelected ? '#1f2937' : '#374151',
|
||||
color: 'white',
|
||||
}),
|
||||
singleValue: (provided) => ({
|
||||
...provided,
|
||||
color: 'white',
|
||||
}),
|
||||
input: (provided) => ({
|
||||
...provided,
|
||||
color: 'white',
|
||||
}),
|
||||
placeholder: (provided) => ({
|
||||
...provided,
|
||||
color: 'white',
|
||||
}),
|
||||
}}
|
||||
placeholder="Select City"
|
||||
isDisabled={!selectedProvince}
|
||||
/>
|
||||
|
||||
<label htmlFor="pincode" className="block text-sm font-medium text-gray-700">
|
||||
Pincode
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="pincode"
|
||||
id="pincode"
|
||||
placeholder="Pincode"
|
||||
onChange={handleChange}
|
||||
className="block w-full bg-gray-700 p-2 rounded-lg mb-2"
|
||||
required
|
||||
/>
|
||||
|
||||
<label htmlFor="screens" className="block text-sm font-medium text-gray-700">
|
||||
No. of. Screens
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
name="screens"
|
||||
id="screens"
|
||||
placeholder="No. of. Screens"
|
||||
onChange={handleChange}
|
||||
className="block w-full bg-gray-700 p-2 rounded-lg mb-2"
|
||||
required
|
||||
min="1"
|
||||
/>
|
||||
</div>
|
||||
<motion.button
|
||||
type="submit"
|
||||
className="bg-blue-500 px-4 py-2 rounded-lg"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
>
|
||||
Submit
|
||||
</motion.button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddPartner;
|
||||
55
src/Pages/AdminDashboard.jsx
Normal file
55
src/Pages/AdminDashboard.jsx
Normal file
@ -0,0 +1,55 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { motion } from "framer-motion";
|
||||
import { FaPlus, FaCircle } from "react-icons/fa";
|
||||
import { api } from "../API/api";
|
||||
import { useLoading } from "../Context/LoadingContext";
|
||||
import HoveringCardComponent from "../Components/HoveringCardComponent";
|
||||
import AdminDashboardComponent from "../Components/AdminDashboardComponent";
|
||||
|
||||
const AdminDashboard = () => {
|
||||
const [clients, setClients] = useState([]);
|
||||
const navigate = useNavigate();
|
||||
const { setLoading } = useLoading();
|
||||
|
||||
// useEffect(() => {
|
||||
// const fetchClients = async () => {
|
||||
// try {
|
||||
// setLoading(true);
|
||||
// const response = await api.get("/admin/clients");
|
||||
|
||||
// const updatedClients = response.map(client => ({
|
||||
// ...client,
|
||||
// is_active: client.status === "Offline" ? null : true,
|
||||
// }));
|
||||
|
||||
// setClients(updatedClients);
|
||||
|
||||
|
||||
// } catch (error) {
|
||||
// console.error("Error fetching clients:", error);
|
||||
// } finally {
|
||||
// setLoading(false);
|
||||
// }
|
||||
// };
|
||||
// fetchClients();
|
||||
// }, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white p-8">
|
||||
<motion.h1
|
||||
className="text-3xl font-bold text-center mb-8"
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
>
|
||||
🏢 Admin Dashboard - Client List
|
||||
</motion.h1>
|
||||
|
||||
<div >
|
||||
<AdminDashboardComponent />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminDashboard;
|
||||
131
src/Pages/Ads copy 2.jsx
Normal file
131
src/Pages/Ads copy 2.jsx
Normal file
@ -0,0 +1,131 @@
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { api } from "../API/api";
|
||||
|
||||
const AdsPage = () => {
|
||||
const { partnerid, screenName } = useParams();
|
||||
const [ads, setAds] = useState([]);
|
||||
const [idx, setIdx] = useState(0);
|
||||
const vidRef = useRef(null);
|
||||
const [isVidPlay, setVidPlay] = useState(false);
|
||||
const [adsLoaded, setAdsLoaded] = useState(false);
|
||||
const [cycleCount, setCycleCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
console.log("Fetching New Ads List");
|
||||
alert("Fetching new ads list...");
|
||||
setAds([]);
|
||||
const fetchedAds = await api.get(`/client/ads/${partnerid}/${screenName}`);
|
||||
console.log("Fetched Ads:", fetchedAds);
|
||||
alert(`Fetched ${fetchedAds.length} ads`);
|
||||
|
||||
const sortedAds = fetchedAds.sort((a, b) => a.transid - b.transid);
|
||||
setAds(sortedAds);
|
||||
} catch (e) {
|
||||
console.error("Ads error:", e);
|
||||
alert("Error fetching ads: " + e.message);
|
||||
}
|
||||
setAdsLoaded(true);
|
||||
})();
|
||||
}, [partnerid, screenName, cycleCount]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!adsLoaded || !ads.length) return;
|
||||
const ad = ads[idx];
|
||||
alert(`Displaying ad ${idx + 1}/${ads.length}: ${ad.file_path}`);
|
||||
|
||||
const isVid = /\.(mp4|webm|ogg|mkv)$/i.test(ad.file_path);
|
||||
let timer;
|
||||
|
||||
if (isVid) {
|
||||
alert(`🟢 Preparing to play video: ${ad.file_path}`);
|
||||
setVidPlay(true);
|
||||
if (vidRef.current) {
|
||||
vidRef.current.src = `https://ads.dine360ads.com/api/${ad.file_path}`;
|
||||
vidRef.current.load();
|
||||
vidRef.current
|
||||
.play()
|
||||
.then(() => {
|
||||
alert(`✅ Video started playing: ${ad.file_path}`);
|
||||
})
|
||||
.catch((err) => {
|
||||
alert(`❌ Video failed to play: ${err.message}`);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setVidPlay(false);
|
||||
alert(`🖼️ Showing image: ${ad.file_path}`);
|
||||
timer = setTimeout(() => {
|
||||
if (idx === ads.length - 1) {
|
||||
setCycleCount((prev) => prev + 1);
|
||||
}
|
||||
setIdx((p) => (p + 1) % ads.length);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [ads, idx, adsLoaded]);
|
||||
|
||||
const vidEnd = () => {
|
||||
alert("🔁 Video ended, moving to next ad...");
|
||||
setVidPlay(false);
|
||||
setIdx((p) => (p + 1) % ads.length);
|
||||
};
|
||||
|
||||
const handleVideoError = () => {
|
||||
alert("❗ Video playback error! Skipping to next ad.");
|
||||
setVidPlay(false);
|
||||
setIdx((p) => (p + 1) % ads.length);
|
||||
};
|
||||
|
||||
const handleVideoPause = () => {
|
||||
alert("⏸️ Video paused.");
|
||||
};
|
||||
|
||||
const handleVideoPlay = () => {
|
||||
alert("▶️ Video is playing.");
|
||||
};
|
||||
|
||||
const handleVideoLoaded = () => {
|
||||
alert("🔄 Video loaded successfully.");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-screen w-screen bg-black flex items-center justify-center overflow-hidden">
|
||||
<AnimatePresence mode="wait">
|
||||
{adsLoaded && ads.length > 0 && (
|
||||
isVidPlay ? (
|
||||
<video
|
||||
key={ads[idx].file_path}
|
||||
ref={vidRef}
|
||||
className="w-full h-full object-contain absolute"
|
||||
autoPlay
|
||||
src={`https://ads.dine360ads.com/api//${ads[idx].file_path}`}
|
||||
muted
|
||||
onEnded={vidEnd}
|
||||
onError={handleVideoError}
|
||||
onPause={handleVideoPause}
|
||||
onPlay={handleVideoPlay}
|
||||
onLoadedData={handleVideoLoaded}
|
||||
/>
|
||||
) : (
|
||||
<motion.img
|
||||
key={ads[idx].file_path}
|
||||
src={`https://ads.dine360ads.com/api/${ads[idx].file_path}`}
|
||||
className="w-full h-full object-contain absolute"
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 1.1 }}
|
||||
transition={{ duration: 1 }}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdsPage;
|
||||
162
src/Pages/Ads.jsx
Normal file
162
src/Pages/Ads.jsx
Normal file
@ -0,0 +1,162 @@
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { api } from "../API/api";
|
||||
|
||||
const AdsPage = () => {
|
||||
const { partnerid, screenName } = useParams();
|
||||
const [ads, setAds] = useState([]);
|
||||
const [idx, setIdx] = useState(0);
|
||||
const vidRef = useRef(null);
|
||||
const [isVidPlay, setVidPlay] = useState(false);
|
||||
const [adsLoaded, setAdsLoaded] = useState(false);
|
||||
const [cycleCount, setCycleCount] = useState(0);
|
||||
const [delay, setDelay] = useState(1); // Default delay
|
||||
// const [imageDuration, setImageDuration] = useState(5); // No longer a common default
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
console.log('Fetching New Files List');
|
||||
setAds([]);
|
||||
const fetchedAds = await api.get(`/client/ads/${partnerid}/${screenName}`);
|
||||
const sortedAds = fetchedAds.sort((a, b) => a.transid - b.transid);
|
||||
console.log(sortedAds);
|
||||
setAds(sortedAds);
|
||||
} catch (e) {
|
||||
console.error("Ads error:", e);
|
||||
}
|
||||
setAdsLoaded(true);
|
||||
})();
|
||||
|
||||
// Fetch settings (delay still needed for logging interval)
|
||||
(async () => {
|
||||
try {
|
||||
const settings = await api.get("/client/get-settings");
|
||||
if (settings && settings.length >= 2) {
|
||||
setDelay(parseInt(settings[1].value, 10) || 1);
|
||||
// setImageDuration is no longer needed here
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Settings error:", e);
|
||||
}
|
||||
})();
|
||||
}, [partnerid, screenName, cycleCount]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!adsLoaded || !ads.length) return;
|
||||
const ad = ads[idx];
|
||||
const isVid = /\.(mp4|webm|ogg|mkv)$/i.test(ad.file_path);
|
||||
let timer;
|
||||
|
||||
if (isVid) {
|
||||
setVidPlay(true);
|
||||
vidRef.current?.play().catch(() => {});
|
||||
} else {
|
||||
setVidPlay(false);
|
||||
// Use the image duration specific to the current ad
|
||||
const currentImageDuration = ad.imageduration !== undefined ? parseInt(ad.imageduration, 10) : 5; // Default to 5 if not present
|
||||
|
||||
timer = setTimeout(() => {
|
||||
if (idx === ads.length - 1) {
|
||||
setCycleCount((prev) => prev + 1);
|
||||
}
|
||||
setIdx((p) => (p + 1) % ads.length);
|
||||
}, currentImageDuration * 1000);
|
||||
}
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [ads, idx, adsLoaded]); // Removed imageDuration from dependency array
|
||||
|
||||
const vidEnd = () => {
|
||||
setVidPlay(false);
|
||||
setIdx((p) => (p + 1) % ads.length);
|
||||
};
|
||||
|
||||
const logAd = async () => {
|
||||
if (!adsLoaded || !ads.length) return console.log("Ads skip log.");
|
||||
try {
|
||||
if (ads[idx]) {
|
||||
await api.post("/client/add-screen-log", {
|
||||
screen_id: ads[0]?.screenid,
|
||||
screen_name: screenName,
|
||||
partner_id: ads[0]?.partnerid,
|
||||
status: "active",
|
||||
});
|
||||
console.log("Logged");
|
||||
} else console.log("Ad idx out.");
|
||||
} catch (e) {
|
||||
console.error("Log error:", e);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!adsLoaded) return;
|
||||
|
||||
const logTimer = setInterval(() => {
|
||||
if (!document.hidden) {
|
||||
logAd();
|
||||
} else {
|
||||
console.log("Skipped logging: Tab is inactive");
|
||||
}
|
||||
}, 60 * delay * 1000);
|
||||
|
||||
return () => clearInterval(logTimer);
|
||||
}, [adsLoaded, delay]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleVisibilityChange = async () => {
|
||||
if (document.hidden) {
|
||||
try {
|
||||
console.log("User minimized or switched tab");
|
||||
await api.post("/client/idle-screen-log", {
|
||||
screen_id: ads[0]?.screenid,
|
||||
screen_name: screenName,
|
||||
partner_id: ads[0]?.partnerid,
|
||||
status: "inactive",
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Visibility Log error:", e);
|
||||
}
|
||||
} else {
|
||||
console.log("User visible");
|
||||
logAd();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("visibilitychange", handleVisibilityChange);
|
||||
return () => document.removeEventListener("visibilitychange", handleVisibilityChange);
|
||||
}, [ads, adsLoaded, screenName]);
|
||||
|
||||
return (
|
||||
<div className="h-screen w-screen bg-black flex items-center justify-center overflow-hidden">
|
||||
<AnimatePresence mode="wait">
|
||||
{adsLoaded && ads.length > 0 && (
|
||||
isVidPlay ? (
|
||||
<video
|
||||
key={ads[idx].file_path}
|
||||
ref={vidRef}
|
||||
src={`https://ads.dine360ads.com/api//${ads[idx].file_path}`}
|
||||
className="w-full h-full object-contain absolute"
|
||||
autoPlay
|
||||
muted
|
||||
onEnded={vidEnd}
|
||||
/>
|
||||
) : (
|
||||
<motion.img
|
||||
key={ads[idx].file_path}
|
||||
src={`https://ads.dine360ads.com/api//${ads[idx].file_path}`}
|
||||
className="w-full h-full object-contain absolute"
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 1.1 }}
|
||||
transition={{ duration: 1 }}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdsPage;
|
||||
159
src/Pages/Ads_Bak_with common ad duration.jsx
Normal file
159
src/Pages/Ads_Bak_with common ad duration.jsx
Normal file
@ -0,0 +1,159 @@
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { api } from "../API/api";
|
||||
|
||||
const AdsPage = () => {
|
||||
const { partnerid, screenName } = useParams();
|
||||
const [ads, setAds] = useState([]);
|
||||
const [idx, setIdx] = useState(0);
|
||||
const vidRef = useRef(null);
|
||||
const [isVidPlay, setVidPlay] = useState(false);
|
||||
const [adsLoaded, setAdsLoaded] = useState(false);
|
||||
const [cycleCount, setCycleCount] = useState(0);
|
||||
const [delay, setDelay] = useState(1); // Default delay
|
||||
const [imageDuration, setImageDuration] = useState(5); // Default image duration
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
console.log('Fetching New Files List');
|
||||
setAds([]);
|
||||
const fetchedAds = await api.get(`/client/ads/${partnerid}/${screenName}`);
|
||||
const sortedAds = fetchedAds.sort((a, b) => a.transid - b.transid);
|
||||
console.log(sortedAds);
|
||||
setAds(sortedAds);
|
||||
} catch (e) {
|
||||
console.error("Ads error:", e);
|
||||
}
|
||||
setAdsLoaded(true);
|
||||
})();
|
||||
|
||||
// Fetch settings
|
||||
(async () => {
|
||||
try {
|
||||
const settings = await api.get("/client/get-settings");
|
||||
if (settings && settings.length >= 2) {
|
||||
setDelay(parseInt(settings[1].value, 10) || 1);
|
||||
setImageDuration(parseInt(settings[0].value, 10) || 5);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Settings error:", e);
|
||||
}
|
||||
})();
|
||||
}, [partnerid, screenName, cycleCount]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!adsLoaded || !ads.length) return;
|
||||
const ad = ads[idx];
|
||||
const isVid = /\.(mp4|webm|ogg|mkv)$/i.test(ad.file_path);
|
||||
let timer;
|
||||
|
||||
if (isVid) {
|
||||
setVidPlay(true);
|
||||
vidRef.current?.play().catch(() => {});
|
||||
} else {
|
||||
setVidPlay(false);
|
||||
timer = setTimeout(() => {
|
||||
if (idx === ads.length - 1) {
|
||||
setCycleCount((prev) => prev + 1);
|
||||
}
|
||||
setIdx((p) => (p + 1) % ads.length);
|
||||
}, imageDuration * 1000);
|
||||
}
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [ads, idx, adsLoaded, imageDuration]);
|
||||
|
||||
const vidEnd = () => {
|
||||
setVidPlay(false);
|
||||
setIdx((p) => (p + 1) % ads.length);
|
||||
};
|
||||
|
||||
const logAd = async () => {
|
||||
if (!adsLoaded || !ads.length) return console.log("Ads skip log.");
|
||||
try {
|
||||
if (ads[idx]) {
|
||||
await api.post("/client/add-screen-log", {
|
||||
screen_id: ads[0]?.screenid,
|
||||
screen_name: screenName,
|
||||
partner_id: ads[0]?.partnerid,
|
||||
status: "active",
|
||||
});
|
||||
console.log("Logged");
|
||||
} else console.log("Ad idx out.");
|
||||
} catch (e) {
|
||||
console.error("Log error:", e);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!adsLoaded) return;
|
||||
|
||||
const logTimer = setInterval(() => {
|
||||
if (!document.hidden) {
|
||||
logAd();
|
||||
} else {
|
||||
console.log("Skipped logging: Tab is inactive");
|
||||
}
|
||||
}, 60 * delay * 1000);
|
||||
|
||||
return () => clearInterval(logTimer);
|
||||
}, [adsLoaded, delay]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleVisibilityChange = async () => {
|
||||
if (document.hidden) {
|
||||
try {
|
||||
console.log("User minimized or switched tab");
|
||||
await api.post("/client/idle-screen-log", {
|
||||
screen_id: ads[0]?.screenid,
|
||||
screen_name: screenName,
|
||||
partner_id: ads[0]?.partnerid,
|
||||
status: "inactive",
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Visibility Log error:", e);
|
||||
}
|
||||
} else {
|
||||
console.log("User visible");
|
||||
logAd();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("visibilitychange", handleVisibilityChange);
|
||||
return () => document.removeEventListener("visibilitychange", handleVisibilityChange);
|
||||
}, [ads, adsLoaded, screenName]);
|
||||
|
||||
return (
|
||||
<div className="h-screen w-screen bg-black flex items-center justify-center overflow-hidden">
|
||||
<AnimatePresence mode="wait">
|
||||
{adsLoaded && ads.length > 0 && (
|
||||
isVidPlay ? (
|
||||
<video
|
||||
key={ads[idx].file_path}
|
||||
ref={vidRef}
|
||||
src={`https://ads.dine360ads.com/api//${ads[idx].file_path}`}
|
||||
className="w-full h-full object-contain absolute"
|
||||
autoPlay
|
||||
muted
|
||||
onEnded={vidEnd}
|
||||
/>
|
||||
) : (
|
||||
<motion.img
|
||||
key={ads[idx].file_path}
|
||||
src={`https://ads.dine360ads.com/api//${ads[idx].file_path}`}
|
||||
className="w-full h-full object-contain absolute"
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 1.1 }}
|
||||
transition={{ duration: 1 }}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdsPage;
|
||||
240
src/Pages/Ads_log.jsx
Normal file
240
src/Pages/Ads_log.jsx
Normal file
@ -0,0 +1,240 @@
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { api } from "../API/api";
|
||||
import Swal from "sweetalert2";
|
||||
|
||||
const AdsPage = () => {
|
||||
const { partnerid, screenName } = useParams();
|
||||
const [ads, setAds] = useState([]);
|
||||
const [idx, setIdx] = useState(0);
|
||||
const vidRef = useRef(null);
|
||||
const [isVidPlay, setVidPlay] = useState(false);
|
||||
const [adsLoaded, setAdsLoaded] = useState(false);
|
||||
const [cycleCount, setCycleCount] = useState(0);
|
||||
const [toastQueue, setToastQueue] = useState([]);
|
||||
const [isToastActive, setIsToastActive] = useState(false);
|
||||
|
||||
const [delay, setDelay] = useState(1); // Default delay
|
||||
const [imageDuration, setImageDuration] = useState(5); // Default image duration
|
||||
|
||||
|
||||
const addLog = (message, icon = "info") => {
|
||||
//setToastQueue((prev) => [...prev, { message, icon }]);
|
||||
};
|
||||
|
||||
const showNextToast = () => {
|
||||
if (toastQueue.length === 0 || isToastActive) return;
|
||||
|
||||
setIsToastActive(true);
|
||||
const { message, icon } = toastQueue[0];
|
||||
|
||||
Swal.fire({
|
||||
title: message,
|
||||
icon: icon,
|
||||
toast: true,
|
||||
position: "top-end",
|
||||
showConfirmButton: false,
|
||||
timer: 2000,
|
||||
timerProgressBar: true,
|
||||
didClose: () => {
|
||||
setToastQueue((prev) => prev.slice(1));
|
||||
setIsToastActive(false);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isToastActive && toastQueue.length > 0) {
|
||||
showNextToast();
|
||||
}
|
||||
}, [toastQueue, isToastActive]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
addLog("Fetching new ads list...");
|
||||
setAds([]);
|
||||
|
||||
const fetchedAds = await api.get(`/client/ads/${partnerid}/${screenName}`);
|
||||
addLog(`Fetched ${fetchedAds.length} ads`, "success");
|
||||
|
||||
const sortedAds = fetchedAds.sort((a, b) => a.transid - b.transid);
|
||||
setAds(sortedAds);
|
||||
} catch (e) {
|
||||
console.error("Ads error:", e);
|
||||
addLog("Error fetching ads: " + e.message, "error");
|
||||
}
|
||||
setAdsLoaded(true);
|
||||
})();
|
||||
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const settings = await api.get("/client/get-settings");
|
||||
if (settings && settings.length >= 2) {
|
||||
setDelay(parseInt(settings[1].value, 10) || 1);
|
||||
setImageDuration(parseInt(settings[0].value, 10) || 5);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Settings error:", e);
|
||||
}
|
||||
})();
|
||||
}, [partnerid, screenName, cycleCount]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!adsLoaded || ads.length === 0) return;
|
||||
|
||||
const ad = ads[idx];
|
||||
addLog(`Displaying ad ${idx + 1}/${ads.length}`);
|
||||
|
||||
const isVid = /\.(mp4|webm|ogg|mkv)$/i.test(ad.file_path);
|
||||
let timer;
|
||||
|
||||
if (isVid) {
|
||||
addLog(`🟢 Preparing to play video: ${ad.file_path}`);
|
||||
setVidPlay(true);
|
||||
|
||||
if (vidRef.current) {
|
||||
vidRef.current.src = `https://ads.dine360ads.com/api/${ad.file_path}`;
|
||||
vidRef.current.load();
|
||||
vidRef.current
|
||||
.play()
|
||||
.then(() => addLog(`✅ Video started playing`, "success"))
|
||||
.catch((err) => addLog(`❌ Video failed to play: ${err.message}`, "error"));
|
||||
}
|
||||
} else {
|
||||
setVidPlay(false);
|
||||
addLog(`🖼️ Showing image`);
|
||||
|
||||
timer = setTimeout(() => {
|
||||
if (idx === ads.length - 1) {
|
||||
setCycleCount((prev) => prev + 1);
|
||||
}
|
||||
setIdx((p) => (p + 1) % ads.length);
|
||||
}, imageDuration * 1000);
|
||||
}
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [ads, idx, adsLoaded]);
|
||||
|
||||
const vidEnd = () => {
|
||||
addLog("🔁 Video ended, moving to next ad...");
|
||||
setVidPlay(false);
|
||||
setIdx((p) => (p + 1) % ads.length);
|
||||
};
|
||||
|
||||
const handleVideoError = () => {
|
||||
addLog("❗ Video playback error! Skipping to next ad.", "error");
|
||||
setVidPlay(false);
|
||||
setIdx((p) => (p + 1) % ads.length);
|
||||
};
|
||||
|
||||
|
||||
const logAd = async () => {
|
||||
if (!adsLoaded || !ads.length) return console.log("Ads skip log.");
|
||||
try {
|
||||
if (ads[idx]) {
|
||||
await api.post("/client/add-screen-log", {
|
||||
screen_id: ads[0]?.screenid,
|
||||
screen_name: screenName,
|
||||
partner_id: ads[0]?.partnerid,
|
||||
status: "active",
|
||||
});
|
||||
console.log("Logged");
|
||||
} else console.log("Ad idx out.");
|
||||
} catch (e) {
|
||||
console.error("Log error:", e);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!adsLoaded) return;
|
||||
|
||||
const logTimer = setInterval(() => {
|
||||
if (!document.hidden) {
|
||||
logAd();
|
||||
} else {
|
||||
console.log("Skipped logging: Tab is inactive");
|
||||
}
|
||||
}, 60 * delay * 1000);
|
||||
|
||||
return () => clearInterval(logTimer);
|
||||
}, [adsLoaded, delay]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleVisibilityChange = async () => {
|
||||
if (document.hidden) {
|
||||
try {
|
||||
console.log("User minimized or switched tab");
|
||||
await api.post("/client/idle-screen-log", {
|
||||
screen_id: ads[0]?.screenid,
|
||||
screen_name: screenName,
|
||||
partner_id: ads[0]?.partnerid,
|
||||
status: "inactive",
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Visibility Log error:", e);
|
||||
}
|
||||
} else {
|
||||
console.log("User visible");
|
||||
logAd();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("visibilitychange", handleVisibilityChange);
|
||||
return () => document.removeEventListener("visibilitychange", handleVisibilityChange);
|
||||
}, [ads, adsLoaded, screenName]);
|
||||
|
||||
return (
|
||||
<div className="h-screen w-screen bg-black flex items-center justify-center overflow-hidden relative">
|
||||
<AnimatePresence mode="wait">
|
||||
{adsLoaded && ads.length > 0 && (
|
||||
isVidPlay ? (
|
||||
// <video
|
||||
// key={ads[idx].file_path}
|
||||
// ref={vidRef}
|
||||
// className="w-full h-full object-contain absolute"
|
||||
// autoPlay
|
||||
// src={`https://ads.dine360ads.com/api/${ads[idx].file_path}`}
|
||||
// muted
|
||||
// onEnded={vidEnd}
|
||||
// onError={handleVideoError}
|
||||
// />
|
||||
|
||||
|
||||
|
||||
|
||||
<video
|
||||
key={ads[idx].file_path}
|
||||
ref={vidRef}
|
||||
className="w-full h-full object-contain absolute"
|
||||
autoPlay
|
||||
// controls
|
||||
src={`https://ads.dine360ads.com/api/${ads[idx].file_path}`}
|
||||
muted
|
||||
onEnded={vidEnd}
|
||||
onError={handleVideoError}
|
||||
onPlay={() => addLog("✅ Video is playing")}
|
||||
onPause={() => addLog("⏸️ Video is paused")}
|
||||
onLoadedMetadata={() => addLog("📂 Video metadata loaded")}
|
||||
/>
|
||||
|
||||
) : (
|
||||
<motion.img
|
||||
key={ads[idx].file_path}
|
||||
src={`https://ads.dine360ads.com/api/${ads[idx].file_path}`}
|
||||
className="w-full h-full object-contain absolute"
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 1.1 }}
|
||||
transition={{ duration: 1 }}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdsPage;
|
||||
53
src/Pages/ClientPartnerMapping.jsx
Normal file
53
src/Pages/ClientPartnerMapping.jsx
Normal file
@ -0,0 +1,53 @@
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { motion } from "framer-motion";
|
||||
import { FaPlus, FaCircle } from "react-icons/fa";
|
||||
import { api } from "../API/api";
|
||||
import { useLoading } from "../Context/LoadingContext";
|
||||
import PartnersCardComponent from "../Components/PartnersPageCardComponent";
|
||||
import ClientsCardComponent from "../Components/ClientsPageCardComponent";
|
||||
import MappingComponent from "../Components/MappingComponent";
|
||||
|
||||
const ClientPartnerMapping = () => {
|
||||
const [clients, setClients] = useState([]);
|
||||
const [upd, setUpd] = useState(0);
|
||||
const navigate = useNavigate();
|
||||
const { setLoading } = useLoading();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchClients = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await api.get("/admin/partners");
|
||||
|
||||
setClients(response);
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error fetching clients:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchClients();
|
||||
}, [upd]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white p-8">
|
||||
<motion.h1
|
||||
className="text-3xl font-bold text-center mb-8"
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
>
|
||||
Ads Configuration
|
||||
</motion.h1>
|
||||
|
||||
|
||||
<MappingComponent Data={clients} setUpd={setUpd} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClientPartnerMapping
|
||||
|
||||
52
src/Pages/ClientsPage.jsx
Normal file
52
src/Pages/ClientsPage.jsx
Normal file
@ -0,0 +1,52 @@
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { motion } from "framer-motion";
|
||||
import { FaPlus, FaCircle } from "react-icons/fa";
|
||||
import { api } from "../API/api";
|
||||
import { useLoading } from "../Context/LoadingContext";
|
||||
import PartnersCardComponent from "../Components/PartnersPageCardComponent";
|
||||
import ClientsCardComponent from "../Components/ClientsPageCardComponent";
|
||||
|
||||
const ClientsPage = () => {
|
||||
const [clients, setClients] = useState([]);
|
||||
const [upd, setUpd] = useState(0);
|
||||
const navigate = useNavigate();
|
||||
const { setLoading } = useLoading();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchClients = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await api.get("/admin/clients");
|
||||
|
||||
setClients(response);
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error fetching clients:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchClients();
|
||||
}, [upd]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white p-8">
|
||||
<motion.h1
|
||||
className="text-3xl font-bold text-center mb-8"
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
>
|
||||
Clients List
|
||||
</motion.h1>
|
||||
|
||||
|
||||
<ClientsCardComponent Data={clients} setUpd={setUpd} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClientsPage
|
||||
|
||||
131
src/Pages/Login copy.jsx
Normal file
131
src/Pages/Login copy.jsx
Normal file
@ -0,0 +1,131 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { motion } from "framer-motion";
|
||||
import { FaLock, FaUser } from "react-icons/fa";
|
||||
|
||||
const Login = () => {
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleLogin = (e) => {
|
||||
e.preventDefault();
|
||||
if (username === "admin" && password === "admin") {
|
||||
localStorage.setItem('loggedIn', JSON.stringify({
|
||||
timestamp: Date.now(),
|
||||
value: true,
|
||||
}));
|
||||
navigate("/admin-dashboard");
|
||||
} else {
|
||||
setError("❌ Invalid credentials! Try again.");
|
||||
}
|
||||
};
|
||||
|
||||
const [floors, setFloors] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const a = "482beca79d9c005";
|
||||
const b = "b8778f51fcca82b";
|
||||
const authHeader = `token ${a}:${b}`;
|
||||
const url = "http://82.25.105.135:8004/api/resource/Dine360%20Room?fields=%5B%22*%22%5D&limit_page_length=100&filters=%5B%5B%22floor%22%2C%22%3D%22%2C%223%22%5D%5D";
|
||||
|
||||
fetch(url, {
|
||||
headers: {
|
||||
Authorization: authHeader,
|
||||
},
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
setFloors(data.data);
|
||||
console.log("regr", data.data)
|
||||
})
|
||||
.catch((err) => console.error(err));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="h-screen flex items-center justify-center bg-gray-900 text-white">
|
||||
<motion.div
|
||||
className="bg-gray-800 p-8 rounded-lg shadow-xl w-96"
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
{/* Animated Heading */}
|
||||
<motion.h2
|
||||
className="text-2xl font-bold text-center mb-6"
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.3 }}
|
||||
>
|
||||
🔐 Admin Login
|
||||
</motion.h2>
|
||||
|
||||
{/* Input Fields */}
|
||||
<form onSubmit={handleLogin} className="space-y-4">
|
||||
<div className="flex items-center border-b-2 border-gray-600 py-2">
|
||||
<FaUser className="text-gray-400 mr-2" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
className="w-full bg-transparent border-none focus:outline-none text-lg"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center border-b-2 border-gray-600 py-2">
|
||||
<FaLock className="text-gray-400 mr-2" />
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
className="w-full bg-transparent border-none focus:outline-none text-lg"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Error Message Animation */}
|
||||
{error && (
|
||||
<motion.p
|
||||
className="text-red-400 text-center"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
>
|
||||
{error}
|
||||
</motion.p>
|
||||
)}
|
||||
|
||||
{/* Login Button */}
|
||||
<motion.button
|
||||
type="submit"
|
||||
className="w-full bg-blue-600 py-2 mt-4 rounded-lg text-lg font-semibold hover:bg-blue-700 transition duration-300"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
🚀 Login
|
||||
</motion.button>
|
||||
</form>
|
||||
</motion.div>
|
||||
|
||||
{/* Floating Emojis Animation */}
|
||||
<motion.div
|
||||
className="absolute top-10 left-10 text-5xl"
|
||||
animate={{ y: [0, -15, 0] }}
|
||||
transition={{ repeat: Infinity, duration: 2 }}
|
||||
>
|
||||
🔑
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="absolute bottom-10 right-10 text-5xl"
|
||||
animate={{ rotate: [0, 20, -20, 0] }}
|
||||
transition={{ repeat: Infinity, duration: 2 }}
|
||||
>
|
||||
🔥
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
131
src/Pages/Login.jsx
Normal file
131
src/Pages/Login.jsx
Normal file
@ -0,0 +1,131 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { motion } from "framer-motion";
|
||||
import { FaLock, FaUser } from "react-icons/fa";
|
||||
|
||||
const Login = () => {
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleLogin = (e) => {
|
||||
e.preventDefault();
|
||||
if (username === "admin" && password === "admin") {
|
||||
localStorage.setItem('loggedIn', JSON.stringify({
|
||||
timestamp: Date.now(),
|
||||
value: true,
|
||||
}));
|
||||
navigate("/admin-dashboard");
|
||||
} else {
|
||||
setError("❌ Invalid credentials! Try again.");
|
||||
}
|
||||
};
|
||||
|
||||
const [floors, setFloors] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const a = "482beca79d9c005";
|
||||
const b = "b8778f51fcca82b";
|
||||
const authHeader = `token ${a}:${b}`;
|
||||
const url = "http://82.25.105.135:8004/api/resource/Dine360%20Room?fields=%5B%22*%22%5D&limit_page_length=100&filters=%5B%5B%22floor%22%2C%22%3D%22%2C%223%22%5D%5D";
|
||||
|
||||
fetch(url, {
|
||||
headers: {
|
||||
Authorization: authHeader,
|
||||
},
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
setFloors(data.data);
|
||||
console.log("regr", data.data)
|
||||
})
|
||||
.catch((err) => console.error(err));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="h-screen flex items-center justify-center bg-gray-900 text-white">
|
||||
<motion.div
|
||||
className="bg-gray-800 p-8 rounded-lg shadow-xl w-96"
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
{/* Animated Heading */}
|
||||
<motion.h2
|
||||
className="text-2xl font-bold text-center mb-6"
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.3 }}
|
||||
>
|
||||
🔐 Admin Login
|
||||
</motion.h2>
|
||||
|
||||
{/* Input Fields */}
|
||||
<form onSubmit={handleLogin} className="space-y-4">
|
||||
<div className="flex items-center border-b-2 border-gray-600 py-2">
|
||||
<FaUser className="text-gray-400 mr-2" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
className="w-full bg-transparent border-none focus:outline-none text-lg"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center border-b-2 border-gray-600 py-2">
|
||||
<FaLock className="text-gray-400 mr-2" />
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
className="w-full bg-transparent border-none focus:outline-none text-lg"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Error Message Animation */}
|
||||
{error && (
|
||||
<motion.p
|
||||
className="text-red-400 text-center"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
>
|
||||
{error}
|
||||
</motion.p>
|
||||
)}
|
||||
|
||||
{/* Login Button */}
|
||||
<motion.button
|
||||
type="submit"
|
||||
className="w-full bg-blue-600 py-2 mt-4 rounded-lg text-lg font-semibold hover:bg-blue-700 transition duration-300"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
🚀 Login
|
||||
</motion.button>
|
||||
</form>
|
||||
</motion.div>
|
||||
|
||||
{/* Floating Emojis Animation */}
|
||||
<motion.div
|
||||
className="absolute top-10 left-10 text-5xl"
|
||||
animate={{ y: [0, -15, 0] }}
|
||||
transition={{ repeat: Infinity, duration: 2 }}
|
||||
>
|
||||
🔑
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="absolute bottom-10 right-10 text-5xl"
|
||||
animate={{ rotate: [0, 20, -20, 0] }}
|
||||
transition={{ repeat: Infinity, duration: 2 }}
|
||||
>
|
||||
🔥
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
202
src/Pages/ManageClient.jsx
Normal file
202
src/Pages/ManageClient.jsx
Normal file
@ -0,0 +1,202 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { motion } from "framer-motion";
|
||||
import { FaArrowLeft, FaUpload } from "react-icons/fa";
|
||||
import { api } from "../API/api";
|
||||
import ManageFiles from "./ManageFilesPage";
|
||||
import { useLoading } from "../Context/LoadingContext";
|
||||
|
||||
const ManageClient = () => {
|
||||
const { setLoading } = useLoading();
|
||||
const { id } = useParams();
|
||||
const [files, setFiles] = useState([]);
|
||||
const [clients, setclients] = useState([]);
|
||||
const [screens, setScreens] = useState([]);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const [isEnabled, setIsEnabled] = useState(true);
|
||||
const [formData, setFormData] = useState({
|
||||
transid: "",
|
||||
name: "",
|
||||
emailid: "",
|
||||
phoneno: "",
|
||||
address: "",
|
||||
});
|
||||
|
||||
|
||||
// Fetch files when component loads
|
||||
useEffect(() => {
|
||||
fetchClients();
|
||||
fetchScreens()
|
||||
}, [id]);
|
||||
|
||||
const fetchFiles = async () => {
|
||||
try {
|
||||
const response = await api.get(`/client/files/${id}`);
|
||||
setFiles(response);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error fetching files:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchClients = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await api.get(`/admin/get-client/${id}`);
|
||||
setclients(response);
|
||||
setFormData(response[0]);
|
||||
} catch (error) {
|
||||
console.error("Error fetching files:", error);
|
||||
}
|
||||
setLoading(false)
|
||||
};
|
||||
const fetchScreens = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await api.get(`/admin/get-screen/${id}`);
|
||||
setScreens(response);
|
||||
} catch (error) {
|
||||
console.error("Error fetching files:", error);
|
||||
}
|
||||
setLoading(false)
|
||||
};
|
||||
|
||||
const handleUpdate = async (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
setLoading(true);
|
||||
await api.post("/admin/update-client", formData);
|
||||
navigate("/clients");
|
||||
} catch (error) {
|
||||
console.error("Error updating Client:", error);
|
||||
}
|
||||
setLoading(false)
|
||||
};
|
||||
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData({ ...formData, [name]: value });
|
||||
};
|
||||
|
||||
const handleEdit = () => {
|
||||
setIsEnabled(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white p-8">
|
||||
{/* Back Button */}
|
||||
<motion.button
|
||||
className="mb-4 flex items-center text-gray-400 hover:text-white"
|
||||
whileHover={{ scale: 1.1 }}
|
||||
onClick={() => navigate("/admin-dashboard")}
|
||||
>
|
||||
<FaArrowLeft className="mr-2" /> Back
|
||||
</motion.button>
|
||||
|
||||
{/* Page Title */}
|
||||
<motion.h1
|
||||
className="text-3xl font-bold mb-6"
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
>
|
||||
Manage Client
|
||||
</motion.h1>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
|
||||
Client Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
id="name"
|
||||
placeholder="Partner Name"
|
||||
onChange={handleChange}
|
||||
value={formData.name || ""}
|
||||
className="block w-full bg-gray-700 p-2 rounded-lg mb-2"
|
||||
required
|
||||
// disabled={true}
|
||||
/>
|
||||
|
||||
|
||||
<label htmlFor="emailid" className="block text-sm font-medium text-gray-700">E-Mail ID</label>
|
||||
<input
|
||||
type="email"
|
||||
name="emailid"
|
||||
id="emailid"
|
||||
placeholder="E-Mail ID"
|
||||
onChange={handleChange}
|
||||
className="block w-full bg-gray-700 p-2 rounded-lg mb-2"
|
||||
required
|
||||
minLength="1" // Example: Add minimum length validation
|
||||
maxLength="255" //Example: add maximum length validation
|
||||
disabled={isEnabled}
|
||||
value={formData.emailid}
|
||||
/>
|
||||
|
||||
<label htmlFor="phoneno" className="block text-sm font-medium text-gray-700">Phone Number</label>
|
||||
<input
|
||||
type="number"
|
||||
name="phoneno"
|
||||
id="phoneno"
|
||||
placeholder="Phone Number"
|
||||
onChange={handleChange}
|
||||
className="block w-full bg-gray-700 p-2 rounded-lg mb-2"
|
||||
required
|
||||
minLength="1" // Example: Add minimum length validation
|
||||
maxLength="255" //Example: add maximum length validation
|
||||
disabled={isEnabled}
|
||||
value={formData.phoneno}
|
||||
/>
|
||||
<label htmlFor="address" className="block text-sm font-medium text-gray-700">Address</label>
|
||||
<input
|
||||
type="text"
|
||||
name="address"
|
||||
id="address"
|
||||
placeholder="Address"
|
||||
onChange={handleChange}
|
||||
className="block w-full bg-gray-700 p-2 rounded-lg mb-2"
|
||||
required
|
||||
value={formData.address}
|
||||
disabled={isEnabled}
|
||||
/>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
{isEnabled ? (
|
||||
<motion.button
|
||||
type="button"
|
||||
className="bg-green-500 px-4 py-2 rounded-lg"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
onClick={handleEdit}
|
||||
>
|
||||
Edit
|
||||
</motion.button>
|
||||
) : (
|
||||
<motion.button
|
||||
type="submit"
|
||||
className="bg-blue-500 px-4 py-2 rounded-lg"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
onClick={handleUpdate}
|
||||
>
|
||||
Update
|
||||
</motion.button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<ManageFiles id={id} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManageClient;
|
||||
253
src/Pages/ManageFilesOrderPage.jsx
Normal file
253
src/Pages/ManageFilesOrderPage.jsx
Normal file
@ -0,0 +1,253 @@
|
||||
import { useEffect, useState, useRef, useMemo } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { motion, AnimatePresence, Reorder } from "framer-motion";
|
||||
import { FaArrowUp, FaArrowDown } from "react-icons/fa";
|
||||
import { api } from "../API/api";
|
||||
import { useLoading } from "../Context/LoadingContext";
|
||||
import ViewPartnerAdsConfiguration from "../Components/ViewPartnerAdsConfiguration";
|
||||
|
||||
const ManageFilesOrder = () => {
|
||||
const { id } = useParams();
|
||||
const BACKEND = 'https://ads.dine360ads.com/api//';
|
||||
const [files, setFiles] = useState([]);
|
||||
const [openScreens, setOpenScreens] = useState({});
|
||||
const navigate = useNavigate();
|
||||
const { setLoading } = useLoading();
|
||||
|
||||
// Fetch and sort files
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await api.get(`/admin/partner-ads?partnerid=${id}`);
|
||||
console.log(response)
|
||||
const sorted = response.sort((a, b) => {
|
||||
const nameA = a?.screenname || '';
|
||||
const nameB = b?.screenname || '';
|
||||
return nameA.localeCompare(nameB, undefined, { numeric: true });
|
||||
});
|
||||
|
||||
setFiles(sorted);
|
||||
console.log("FILESSSSSSSSSSSSS<",sorted)
|
||||
} catch (error) {
|
||||
console.error("Error fetching files:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
})();
|
||||
}, [id, setLoading]);
|
||||
|
||||
// Group files by screenname
|
||||
const filesByScreen = useMemo(() =>
|
||||
files.reduce((acc, file) => {
|
||||
const key = file.screenname;
|
||||
acc[key] = acc[key] || [];
|
||||
acc[key].push(file);
|
||||
return acc;
|
||||
}, {}),
|
||||
[files]
|
||||
);
|
||||
|
||||
// Toggle collapse for screen
|
||||
const toggleScreen = (screen) => {
|
||||
setOpenScreens(prev => ({ ...prev, [screen]: !prev[screen] }));
|
||||
};
|
||||
|
||||
// Merge reordered group back into files array
|
||||
const handleGroupReorder = (screen, newGroup) => {
|
||||
const other = files.filter(f => f.screenname !== screen);
|
||||
setFiles([...other, ...newGroup]);
|
||||
};
|
||||
|
||||
// Save flattened order
|
||||
const saveOrder = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
await api.post("/admin/reorder-partner-ads", files);
|
||||
alert("Order saved successfully");
|
||||
} catch (error) {
|
||||
console.error("Error saving order:", error);
|
||||
alert("Error saving order. Check console for details.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Checkbox handlers update flat files state
|
||||
const handleToggleMainAd = (file, checked) => {
|
||||
setFiles(files.map(f =>
|
||||
f.file_path === file.file_path && file.screenid === f.screenid? { ...f, ismainad: checked ? 1 : 0 } : f
|
||||
));
|
||||
};
|
||||
const handleToggleCarousel = (file, checked) => {
|
||||
setFiles(files.map(f =>
|
||||
f.file_path === file.file_path && file.screenid === f.screenid? { ...f, iscarousel: checked ? 1 : 0 } : f
|
||||
));
|
||||
};
|
||||
const handleToggleInHouse = (file, checked) => {
|
||||
setFiles(files.map(f =>
|
||||
f.file_path === file.file_path && file.screenid === f.screenid? { ...f, isinhousead: checked ? 1 : 0 } : f
|
||||
));
|
||||
};
|
||||
|
||||
// Move up/down within a screen group
|
||||
const groupMoveUp = (screen, index) => {
|
||||
const group = filesByScreen[screen];
|
||||
if (index <= 0) return;
|
||||
const newGroup = [...group];
|
||||
[newGroup[index - 1], newGroup[index]] = [newGroup[index], newGroup[index - 1]];
|
||||
handleGroupReorder(screen, newGroup);
|
||||
};
|
||||
const groupMoveDown = (screen, index) => {
|
||||
const group = filesByScreen[screen];
|
||||
if (index >= group.length - 1) return;
|
||||
const newGroup = [...group];
|
||||
[newGroup[index], newGroup[index + 1]] = [newGroup[index + 1], newGroup[index]];
|
||||
handleGroupReorder(screen, newGroup);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white p-8">
|
||||
<motion.h1
|
||||
className="text-3xl font-bold mb-6"
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
>
|
||||
📂 Manage Ads Order
|
||||
</motion.h1>
|
||||
|
||||
{/* Collapsible groups by screen */}
|
||||
<div className="space-y-6">
|
||||
{Object.entries(filesByScreen).map(([screen, groupFiles]) => (
|
||||
<div key={screen}>
|
||||
<button
|
||||
onClick={() => toggleScreen(screen)}
|
||||
className="w-full flex justify-between items-center bg-gray-700 px-4 py-2 rounded-lg"
|
||||
>
|
||||
<span className="text-lg font-medium">{screen}</span>
|
||||
<span className="text-2xl">{openScreens[screen] ? '−' : '+'}</span>
|
||||
</button>
|
||||
|
||||
<AnimatePresence>
|
||||
{openScreens[screen] && (
|
||||
<Reorder.Group
|
||||
axis="y"
|
||||
values={groupFiles}
|
||||
onReorder={(newOrder) => handleGroupReorder(screen, newOrder)}
|
||||
className="bg-gray-800 p-4 rounded-lg mt-2 space-y-2"
|
||||
>
|
||||
{groupFiles.map((file, index) => {
|
||||
const fileName = file.file_path.split("_")[1];
|
||||
const fileUrl = BACKEND + file.file_path;
|
||||
const isImage = /\.(jpe?g|png|gif)$/i.test(fileUrl);
|
||||
const isVideo = /\.(mp4|webm|ogg|mkv)$/i.test(fileUrl);
|
||||
|
||||
return (
|
||||
<Reorder.Item
|
||||
key={file.id || file.file_path}
|
||||
value={file}
|
||||
layout
|
||||
className="flex items-center justify-between border-b border-gray-700 pb-2"
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
{isImage && (
|
||||
// <img src={fileUrl} alt={fileName} className="w-20 h-20 object-cover rounded-lg" />
|
||||
<img
|
||||
src={fileUrl}
|
||||
alt={fileName}
|
||||
className="h-40 w-auto object-cover rounded-lg"
|
||||
/>
|
||||
|
||||
)}
|
||||
{isVideo && (
|
||||
// <video controls className="w-20 h-20 rounded-lg">
|
||||
// <source src={fileUrl} type="video/mp4" />
|
||||
// </video>
|
||||
<video
|
||||
controls
|
||||
className="h-40 w-auto rounded-lg"
|
||||
>
|
||||
<source src={fileUrl} type="video/mp4" />
|
||||
</video>
|
||||
|
||||
)}
|
||||
<span className="text-gray-300">{fileName}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
{/* Checkboxes */}
|
||||
<label className="flex items-center space-x-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={file.ismainad}
|
||||
onChange={e => handleToggleMainAd(file, e.target.checked)}
|
||||
/>
|
||||
<span>Main Ad</span>
|
||||
</label>
|
||||
<label className="flex items-center space-x-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={file.iscarousel}
|
||||
onChange={e => handleToggleCarousel(file, e.target.checked)}
|
||||
/>
|
||||
<span>Carousel</span>
|
||||
</label>
|
||||
<label className="flex items-center space-x-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={file.isinhousead}
|
||||
onChange={e => handleToggleInHouse(file, e.target.checked)}
|
||||
/>
|
||||
<span>In-House</span>
|
||||
</label>
|
||||
|
||||
{/* Up/Down buttons */}
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
onClick={() => groupMoveUp(screen, index)}
|
||||
disabled={index === 0}
|
||||
className="p-2 rounded-full bg-gray-700 disabled:opacity-50"
|
||||
>
|
||||
<FaArrowUp className="text-gray-400" />
|
||||
</motion.button>
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
onClick={() => groupMoveDown(screen, index)}
|
||||
disabled={index === groupFiles.length - 1}
|
||||
className="p-2 rounded-full bg-gray-700 disabled:opacity-50"
|
||||
>
|
||||
<FaArrowDown className="text-gray-400" />
|
||||
</motion.button>
|
||||
</div>
|
||||
</Reorder.Item>
|
||||
);
|
||||
})}
|
||||
</Reorder.Group>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<button
|
||||
onClick={saveOrder}
|
||||
className="mt-4 bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg font-bold mx-auto block"
|
||||
>
|
||||
Save Ads Order
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<motion.h1
|
||||
className="text-3xl font-bold mb-6 mt-12"
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
>
|
||||
📂 Ads Configurations
|
||||
</motion.h1>
|
||||
<ViewPartnerAdsConfiguration data={files} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManageFilesOrder;
|
||||
213
src/Pages/ManageFilesOrderPage_27-04-25.jsx
Normal file
213
src/Pages/ManageFilesOrderPage_27-04-25.jsx
Normal file
@ -0,0 +1,213 @@
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { motion, AnimatePresence, Reorder } from "framer-motion";
|
||||
import { FaArrowUp, FaArrowDown } from "react-icons/fa";
|
||||
import { api } from "../API/api";
|
||||
import { useLoading } from "../Context/LoadingContext";
|
||||
import ViewPartnerAdsConfiguration from "../Components/ViewPartnerAdsConfiguration";
|
||||
|
||||
const ManageFilesOrder = () => {
|
||||
const { id } = useParams();
|
||||
const BACKEND = 'https://ads.dine360ads.com/api//';
|
||||
const [files, setFiles] = useState([]);
|
||||
const [upd, setupd] = useState(0);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const { setLoading } = useLoading();
|
||||
const fileRefs = useRef([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchFiles();
|
||||
}, [id, upd]);
|
||||
|
||||
const fetchFiles = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await api.get(`/admin/partner-ads?partnerid=${id}`);
|
||||
console.log(response)
|
||||
const res = response.sort((a, b) => a.screenname.localeCompare(b.screenname, undefined, { numeric: true }));
|
||||
|
||||
|
||||
setFiles(res);
|
||||
} catch (error) {
|
||||
console.error("Error fetching files:", error);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const ReorderAds = async (e) => {
|
||||
setLoading(true);
|
||||
console.log("Final Data to Insert:", files);
|
||||
|
||||
try {
|
||||
await api.post("/admin/reorder-partner-ads", files);
|
||||
alert("Client Partner Mapping data added successfully!");
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error adding client partner mapping:", error);
|
||||
alert("Error adding client partner mapping. Please check console for details.");
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const moveFileUp = (index) => {
|
||||
if (index > 0) {
|
||||
const newFiles = [...files];
|
||||
[newFiles[index], newFiles[index - 1]] = [newFiles[index - 1], newFiles[index]];
|
||||
setFiles(newFiles);
|
||||
}
|
||||
};
|
||||
|
||||
const moveFileDown = (index) => {
|
||||
if (index < files.length - 1) {
|
||||
const newFiles = [...files];
|
||||
[newFiles[index], newFiles[index + 1]] = [newFiles[index + 1], newFiles[index]];
|
||||
setFiles(newFiles);
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleCarousel = (index, checked) => {
|
||||
// old: no-op
|
||||
setFiles(files.map((f, i) =>
|
||||
i === index
|
||||
? { ...f, iscarousel: checked ? 1 : 0 }
|
||||
: f
|
||||
));
|
||||
};
|
||||
|
||||
const handleToggleInHouse = (index, checked) => {
|
||||
// old: no-op
|
||||
setFiles(files.map((f, i) =>
|
||||
i === index
|
||||
? { ...f, isinhousead: checked ? 1 : 0 }
|
||||
: f
|
||||
));
|
||||
};
|
||||
|
||||
const handleToggleMainAd = (index, checked) => {
|
||||
// old: no-op
|
||||
setFiles(files.map((f, i) =>
|
||||
i === index
|
||||
? { ...f, ismainad: checked ? 1 : 0 }
|
||||
: f
|
||||
));
|
||||
};
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white p-8">
|
||||
<motion.h1
|
||||
className="text-3xl font-bold mb-6"
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
>
|
||||
📂 Manage Ads Order
|
||||
</motion.h1>
|
||||
|
||||
<div className="space-y-4">
|
||||
<Reorder.Group axis="y" values={files} onReorder={setFiles} className="bg-gray-800 p-4 rounded-lg space-y-2">
|
||||
<AnimatePresence>
|
||||
{files.length > 0 ? (
|
||||
files.map((file, index) => {
|
||||
const fileName = file.file_path.split("_")[1];
|
||||
const fileUrl = BACKEND + file.file_path;
|
||||
const isImage = fileUrl.match(/\.(jpeg|jpg|png|gif)$/i);
|
||||
const isVideo = fileUrl.match(/\.(mp4|webm|ogg|mkv)$/i);
|
||||
|
||||
return (
|
||||
<Reorder.Item key={index} value={file} layout className="text-gray-300 flex items-center justify-between border-b border-gray-700 pb-2 space-x-4">
|
||||
<div className="flex items-center space-x-4">
|
||||
{isImage && <img src={fileUrl} alt={fileName} className="w-32 h-32 object-cover rounded-lg" />}
|
||||
{isVideo && (
|
||||
<video controls className="w-32 h-32 rounded-lg">
|
||||
<source src={fileUrl} type="video/mp4" />
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
)}
|
||||
<span> {file.screenname} - 📄 {fileName}</span>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<div className="flex space-x-2 mr-10">
|
||||
|
||||
<input
|
||||
className="mr-2"
|
||||
type="checkbox"
|
||||
|
||||
checked={file.ismainad}
|
||||
onChange={e => handleToggleMainAd(index, e.target.checked)}
|
||||
/>
|
||||
Set Ad as Main Ad
|
||||
</div><br></br>
|
||||
<div className="flex space-x-2 mr-10">
|
||||
|
||||
<input
|
||||
className="mr-2"
|
||||
type="checkbox"
|
||||
|
||||
checked={file.iscarousel}
|
||||
onChange={e => handleToggleCarousel(index, e.target.checked)}
|
||||
/>
|
||||
Set Ad in Image Carousel
|
||||
</div>
|
||||
<br>
|
||||
</br>
|
||||
<div className="flex space-x-2 mr-30">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mr-2"
|
||||
checked={file.isinhousead}
|
||||
onChange={e => handleToggleInHouse(index, e.target.checked)}
|
||||
/>
|
||||
Set Ad as In-House Ad
|
||||
</div>
|
||||
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
onClick={() => moveFileUp(index)}
|
||||
disabled={index === 0}
|
||||
className="p-2 rounded-full bg-gray-700 disabled:opacity-50"
|
||||
>
|
||||
<FaArrowUp className="text-gray-400 hover:text-white" />
|
||||
</motion.button>
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
onClick={() => moveFileDown(index)}
|
||||
disabled={index === files.length - 1}
|
||||
className="p-2 rounded-full bg-gray-700 disabled:opacity-50"
|
||||
>
|
||||
<FaArrowDown className="text-gray-400 hover:text-white" />
|
||||
</motion.button>
|
||||
</div>
|
||||
</Reorder.Item>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<p className="text-gray-500">No files uploaded yet.</p>
|
||||
)}
|
||||
|
||||
|
||||
</AnimatePresence>
|
||||
</Reorder.Group>
|
||||
</div>
|
||||
<button onClick={ReorderAds} className="mt-4 bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg font-bold block mx-auto">Save Ads Order</button>
|
||||
|
||||
|
||||
<br></br>
|
||||
<motion.h1
|
||||
className="text-3xl font-bold mb-6"
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
>
|
||||
📂 Ads Configurations
|
||||
</motion.h1>
|
||||
<div>
|
||||
<ViewPartnerAdsConfiguration data={files} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManageFilesOrder;
|
||||
158
src/Pages/ManageFilesPage.jsx
Normal file
158
src/Pages/ManageFilesPage.jsx
Normal file
@ -0,0 +1,158 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { motion } from "framer-motion";
|
||||
import { FaArrowLeft, FaUpload } from "react-icons/fa";
|
||||
import { api } from "../API/api";
|
||||
import { useLoading } from "../Context/LoadingContext";
|
||||
|
||||
const ManageFiles = ({ id }) => {
|
||||
const BACKEND = 'https://ads.dine360ads.com/api//'
|
||||
const [files, setFiles] = useState([]);
|
||||
const [upd, setupd] = useState(0);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const { setLoading } = useLoading();
|
||||
// Fetch files when component loads
|
||||
useEffect(() => {
|
||||
fetchFiles();
|
||||
}, [id, upd]);
|
||||
|
||||
const fetchFiles = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await api.get(`/client/files/${id}`);
|
||||
setFiles(response);
|
||||
} catch (error) {
|
||||
console.error("Error fetching files:", error);
|
||||
}
|
||||
setLoading(false)
|
||||
};
|
||||
|
||||
const deleteFiles = async (fileid) => {
|
||||
console.log(fileid)
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await api.delete(`/files/del/${fileid}`);
|
||||
console.log(response);
|
||||
setupd(upd + 1)
|
||||
} catch (error) {
|
||||
console.error("Error fetching files:", error);
|
||||
}
|
||||
setLoading(false)
|
||||
};
|
||||
|
||||
const handleFileUpload = async (file) => {
|
||||
if (!file) return;
|
||||
|
||||
setUploading(true);
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
formData.append("file_name", file.name);
|
||||
formData.append("client_id", id); // Attach client ID
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
await api.post("/files/upload", formData, {
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
});
|
||||
|
||||
// Refresh file list after each successful upload
|
||||
fetchFiles();
|
||||
} catch (error) {
|
||||
console.error(`File upload failed for ${file.name}:`, error);
|
||||
// Optionally, you could add some user feedback here for individual file failures
|
||||
} finally {
|
||||
setUploading(false); // Keep this false, as we're handling individual uploads
|
||||
setLoading(false)
|
||||
}
|
||||
};
|
||||
|
||||
const handleMultipleUpload = async (e) => {
|
||||
const filesToUpload = e.target.files;
|
||||
if (!filesToUpload || filesToUpload.length === 0) return;
|
||||
|
||||
setUploading(true);
|
||||
// Iterate through each selected file and call the upload function
|
||||
for (let i = 0; i < filesToUpload.length; i++) {
|
||||
await handleFileUpload(filesToUpload[i]);
|
||||
}
|
||||
setUploading(false); // Set uploading to false after all files are processed
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white p-8">
|
||||
{/* Back Button */}
|
||||
{/* <motion.button
|
||||
className="mb-4 flex items-center text-gray-400 hover:text-white"
|
||||
whileHover={{ scale: 1.1 }}
|
||||
onClick={() => navigate("/admin-dashboard")}
|
||||
>
|
||||
<FaArrowLeft className="mr-2" /> Back
|
||||
</motion.button> */}
|
||||
|
||||
{/* Page Title */}
|
||||
<motion.h1
|
||||
className="text-3xl font-bold mb-6"
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
>
|
||||
📂 Manage Client Files
|
||||
</motion.h1>
|
||||
|
||||
{/* Upload Button */}
|
||||
<div className="space-y-4">
|
||||
<label className="bg-gray-700 px-4 py-2 rounded-lg cursor-pointer flex items-center gap-2 hover:bg-gray-600">
|
||||
<FaUpload />
|
||||
<span>{uploading ? "Uploading..." : "Upload Files"}</span>
|
||||
<input
|
||||
type="file"
|
||||
onChange={handleMultipleUpload}
|
||||
className="hidden"
|
||||
multiple // Add the 'multiple' attribute here
|
||||
/>
|
||||
</label>
|
||||
|
||||
<ul className="bg-gray-800 p-4 rounded-lg space-y-2">
|
||||
{files.length > 0 ? (
|
||||
files.map((file, index) => {
|
||||
const fileName = file.file_name
|
||||
const fileUrl = BACKEND+ file.file_path // Assuming this holds the correct file URL
|
||||
const isImage = fileUrl.match(/\.(jpeg|jpg|png|gif)$/i);
|
||||
const isVideo = fileUrl.match(/\.(mp4|webm|ogg|mkv)$/i);
|
||||
|
||||
return (
|
||||
<li key={index} className="text-gray-300 flex flex-col border-b border-gray-700 pb-2 space-y-2">
|
||||
<span>📄 {fileName}</span>
|
||||
|
||||
{isImage && <img src={fileUrl} alt={fileName} className="w-32 h-32 object-cover rounded-lg" />}
|
||||
|
||||
{isVideo && (
|
||||
<video controls className="w-32 h-32 rounded-lg">
|
||||
<source src={fileUrl} type="video/mp4" />
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
if (confirm("Are you sure you want to delete this file?")) {
|
||||
deleteFiles(file.transid);
|
||||
}
|
||||
}}
|
||||
className="text-red-500 hover:text-red-700 text-sm font-bold"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<p className="text-gray-500">No files uploaded yet.</p>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManageFiles;
|
||||
145
src/Pages/ManageFilesPage_Bak_singleFileUpload.jsx
Normal file
145
src/Pages/ManageFilesPage_Bak_singleFileUpload.jsx
Normal file
@ -0,0 +1,145 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { motion } from "framer-motion";
|
||||
import { FaArrowLeft, FaUpload } from "react-icons/fa";
|
||||
import { api } from "../API/api";
|
||||
import { useLoading } from "../Context/LoadingContext";
|
||||
|
||||
const ManageFiles = ({ id }) => {
|
||||
const BACKEND = 'https://ads.dine360ads.com/api//'
|
||||
const [files, setFiles] = useState([]);
|
||||
const [upd, setupd] = useState(0);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const { setLoading } = useLoading();
|
||||
// Fetch files when component loads
|
||||
useEffect(() => {
|
||||
fetchFiles();
|
||||
}, [id, upd]);
|
||||
|
||||
const fetchFiles = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await api.get(`/client/files/${id}`);
|
||||
setFiles(response);
|
||||
} catch (error) {
|
||||
console.error("Error fetching files:", error);
|
||||
}
|
||||
setLoading(false)
|
||||
};
|
||||
|
||||
const deleteFiles = async (fileid) => {
|
||||
console.log(fileid)
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await api.delete(`/files/del/${fileid}`);
|
||||
console.log(response);
|
||||
setupd(upd + 1)
|
||||
} catch (error) {
|
||||
console.error("Error fetching files:", error);
|
||||
}
|
||||
setLoading(false)
|
||||
};
|
||||
|
||||
|
||||
|
||||
const handleUpload = async (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
setUploading(true);
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
formData.append("file_name", file.name);
|
||||
formData.append("client_id", id); // Attach client ID
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
await api.post("/files/upload", formData, {
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
});
|
||||
|
||||
// Refresh file list after upload
|
||||
fetchFiles();
|
||||
} catch (error) {
|
||||
console.error("File upload failed:", error);
|
||||
} finally {
|
||||
setUploading(false);
|
||||
setLoading(false)
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white p-8">
|
||||
{/* Back Button */}
|
||||
{/* <motion.button
|
||||
className="mb-4 flex items-center text-gray-400 hover:text-white"
|
||||
whileHover={{ scale: 1.1 }}
|
||||
onClick={() => navigate("/admin-dashboard")}
|
||||
>
|
||||
<FaArrowLeft className="mr-2" /> Back
|
||||
</motion.button> */}
|
||||
|
||||
{/* Page Title */}
|
||||
<motion.h1
|
||||
className="text-3xl font-bold mb-6"
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
>
|
||||
📂 Manage Client Files
|
||||
</motion.h1>
|
||||
|
||||
{/* Upload Button */}
|
||||
<div className="space-y-4">
|
||||
<label className="bg-gray-700 px-4 py-2 rounded-lg cursor-pointer flex items-center gap-2 hover:bg-gray-600">
|
||||
<FaUpload />
|
||||
<span>{uploading ? "Uploading..." : "Upload File"}</span>
|
||||
<input type="file" onChange={handleUpload} className="hidden" />
|
||||
</label>
|
||||
|
||||
<ul className="bg-gray-800 p-4 rounded-lg space-y-2">
|
||||
{files.length > 0 ? (
|
||||
files.map((file, index) => {
|
||||
const fileName = file.file_name
|
||||
const fileUrl = BACKEND+ file.file_path // Assuming this holds the correct file URL
|
||||
const isImage = fileUrl.match(/\.(jpeg|jpg|png|gif)$/i);
|
||||
const isVideo = fileUrl.match(/\.(mp4|webm|ogg|mkv)$/i);
|
||||
|
||||
return (
|
||||
<li key={index} className="text-gray-300 flex flex-col border-b border-gray-700 pb-2 space-y-2">
|
||||
<span>📄 {fileName}</span>
|
||||
|
||||
{isImage && <img src={fileUrl} alt={fileName} className="w-32 h-32 object-cover rounded-lg" />}
|
||||
|
||||
{isVideo && (
|
||||
<video controls className="w-32 h-32 rounded-lg">
|
||||
<source src={fileUrl} type="video/mp4" />
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
if (confirm("Are you sure you want to delete this file?")) {
|
||||
deleteFiles(file.transid);
|
||||
}
|
||||
}}
|
||||
className="text-red-500 hover:text-red-700 text-sm font-bold"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<p className="text-gray-500">No files uploaded yet.</p>
|
||||
)}
|
||||
</ul>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManageFiles;
|
||||
663
src/Pages/ManagePartner copy.jsx
Normal file
663
src/Pages/ManagePartner copy.jsx
Normal file
@ -0,0 +1,663 @@
|
||||
import { useEffect, useState, useMemo } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { motion } from "framer-motion";
|
||||
import { FaArrowLeft, FaTrash, FaUpload } from "react-icons/fa";
|
||||
import { api } from "../API/api";
|
||||
import { FaPlus, FaCircle } from "react-icons/fa";
|
||||
import Select from 'react-select';
|
||||
import canada_cities from '../assets/canada_cities.json';
|
||||
import { useLoading } from "../Context/LoadingContext";
|
||||
import { FiPlus } from "react-icons/fi";
|
||||
|
||||
const ManagePartner = () => {
|
||||
const { id } = useParams();
|
||||
const [files, setFiles] = useState([]);
|
||||
const [partners, setPartners] = useState([]);
|
||||
const [screens, setScreens] = useState([]);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const [isEnabled, setIsEnabled] = useState(true);
|
||||
const { setLoading } = useLoading();
|
||||
const [formData, setFormData] = useState({
|
||||
transid: "",
|
||||
name: "",
|
||||
logo_url: "",
|
||||
open_time: "",
|
||||
close_time: "",
|
||||
address: "",
|
||||
city: "",
|
||||
state: "",
|
||||
pincode: "",
|
||||
screens: "",
|
||||
yt: "",
|
||||
scrolltext: ""
|
||||
});
|
||||
|
||||
const [selectedProvince, setSelectedProvince] = useState(null);
|
||||
const [selectedCity, setSelectedCity] = useState(null);
|
||||
|
||||
const provinceOptions = useMemo(
|
||||
() =>
|
||||
Object.keys(canada_cities).map((province) => ({
|
||||
value: province,
|
||||
label: province,
|
||||
})),
|
||||
[canada_cities]
|
||||
);
|
||||
|
||||
const cityOptions = useMemo(
|
||||
() =>
|
||||
selectedProvince
|
||||
? canada_cities[selectedProvince.value].map((city) => ({
|
||||
value: city,
|
||||
label: city,
|
||||
}))
|
||||
: [],
|
||||
[selectedProvince, canada_cities]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetchPartners();
|
||||
fetchScreens();
|
||||
}, [id]);
|
||||
|
||||
const fetchFiles = async () => {
|
||||
try {
|
||||
const response = await api.get(`/client/files/${id}`);
|
||||
setFiles(response);
|
||||
} catch (error) {
|
||||
console.error("Error fetching files:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchPartners = async () => {
|
||||
try {
|
||||
|
||||
const response = await api.get(`/admin/get-partner/${id}`);
|
||||
setPartners(response);
|
||||
setFormData(response[0]);
|
||||
if (response[0]) {
|
||||
setSelectedProvince({ value: response[0].state, label: response[0].state });
|
||||
setSelectedCity({ value: response[0].city, label: response[0].city });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching files:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchScreens = async () => {
|
||||
try {
|
||||
const response = await api.get(`/admin/get-screen/${id}`);
|
||||
setScreens(response);
|
||||
console.log(response)
|
||||
} catch (error) {
|
||||
console.error("Error fetching files:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileUpload = async (file) => {
|
||||
if (!file) return;
|
||||
|
||||
setUploading(true);
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
formData.append("file_name", file.name);
|
||||
formData.append("client_id", id); // Attach client ID
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
await api.post("/files/update-partner-logo", formData, {
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
});
|
||||
navigate("/partners");
|
||||
|
||||
// Refresh file list after each successful upload
|
||||
fetchFiles();
|
||||
} catch (error) {
|
||||
console.error(`File upload failed for ${file.name}:`, error);
|
||||
// Optionally, you could add some user feedback here for individual file failures
|
||||
} finally {
|
||||
setUploading(false); // Keep this false, as we're handling individual uploads
|
||||
setLoading(false)
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileChange = async (e) => {
|
||||
console.log("oiouyt")
|
||||
await handleFileUpload(e.target.files[0]);
|
||||
setFormData({ ...formData, image: e.target.files[0] });
|
||||
};
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData({ ...formData, [name]: value });
|
||||
};
|
||||
|
||||
const handleProvinceChange = (selectedOption) => {
|
||||
setSelectedProvince(selectedOption);
|
||||
setSelectedCity(null);
|
||||
setFormData({ ...formData, state: selectedOption?.value || "" });
|
||||
};
|
||||
|
||||
const handleCityChange = (selectedOption) => {
|
||||
setSelectedCity(selectedOption);
|
||||
setFormData({ ...formData, city: selectedOption?.value || "" });
|
||||
};
|
||||
|
||||
const handleEdit = () => {
|
||||
setIsEnabled(false);
|
||||
};
|
||||
|
||||
const handleUpdate = async (e) => {
|
||||
|
||||
e.preventDefault();
|
||||
try {
|
||||
console.log(formData)
|
||||
|
||||
await api.post("/admin/update-partner", formData);
|
||||
navigate("/partners");
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error updating Partner:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const AddScreen = async () => {
|
||||
|
||||
try {
|
||||
console.log({ partnerid: id })
|
||||
setLoading(true);
|
||||
var data = await api.post("/admin/add-screen", { partnerid: id });
|
||||
console.log(data)
|
||||
|
||||
fetchScreens();
|
||||
} catch (error) {
|
||||
console.error("Error adding Screen:", error);
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
|
||||
const DeleteScreen = async (id) => {
|
||||
|
||||
try {
|
||||
|
||||
setLoading(true);
|
||||
var data = await api.post("/admin/delete-screen", { screenid: id });
|
||||
console.log(data)
|
||||
|
||||
fetchScreens();
|
||||
} catch (error) {
|
||||
console.error("Error adding Screen:", error);
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
const handleToggleYouTube = async (screenid, sts, idx) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// call your API – adjust the endpoint & payload as needed
|
||||
await api.post("/admin/update-screen-youtube", {
|
||||
screenid,
|
||||
sts: sts ? 1 : 0,
|
||||
});
|
||||
// update local state so the UI flips instantly
|
||||
// setScreens((prev) =>
|
||||
// prev.map((s, i) =>
|
||||
// i === idx ? { ...s, youtubeEnabled: enable ? 1 : 0 } : s
|
||||
// )
|
||||
// );
|
||||
fetchScreens()
|
||||
} catch (err) {
|
||||
console.error("Error toggling YouTube:", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white p-8">
|
||||
{/* Back button */}
|
||||
<motion.button
|
||||
className="mb-4 flex items-center text-gray-400 hover:text-white"
|
||||
whileHover={{ scale: 1.1 }}
|
||||
onClick={() => navigate("/Partners")}
|
||||
>
|
||||
<FaArrowLeft className="mr-2" /> Back
|
||||
</motion.button>
|
||||
|
||||
{/* Header */}
|
||||
<motion.h1
|
||||
className="text-3xl font-bold mb-6"
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
>
|
||||
Manage Partners
|
||||
</motion.h1>
|
||||
|
||||
{/* Form */}
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Partner Name */}
|
||||
<div>
|
||||
<label htmlFor="name" className="block text-sm font-medium text-gray-300">
|
||||
Partner Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
id="name"
|
||||
value={formData.name || ""}
|
||||
onChange={handleChange}
|
||||
disabled={isEnabled}
|
||||
className="mt-1 block w-full bg-gray-700 p-2 rounded-lg"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Logo */}
|
||||
|
||||
|
||||
{/* Open Time */}
|
||||
<div>
|
||||
<label htmlFor="open_time" className="block text-sm font-medium text-gray-300">
|
||||
Open Time
|
||||
</label>
|
||||
<input
|
||||
type="time"
|
||||
name="open_time"
|
||||
id="open_time"
|
||||
value={formData.open_time || ""}
|
||||
onChange={handleChange}
|
||||
disabled={isEnabled}
|
||||
className="mt-1 block w-full bg-gray-700 p-2 rounded-lg"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Close Time */}
|
||||
<div>
|
||||
<label htmlFor="close_time" className="block text-sm font-medium text-gray-300">
|
||||
Close Time
|
||||
</label>
|
||||
<input
|
||||
type="time"
|
||||
name="close_time"
|
||||
id="close_time"
|
||||
value={formData.close_time || ""}
|
||||
onChange={handleChange}
|
||||
disabled={isEnabled}
|
||||
className="mt-1 block w-full bg-gray-700 p-2 rounded-lg"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Address */}
|
||||
<div>
|
||||
<label htmlFor="address" className="block text-sm font-medium text-gray-300">
|
||||
Address
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="address"
|
||||
id="address"
|
||||
value={formData.address || ""}
|
||||
onChange={handleChange}
|
||||
disabled={isEnabled}
|
||||
className="mt-1 block w-full bg-gray-700 p-2 rounded-lg"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Province */}
|
||||
<div>
|
||||
<label htmlFor="state" className="block text-sm font-medium text-gray-300">
|
||||
Province
|
||||
</label>
|
||||
<Select
|
||||
options={provinceOptions}
|
||||
value={selectedProvince}
|
||||
onChange={handleProvinceChange}
|
||||
isDisabled={isEnabled}
|
||||
className="mt-1"
|
||||
styles={{ /* your existing styles */ }}
|
||||
placeholder="Select Province"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* City */}
|
||||
<div>
|
||||
<label htmlFor="city" className="block text-sm font-medium text-gray-300">
|
||||
City
|
||||
</label>
|
||||
<Select
|
||||
options={cityOptions}
|
||||
value={selectedCity}
|
||||
onChange={handleCityChange}
|
||||
isDisabled={isEnabled || !selectedProvince}
|
||||
className="mt-1"
|
||||
styles={{ /* your existing styles */ }}
|
||||
placeholder="Select City"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Pincode */}
|
||||
<div>
|
||||
<label htmlFor="pincode" className="block text-sm font-medium text-gray-300">
|
||||
Pincode
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="pincode"
|
||||
id="pincode"
|
||||
value={formData.pincode || ""}
|
||||
onChange={handleChange}
|
||||
disabled={isEnabled}
|
||||
className="mt-1 block w-full bg-gray-700 p-2 rounded-lg"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* No. of Screens */}
|
||||
<div>
|
||||
<label htmlFor="screens" className="block text-sm font-medium text-gray-300">
|
||||
No. of Screens
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
name="screens"
|
||||
id="screens"
|
||||
value={formData.screens || ""}
|
||||
onChange={handleChange}
|
||||
disabled={isEnabled}
|
||||
className="mt-1 block w-full bg-gray-700 p-2 rounded-lg"
|
||||
required
|
||||
min="1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
<label htmlFor="yt" className="block text-sm font-medium text-gray-700">
|
||||
Youtube Links
|
||||
</label>
|
||||
<textarea
|
||||
name="yt"
|
||||
id="yt"
|
||||
placeholder="Enter YouTube links, separated by commas"
|
||||
onChange={handleChange}
|
||||
value={formData.yt || ""}
|
||||
className="block w-full bg-gray-700 p-2 rounded-lg mb-2"
|
||||
rows={4}
|
||||
required
|
||||
disabled={isEnabled}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="scrolltext" className="block text-sm font-medium text-gray-700">
|
||||
Scroll Text
|
||||
</label>
|
||||
<textarea
|
||||
type="text"
|
||||
name="scrolltext"
|
||||
id="scrolltext"
|
||||
placeholder="Scroll Text"
|
||||
onChange={handleChange}
|
||||
value={formData.scrolltext || ""}
|
||||
className="block w-full bg-gray-700 p-2 rounded-lg mb-2"
|
||||
required
|
||||
disabled={isEnabled}
|
||||
></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="logo_url" className="block text-sm font-medium text-gray-300">
|
||||
Logo
|
||||
</label>
|
||||
<img
|
||||
src={`https://ads.dine360ads.com/api/${formData.logo_url || ""}`}
|
||||
alt="Logo"
|
||||
className="mt-1 w-[60%] rounded-lg"
|
||||
/>
|
||||
<input
|
||||
type="file"
|
||||
name="logo_url"
|
||||
id="logo_url"
|
||||
accept="image/*"
|
||||
onChange={handleFileChange}
|
||||
disabled={isEnabled}
|
||||
className="mt-2 block w-full bg-gray-700 p-2 rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Edit / Update button */}
|
||||
<div className="mt-6">
|
||||
{isEnabled ? (
|
||||
<motion.button
|
||||
type="button"
|
||||
onClick={handleEdit}
|
||||
className="bg-green-500 px-4 py-2 rounded-lg"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
>
|
||||
Edit
|
||||
</motion.button>
|
||||
) : (
|
||||
<motion.button
|
||||
type="button"
|
||||
className="bg-blue-500 px-4 py-2 rounded-lg"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
onClick={handleUpdate}
|
||||
>
|
||||
Update
|
||||
</motion.button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
{/* Screens Preview */}
|
||||
{formData.screens && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mt-8">
|
||||
<div className="bg-gray-800 p-6 rounded-xl shadow-xl flex justify-center items-center ">
|
||||
<div className="">
|
||||
<motion.button
|
||||
type="button"
|
||||
onClick={AddScreen}
|
||||
className={`
|
||||
inline-flex items-center
|
||||
bg-gradient-to-r from-blue-500 to-teal-400
|
||||
hover:from-blue-600 hover:to-teal-500
|
||||
text-white font-semibold
|
||||
px-6 py-3 rounded-lg
|
||||
shadow-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-300
|
||||
transition-transform
|
||||
`}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
<FiPlus className="w-5 h-5 mr-2" />
|
||||
Add New Screen
|
||||
</motion.button>
|
||||
</div>
|
||||
</div>
|
||||
{screens.map((screen, index) => (
|
||||
// <div key={index} className="bg-gray-800 p-4 rounded-lg shadow-md">
|
||||
// <h3 className="text-lg font-semibold mb-2">
|
||||
// Screen {index + 1} ({id.substring(0, 2) + 'S' + (index + 1)})
|
||||
// </h3>
|
||||
// <div className="flex space-x-2">
|
||||
// <motion.button
|
||||
// type="button"
|
||||
// className="bg-blue-500 px-4 py-2 rounded-lg"
|
||||
// whileHover={{ scale: 1.05 }}
|
||||
// onClick={() => {
|
||||
// const prefix = id.substring(0, 5);
|
||||
// const screenName = `Screen${index + 1}`.replace(/\s+/g, '');
|
||||
// window.open(`${window.location.origin}/ads/${prefix}/${screenName}`, '_blank');
|
||||
// }}
|
||||
// >
|
||||
// Preview
|
||||
// </motion.button>
|
||||
|
||||
// <motion.button
|
||||
// type="button"
|
||||
// className="bg-green-500 px-4 py-2 rounded-lg"
|
||||
// whileHover={{ scale: 1.05 }}
|
||||
// onClick={() => {
|
||||
// const prefix = id.substring(0, 5);
|
||||
// const screenName = `Screen${index + 1}`.replace(/\s+/g, '');
|
||||
// navigator.clipboard.writeText(`${window.location.origin}/ads/${prefix}/${screenName}`);
|
||||
// alert('Link copied to clipboard!');
|
||||
// }}
|
||||
// >
|
||||
// Copy Link
|
||||
// </motion.button>
|
||||
|
||||
|
||||
// <motion.button
|
||||
// type="button"
|
||||
// className={`px-2 py-2 rounded-lg ${screen.isyoutube === 1 ? "bg-amber-400" : "bg-fuchsia-500"
|
||||
// }`}
|
||||
// whileHover={{ scale: 1.05 }}
|
||||
// onClick={() => {
|
||||
// console.log(screen)
|
||||
// handleToggleYouTube(
|
||||
// screen.transid,
|
||||
// screen.isyoutube !== 1,
|
||||
// index
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// >
|
||||
// {screen.isyoutube === 1
|
||||
// ? "Disable YouTube"
|
||||
// : "Enable YouTube"}
|
||||
// </motion.button>
|
||||
|
||||
|
||||
// <div className="flex items-center ml-auto">
|
||||
// <FaCircle
|
||||
// className={`mr-2 ${screen.isactive === 1
|
||||
// ? "text-green-400"
|
||||
// : screen.isactive === 2
|
||||
// ? "text-orange-400"
|
||||
// : "text-red-400"
|
||||
// }`}
|
||||
// />
|
||||
// <span className="uppercase">
|
||||
// {screen.isactive === 1 ? "ACTIVE" : screen.isactive === 2 ? "IDLE" : "INACTIVE"}
|
||||
// </span>
|
||||
|
||||
|
||||
|
||||
// </div>
|
||||
// <br></br>
|
||||
|
||||
// </div>
|
||||
// <div>
|
||||
// <br>
|
||||
// </br>
|
||||
// {screen.status}
|
||||
// <br></br>
|
||||
// {screen.stsupdat && (
|
||||
// <div>
|
||||
// {new Date(screen.stsupdat).toLocaleDateString('en-GB')} {new Date(screen.stsupdat).toLocaleTimeString()}
|
||||
// </div>
|
||||
// )}
|
||||
|
||||
// </div>
|
||||
// </div>
|
||||
<div key={index} className="bg-gray-800 p-4 rounded-lg shadow-md relative">
|
||||
<h3 className="text-lg font-semibold mb-2">
|
||||
Screen {index + 1} ({id.substring(0, 2) + 'S' + (index + 1)})
|
||||
</h3>
|
||||
<div className="flex space-x-2 items-center">
|
||||
<motion.button
|
||||
type="button"
|
||||
className="bg-blue-500 px-4 py-2 rounded-lg"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
onClick={() => {
|
||||
const prefix = id.substring(0, 5);
|
||||
const screenName = `Screen${index + 1}`.replace(/\s+/g, '');
|
||||
window.open(`${window.location.origin}/ads/${prefix}/${screenName}`, '_blank');
|
||||
}}
|
||||
>
|
||||
Preview
|
||||
</motion.button>
|
||||
|
||||
<motion.button
|
||||
type="button"
|
||||
className="bg-green-500 px-4 py-2 rounded-lg"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
onClick={() => {
|
||||
const prefix = id.substring(0, 5);
|
||||
const screenName = `Screen${index + 1}`.replace(/\s+/g, '');
|
||||
navigator.clipboard.writeText(`${window.location.origin}/ads/${prefix}/${screenName}`);
|
||||
alert('Link copied to clipboard!');
|
||||
}}
|
||||
>
|
||||
Copy Link
|
||||
</motion.button>
|
||||
|
||||
<motion.button
|
||||
type="button"
|
||||
className={`px-2 py-2 rounded-lg ${screen.isyoutube === 1 ? "bg-amber-400" : "bg-fuchsia-500"
|
||||
}`}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
onClick={() => {
|
||||
console.log(screen);
|
||||
handleToggleYouTube(screen.transid, screen.isyoutube !== 1, index);
|
||||
}}
|
||||
>
|
||||
{screen.isyoutube === 1 ? "Disable YouTube" : "Enable YouTube"}
|
||||
</motion.button>
|
||||
|
||||
<div className="flex items-center ml-auto">
|
||||
<FaCircle
|
||||
className={`mr-2 ${screen.isactive === 1
|
||||
? "text-green-400"
|
||||
: screen.isactive === 2
|
||||
? "text-orange-400"
|
||||
: "text-red-400"
|
||||
}`}
|
||||
/>
|
||||
<span className="uppercase">
|
||||
{screen.isactive === 1 ? "ACTIVE" : screen.isactive === 2 ? "IDLE" : "INACTIVE"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Delete button at the top right */}
|
||||
<motion.button
|
||||
type="button"
|
||||
className="absolute top-2 right-2 text-red-500 hover:text-red-700"
|
||||
whileHover={{ scale: 1.1 }}
|
||||
onClick={() => {
|
||||
// Add your delete functionality here, e.g., open a confirmation modal
|
||||
console.log('Delete clicked for screen:', screen.transid);
|
||||
DeleteScreen(screen.transid)
|
||||
}}
|
||||
>
|
||||
<FaTrash /> {/* Using FontAwesome for the trash icon */}
|
||||
</motion.button>
|
||||
</div>
|
||||
<br />
|
||||
<div>
|
||||
<br />
|
||||
{screen.status}
|
||||
<br />
|
||||
{screen.stsupdat && (
|
||||
<div>
|
||||
{new Date(screen.stsupdat).toLocaleDateString('en-GB')}{' '}
|
||||
{new Date(screen.stsupdat).toLocaleTimeString()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
export default ManagePartner;
|
||||
626
src/Pages/ManagePartner.jsx
Normal file
626
src/Pages/ManagePartner.jsx
Normal file
@ -0,0 +1,626 @@
|
||||
import { useEffect, useState, useMemo } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { motion } from "framer-motion";
|
||||
import { FaArrowLeft, FaTrash, FaUpload } from "react-icons/fa";
|
||||
import { api } from "../API/api";
|
||||
import { FaPlus, FaCircle } from "react-icons/fa";
|
||||
import Select from 'react-select';
|
||||
import canada_cities from '../assets/canada_cities.json';
|
||||
import { useLoading } from "../Context/LoadingContext";
|
||||
import { FiPlus } from "react-icons/fi";
|
||||
|
||||
const ManagePartner = () => {
|
||||
const { id } = useParams();
|
||||
const [files, setFiles] = useState([]);
|
||||
const [partners, setPartners] = useState([]);
|
||||
const [screens, setScreens] = useState([]);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const [isEnabled, setIsEnabled] = useState(true);
|
||||
const { setLoading } = useLoading();
|
||||
const [formData, setFormData] = useState({
|
||||
transid: "",
|
||||
name: "",
|
||||
logo_url: "",
|
||||
open_time: "",
|
||||
close_time: "",
|
||||
address: "",
|
||||
city: "",
|
||||
state: "",
|
||||
pincode: "",
|
||||
screens: "",
|
||||
yt: "",
|
||||
scrolltext: ""
|
||||
});
|
||||
|
||||
const [selectedProvince, setSelectedProvince] = useState(null);
|
||||
const [selectedCity, setSelectedCity] = useState(null);
|
||||
|
||||
const provinceOptions = useMemo(
|
||||
() =>
|
||||
Object.keys(canada_cities).map((province) => ({
|
||||
value: province,
|
||||
label: province,
|
||||
})),
|
||||
[canada_cities]
|
||||
);
|
||||
|
||||
const cityOptions = useMemo(
|
||||
() =>
|
||||
selectedProvince
|
||||
? canada_cities[selectedProvince.value].map((city) => ({
|
||||
value: city,
|
||||
label: city,
|
||||
}))
|
||||
: [],
|
||||
[selectedProvince, canada_cities]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetchPartners();
|
||||
fetchScreens();
|
||||
}, [id]);
|
||||
|
||||
const fetchFiles = async () => {
|
||||
try {
|
||||
const response = await api.get(`/client/files/${id}`);
|
||||
setFiles(response);
|
||||
} catch (error) {
|
||||
console.error("Error fetching files:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchPartners = async () => {
|
||||
try {
|
||||
|
||||
const response = await api.get(`/admin/get-partner/${id}`);
|
||||
setPartners(response);
|
||||
setFormData(response[0]);
|
||||
if (response[0]) {
|
||||
setSelectedProvince({ value: response[0].state, label: response[0].state });
|
||||
setSelectedCity({ value: response[0].city, label: response[0].city });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching files:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchScreens = async () => {
|
||||
try {
|
||||
const response = await api.get(`/admin/get-screen/${id}`);
|
||||
console.log(response)
|
||||
setScreens(response);
|
||||
console.log(response)
|
||||
} catch (error) {
|
||||
console.error("Error fetching files:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileUpload = async (file) => {
|
||||
if (!file) return;
|
||||
|
||||
setUploading(true);
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
formData.append("file_name", file.name);
|
||||
formData.append("client_id", id); // Attach client ID
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
await api.post("/files/update-partner-logo", formData, {
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
});
|
||||
navigate("/partners");
|
||||
|
||||
// Refresh file list after each successful upload
|
||||
fetchFiles();
|
||||
} catch (error) {
|
||||
console.error(`File upload failed for ${file.name}:`, error);
|
||||
// Optionally, you could add some user feedback here for individual file failures
|
||||
} finally {
|
||||
setUploading(false); // Keep this false, as we're handling individual uploads
|
||||
setLoading(false)
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileChange = async (e) => {
|
||||
console.log("oiouyt")
|
||||
await handleFileUpload(e.target.files[0]);
|
||||
setFormData({ ...formData, image: e.target.files[0] });
|
||||
};
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData({ ...formData, [name]: value });
|
||||
};
|
||||
|
||||
const handleProvinceChange = (selectedOption) => {
|
||||
setSelectedProvince(selectedOption);
|
||||
setSelectedCity(null);
|
||||
setFormData({ ...formData, state: selectedOption?.value || "" });
|
||||
};
|
||||
|
||||
const handleCityChange = (selectedOption) => {
|
||||
setSelectedCity(selectedOption);
|
||||
setFormData({ ...formData, city: selectedOption?.value || "" });
|
||||
};
|
||||
|
||||
const handleEdit = () => {
|
||||
setIsEnabled(false);
|
||||
};
|
||||
|
||||
const handleUpdate = async (e) => {
|
||||
|
||||
e.preventDefault();
|
||||
try {
|
||||
console.log(formData)
|
||||
|
||||
await api.post("/admin/update-partner", formData);
|
||||
navigate("/partners");
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error updating Partner:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const AddScreen = async () => {
|
||||
|
||||
try {
|
||||
console.log({ partnerid: id })
|
||||
setLoading(true);
|
||||
var data = await api.post("/admin/add-screen", { partnerid: id });
|
||||
console.log(data)
|
||||
|
||||
fetchScreens();
|
||||
} catch (error) {
|
||||
console.error("Error adding Screen:", error);
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
|
||||
const DeleteScreen = async (id) => {
|
||||
|
||||
try {
|
||||
|
||||
setLoading(true);
|
||||
var data = await api.post("/admin/delete-screen", { screenid: id });
|
||||
console.log(data)
|
||||
|
||||
fetchScreens();
|
||||
} catch (error) {
|
||||
console.error("Error adding Screen:", error);
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
const handleToggleYouTube = async (screenid, sts, idx) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// call your API – adjust the endpoint & payload as needed
|
||||
await api.post("/admin/update-screen-youtube", {
|
||||
screenid,
|
||||
sts: sts ? 1 : 0,
|
||||
});
|
||||
|
||||
fetchScreens()
|
||||
} catch (err) {
|
||||
console.error("Error toggling YouTube:", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white p-8">
|
||||
{/* Back button */}
|
||||
<motion.button
|
||||
className="mb-4 flex items-center text-gray-400 hover:text-white"
|
||||
whileHover={{ scale: 1.1 }}
|
||||
onClick={() => navigate("/Partners")}
|
||||
>
|
||||
<FaArrowLeft className="mr-2" /> Back
|
||||
</motion.button>
|
||||
|
||||
{/* Header */}
|
||||
<motion.h1
|
||||
className="text-3xl font-bold mb-6"
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
>
|
||||
Manage Partners
|
||||
</motion.h1>
|
||||
|
||||
{/* Form */}
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Partner Name */}
|
||||
<div>
|
||||
<label htmlFor="name" className="block text-sm font-medium text-gray-300">
|
||||
Partner Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
id="name"
|
||||
value={formData.name || ""}
|
||||
onChange={handleChange}
|
||||
disabled={isEnabled}
|
||||
className="mt-1 block w-full bg-gray-700 p-2 rounded-lg"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Logo */}
|
||||
|
||||
|
||||
{/* Open Time */}
|
||||
<div>
|
||||
<label htmlFor="open_time" className="block text-sm font-medium text-gray-300">
|
||||
Open Time
|
||||
</label>
|
||||
<input
|
||||
type="time"
|
||||
name="open_time"
|
||||
id="open_time"
|
||||
value={formData.open_time || ""}
|
||||
onChange={handleChange}
|
||||
disabled={isEnabled}
|
||||
className="mt-1 block w-full bg-gray-700 p-2 rounded-lg"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Close Time */}
|
||||
<div>
|
||||
<label htmlFor="close_time" className="block text-sm font-medium text-gray-300">
|
||||
Close Time
|
||||
</label>
|
||||
<input
|
||||
type="time"
|
||||
name="close_time"
|
||||
id="close_time"
|
||||
value={formData.close_time || ""}
|
||||
onChange={handleChange}
|
||||
disabled={isEnabled}
|
||||
className="mt-1 block w-full bg-gray-700 p-2 rounded-lg"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Address */}
|
||||
<div>
|
||||
<label htmlFor="address" className="block text-sm font-medium text-gray-300">
|
||||
Address
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="address"
|
||||
id="address"
|
||||
value={formData.address || ""}
|
||||
onChange={handleChange}
|
||||
disabled={isEnabled}
|
||||
className="mt-1 block w-full bg-gray-700 p-2 rounded-lg"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Province */}
|
||||
<div>
|
||||
<label htmlFor="state" className="block text-sm font-medium text-gray-300">
|
||||
Province
|
||||
</label>
|
||||
<Select
|
||||
options={provinceOptions}
|
||||
value={selectedProvince}
|
||||
onChange={handleProvinceChange}
|
||||
isDisabled={isEnabled}
|
||||
className="mt-1"
|
||||
styles={{ /* your existing styles */ }}
|
||||
placeholder="Select Province"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* City */}
|
||||
<div>
|
||||
<label htmlFor="city" className="block text-sm font-medium text-gray-300">
|
||||
City
|
||||
</label>
|
||||
<Select
|
||||
options={cityOptions}
|
||||
value={selectedCity}
|
||||
onChange={handleCityChange}
|
||||
isDisabled={isEnabled || !selectedProvince}
|
||||
className="mt-1"
|
||||
styles={{ /* your existing styles */ }}
|
||||
placeholder="Select City"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Pincode */}
|
||||
<div>
|
||||
<label htmlFor="pincode" className="block text-sm font-medium text-gray-300">
|
||||
Pincode
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="pincode"
|
||||
id="pincode"
|
||||
value={formData.pincode || ""}
|
||||
onChange={handleChange}
|
||||
disabled={isEnabled}
|
||||
className="mt-1 block w-full bg-gray-700 p-2 rounded-lg"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* No. of Screens */}
|
||||
<div>
|
||||
<label htmlFor="screens" className="block text-sm font-medium text-gray-300">
|
||||
No. of Screens
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
name="screens"
|
||||
id="screens"
|
||||
value={formData.screens || ""}
|
||||
onChange={handleChange}
|
||||
disabled={isEnabled}
|
||||
className="mt-1 block w-full bg-gray-700 p-2 rounded-lg"
|
||||
required
|
||||
min="1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
<label htmlFor="yt" className="block text-sm font-medium text-gray-700">
|
||||
Youtube Links
|
||||
</label>
|
||||
<textarea
|
||||
name="yt"
|
||||
id="yt"
|
||||
placeholder="Enter YouTube links, separated by commas"
|
||||
onChange={handleChange}
|
||||
value={formData.yt || ""}
|
||||
className="block w-full bg-gray-700 p-2 rounded-lg mb-2"
|
||||
rows={4}
|
||||
required
|
||||
disabled={isEnabled}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="scrolltext" className="block text-sm font-medium text-gray-700">
|
||||
Scroll Text
|
||||
</label>
|
||||
<textarea
|
||||
type="text"
|
||||
name="scrolltext"
|
||||
id="scrolltext"
|
||||
placeholder="Scroll Text"
|
||||
onChange={handleChange}
|
||||
value={formData.scrolltext || ""}
|
||||
className="block w-full bg-gray-700 p-2 rounded-lg mb-2"
|
||||
required
|
||||
disabled={isEnabled}
|
||||
></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="logo_url" className="block text-sm font-medium text-gray-300">
|
||||
Logo
|
||||
</label>
|
||||
<img
|
||||
src={`https://ads.dine360ads.com/api/${formData.logo_url || ""}`}
|
||||
alt="Logo"
|
||||
className="mt-1 w-[60%] rounded-lg"
|
||||
/>
|
||||
<input
|
||||
type="file"
|
||||
name="logo_url"
|
||||
id="logo_url"
|
||||
accept="image/*"
|
||||
onChange={handleFileChange}
|
||||
disabled={isEnabled}
|
||||
className="mt-2 block w-full bg-gray-700 p-2 rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Edit / Update button */}
|
||||
<div className="mt-6">
|
||||
{isEnabled ? (
|
||||
<motion.button
|
||||
type="button"
|
||||
onClick={handleEdit}
|
||||
className="bg-green-500 px-4 py-2 rounded-lg"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
>
|
||||
Edit
|
||||
</motion.button>
|
||||
) : (
|
||||
<motion.button
|
||||
type="button"
|
||||
className="bg-blue-500 px-4 py-2 rounded-lg"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
onClick={handleUpdate}
|
||||
>
|
||||
Update
|
||||
</motion.button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
{/* Screens Preview */}
|
||||
{formData.screens && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mt-8">
|
||||
<div className="bg-gray-800 p-6 rounded-xl shadow-xl flex justify-center items-center ">
|
||||
<div className="">
|
||||
<motion.button
|
||||
type="button"
|
||||
onClick={AddScreen}
|
||||
className={`
|
||||
inline-flex items-center
|
||||
bg-gradient-to-r from-blue-500 to-teal-400
|
||||
hover:from-blue-600 hover:to-teal-500
|
||||
text-white font-semibold
|
||||
px-6 py-3 rounded-lg
|
||||
shadow-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-300
|
||||
transition-transform
|
||||
`}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
<FiPlus className="w-5 h-5 mr-2" />
|
||||
Add New Screen
|
||||
</motion.button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{screens.map((screen, index) => (
|
||||
|
||||
<div key={index} className="bg-gray-800 p-4 rounded-lg shadow-md relative">
|
||||
{/* Delete Button - Top Right of Card */}
|
||||
<motion.button
|
||||
type="button"
|
||||
className="absolute top-3 right-3 text-red-500 hover:text-red-700 bg-white bg-opacity-10 rounded-full p-2 transition z-10"
|
||||
whileHover={{ scale: 1.2, rotate: 15 }}
|
||||
onClick={() => {
|
||||
if (window.confirm("Are you sure you want to delete this screen?")) {
|
||||
DeleteScreen(screen.transid);
|
||||
}
|
||||
}}
|
||||
title="Delete this screen"
|
||||
>
|
||||
<FaTrash />
|
||||
</motion.button>
|
||||
|
||||
<h3 className="text-lg font-semibold mb-2">
|
||||
Screen {index + 1} ({id.substring(0, 2) + 'S' + (index + 1)})
|
||||
</h3>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-3 relative">
|
||||
{/* Preview Button */}
|
||||
<motion.button
|
||||
type="button"
|
||||
className="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded-lg shadow transition"
|
||||
whileHover={{ scale: 1.07 }}
|
||||
onClick={() => {
|
||||
const prefix = id.substring(0, 5);
|
||||
const screenName = `Screen${index + 1}`.replace(/\s+/g, '');
|
||||
window.open(`${window.location.origin}/ads/${prefix}/${screenName}`, '_blank');
|
||||
}}
|
||||
>
|
||||
Preview
|
||||
</motion.button>
|
||||
|
||||
{/* Copy Link Button */}
|
||||
<motion.button
|
||||
type="button"
|
||||
className="bg-green-600 hover:bg-green-700 px-4 py-2 rounded-lg shadow transition"
|
||||
whileHover={{ scale: 1.07 }}
|
||||
onClick={() => {
|
||||
const prefix = id.substring(0, 5);
|
||||
const screenName = `Screen${index + 1}`.replace(/\s+/g, '');
|
||||
navigator.clipboard.writeText(`${window.location.origin}/ads/${prefix}/${screenName}`);
|
||||
alert('Link copied to clipboard!');
|
||||
}}
|
||||
>
|
||||
Copy Link
|
||||
</motion.button>
|
||||
|
||||
{/* YouTube Toggle Switch */}
|
||||
<motion.div
|
||||
className="flex items-center gap-2"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
title={screen.isyoutube === 1 ? "YouTube is enabled for this screen" : "YouTube is disabled for this screen"}
|
||||
>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={screen.isyoutube === 1}
|
||||
onChange={() => handleToggleYouTube(screen.transid, screen.isyoutube !== 1, index)}
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<div className={`w-14 h-7 bg-gray-400 rounded-full peer peer-checked:bg-amber-400 peer-focus:ring-2 peer-focus:ring-amber-300 transition-all duration-300`}>
|
||||
<div
|
||||
className={`absolute left-0 top-0 w-7 h-7 rounded-full bg-white shadow-md transform transition-all duration-300 ${screen.isyoutube === 1 ? 'translate-x-7' : ''}`}
|
||||
></div>
|
||||
</div>
|
||||
<span className={`ml-4 text-sm font-medium ${screen.isyoutube === 1 ? 'text-amber-300' : 'text-fuchsia-300'}`}>
|
||||
{screen.isyoutube === 1 ? "YouTube ON" : "YouTube OFF"}
|
||||
</span>
|
||||
</label>
|
||||
</motion.div>
|
||||
|
||||
|
||||
<motion.div
|
||||
className="ml-2"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
title="Change Screen Type"
|
||||
>
|
||||
<select
|
||||
value={screen.screentype || 1}
|
||||
onChange={async (e) => {
|
||||
const newType = Number(e.target.value);
|
||||
try {
|
||||
console.log(screen.screentype)
|
||||
setLoading(true);
|
||||
await api.post("/admin/update-screen-type", {
|
||||
screenid: screen.transid,
|
||||
sts: newType,
|
||||
});
|
||||
fetchScreens();
|
||||
} catch (err) {
|
||||
console.error("Error updating screen type:", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}}
|
||||
className="bg-gray-700 text-white rounded-lg w-full px-3 py-1 focus:outline-none focus:ring-2 focus:ring-blue-400"
|
||||
>
|
||||
<option value={1}>Screen Type 1 (Normal)</option>
|
||||
{/* <option value={2}>Screen Type 2 (InHouse)</option> */}
|
||||
<option value={3}>Screen Type 3 (In-house and Scroll Text)</option>
|
||||
</select>
|
||||
</motion.div>
|
||||
|
||||
|
||||
{/* Status Indicator */}
|
||||
<div className="flex items-center ml-auto">
|
||||
<FaCircle
|
||||
className={`mr-2 ${screen.isactive === 1
|
||||
? "text-green-400"
|
||||
: screen.isactive === 2
|
||||
? "text-orange-400"
|
||||
: "text-red-400"
|
||||
}`}
|
||||
/>
|
||||
<span className="uppercase font-semibold tracking-wide">
|
||||
{screen.isactive === 1 ? "ACTIVE" : screen.isactive === 2 ? "IDLE" : "INACTIVE"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
<div>
|
||||
<br />
|
||||
{screen.status}
|
||||
<br />
|
||||
{screen.stsupdat && (
|
||||
<div>
|
||||
{new Date(screen.stsupdat).toLocaleDateString('en-GB')}{' '}
|
||||
{new Date(screen.stsupdat).toLocaleTimeString()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
export default ManagePartner;
|
||||
53
src/Pages/NewAdConfiguration.jsx
Normal file
53
src/Pages/NewAdConfiguration.jsx
Normal file
@ -0,0 +1,53 @@
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { motion } from "framer-motion";
|
||||
import { FaPlus, FaCircle } from "react-icons/fa";
|
||||
import { api } from "../API/api";
|
||||
import { useLoading } from "../Context/LoadingContext";
|
||||
import PartnersCardComponent from "../Components/PartnersPageCardComponent";
|
||||
import ClientsCardComponent from "../Components/ClientsPageCardComponent";
|
||||
import NewMappingCardComponent from "../Components/NewMappingCardComponent copy";
|
||||
|
||||
const NewAdConfiguration = () => {
|
||||
const [clients, setClients] = useState([]);
|
||||
const [upd, setUpd] = useState(0);
|
||||
const navigate = useNavigate();
|
||||
const { setLoading } = useLoading();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchClients = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await api.get("/admin/clients");
|
||||
|
||||
setClients(response);
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error fetching clients:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchClients();
|
||||
}, [upd]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white p-8">
|
||||
<motion.h1
|
||||
className="text-3xl font-bold text-center mb-8"
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
>
|
||||
Clients List
|
||||
</motion.h1>
|
||||
|
||||
|
||||
<NewMappingCardComponent Data={clients} setUpd={setUpd} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewAdConfiguration
|
||||
|
||||
57
src/Pages/NotFound.jsx
Normal file
57
src/Pages/NotFound.jsx
Normal file
@ -0,0 +1,57 @@
|
||||
import { motion } from "framer-motion";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
const NotFound = () => {
|
||||
return (
|
||||
<div className="h-screen flex flex-col items-center justify-center bg-gray-900 text-white">
|
||||
{/* Animated 404 Text */}
|
||||
<motion.h1
|
||||
className="text-9xl font-bold text-red-500"
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
transition={{ duration: 0.5, ease: "easeOut" }}
|
||||
>
|
||||
404
|
||||
</motion.h1>
|
||||
|
||||
{/* Error Message */}
|
||||
<motion.p
|
||||
className="text-xl mt-4 text-gray-300"
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.3 }}
|
||||
>
|
||||
Oops! The page you are looking for does not exist.
|
||||
</motion.p>
|
||||
|
||||
{/* Animated Button */}
|
||||
<motion.div
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
transition={{ delay: 0.6, duration: 0.3 }}
|
||||
className="mt-6"
|
||||
>
|
||||
<Link
|
||||
to="/"
|
||||
className="px-6 py-3 bg-blue-500 rounded-full text-lg font-semibold hover:bg-blue-700 transition duration-300"
|
||||
>
|
||||
Go Back Home
|
||||
</Link>
|
||||
</motion.div>
|
||||
|
||||
{/* Floating Animation */}
|
||||
<motion.div
|
||||
className="absolute top-10 left-10 w-24 h-24 bg-blue-500 rounded-full opacity-30"
|
||||
animate={{ y: [0, -20, 0] }}
|
||||
transition={{ repeat: Infinity, duration: 3, ease: "easeInOut" }}
|
||||
/>
|
||||
<motion.div
|
||||
className="absolute bottom-20 right-20 w-32 h-32 bg-red-500 rounded-full opacity-30"
|
||||
animate={{ x: [0, -20, 0] }}
|
||||
transition={{ repeat: Infinity, duration: 3, ease: "easeInOut" }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotFound;
|
||||
99
src/Pages/PartnersPage.jsx
Normal file
99
src/Pages/PartnersPage.jsx
Normal file
@ -0,0 +1,99 @@
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { motion } from "framer-motion";
|
||||
import { FaPlus, FaCircle } from "react-icons/fa";
|
||||
import { api } from "../API/api";
|
||||
import { useLoading } from "../Context/LoadingContext";
|
||||
import PartnersCardComponent from "../Components/PartnersPageCardComponent";
|
||||
|
||||
const PartnersPage = () => {
|
||||
const [clients, setClients] = useState([]);
|
||||
const [upd, setUpd] = useState(0);upd
|
||||
const navigate = useNavigate();
|
||||
const { setLoading } = useLoading();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchClients = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await api.get("/admin/partners");
|
||||
|
||||
const updatedClients = response.map(client => ({
|
||||
...client,
|
||||
is_active: client.status === "Offline" ? null : true,
|
||||
}));
|
||||
|
||||
setClients(updatedClients);
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error fetching clients:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchClients();
|
||||
}, [upd]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white p-8">
|
||||
<motion.h1
|
||||
className="text-3xl font-bold text-center mb-8"
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
>
|
||||
Partners List
|
||||
</motion.h1>
|
||||
|
||||
{/* <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<motion.div
|
||||
className="bg-gray-800 p-6 rounded-lg shadow-lg text-center cursor-pointer hover:bg-gray-700 transition flex items-center justify-center"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={() => navigate("/add-client")}
|
||||
>
|
||||
<div>
|
||||
<FaPlus className="text-5xl text-blue-500 mx-auto mb-4" />
|
||||
<p className="text-lg font-semibold">Add New Client</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
|
||||
{clients?.map((client) => (
|
||||
<motion.div
|
||||
key={client.id}
|
||||
className="bg-gray-800 p-6 rounded-lg shadow-lg relative cursor-pointer hover:bg-gray-700 transition"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
whileHover={{ scale: 1.03 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
onClick={() => navigate(`/manage-client/${client.id}`)}
|
||||
>
|
||||
<div
|
||||
className="bg-cover bg-center h-40 rounded-lg filter blur-sm"
|
||||
style={{
|
||||
backgroundImage: `url('///')`,
|
||||
}}
|
||||
></div>
|
||||
|
||||
<div className="mt-4">
|
||||
<h2 className="text-xl font-bold">{client.name}</h2>
|
||||
<p className="text-gray-400">🕒 {client.open_time} - {client.close_time}</p>
|
||||
|
||||
<div className="mt-2 ml-1 flex items-center">
|
||||
<FaCircle className={`mr-2 ${client.is_active ? "text-green-400" : "text-red-400"}`} />
|
||||
<span> {client.is_active ? "Active" : "Inactive"}</span>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div> */}
|
||||
<PartnersCardComponent Data={clients} setUpd={setUpd} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PartnersPage
|
||||
|
||||
95
src/Pages/SettingsPage.jsx
Normal file
95
src/Pages/SettingsPage.jsx
Normal file
@ -0,0 +1,95 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { motion } from "framer-motion";
|
||||
import { FaArrowLeft } from "react-icons/fa";
|
||||
import { api } from "../API/api";
|
||||
import { useLoading } from "../Context/LoadingContext";
|
||||
|
||||
const SettingsPage = () => {
|
||||
const { setLoading } = useLoading();
|
||||
const navigate = useNavigate();
|
||||
const [settingsData, setSettingsData] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchSettings();
|
||||
}, []);
|
||||
|
||||
const fetchSettings = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await api.get("/client/get-settings");
|
||||
setSettingsData(response);
|
||||
} catch (error) {
|
||||
console.error("Error fetching settings:", error);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const handleUpdate = async (transid, newValue) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
await api.post("/admin/update-settings", {
|
||||
transid: transid,
|
||||
value: newValue,
|
||||
});
|
||||
alert("General Settings Updated Successfully ")
|
||||
fetchSettings();
|
||||
} catch (error) {
|
||||
console.error("Error updating setting:", error);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white p-8">
|
||||
<motion.button
|
||||
className="mb-4 flex items-center text-gray-400 hover:text-white"
|
||||
whileHover={{ scale: 1.1 }}
|
||||
onClick={() => navigate("/admin-dashboard")}
|
||||
>
|
||||
<FaArrowLeft className="mr-2" /> Back
|
||||
</motion.button>
|
||||
|
||||
<motion.h1
|
||||
className="text-3xl font-bold mb-6"
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
>
|
||||
Settings
|
||||
</motion.h1>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> {/* Grid Layout */}
|
||||
{settingsData.map((item) => (
|
||||
<div key={item.transid} className="item">
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
{item.name}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={item.value}
|
||||
placeholder= {item.name}
|
||||
onChange={(e) => {
|
||||
const updatedSettings = settingsData.map((setting) =>
|
||||
setting.transid === item.transid
|
||||
? { ...setting, value: e.target.value }
|
||||
: setting
|
||||
);
|
||||
setSettingsData(updatedSettings);
|
||||
}}
|
||||
className="block w-full bg-gray-700 p-2 rounded-lg mb-2"
|
||||
/>
|
||||
<motion.button
|
||||
onClick={() => handleUpdate(item.transid, item.value)}
|
||||
className="bg-blue-500 px-4 py-2 rounded-lg"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
>
|
||||
Update
|
||||
</motion.button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsPage;
|
||||
67
src/Pages/YT.jsx
Normal file
67
src/Pages/YT.jsx
Normal file
@ -0,0 +1,67 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
|
||||
const YouTubePlayer = () => {
|
||||
const playerRef = useRef(null);
|
||||
const youtubeUrl = 'https://www.youtube.com/watch?v=MWm-ooierpw';
|
||||
|
||||
const getVideoId = (url) => {
|
||||
try {
|
||||
const parsedUrl = new URL(url);
|
||||
if (parsedUrl.hostname === 'youtu.be') {
|
||||
return parsedUrl.pathname.slice(1);
|
||||
} else if (parsedUrl.hostname.includes('youtube.com')) {
|
||||
return parsedUrl.searchParams.get('v');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const videoId = getVideoId(youtubeUrl);
|
||||
|
||||
useEffect(() => {
|
||||
const loadYouTubeAPI = () => {
|
||||
const tag = document.createElement('script');
|
||||
tag.src = 'https://www.youtube.com/iframe_api';
|
||||
const firstScriptTag = document.getElementsByTagName('script')[0];
|
||||
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
|
||||
};
|
||||
|
||||
loadYouTubeAPI();
|
||||
|
||||
window.onYouTubeIframeAPIReady = () => {
|
||||
playerRef.current = new window.YT.Player('ytplayer', {
|
||||
height: '450',
|
||||
width: '800',
|
||||
videoId,
|
||||
playerVars: {
|
||||
autoplay: 1,
|
||||
mute: 1,
|
||||
},
|
||||
events: {
|
||||
onReady: (event) => {
|
||||
event.target.playVideo();
|
||||
|
||||
// Try to unmute after 5 seconds
|
||||
setTimeout(() => {
|
||||
try {
|
||||
event.target.unMute();
|
||||
} catch (err) {
|
||||
console.log('Auto-unmute failed:', err);
|
||||
}
|
||||
}, 5000); // 5000ms = 5 seconds
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
}, [videoId]);
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center h-screen bg-black">
|
||||
<div id="ytplayer"></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default YouTubePlayer;
|
||||
1765
src/assets/canada_cities.json
Normal file
1765
src/assets/canada_cities.json
Normal file
File diff suppressed because it is too large
Load Diff
BIN
src/assets/dine_360_ads_fav_icon.png
Normal file
BIN
src/assets/dine_360_ads_fav_icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
1
src/assets/react.svg
Normal file
1
src/assets/react.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
1
src/index.css
Normal file
1
src/index.css
Normal file
@ -0,0 +1 @@
|
||||
@import "tailwindcss";
|
||||
11
src/main.jsx
Normal file
11
src/main.jsx
Normal file
@ -0,0 +1,11 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App";
|
||||
import { LoadingProvider } from "./Context/LoadingContext";
|
||||
import './index.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")).render(
|
||||
<LoadingProvider>
|
||||
<App />
|
||||
</LoadingProvider>
|
||||
);
|
||||
8
vite.config.js
Normal file
8
vite.config.js
Normal file
@ -0,0 +1,8 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(),tailwindcss(),],
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user