Complete UI/UX redesign — vibrant dark theme with orange/amber brand

- New collapsible sidebar (64px → 220px on hover) replaces top header
- Vibrant dark purple base (#0c0a1e) with glassmorphism cards
- Orange/amber gradient brand color throughout
- Redesigned Login page with floating orbs and glass card
- New AdminDashboard with gradient icon cards
- Partners/Clients/Mapping cards with modern hover effects
- AddPartner/ManagePartner forms with section-based glass layout
- ManageFilesPage with drag-upload zone and media grid
- ManageFilesOrderPage with collapsible screen groups and toggle UI
- SettingsPage with card-per-setting layout
- Shared CSS design system (glass-card, btn-primary, input-field, etc.)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
MOHAN 2026-06-13 16:53:05 +05:30
parent 6e8694ba96
commit 56dd06b0f7
17 changed files with 1702 additions and 1878 deletions

View File

@ -11,85 +11,59 @@ import AddClient from "./Pages/AddClient";
import ManageClient from "./Pages/ManageClient"; import ManageClient from "./Pages/ManageClient";
import ClientPartnerMapping from "./Pages/ClientPartnerMapping"; import ClientPartnerMapping from "./Pages/ClientPartnerMapping";
import NewAdConfiguration from "./Pages/NewAdConfiguration"; import NewAdConfiguration from "./Pages/NewAdConfiguration";
import Header from "./Components/Header";
import ManageFilesOrder from "./Pages/ManageFilesOrderPage"; import ManageFilesOrder from "./Pages/ManageFilesOrderPage";
import SettingsPage from "./Pages/SettingsPage"; import SettingsPage from "./Pages/SettingsPage";
import YouTubePlayer from "./Pages/YT"; import YouTubePlayer from "./Pages/YT";
import Sidebar from "./Components/Sidebar";
const ProtectedRoute = ({ children }) => {
const location = useLocation();
const storedLogin = localStorage.getItem("loggedIn");
let isLoggedIn = false;
if (storedLogin) {
try {
const parsed = JSON.parse(storedLogin);
const expired = Date.now() - parsed.timestamp > 1 * 60 * 60 * 1000;
isLoggedIn = parsed.value && !expired;
if (!isLoggedIn) localStorage.removeItem("loggedIn");
} catch {
localStorage.removeItem("loggedIn");
}
}
return isLoggedIn ? children : <Navigate to="/login" state={{ from: location }} replace />;
};
const Layout = () => { const Layout = () => {
const location = useLocation(); const location = useLocation();
const getLoginState = () => { const hideSidebarRoutes = ["/", "/login"];
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/"); const isAdsPage = location.pathname.startsWith("/ads/");
const showSidebar = !hideSidebarRoutes.includes(location.pathname) && !isAdsPage;
// 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 ( return (
<> <>
{!hideHeaderRoutes.includes(location.pathname) && !isAdsPage && <Header />} {showSidebar && <Sidebar />}
<Routes> <main style={{ paddingLeft: showSidebar ? 64 : 0, minHeight: "100vh" }}>
<Route path="/" element={<Login />} /> <Routes>
<Route path="/login" element={<Login />} /> <Route path="/" element={<Login />} />
<Route path="/yt" element={<YouTubePlayer />} /> <Route path="/login" element={<Login />} />
<Route path="/ads/:partnerid/:screenName" element={<AdsPage />} /> <Route path="/yt" element={<YouTubePlayer />} />
<Route path="/ads/:partnerid/:screenName" element={<AdsPage />} />
<Route path="/admin-dashboard" element={<ProtectedRoute><AdminDashboard /></ProtectedRoute>} /> <Route path="/admin-dashboard" element={<ProtectedRoute><AdminDashboard /></ProtectedRoute>} />
<Route path="/partners" element={<ProtectedRoute><PartnersPage /></ProtectedRoute>} /> <Route path="/partners" element={<ProtectedRoute><PartnersPage /></ProtectedRoute>} />
<Route path="/add-partner" element={<ProtectedRoute><AddPartner /></ProtectedRoute>} /> <Route path="/add-partner" element={<ProtectedRoute><AddPartner /></ProtectedRoute>} />
<Route path="/manage-partner/:id" element={<ProtectedRoute><ManagePartner /></ProtectedRoute>} /> <Route path="/manage-partner/:id" element={<ProtectedRoute><ManagePartner /></ProtectedRoute>} />
<Route path="/clients" element={<ProtectedRoute><ClientsPage /></ProtectedRoute>} /> <Route path="/clients" element={<ProtectedRoute><ClientsPage /></ProtectedRoute>} />
<Route path="/add-client" element={<ProtectedRoute><AddClient /></ProtectedRoute>} /> <Route path="/add-client" element={<ProtectedRoute><AddClient /></ProtectedRoute>} />
<Route path="/manage-client/:id" element={<ProtectedRoute><ManageClient /></ProtectedRoute>} /> <Route path="/manage-client/:id" element={<ProtectedRoute><ManageClient /></ProtectedRoute>} />
<Route path="/Client-Partner-Mapping" element={<ProtectedRoute><ClientPartnerMapping /></ProtectedRoute>} /> <Route path="/Client-Partner-Mapping" element={<ProtectedRoute><ClientPartnerMapping /></ProtectedRoute>} />
<Route path="/new-ad-configuration" element={<ProtectedRoute><NewAdConfiguration /></ProtectedRoute>} /> <Route path="/new-ad-configuration" element={<ProtectedRoute><NewAdConfiguration /></ProtectedRoute>} />
<Route path="/ads-order-configuration/:id" element={<ProtectedRoute><ManageFilesOrder /></ProtectedRoute>} /> <Route path="/ads-order-configuration/:id" element={<ProtectedRoute><ManageFilesOrder /></ProtectedRoute>} />
<Route path="/settings" element={<ProtectedRoute><SettingsPage /></ProtectedRoute>} /> <Route path="/settings" element={<ProtectedRoute><SettingsPage /></ProtectedRoute>} />
<Route path="*" element={<NotFound />} /> <Route path="*" element={<NotFound />} />
</Routes> </Routes>
</main>
</> </>
); );
}; };
@ -102,4 +76,4 @@ function App() {
); );
} }
export default App; export default App;

View File

