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:
parent
6e8694ba96
commit
56dd06b0f7
110
src/App.jsx
110
src/App.jsx
@ -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;
|
||||||
|
|||||||
@ -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>
|
|
||||||
</>
|
|
||||||
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
|
||||||
</>
|
|
||||||
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
|
||||||
</>
|
|
||||||
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
142
src/Components/Sidebar.jsx
Normal 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;
|
||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
137
src/index.css
137
src/index.css
@ -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); }
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user