@ -1,95 +1,89 @@
import React from 'react'
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { FaBuilding, FaUsers, FaTv, FaCog } from "react-icons/fa";
import { useNavigate } from "react-router-dom";
import { FaPlus, FaCircle } from "react-icons/fa"; const cards = [
import { useNavigate } from 'react-router-dom'; {
icon: FaBuilding,
label: "Partners",
description: "Manage restaurant partners",
path: "/partners",
gradient: "linear-gradient(135deg, #f97316, #f59e0b)",
glow: "rgba(249,115,22,0.3)",
bg: "rgba(249,115,22,0.08)",
border: "rgba(249,115,22,0.15)",
},
{
icon: FaUsers,
label: "Clients",
description: "Manage advertising clients",
path: "/clients",
gradient: "linear-gradient(135deg, #a855f7, #6366f1)",
glow: "rgba(168,85,247,0.3)",
bg: "rgba(168,85,247,0.08)",
border: "rgba(168,85,247,0.15)",
},
{
icon: FaTv,
label: "Ads Configuration",
description: "Configure ad campaigns",
path: "/Client-Partner-Mapping",
gradient: "linear-gradient(135deg, #06b6d4, #3b82f6)",
glow: "rgba(6,182,212,0.3)",
bg: "rgba(6,182,212,0.08)",
border: "rgba(6,182,212,0.15)",
},
{
icon: FaCog,
label: "General Settings",
description: "System configuration",
path: "/settings",
gradient: "linear-gradient(135deg, #10b981, #059669)",
glow: "rgba(16,185,129,0.3)",
bg: "rgba(16,185,129,0.08)",
border: "rgba(16,185,129,0.15)",
},
];
export default function AdminDashboardComponent() { export default function AdminDashboardComponent() {
const navigate = useNavigate(); const navigate = useNavigate();
return (
<> return (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-5">
{cards.map(({ icon: Icon, label, description, path, gradient, glow, bg, border }, i) => (
<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" key={path}
whileHover={{ scale: 1.05 }} onClick={() => navigate(path)}
whileTap={{ scale: 0.95 }} className="rounded-2xl p-6 cursor-pointer flex flex-col gap-4"
onClick={() => navigate("/Partners")} style={{ background: bg, border: `1px solid ${border}`, transition: "all 0.2s" }}
> initial={{ opacity: 0, y: 20 }}
<div animate={{ opacity: 1, y: 0 }}
className="bg-cover bg-center h-40 rounded-lg filter blur-sm" transition={{ delay: i * 0.08 }}
style={{ whileHover={{
backgroundImage: `url('///')`, scale: 1.03,
}} boxShadow: `0 16px 40px ${glow}`,
></div> borderColor: border.replace("0.15", "0.4"),
}}
<div> whileTap={{ scale: 0.98 }}
{/* <FaPlus className="text-5xl text-blue-500 mx-auto mb-4" /> */} >
<p className="text-lg font-semibold">Partners</p> <div
</div> className="w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0"
</motion.div> style={{ background: gradient, boxShadow: `0 6px 20px ${glow}` }}
>
<motion.div <Icon className="w-5 h-5 text-white" />
className="bg-gray-800 p-6 rounded-lg shadow-lg text-center cursor-pointer hover:bg-gray-700 transition flex items-center justify-center" </div>
whileHover={{ scale: 1.05 }} <div>
whileTap={{ scale: 0.95 }} <h3 className="text-white font-bold text-base mb-0.5">{label}</h3>
onClick={() => navigate("/Clients")} <p className="text-sm" style={{ color: "#64748b" }}>{description}</p>
> </div>
<div <div
className="bg-cover bg-center h-40 rounded-lg filter blur-sm" className="mt-auto text-xs font-semibold flex items-center gap-1"
style={{ style={{ color: gradient.includes("f97316") ? "#fb923c" : gradient.includes("a855f7") ? "#c084fc" : gradient.includes("06b6d4") ? "#22d3ee" : "#34d399" }}
backgroundImage: `url('///')`, >
}} Open
></div> </div>
</motion.div>
))}
<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>
</>
)
} }

View File

@ -1,87 +1,95 @@
import React from 'react'
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { FaPlus, FaTrash, FaUser } from "react-icons/fa";
import { FaPlus, FaCircle } from "react-icons/fa";
import { api } from "../API/api"; import { api } from "../API/api";
import { useNavigate } from 'react-router-dom'; import { useNavigate } from "react-router-dom";
import { useLoading } from '../Context/LoadingContext'; import { useLoading } from "../Context/LoadingContext";
export default function ClientsCardComponent({ Data, setUpd }) { export default function ClientsCardComponent({ Data, setUpd }) {
const navigate = useNavigate(); const navigate = useNavigate();
const { setLoading } = useLoading(); const { setLoading } = useLoading();
const handleDelete = async (clientid) => {
try {
setLoading(true);
await api.delete(`/admin/delete-client/${clientid}`);
setUpd(clientid);
} catch (error) {
console.error("Error deleting client:", error);
}
setLoading(false);
};
const handleDelete = async (clientid) => { return (
console.log(clientid) <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-5">
try { {/* Add card */}
setLoading(true); <motion.div
const response = await api.delete(`/admin/delete-client/${clientid}`); className="rounded-2xl p-6 cursor-pointer flex flex-col items-center justify-center gap-3 min-h-[160px]"
console.log(response); style={{
setUpd(clientid) background: "rgba(168,85,247,0.05)",
} catch (error) { border: "2px dashed rgba(168,85,247,0.25)",
console.error("Error fetching files:", error); }}
} whileHover={{
setLoading(false) scale: 1.03,
}; background: "rgba(168,85,247,0.1)",
borderColor: "rgba(168,85,247,0.5)",
}}
whileTap={{ scale: 0.97 }}
onClick={() => navigate("/add-client")}
>
<div
className="w-12 h-12 rounded-full flex items-center justify-center"
style={{ background: "rgba(168,85,247,0.15)" }}
>
<FaPlus className="w-5 h-5" style={{ color: "#c084fc" }} />
</div>
<p className="text-sm font-semibold" style={{ color: "#c084fc" }}>Add New Client</p>
</motion.div>
{/* Client cards */}
{Data?.map((client, i) => (
<motion.div
key={client.transid}
className="rounded-2xl p-5 cursor-pointer relative flex items-center gap-4"
style={{
background: "rgba(255,255,255,0.04)",
border: "1px solid rgba(255,255,255,0.08)",
}}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: i * 0.05 }}
whileHover={{
scale: 1.02,
borderColor: "rgba(168,85,247,0.25)",
boxShadow: "0 10px 30px rgba(0,0,0,0.4)",
}}
whileTap={{ scale: 0.98 }}
onClick={() => navigate(`/manage-client/${client.transid}`)}
>
<div
className="w-10 h-10 rounded-xl flex items-center justify-center flex-shrink-0"
style={{ background: "rgba(168,85,247,0.15)" }}
>
<FaUser className="w-4 h-4" style={{ color: "#c084fc" }} />
</div>
<div className="flex-1 min-w-0">
<h3 className="text-white font-semibold text-sm truncate">{client.name}</h3>
<p className="text-xs mt-0.5" style={{ color: "#475569" }}>Client</p>
</div>
<button
return ( className="w-7 h-7 rounded-lg flex items-center justify-center flex-shrink-0 transition-all"
style={{ background: "rgba(239,68,68,0.12)", border: "1px solid rgba(239,68,68,0.18)" }}
<> onClick={(e) => {
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6"> e.stopPropagation();
if (confirm("Delete this client?")) handleDelete(client.transid);
<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" onMouseEnter={e => { e.currentTarget.style.background = "rgba(239,68,68,0.28)"; }}
whileHover={{ scale: 1.05 }} onMouseLeave={e => { e.currentTarget.style.background = "rgba(239,68,68,0.12)"; }}
whileTap={{ scale: 0.95 }} >
onClick={() => navigate("/add-client")} <FaTrash className="w-3 h-3 text-red-400" />
> </button>
<div> </motion.div>
<FaPlus className="text-5xl text-blue-500 mx-auto mb-4" /> ))}
<p className="text-lg font-semibold">Add New Client</p> </div>
</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>
</>
)
} }

View File

@ -1,87 +1,70 @@
import React from 'react'
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { FaPlus, FaTv } from "react-icons/fa";
import { useNavigate } from "react-router-dom";
import { FaPlus, FaCircle } from "react-icons/fa"; export default function MappingComponent({ Data }) {
import { api } from "../API/api"; const navigate = useNavigate();
import { useNavigate } from 'react-router-dom';
import { useLoading } from '../Context/LoadingContext';
export default function MappingComponent({ Data, setUpd }) { return (
const navigate = useNavigate(); <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-5">
const { setLoading } = useLoading(); {/* Add card */}
<motion.div
className="rounded-2xl p-6 cursor-pointer flex flex-col items-center justify-center gap-3 min-h-[160px]"
style={{
background: "rgba(6,182,212,0.05)",
border: "2px dashed rgba(6,182,212,0.25)",
}}
whileHover={{
scale: 1.03,
background: "rgba(6,182,212,0.1)",
borderColor: "rgba(6,182,212,0.5)",
}}
whileTap={{ scale: 0.97 }}
onClick={() => navigate("/new-ad-configuration")}
>
<div
className="w-12 h-12 rounded-full flex items-center justify-center"
style={{ background: "rgba(6,182,212,0.15)" }}
>
<FaPlus className="w-5 h-5" style={{ color: "#22d3ee" }} />
</div>
<p className="text-sm font-semibold text-center" style={{ color: "#22d3ee" }}>
Add / Edit Ad Configuration
</p>
</motion.div>
{/* Partner cards */}
const handleDelete = async (clientid) => { {Data?.map((client, i) => (
console.log(clientid) <motion.div
try { key={client.transid}
setLoading(true); className="rounded-2xl p-5 cursor-pointer relative flex items-center gap-4"
const response = await api.delete(`/admin/delete-client/${clientid}`); style={{
console.log(response); background: "rgba(255,255,255,0.04)",
setUpd(clientid) border: "1px solid rgba(255,255,255,0.08)",
} catch (error) { }}
console.error("Error fetching files:", error); initial={{ opacity: 0, y: 20 }}
} animate={{ opacity: 1, y: 0 }}
setLoading(false) transition={{ delay: i * 0.05 }}
}; whileHover={{
scale: 1.02,
borderColor: "rgba(6,182,212,0.25)",
boxShadow: "0 10px 30px rgba(0,0,0,0.4)",
return ( }}
whileTap={{ scale: 0.98 }}
<> onClick={() => navigate(`/ads-order-configuration/${client.transid}`)}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6"> >
<div
<motion.div className="w-10 h-10 rounded-xl flex items-center justify-center flex-shrink-0"
className="bg-gray-800 p-6 rounded-lg shadow-lg text-center cursor-pointer hover:bg-gray-700 transition flex items-center justify-center" style={{ background: "rgba(6,182,212,0.12)" }}
whileHover={{ scale: 1.05 }} >
whileTap={{ scale: 0.95 }} <FaTv className="w-4 h-4" style={{ color: "#22d3ee" }} />
onClick={() => navigate("/new-ad-configuration")} </div>
> <div className="flex-1 min-w-0">
<div> <h3 className="text-white font-semibold text-sm truncate">{client.name}</h3>
<FaPlus className="text-5xl text-blue-500 mx-auto mb-4" /> <p className="text-xs mt-0.5" style={{ color: "#475569" }}>Configure ads </p>
<p className="text-lg font-semibold">Add / Edit Ad Configuration</p> </div>
</div> </motion.div>
</motion.div> ))}
{Data?.map((client) => ( </div>
<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>
</>
)
} }

View File

@ -1,92 +1,110 @@
import React from 'react'
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { FaPlus, FaTrash, FaClock } from "react-icons/fa";
import { FaPlus, FaCircle } from "react-icons/fa";
import { api } from "../API/api"; import { api } from "../API/api";
import { useNavigate } from 'react-router-dom'; import { useNavigate } from "react-router-dom";
import { useLoading } from '../Context/LoadingContext'; import { useLoading } from "../Context/LoadingContext";
export default function PartnersCardComponent({ Data, setUpd }) { export default function PartnersCardComponent({ Data, setUpd }) {
const navigate = useNavigate(); const navigate = useNavigate();
const { setLoading } = useLoading(); const { setLoading } = useLoading();
const handleDelete = async (partnerid) => {
try {
setLoading(true);
await api.delete(`/admin/delete-partner/${partnerid}`);
setUpd(partnerid);
} catch (error) {
console.error("Error deleting partner:", error);
}
setLoading(false);
};
const handleDelete = async (partnerid) => { return (
console.log(partnerid) <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-5">
try { {/* Add card */}
setLoading(true); <motion.div
const response = await api.delete(`/admin/delete-partner/${partnerid}`); className="rounded-2xl p-6 cursor-pointer flex flex-col items-center justify-center gap-3 min-h-[200px]"
console.log(response); style={{
setUpd(partnerid) background: "rgba(249,115,22,0.05)",
} catch (error) { border: "2px dashed rgba(249,115,22,0.25)",
console.error("Error fetching files:", error); }}
} whileHover={{
setLoading(false) scale: 1.03,
}; background: "rgba(249,115,22,0.1)",
borderColor: "rgba(249,115,22,0.5)",
}}
whileTap={{ scale: 0.97 }}
onClick={() => navigate("/add-partner")}
>
<div
className="w-12 h-12 rounded-full flex items-center justify-center"
style={{ background: "rgba(249,115,22,0.15)" }}
>
<FaPlus className="text-orange-400 w-5 h-5" />
</div>
<p className="text-orange-400 font-semibold text-sm">Add New Partner</p>
</motion.div>
{/* Partner cards */}
{Data?.map((client, i) => (
<motion.div
key={client.transid}
className="rounded-2xl overflow-hidden cursor-pointer relative"
style={{
background: "rgba(255,255,255,0.04)",
border: "1px solid rgba(255,255,255,0.08)",
}}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: i * 0.05 }}
whileHover={{
scale: 1.02,
borderColor: "rgba(249,115,22,0.25)",
boxShadow: "0 12px 35px rgba(0,0,0,0.4)",
}}
whileTap={{ scale: 0.98 }}
onClick={() => navigate(`/manage-partner/${client.transid}`)}
>
{/* Thumbnail */}
<div
className="h-36 bg-cover bg-center relative"
style={{
backgroundImage: `url('https://backend.dine360ads.com/${client.logo_url || ""}')`,
backgroundColor: "rgba(255,255,255,0.03)",
}}
>
<div
className="absolute inset-0"
style={{ background: "linear-gradient(to bottom, transparent 40%, rgba(12,10,30,0.9) 100%)" }}
/>
</div>
{/* Info */}
<div className="p-4">
<h3 className="text-white font-bold text-sm mb-1 truncate">{client.name}</h3>
{(client.open_time || client.close_time) && (
<div className="flex items-center gap-1.5 text-xs" style={{ color: "#64748b" }}>
<FaClock className="w-3 h-3" />
<span>{client.open_time} {client.close_time}</span>
</div>
)}
</div>
{/* Delete */}
return ( <button
className="absolute top-2 right-2 w-7 h-7 rounded-lg flex items-center justify-center transition-all"
<> style={{ background: "rgba(239,68,68,0.15)", border: "1px solid rgba(239,68,68,0.2)" }}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6"> onClick={(e) => {
e.stopPropagation();
<motion.div if (confirm("Delete this partner?")) handleDelete(client.transid);
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 }} onMouseEnter={e => { e.currentTarget.style.background = "rgba(239,68,68,0.3)"; }}
whileTap={{ scale: 0.95 }} onMouseLeave={e => { e.currentTarget.style.background = "rgba(239,68,68,0.15)"; }}
onClick={() => navigate("/add-partner")} >
> <FaTrash className="w-3 h-3 text-red-400" />
<div> </button>
<FaPlus className="text-5xl text-blue-500 mx-auto mb-4" /> </motion.div>
<p className="text-lg font-semibold">Add New Partner</p> ))}
</div> </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://backend.dine360ads.com/${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>
</>
)
} }

142
src/Components/Sidebar.jsx Normal file
View File

@ -0,0 +1,142 @@
import { useState } from "react";
import { Link, useLocation, useNavigate } from "react-router-dom";
import { motion, AnimatePresence } from "framer-motion";
import {
FaTachometerAlt, FaBuilding, FaUsers, FaTv, FaCog, FaSignOutAlt,
} from "react-icons/fa";
const navItems = [
{ icon: FaTachometerAlt, label: "Dashboard", path: "/admin-dashboard" },
{ icon: FaBuilding, label: "Partners", path: "/partners" },
{ icon: FaUsers, label: "Clients", path: "/clients" },
{ icon: FaTv, label: "Ads Config",path: "/Client-Partner-Mapping" },
{ icon: FaCog, label: "Settings", path: "/settings" },
];
const NavItem = ({ icon: Icon, label, path, expanded }) => {
const location = useLocation();
const isActive =
location.pathname.toLowerCase() === path.toLowerCase() ||
location.pathname.toLowerCase().startsWith(path.toLowerCase() + "/");
return (
<Link
to={path}
className={`relative flex items-center h-11 px-4 mx-2 rounded-xl transition-all duration-200 group
${isActive
? "text-orange-400"
: "text-slate-400 hover:text-white"
}`}
style={{ background: isActive ? "rgba(249,115,22,0.12)" : undefined }}
onMouseEnter={e => { if (!isActive) e.currentTarget.style.background = "rgba(255,255,255,0.05)"; }}
onMouseLeave={e => { if (!isActive) e.currentTarget.style.background = ""; }}
>
{isActive && (
<span className="absolute left-0 top-1/2 -translate-y-1/2 w-[3px] h-5 rounded-full bg-orange-400" />
)}
<Icon className="w-4 h-4 flex-shrink-0" />
<AnimatePresence>
{expanded && (
<motion.span
className="ml-3 text-sm font-medium whitespace-nowrap overflow-hidden"
initial={{ opacity: 0, x: -6 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -6 }}
transition={{ duration: 0.15 }}
>
{label}
</motion.span>
)}
</AnimatePresence>
</Link>
);
};
const Sidebar = () => {
const [expanded, setExpanded] = useState(false);
const navigate = useNavigate();
const handleLogout = () => {
localStorage.removeItem("loggedIn");
navigate("/login");
};
return (
<motion.aside
onMouseEnter={() => setExpanded(true)}
onMouseLeave={() => setExpanded(false)}
animate={{ width: expanded ? 220 : 64 }}
transition={{ duration: 0.25, ease: "easeInOut" }}
className="fixed left-0 top-0 h-full z-50 flex flex-col overflow-hidden"
style={{
background: "linear-gradient(180deg, #100c24 0%, #160e30 100%)",
borderRight: "1px solid rgba(249,115,22,0.1)",
boxShadow: "4px 0 30px rgba(0,0,0,0.5)",
}}
>
{/* Logo row */}
<div
className="flex items-center h-16 px-4 flex-shrink-0"
style={{ borderBottom: "1px solid rgba(255,255,255,0.05)" }}
>
<img
src="/logo.png"
alt="Dine360"
className="w-8 h-8 rounded-full flex-shrink-0"
style={{ boxShadow: "0 0 0 2px rgba(249,115,22,0.3)" }}
/>
<AnimatePresence>
{expanded && (
<motion.div
className="ml-3 overflow-hidden"
initial={{ opacity: 0, width: 0 }}
animate={{ opacity: 1, width: "auto" }}
exit={{ opacity: 0, width: 0 }}
transition={{ duration: 0.2 }}
>
<p className="text-white font-bold text-sm whitespace-nowrap leading-tight">Dine 360 Ads</p>
<p className="whitespace-nowrap text-xs" style={{ color: "rgba(249,115,22,0.6)" }}>Admin Panel</p>
</motion.div>
)}
</AnimatePresence>
</div>
{/* Nav */}
<nav className="flex-1 py-3 space-y-0.5 overflow-hidden">
{navItems.map((item) => (
<NavItem key={item.path} {...item} expanded={expanded} />
))}
</nav>
{/* Logout */}
<div
className="flex-shrink-0 pb-3 pt-2"
style={{ borderTop: "1px solid rgba(255,255,255,0.05)" }}
>
<button
onClick={handleLogout}
className="flex items-center h-11 px-4 mx-2 rounded-xl transition-all duration-200 w-[calc(100%-16px)] text-red-400"
onMouseEnter={e => { e.currentTarget.style.background = "rgba(248,113,113,0.08)"; }}
onMouseLeave={e => { e.currentTarget.style.background = ""; }}
>
<FaSignOutAlt className="w-4 h-4 flex-shrink-0" />
<AnimatePresence>
{expanded && (
<motion.span
className="ml-3 text-sm font-medium whitespace-nowrap"
initial={{ opacity: 0, x: -6 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -6 }}
transition={{ duration: 0.15 }}
>
Logout
</motion.span>
)}
</AnimatePresence>
</button>
</div>
</motion.aside>
);
};
export default Sidebar;

View File

@ -1,310 +1,199 @@
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { FaArrowLeft } from "react-icons/fa"; import { FaArrowLeft, FaBuilding } from "react-icons/fa";
import { api } from "../API/api"; import { api } from "../API/api";
import { useLoading } from "../Context/LoadingContext"; import { useLoading } from "../Context/LoadingContext";
import Select from 'react-select'; import Select from "react-select";
import canada_cities from '../assets/canada_cities.json'; import canada_cities from "../assets/canada_cities.json";
const selectStyles = {
control: (b) => ({
...b,
background: "rgba(255,255,255,0.05)",
border: "1px solid rgba(255,255,255,0.1)",
borderRadius: "0.75rem",
padding: "4px",
boxShadow: "none",
"&:hover": { borderColor: "rgba(249,115,22,0.4)" },
}),
menu: (b) => ({
...b,
background: "#1a1530",
border: "1px solid rgba(255,255,255,0.1)",
borderRadius: "0.75rem",
zIndex: 99,
}),
option: (b, s) => ({
...b,
background: s.isSelected ? "rgba(249,115,22,0.2)" : s.isFocused ? "rgba(255,255,255,0.05)" : "transparent",
color: s.isSelected ? "#fb923c" : "#f1f5f9",
}),
singleValue: (b) => ({ ...b, color: "#f1f5f9" }),
input: (b) => ({ ...b, color: "#f1f5f9" }),
placeholder: (b) => ({ ...b, color: "#475569" }),
};
const AddPartner = () => { const AddPartner = () => {
const [uploading, setUploading] = useState(false); const [uploading, setUploading] = useState(false);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: "", name: "", open_time: "", close_time: "", image: null,
open_time: "", address: "", city: "", state: "", pincode: "", screens: "",
close_time: "",
image: null,
address: "",
city: "",
state: "",
pincode: "",
screens: "",
}); });
const [selectedProvince, setSelectedProvince] = useState(null);
const [selectedCity, setSelectedCity] = useState(null);
const { setLoading } = useLoading(); const { setLoading } = useLoading();
const navigate = useNavigate(); const navigate = useNavigate();
const handleChange = (e) => { const provinceOptions = useMemo(
const { name, value } = e.target; () => Object.keys(canada_cities).map((p) => ({ value: p, label: p })),
setFormData({ ...formData, [name]: value }); []
}; );
const cityOptions = useMemo(
() => selectedProvince ? canada_cities[selectedProvince.value].map((c) => ({ value: c, label: c })) : [],
[selectedProvince]
);
const handleChange = (e) => setFormData({ ...formData, [e.target.name]: e.target.value });
const handleFileChange = (e) => setFormData({ ...formData, image: e.target.files[0] });
const handleProvinceChange = (o) => { setSelectedProvince(o); setSelectedCity(null); setFormData({ ...formData, state: o?.value || "" }); };
const handleCityChange = (o) => { setSelectedCity(o); setFormData({ ...formData, city: o?.value || "" }); };
const handleFileUpload = async (file,id) => { const handleFileUpload = async (file, id) => {
if (!file) return; if (!file) return;
setUploading(true); setUploading(true);
const formData = new FormData(); const fd = new FormData();
formData.append("file", file); fd.append("file", file);
formData.append("file_name", file.name); fd.append("file_name", file.name);
formData.append("client_id", id); // Attach client ID fd.append("client_id", id);
try { try {
console.log(formData) setLoading(true);
setLoading(true) await api.post("/files/update-partner-logo", fd, { headers: { "Content-Type": "multipart/form-data" } });
await api.post("/files/update-partner-logo", formData, {
headers: { "Content-Type": "multipart/form-data" },
});
navigate("/partners"); navigate("/partners");
} catch (e) {
// Refresh file list after each successful upload console.error(e);
//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 { } finally {
setUploading(false); // Keep this false, as we're handling individual uploads setUploading(false);
setLoading(false) setLoading(false);
} }
}; };
const handleFileChange = async (e) => {
setFormData({ ...formData, image: e.target.files[0] });
};
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
try { try {
console.log("iuytg")
setLoading(true); setLoading(true);
var data = await api.post("/admin/add-partner", formData); const data = await api.post("/admin/add-partner", formData);
console.log(data) await handleFileUpload(formData.image, data.id);
console.log("iuytg") navigate("/partners");
await handleFileUpload(formData.image,data.id); } catch (e) {
navigate("/Partners"); console.error(e);
} catch (error) {
console.error("Error adding Partner:", error);
} }
setLoading(false); 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 ( return (
<div className="min-h-screen bg-gray-900 text-white p-8"> <div className="page-wrapper">
<motion.button {/* Header */}
className="mb-4 flex items-center text-gray-400 hover:text-white" <motion.div className="mb-8" initial={{ opacity: 0, y: -15 }} animate={{ opacity: 1, y: 0 }}>
whileHover={{ scale: 1.1 }} <button
onClick={() => navigate("/admin-dashboard")} className="flex items-center gap-2 text-sm mb-4 transition-colors"
> style={{ color: "#64748b" }}
<FaArrowLeft className="mr-2" /> Back onMouseEnter={e => { e.currentTarget.style.color = "#f1f5f9"; }}
</motion.button> onMouseLeave={e => { e.currentTarget.style.color = "#64748b"; }}
onClick={() => navigate("/partners")}
<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 <FaArrowLeft className="w-3 h-3" /> Back to Partners
</motion.button> </button>
</form> <div className="flex items-center gap-3">
<div
className="w-10 h-10 rounded-xl flex items-center justify-center"
style={{ background: "linear-gradient(135deg, #f97316, #f59e0b)", boxShadow: "0 4px 15px rgba(249,115,22,0.35)" }}
>
<FaBuilding className="w-4 h-4 text-white" />
</div>
<div>
<h1 className="page-title">Add New Partner</h1>
<p className="page-subtitle">Fill in the details to register a new partner</p>
</div>
</div>
</motion.div>
<div className="mb-6 h-px" style={{ background: "linear-gradient(90deg, rgba(249,115,22,0.4), transparent)" }} />
{/* Form */}
<motion.form
onSubmit={handleSubmit}
initial={{ opacity: 0, y: 15 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.15 }}
>
<div
className="rounded-2xl p-6 mb-6"
style={{ background: "rgba(255,255,255,0.03)", border: "1px solid rgba(255,255,255,0.07)" }}
>
<h2 className="text-white font-bold text-sm mb-5 uppercase tracking-widest" style={{ color: "#94a3b8" }}>
Basic Information
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
<div>
<label className="label">Partner Name</label>
<input name="name" type="text" placeholder="e.g. Shiva's Dosa" onChange={handleChange} className="input-field" required minLength="1" maxLength="255" />
</div>
<div>
<label className="label">Logo</label>
<input name="logo_url" type="file" accept="image/*" onChange={handleFileChange} className="input-field" style={{ paddingTop: "0.6rem" }} />
</div>
<div>
<label className="label">Open Time</label>
<input name="open_time" type="time" onChange={handleChange} className="input-field" required />
</div>
<div>
<label className="label">Close Time</label>
<input name="close_time" type="time" onChange={handleChange} className="input-field" required />
</div>
<div>
<label className="label">No. of Screens</label>
<input name="screens" type="number" placeholder="e.g. 3" onChange={handleChange} className="input-field" required min="1" />
</div>
</div>
</div>
<div
className="rounded-2xl p-6 mb-6"
style={{ background: "rgba(255,255,255,0.03)", border: "1px solid rgba(255,255,255,0.07)" }}
>
<h2 className="text-white font-bold text-sm mb-5 uppercase tracking-widest" style={{ color: "#94a3b8" }}>
Location
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
<div className="md:col-span-2">
<label className="label">Address</label>
<input name="address" type="text" placeholder="Street address" onChange={handleChange} className="input-field" required />
</div>
<div>
<label className="label">Province</label>
<Select options={provinceOptions} value={selectedProvince} onChange={handleProvinceChange} isSearchable styles={selectStyles} placeholder="Select Province" />
</div>
<div>
<label className="label">City</label>
<Select options={cityOptions} value={selectedCity} onChange={handleCityChange} isSearchable isDisabled={!selectedProvince} styles={selectStyles} placeholder="Select City" />
</div>
<div>
<label className="label">Postal Code</label>
<input name="pincode" type="text" placeholder="Postal code" onChange={handleChange} className="input-field" required />
</div>
</div>
</div>
<div className="flex gap-3">
<motion.button type="submit" className="btn-primary" whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
{uploading ? "Saving..." : "Add Partner"}
</motion.button>
<button type="button" className="btn-secondary" onClick={() => navigate("/partners")}>Cancel</button>
</div>
</motion.form>
</div> </div>
); );
}; };
export default AddPartner; export default AddPartner;

View File

@ -1,55 +1,31 @@
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { motion } from "framer-motion"; 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"; import AdminDashboardComponent from "../Components/AdminDashboardComponent";
const AdminDashboard = () => { const AdminDashboard = () => {
const [clients, setClients] = useState([]); return (
const navigate = useNavigate(); <div className="page-wrapper">
const { setLoading } = useLoading(); {/* Header */}
<motion.div
className="mb-8"
initial={{ opacity: 0, y: -15 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
>
<h1 className="page-title">
<span className="gradient-text">Admin Dashboard</span>
</h1>
<p className="page-subtitle">Welcome back manage your Dine 360 Ads platform</p>
</motion.div>
// useEffect(() => { {/* Divider */}
// const fetchClients = async () => { <div
// try { className="mb-6 h-px"
// setLoading(true); style={{ background: "linear-gradient(90deg, rgba(249,115,22,0.4), transparent)" }}
// const response = await api.get("/admin/clients"); />
// const updatedClients = response.map(client => ({ <AdminDashboardComponent />
// ...client, </div>
// 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; export default AdminDashboard;

View File

@ -1,53 +1,39 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { FaPlus, FaCircle } from "react-icons/fa";
import { api } from "../API/api"; import { api } from "../API/api";
import { useLoading } from "../Context/LoadingContext"; import { useLoading } from "../Context/LoadingContext";
import PartnersCardComponent from "../Components/PartnersPageCardComponent";
import ClientsCardComponent from "../Components/ClientsPageCardComponent";
import MappingComponent from "../Components/MappingComponent"; import MappingComponent from "../Components/MappingComponent";
const ClientPartnerMapping = () => { const ClientPartnerMapping = () => {
const [clients, setClients] = useState([]); const [clients, setClients] = useState([]);
const [upd, setUpd] = useState(0); const [upd, setUpd] = useState(0);
const navigate = useNavigate(); const { setLoading } = useLoading();
const { setLoading } = useLoading();
useEffect(() => { useEffect(() => {
const fetchClients = async () => { const fetch = async () => {
try { try {
setLoading(true); setLoading(true);
const response = await api.get("/admin/partners"); const response = await api.get("/admin/partners");
setClients(response);
} catch (e) {
console.error(e);
} finally {
setLoading(false);
}
};
fetch();
}, [upd]);
setClients(response); return (
<div className="page-wrapper">
<motion.div className="mb-8" initial={{ opacity: 0, y: -15 }} animate={{ opacity: 1, y: 0 }}>
} catch (error) { <h1 className="page-title"><span className="gradient-text">Ads Configuration</span></h1>
console.error("Error fetching clients:", error); <p className="page-subtitle">Configure ad campaigns per partner and screen</p>
} finally { </motion.div>
setLoading(false); <div className="mb-6 h-px" style={{ background: "linear-gradient(90deg, rgba(6,182,212,0.4), transparent)" }} />
} <MappingComponent Data={clients} setUpd={setUpd} />
}; </div>
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 export default ClientPartnerMapping;

View File

@ -1,52 +1,39 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { FaPlus, FaCircle } from "react-icons/fa";
import { api } from "../API/api"; import { api } from "../API/api";
import { useLoading } from "../Context/LoadingContext"; import { useLoading } from "../Context/LoadingContext";
import PartnersCardComponent from "../Components/PartnersPageCardComponent";
import ClientsCardComponent from "../Components/ClientsPageCardComponent"; import ClientsCardComponent from "../Components/ClientsPageCardComponent";
const ClientsPage = () => { const ClientsPage = () => {
const [clients, setClients] = useState([]); const [clients, setClients] = useState([]);
const [upd, setUpd] = useState(0); const [upd, setUpd] = useState(0);
const navigate = useNavigate(); const { setLoading } = useLoading();
const { setLoading } = useLoading();
useEffect(() => { useEffect(() => {
const fetchClients = async () => { const fetch = async () => {
try { try {
setLoading(true); setLoading(true);
const response = await api.get("/admin/clients"); const response = await api.get("/admin/clients");
setClients(response);
} catch (e) {
console.error(e);
} finally {
setLoading(false);
}
};
fetch();
}, [upd]);
setClients(response); return (
<div className="page-wrapper">
<motion.div className="mb-8" initial={{ opacity: 0, y: -15 }} animate={{ opacity: 1, y: 0 }}>
} catch (error) { <h1 className="page-title"><span className="gradient-text">Clients</span></h1>
console.error("Error fetching clients:", error); <p className="page-subtitle">Manage all advertising clients</p>
} finally { </motion.div>
setLoading(false); <div className="mb-6 h-px" style={{ background: "linear-gradient(90deg, rgba(168,85,247,0.4), transparent)" }} />
} <ClientsCardComponent Data={clients} setUpd={setUpd} />
}; </div>
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 export default ClientsPage;

View File

@ -1,7 +1,7 @@
import { useEffect, useState } from "react"; import { useState } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { FaLock, FaUser } from "react-icons/fa"; import { FaLock, FaUser, FaFire } from "react-icons/fa";
const Login = () => { const Login = () => {
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
@ -12,117 +12,175 @@ const Login = () => {
const handleLogin = (e) => { const handleLogin = (e) => {
e.preventDefault(); e.preventDefault();
if (username === "admin" && password === "admin") { if (username === "admin" && password === "admin") {
localStorage.setItem('loggedIn', JSON.stringify({ localStorage.setItem("loggedIn", JSON.stringify({ timestamp: Date.now(), value: true }));
timestamp: Date.now(),
value: true,
}));
navigate("/admin-dashboard"); navigate("/admin-dashboard");
} else { } else {
setError("❌ Invalid credentials! Try again."); setError("Invalid credentials. Please 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 ( return (
<div className="h-screen flex items-center justify-center bg-gray-900 text-white"> <div
className="min-h-screen flex items-center justify-center relative overflow-hidden"
style={{ background: "linear-gradient(135deg, #080614 0%, #130929 50%, #0c0518 100%)" }}
>
{/* Background orbs */}
<div
className="absolute pointer-events-none"
style={{
width: 600, height: 600,
borderRadius: "50%",
background: "radial-gradient(circle, rgba(249,115,22,0.12) 0%, transparent 70%)",
top: "-20%", left: "-15%",
filter: "blur(40px)",
}}
/>
<div
className="absolute pointer-events-none"
style={{
width: 500, height: 500,
borderRadius: "50%",
background: "radial-gradient(circle, rgba(168,85,247,0.1) 0%, transparent 70%)",
bottom: "-15%", right: "-10%",
filter: "blur(40px)",
}}
/>
<div
className="absolute pointer-events-none"
style={{
width: 300, height: 300,
borderRadius: "50%",
background: "radial-gradient(circle, rgba(245,158,11,0.08) 0%, transparent 70%)",
top: "40%", right: "20%",
filter: "blur(30px)",
}}
/>
{/* Card */}
<motion.div <motion.div
className="bg-gray-800 p-8 rounded-lg shadow-xl w-96" className="relative z-10 w-full max-w-md mx-4"
initial={{ scale: 0 }} initial={{ opacity: 0, y: 30 }}
animate={{ scale: 1 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }} transition={{ duration: 0.5, ease: "easeOut" }}
> >
{/* Animated Heading */} <div
<motion.h2 className="rounded-3xl p-8"
className="text-2xl font-bold text-center mb-6" style={{
initial={{ opacity: 0, y: -20 }} background: "rgba(255,255,255,0.04)",
animate={{ opacity: 1, y: 0 }} backdropFilter: "blur(30px)",
transition={{ delay: 0.3 }} border: "1px solid rgba(255,255,255,0.08)",
boxShadow: "0 25px 60px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.03)",
}}
> >
🔐 Admin Login {/* Logo + Brand */}
</motion.h2> <div className="text-center mb-8">
<motion.div
{/* Input Fields */} className="inline-flex items-center justify-center w-16 h-16 rounded-2xl mb-4"
<form onSubmit={handleLogin} className="space-y-4"> style={{ background: "linear-gradient(135deg, #f97316, #f59e0b)", boxShadow: "0 8px 30px rgba(249,115,22,0.4)" }}
<div className="flex items-center border-b-2 border-gray-600 py-2"> initial={{ scale: 0 }}
<FaUser className="text-gray-400 mr-2" /> animate={{ scale: 1 }}
<input transition={{ delay: 0.2, type: "spring", stiffness: 200 }}
type="text" >
placeholder="Username" <img src="/logo.png" alt="Dine360" className="w-10 h-10 rounded-xl object-cover" />
className="w-full bg-transparent border-none focus:outline-none text-lg" </motion.div>
value={username} <motion.h1
onChange={(e) => setUsername(e.target.value)} className="text-2xl font-extrabold text-white mb-1"
/> initial={{ opacity: 0, y: -10 }}
</div> animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
<div className="flex items-center border-b-2 border-gray-600 py-2"> >
<FaLock className="text-gray-400 mr-2" /> Welcome Back
<input </motion.h1>
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 <motion.p
className="text-red-400 text-center" className="text-sm"
style={{ color: "#64748b" }}
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
transition={{ delay: 0.4 }}
> >
{error} Sign in to Dine 360 Ads Admin
</motion.p> </motion.p>
)} </div>
{/* Login Button */} {/* Form */}
<motion.button <motion.form
type="submit" onSubmit={handleLogin}
className="w-full bg-blue-600 py-2 mt-4 rounded-lg text-lg font-semibold hover:bg-blue-700 transition duration-300" className="space-y-4"
whileHover={{ scale: 1.05 }} initial={{ opacity: 0 }}
whileTap={{ scale: 0.95 }} animate={{ opacity: 1 }}
transition={{ delay: 0.35 }}
> >
🚀 Login {/* Username */}
</motion.button> <div>
</form> <label className="label">Username</label>
</motion.div> <div className="relative">
<FaUser
className="absolute left-4 top-1/2 -translate-y-1/2 w-3.5 h-3.5"
style={{ color: "#475569" }}
/>
<input
type="text"
placeholder="Enter username"
value={username}
onChange={(e) => setUsername(e.target.value)}
className="input-field"
style={{ paddingLeft: "2.5rem" }}
required
/>
</div>
</div>
{/* Floating Emojis Animation */} {/* Password */}
<motion.div <div>
className="absolute top-10 left-10 text-5xl" <label className="label">Password</label>
animate={{ y: [0, -15, 0] }} <div className="relative">
transition={{ repeat: Infinity, duration: 2 }} <FaLock
> className="absolute left-4 top-1/2 -translate-y-1/2 w-3.5 h-3.5"
🔑 style={{ color: "#475569" }}
</motion.div> />
<input
type="password"
placeholder="Enter password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="input-field"
style={{ paddingLeft: "2.5rem" }}
required
/>
</div>
</div>
<motion.div {/* Error */}
className="absolute bottom-10 right-10 text-5xl" {error && (
animate={{ rotate: [0, 20, -20, 0] }} <motion.div
transition={{ repeat: Infinity, duration: 2 }} className="flex items-center gap-2 px-4 py-3 rounded-xl text-sm text-red-300"
> style={{ background: "rgba(239,68,68,0.1)", border: "1px solid rgba(239,68,68,0.2)" }}
🔥 initial={{ opacity: 0, y: -5 }}
animate={{ opacity: 1, y: 0 }}
>
<span></span>
{error}
</motion.div>
)}
{/* Submit */}
<motion.button
type="submit"
className="btn-primary w-full flex items-center justify-center gap-2 mt-2"
style={{ padding: "0.85rem", fontSize: "0.95rem" }}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<FaFire className="w-4 h-4" />
Sign In
</motion.button>
</motion.form>
</div>
{/* Tagline */}
<p className="text-center mt-6 text-xs" style={{ color: "#334155" }}>
Dine 360 Ads · Digital Signage Platform
</p>
</motion.div> </motion.div>
</div> </div>
); );

View File

@ -1,253 +1,233 @@
import { useEffect, useState, useRef, useMemo } from "react"; import { useEffect, useState, useMemo } from "react";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { motion, AnimatePresence, Reorder } from "framer-motion"; import { motion, AnimatePresence, Reorder } from "framer-motion";
import { FaArrowUp, FaArrowDown } from "react-icons/fa"; import { FaArrowUp, FaArrowDown, FaArrowLeft, FaSave, FaChevronDown, FaChevronRight } from "react-icons/fa";
import { api } from "../API/api"; import { api } from "../API/api";
import { useLoading } from "../Context/LoadingContext"; import { useLoading } from "../Context/LoadingContext";
import ViewPartnerAdsConfiguration from "../Components/ViewPartnerAdsConfiguration"; import ViewPartnerAdsConfiguration from "../Components/ViewPartnerAdsConfiguration";
const ManageFilesOrder = () => { const ManageFilesOrder = () => {
const { id } = useParams(); const { id } = useParams();
const BACKEND = 'https://backend.dine360ads.com/'; const BACKEND = "https://backend.dine360ads.com/";
const [files, setFiles] = useState([]); const [files, setFiles] = useState([]);
const [openScreens, setOpenScreens] = useState({}); const [openScreens, setOpenScreens] = useState({});
const navigate = useNavigate(); const navigate = useNavigate();
const { setLoading } = useLoading(); const { setLoading } = useLoading();
// Fetch and sort files
useEffect(() => { useEffect(() => {
(async () => { (async () => {
try { try {
setLoading(true); setLoading(true);
const response = await api.get(`/admin/partner-ads?partnerid=${id}`); const res = await api.get(`/admin/partner-ads?partnerid=${id}`);
console.log(response) const sorted = res.sort((a, b) => (a?.screenname || "").localeCompare(b?.screenname || "", undefined, { numeric: true }));
const sorted = response.sort((a, b) => {
const nameA = a?.screenname || '';
const nameB = b?.screenname || '';
return nameA.localeCompare(nameB, undefined, { numeric: true });
});
setFiles(sorted); setFiles(sorted);
console.log("FILESSSSSSSSSSSSS<",sorted) } catch (e) { console.error(e); }
} catch (error) { finally { setLoading(false); }
console.error("Error fetching files:", error);
} finally {
setLoading(false);
}
})(); })();
}, [id, setLoading]); }, [id, setLoading]);
// Group files by screenname
const filesByScreen = useMemo(() => const filesByScreen = useMemo(() =>
files.reduce((acc, file) => { files.reduce((acc, file) => {
const key = file.screenname; const key = file.screenname;
acc[key] = acc[key] || []; acc[key] = acc[key] || [];
acc[key].push(file); acc[key].push(file);
return acc; return acc;
}, {}), }, {}), [files]
[files]
); );
// Toggle collapse for screen const toggleScreen = (s) => setOpenScreens(prev => ({ ...prev, [s]: !prev[s] }));
const toggleScreen = (screen) => {
setOpenScreens(prev => ({ ...prev, [screen]: !prev[screen] }));
};
// Merge reordered group back into files array
const handleGroupReorder = (screen, newGroup) => { const handleGroupReorder = (screen, newGroup) => {
const other = files.filter(f => f.screenname !== screen); setFiles([...files.filter(f => f.screenname !== screen), ...newGroup]);
setFiles([...other, ...newGroup]);
}; };
// Save flattened order
const saveOrder = async () => { const saveOrder = async () => {
try { try {
setLoading(true); setLoading(true);
await api.post("/admin/reorder-partner-ads", files); await api.post("/admin/reorder-partner-ads", files);
alert("Order saved successfully"); alert("Order saved successfully");
} catch (error) { } catch (e) { console.error(e); alert("Error saving order."); }
console.error("Error saving order:", error); finally { setLoading(false); }
alert("Error saving order. Check console for details.");
} finally {
setLoading(false);
}
}; };
// Checkbox handlers update flat files state const handleToggleMainAd = (file, checked) =>
const handleToggleMainAd = (file, checked) => { setFiles(files.map(f => f.file_path === file.file_path && file.screenid === f.screenid ? { ...f, ismainad: checked ? 1 : 0 } : f));
setFiles(files.map(f => const handleToggleCarousel = (file, checked) =>
f.file_path === file.file_path && file.screenid === f.screenid? { ...f, ismainad: checked ? 1 : 0 } : f 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));
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 groupMoveUp = (screen, index) => {
const group = filesByScreen[screen]; const group = [...filesByScreen[screen]];
if (index <= 0) return; if (index <= 0) return;
const newGroup = [...group]; [group[index - 1], group[index]] = [group[index], group[index - 1]];
[newGroup[index - 1], newGroup[index]] = [newGroup[index], newGroup[index - 1]]; handleGroupReorder(screen, group);
handleGroupReorder(screen, newGroup);
}; };
const groupMoveDown = (screen, index) => { const groupMoveDown = (screen, index) => {
const group = filesByScreen[screen]; const group = [...filesByScreen[screen]];
if (index >= group.length - 1) return; if (index >= group.length - 1) return;
const newGroup = [...group]; [group[index], group[index + 1]] = [group[index + 1], group[index]];
[newGroup[index], newGroup[index + 1]] = [newGroup[index + 1], newGroup[index]]; handleGroupReorder(screen, group);
handleGroupReorder(screen, newGroup);
}; };
return ( const Toggle = ({ label, checked, onChange }) => (
<div className="min-h-screen bg-gray-900 text-white p-8"> <label className="flex items-center gap-2 cursor-pointer">
<motion.h1 <div
className="text-3xl font-bold mb-6" className="relative w-9 h-5 rounded-full transition-all duration-250"
initial={{ opacity: 0, y: -20 }} style={{ background: checked ? "rgba(249,115,22,0.6)" : "rgba(255,255,255,0.1)" }}
animate={{ opacity: 1, y: 0 }} onClick={onChange}
> >
📂 Manage Ads Order <div
</motion.h1> className="absolute top-0.5 w-4 h-4 rounded-full bg-white shadow transition-all duration-250"
style={{ left: checked ? "calc(100% - 18px)" : "2px" }}
/>
</div>
<span className="text-xs font-medium" style={{ color: checked ? "#fb923c" : "#475569" }}>{label}</span>
</label>
);
{/* Collapsible groups by screen */} return (
<div className="space-y-6"> <div className="page-wrapper">
<motion.div className="mb-8" initial={{ opacity: 0, y: -15 }} animate={{ opacity: 1, y: 0 }}>
<button
className="flex items-center gap-2 text-sm mb-4 transition-colors"
style={{ color: "#64748b" }}
onMouseEnter={e => { e.currentTarget.style.color = "#f1f5f9"; }}
onMouseLeave={e => { e.currentTarget.style.color = "#64748b"; }}
onClick={() => navigate("/Client-Partner-Mapping")}
>
<FaArrowLeft className="w-3 h-3" /> Back
</button>
<div className="flex items-center justify-between flex-wrap gap-4">
<div>
<h1 className="page-title"><span className="gradient-text">Manage Ads Order</span></h1>
<p className="page-subtitle">Drag to reorder, toggle ad types, save changes</p>
</div>
<motion.button
onClick={saveOrder}
className="btn-primary flex items-center gap-2"
whileHover={{ scale: 1.03 }}
>
<FaSave className="w-3.5 h-3.5" /> Save Order
</motion.button>
</div>
</motion.div>
<div className="mb-6 h-px" style={{ background: "linear-gradient(90deg, rgba(249,115,22,0.4), transparent)" }} />
<div className="space-y-4 mb-12">
{Object.entries(filesByScreen).map(([screen, groupFiles]) => ( {Object.entries(filesByScreen).map(([screen, groupFiles]) => (
<div key={screen}> <div
key={screen}
className="rounded-2xl overflow-hidden"
style={{ background: "rgba(255,255,255,0.03)", border: "1px solid rgba(255,255,255,0.07)" }}
>
<button <button
onClick={() => toggleScreen(screen)} onClick={() => toggleScreen(screen)}
className="w-full flex justify-between items-center bg-gray-700 px-4 py-2 rounded-lg" className="w-full flex justify-between items-center px-5 py-4 transition-all"
style={{ background: openScreens[screen] ? "rgba(249,115,22,0.06)" : "transparent" }}
onMouseEnter={e => { if (!openScreens[screen]) e.currentTarget.style.background = "rgba(255,255,255,0.03)"; }}
onMouseLeave={e => { if (!openScreens[screen]) e.currentTarget.style.background = "transparent"; }}
> >
<span className="text-lg font-medium">{screen}</span> <div className="flex items-center gap-3">
<span className="text-2xl">{openScreens[screen] ? '' : '+'}</span> <span
className="text-xs font-mono px-2 py-0.5 rounded-md"
style={{ background: "rgba(249,115,22,0.1)", color: "#fb923c" }}
>
{screen}
</span>
<span className="text-white font-semibold text-sm">
{groupFiles.length} ad{groupFiles.length !== 1 ? "s" : ""}
</span>
</div>
{openScreens[screen]
? <FaChevronDown className="w-3.5 h-3.5 text-orange-400" />
: <FaChevronRight className="w-3.5 h-3.5" style={{ color: "#475569" }} />}
</button> </button>
<AnimatePresence> <AnimatePresence>
{openScreens[screen] && ( {openScreens[screen] && (
<Reorder.Group <motion.div
axis="y" initial={{ height: 0, opacity: 0 }}
values={groupFiles} animate={{ height: "auto", opacity: 1 }}
onReorder={(newOrder) => handleGroupReorder(screen, newOrder)} exit={{ height: 0, opacity: 0 }}
className="bg-gray-800 p-4 rounded-lg mt-2 space-y-2" transition={{ duration: 0.2 }}
style={{ overflow: "hidden" }}
> >
{groupFiles.map((file, index) => { <Reorder.Group
const fileName = file.file_path.split("_")[1]; axis="y"
const fileUrl = BACKEND + file.file_path; values={groupFiles}
const isImage = /\.(jpe?g|png|gif)$/i.test(fileUrl); onReorder={(newOrder) => handleGroupReorder(screen, newOrder)}
const isVideo = /\.(mp4|webm|ogg|mkv)$/i.test(fileUrl); className="p-4 space-y-3"
>
{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 ( return (
<Reorder.Item <Reorder.Item
key={file.id || file.file_path} key={file.id || file.file_path}
value={file} value={file}
layout layout
className="flex items-center justify-between border-b border-gray-700 pb-2" className="rounded-xl p-3 flex flex-wrap items-center gap-4 cursor-grab active:cursor-grabbing"
> style={{ background: "rgba(255,255,255,0.04)", border: "1px solid rgba(255,255,255,0.07)" }}
<div className="flex items-center space-x-4"> >
{isImage && ( {/* Preview */}
// <img src={fileUrl} alt={fileName} className="w-20 h-20 object-cover rounded-lg" /> <div className="rounded-lg overflow-hidden flex-shrink-0" style={{ width: 80, height: 56 }}>
<img {isImage && <img src={fileUrl} alt={fileName} className="w-full h-full object-cover" />}
src={fileUrl} {isVideo && <video className="w-full h-full object-cover"><source src={fileUrl} type="video/mp4" /></video>}
alt={fileName} </div>
className="h-40 w-auto object-cover rounded-lg"
/>
)} {/* Name */}
{isVideo && ( <span className="text-white text-xs font-medium flex-1 min-w-0 truncate">{fileName}</span>
// <video controls className="w-20 h-20 rounded-lg">
// <source src={fileUrl} type="video/mp4" /> {/* Toggles */}
// </video> <div className="flex flex-wrap gap-4">
<video <Toggle label="Main Ad" checked={!!file.ismainad} onChange={() => handleToggleMainAd(file, !file.ismainad)} />
controls <Toggle label="Carousel" checked={!!file.iscarousel} onChange={() => handleToggleCarousel(file, !file.iscarousel)} />
className="h-40 w-auto rounded-lg" <Toggle label="In-House" checked={!!file.isinhousead} onChange={() => handleToggleInHouse(file, !file.isinhousead)} />
</div>
{/* Up/Down */}
<div className="flex gap-1 flex-shrink-0">
<button
onClick={() => groupMoveUp(screen, index)}
disabled={index === 0}
className="w-7 h-7 rounded-lg flex items-center justify-center transition-all disabled:opacity-30"
style={{ background: "rgba(255,255,255,0.06)" }}
onMouseEnter={e => { if (index !== 0) e.currentTarget.style.background = "rgba(249,115,22,0.2)"; }}
onMouseLeave={e => { e.currentTarget.style.background = "rgba(255,255,255,0.06)"; }}
> >
<source src={fileUrl} type="video/mp4" /> <FaArrowUp className="w-3 h-3" style={{ color: "#94a3b8" }} />
</video> </button>
<button
)} onClick={() => groupMoveDown(screen, index)}
<span className="text-gray-300">{fileName}</span> disabled={index === groupFiles.length - 1}
</div> className="w-7 h-7 rounded-lg flex items-center justify-center transition-all disabled:opacity-30"
style={{ background: "rgba(255,255,255,0.06)" }}
<div className="flex items-center space-x-4"> onMouseEnter={e => { if (index !== groupFiles.length - 1) e.currentTarget.style.background = "rgba(249,115,22,0.2)"; }}
{/* Checkboxes */} onMouseLeave={e => { e.currentTarget.style.background = "rgba(255,255,255,0.06)"; }}
<label className="flex items-center space-x-1"> >
<input <FaArrowDown className="w-3 h-3" style={{ color: "#94a3b8" }} />
type="checkbox" </button>
checked={file.ismainad} </div>
onChange={e => handleToggleMainAd(file, e.target.checked)} </Reorder.Item>
/> );
<span>Main Ad</span> })}
</label> </Reorder.Group>
<label className="flex items-center space-x-1"> </motion.div>
<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> </AnimatePresence>
</div> </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> </div>
<motion.h1 {/* Ads Configurations section */}
className="text-3xl font-bold mb-6 mt-12" <div className="mb-4 h-px" style={{ background: "linear-gradient(90deg, rgba(249,115,22,0.4), transparent)" }} />
initial={{ opacity: 0, y: -20 }} <h2 className="text-white font-bold text-lg mb-6">Ads Configurations</h2>
animate={{ opacity: 1, y: 0 }}
>
📂 Ads Configurations
</motion.h1>
<ViewPartnerAdsConfiguration data={files} /> <ViewPartnerAdsConfiguration data={files} />
</div> </div>
); );
}; };
export default ManageFilesOrder; export default ManageFilesOrder;

View File

@ -1,158 +1,136 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { FaArrowLeft, FaUpload } from "react-icons/fa"; import { FaUpload, FaTrash, FaFileImage, FaFileVideo } from "react-icons/fa";
import { api } from "../API/api"; import { api } from "../API/api";
import { useLoading } from "../Context/LoadingContext"; import { useLoading } from "../Context/LoadingContext";
const ManageFiles = ({ id }) => { const ManageFiles = ({ id }) => {
const BACKEND = 'https://backend.dine360ads.com/' const BACKEND = "https://backend.dine360ads.com/";
const [files, setFiles] = useState([]); const [files, setFiles] = useState([]);
const [upd, setupd] = useState(0); const [upd, setupd] = useState(0);
const [uploading, setUploading] = useState(false); const [uploading, setUploading] = useState(false);
const navigate = useNavigate();
const { setLoading } = useLoading(); const { setLoading } = useLoading();
// Fetch files when component loads
useEffect(() => { useEffect(() => { fetchFiles(); }, [id, upd]);
fetchFiles();
}, [id, upd]);
const fetchFiles = async () => { const fetchFiles = async () => {
try { try {
setLoading(true); setLoading(true);
const response = await api.get(`/client/files/${id}`); const res = await api.get(`/client/files/${id}`);
setFiles(response); setFiles(res);
} catch (error) { } catch (e) { console.error(e); }
console.error("Error fetching files:", error); setLoading(false);
}
setLoading(false)
}; };
const deleteFiles = async (fileid) => { const deleteFiles = async (fileid) => {
console.log(fileid)
try { try {
setLoading(true); setLoading(true);
const response = await api.delete(`/files/del/${fileid}`); await api.delete(`/files/del/${fileid}`);
console.log(response); setupd(upd + 1);
setupd(upd + 1) } catch (e) { console.error(e); }
} catch (error) { setLoading(false);
console.error("Error fetching files:", error);
}
setLoading(false)
}; };
const handleFileUpload = async (file) => { const handleFileUpload = async (file) => {
if (!file) return; if (!file) return;
setUploading(true); setUploading(true);
const formData = new FormData(); const fd = new FormData();
formData.append("file", file); fd.append("file", file);
formData.append("file_name", file.name); fd.append("file_name", file.name);
formData.append("client_id", id); // Attach client ID fd.append("client_id", id);
try { try {
setLoading(true) setLoading(true);
await api.post("/files/upload", formData, { await api.post("/files/upload", fd, { headers: { "Content-Type": "multipart/form-data" } });
headers: { "Content-Type": "multipart/form-data" },
});
// Refresh file list after each successful upload
fetchFiles(); fetchFiles();
} catch (error) { } catch (e) { console.error(e); }
console.error(`File upload failed for ${file.name}:`, error); finally { setUploading(false); setLoading(false); }
// 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 handleMultipleUpload = async (e) => {
const filesToUpload = e.target.files; const filesToUpload = e.target.files;
if (!filesToUpload || filesToUpload.length === 0) return; if (!filesToUpload?.length) return;
setUploading(true); setUploading(true);
// Iterate through each selected file and call the upload function
for (let i = 0; i < filesToUpload.length; i++) { for (let i = 0; i < filesToUpload.length; i++) {
await handleFileUpload(filesToUpload[i]); await handleFileUpload(filesToUpload[i]);
} }
setUploading(false); // Set uploading to false after all files are processed setUploading(false);
}; };
return ( return (
<div className="min-h-screen bg-gray-900 text-white p-8"> <div>
{/* Back Button */} <h2 className="text-white font-bold text-lg mb-4">Media Files</h2>
{/* <motion.button
className="mb-4 flex items-center text-gray-400 hover:text-white" {/* Upload zone */}
whileHover={{ scale: 1.1 }} <label
onClick={() => navigate("/admin-dashboard")} className="flex flex-col items-center justify-center gap-3 rounded-2xl p-8 mb-6 cursor-pointer transition-all"
style={{
background: "rgba(249,115,22,0.04)",
border: "2px dashed rgba(249,115,22,0.2)",
}}
onMouseEnter={e => { e.currentTarget.style.borderColor = "rgba(249,115,22,0.45)"; e.currentTarget.style.background = "rgba(249,115,22,0.08)"; }}
onMouseLeave={e => { e.currentTarget.style.borderColor = "rgba(249,115,22,0.2)"; e.currentTarget.style.background = "rgba(249,115,22,0.04)"; }}
> >
<FaArrowLeft className="mr-2" /> Back <div
</motion.button> */} className="w-12 h-12 rounded-xl flex items-center justify-center"
style={{ background: "linear-gradient(135deg, #f97316, #f59e0b)", boxShadow: "0 4px 15px rgba(249,115,22,0.3)" }}
>
<FaUpload className="w-5 h-5 text-white" />
</div>
<div className="text-center">
<p className="text-white font-semibold text-sm">{uploading ? "Uploading..." : "Click to upload files"}</p>
<p className="text-xs mt-1" style={{ color: "#475569" }}>Images and videos supported</p>
</div>
<input type="file" onChange={handleMultipleUpload} className="hidden" multiple />
</label>
{/* Page Title */} {/* File list */}
<motion.h1 {files.length === 0 ? (
className="text-3xl font-bold mb-6" <p className="text-center py-8 text-sm" style={{ color: "#334155" }}>No files uploaded yet.</p>
initial={{ opacity: 0, y: -20 }} ) : (
animate={{ opacity: 1, y: 0 }} <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
> {files.map((file, index) => {
📂 Manage Client Files const fileName = file.file_name;
</motion.h1> const fileUrl = BACKEND + file.file_path;
const isImage = /\.(jpe?g|png|gif)$/i.test(fileUrl);
const isVideo = /\.(mp4|webm|ogg|mkv)$/i.test(fileUrl);
{/* Upload Button */} return (
<div className="space-y-4"> <motion.div
<label className="bg-gray-700 px-4 py-2 rounded-lg cursor-pointer flex items-center gap-2 hover:bg-gray-600"> key={index}
<FaUpload /> className="rounded-2xl overflow-hidden"
<span>{uploading ? "Uploading..." : "Upload Files"}</span> style={{ background: "rgba(255,255,255,0.04)", border: "1px solid rgba(255,255,255,0.08)" }}
<input initial={{ opacity: 0, scale: 0.97 }}
type="file" animate={{ opacity: 1, scale: 1 }}
onChange={handleMultipleUpload} transition={{ delay: index * 0.04 }}
className="hidden" >
multiple // Add the 'multiple' attribute here <div className="relative h-36 bg-black flex items-center justify-center" style={{ background: "rgba(0,0,0,0.3)" }}>
/> {isImage && <img src={fileUrl} alt={fileName} className="h-full w-full object-cover" />}
</label> {isVideo && <video className="h-full w-full object-cover" controls><source src={fileUrl} type="video/mp4" /></video>}
{!isImage && !isVideo && (
<ul className="bg-gray-800 p-4 rounded-lg space-y-2"> <div className="flex flex-col items-center gap-2 opacity-40">
{files.length > 0 ? ( <FaFileImage className="w-8 h-8 text-white" />
files.map((file, index) => { </div>
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>
)} )}
</div>
<div className="p-3 flex items-center justify-between gap-2">
<p className="text-xs text-white truncate flex-1">{fileName}</p>
<button <button
onClick={() => { className="w-7 h-7 rounded-lg flex items-center justify-center flex-shrink-0 transition-all"
if (confirm("Are you sure you want to delete this file?")) { style={{ background: "rgba(239,68,68,0.12)", border: "1px solid rgba(239,68,68,0.18)" }}
deleteFiles(file.transid); onClick={() => { if (confirm("Delete this file?")) deleteFiles(file.transid); }}
} onMouseEnter={e => { e.currentTarget.style.background = "rgba(239,68,68,0.28)"; }}
}} onMouseLeave={e => { e.currentTarget.style.background = "rgba(239,68,68,0.12)"; }}
className="text-red-500 hover:text-red-700 text-sm font-bold"
> >
Delete <FaTrash className="w-3 h-3 text-red-400" />
</button> </button>
</li> </div>
); </motion.div>
}) );
) : ( })}
<p className="text-gray-500">No files uploaded yet.</p> </div>
)} )}
</ul>
</div>
</div> </div>
); );
}; };
export default ManageFiles; export default ManageFiles;

View File

@ -1,59 +1,51 @@
import { useEffect, useState, useMemo } from "react"; import { useEffect, useState, useMemo } from "react";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { motion } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import { FaArrowLeft, FaTrash, FaUpload } from "react-icons/fa"; import { FaArrowLeft, FaTrash, FaPlus, FaCircle, FaEdit, FaSave } from "react-icons/fa";
import { api } from "../API/api"; import { api } from "../API/api";
import { FaPlus, FaCircle } from "react-icons/fa"; import Select from "react-select";
import Select from 'react-select'; import canada_cities from "../assets/canada_cities.json";
import canada_cities from '../assets/canada_cities.json';
import { useLoading } from "../Context/LoadingContext"; import { useLoading } from "../Context/LoadingContext";
import { FiPlus } from "react-icons/fi";
const selectStyles = {
control: (b, s) => ({
...b,
background: "rgba(255,255,255,0.05)",
border: `1px solid ${s.isFocused ? "rgba(249,115,22,0.5)" : "rgba(255,255,255,0.1)"}`,
borderRadius: "0.75rem",
padding: "4px",
boxShadow: s.isFocused ? "0 0 0 3px rgba(249,115,22,0.08)" : "none",
opacity: s.isDisabled ? 0.5 : 1,
"&:hover": { borderColor: "rgba(249,115,22,0.4)" },
}),
menu: (b) => ({ ...b, background: "#1a1530", border: "1px solid rgba(255,255,255,0.1)", borderRadius: "0.75rem", zIndex: 99 }),
option: (b, s) => ({
...b,
background: s.isSelected ? "rgba(249,115,22,0.2)" : s.isFocused ? "rgba(255,255,255,0.05)" : "transparent",
color: s.isSelected ? "#fb923c" : "#f1f5f9",
}),
singleValue: (b) => ({ ...b, color: "#f1f5f9" }),
input: (b) => ({ ...b, color: "#f1f5f9" }),
placeholder: (b) => ({ ...b, color: "#475569" }),
};
const ManagePartner = () => { const ManagePartner = () => {
const { id } = useParams(); const { id } = useParams();
const [files, setFiles] = useState([]);
const [partners, setPartners] = useState([]);
const [screens, setScreens] = useState([]); const [screens, setScreens] = useState([]);
const [uploading, setUploading] = useState(false);
const navigate = useNavigate();
const [isEnabled, setIsEnabled] = useState(true); const [isEnabled, setIsEnabled] = useState(true);
const { setLoading } = useLoading(); const { setLoading } = useLoading();
const navigate = useNavigate();
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
transid: "", transid: "", name: "", logo_url: "", open_time: "", close_time: "",
name: "", address: "", city: "", state: "", pincode: "", screens: "", yt: "", scrolltext: "",
logo_url: "",
open_time: "",
close_time: "",
address: "",
city: "",
state: "",
pincode: "",
screens: "",
yt: "",
scrolltext: ""
}); });
const [selectedProvince, setSelectedProvince] = useState(null); const [selectedProvince, setSelectedProvince] = useState(null);
const [selectedCity, setSelectedCity] = useState(null); const [selectedCity, setSelectedCity] = useState(null);
const provinceOptions = useMemo( const provinceOptions = useMemo(() => Object.keys(canada_cities).map((p) => ({ value: p, label: p })), []);
() =>
Object.keys(canada_cities).map((province) => ({
value: province,
label: province,
})),
[canada_cities]
);
const cityOptions = useMemo( const cityOptions = useMemo(
() => () => selectedProvince ? canada_cities[selectedProvince.value].map((c) => ({ value: c, label: c })) : [],
selectedProvince [selectedProvince]
? canada_cities[selectedProvince.value].map((city) => ({
value: city,
label: city,
}))
: [],
[selectedProvince, canada_cities]
); );
useEffect(() => { useEffect(() => {
@ -61,566 +53,352 @@ const ManagePartner = () => {
fetchScreens(); fetchScreens();
}, [id]); }, [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 () => { const fetchPartners = async () => {
try { try {
const res = await api.get(`/admin/get-partner/${id}`);
const response = await api.get(`/admin/get-partner/${id}`); setFormData(res[0]);
setPartners(response); if (res[0]) {
setFormData(response[0]); setSelectedProvince({ value: res[0].state, label: res[0].state });
if (response[0]) { setSelectedCity({ value: res[0].city, label: res[0].city });
setSelectedProvince({ value: response[0].state, label: response[0].state });
setSelectedCity({ value: response[0].city, label: response[0].city });
} }
} catch (error) { } catch (e) { console.error(e); }
console.error("Error fetching files:", error);
}
}; };
const fetchScreens = async () => { const fetchScreens = async () => {
try { try {
const response = await api.get(`/admin/get-screen/${id}`); const res = await api.get(`/admin/get-screen/${id}`);
console.log(response) setScreens(res);
setScreens(response); } catch (e) { console.error(e); }
console.log(response)
} catch (error) {
console.error("Error fetching files:", error);
}
}; };
const handleFileUpload = async (file) => { const handleFileUpload = async (file) => {
if (!file) return; if (!file) return;
const fd = new FormData();
setUploading(true); fd.append("file", file);
const formData = new FormData(); fd.append("file_name", file.name);
formData.append("file", file); fd.append("client_id", id);
formData.append("file_name", file.name);
formData.append("client_id", id); // Attach client ID
try { try {
setLoading(true) setLoading(true);
await api.post("/files/update-partner-logo", formData, { await api.post("/files/update-partner-logo", fd, { headers: { "Content-Type": "multipart/form-data" } });
headers: { "Content-Type": "multipart/form-data" }, fetchPartners();
}); } catch (e) { console.error(e); }
navigate("/partners"); finally { setLoading(false); }
// 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 handleChange = (e) => setFormData({ ...formData, [e.target.name]: e.target.value });
const handleFileChange = async (e) => { const handleFileChange = async (e) => {
console.log("oiouyt")
await handleFileUpload(e.target.files[0]);
setFormData({ ...formData, image: e.target.files[0] }); setFormData({ ...formData, image: e.target.files[0] });
await handleFileUpload(e.target.files[0]);
}; };
const handleProvinceChange = (o) => { setSelectedProvince(o); setSelectedCity(null); setFormData({ ...formData, state: o?.value || "" }); };
const handleChange = (e) => { const handleCityChange = (o) => { setSelectedCity(o); setFormData({ ...formData, city: o?.value || "" }); };
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) => { const handleUpdate = async (e) => {
e.preventDefault(); e.preventDefault();
try { try {
console.log(formData)
await api.post("/admin/update-partner", formData); await api.post("/admin/update-partner", formData);
setIsEnabled(true);
navigate("/partners"); navigate("/partners");
} catch (e) { console.error(e); }
} catch (error) {
console.error("Error updating Partner:", error);
}
}; };
const AddScreen = async () => { const AddScreen = async () => {
try { try {
console.log({ partnerid: id })
setLoading(true); setLoading(true);
var data = await api.post("/admin/add-screen", { partnerid: id }); await api.post("/admin/add-screen", { partnerid: id });
console.log(data)
fetchScreens(); fetchScreens();
} catch (error) { } catch (e) { console.error(e); }
console.error("Error adding Screen:", error); finally { setLoading(false); }
}
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);
}
}; };
const DeleteScreen = async (sid) => {
try {
setLoading(true);
await api.post("/admin/delete-screen", { screenid: sid });
fetchScreens();
} catch (e) { console.error(e); }
finally { setLoading(false); }
};
const handleToggleYouTube = async (screenid, sts) => {
try {
setLoading(true);
await api.post("/admin/update-screen-youtube", { screenid, sts: sts ? 1 : 0 });
fetchScreens();
} catch (e) { console.error(e); }
finally { setLoading(false); }
};
const statusColor = (s) => s === 1 ? "#4ade80" : s === 2 ? "#fb923c" : "#f87171";
const statusLabel = (s) => s === 1 ? "Active" : s === 2 ? "Idle" : "Inactive";
return ( return (
<div className="min-h-screen bg-gray-900 text-white p-8"> <div className="page-wrapper">
{/* 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 */} {/* Header */}
<motion.h1 <motion.div className="mb-8" initial={{ opacity: 0, y: -15 }} animate={{ opacity: 1, y: 0 }}>
className="text-3xl font-bold mb-6" <button
initial={{ opacity: 0, y: -20 }} className="flex items-center gap-2 text-sm mb-4 transition-colors"
animate={{ opacity: 1, y: 0 }} style={{ color: "#64748b" }}
> onMouseEnter={e => { e.currentTarget.style.color = "#f1f5f9"; }}
Manage Partners onMouseLeave={e => { e.currentTarget.style.color = "#64748b"; }}
</motion.h1> onClick={() => navigate("/partners")}
>
<FaArrowLeft className="w-3 h-3" /> Back to Partners
</button>
<div className="flex items-center justify-between flex-wrap gap-4">
<div>
<h1 className="page-title"><span className="gradient-text">{formData.name || "Manage Partner"}</span></h1>
<p className="page-subtitle">Edit partner details and manage screens</p>
</div>
{isEnabled ? (
<motion.button
onClick={() => setIsEnabled(false)}
className="btn-secondary flex items-center gap-2"
whileHover={{ scale: 1.03 }}
>
<FaEdit className="w-3.5 h-3.5" /> Edit Details
</motion.button>
) : (
<motion.button
onClick={handleUpdate}
className="btn-primary flex items-center gap-2"
whileHover={{ scale: 1.03 }}
>
<FaSave className="w-3.5 h-3.5" /> Save Changes
</motion.button>
)}
</div>
</motion.div>
<div className="mb-6 h-px" style={{ background: "linear-gradient(90deg, rgba(249,115,22,0.4), transparent)" }} />
{/* Form */} {/* Form */}
<motion.div initial={{ opacity: 0, y: 15 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.1 }}>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> {/* Logo preview */}
{/* Partner Name */} <div className="rounded-2xl p-6 mb-5" style={{ background: "rgba(255,255,255,0.03)", border: "1px solid rgba(255,255,255,0.07)" }}>
<div> <h2 className="label mb-4">Logo</h2>
<label htmlFor="name" className="block text-sm font-medium text-gray-300"> <div className="flex items-start gap-5">
Partner Name <img
</label> src={`https://backend.dine360ads.com/${formData.logo_url || ""}`}
<input alt="Logo"
type="text" className="w-24 h-24 rounded-xl object-cover"
name="name" style={{ border: "1px solid rgba(255,255,255,0.1)" }}
id="name" />
value={formData.name || ""} {!isEnabled && (
onChange={handleChange} <div>
disabled={isEnabled} <label className="label mb-2">Upload New Logo</label>
className="mt-1 block w-full bg-gray-700 p-2 rounded-lg" <input type="file" name="logo_url" accept="image/*" onChange={handleFileChange} className="input-field" style={{ paddingTop: "0.6rem" }} />
required </div>
/> )}
</div>
</div> </div>
{/* Logo */} {/* Basic info */}
<div className="rounded-2xl p-6 mb-5" style={{ background: "rgba(255,255,255,0.03)", border: "1px solid rgba(255,255,255,0.07)" }}>
<h2 className="label mb-4">Basic Information</h2>
{/* Open Time */} <div className="grid grid-cols-1 md:grid-cols-2 gap-5">
<div> <div>
<label htmlFor="open_time" className="block text-sm font-medium text-gray-300"> <label className="label">Partner Name</label>
Open Time <input name="name" type="text" value={formData.name || ""} onChange={handleChange} disabled={isEnabled} className="input-field" />
</label> </div>
<input <div>
type="time" <label className="label">No. of Screens</label>
name="open_time" <input name="screens" type="number" value={formData.screens || ""} onChange={handleChange} disabled={isEnabled} className="input-field" min="1" />
id="open_time" </div>
value={formData.open_time || ""} <div>
onChange={handleChange} <label className="label">Open Time</label>
disabled={isEnabled} <input name="open_time" type="time" value={formData.open_time || ""} onChange={handleChange} disabled={isEnabled} className="input-field" />
className="mt-1 block w-full bg-gray-700 p-2 rounded-lg" </div>
required <div>
/> <label className="label">Close Time</label>
</div> <input name="close_time" type="time" value={formData.close_time || ""} onChange={handleChange} disabled={isEnabled} className="input-field" />
{/* 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://backend.dine360ads.com/${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>
</div> </div>
</div>
{screens.map((screen, index) => ( {/* Location */}
<div className="rounded-2xl p-6 mb-5" style={{ background: "rgba(255,255,255,0.03)", border: "1px solid rgba(255,255,255,0.07)" }}>
<h2 className="label mb-4">Location</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
<div className="md:col-span-2">
<label className="label">Address</label>
<input name="address" type="text" value={formData.address || ""} onChange={handleChange} disabled={isEnabled} className="input-field" />
</div>
<div>
<label className="label">Province</label>
<Select options={provinceOptions} value={selectedProvince} onChange={handleProvinceChange} isDisabled={isEnabled} styles={selectStyles} placeholder="Select Province" />
</div>
<div>
<label className="label">City</label>
<Select options={cityOptions} value={selectedCity} onChange={handleCityChange} isDisabled={isEnabled || !selectedProvince} styles={selectStyles} placeholder="Select City" />
</div>
<div>
<label className="label">Postal Code</label>
<input name="pincode" type="text" value={formData.pincode || ""} onChange={handleChange} disabled={isEnabled} className="input-field" />
</div>
</div>
</div>
<div key={index} className="bg-gray-800 p-4 rounded-lg shadow-md relative"> {/* Content */}
{/* Delete Button - Top Right of Card */} <div className="rounded-2xl p-6 mb-5" style={{ background: "rgba(255,255,255,0.03)", border: "1px solid rgba(255,255,255,0.07)" }}>
<motion.button <h2 className="label mb-4">Content Settings</h2>
type="button" <div className="grid grid-cols-1 md:grid-cols-2 gap-5">
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" <div>
whileHover={{ scale: 1.2, rotate: 15 }} <label className="label">YouTube Links (comma separated)</label>
onClick={() => { <textarea name="yt" rows={3} value={formData.yt || ""} onChange={handleChange} disabled={isEnabled} className="input-field" style={{ resize: "vertical" }} />
if (window.confirm("Are you sure you want to delete this screen?")) { </div>
DeleteScreen(screen.transid); <div>
} <label className="label">Scroll Text</label>
}} <textarea name="scrolltext" rows={3} value={formData.scrolltext || ""} onChange={handleChange} disabled={isEnabled} className="input-field" style={{ resize: "vertical" }} />
title="Delete this screen" </div>
</div>
</div>
</motion.div>
{/* Screens */}
{formData.screens && (
<motion.div initial={{ opacity: 0, y: 15 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.2 }}>
<div className="flex items-center justify-between mb-4">
<h2 className="text-white font-bold text-lg">Screens</h2>
<motion.button
onClick={AddScreen}
className="btn-primary flex items-center gap-2"
style={{ padding: "0.5rem 1rem", fontSize: "0.85rem" }}
whileHover={{ scale: 1.03 }}
>
<FaPlus className="w-3 h-3" /> Add Screen
</motion.button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{screens.map((screen, index) => (
<div
key={screen.transid}
className="rounded-2xl p-5"
style={{ background: "rgba(255,255,255,0.04)", border: "1px solid rgba(255,255,255,0.08)" }}
> >
<FaTrash /> <div className="flex items-start justify-between mb-4">
</motion.button> <div>
<h3 className="text-white font-bold text-sm">
<h3 className="text-lg font-semibold mb-2"> Screen {index + 1}
Screen {index + 1} ({id.substring(0, 2) + 'S' + (index + 1)}) <span className="ml-2 text-xs font-mono px-2 py-0.5 rounded-md" style={{ background: "rgba(249,115,22,0.1)", color: "#fb923c" }}>
</h3> {id.substring(0, 2) + "S" + (index + 1)}
</span>
<div className="flex flex-wrap items-center gap-3 relative"> </h3>
{/* Preview Button */} <div className="flex items-center gap-1.5 mt-1">
<motion.button <FaCircle className="w-2 h-2" style={{ color: statusColor(screen.isactive) }} />
type="button" <span className="text-xs font-medium" style={{ color: statusColor(screen.isactive) }}>{statusLabel(screen.isactive)}</span>
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> </div>
<span className={`ml-4 text-sm font-medium ${screen.isyoutube === 1 ? 'text-amber-300' : 'text-fuchsia-300'}`}> </div>
{screen.isyoutube === 1 ? "YouTube ON" : "YouTube OFF"} <button
</span> className="w-7 h-7 rounded-lg flex items-center justify-center transition-all"
</label> style={{ background: "rgba(239,68,68,0.12)", border: "1px solid rgba(239,68,68,0.18)" }}
</motion.div> onClick={() => { if (window.confirm("Delete this screen?")) DeleteScreen(screen.transid); }}
onMouseEnter={e => { e.currentTarget.style.background = "rgba(239,68,68,0.28)"; }}
onMouseLeave={e => { e.currentTarget.style.background = "rgba(239,68,68,0.12)"; }}
<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> <FaTrash className="w-3 h-3 text-red-400" />
{/* <option value={2}>Screen Type 2 (InHouse)</option> */} </button>
<option value={3}>Screen Type 3 (In-house and Scroll Text)</option> </div>
</select>
</motion.div>
{screen.status && (
<p className="text-xs mb-3" style={{ color: "#475569" }}>
{screen.status}
{screen.stsupdat && (
<span className="ml-2">
{new Date(screen.stsupdat).toLocaleDateString("en-GB")}{" "}
{new Date(screen.stsupdat).toLocaleTimeString()}
</span>
)}
</p>
)}
{/* Status Indicator */} {/* Actions */}
<div className="flex items-center ml-auto"> <div className="space-y-3">
<FaCircle <div className="flex gap-2">
className={`mr-2 ${screen.isactive === 1 <button
? "text-green-400" className="flex-1 text-xs font-semibold py-2 rounded-lg transition-all"
: screen.isactive === 2 style={{ background: "rgba(59,130,246,0.15)", color: "#60a5fa", border: "1px solid rgba(59,130,246,0.2)" }}
? "text-orange-400" onMouseEnter={e => { e.currentTarget.style.background = "rgba(59,130,246,0.25)"; }}
: "text-red-400" onMouseLeave={e => { e.currentTarget.style.background = "rgba(59,130,246,0.15)"; }}
}`} onClick={() => {
/> const prefix = id.substring(0, 5);
<span className="uppercase font-semibold tracking-wide"> const sn = `Screen${index + 1}`;
{screen.isactive === 1 ? "ACTIVE" : screen.isactive === 2 ? "IDLE" : "INACTIVE"} window.open(`${window.location.origin}/ads/${prefix}/${sn}`, "_blank");
</span> }}
>
Preview
</button>
<button
className="flex-1 text-xs font-semibold py-2 rounded-lg transition-all"
style={{ background: "rgba(16,185,129,0.15)", color: "#34d399", border: "1px solid rgba(16,185,129,0.2)" }}
onMouseEnter={e => { e.currentTarget.style.background = "rgba(16,185,129,0.25)"; }}
onMouseLeave={e => { e.currentTarget.style.background = "rgba(16,185,129,0.15)"; }}
onClick={() => {
const prefix = id.substring(0, 5);
const sn = `Screen${index + 1}`;
navigator.clipboard.writeText(`${window.location.origin}/ads/${prefix}/${sn}`);
alert("Link copied!");
}}
>
Copy Link
</button>
</div>
{/* YouTube toggle */}
<div className="flex items-center justify-between">
<span className="text-xs" style={{ color: "#64748b" }}>YouTube</span>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={screen.isyoutube === 1}
onChange={() => handleToggleYouTube(screen.transid, screen.isyoutube !== 1)}
className="sr-only peer"
/>
<div
className="w-10 h-5 rounded-full transition-all duration-300 relative"
style={{ background: screen.isyoutube === 1 ? "rgba(249,115,22,0.6)" : "rgba(255,255,255,0.1)" }}
>
<div
className="absolute top-0.5 w-4 h-4 rounded-full bg-white shadow transition-all duration-300"
style={{ left: screen.isyoutube === 1 ? "calc(100% - 18px)" : "2px" }}
/>
</div>
<span className="ml-2 text-xs font-medium" style={{ color: screen.isyoutube === 1 ? "#fb923c" : "#475569" }}>
{screen.isyoutube === 1 ? "ON" : "OFF"}
</span>
</label>
</div>
{/* Screen type */}
<div>
<label className="label">Screen Type</label>
<select
value={screen.screentype || 1}
onChange={async (e) => {
const newType = Number(e.target.value);
try {
setLoading(true);
await api.post("/admin/update-screen-type", { screenid: screen.transid, sts: newType });
fetchScreens();
} catch (err) { console.error(err); }
finally { setLoading(false); }
}}
className="input-field"
style={{ paddingTop: "0.5rem", paddingBottom: "0.5rem" }}
>
<option value={1}>Type 1 Normal</option>
<option value={3}>Type 3 In-house + Scroll Text</option>
</select>
</div>
</div> </div>
</div> </div>
))}
<br /> </div>
<div> </motion.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> </div>
); );
}; };
export default ManagePartner; export default ManagePartner;

View File

@ -1,99 +1,43 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { FaPlus, FaCircle } from "react-icons/fa";
import { api } from "../API/api"; import { api } from "../API/api";
import { useLoading } from "../Context/LoadingContext"; import { useLoading } from "../Context/LoadingContext";
import PartnersCardComponent from "../Components/PartnersPageCardComponent"; import PartnersCardComponent from "../Components/PartnersPageCardComponent";
const PartnersPage = () => { const PartnersPage = () => {
const [clients, setClients] = useState([]); const [clients, setClients] = useState([]);
const [upd, setUpd] = useState(0);upd const [upd, setUpd] = useState(0);
const navigate = useNavigate(); const { setLoading } = useLoading();
const { setLoading } = useLoading();
useEffect(() => { useEffect(() => {
const fetchClients = async () => { const fetch = async () => {
try { try {
setLoading(true); setLoading(true);
const response = await api.get("/admin/partners"); const response = await api.get("/admin/partners");
setClients(response.map(c => ({ ...c, is_active: c.status !== "Offline" })));
} catch (e) {
console.error(e);
} finally {
setLoading(false);
}
};
fetch();
}, [upd]);
const updatedClients = response.map(client => ({ return (
...client, <div className="page-wrapper">
is_active: client.status === "Offline" ? null : true, <motion.div
})); className="mb-8"
initial={{ opacity: 0, y: -15 }}
setClients(updatedClients); animate={{ opacity: 1, y: 0 }}
>
<h1 className="page-title"><span className="gradient-text">Partners</span></h1>
} catch (error) { <p className="page-subtitle">Manage all restaurant and venue partners</p>
console.error("Error fetching clients:", error); </motion.div>
} finally { <div className="mb-6 h-px" style={{ background: "linear-gradient(90deg, rgba(249,115,22,0.4), transparent)" }} />
setLoading(false); <PartnersCardComponent Data={clients} setUpd={setUpd} />
} </div>
}; );
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 export default PartnersPage;

View File

@ -1,95 +1,89 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { FaArrowLeft } from "react-icons/fa"; import { FaCog, FaSave } from "react-icons/fa";
import { api } from "../API/api"; import { api } from "../API/api";
import { useLoading } from "../Context/LoadingContext"; import { useLoading } from "../Context/LoadingContext";
const SettingsPage = () => { const SettingsPage = () => {
const { setLoading } = useLoading(); const { setLoading } = useLoading();
const navigate = useNavigate();
const [settingsData, setSettingsData] = useState([]); const [settingsData, setSettingsData] = useState([]);
useEffect(() => { useEffect(() => { fetchSettings(); }, []);
fetchSettings();
}, []);
const fetchSettings = async () => { const fetchSettings = async () => {
try { try {
setLoading(true); setLoading(true);
const response = await api.get("/client/get-settings"); const res = await api.get("/client/get-settings");
setSettingsData(response); setSettingsData(res);
} catch (error) { } catch (e) { console.error(e); }
console.error("Error fetching settings:", error);
}
setLoading(false); setLoading(false);
}; };
const handleUpdate = async (transid, newValue) => { const handleUpdate = async (transid, newValue) => {
try { try {
setLoading(true); setLoading(true);
await api.post("/admin/update-settings", { await api.post("/admin/update-settings", { transid, value: newValue });
transid: transid, alert("Setting updated successfully");
value: newValue,
});
alert("General Settings Updated Successfully ")
fetchSettings(); fetchSettings();
} catch (error) { } catch (e) { console.error(e); }
console.error("Error updating setting:", error);
}
setLoading(false); setLoading(false);
}; };
return ( return (
<div className="min-h-screen bg-gray-900 text-white p-8"> <div className="page-wrapper">
<motion.button <motion.div className="mb-8" initial={{ opacity: 0, y: -15 }} animate={{ opacity: 1, y: 0 }}>
className="mb-4 flex items-center text-gray-400 hover:text-white" <div className="flex items-center gap-3">
whileHover={{ scale: 1.1 }} <div
onClick={() => navigate("/admin-dashboard")} className="w-10 h-10 rounded-xl flex items-center justify-center"
> style={{ background: "linear-gradient(135deg, #10b981, #059669)", boxShadow: "0 4px 15px rgba(16,185,129,0.3)" }}
<FaArrowLeft className="mr-2" /> Back >
</motion.button> <FaCog className="w-4 h-4 text-white" />
</div>
<div>
<h1 className="page-title"><span className="gradient-text">General Settings</span></h1>
<p className="page-subtitle">System-wide configuration values</p>
</div>
</div>
</motion.div>
<motion.h1 <div className="mb-6 h-px" style={{ background: "linear-gradient(90deg, rgba(16,185,129,0.4), transparent)" }} />
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 */} <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-5">
{settingsData.map((item) => ( {settingsData.map((item, i) => (
<div key={item.transid} className="item"> <motion.div
<label className="block text-sm font-medium text-gray-700"> key={item.transid}
{item.name} className="rounded-2xl p-5"
</label> style={{ background: "rgba(255,255,255,0.04)", border: "1px solid rgba(255,255,255,0.08)" }}
initial={{ opacity: 0, y: 15 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: i * 0.06 }}
>
<label className="label mb-3">{item.name}</label>
<input <input
type="text" type="text"
value={item.value} value={item.value}
placeholder= {item.name} placeholder={item.name}
onChange={(e) => { onChange={(e) => {
const updatedSettings = settingsData.map((setting) => setSettingsData(settingsData.map((s) =>
setting.transid === item.transid s.transid === item.transid ? { ...s, value: e.target.value } : s
? { ...setting, value: e.target.value } ));
: setting
);
setSettingsData(updatedSettings);
}} }}
className="block w-full bg-gray-700 p-2 rounded-lg mb-2" className="input-field mb-3"
/> />
<motion.button <motion.button
onClick={() => handleUpdate(item.transid, item.value)} onClick={() => handleUpdate(item.transid, item.value)}
className="bg-blue-500 px-4 py-2 rounded-lg" className="btn-primary flex items-center gap-2 w-full justify-center"
whileHover={{ scale: 1.05 }} style={{ padding: "0.55rem 1rem", fontSize: "0.8rem" }}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.97 }}
> >
Update <FaSave className="w-3 h-3" /> Update
</motion.button> </motion.button>
</div> </motion.div>
))} ))}
</div> </div>
</div> </div>
); );
}; };
export default SettingsPage; export default SettingsPage;

View File

@ -1 +1,136 @@
@import "tailwindcss"; @import "tailwindcss";
body {
background: #0c0a1e;
color: #f1f5f9;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.glass-card {
background: rgba(255, 255, 255, 0.04);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.08);
}
.glass-card-hover {
background: rgba(255, 255, 255, 0.04);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.08);
transition: all 0.2s ease;
}
.glass-card-hover:hover {
background: rgba(255, 255, 255, 0.07);
border-color: rgba(249, 115, 22, 0.25);
box-shadow: 0 0 30px rgba(249, 115, 22, 0.08);
}
.gradient-text {
background: linear-gradient(135deg, #f97316, #f59e0b);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.btn-primary {
background: linear-gradient(135deg, #f97316, #f59e0b);
color: white;
border: none;
border-radius: 0.75rem;
padding: 0.65rem 1.5rem;
font-weight: 600;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 4px 20px rgba(249, 115, 22, 0.3);
}
.btn-primary:hover {
transform: translateY(-1px);
box-shadow: 0 6px 25px rgba(249, 115, 22, 0.45);
}
.btn-secondary {
background: rgba(255,255,255,0.06);
color: #f1f5f9;
border: 1px solid rgba(255,255,255,0.1);
border-radius: 0.75rem;
padding: 0.65rem 1.5rem;
font-weight: 600;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-secondary:hover {
background: rgba(255,255,255,0.1);
border-color: rgba(255,255,255,0.2);
}
.btn-danger {
background: rgba(239, 68, 68, 0.15);
color: #f87171;
border: 1px solid rgba(239,68,68,0.2);
border-radius: 0.75rem;
padding: 0.65rem 1.5rem;
font-weight: 600;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-danger:hover {
background: rgba(239,68,68,0.25);
}
.input-field {
width: 100%;
background: rgba(255,255,255,0.05);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 0.75rem;
padding: 0.75rem 1rem;
color: #f1f5f9;
font-size: 0.9rem;
outline: none;
transition: all 0.2s ease;
}
.input-field::placeholder { color: #475569; }
.input-field:focus {
border-color: rgba(249,115,22,0.5);
box-shadow: 0 0 0 3px rgba(249,115,22,0.08);
}
.input-field:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.label {
display: block;
font-size: 0.8rem;
font-weight: 600;
color: #94a3b8;
margin-bottom: 0.4rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.page-wrapper {
min-height: 100vh;
padding: 2rem;
}
.page-title {
font-size: 1.75rem;
font-weight: 800;
color: #f1f5f9;
margin-bottom: 0.25rem;
}
.page-subtitle {
font-size: 0.875rem;
color: #64748b;
}
::-webkit-scrollbar { width: 5px; }
::-webkit-scrollbar-track { background: #0c0a1e; }
::-webkit-scrollbar-thumb { background: rgba(249,115,22,0.25); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: rgba(249,115,22,0.45); }