first commit

This commit is contained in:
Manesh 2025-12-26 13:12:37 +00:00
commit aba50b0fae
607 changed files with 67201 additions and 0 deletions

13
.editorconfig Normal file
View File

@ -0,0 +1,13 @@
# editorconfig.org
root = true
[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false

1
.env Normal file
View File

@ -0,0 +1 @@
NEXT_PUBLIC_BACKEND_BASEURL=https://ebay.backend.data4autos.com

3
.eslintrc.json Normal file
View File

@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}

36
.gitignore vendored Normal file
View File

@ -0,0 +1,36 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

5
.prettierrc Normal file
View File

@ -0,0 +1,5 @@
{
"semi": true,
"singleQuote": true,
"printWidth": 200
}

40
App.tsx Normal file
View File

@ -0,0 +1,40 @@
'use client';
import { PropsWithChildren, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { IRootState } from '@/store';
import { toggleRTL, toggleTheme, toggleMenu, toggleLayout, toggleAnimation, toggleNavbar, toggleSemidark } from '@/store/themeConfigSlice';
import Loading from '@/components/layouts/loading';
import { getTranslation } from '@/i18n';
function App({ children }: PropsWithChildren) {
const themeConfig = useSelector((state: IRootState) => state.themeConfig);
const dispatch = useDispatch();
const { initLocale } = getTranslation();
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
dispatch(toggleTheme(localStorage.getItem('theme') || themeConfig.theme));
dispatch(toggleMenu(localStorage.getItem('menu') || themeConfig.menu));
dispatch(toggleLayout(localStorage.getItem('layout') || themeConfig.layout));
dispatch(toggleRTL(localStorage.getItem('rtlClass') || themeConfig.rtlClass));
dispatch(toggleAnimation(localStorage.getItem('animation') || themeConfig.animation));
dispatch(toggleNavbar(localStorage.getItem('navbar') || themeConfig.navbar));
dispatch(toggleSemidark(localStorage.getItem('semidark') || themeConfig.semidark));
// locale
initLocale(themeConfig.locale);
setIsLoading(false);
}, [dispatch, initLocale, themeConfig.theme, themeConfig.menu, themeConfig.layout, themeConfig.rtlClass, themeConfig.animation, themeConfig.navbar, themeConfig.locale, themeConfig.semidark]);
return (
<div
className={`${(themeConfig.sidebar && 'toggle-sidebar') || ''} ${themeConfig.menu} ${themeConfig.layout} ${
themeConfig.rtlClass
} main-section relative font-nunito text-sm font-normal antialiased`}
>
{isLoading ? <Loading /> : children}
</div>
);
}
export default App;

38
README.md Normal file
View File

@ -0,0 +1,38 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

View File

@ -0,0 +1,78 @@
import React from "react";
import IconGoogle from "@/components/icon/icon-google";
import Link from "next/link";
import ComponentsAuthForgotForm from '@/components/auth/components-auth-forgot-form';
import ComponentsAuthUnlockForm from "@/components/auth/components-auth-unlock-form";
export default function PremiumLoginPage() {
return (
<div className="min-h-screen w-full flex items-center justify-center bg-black relative overflow-hidden">
{/* Background abstract neon lines */}
{/* <div
className="absolute inset-0 bg-cover bg-center opacity-60"
style={{
backgroundImage:
"url('https://as2.ftcdn.net/jpg/03/43/34/57/1000_F_343345747_wJfBUpacjstgfxqK6XyH45jXXZTeHx44.jpg')",
}}
></div> */}
{/* 🔥 Background YouTube Video */}
<div className="absolute inset-0 w-full h-full overflow-hidden">
{/* <iframe
className="w-full h-full object-cover"
src="https://www.youtube.com/embed/g6qV2cQ2Fhw?autoplay=1&mute=1&controls=0&showinfo=0&rel=0&loop=1&playlist=g6qV2cQ2Fhw"
allow="autoplay; fullscreen"
allowFullScreen
/> */}
<video
className="w-full h-full object-cover"
autoPlay
loop
muted
playsInline
>
<source src="/assets/images/auth/D4A_ebay_background-1.mp4" type="video/mp4" />
</video>
</div>
{/* Glass card */}
<div className="relative z-10 bg-white/10 backdrop-blur-xl border border-white/20 shadow-2xl rounded-2xl p-10 w-full max-w-md text-white animate-fadeIn">
{/* <h2 className="text-3xl font-semibold text-center mb-8">Login</h2> */}
{/* Login Form Section */}
<div className="w-full max-w-[440px] ">
<div className="mb-10">
<h1 className="text-xl font-extrabold uppercase !leading-snug text-[#19d4fb] md:text-3xl">
CHANGE PASSWORD
</h1>
<p className="text-base font-bold leading-normal text-white">
Enter your password to change your credentials
</p>
</div>
{/* Forgot form */}
<ComponentsAuthUnlockForm />
{/* <div className="text-center mt-4">
<a href="/forgot-password" className="text-sm text-blue-500 hover:underline">
Forgot Password?
</a>
</div> */}
{/* Divider */}
{/* Sign-in link */}
{/* <div className="text-center dark:text-white mt-5">
Remember your password? &nbsp;
<Link
href="/login"
className="uppercase text-[#19d4fb] underline transition hover:text-black dark:hover:text-white"
>
SIGN IN
</Link>
</div> */}
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,77 @@
import React from "react";
import IconGoogle from "@/components/icon/icon-google";
import Link from "next/link";
import ComponentsAuthForgotForm from '@/components/auth/components-auth-forgot-form';
export default function PremiumLoginPage() {
return (
<div className="min-h-screen w-full flex items-center justify-center bg-black relative overflow-hidden">
{/* Background abstract neon lines */}
{/* <div
className="absolute inset-0 bg-cover bg-center opacity-60"
style={{
backgroundImage:
"url('https://as2.ftcdn.net/jpg/03/43/34/57/1000_F_343345747_wJfBUpacjstgfxqK6XyH45jXXZTeHx44.jpg')",
}}
></div> */}
{/* 🔥 Background YouTube Video */}
<div className="absolute inset-0 w-full h-full overflow-hidden">
{/* <iframe
className="w-full h-full object-cover"
src="https://www.youtube.com/embed/g6qV2cQ2Fhw?autoplay=1&mute=1&controls=0&showinfo=0&rel=0&loop=1&playlist=g6qV2cQ2Fhw"
allow="autoplay; fullscreen"
allowFullScreen
/> */}
<video
className="w-full h-full object-cover"
autoPlay
loop
muted
playsInline
>
<source src="/assets/images/auth/D4A_ebay_background-1.mp4" type="video/mp4" />
</video>
</div>
{/* Glass card */}
<div className="relative z-10 bg-white/10 backdrop-blur-xl border border-white/20 shadow-2xl rounded-2xl p-10 w-full max-w-md text-white animate-fadeIn">
{/* <h2 className="text-3xl font-semibold text-center mb-8">Login</h2> */}
{/* Login Form Section */}
<div className="w-full max-w-[440px] ">
<div className="mb-10">
<h1 className="text-xl font-extrabold uppercase !leading-snug text-[#19d4fb] md:text-3xl">
Forgot Password
</h1>
<p className="text-base font-bold leading-normal text-white">
Enter your email to receive the reset link
</p>
</div>
{/* Forgot form */}
<ComponentsAuthForgotForm />
{/* <div className="text-center mt-4">
<a href="/forgot-password" className="text-sm text-blue-500 hover:underline">
Forgot Password?
</a>
</div> */}
{/* Divider */}
{/* Sign-in link */}
<div className="text-center dark:text-white mt-5">
Remember your password? &nbsp;
<Link
href="/login"
className="uppercase text-[#19d4fb] underline transition hover:text-black dark:hover:text-white"
>
SIGN IN
</Link>
</div>
</div>
</div>
</div>
);
}

7
app/(auth)/layout.tsx Normal file
View File

@ -0,0 +1,7 @@
import React from 'react';
const AuthLayout = ({ children }: { children: React.ReactNode }) => {
return <div className="min-h-screen text-black dark:text-white-dark">{children} </div>;
};
export default AuthLayout;

104
app/(auth)/login/page.tsx Normal file
View File

@ -0,0 +1,104 @@
import React from "react";
import ComponentsAuthLoginForm from '@/components/auth/components-auth-login-form';
import IconGoogle from "@/components/icon/icon-google";
import Link from "next/link";
export default function PremiumLoginPage() {
return (
<div className="min-h-screen w-full flex items-center justify-center bg-black relative overflow-hidden">
{/* Background abstract neon lines */}
{/* <div
className="absolute inset-0 bg-cover bg-center opacity-60"
style={{
backgroundImage:
"url('https://as2.ftcdn.net/jpg/03/43/34/57/1000_F_343345747_wJfBUpacjstgfxqK6XyH45jXXZTeHx44.jpg')",
}}
/> */}
{/* 🔥 Background YouTube Video */}
<div className="absolute inset-0 w-full h-full overflow-hidden">
{/* <iframe
className="w-full h-full object-cover"
src="https://www.youtube.com/embed/g6qV2cQ2Fhw?autoplay=1&mute=1&controls=0&showinfo=0&rel=0&loop=1&playlist=g6qV2cQ2Fhw"
allow="autoplay; fullscreen"
allowFullScreen
/> */}
<video
className="w-full h-full object-cover"
autoPlay
loop
muted
playsInline
>
<source src="/assets/images/auth/D4A_ebay_background-1.mp4" type="video/mp4" />
</video>
</div>
{/* Glass card */}
<div className="relative z-10 bg-white/10 backdrop-blur-xl border border-white/20 shadow-2xl rounded-2xl p-10 w-full max-w-md text-white animate-fadeIn">
{/* <h2 className="text-3xl font-semibold text-center mb-8">Login</h2> */}
{/* Login Form Section */}
<div className="w-full max-w-[440px] ">
<div className="mb-10">
<h1 className="text-3xl font-extrabold uppercase !leading-snug text-[#19d4fb] md:text-4xl">
Sign In
</h1>
<p className="text-base font-bold leading-normal text-white">
Enter your email and password to login
</p>
</div>
{/* Login form */}
<ComponentsAuthLoginForm />
{/* <div className="text-center mt-4">
<a href="/forgot-password" className="text-sm text-blue-500 hover:underline">
Forgot Password?
</a>
</div> */}
{/* Divider */}
<div className="relative my-7">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t border-white/20"></span>
</div>
<div className="relative flex justify-center text-sm">
<span className="px-3 bg-black/40 backdrop-blur-md text-white/80">
OR
</span>
</div>
</div>
{/* Social icons */}
<div className="mb-10 md:mb-[20px]">
<ul className="flex justify-center gap-3.5 text-white">
<li>
<Link
href="https://ebay.backend.data4autos.com/api/auth/google/"
className="inline-flex h-10 w-10 items-center justify-center rounded-full p-0 transition hover:scale-110"
style={{ background: 'linear-gradient(135deg, #0EA5E9 0%, #19D4FB 50%, #67E8F9 100%)' }}
>
<IconGoogle />
</Link>
</li>
</ul>
</div>
{/* Sign-up link */}
<div className="text-center dark:text-white">
Don&apos;t have an account?&nbsp;
<Link
href="/signup"
className="uppercase text-[#19d4fb] underline transition hover:text-black dark:hover:text-white"
>
SIGN UP
</Link>
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,79 @@
import React from "react";
import IconGoogle from "@/components/icon/icon-google";
import Link from "next/link";
import ComponentsAuthForgotForm from '@/components/auth/components-auth-forgot-form';
import ComponentsAuthUnlockForm from "@/components/auth/components-auth-unlock-form";
import ResetPasswordForm from "@/components/auth/components-auth-reset-password-form";
export default function PremiumLoginPage() {
return (
<div className="min-h-screen w-full flex items-center justify-center bg-black relative overflow-hidden">
{/* Background abstract neon lines */}
{/* <div
className="absolute inset-0 bg-cover bg-center opacity-60"
style={{
backgroundImage:
"url('https://as2.ftcdn.net/jpg/03/43/34/57/1000_F_343345747_wJfBUpacjstgfxqK6XyH45jXXZTeHx44.jpg')",
}}
></div> */}
{/* 🔥 Background YouTube Video */}
<div className="absolute inset-0 w-full h-full overflow-hidden">
{/* <iframe
className="w-full h-full object-cover"
src="https://www.youtube.com/embed/g6qV2cQ2Fhw?autoplay=1&mute=1&controls=0&showinfo=0&rel=0&loop=1&playlist=g6qV2cQ2Fhw"
allow="autoplay; fullscreen"
allowFullScreen
/> */}
<video
className="w-full h-full object-cover"
autoPlay
loop
muted
playsInline
>
<source src="/assets/images/auth/D4A_ebay_background-1.mp4" type="video/mp4" />
</video>
</div>
{/* Glass card */}
<div className="relative z-10 bg-white/10 backdrop-blur-xl border border-white/20 shadow-2xl rounded-2xl p-10 w-full max-w-md text-white animate-fadeIn">
{/* <h2 className="text-3xl font-semibold text-center mb-8">Login</h2> */}
{/* Login Form Section */}
<div className="w-full max-w-[440px] ">
<div className="mb-10">
<h1 className="text-xl font-extrabold uppercase !leading-snug text-[#19d4fb] md:text-3xl">
Reset Password
</h1>
<p className="text-base font-bold leading-normal text-white">
Enter your email to recover your ID
</p>
</div>
{/* Forgot form */}
<ResetPasswordForm />
{/* <div className="text-center mt-4">
<a href="/forgot-password" className="text-sm text-blue-500 hover:underline">
Forgot Password?
</a>
</div> */}
{/* Divider */}
{/* Sign-in link */}
{/* <div className="text-center dark:text-white mt-5">
Remember your password? &nbsp;
<Link
href="/login"
className="uppercase text-[#19d4fb] underline transition hover:text-black dark:hover:text-white"
>
SIGN IN
</Link>
</div> */}
</div>
</div>
</div>
);
}

130
app/(auth)/signup/page.tsx Normal file
View File

@ -0,0 +1,130 @@
import React from "react";
import IconGoogle from "@/components/icon/icon-google";
import Link from "next/link";
import ComponentsAuthRegisterForm from "@/components/auth/components-auth-register-form";
export default function PremiumLoginPage() {
return (
<div className="min-h-screen w-full flex items-center justify-center bg-black relative overflow-hidden">
{/* Background abstract neon lines */}
{/* <div
className="absolute inset-0 bg-cover bg-center opacity-60"
style={{
backgroundImage:
"url('https://as2.ftcdn.net/jpg/03/43/34/57/1000_F_343345747_wJfBUpacjstgfxqK6XyH45jXXZTeHx44.jpg')",
}}
></div> */}
{/* 🔥 Background YouTube Video */}
<div className="absolute inset-0 w-full h-full overflow-hidden">
{/* <iframe
className="w-full h-full object-cover"
src="https://www.youtube.com/embed/g6qV2cQ2Fhw?autoplay=1&mute=1&controls=0&showinfo=0&rel=0&loop=1&playlist=g6qV2cQ2Fhw"
allow="autoplay; fullscreen"
allowFullScreen
/> */}
<video
className="w-full h-full object-cover"
autoPlay
loop
muted
playsInline
>
<source src="/assets/images/auth/D4A_ebay_background-1.mp4" type="video/mp4" />
</video>
</div>
{/* Glass card */}
<div className="relative z-10 bg-white/10 backdrop-blur-xl border border-white/20 shadow-2xl rounded-2xl p-10 w-full max-w-lg text-white animate-fadeIn">
{/* <h2 className="text-3xl font-semibold text-center mb-8">Login</h2> */}
{/* Login Form Section */}
<div className="w-full max-w-[440px] ">
<div className="mb-10">
<h1 className="text-3xl font-extrabold uppercase !leading-snug text-[#19d4fb] md:text-4xl">
Sign Up
</h1>
<p className="text-base font-bold leading-normal text-white">
Enter your email and password to register
</p>
</div>
{/* Login form */}
<ComponentsAuthRegisterForm />
{/* <div className="text-center mt-4">
<a href="/forgot-password" className="text-sm text-blue-500 hover:underline">
Forgot Password?
</a>
</div> */}
{/* Divider */}
<div className="relative my-7">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t border-white/20"></span>
</div>
<div className="relative flex justify-center text-sm">
<span className="px-3 bg-black/40 backdrop-blur-md text-white/80">
OR
</span>
</div>
</div>
{/* Social icons */}
<div className="mb-10 md:mb-[20px]">
<ul className="flex justify-center gap-3.5 text-white">
{/* <li>
<Link
href="#"
className="inline-flex h-10 w-10 items-center justify-center rounded-full p-0 transition hover:scale-110"
style={{
background: 'linear-gradient(135deg, #0EA5E9 0%, #19D4FB 50%, #67E8F9 100%)'
}}
>
<IconInstagram />
</Link>
</li>
<li>
<Link
href="https://ebay.backend.data4autos.com/api/auth/facebook/"
className="inline-flex h-10 w-10 items-center justify-center rounded-full p-0 transition hover:scale-110"
style={{ background: 'linear-gradient(135deg, #0EA5E9 0%, #19D4FB 50%, #67E8F9 100%)' }}
>
<IconFacebookCircle />
</Link>
</li> */}
{/* <li>
<Link
href="#"
className="inline-flex h-8 w-8 items-center justify-center rounded-full p-0 transition hover:scale-110"
style={{ background: 'linear-gradient(135deg, #1E3A8A 0%, #2563EB 50%, #00C6FF 100%)' }}
>
<IconTwitter fill={true} />
</Link>
</li> */}
<li>
<Link
href="https://ebay.backend.data4autos.com/api/auth/google/"
className="inline-flex h-10 w-10 items-center justify-center rounded-full p-0 transition hover:scale-110"
style={{ background: 'linear-gradient(135deg, #0EA5E9 0%, #19D4FB 50%, #67E8F9 100%)' }}
>
<IconGoogle />
</Link>
</li>
</ul>
</div>
{/* Sign-up link */}
<div className="text-center dark:text-white">
Already have an account?&nbsp;
<Link
href="/login"
className="uppercase text-[#19d4fb] underline transition hover:text-black dark:hover:text-white"
>
SIGN IN
</Link>
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,337 @@
"use client";
import { formatCreatedAtWithEnd, showMessage } from "@/utils/commonFunction.utils";
import axios from "axios";
import { useRouter } from "next/navigation";
import React, { useEffect, useState } from "react";
import EditProfileModal from "@/components/account/EditProfileModal";
export default function AccountPage() {
const router = useRouter();
const [userId, setUserId] = useState<string | null>(null);
const [user, setUser] = useState<any>(null);
const [payment, setPayment] = useState<any>(null);
const [loading, setLoading] = useState(false);
const [editModalOpen, setEditModalOpen] = useState(false);
const [role, setRole] = useState()
const [userDetails, setUserDetails] = useState<any>({});
const defaultParams = {
userid: user?.userid || userId,
name: user?.name || "",
email: user?.email || "",
phonenumber: user?.phonenumber || "",
// ✅ Role is taken from login payload (localStorage), not editable
// role: storedRole || user?.role || "",
// companyname: user?.companyname || "",
};
const [params, setParams] = useState<any>({ ...defaultParams });
const fetchUsers = async () => {
try {
const uid = localStorage.getItem("data4auto_uid");
const res: any = await axios.get(`https://ebay.backend.data4autos.com/api/motorstate/auth/users/${uid}`);
setUserDetails(res.data?.user || []);
console.log("user", res.data?.user)
} catch (err) {
console.error(err);
showMessage('Failed to load users', 'error');
}
};
useEffect(() => {
fetchUsers();
}, []);
// ✅ Update params when userDetails are loaded
useEffect(() => {
if (userDetails && Object.keys(userDetails).length > 0) {
setParams({
userid: userDetails.userid || "",
name: userDetails.name || "",
email: userDetails.email || "",
phonenumber: userDetails.phonenumber || "",
});
}
}, [userDetails]);
// ✅ Fetch payment details
useEffect(() => {
const Role: any = localStorage.getItem("user_role");
setRole(Role)
const sessionId = localStorage.getItem("payment_session");
if (Role === "admin" || Role === "partner") return;
if (!sessionId) {
router.push("/pricing");
return;
}
const fetchPaymentDetails = async () => {
try {
const res: any = await axios.get(
"https://ebay.backend.data4autos.com/api/payment/details",
{ params: { session_id: sessionId } }
);
setPayment(res.data.payment);
} catch (err) {
console.error("Error fetching payment details:", err);
}
};
fetchPaymentDetails();
}, [router]);
// ✅ Check authentication
useEffect(() => {
const uid = localStorage.getItem("data4auto_uid");
const email = localStorage.getItem("d4a_email");
if (uid && email) {
setUserId(uid);
setUser(email);
} else {
axios
.get("https://ebay.backend.data4autos.com/api/auth/protected", {
withCredentials: true,
})
.then((res: any) => {
const userData = res.data.user;
if (userData) {
setUser(userData.email);
setUserId(userData.userid);
localStorage.setItem("data4auto_uid", userData.userid);
localStorage.setItem("d4a_email", userData.email);
}
})
.catch(() => {
router.push("/login");
});
}
}, [router]);
const handleInputChange = (e: any) => {
const { id, value } = e.target;
// ✅ Skip updating "role" manually
// if (id === "role") return;
setParams({ ...params, [id]: value });
};
const saveProfile = async () => {
console.log("Saving profile...", params); // debug log
if (!params.name || !params.email || !params.phonenumber) {
showMessage("Please fill all required fields", "error");
return;
}
try {
// ✅ Always use the role from localStorage (payload), not user-edited
const finalParams = {
...params,
role: role || "customer",
};
if (finalParams.userid) {
console.log("PUT request →", `http://localhost:3003/api/auth/users/${finalParams.userid}`);
await axios.put(`http://localhost:3003/api/auth/users/${finalParams.userid}`, finalParams);
fetchUsers();
showMessage("User updated successfully");
setTimeout(() => setEditModalOpen(false), 800);
} else {
console.warn("Missing userid — skipping PUT");
}
setEditModalOpen(false);
} catch (err: any) {
console.error("Error saving user:", err);
showMessage("Error saving user", "error");
}
};
const handleViewInvoice = () => router.push("/invoice/preview");
const handleCancel = async () => {
if (!confirm("Are you sure you want to cancel your subscription?")) return;
const subscriptionId = 1;
setLoading(true);
try {
const res = await axios.post("https://ebay.backend.data4autos.com/api/payment/cancel", {
subscriptionId,
});
alert("Subscription cancelled successfully!");
console.log(res.data);
} catch (err) {
alert("Failed to cancel subscription.");
console.error(err);
} finally {
setLoading(false);
}
};
return (
<div className="min-h-[83vh] bg-gradient-to-br from-[#00d1ff]/10 via-white to-[#00d1ff]/20 py-10 px-4">
<div className="max-w-6xl mx-auto space-y-8">
{/* Subscription / Billing / Profile cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{/* Subscription Card */}
<div className="bg-white border border-gray-200 rounded-xl shadow-sm p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4">Subscription</h2>
<div className="space-y-3 text-sm">
<div className="flex justify-between">
<span className="text-gray-600">Subscription</span>
<span className="font-medium text-gray-900">{payment?.plan || "N/A"}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Period</span>
<span className="font-medium text-gray-900">
{formatCreatedAtWithEnd(payment?.createdAt, payment?.plan)}
</span>
</div>
</div>
<div className="flex gap-3 mt-6">
<button
onClick={handleCancel}
disabled={loading}
className="flex-1 border border-red-400 text-red-500 font-medium py-2 rounded-lg hover:bg-red-50 transition disabled:opacity-50"
>
{loading ? "Cancelling..." : "Cancel Subscription"}
</button>
</div>
</div>
{/* Billing Card */}
<div className="bg-white border border-gray-200 rounded-xl shadow-sm p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4">Billing</h2>
<div className="space-y-3 text-sm">
<div className="flex justify-between">
<span className="text-gray-600">Card</span>
<span className="font-medium text-gray-900">
1325{" "}
<span className="text-blue-600 font-semibold">VISA</span>
</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Expires</span>
<span className="font-medium text-gray-900">3 / 2028</span>
</div>
</div>
<div className="flex gap-3 mt-6">
<button className="flex-1 bg-[#00d1ff] text-white font-medium py-2 rounded-lg hover:bg-[#00b8e6] transition">
Update Billing Details
</button>
<button className="flex-1 border border-gray-300 text-gray-700 font-medium py-2 rounded-lg hover:bg-gray-100 transition">
Add Coupon
</button>
</div>
</div>
{/* Profile Card */}
<div className="bg-white border border-gray-200 rounded-xl shadow-sm p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4">Profile</h2>
<div className="space-y-3 text-sm">
<div className="flex justify-between">
<span className="text-gray-600">Full Name</span>
<span className="font-medium text-gray-900">{userDetails?.name}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Email</span>
<span className="font-medium text-gray-900">{userDetails?.email || "N/A"}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Phone</span>
<span className="font-medium text-gray-900">{userDetails?.phonenumber}</span>
</div>
</div>
<div className="flex gap-3 mt-6">
<button
className="flex-1 bg-[#00d1ff] text-white font-medium py-2 rounded-lg hover:bg-[#00b8e6] transition"
onClick={() => {
setParams((prev: any) => ({
...prev,
userid: userId || localStorage.getItem("data4auto_uid"),
email: user || localStorage.getItem("d4a_email"),
}));
setEditModalOpen(true);
}}
>
Update Details
</button>
<button
className="flex-1 border border-gray-300 text-gray-700 font-medium py-2 rounded-lg hover:bg-gray-100 transition"
onClick={() => router.push("/change-password")}
>
Change Password
</button>
</div>
</div>
</div>
{/* Billing History Table */}
<div className="bg-white border border-gray-200 rounded-xl shadow-sm p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4">Billing History</h2>
<div className="overflow-x-auto">
<table className="w-full text-sm text-left border-t border-gray-100">
<thead>
<tr className="text-gray-600 bg-gray-50">
<th className="py-3 px-4 font-medium">INVOICE NUMBER</th>
<th className="py-3 px-4 font-medium">DATE</th>
<th className="py-3 px-4 font-medium">STATUS</th>
<th className="py-3 px-4 font-medium">AMOUNT</th>
<th className="py-3 px-4 font-medium">PDF INVOICE</th>
</tr>
</thead>
<tbody>
{payment && (
<tr className="border-t">
<td className="py-3 px-4">{payment.invoice_number || "INV-0001"}</td>
<td className="py-3 px-4">
{formatCreatedAtWithEnd(payment?.createdAt).split(" - ")[0]}
</td>
<td className="py-3 px-4">
<span className="px-2 py-1 text-xs rounded-full bg-green-100 text-green-600 font-medium">
Paid
</span>
</td>
<td className="py-3 px-4 text-gray-800 font-medium">
${payment.amount || "59.00"}
</td>
<td className="py-3 px-4">
<button
onClick={handleViewInvoice}
className="bg-gray-100 hover:bg-[#00d1ff] hover:text-white text-gray-700 text-sm font-medium py-1.5 px-4 rounded-md transition"
>
View
</button>
</td>
</tr>
)}
</tbody>
</table>
</div>
<div className="text-gray-500 text-sm mt-4">
Showing {payment ? 1 : 0} of {payment ? 1 : 0} entries
</div>
</div>
</div>
<EditProfileModal
isOpen={editModalOpen}
onClose={() => setEditModalOpen(false)}
params={params}
onChange={handleInputChange}
onSave={saveProfile}
/>
</div>
);
}

View File

@ -0,0 +1,30 @@
import ComponentsUsersAccountSettingsTabs from '@/components/users/account-settings/components-users-account-settings-tabs';
import { SubscriptionProvider } from '@/components/billing/subscription-context';
import { Metadata } from 'next';
import Link from 'next/link';
import React from 'react';
export const metadata: Metadata = {
title: 'Account Setting',
};
export default function UserAccountSettings() {
return (
<SubscriptionProvider>
<div>
<ul className="flex space-x-2 rtl:space-x-reverse">
<li>
<Link href="#" className="text-primary hover:underline">
Users
</Link>
</li>
<li className="before:content-['/'] ltr:before:mr-2 rtl:before:ml-2">
<span>Account Settings</span>
</li>
</ul>
<ComponentsUsersAccountSettingsTabs />
</div>
</SubscriptionProvider>
);
}

View File

@ -0,0 +1,11 @@
// NO "use client" here — this stays a server component
import type { Metadata } from "next";
import AuthCompleteClient from "./ui/AuthCompleteClient";
export const metadata: Metadata = {
title: "Sales Admin",
};
export default function Page() {
return <AuthCompleteClient />;
}

View File

@ -0,0 +1,201 @@
"use client";
import { getAccessToken_client } from "@/utils/apiHelper_client";
import React, { useEffect, useMemo, useState } from "react";
type Store = {
description?: string;
lastOpenedTime?: string;
logo?: { url?: string };
name?: string;
url?: string;
urlPath?: string;
};
type Payload = {
userid?: string;
message?: string;
store?: Store;
storeId?: string;
};
function tryParseData(raw: string | null): Payload | null {
if (!raw) return null;
try {
return JSON.parse(decodeURIComponent(raw));
} catch {
return null;
}
}
function formatDate(iso?: string) {
if (!iso) return "—";
const d = new Date(iso);
return isNaN(d.getTime()) ? "—" : d.toLocaleString();
}
const read = async (r: Response) =>
r.headers.get("content-type")?.includes("application/json")
? r.json()
: r.text();
export default function AuthCompleteClient() {
const [payload, setPayload] = useState<Payload | null>(null);
useEffect(() => {
const url = new URL(window.location.href);
const hash = window.location.hash;
const hashVal = hash.startsWith("#data=") ? hash.slice(6) : null;
const hashPayload = tryParseData(hashVal);
const qpPayload = tryParseData(url.searchParams.get("data"));
const storeIdOnly = url.searchParams.get("storeId");
setPayload(
hashPayload || qpPayload || (storeIdOnly ? { storeId: storeIdOnly } : null)
);
}, []);
const { title, subtitle, logoUrl, desc, link, lastOpen } = useMemo(() => {
const store = payload?.store ?? {};
const id = payload?.storeId || store.urlPath || "—";
return {
title:
payload?.message ||
(store.name
? `eBay connected! Tokens saved for Store ${store.urlPath}.`
: `eBay connected! Store ${id}`),
subtitle: store.name || id,
logoUrl: store.logo?.url,
desc: store.description,
link: store.url,
lastOpen: formatDate(store.lastOpenedTime),
};
}, [payload]);
useEffect(() => {
if (payload) {
(async () => {
const accessToken = await getAccessToken_client();
payload.userid = sessionStorage.getItem("USERID") || undefined;
if (!payload.userid) return;
const saveRes = await fetch(
"https://ebay.backend.data4autos.com/api/motorstate/auth/ebay/store/save",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
}
);
const saveData = await read(saveRes);
if (!saveRes.ok) {
console.error("Save failed:", saveData);
}
})();
}
}, [payload]);
return (
<div className="min-h-[83vh] flex flex-col items-center justify-center bg-gradient-to-br from-[#00d1ff]/10 via-white to-[#00d1ff]/20 text-gray-800 px-6 py-12">
{/* 🎉 Confetti Animation */}
<Confetti />
{/* Card */}
<div className="w-full max-w-2xl bg-white shadow-xl border border-[#00d1ff]/30 rounded-3xl p-8 text-center relative">
<div className="absolute top-0 left-0 w-full h-2 bg-[#00d1ff]" />
<div className="inline-flex items-center gap-2 bg-[#00d1ff]/10 border border-[#00d1ff]/30 text-[#00d1ff] px-4 py-1 rounded-full mb-6 font-medium">
Connected
</div>
<h1 className="text-2xl md:text-3xl font-bold text-[#00d1ff] mb-4">
{title || "eBay connected successfully!"} 🎉
</h1>
<div className="flex items-center justify-center gap-4 mb-6">
{logoUrl ? (
<img
src={logoUrl}
alt={subtitle || "Store Logo"}
className="w-20 h-20 rounded-xl border border-[#00d1ff]/30 object-cover"
/>
) : (
<div className="w-20 h-20 rounded-xl bg-[#00d1ff]/10 flex items-center justify-center text-2xl font-bold text-[#00d1ff] border border-[#00d1ff]/20">
{subtitle?.[0] || "S"}
</div>
)}
<div className="text-left">
<div className="text-lg font-semibold text-gray-700">{subtitle}</div>
<div className="text-sm text-gray-500">
Last opened: <strong>{lastOpen}</strong>
</div>
</div>
</div>
{desc && <p className="text-gray-600 mb-6">{desc}</p>}
<div className="flex flex-wrap justify-center gap-3">
{link && (
<a
href={link}
target="_blank"
rel="noopener noreferrer"
className="bg-[#00d1ff] hover:bg-[#00b0e6] text-white px-5 py-2 rounded-lg font-medium transition"
>
Visit eBay Store
</a>
)}
<a
href="/"
className="border border-[#00d1ff] text-[#00d1ff] hover:bg-[#00d1ff]/10 px-5 py-2 rounded-lg font-medium transition"
>
Go to Dashboard
</a>
</div>
{!payload && (
<p className="text-gray-500 text-sm mt-6">
Waiting for data redirect with <code>?data=...</code>
</p>
)}
</div>
</div>
);
}
/* ===== Tiny Confetti ===== */
const Confetti = () => {
const pieces = Array.from({ length: 24 });
return (
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<style>{`
@keyframes drop {
0% { transform: translateY(-10vh) rotate(0deg); opacity: 0; }
10% { opacity: 1; }
100% { transform: translateY(110vh) rotate(720deg); opacity: 0; }
}
`}</style>
{pieces.map((_, i) => {
const left = Math.random() * 100;
const size = 6 + Math.random() * 8;
const duration = 2.8 + Math.random() * 1.6;
const delay = Math.random() * 0.8;
const color = ["#00d1ff", "#ff69b4", "#16a34a"][Math.floor(Math.random() * 3)];
return (
<span
key={i}
className="absolute top-[-10vh] rounded-full"
style={{
left: `${left}vw`,
width: size,
height: size,
backgroundColor: color,
animation: `drop ${duration}s ease-in ${delay}s both`,
}}
/>
);
})}
</div>
);
};

View File

@ -0,0 +1,392 @@
'use client';
import React, { useEffect, useMemo, useState } from 'react';
import { getAccessToken_client } from '@/utils/apiHelper_client';
/** ===== Types from your API ===== */
type StoreFromAPI = {
userid: string;
store_name: string;
store_description: string;
store_url: string;
store_url_path: string;
store_last_opened_time: string | null;
store_last_opened_time_raw: string | null;
store_logo_url: string;
};
type ApiOk = { code: 'STORE_PRESENT'; message: string; store: StoreFromAPI };
type ApiMiss = { code: 'USER_NOT_FOUND_OR_NO_STORE'; message: string };
type ApiResp = ApiOk | ApiMiss | any;
/** ===== Helpers ===== */
const read = async (r: Response) =>
r.headers.get('content-type')?.includes('application/json') ? r.json() : r.text();
function safeDateFormat(input?: string | null) {
if (!input) return '—';
const d = new Date(input);
return isNaN(d.getTime()) ? '—' : d.toLocaleString();
}
/** ===== Page ===== */
export default function EbayAuthPage() {
const [status, setStatus] = useState<'loading' | 'connected' | 'disconnected'>('loading');
const [store, setStore] = useState<StoreFromAPI | null>(null);
const [toast, setToast] = useState<string>('');
const [connecting, setConnecting] = useState(false);
// Build a return URL back to THIS page (works no matter which route you use)
const returnUrl = useMemo(() => {
if (typeof window === 'undefined') return '';
return `${window.location.origin}${window.location.pathname}`;
}, []);
// On load: check store status with only the userid
useEffect(() => {
(async () => {
try {
// Optional: keep your access token call if you need it for other things
await getAccessToken_client().catch(() => null);
const userid = sessionStorage.getItem('USERID') || undefined;
if (!userid) {
setStatus('disconnected');
setToast('No user session found. Please sign in and try again.');
return;
}
const res = await fetch(
'https://ebay.backend.data4autos.com/api/auth/ebay/store/checkstorestatus',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userid }),
cache: 'no-store',
}
);
const data: ApiResp = await read(res);
if (!res.ok) {
console.error('checkstorestatus failed:', res.status, data);
setStatus('disconnected');
return;
}
if (data?.code === 'STORE_PRESENT' && data?.store) {
setStore(data.store);
setStatus('connected');
setToast('eBay connected successfully');
} else {
setStatus('disconnected');
}
} catch (err) {
console.error('checkstorestatus error:', err);
setStatus('disconnected');
}
})();
}, []);
/** Start OAuth if disconnected */
const OAUTH_ENDPOINT = 'https://ebay.backend.data4autos.com/api/ebay/oauth/login';
const startOauth = () => {
try {
setConnecting(true);
const url = `${OAUTH_ENDPOINT}?return_url=${encodeURIComponent(returnUrl)}`;
window.location.href = url;
} catch {
setConnecting(false);
setToast('Failed to start eBay connection. Please try again.');
}
};
/** Derive UI fields from API store */
const { title, subtitle, logoUrl, desc, link, lastOpen } = useMemo(() => {
if (!store) {
return {
title: '',
subtitle: '',
logoUrl: '',
desc: '',
link: '',
lastOpen: '—',
};
}
return {
title: `eBay connected! Store ${store.store_url_path}`,
subtitle: store.store_name || store.store_url_path,
logoUrl: store.store_logo_url,
desc: store.store_description,
link: store.store_url,
lastOpen: safeDateFormat(store.store_last_opened_time || store.store_last_opened_time_raw),
};
}, [store]);
return (
<div style={styles.page}>
{status === 'connected' && <Confetti />}
<div style={styles.cardWrap}>
{toast && <div style={styles.toast}>{toast}</div>}
{status === 'loading' && (
<>
<div style={styles.badge}>
<span style={styles.badgeEmoji}></span>
<span style={styles.badgeText}>Checking your store status</span>
</div>
<h1 style={styles.title}>Verifying eBay connection</h1>
</>
)}
{status === 'connected' && (
<>
<div style={styles.badge}>
<span style={styles.badgeEmoji}></span>
<span style={styles.badgeText}>Connected</span>
</div>
<h1 style={styles.title}>
{title || 'eBay connected successfully!'} <span>🎉</span>
</h1>
<div style={styles.card}>
<div style={styles.headerRow}>
{logoUrl ? (
<img src={logoUrl} alt={subtitle || 'Store Logo'} style={styles.logo} />
) : (
<div style={styles.logoFallback}>{(subtitle || 'S').slice(0, 1)}</div>
)}
<div style={{ minWidth: 0 }}>
<div style={styles.storeName} title={subtitle}>
{subtitle}
</div>
<div style={styles.storeMeta}>
Last opened: <strong>{lastOpen}</strong>
</div>
</div>
</div>
{desc ? <p style={styles.desc}>{desc}</p> : null}
<div style={styles.actions}>
{link ? (
<a href={link} target="_blank" rel="noopener noreferrer" style={styles.primaryBtn}>
Visit eBay Store
</a>
) : null}
<a href="/dashboard" style={styles.secondaryBtn}>
Go to Dashboard
</a>
</div>
</div>
</>
)}
{status === 'disconnected' && (
<div style={styles.connectWrap}>
<h1 style={styles.title}>eBay Settings</h1>
<p style={styles.subtleText}>
Connect your eBay store to enable product sync, inventory updates, and order flow.
</p>
<button
onClick={startOauth}
disabled={connecting}
style={{
...styles.buttonBase,
...(connecting ? styles.btnDisabled : styles.btnPrimary),
}}
>
{connecting ? 'Redirecting to eBay…' : 'Connect your eBay store'}
</button>
<p style={styles.hint}>
Youll be redirected to eBay to authorize access, then returned here automatically.
</p>
</div>
)}
</div>
</div>
);
}
/** ===== Styles (unchanged from your design) ===== */
const styles: Record<string, React.CSSProperties> = {
page: {
minHeight: '100svh',
background:
'radial-gradient(1200px 600px at 10% -10%, rgba(99,102,241,0.25), transparent 50%), radial-gradient(1000px 500px at 110% 10%, rgba(16,185,129,0.25), transparent 50%), linear-gradient(180deg, #0b1020 0%, #0b0f1a 100%)',
padding: '48px 16px',
color: 'white',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
},
cardWrap: { width: 'min(920px, 92vw)', textAlign: 'center' },
toast: {
marginBottom: 12,
padding: '10px 12px',
borderRadius: 8,
border: '1px solid rgba(34,197,94,0.35)',
background: 'rgba(34,197,94,0.15)',
color: '#d1fae5',
fontSize: 14,
},
badge: {
display: 'inline-flex',
alignItems: 'center',
gap: 8,
background: 'rgba(34,197,94,0.15)',
border: '1px solid rgba(34,197,94,0.35)',
padding: '8px 12px',
borderRadius: 999,
marginBottom: 12,
},
badgeEmoji: { fontSize: 18 },
badgeText: { fontSize: 14, letterSpacing: 0.4 },
title: { fontSize: 28, lineHeight: 1.25, margin: '0 0 18px', fontWeight: 700 },
card: {
textAlign: 'left',
background: 'linear-gradient(180deg, rgba(255,255,255,0.04), rgba(255,255,255,0.02))',
border: '1px solid rgba(255,255,255,0.08)',
borderRadius: 16,
boxShadow: '0 20px 60px rgba(0,0,0,0.35)',
padding: 20,
backdropFilter: 'blur(6px)',
},
headerRow: {
display: 'grid',
gridTemplateColumns: '72px 1fr',
gap: 16,
alignItems: 'center',
marginBottom: 12,
},
logo: {
width: 72,
height: 72,
borderRadius: 12,
objectFit: 'cover',
border: '1px solid rgba(255,255,255,0.15)',
background: 'rgba(255,255,255,0.06)',
},
logoFallback: {
width: 72,
height: 72,
borderRadius: 12,
display: 'grid',
placeItems: 'center',
background: 'rgba(255,255,255,0.06)',
border: '1px solid rgba(255,255,255,0.15)',
fontSize: 28,
fontWeight: 700,
},
storeName: {
fontSize: 22,
fontWeight: 700,
marginBottom: 4,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
},
storeMeta: { opacity: 0.8, fontSize: 14 },
desc: { marginTop: 10, marginBottom: 16, lineHeight: 1.6, opacity: 0.95 },
actions: { display: 'flex', gap: 12, flexWrap: 'wrap' },
primaryBtn: {
background: 'linear-gradient(180deg, #22c55e, #16a34a)',
color: 'white',
textDecoration: 'none',
padding: '10px 14px',
borderRadius: 10,
fontWeight: 600,
border: '1px solid rgba(255,255,255,0.15)',
},
secondaryBtn: {
background: 'transparent',
color: 'white',
textDecoration: 'none',
padding: '10px 14px',
borderRadius: 10,
fontWeight: 600,
border: '1px solid rgba(255,255,255,0.2)',
},
hint: { marginTop: 14, opacity: 0.75, fontSize: 14 },
connectWrap: {
textAlign: 'left',
background: 'linear-gradient(180deg, rgba(255,255,255,0.04), rgba(255,255,255,0.02))',
border: '1px solid rgba(255,255,255,0.08)',
borderRadius: 16,
boxShadow: '0 20px 60px rgba(0,0,0,0.35)',
padding: 20,
backdropFilter: 'blur(6px)',
},
subtleText: { opacity: 0.85, marginBottom: 14 },
buttonBase: {
width: '100%',
padding: '10px 14px',
borderRadius: 10,
fontWeight: 700,
border: '1px solid rgba(255,255,255,0.2)',
cursor: 'pointer',
},
btnPrimary: {
background: 'linear-gradient(180deg, #2563eb, #1d4ed8)',
color: 'white',
},
btnDisabled: {
opacity: 0.7,
color: 'white',
background: 'linear-gradient(180deg, #2563eb, #1d4ed8)',
cursor: 'not-allowed',
},
};
/** ===== Confetti (pure CSS) ===== */
const Confetti = () => {
const pieces = Array.from({ length: 24 });
return (
<div style={confettiStyles.wrap} aria-hidden>
<style>{confettiKeyframes}</style>
{pieces.map((_, i) => (
<span key={i} style={pieceStyle(i)} />
))}
</div>
);
};
const confettiStyles: Record<string, React.CSSProperties> = {
wrap: { position: 'fixed', inset: 0, pointerEvents: 'none', overflow: 'hidden' },
};
const confettiKeyframes = `
@keyframes drop {
0% { transform: translateY(-10vh) rotate(0deg); opacity: 0; }
10% { opacity: 1; }
100% { transform: translateY(110vh) rotate(720deg); opacity: 0; }
}
`;
function pieceStyle(i: number): React.CSSProperties {
const left = Math.random() * 100;
const size = 6 + Math.random() * 8;
const duration = 2.8 + Math.random() * 1.6;
const delay = Math.random() * 0.8;
const borderRadius = Math.random() > 0.5 ? 2 : 999;
return {
position: 'absolute',
top: '-10vh',
left: `${left}vw`,
width: size,
height: size,
background:
Math.random() > 0.5
? 'rgba(99,102,241,0.95)'
: Math.random() > 0.5
? 'rgba(16,185,129,0.95)'
: 'rgba(244,114,182,0.95)',
borderRadius,
animation: `drop ${duration}s ease-in ${delay}s both`,
};
}

View File

@ -0,0 +1,91 @@
// app/(defaults)/ebay-settings/EbaySettingsClient.tsx
'use client';
import { useEffect, useMemo, useState } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
const OAUTH_ENDPOINT = 'https://ebay.backend.data4autos.com/api/ebay/oauth/login';
/**
* This expects your backend to redirect back to this page with ?status=success
* (or ?connected=1). If your backend uses a different param, adjust below.
*/
export default function EbaySettingsClient() {
const router = useRouter();
const searchParams = useSearchParams();
const [connecting, setConnecting] = useState(false);
const [connected, setConnected] = useState(false);
const [toast, setToast] = useState<string>('');
// Compute a clean return URL for after OAuth
const returnUrl = useMemo(() => {
if (typeof window === 'undefined') return '';
// Send user back here after OAuth
return `${window.location.origin}/ebay-settings?status=success`;
}, []);
// On load, detect success flags from query params
useEffect(() => {
const status = searchParams.get('status');
const connectedFlag = searchParams.get('connected');
if (status === 'success' || connectedFlag === '1') {
setConnected(true);
setToast('Connection successful');
// Remove query params from the URL after showing success
router.replace('/ebay-settings');
}
}, [router, searchParams]);
const startOauth = () => {
try {
setConnecting(true);
// If your backend supports a return/redirect param, include it:
const url = `${OAUTH_ENDPOINT}?return_url=${encodeURIComponent(returnUrl)}`;
// Open in the same window
window.location.href = url;
} catch (e) {
setConnecting(false);
setToast('Failed to start eBay connection. Please try again.');
}
};
return (
<div className="max-w-2xl mx-auto p-6">
{/* Toast / popup */}
{toast && (
<div className="mb-4 rounded-md border border-green-300 bg-green-50 px-4 py-3 text-sm text-green-800">
{toast}
</div>
)}
<h1 className="text-2xl font-bold mb-4">eBay Settings</h1>
<p className="text-gray-600 mb-6">
Connect your eBay store to enable product sync, inventory updates, and order flow.
</p>
<button
onClick={startOauth}
disabled={connecting || connected}
className={[
'w-full py-2 px-4 font-semibold rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2',
connected
? 'bg-green-600 text-white hover:bg-green-700 focus:ring-green-500'
: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
connecting ? 'opacity-70 cursor-not-allowed' : ''
].join(' ')}
>
{connected
? 'Connected to eBay'
: connecting
? 'Redirecting to eBay…'
: 'Connect your eBay store'}
</button>
<p className="mt-3 text-xs text-gray-500">
Youll be redirected to eBay to authorize access, then returned here automatically.
</p>
</div>
);
}

View File

@ -0,0 +1,254 @@
'use client';
import React, { useEffect, useMemo, useState } from 'react';
import { getAccessToken_client } from '@/utils/apiHelper_client';
import { useRouter } from 'next/navigation';
import axios from 'axios';
type StoreFromAPI = {
userid: string;
store_name: string;
store_description: string;
store_url: string;
store_url_path: string;
store_last_opened_time: string | null;
store_last_opened_time_raw: string | null;
store_logo_url: string;
};
type ApiOk = { code: 'STORE_PRESENT'; message: string; store: StoreFromAPI };
type ApiMiss = { code: 'USER_NOT_FOUND_OR_NO_STORE'; message: string };
type ApiResp = ApiOk | ApiMiss | any;
const read = async (r: Response) =>
r.headers.get('content-type')?.includes('application/json') ? r.json() : r.text();
function safeDateFormat(input?: string | null) {
if (!input) return '—';
const d = new Date(input);
return isNaN(d.getTime()) ? '—' : d.toLocaleString();
}
export default function EbayAuthPage() {
const router = useRouter()
const [status, setStatus] = useState<'loading' | 'connected' | 'disconnected'>('loading');
const [store, setStore] = useState<StoreFromAPI | null>(null);
const [toast, setToast] = useState<string>('');
const [connecting, setConnecting] = useState(false);
const [connectingAnother, setConnectingAnother] = useState(false);
const [payment, setPayment] = useState<any>(null);
const returnUrl = useMemo(() => {
if (typeof window === 'undefined') return '';
return `${window.location.origin}${window.location.pathname}`;
}, []);
useEffect(() => {
const role = localStorage.getItem("user_role");
const sessionId = localStorage.getItem("payment_session");
// ✅ Admins and Partners can access directly (skip payment check)
if (role === "admin" || role === "partner") {
return;
}
// 🚫 If no payment session, redirect to pricing
if (!sessionId) {
router.push("/pricing");
return;
}
// ✅ Otherwise, check payment details
const fetchPaymentDetails = async () => {
try {
const res: any = await axios.get(
"https://ebay.backend.data4autos.com/api/payment/details",
{ params: { session_id: sessionId } }
);
setPayment(res.data.payment);
} catch (err) {
console.error("Error fetching payment details:", err);
}
};
fetchPaymentDetails();
}, [router]);
useEffect(() => {
(async () => {
try {
await getAccessToken_client().catch(() => null);
const userid = sessionStorage.getItem('USERID') || undefined;
if (!userid) {
setStatus('disconnected');
setToast('No user session found. Please sign in.');
return;
}
const res = await fetch(
'https://ebay.backend.data4autos.com/api/auth/ebay/store/checkstorestatus',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userid }),
cache: 'no-store',
}
);
const data: ApiResp = await read(res);
if (data?.code === 'STORE_PRESENT' && data?.store) {
setStore(data.store);
setStatus('connected');
} else {
setStatus('disconnected');
}
} catch {
setStatus('disconnected');
}
})();
}, []);
const OAUTH_ENDPOINT = 'https://ebay.backend.data4autos.com/api/ebay/oauth/motorstate/login';
const startOauth = () => {
setConnecting(true);
const url = `${OAUTH_ENDPOINT}?return_url=${encodeURIComponent(returnUrl)}`;
window.location.href = url;
};
const startOauthAnother = () => {
setConnectingAnother(true);
const url = `${OAUTH_ENDPOINT}?return_url=${encodeURIComponent(returnUrl)}&add_another=1`;
window.location.href = url;
};
const { subtitle, logoUrl, desc, link, lastOpen } = useMemo(() => {
if (!store)
return { subtitle: '', logoUrl: '', desc: '', link: '', lastOpen: '—' };
return {
subtitle: store.store_name || store.store_url_path,
logoUrl: store.store_logo_url,
desc: store.store_description,
link: store.store_url,
lastOpen: safeDateFormat(store.store_last_opened_time || store.store_last_opened_time_raw),
};
}, [store]);
return (
<div className="min-h-[83vh] flex items-center justify-center bg-gradient-to-br from-[#00d1ff]/10 via-white to-[#00d1ff]/20 p-6">
<div className="max-w-2xl w-full bg-white shadow-xl rounded-2xl p-8 border border-blue-100">
{toast && (
<div className="mb-4 text-sm text-blue-800 bg-blue-50 border border-blue-200 rounded-lg px-4 py-2">
{toast}
</div>
)}
{status === 'loading' && (
<div className="text-center space-y-3">
<div className="inline-flex items-center gap-2 bg-blue-50 text-blue-600 px-4 py-1 rounded-full text-sm">
<span className="animate-spin"></span> Checking your store status
</div>
<h1 className="text-2xl font-semibold text-gray-800">Verifying eBay connection</h1>
</div>
)}
{status === 'connected' && (
<div className="space-y-5">
<div className="flex items-center justify-center gap-2 bg-green-50 text-green-700 border border-green-200 rounded-full px-4 py-1 w-fit mx-auto text-sm">
Connected
</div>
<h1 className="text-center text-3xl font-bold text-gray-900">
eBay connected successfully! 🎉
</h1>
<div className="border border-gray-200 rounded-xl shadow-inner p-6 bg-gradient-to-b from-white to-gray-50">
<div className="flex items-center gap-4 mb-4">
{logoUrl ? (
<img
src={logoUrl}
alt="Store Logo"
className="w-16 h-16 rounded-lg border border-gray-200 object-cover"
/>
) : (
<div className="w-16 h-16 bg-gray-100 rounded-lg flex items-center justify-center text-xl font-bold text-gray-600">
{(subtitle || 'S').slice(0, 1)}
</div>
)}
<div>
<h2 className="text-lg font-semibold text-gray-800">{subtitle}</h2>
<p className="text-sm text-gray-500">
Last opened: <span className="font-medium">{lastOpen}</span>
</p>
</div>
</div>
{desc && <p className="text-gray-600 mb-4">{desc}</p>}
<div className="flex flex-wrap gap-3">
{link && (
<a
href={link}
target="_blank"
rel="noopener noreferrer"
className="bg-[#00d1ff] text-white px-4 py-2 rounded-lg font-semibold shadow hover:bg-[#00bce6] transition"
>
Visit eBay Store
</a>
)}
<a
href="/"
className="px-4 py-2 border border-[#00d1ff] text-[#00d1ff] rounded-lg font-semibold hover:bg-[#00d1ff] hover:text-white transition"
>
Go to Dashboard
</a>
</div>
<div className="mt-6">
<button
onClick={startOauthAnother}
disabled={connectingAnother}
className={`w-full px-4 py-2 rounded-lg font-semibold transition ${connectingAnother
? 'bg-blue-300 cursor-not-allowed'
: 'bg-[#00d1ff] hover:bg-[#00bce6] text-white'
}`}
>
{connectingAnother ? 'Redirecting…' : 'Connect another eBay store'}
</button>
<p className="text-sm text-gray-500 mt-2">
Use this to link an additional eBay store.
</p>
</div>
</div>
</div>
)}
{status === 'disconnected' && (
<div className="space-y-4 text-center">
<h1 className="text-3xl font-bold text-gray-900">eBay Settings</h1>
<p className="text-gray-600">
Connect your eBay store to enable product sync, inventory updates, and order flow.
</p>
<button
onClick={startOauth}
disabled={connecting}
className={`w-full px-4 py-2 rounded-lg font-semibold transition ${connecting
? 'bg-blue-300 cursor-not-allowed'
: 'bg-[#00d1ff] hover:bg-[#00bce6] text-white'
}`}
>
{connecting ? 'Redirecting to eBay…' : 'Connect your eBay store'}
</button>
<p className="text-sm text-gray-500">
Youll be redirected to eBay to authorize access, then returned here.
</p>
</div>
)}
</div>
</div>
);
}

View File

@ -0,0 +1,12 @@
// app/(defaults)/ebay-settings/page.tsx
import type { Metadata } from 'next';
import EbaySettingsClient from './EbaySettingsClient';
export const metadata: Metadata = {
title: 'eBay Settings',
};
export default function Page() {
return <EbaySettingsClient />;
}

View File

@ -0,0 +1,77 @@
import IconArrowWaveLeftUp from '@/components/icon/icon-arrow-wave-left-up';
import ComponentsPagesFaqWithTabs from '@/components/pages/components-pages-faq-with-tabs';
import { Metadata } from 'next';
import Link from 'next/link';
import React from 'react';
export const metadata: Metadata = {
title: 'FAQ',
};
const Faq = () => {
return (
<div>
<div className="relative rounded-t-md bg-primary-light bg-[url('/assets/images/knowledge/pattern.png')] bg-contain bg-left-top bg-no-repeat px-5 py-10 dark:bg-black md:px-10">
<div className="absolute -bottom-1 -end-6 hidden text-[#DBE7FF] rtl:rotate-y-180 dark:text-[#1B2E4B] lg:block xl:end-0">
<img src="/assets/images/faq/faq-dark.svg" alt="faqs" className="dark-img w-56 object-cover xl:w-80" />
<img src="/assets/images/faq/faq-light.svg" alt="faqs" className="light-img w-56 object-cover xl:w-80" />
</div>
<div className="relative">
<div className="flex flex-col items-center justify-center sm:-ms-32 sm:flex-row xl:-ms-60">
<div className="mb-2 flex gap-1 text-end text-base leading-5 sm:flex-col xl:text-xl">
<span>It&apos;s free </span>
<span>For everyone</span>
</div>
<div className="me-4 ms-2 hidden text-[#0E1726] rtl:rotate-y-180 dark:text-white sm:block">
<IconArrowWaveLeftUp className="w-16 xl:w-28" />
</div>
<div className="mb-2 text-center text-2xl font-bold dark:text-white md:text-5xl">FAQs</div>
</div>
<p className="mb-9 text-center text-base font-semibold">Search instant answers & questions asked by popular users</p>
<form action="" method="" className="mb-6">
<div className="relative mx-auto max-w-[580px]">
<input type="text" placeholder="Ask a question" className="form-input py-3 ltr:pr-[100px] rtl:pl-[100px]" />
<button type="button" className="btn btn-primary absolute top-1 shadow-none ltr:right-1 rtl:left-1">
Search
</button>
</div>
</form>
<div className="flex flex-wrap items-center justify-center gap-2 font-semibold text-[#2196F3] sm:gap-5">
<div className="whitespace-nowrap font-medium text-black dark:text-white">Popular topics :</div>
<div className="flex items-center justify-center gap-2 sm:gap-5">
<Link href="#" className="duration-300 hover:underline">
Sales
</Link>
<Link href="#" className="duration-300 hover:underline">
Charts
</Link>
<Link href="#" className="duration-300 hover:underline">
Finance
</Link>
<Link href="#" className="duration-300 hover:underline">
Trending
</Link>
</div>
</div>
</div>
</div>
<ComponentsPagesFaqWithTabs title='Frequently asked <span className="text-primary">questions</span>' />
<div className="panel mt-10 text-center md:mt-20">
<h3 className="mb-2 text-xl font-bold dark:text-white md:text-2xl">Still need help?</h3>
<div className="text-lg font-medium text-white-dark">
Our specialists are always happy to help. Contact us during standard business hours or email us24/7 and we&apos;ll get back to you.
</div>
<div className="mt-8 flex flex-col items-center justify-center gap-6 sm:flex-row">
<button type="button" className="btn btn-primary">
Contact Us
</button>
<button type="button" className="btn btn-primary">
Visit our community
</button>
</div>
</div>
</div>
);
};
export default Faq;

View File

@ -0,0 +1,13 @@
import ComponentsAppsInvoicePreview from '@/components/components-apps-invoice-preview';
import { Metadata } from 'next';
import React from 'react';
export const metadata: Metadata = {
title: 'Invoice Preview',
};
const InvoicePreview = () => {
return <ComponentsAppsInvoicePreview />;
};
export default InvoicePreview;

View File

@ -0,0 +1,260 @@
import IconArrowWaveLeftUp from '@/components/icon/icon-arrow-wave-left-up';
import ComponentsPagesFaqWithTabs from '@/components/pages/components-pages-faq-with-tabs';
import ComponentsPagesKnowledgeBaseVideoTutorial from '@/components/pages/knowledge-base/components-pages-knowledge-base-video-tutorial';
import { Metadata } from 'next';
import Link from 'next/link';
import React from 'react';
export const metadata: Metadata = {
title: 'Knowledge Base',
};
const KnowledgeBase = () => {
return (
<div>
<div className="relative rounded-t-md bg-primary-light bg-[url('/assets/images/knowledge/pattern.png')] bg-contain bg-left-top bg-no-repeat px-5 py-10 dark:bg-black md:px-10">
<div className="absolute -bottom-1 -end-6 hidden text-[#DBE7FF] rtl:rotate-y-180 dark:text-[#1B2E4B] lg:block xl:end-0">
<svg width="375" height="185" viewBox="0 0 375 185" fill="none" xmlns="http://www.w3.org/2000/svg" className="w-72 max-w-xs xl:w-full">
<g clipPath="url(#clip0_1109_89938)">
<path
d="M215.023 181.044C212.702 181.042 210.477 180.122 208.836 178.487C207.196 176.851 206.274 174.633 206.274 172.321C206.274 170.008 207.196 167.79 208.836 166.155C210.477 164.519 212.702 163.599 215.023 163.598H345.07C344.415 162.401 343.45 161.403 342.275 160.707C341.099 160.01 339.757 159.643 338.39 159.642H79.8922C76.5197 159.645 73.2866 160.983 70.9034 163.36C68.5202 165.738 67.1817 168.961 67.1821 172.321C67.1838 175.68 68.523 178.902 70.9058 181.279C73.2885 183.656 76.5204 184.994 79.8922 185H338.39C339.757 184.999 341.099 184.631 342.275 183.935C343.45 183.239 344.415 182.24 345.069 181.044L215.023 181.044Z"
fill="currentColor"
/>
<path
d="M345.242 168.405H221.598C221.409 168.404 221.228 168.329 221.094 168.195C220.96 168.062 220.885 167.881 220.885 167.693C220.885 167.504 220.96 167.323 221.094 167.19C221.228 167.056 221.409 166.981 221.598 166.98H345.242C345.431 166.981 345.613 167.056 345.746 167.19C345.88 167.323 345.955 167.504 345.955 167.693C345.955 167.881 345.88 168.062 345.746 168.195C345.613 168.329 345.431 168.404 345.242 168.405Z"
fill="currentColor"
/>
<path
d="M345.242 173.033H221.598C221.409 173.033 221.227 172.958 221.093 172.824C220.959 172.691 220.884 172.51 220.884 172.321C220.884 172.132 220.959 171.951 221.093 171.817C221.227 171.684 221.409 171.609 221.598 171.609H345.242C345.432 171.609 345.614 171.684 345.748 171.817C345.882 171.951 345.957 172.132 345.957 172.321C345.957 172.51 345.882 172.691 345.748 172.824C345.614 172.958 345.432 173.033 345.242 173.033Z"
fill="currentColor"
/>
<path
d="M345.242 177.661H221.598C221.409 177.661 221.228 177.586 221.094 177.452C220.96 177.319 220.885 177.138 220.885 176.949C220.885 176.761 220.96 176.58 221.094 176.446C221.228 176.313 221.409 176.238 221.598 176.237H345.242C345.431 176.238 345.613 176.313 345.746 176.446C345.88 176.58 345.955 176.761 345.955 176.949C345.955 177.138 345.88 177.319 345.746 177.452C345.613 177.586 345.431 177.661 345.242 177.661Z"
fill="currentColor"
/>
<path
d="M181.432 156.477C179.111 156.476 176.885 155.556 175.245 153.92C173.604 152.285 172.683 150.067 172.683 147.754C172.683 145.442 173.604 143.224 175.245 141.588C176.885 139.953 179.111 139.033 181.432 139.031H311.478C310.824 137.835 309.859 136.836 308.683 136.14C307.508 135.444 306.166 135.076 304.799 135.075H46.3011C42.9286 135.079 39.6955 136.417 37.3123 138.794C34.929 141.171 33.5905 144.394 33.5908 147.754C33.5926 151.114 34.9318 154.335 37.3146 156.712C39.6974 159.089 42.9293 160.428 46.3011 160.433H304.799C306.166 160.432 307.508 160.065 308.683 159.368C309.859 158.672 310.824 157.674 311.478 156.477L181.432 156.477Z"
fill="#4361EE"
/>
<path
d="M311.651 143.838H188.007C187.818 143.837 187.637 143.762 187.503 143.629C187.37 143.495 187.294 143.314 187.294 143.126C187.294 142.937 187.37 142.756 187.503 142.623C187.637 142.489 187.818 142.414 188.007 142.414H311.651C311.841 142.414 312.023 142.489 312.157 142.622C312.291 142.756 312.366 142.937 312.366 143.126C312.366 143.314 312.291 143.496 312.157 143.629C312.023 143.763 311.841 143.838 311.651 143.838Z"
fill="currentColor"
/>
<path
d="M311.651 148.466H188.007C187.818 148.466 187.636 148.391 187.502 148.258C187.368 148.124 187.292 147.943 187.292 147.754C187.292 147.565 187.368 147.384 187.502 147.251C187.636 147.117 187.818 147.042 188.007 147.042H311.651C311.841 147.042 312.022 147.117 312.156 147.251C312.291 147.384 312.366 147.565 312.366 147.754C312.366 147.943 312.291 148.124 312.156 148.258C312.022 148.391 311.841 148.466 311.651 148.466Z"
fill="currentColor"
/>
<path
d="M311.651 153.095H188.007C187.818 153.095 187.637 153.019 187.503 152.886C187.37 152.752 187.294 152.572 187.294 152.383C187.294 152.194 187.37 152.014 187.503 151.88C187.637 151.747 187.818 151.671 188.007 151.671H311.651C311.745 151.671 311.838 151.689 311.925 151.725C312.012 151.76 312.091 151.813 312.158 151.879C312.224 151.945 312.277 152.024 312.313 152.11C312.349 152.197 312.368 152.289 312.368 152.383C312.368 152.477 312.349 152.569 312.313 152.656C312.277 152.742 312.224 152.821 312.158 152.887C312.091 152.953 312.012 153.006 311.925 153.041C311.838 153.077 311.745 153.095 311.651 153.095Z"
fill="currentColor"
/>
<path
d="M147.841 131.091C145.52 131.089 143.295 130.17 141.654 128.534C140.013 126.898 139.092 124.681 139.092 122.368C139.092 120.056 140.013 117.838 141.654 116.202C143.295 114.567 145.52 113.647 147.841 113.645H277.887C277.233 112.449 276.268 111.45 275.093 110.754C273.917 110.058 272.575 109.69 271.208 109.689H12.7101C9.33759 109.693 6.10452 111.03 3.72128 113.408C1.33804 115.785 -0.000419607 119.008 9.86767e-08 122.368C0.00170636 125.728 1.34085 128.949 3.72363 131.326C6.10641 133.703 9.33824 135.041 12.7101 135.047H271.208C272.575 135.046 273.917 134.678 275.093 133.982C276.268 133.286 277.233 132.287 277.887 131.091L147.841 131.091Z"
fill="currentColor"
/>
<path
d="M278.06 118.452H154.416C154.227 118.451 154.046 118.376 153.912 118.242C153.778 118.109 153.703 117.928 153.703 117.739C153.703 117.551 153.778 117.37 153.912 117.237C154.046 117.103 154.227 117.028 154.416 117.027H278.06C278.25 117.027 278.431 117.102 278.565 117.236C278.699 117.369 278.775 117.551 278.775 117.739C278.775 117.928 278.699 118.109 278.565 118.243C278.431 118.376 278.25 118.452 278.06 118.452Z"
fill="currentColor"
/>
<path
d="M278.06 123.08H154.416C154.323 123.08 154.23 123.062 154.143 123.026C154.056 122.99 153.977 122.937 153.911 122.871C153.845 122.805 153.792 122.727 153.756 122.64C153.72 122.554 153.702 122.461 153.702 122.368C153.702 122.274 153.72 122.182 153.756 122.095C153.792 122.009 153.845 121.93 153.911 121.864C153.977 121.798 154.056 121.746 154.143 121.71C154.23 121.674 154.323 121.656 154.416 121.656H278.06C278.154 121.656 278.247 121.674 278.334 121.71C278.42 121.746 278.499 121.798 278.566 121.864C278.632 121.93 278.685 122.009 278.721 122.095C278.756 122.182 278.775 122.274 278.775 122.368C278.775 122.461 278.756 122.554 278.721 122.64C278.685 122.727 278.632 122.805 278.566 122.871C278.499 122.937 278.42 122.99 278.334 123.026C278.247 123.062 278.154 123.08 278.06 123.08Z"
fill="currentColor"
/>
<path
d="M278.06 127.708H154.416C154.227 127.708 154.045 127.633 153.911 127.5C153.777 127.366 153.702 127.185 153.702 126.996C153.702 126.807 153.777 126.626 153.911 126.493C154.045 126.359 154.227 126.284 154.416 126.284H278.06C278.154 126.284 278.247 126.303 278.334 126.338C278.42 126.374 278.499 126.427 278.566 126.493C278.632 126.559 278.685 126.637 278.721 126.724C278.756 126.81 278.775 126.903 278.775 126.996C278.775 127.09 278.756 127.182 278.721 127.269C278.685 127.355 278.632 127.434 278.566 127.5C278.499 127.566 278.42 127.618 278.334 127.654C278.247 127.69 278.154 127.708 278.06 127.708Z"
fill="currentColor"
/>
<path
d="M280.915 76.9348C280.237 76.9337 279.585 76.676 279.09 76.2137C278.595 75.7514 278.296 75.1191 278.251 74.4449L277.854 68.4086C277.808 67.7035 278.045 67.009 278.512 66.4778C278.98 65.9466 279.64 65.6222 280.348 65.576L327.814 62.479C328.562 62.4301 329.313 62.5286 330.023 62.7688C330.733 63.009 331.388 63.3861 331.952 63.8787C332.516 64.3713 332.977 64.9697 333.308 65.6398C333.64 66.3098 333.836 67.0383 333.885 67.7838C333.934 68.5292 333.835 69.277 333.594 69.9844C333.353 70.6918 332.974 71.345 332.48 71.9066C331.985 72.4682 331.385 72.9274 330.712 73.2577C330.04 73.5881 329.308 73.7833 328.56 73.8321L281.094 76.929C281.034 76.9328 280.975 76.9347 280.915 76.9348Z"
fill="#4361EE"
/>
<path
d="M290.275 77.713C289.583 77.7119 288.919 77.4442 288.421 76.9662C287.924 76.4881 287.631 75.8366 287.604 75.1482L287.266 66.1324C287.253 65.7828 287.309 65.434 287.431 65.106C287.553 64.778 287.739 64.4772 287.978 64.2208C288.217 63.9644 288.504 63.7573 288.823 63.6115C289.142 63.4657 289.487 63.3839 289.838 63.3709L328.871 61.9174C329.579 61.891 330.27 62.1462 330.789 62.6268C331.309 63.1074 331.616 63.7741 331.643 64.4801L331.981 73.496C331.994 73.8456 331.938 74.1944 331.816 74.5224C331.694 74.8504 331.508 75.1512 331.269 75.4076C331.03 75.664 330.743 75.8711 330.424 76.0169C330.104 76.1627 329.76 76.2445 329.409 76.2575L290.376 77.711C290.342 77.7124 290.308 77.713 290.275 77.713Z"
fill="#2F2E41"
/>
<path
d="M336.015 160.823H329.943C329.234 160.822 328.555 160.541 328.054 160.041C327.552 159.542 327.27 158.865 327.27 158.159V107.741C327.27 107.035 327.552 106.358 328.054 105.858C328.555 105.359 329.234 105.078 329.943 105.077H336.015C336.724 105.078 337.404 105.359 337.905 105.858C338.406 106.358 338.688 107.035 338.689 107.741V158.159C338.688 158.865 338.406 159.542 337.905 160.041C337.404 160.541 336.724 160.822 336.015 160.823Z"
fill="#2F2E41"
/>
<path
d="M309.066 134.173L303.873 131.037C303.268 130.67 302.833 130.079 302.664 129.393C302.495 128.707 302.606 127.982 302.973 127.378L329.203 84.2625C329.571 83.6589 330.165 83.2254 330.853 83.0572C331.542 82.889 332.269 82.9998 332.876 83.3652L338.068 86.5008C338.674 86.8676 339.109 87.4588 339.278 88.1448C339.446 88.8308 339.335 89.5554 338.969 90.1599L312.738 133.275C312.37 133.879 311.777 134.312 311.088 134.481C310.4 134.649 309.672 134.538 309.066 134.173Z"
fill="#2F2E41"
/>
<path
d="M326.794 52.4612C338.38 52.4612 347.773 43.1029 347.773 31.5589C347.773 20.0148 338.38 10.6565 326.794 10.6565C315.207 10.6565 305.814 20.0148 305.814 31.5589C305.814 43.1029 315.207 52.4612 326.794 52.4612Z"
fill="#4361EE"
/>
<path
d="M315.438 38.6956C314.763 38.4746 314.143 38.1123 313.621 37.6329C313.274 37.2907 313.007 36.8771 312.838 36.4212C312.67 35.9654 312.603 35.4782 312.643 34.9939C312.665 34.6488 312.766 34.3135 312.94 34.0141C313.113 33.7147 313.354 33.4593 313.643 33.268C314.393 32.7885 315.397 32.7871 316.419 33.2357L316.38 25.0757L317.203 25.0718L317.248 34.6647L316.614 34.2674C315.879 33.8075 314.829 33.4838 314.088 33.958C313.904 34.0833 313.752 34.249 313.644 34.4423C313.535 34.6355 313.473 34.8511 313.462 35.0723C313.434 35.4331 313.484 35.7956 313.61 36.1351C313.735 36.4747 313.933 36.7833 314.189 37.04C315.097 37.9046 316.423 38.175 317.934 38.4166L317.804 39.2258C317.001 39.1199 316.209 38.9424 315.438 38.6956Z"
fill="#2F2E41"
/>
<path d="M307.899 24.6635L307.791 25.4761L312.184 26.0541L312.292 25.2415L307.899 24.6635Z" fill="#2F2E41" />
<path d="M321.765 26.4873L321.657 27.2998L326.05 27.8778L326.158 27.0653L321.765 26.4873Z" fill="#2F2E41" />
<path
d="M344.312 121.268H308.934C308.007 121.267 307.119 120.9 306.463 120.247C305.808 119.593 305.439 118.708 305.438 117.784L312.844 61.1985C312.85 60.2791 313.221 59.3994 313.876 58.7516C314.531 58.1038 315.416 57.7405 316.339 57.7412H328.894C333.908 57.7469 338.716 59.7341 342.262 63.267C345.808 66.7999 347.803 71.5899 347.808 76.5861V117.784C347.807 118.708 347.438 119.593 346.783 120.247C346.127 120.9 345.239 121.267 344.312 121.268Z"
fill="#2F2E41"
/>
<path
d="M374.749 98.9713C374.967 99.4372 375.046 99.9562 374.975 100.466C374.905 100.975 374.688 101.453 374.351 101.843L370.381 106.42C370.152 106.685 369.872 106.902 369.558 107.059C369.245 107.216 368.903 107.31 368.553 107.336C368.202 107.361 367.851 107.318 367.517 107.208C367.184 107.098 366.876 106.923 366.61 106.695L330.615 75.7111C330.048 75.2228 329.582 74.6279 329.246 73.9603C328.909 73.2928 328.707 72.5657 328.653 71.8205C328.598 71.0754 328.691 70.3268 328.927 69.6175C329.163 68.9083 329.536 68.2522 330.027 67.6869C330.517 67.1215 331.114 66.6579 331.784 66.3226C332.454 65.9872 333.184 65.7866 333.932 65.7323C334.68 65.678 335.431 65.7711 336.143 66.0061C336.855 66.2411 337.513 66.6136 338.081 67.1021L374.075 98.0854C374.36 98.33 374.59 98.6319 374.749 98.9713Z"
fill="#4361EE"
/>
<path
d="M366.804 88.7464C367.023 89.2124 367.101 89.7313 367.031 90.2408C366.96 90.7503 366.743 91.2286 366.406 91.6181L359.626 99.4367C359.162 99.9706 358.505 100.299 357.798 100.351C357.091 100.402 356.392 100.172 355.855 99.7109L326.298 74.2689C326.032 74.0402 325.815 73.7616 325.657 73.449C325.499 73.1364 325.405 72.7959 325.379 72.447C325.354 72.0981 325.398 71.7475 325.508 71.4154C325.618 71.0833 325.793 70.7761 326.023 70.5114L332.803 62.6929C333.032 62.4282 333.312 62.2111 333.626 62.054C333.939 61.897 334.281 61.803 334.631 61.7776C334.982 61.7521 335.333 61.7956 335.667 61.9056C336 62.0157 336.308 62.19 336.574 62.4187L366.131 87.8606C366.416 88.1052 366.645 88.4071 366.804 88.7464Z"
fill="#2F2E41"
/>
<path
d="M327.186 19.1616C324.381 16.8493 320.632 19.0362 317.461 18.7258C314.426 18.4289 311.984 15.7684 311.319 12.9054C310.542 9.56531 312.225 6.1724 314.761 4.03273C317.537 1.68928 321.265 0.961046 324.818 1.31764C328.89 1.72635 332.641 3.54275 336.019 5.76177C339.277 7.83543 342.284 10.2778 344.979 13.0401C347.394 15.5981 349.457 18.6626 350.164 22.1529C350.806 25.3248 350.43 28.8604 348.593 31.5925C347.617 32.9935 346.296 34.1205 344.757 34.8647C343.153 35.6876 341.436 36.2867 339.884 37.2122C337.538 38.6116 335.286 41.4636 336.084 44.3721C336.256 45.0075 336.582 45.5912 337.033 46.0718C337.575 46.6488 338.517 45.8549 337.974 45.2763C337.019 44.2601 337.03 42.8806 337.505 41.6368C338.071 40.2346 339.098 39.0655 340.418 38.321C342.042 37.3544 343.844 36.7426 345.511 35.8587C347.107 35.0498 348.484 33.8686 349.523 32.4156C351.485 29.5881 352.018 25.9466 351.497 22.5971C350.934 18.9723 348.996 15.7055 346.594 12.9827C343.98 10.0194 340.791 7.52961 337.539 5.30018C334.05 2.90798 330.207 0.902431 325.984 0.230686C322.323 -0.351613 318.377 0.124257 315.224 2.16544C312.282 4.07076 310.059 7.29454 309.899 10.8528C309.849 12.4714 310.232 14.0742 311.009 15.4963C311.787 16.9184 312.931 18.1084 314.323 18.9438C315.751 19.7588 317.4 20.1066 319.036 19.9381C320.787 19.7932 322.531 19.2252 324.298 19.3403C325.097 19.3735 325.864 19.6653 326.481 20.1715C327.094 20.6767 327.794 19.6626 327.186 19.1616Z"
fill="#2F2E41"
/>
<path
d="M302.517 128.106C302.524 128.06 302.533 128.014 302.543 127.968C302.616 127.625 302.757 127.301 302.957 127.013C303.157 126.725 303.412 126.48 303.708 126.291L308.81 123.012C309.406 122.63 310.13 122.499 310.823 122.648C311.516 122.797 312.121 123.214 312.506 123.807L328.245 148.119C328.63 148.713 328.762 149.434 328.612 150.125C328.462 150.815 328.044 151.419 327.448 151.802L322.345 155.08C321.749 155.463 321.025 155.594 320.332 155.445C319.639 155.296 319.034 154.879 318.649 154.286L302.91 129.974C302.55 129.421 302.41 128.756 302.517 128.106Z"
fill="#2F2E41"
/>
</g>
<defs>
<clipPath id="clip0_1109_89938">
<rect width="375" height="185" fill="white" />
</clipPath>
</defs>
</svg>
</div>
<div className="relative">
<div className="flex flex-col items-center justify-center sm:-ms-32 sm:flex-row xl:-ms-60">
<div className="mb-2 flex gap-1 text-end text-base leading-5 sm:flex-col xl:text-xl">
<span>It&apos;s free </span>
<span>For everyone</span>
</div>
<div className="me-4 ms-2 hidden text-[#0E1726] rtl:rotate-y-180 dark:text-white sm:block">
<IconArrowWaveLeftUp className="w-16 xl:w-28" />
</div>
<div className="mb-2 text-center text-2xl font-bold dark:text-white md:text-5xl">Knowledge Base</div>
</div>
<p className="mb-9 text-center text-base font-semibold">Search instant answers & questions asked by popular users</p>
<form action="" method="" className="mb-6">
<div className="relative mx-auto max-w-[580px]">
<input type="text" placeholder="Ask a question" className="form-input py-3 ltr:pr-[100px] rtl:pl-[100px]" />
<button type="button" className="btn btn-primary absolute top-1 shadow-none ltr:right-1 rtl:left-1">
Search
</button>
</div>
</form>
<div className="flex flex-wrap items-center justify-center gap-2 font-semibold text-[#2196F3] sm:gap-5">
<div className="whitespace-nowrap font-medium text-black dark:text-white">Popular topics :</div>
<div className="flex items-center justify-center gap-2 sm:gap-5">
<Link href="#" className="duration-300 hover:underline">
Sales
</Link>
<Link href="#" className="duration-300 hover:underline">
Charts
</Link>
<Link href="#" className="duration-300 hover:underline">
Finance
</Link>
<Link href="#" className="duration-300 hover:underline">
Trending
</Link>
</div>
</div>
</div>
</div>
<ComponentsPagesFaqWithTabs />
<div className="panel mt-10 text-center md:mt-20">
<h3 className="mb-2 text-xl font-bold dark:text-white md:text-2xl">Still need help?</h3>
<div className="text-lg font-medium text-white-dark">
Our specialists are always happy to help. Contact us during standard business hours or email us24/7 and we&apos;ll get back to you.
</div>
<div className="mt-8 flex flex-col items-center justify-center gap-6 sm:flex-row">
<button type="button" className="btn btn-primary">
Contact Us
</button>
<button type="button" className="btn btn-primary">
Visit our community
</button>
</div>
</div>
<div className="mt-10 flex flex-col-reverse items-center justify-between gap-5 rounded-md bg-gradient-to-tl from-[rgba(234,241,255,0.44)] to-[rgba(234,241,255,0.96)] px-6 py-2.5 dark:from-[rgba(14,23,38,0.44)] dark:to-[#0E1726] md:flex-row lg:mt-20 xl:px-16">
<div className="flex-1 py-3.5 text-center md:text-start">
<h3 className="mb-2 text-xl font-bold dark:text-white md:text-2xl">Didnt find any solutions?</h3>
<div className="text-lg font-medium text-white-dark">Loaded with awesome features like documentation, knowledge base forum, domain transfer, affiliates etc.</div>
<div className="mt-8 flex justify-center md:justify-start lg:mt-16">
<button type="button" className="btn btn-primary">
Raise support tickets
</button>
</div>
</div>
<div className="w-52 max-w-xs lg:w-full">
<img src="/assets/images/knowledge/find-solution.svg" alt="find-solution" className="w-full object-cover rtl:rotate-y-180 dark:brightness-[2.59] dark:grayscale-[83%]" />
</div>
</div>
<div className="mt-10">
<h3 className="mb-6 text-xl font-bold md:text-3xl">Popular Topics</h3>
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 xl:grid-cols-4">
<div className="space-y-5 rounded-md border border-white-light bg-white p-5 shadow-[0px_0px_2px_0px_rgba(145,158,171,0.20),0px_12px_24px_-4px_rgba(145,158,171,0.12)] dark:border-[#1B2E4B] dark:bg-black">
<div className="max-h-56 overflow-hidden rounded-md">
<img src="/assets/images/knowledge/image-1.jpg" alt="..." className="w-full object-cover" />
</div>
<h5 className="text-xl dark:text-white">Excessive sugar is harmful</h5>
<div className="flex">
<div className="me-4 overflow-hidden rounded-full bg-white-dark">
<img src="/assets/images/profile-1.jpeg" className="h-11 w-11 object-cover" alt="profile1" />
</div>
<div className="flex-1">
<h4 className="mb-1.5 font-semibold dark:text-white">Alma Clark</h4>
<p>06 May</p>
</div>
</div>
</div>
<div className="space-y-5 rounded-md border border-white-light bg-white p-5 shadow-[0px_0px_2px_0px_rgba(145,158,171,0.20),0px_12px_24px_-4px_rgba(145,158,171,0.12)] dark:border-[#1B2E4B] dark:bg-black">
<div className="max-h-56 overflow-hidden rounded-md">
<img src="/assets/images/knowledge/image-2.jpg" alt="..." className="w-full object-cover" />
</div>
<h5 className="text-xl dark:text-white">Creative Photography</h5>
<div className="flex">
<div className="me-4 overflow-hidden rounded-full bg-white-dark">
<img src="/assets/images/profile-2.jpeg" className="h-11 w-11 object-cover" alt="profile1" />
</div>
<div className="flex-1">
<h4 className="mb-1.5 font-semibold dark:text-white">Alma Clark</h4>
<p>06 May</p>
</div>
</div>
</div>
<div className="space-y-5 rounded-md border border-white-light bg-white p-5 shadow-[0px_0px_2px_0px_rgba(145,158,171,0.20),0px_12px_24px_-4px_rgba(145,158,171,0.12)] dark:border-[#1B2E4B] dark:bg-black">
<div className="max-h-56 overflow-hidden rounded-md">
<img src="/assets/images/knowledge/image-3.jpg" alt="..." className="w-full object-cover" />
</div>
<h5 className="text-xl dark:text-white">Plan your next trip</h5>
<div className="flex">
<div className="me-4 overflow-hidden rounded-full bg-white-dark">
<img src="/assets/images/profile-3.jpeg" className="h-11 w-11 object-cover" alt="profile1" />
</div>
<div className="flex-1">
<h4 className="mb-1.5 font-semibold dark:text-white">Alma Clark</h4>
<p>06 May</p>
</div>
</div>
</div>
<div className="space-y-5 rounded-md border border-white-light bg-white p-5 shadow-[0px_0px_2px_0px_rgba(145,158,171,0.20),0px_12px_24px_-4px_rgba(145,158,171,0.12)] dark:border-[#1B2E4B] dark:bg-black">
<div className="max-h-56 overflow-hidden rounded-md">
<img src="/assets/images/knowledge/image-4.jpg" alt="..." className="w-full object-cover" />
</div>
<h5 className="text-xl dark:text-white">My latest Vlog</h5>
<div className="flex">
<div className="me-4 overflow-hidden rounded-full bg-white-dark">
<img src="/assets/images/profile-4.jpeg" className="h-11 w-11 object-cover" alt="profile1" />
</div>
<div className="flex-1">
<h4 className="mb-1.5 font-semibold dark:text-white">Alma Clark</h4>
<p>06 May</p>
</div>
</div>
</div>
</div>
</div>
<ComponentsPagesKnowledgeBaseVideoTutorial />
</div>
);
};
export default KnowledgeBase;

98
app/(defaults)/layout.tsx Normal file
View File

@ -0,0 +1,98 @@
// 'use client'; // Add this to enable client-side rendering
// import ContentAnimation from '@/components/layouts/content-animation';
// import Footer from '@/components/layouts/footer';
// import Header from '@/components/layouts/header';
// import MainContainer from '@/components/layouts/main-container';
// import Overlay from '@/components/layouts/overlay';
// import ScrollToTop from '@/components/layouts/scroll-to-top';
// import Setting from '@/components/layouts/setting';
// import Sidebar from '@/components/layouts/sidebar';
// import Portals from '@/components/portals';
// export default function DefaultLayout({ children }: { children: React.ReactNode }) {
// return (
// <>
// {/* BEGIN MAIN CONTAINER */}
// <div className="relative">
// <Overlay />
// <ScrollToTop />
// {/* BEGIN APP SETTING LAUNCHER */}
// <Setting />
// {/* END APP SETTING LAUNCHER */}
// <MainContainer>
// {/* BEGIN SIDEBAR */}
// <Sidebar />
// {/* END SIDEBAR */}
// <div className="main-content flex min-h-screen flex-col">
// {/* BEGIN TOP NAVBAR */}
// <Header />
// {/* END TOP NAVBAR */}
// {/* BEGIN CONTENT AREA */}
// <ContentAnimation>{children}</ContentAnimation>
// {/* END CONTENT AREA */}
// {/* BEGIN FOOTER */}
// <Footer />
// {/* END FOOTER */}
// <Portals />
// </div>
// </MainContainer>
// </div>
// </>
// );
// }
'use client';
import React from 'react';
import ContentAnimation from '@/components/layouts/content-animation';
import Footer from '@/components/layouts/footer';
import Header from '@/components/layouts/header';
import MainContainer from '@/components/layouts/main-container';
import Overlay from '@/components/layouts/overlay';
import ScrollToTop from '@/components/layouts/scroll-to-top';
import Setting from '@/components/layouts/setting';
import Sidebar from '@/components/layouts/sidebar';
import Portals from '@/components/portals';
import { SubscriptionProvider } from '@/components/billing/subscription-context';
export default function DefaultLayout({ children }: { children: React.ReactNode }) {
return (
<>
{/* BEGIN MAIN CONTAINER */}
<div className="relative">
<Overlay />
<ScrollToTop />
{/* BEGIN APP SETTING LAUNCHER */}
{/* <Setting /> */}
{/* END APP SETTING LAUNCHER */}
<MainContainer>
{/* BEGIN SIDEBAR */}
<Sidebar />
{/* END SIDEBAR */}
<div className="main-content flex min-h-screen flex-col">
{/* BEGIN TOP NAVBAR */}
<Header />
{/* END TOP NAVBAR */}
{/* BEGIN CONTENT AREA (wrapped with SubscriptionProvider) */}
<SubscriptionProvider>
<ContentAnimation>{children}</ContentAnimation>
</SubscriptionProvider>
{/* END CONTENT AREA */}
{/* BEGIN FOOTER */}
<Footer />
{/* END FOOTER */}
<Portals />
</div>
</MainContainer>
</div>
</>
);
}

View File

@ -0,0 +1,720 @@
'use client';
import axios from 'axios';
import { useRouter } from 'next/navigation';
import React, { useEffect, useMemo, useState } from 'react';
/** ================== Config ================== */
const BACKEND_BASE =
process.env.NEXT_PUBLIC_BACKEND_BASEURL || 'https://ebay.backend.data4autos.com';
// Your GET route that lists from tbl_user_products_mapping
// Example: GET {BACKEND_BASE}/api/user-products-mapping?userid=...&page=1&pageSize=50&search=...
const ENDPOINT_PRODUCTS = `${BACKEND_BASE}/api/motorstate/user-products`;
// Optional: where you queue selected products (same as your other page)
const ENDPOINT_QUEUE = `${BACKEND_BASE}/api/ebay/withdraw-offer`;
/** ================== Types ================== */
type ProductRow = {
trand_id: number;
user_id: string;
id: string | null; // Turn14 item id
sku: string | null;
imgSrc: string | null;
name: string | null;
partNumber: string | null;
category: string | null;
subcategory: string | null;
price: number | string | null; // handle either
inventory: number | string | null;
description: string | null;
offer_status: string | null;
offer_offerId: string | null;
offer_listingId: string | null;
offer_categoryId: string | null;
offer_url: string | null;
offer_error: string | null;
created_at: string;
updated_at: string;
};
type ApiListResp = {
code: 'PRODUCT_MAPPING_LIST';
page: number;
pageSize: number;
total: number;
items: ProductRow[];
};
type Filters = {
inStockOnly: boolean;
priceMin: number;
category: string;
subcategory: string;
offer_status: string;
};
/** ================== Helpers ================== */
const toNumber = (val: unknown): number => {
if (typeof val === 'number') return Number.isFinite(val) ? val : 0;
if (typeof val === 'string') {
const cleaned = val.replace(/[^0-9.\-]/g, '');
const n = parseFloat(cleaned);
return Number.isFinite(n) ? n : 0;
}
return 0;
};
const quantityToList = (qty: number) => {
if (qty <= 1) return 1;
if (qty >= 2 && qty <= 8) return qty;
return 8;
};
/** ================== Component ================== */
const ManageAddedProducts: React.FC = () => {
// Session values (only available client-side)
const userId =
typeof window !== 'undefined' ? sessionStorage.getItem('USERID') : null;
const EBAYSTOREID =
typeof window !== 'undefined' ? sessionStorage.getItem('EBAYSTOREID') : null;
const router = useRouter()
// Data state
const [items, setItems] = useState<ProductRow[]>([]);
const [page, setPage] = useState<number>(1);
const [pageSize, setPageSize] = useState<number>(48);
const [total, setTotal] = useState<number>(0);
// UI state
const [isScrolled, setIsScrolled] = useState(false);
const [loading, setLoading] = useState(false);
const [toast, setToast] = useState('');
const [toastActive, setToastActive] = useState(false);
// Filters / query
const [searchText, setSearchText] = useState('');
const [filters, setFilters] = useState<Filters>({
inStockOnly: false,
priceMin: 0,
category: '',
subcategory: '',
offer_status: '',
});
// Selection
const [selected, setSelected] = useState<Record<number, ProductRow>>({});
const [payment, setPayment] = useState<any>(null);
useEffect(() => {
const role = localStorage.getItem("user_role");
const sessionId = localStorage.getItem("payment_session");
// ✅ Admins and Partners can access directly (skip payment check)
if (role === "admin" || role === "partner") {
return;
}
// 🚫 If no payment session, redirect to pricing
if (!sessionId) {
router.push("/pricing");
return;
}
// ✅ Otherwise, check payment details
const fetchPaymentDetails = async () => {
try {
const res: any = await axios.get(
"https://ebay.backend.data4autos.com/api/payment/details",
{ params: { session_id: sessionId } }
);
setPayment(res.data.payment);
} catch (err) {
console.error("Error fetching payment details:", err);
}
};
fetchPaymentDetails();
}, [router]);
/** Scroll shadow effect */
useEffect(() => {
const onScroll = () => setIsScrolled(window.scrollY > 10);
window.addEventListener('scroll', onScroll);
return () => window.removeEventListener('scroll', onScroll);
}, []);
/** Data loader */
useEffect(() => {
if (!userId) return;
const load = async () => {
setLoading(true);
try {
const params = new URLSearchParams({
userid: userId,
page: String(page),
pageSize: String(pageSize),
});
if (searchText) params.set('search', searchText);
if (filters.category) params.set('category', filters.category);
if (filters.subcategory) params.set('subcategory', filters.subcategory);
if (filters.offer_status) params.set('offer_status', filters.offer_status);
const url = `${ENDPOINT_PRODUCTS}?${params.toString()}`;
const res = await fetch(url, { method: 'GET' });
const data: ApiListResp = await res.json();
setItems(data.items || []);
setTotal(data.total || 0);
setToast(`Loaded ${data.items?.length || 0} items (Total: ${data.total || 0})`);
setTimeout(() => setToast(''), 3000);
} catch (e) {
console.error(e);
setToast('Failed to load products');
setTimeout(() => setToast(''), 3000);
} finally {
setLoading(false);
}
};
load();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [userId, page, pageSize, searchText, filters.category, filters.subcategory, filters.offer_status]);
/** Dropdown options (use Array.from to avoid TS downlevel iteration issue) */
const categoryOptions = useMemo(() => {
const s = new Set<string>();
items.forEach((i) => i.category && s.add(i.category));
return [''].concat(Array.from(s).sort());
}, [items]);
const subcategoryOptions = useMemo(() => {
const s = new Set<string>();
items.forEach((i) => {
if (!filters.category || i.category === filters.category) {
i.subcategory && s.add(i.subcategory);
}
});
return [''].concat(Array.from(s).sort());
}, [items, filters.category]);
const offerStatusOptions = useMemo(() => {
const s = new Set<string>();
items.forEach((i) => i.offer_status && s.add(i.offer_status));
return [''].concat(Array.from(s).sort());
}, [items]);
/** Client-side filters (in-stock & price floor + search) */
const filteredItems = useMemo(() => {
return items.filter((it) => {
const inv = toNumber(it.inventory);
const price = toNumber(it.price);
if (filters.inStockOnly && inv <= 0) return false;
if (price < filters.priceMin) return false;
if (searchText) {
const hay = `${it.id ?? ''} ${it.sku ?? ''} ${it.name ?? ''} ${it.partNumber ?? ''} ${it.category ?? ''} ${it.subcategory ?? ''}`.toLowerCase();
if (!hay.includes(searchText.toLowerCase())) return false;
}
return true;
});
}, [items, filters.inStockOnly, filters.priceMin, searchText]);
/** Selection helpers */
const selectedCount = useMemo(() => Object.keys(selected).length, [selected]);
const isSelected = (row: ProductRow) => Boolean(selected[row.trand_id]);
const toggleSelect = (row: ProductRow) => {
setSelected((prev) => {
const copy = { ...prev };
if (copy[row.trand_id]) delete copy[row.trand_id];
else copy[row.trand_id] = row;
return copy;
});
};
const toggleSelectAllVisible = () => {
setSelected((prev) => {
const copy = { ...prev };
const allVisibleSelected =
filteredItems.length > 0 &&
filteredItems.every((it) => copy[it.trand_id]);
if (allVisibleSelected) {
filteredItems.forEach((it) => {
if (copy[it.trand_id]) delete copy[it.trand_id];
});
} else {
filteredItems.forEach((it) => (copy[it.trand_id] = it));
}
return copy;
});
};
const clearVisibleSelection = () => {
setSelected((prev) => {
const copy = { ...prev };
filteredItems.forEach((it) => {
if (copy[it.trand_id]) delete copy[it.trand_id];
});
return copy;
});
};
/** Queue selected products (mirrors your other pages payload shape) */
const handleAddSelectedProducts = async () => {
const picked = Object.values(selected);
const queuePayload = picked.map(x => x.offer_offerId)
console.log('Adding products:', queuePayload);
setToast(`Queued ${queuePayload.length} product(s)`);
try {
const res = await fetch(ENDPOINT_QUEUE, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userKey: EBAYSTOREID,
products: queuePayload,
userId,
}),
});
const data = await res.json();
console.log('Queue response:', data);
setToastActive(true);
setTimeout(() => setToastActive(false), 2500);
} catch (e) {
console.error(e);
setToast('Failed to queue products');
}
};
const totalPages = Math.max(1, Math.ceil(total / pageSize));
/** ================== Render ================== */
return (
<div className="bg-gradient-to-br from-[#00d1ff]/10 via-white to-[#00d1ff]/20 min-h-screen">
{/* Sticky Header */}
<div
className={`sticky top-14 z-10 transition-all duration-300 ${isScrolled
? 'bg-white/95 backdrop-blur-md shadow-lg py-3'
: 'bg-white/80 backdrop-blur-sm py-4'
}`}
>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div className="flex flex-col">
<h1 className="text-2xl font-bold bg-[#00d1ff] bg-clip-text text-[#00d1ff]">
Data4Autos Your Listed Products
</h1>
<p className="text-sm text-gray-500 mt-1">
Showing {filteredItems.length} of {items.length} (Total in DB: {total})
</p>
<p className="text-sm font-medium text-[#00d1ff] mt-1">
Selected: <span className="font-semibold">{selectedCount}</span>
</p>
</div>
<div className="flex flex-col sm:flex-row gap-3 items-stretch sm:items-center w-full md:w-auto">
{/* Search */}
<div className="relative flex-grow">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg className="h-5 w-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
clipRule="evenodd"
/>
</svg>
</div>
<input
type="text"
value={searchText}
onChange={(e) => {
setPage(1);
setSearchText(e.target.value);
}}
placeholder="Search (sku, name, id)…"
className="block w-full pl-10 pr-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
/>
</div>
{/* In-stock-only pill toggle */}
<div className="flex flex-wrap items-center gap-3">
<label className="flex items-center gap-2 text-sm font-medium text-gray-700 cursor-pointer select-none">
<div className="relative">
<input
type="checkbox"
checked={filters.inStockOnly}
onChange={() =>
setFilters((f) => ({ ...f, inStockOnly: !f.inStockOnly }))
}
className="sr-only"
/>
<div
className={`block w-10 h-6 rounded-full transition-colors ${filters.inStockOnly ? 'bg-[#00d1ff]' : 'bg-gray-300'
}`}
></div>
<div
className={`absolute left-1 top-1 bg-white w-4 h-4 rounded-full transition-transform ${filters.inStockOnly ? 'transform translate-x-4' : ''
}`}
></div>
</div>
In Stock Only
</label>
<button
onClick={handleAddSelectedProducts}
disabled={selectedCount === 0}
className="px-5 py-2.5 bg-[#00d1ff] to-purple-600 text-white font-medium rounded-lg hover:from-blue-700 hover:to-purple-700 transition-all duration-300 transform hover:-translate-y-0.5 disabled:opacity-50 disabled:transform-none disabled:cursor-not-allowed flex items-center gap-2 shadow-md hover:shadow-lg"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
</svg>
Unlist {selectedCount} Product{selectedCount === 1 ? '' : 's'}
</button>
</div>
</div>
</div>
{/* Global Filters */}
<div className="mt-3 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3">
{/* Category */}
<label className="text-sm text-gray-700 flex items-center">
<span className="mr-2 whitespace-nowrap">Category:</span>
<select
value={filters.category}
onChange={(e) => {
setPage(1);
setFilters((f) => ({
...f,
category: e.target.value,
subcategory: '', // reset when category changes
}));
}}
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 w-full"
>
{categoryOptions.map((c) => (
<option key={c} value={c}>
{c || 'All'}
</option>
))}
</select>
</label>
{/* Subcategory */}
<label className="text-sm text-gray-700 flex items-center">
<span className="mr-2 whitespace-nowrap">Subcategory:</span>
<select
value={filters.subcategory}
onChange={(e) => {
setPage(1);
setFilters((f) => ({ ...f, subcategory: e.target.value }));
}}
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 w-full"
>
{subcategoryOptions.map((s) => (
<option key={s} value={s}>
{s || 'All'}
</option>
))}
</select>
</label>
{/* Offer Status */}
<label className="text-sm text-gray-700 flex items-center">
<span className="mr-2 whitespace-nowrap">Offer Status:</span>
<select
value={filters.offer_status}
onChange={(e) => {
setPage(1);
setFilters((f) => ({ ...f, offer_status: e.target.value }));
}}
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 w-full"
>
{offerStatusOptions.map((s) => (
<option key={s} value={s}>
{s || 'All'}
</option>
))}
</select>
</label>
{/* Price Floor */}
<div className="text-sm text-gray-700">
<div className="mb-1 font-medium">Price Floor</div>
<div className="flex items-center gap-3">
<input
type="range"
min={0}
max={5000}
step={5}
value={filters.priceMin}
onChange={(e) =>
setFilters((f) => ({ ...f, priceMin: toNumber(e.target.value) }))
}
className="w-full"
aria-label="Minimum price"
/>
<input
type="number"
min={0}
value={filters.priceMin}
onChange={(e) =>
setFilters((f) => ({ ...f, priceMin: toNumber(e.target.value) }))
}
className="w-24 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div className="text-xs text-gray-500 mt-1">
Showing items with price ${filters.priceMin}
</div>
</div>
</div>
{/* Pagination Controls */}
<div className="mt-3 flex items-center gap-3 text-sm text-gray-700">
<div>
Page <span className="font-semibold">{page}</span> of{' '}
<span className="font-semibold">{Math.max(1, Math.ceil(total / pageSize))}</span>
</div>
<div className="flex items-center gap-2">
<button
className="px-3 py-1.5 rounded border border-gray-300 hover:bg-gray-50 disabled:opacity-50"
onClick={() => setPage((p) => Math.max(1, p - 1))}
disabled={page <= 1 || loading}
>
Prev
</button>
<button
className="px-3 py-1.5 rounded border border-gray-300 hover:bg-gray-50 disabled:opacity-50"
onClick={() => setPage((p) => p + 1)}
disabled={page >= Math.max(1, Math.ceil(total / pageSize)) || loading}
>
Next
</button>
<label className="ml-3">
<span className="mr-2">Page size</span>
<select
className="border rounded px-2 py-1"
value={pageSize}
onChange={(e) => {
setPage(1);
setPageSize(Math.max(1, Math.min(200, Number(e.target.value) || 48)));
}}
>
{[24, 36, 48, 60, 96, 120, 200].map((n) => (
<option key={n} value={n}>
{n}
</option>
))}
</select>
</label>
</div>
</div>
</div>
</div>
{/* Grid */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pt-6 pb-16">
{loading ? (
<div className="py-20 text-center text-gray-500">Loading products</div>
) : filteredItems.length === 0 ? (
<div className="flex items-center justify-center py-24 bg-gradient-to-b from-gray-50 to-gray-100">
<div className="relative text-center bg-white/80 backdrop-blur-md border border-white/40 shadow-2xl rounded-3xl p-10 transition-all duration-500 hover:scale-[1.02]">
{/* Icon Circle */}
<div className="w-20 h-20 mx-auto flex items-center justify-center rounded-full bg-[#00d1ff] shadow-lg">
<svg
className="w-10 h-10 text-white"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M9 12h6m2 0a8 8 0 11-16 0 8 8 0 0116 0z"
/>
</svg>
</div>
{/* Text */}
<h3 className="mt-6 text-2xl font-semibold text-gray-800 tracking-tight">
No Products Found
</h3>
<p className="mt-3 text-gray-500 max-w-sm mx-auto">
No products match the current filters. Try adjusting your search or filters to find what you need.
</p>
{/* Button */}
{/* <button className="mt-6 px-6 py-3 bg-[#00d1ff] text-white rounded-xl shadow-md hover:shadow-lg transition-transform duration-300 hover:scale-105">
Reset Filters
</button> */}
{/* Decorative Blur Circle */}
<div className="absolute -top-10 -right-10 w-32 h-32 bg-[#00d1ff] rounded-full blur-3xl"></div>
<div className="absolute -bottom-10 -left-10 w-32 h-32 bg-[#00d1ff] rounded-full blur-3xl"></div>
</div>
</div>
) : (
<>
<div className="mb-3 flex items-center justify-between">
<div className="text-xs text-gray-600">
Showing <span className="font-semibold">{filteredItems.length}</span> of{' '}
<span className="font-semibold">{items.length}</span>
</div>
<div className="flex items-center gap-2">
<button
onClick={toggleSelectAllVisible}
className="text-xs px-3 py-1.5 rounded border border-gray-300 hover:bg-gray-50"
>
{filteredItems.length > 0 &&
filteredItems.every((it) => selected[it.trand_id])
? 'Unselect All Visible'
: 'Select All Visible'}
</button>
{Object.values(selected).some((r) =>
filteredItems.find((fi) => fi.trand_id === r.trand_id)
) && (
<button
onClick={clearVisibleSelection}
className="text-xs px-3 py-1.5 rounded border border-gray-300 hover:bg-gray-50"
>
Clear Visible
</button>
)}
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">
{filteredItems.map((it) => {
const img =
it.imgSrc ||
'https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png';
const inv = toNumber(it.inventory);
const price = toNumber(it.price);
const willList = quantityToList(inv);
return (
<div key={it.trand_id} className="border border-gray-200 rounded-lg p-3 hover:shadow-sm bg-white">
{/* Select */}
<div className="flex items-center justify-between mb-2">
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={Boolean(selected[it.trand_id])}
onChange={() => toggleSelect(it)}
className="h-4 w-4 rounded border-gray-300 text-[#00d1ff] focus:ring-blue-500"
/>
<span className="text-xs text-gray-600">Select</span>
</label>
<span className="text-[10px] px-2 py-0.5 rounded-full bg-slate-100 text-slate-700">
Will list: {willList}
</span>
</div>
<img
src={img}
alt={it.name || 'Product'}
className="w-full h-24 object-contain mb-2 bg-gray-50 rounded"
/>
<div className="text-sm font-medium line-clamp-2">{it.name || 'Unnamed Product'}</div>
<div className="text-xs text-gray-600 mt-1">
Part #: <span className="font-medium">{it.partNumber || '-'}</span>
</div>
<div className="text-xs text-gray-600">
{it.category || '-'} &gt; {it.subcategory || '-'}
</div>
<div className="text-xs">
Price:{' '}
<span className="font-semibold">
{Number.isFinite(price) && price > 0 ? `$${price.toFixed(2)}` : '-'}
</span>
</div>
<div className="text-xs text-gray-700">
Inventory: <span className="font-semibold">{inv}</span>
</div>
{it.description && (
<p className="mt-2 text-xs text-gray-700 line-clamp-3">{it.description}</p>
)}
{/* Offer / URL badges */}
<div className="mt-2 flex flex-wrap gap-1">
{it.offer_status && (
<span className="text-[10px] px-2 py-0.5 rounded bg-blue-50 text-blue-700 border border-blue-200">
{it.offer_status}
</span>
)}
{it.offer_url && (
<a
href={it.offer_url}
target="_blank"
rel="noreferrer"
className="text-[10px] px-2 py-0.5 rounded bg-emerald-50 text-emerald-700 border border-emerald-200"
>
Listing
</a>
)}
</div>
</div>
);
})}
</div>
</>
)}
</div>
{/* Toasts */}
{toast && (
<div className="fixed bottom-6 right-6 bg-gradient-to-r from-slate-800 to-black text-white px-6 py-4 rounded-2xl shadow-2xl z-30 animate-fade-in-up">
<div className="flex items-center gap-3">
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
</svg>
<span className="font-medium">{toast}</span>
</div>
</div>
)}
{toastActive && (
<div className="fixed bottom-6 right-6 bg-gradient-to-r from-green-500 to-emerald-600 text-white px-6 py-4 rounded-2xl shadow-2xl z-30 animate-fade-in-up">
<div className="flex items-center gap-3">
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
</svg>
<span className="font-medium">Products Unlisted successfully!</span>
</div>
</div>
)}
{/* Tiny animation helper */}
<style jsx global>{`
@keyframes fadeInUp {
from {
opacity: 0;
transform: translate3d(0, 40px, 0);
}
to {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
.animate-fade-in-up {
animation: fadeInUp 0.5s ease-out;
}
`}</style>
</div>
);
};
export default ManageAddedProducts;

View File

@ -0,0 +1,6 @@
// app/(defaults)/manage-added-products/page.tsx
import ManageAddedProducts from './manage-added-products';
export default function Page() {
return <ManageAddedProducts />;
}

View File

@ -0,0 +1,680 @@
// // app/brands/BrandsClient.tsx
// 'use client';
// import React, { useState, useEffect } from 'react';
// type Brand = {
// id: string;
// name: string;
// logo?: string;
// dropship: boolean;
// };
// export default function BrandsClient({ brands }: { brands: Brand[] }) {
// const [search, setSearch] = useState('');
// const [selectedIds, setSelectedIds] = useState<string[]>([]);
// const [toast, setToast] = useState('');
// const [isScrolled, setIsScrolled] = useState(false);
// const [showDropshipOnly, setShowDropshipOnly] = useState(false);
// useEffect(() => {
// const handleScroll = () => {
// setIsScrolled(window.scrollY > 10);
// };
// window.addEventListener('scroll', handleScroll);
// return () => window.removeEventListener('scroll', handleScroll);
// }, []);
// const filteredBrands = brands.filter((b) => {
// const matchesSearch = b.name.toLowerCase().includes(search.toLowerCase());
// const matchesDropship = showDropshipOnly ? b.dropship : true;
// return matchesSearch && matchesDropship;
// });
// const allFilteredSelected = filteredBrands.length > 0 && filteredBrands.every((b) => selectedIds.includes(b.id));
// const toggleSelect = (id: string) => {
// setSelectedIds((prev) => (prev.includes(id) ? prev.filter((i) => i !== id) : [...prev, id]));
// };
// const toggleSelectAll = () => {
// console.log('Toggling select all', filteredBrands);
// const ids = filteredBrands.map((b) => b.id);
// if (allFilteredSelected) {
// setSelectedIds((prev) => prev.filter((id) => !ids.includes(id)));
// } else {
// setSelectedIds((prev) => Array.from(new Set([...prev, ...ids])));
// }
// };
// const getSelectedStatusText = () => {
// if (selectedIds.length === 0) return 'No brands selected';
// if (selectedIds.length === 1) return '1 brand selected';
// return `${selectedIds.length} brands selected`;
// };
// const USER_ID = '6ac40b29-8c50-4800-9ece-62b44b69019a'; // static user id
// const handleSave = async () => {
// const payload = {
// userid: USER_ID,
// brands: brands
// .filter((b) => selectedIds.includes(b.id))
// .map((b) => ({
// id: b.id,
// name: b.name,
// logo: b.logo,
// dropship: b.dropship,
// })),
// };
// try {
// const res = await fetch('https://ebay.backend.data4autos.com/api/brands/bulk-insert', {
// method: 'POST',
// headers: {
// 'Content-Type': 'application/json',
// },
// body: JSON.stringify(payload),
// });
// const data = await res.json();
// console.log('Save response:', data);
// // Show beautiful message
// setToast(`${data.message} (Code: ${data.code}, User: ${data.userid})`);
// setTimeout(() => setToast(''), 4000);
// } catch (error) {
// console.error('Error saving brands:', error);
// setToast('Failed to save collections. Please try again.');
// setTimeout(() => setToast(''), 4000);
// }
// };
// // Function to generate filter status text
// const getFilterStatusText = () => {
// if (filteredBrands.length === brands.length && !search && !showDropshipOnly) {
// return `Showing all ${brands.length} brands`;
// }
// let status = `Showing ${filteredBrands.length} of ${brands.length} brands`;
// if (search && showDropshipOnly) {
// status += ` matching "${search}" and dropship only`;
// } else if (search) {
// status += ` matching "${search}"`;
// } else if (showDropshipOnly) {
// status += ` (dropship only)`;
// }
// return status;
// };
// return (
// <div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-200">
// {/* Enhanced Fixed Header */}
// <div className={`fixed top-0 left-0 w-full z-20 transition-all duration-300 ${isScrolled ? 'bg-white/95 backdrop-blur-md shadow-lg py-3' : 'bg-white/80 backdrop-blur-sm py-4'}`}>
// <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
// <div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
// <div className="flex flex-col">
// <h1 className="text-2xl font-bold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">Data4Autos Turn14 Brands</h1>
// <p className="text-sm text-gray-500 mt-1">{getFilterStatusText()}</p>
// <p className="text-sm font-medium text-blue-600 mt-1">{getSelectedStatusText()}</p>
// </div>
// <div className="flex flex-col sm:flex-row gap-3 items-stretch sm:items-center">
// <div className="relative flex-grow">
// <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
// <svg className="h-5 w-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
// <path
// fillRule="evenodd"
// d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
// clipRule="evenodd"
// />
// </svg>
// </div>
// <input
// type="text"
// value={search}
// onChange={(e) => setSearch(e.target.value)}
// placeholder="Search brands…"
// className="block w-full pl-10 pr-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
// />
// </div>
// <div className="flex flex-wrap items-center gap-3">
// <label className="flex items-center gap-2 text-sm font-medium text-gray-700 cursor-pointer select-none">
// <div className="relative">
// <input type="checkbox" checked={showDropshipOnly} onChange={() => setShowDropshipOnly(!showDropshipOnly)} className="sr-only" />
// <div className={`block w-10 h-6 rounded-full transition-colors ${showDropshipOnly ? 'bg-blue-600' : 'bg-gray-300'}`}></div>
// <div className={`absolute left-1 top-1 bg-white w-4 h-4 rounded-full transition-transform ${showDropshipOnly ? 'transform translate-x-4' : ''}`}></div>
// </div>
// Dropship Only
// </label>
// <label className="flex items-center gap-2 text-sm font-medium text-gray-700 cursor-pointer select-none">
// <div className="relative">
// <input type="checkbox" checked={allFilteredSelected} onChange={toggleSelectAll} className="sr-only" />
// <div className={`block w-10 h-6 rounded-full transition-colors ${allFilteredSelected ? 'bg-blue-600' : 'bg-gray-300'}`}></div>
// <div className={`absolute left-1 top-1 bg-white w-4 h-4 rounded-full transition-transform ${allFilteredSelected ? 'transform translate-x-4' : ''}`}></div>
// </div>
// Select All
// </label>
// <button
// onClick={handleSave}
// className="px-5 py-2.5 bg-gradient-to-r from-blue-600 to-purple-600 text-white font-medium rounded-lg hover:from-blue-700 hover:to-purple-700 transition-all duration-300 transform hover:-translate-y-0.5 disabled:opacity-50 disabled:transform-none disabled:cursor-not-allowed flex items-center gap-2 shadow-md hover:shadow-lg"
// >
// <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
// <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
// </svg>
// Save Collections
// </button>
// </div>
// </div>
// </div>
// </div>
// </div>
// {/* Brand Grid */}
// <div className="pt-32 pb-12 px-4 sm:px-6 lg:px-8 max-w-7xl mx-auto">
// {filteredBrands.length > 0 ? (
// <div className="grid grid-cols-1 xs:grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-5">
// {/* {filteredBrands.map((brand, index) => ( */}
// {[...filteredBrands]
// .sort((a, b) => {
// const aSelected = selectedIds.includes(a.id);
// const bSelected = selectedIds.includes(b.id);
// if (aSelected && !bSelected) return -1;
// if (!aSelected && bSelected) return 1;
// return 0;
// })
// .map((brand, index) => (
// <div
// key={brand.id}
// className="bg-white rounded-xl shadow-md overflow-hidden hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1.5 relative group"
// style={{ animationDelay: `${index * 0.05}s` }}
// onClick={() => toggleSelect(brand.id)}
// >
// <div className="absolute top-3 right-3 z-10">
// <label className="inline-flex items-center">
// <input type="checkbox" checked={selectedIds.includes(brand.id)} onChange={() => toggleSelect(brand.id)} className="absolute opacity-0 h-0 w-0" />
// <span
// className={`checkmark w-6 h-6 rounded-md border-2 flex items-center justify-center transition-all ${selectedIds.includes(brand.id) ? 'bg-blue-600 border-blue-600' : 'bg-white border-gray-300 group-hover:border-blue-400'}`}
// >
// {selectedIds.includes(brand.id) && (
// <svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
// <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="3" d="M5 13l4 4L19 7"></path>
// </svg>
// )}
// </span>
// </label>
// </div>
// {brand.dropship && (
// <div className="absolute top-3 left-3 z-10">
// <span className="inline-flex items-center px-2 py-1 bg-green-100 text-green-800 text-xs font-medium rounded-md">Dropship</span>
// </div>
// )}
// <div className="p-5 flex flex-col items-center h-full">
// <div className="w-28 h-28 flex items-center justify-center p-2 bg-gray-50 rounded-lg mb-4">
// <img
// src={brand.logo || 'https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png'}
// alt={brand.name}
// className="max-w-full max-h-full object-contain"
// />
// </div>
// <p className="text-center font-medium text-gray-800 mt-auto">{brand.name}</p>
// </div>
// </div>
// ))}
// </div>
// ) : (
// <div className="text-center py-20">
// <div className="inline-block p-4 bg-white rounded-xl shadow-md">
// <svg className="w-16 h-16 mx-auto text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
// <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
// </svg>
// <h3 className="mt-4 text-xl font-medium text-gray-700">No brands found</h3>
// <p className="mt-2 text-gray-500">Try adjusting your search query or filter settings</p>
// </div>
// </div>
// )}
// </div>
// {/* Enhanced Toast Notification */}
// {/* {toast && (
// <div className="fixed bottom-6 right-6 bg-green-600 text-white px-6 py-3 rounded-xl shadow-lg z-30 animate-fade-in-up">
// <div className="flex items-center gap-3">
// <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
// <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
// </svg>
// <span>{toast}</span>
// </div>
// </div>
// )} */}
// {toast && (
// <div className="fixed bottom-6 right-6 bg-gradient-to-r from-green-500 to-emerald-600 text-white px-6 py-4 rounded-2xl shadow-2xl z-30 animate-fade-in-up">
// <div className="flex items-center gap-3">
// <svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
// <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
// </svg>
// <span className="font-medium">{toast}</span>
// </div>
// </div>
// )}
// {/* Add custom animations */}
// <style jsx global>{`
// @keyframes fadeInUp {
// from {
// opacity: 0;
// transform: translate3d(0, 40px, 0);
// }
// to {
// opacity: 1;
// transform: translate3d(0, 0, 0);
// }
// }
// .animate-fade-in-up {
// animation: fadeInUp 0.5s ease-out;
// }
// .checkmark {
// transition: all 0.2s ease;
// }
// `}</style>
// </div>
// );
// }
'use client';
import { getAccessToken_client } from '@/utils/apiHelper_client';
import axios from 'axios';
import { useRouter } from 'next/navigation';
import React, { useState, useEffect } from 'react';
type Brand = {
id: string;
name: string;
logo?: string;
dropship: boolean;
};
async function fetchBrands(accessToken: string): Promise<Brand[]> {
const resp = await fetch('https://turn14.data4autos.com/v1/brands', {
headers: { Authorization: `Bearer ${accessToken}` },
cache: 'no-store',
});
if (!resp.ok) {
throw new Error(`Failed to fetch brands: ${resp.statusText}`);
}
const data = await resp.json();
return data.data || [];
}
export default function BrandsClient() {
const router = useRouter()
const [brands, setBrands] = useState<Brand[]>([]);
const [search, setSearch] = useState('');
const [selectedIds, setSelectedIds] = useState<string[]>([]);
const [toast, setToast] = useState('');
const [isScrolled, setIsScrolled] = useState(false);
const [showDropshipOnly, setShowDropshipOnly] = useState(false);
const [payment, setPayment] = useState<any>(null);
const userId = sessionStorage.getItem('USERID');
useEffect(() => {
const role = localStorage.getItem("user_role");
const sessionId = localStorage.getItem("payment_session");
// ✅ Admins and Partners can access directly (skip payment check)
if (role === "admin" || role === "partner") {
return;
}
// 🚫 If no payment session, redirect to pricing
if (!sessionId) {
router.push("/pricing");
return;
}
// ✅ Otherwise, check payment details
const fetchPaymentDetails = async () => {
try {
const res: any = await axios.get(
"https://ebay.backend.data4autos.com/api/payment/details",
{ params: { session_id: sessionId } }
);
setPayment(res.data.payment);
} catch (err) {
console.error("Error fetching payment details:", err);
}
};
fetchPaymentDetails();
}, [router]);
useEffect(() => {
const fetchUserBrands = async () => {
try {
//console.log('Fetching access token...'); // Debugging line
const accessToken = await getAccessToken_client();
//console.log('Access Token:', accessToken); // Debugging line
const brands = accessToken ? await fetchBrands(accessToken) : [];
setBrands(brands);
const res = await fetch(`https://ebay.backend.data4autos.com/api/brands/${userId}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
// Optionally add Authorization: `Bearer ${accessToken}` if needed
},
});
const data = await res.json();
console.log('GET response:', data);
// Extract selected brand IDs from the response
const userSelectedIds = data.map((b: any) => String(b.brandid)); // brandid from your response
setSelectedIds(userSelectedIds);
// Optional: show toast
setToast(`Loaded ${userSelectedIds.length} selected brands`);
setTimeout(() => setToast(''), 4000);
} catch (error) {
console.error('Error fetching brands:', error);
setToast('Failed to load user brands');
setTimeout(() => setToast(''), 4000);
}
};
fetchUserBrands();
}, []);
useEffect(() => {
const handleScroll = () => {
setIsScrolled(window.scrollY > 10);
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
const filteredBrands = brands.filter((b) => {
const matchesSearch = b.name.toLowerCase().includes(search.toLowerCase());
const matchesDropship = showDropshipOnly ? b.dropship : true;
return matchesSearch && matchesDropship;
});
const allFilteredSelected = filteredBrands.length > 0 && filteredBrands.every((b) => selectedIds.includes(b.id));
const toggleSelect = (id: string) => {
setSelectedIds((prev) => (prev.includes(id) ? prev.filter((i) => i !== id) : [...prev, id]));
};
const toggleSelectAll = () => {
const ids = filteredBrands.map((b) => b.id);
if (allFilteredSelected) {
setSelectedIds((prev) => prev.filter((id) => !ids.includes(id)));
} else {
setSelectedIds((prev) => Array.from(new Set([...prev, ...ids])));
}
};
const getSelectedStatusText = () => {
if (selectedIds.length === 0) return 'No brands selected';
if (selectedIds.length === 1) return '1 brand selected';
return `${selectedIds.length} brands selected`;
};
//const userId = sessionStorage.getItem('USERID'); // dynamic user id
const handleSave = async () => {
const payload = {
userid: userId,
brands: brands
.filter((b) => selectedIds.includes(b.id))
.map((b) => ({
id: b.id,
name: b.name,
logo: b.logo,
dropship: b.dropship,
})),
};
try {
const res = await fetch('https://ebay.backend.data4autos.com/api/brands/bulk-insert', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});
const data = await res.json();
setToast(`${data.message} (Code: ${data.code}, User: ${data.userid})`);
setTimeout(() => setToast(''), 4000);
} catch (error) {
console.error('Error saving brands:', error);
setToast('Failed to save collections. Please try again.');
setTimeout(() => setToast(''), 4000);
}
};
const getFilterStatusText = () => {
if (filteredBrands.length === brands.length && !search && !showDropshipOnly) {
return `Showing all ${brands.length} brands`;
}
let status = `Showing ${filteredBrands.length} of ${brands.length} brands`;
if (search && showDropshipOnly) {
status += ` matching "${search}" and dropship only`;
} else if (search) {
status += ` matching "${search}"`;
} else if (showDropshipOnly) {
status += ` (dropship only)`;
}
return status;
};
return (
<div className="bg-gradient-to-br from-[#00d1ff]/10 via-white to-[#00d1ff]/20">
{/* Sticky Header (below default Header) */}
<div
className={`sticky top-14 z-10 transition-all duration-300 ${isScrolled ? 'bg-white/95 backdrop-blur-md shadow-lg py-3' : 'bg-white/80 backdrop-blur-sm py-4'
}`}
>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div className="flex flex-col">
<h1 className="text-2xl font-bold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-[#00d1ff]">
Data4Autos Turn14 Brands
</h1>
<p className="text-sm text-gray-500 mt-1">{getFilterStatusText()}</p>
<p className="text-sm font-medium text-[#00d1ff] mt-1">{getSelectedStatusText()}</p>
</div>
<div className="flex flex-col sm:flex-row gap-3 items-stretch sm:items-center">
<div className="relative flex-grow">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg className="h-5 w-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
clipRule="evenodd"
/>
</svg>
</div>
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search brands…"
className="block w-full pl-10 pr-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
/>
</div>
<div className="flex flex-wrap items-center gap-3">
<label className="flex items-center gap-2 text-sm font-medium text-gray-700 cursor-pointer select-none">
<div className="relative">
<input
type="checkbox"
checked={showDropshipOnly}
onChange={() => setShowDropshipOnly(!showDropshipOnly)}
className="sr-only"
/>
<div className={`block w-10 h-6 rounded-full transition-colors ${showDropshipOnly ? 'bg-[#00d1ff]' : 'bg-gray-300'}`}></div>
<div
className={`absolute left-1 top-1 bg-white w-4 h-4 rounded-full transition-transform ${showDropshipOnly ? 'transform translate-x-4' : ''
}`}
></div>
</div>
Dropship Only
</label>
<label className="flex items-center gap-2 text-sm font-medium text-gray-700 cursor-pointer select-none">
<div className="relative">
<input type="checkbox" checked={allFilteredSelected} onChange={toggleSelectAll} className="sr-only" />
<div className={`block w-10 h-6 rounded-full transition-colors ${allFilteredSelected ? 'bg-[#00d1ff]' : 'bg-gray-300'}`}></div>
<div
className={`absolute left-1 top-1 bg-white w-4 h-4 rounded-full transition-transform ${allFilteredSelected ? 'transform translate-x-4' : ''
}`}
></div>
</div>
Select All
</label>
<button
onClick={handleSave}
className="px-5 py-2.5 bg-[#00d1ff] text-white font-medium rounded-lg hover:from-blue-700 hover:to-purple-700 transition-all duration-300 transform hover:-translate-y-0.5 disabled:opacity-50 disabled:transform-none disabled:cursor-not-allowed flex items-center gap-2 shadow-md hover:shadow-lg"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
</svg>
Save Collections
</button>
</div>
</div>
</div>
</div>
</div>
{/* Brand Grid */}
<div className={`pb-12 px-4 sm:px-6 lg:px-8 max-w-7xl mx-auto ${isScrolled ? 'pt-[100px]' : 'pt-16'}`}>
{filteredBrands.length > 0 ? (
<div className="grid grid-cols-1 xs:grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-5">
{[...filteredBrands]
.sort((a, b) => {
const aSelected = selectedIds.includes(a.id);
const bSelected = selectedIds.includes(b.id);
if (aSelected && !bSelected) return -1;
if (!aSelected && bSelected) return 1;
return 0;
})
.map((brand, index) => (
<div
key={brand.id}
className="bg-white rounded-xl shadow-md overflow-hidden hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1.5 relative group"
style={{ animationDelay: `${index * 0.05}s` }}
onClick={() => toggleSelect(brand.id)}
>
<div className="absolute top-3 right-3 z-10">
<label className="inline-flex items-center">
<input
type="checkbox"
checked={selectedIds.includes(brand.id)}
onChange={() => toggleSelect(brand.id)}
className="absolute opacity-0 h-0 w-0"
/>
<span
className={`checkmark w-6 h-6 rounded-md border-2 flex items-center justify-center transition-all ${selectedIds.includes(brand.id) ? 'bg-[#00d1ff] border-[#00d1ff]' : 'bg-white border-gray-300 group-hover:border-blue-400'
}`}
>
{selectedIds.includes(brand.id) && (
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="3" d="M5 13l4 4L19 7"></path>
</svg>
)}
</span>
</label>
</div>
{brand.dropship && (
<div className="absolute top-3 left-3 z-10">
<span className="inline-flex items-center px-2 py-1 bg-green-100 text-green-800 text-xs font-medium rounded-md">Dropship</span>
</div>
)}
<div className="p-5 flex flex-col items-center h-full">
<div className="w-28 h-28 flex items-center justify-center p-2 bg-gray-50 rounded-lg mb-4">
<img
src={brand.logo || 'https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png'}
alt={brand.name}
className="max-w-full max-h-full object-contain"
/>
</div>
<p className="text-center font-medium text-gray-800 mt-auto">{brand.name}</p>
<p className="text-center font-medium text-gray-800 mt-auto">ID : {brand.id}</p>
</div>
</div>
))}
</div>
) : (
<div className="text-center py-20">
<div className="inline-block p-4 bg-white rounded-xl shadow-md">
<svg className="w-16 h-16 mx-auto text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<h3 className="mt-4 text-xl font-medium text-gray-700">No brands found</h3>
<p className="mt-2 text-gray-500">Try adjusting your search query or filter settings</p>
</div>
</div>
)}
</div>
{/* Toast Notification */}
{toast && (
<div className="fixed bottom-6 right-6 bg-gradient-to-r from-green-500 to-emerald-600 text-white px-6 py-4 rounded-2xl shadow-2xl z-30 animate-fade-in-up">
<div className="flex items-center gap-3">
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
</svg>
<span className="font-medium">{toast}</span>
</div>
</div>
)}
{/* Custom Animations */}
<style jsx global>{`
@keyframes fadeInUp {
from {
opacity: 0;
transform: translate3d(0, 40px, 0);
}
to {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
.animate-fade-in-up {
animation: fadeInUp 0.5s ease-out;
}
.checkmark {
transition: all 0.2s ease;
}
`}</style>
</div>
);
}

View File

@ -0,0 +1,436 @@
'use client';
import { getAccessToken_client } from '@/utils/apiHelper_client';
import axios from 'axios';
import { useRouter } from 'next/navigation';
import React, { useState, useEffect } from 'react';
type Brand = { id: string; name: string; logo?: string; dropship: boolean; };
type BrandIndividual = {
name: string;
};
type BrandResponse = {
meta: {
eta: string;
count: number;
generatedAt: string;
};
data: string[]; // API returns a list of brand names
};
async function fetchBrands(): Promise<BrandResponse> {
const resp = await fetch('https://motorstate.data4autos.com/api/data/brands', {
cache: 'no-store',
});
if (!resp.ok) {
throw new Error(`Failed to fetch brands: ${resp.statusText}`);
}
const data = await resp.json();
return data as BrandResponse;
}
export default function BrandsClient() {
const router = useRouter()
const [brands, setBrands] = useState<BrandIndividual[]>([]);
const [search, setSearch] = useState('');
const [selectedIds, setSelectedIds] = useState<string[]>([]);
const [toast, setToast] = useState('');
const [isScrolled, setIsScrolled] = useState(false);
const [showDropshipOnly, setShowDropshipOnly] = useState(false);
const [payment, setPayment] = useState<any>(null);
const userId = sessionStorage.getItem('USERID');
interface BrandLogoMap {
[key: string]: {
logo: string;
};
}
const [brandLogos, setBrandLogos] = useState<BrandLogoMap>({});
useEffect(() => {
fetch("/data/brandMap.json")
.then(res => res.json())
.then(data => {
const normalized: BrandLogoMap = {};
Object.keys(data).forEach(key => {
const cleanKey = key.trim().toLowerCase();
normalized[cleanKey] = data[key];
});
setBrandLogos(normalized);
});
}, []);
useEffect(() => {
const role = localStorage.getItem("user_role");
const sessionId = localStorage.getItem("payment_session");
// ✅ Admins and Partners can access directly (skip payment check)
if (role === "admin" || role === "partner") {
return;
}
// 🚫 If no payment session, redirect to pricing
if (!sessionId) {
router.push("/pricing");
return;
}
// ✅ Otherwise, check payment details
const fetchPaymentDetails = async () => {
try {
const res: any = await axios.get(
"https://ebay.backend.data4autos.com/api/payment/details",
{ params: { session_id: sessionId } }
);
setPayment(res.data.payment);
} catch (err) {
console.error("Error fetching payment details:", err);
}
};
fetchPaymentDetails();
}, [router]);
useEffect(() => {
const fetchUserBrands = async () => {
try {
//console.log('Fetching access token...'); // Debugging line
const accessToken = await getAccessToken_client();
//console.log('Access Token:', accessToken); // Debugging line
const brandResponse = await fetchBrands();
// Convert string[] → BrandIndividual[]
const mappedBrands: BrandIndividual[] = brandResponse.data.map(
(name) => ({ name })
);
setBrands(mappedBrands);
const res = await fetch(`https://ebay.backend.data4autos.com/api/motorstate/brands/${userId}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
// Optionally add Authorization: `Bearer ${accessToken}` if needed
},
});
const data = await res.json();
console.log('GET response:', data);
// Extract selected brand IDs from the response
const userSelectedIds = data.map((b: any) => String(b.name)); // brandid from your response
setSelectedIds(userSelectedIds);
// Optional: show toast
setToast(`Loaded ${userSelectedIds.length} selected brands`);
setTimeout(() => setToast(''), 4000);
} catch (error) {
console.error('Error fetching brands:', error);
setToast('Failed to load user brands');
setTimeout(() => setToast(''), 4000);
}
};
fetchUserBrands();
}, []);
useEffect(() => {
const handleScroll = () => {
setIsScrolled(window.scrollY > 10);
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
const filteredBrands = brands.filter((b) => {
const matchesSearch = b.name.toLowerCase().includes(search.toLowerCase());
const matchesDropship = showDropshipOnly ? b.name : true;
return matchesSearch && matchesDropship;
});
const allFilteredSelected = filteredBrands.length > 0 && filteredBrands.every((b) => selectedIds.includes(b.name));
const toggleSelect = (id: string) => {
setSelectedIds((prev) => (prev.includes(id) ? prev.filter((i) => i !== id) : [...prev, id]));
};
const toggleSelectAll = () => {
const ids = filteredBrands.map((b) => b.name);
if (allFilteredSelected) {
setSelectedIds((prev) => prev.filter((id) => !ids.includes(id)));
} else {
setSelectedIds((prev) => Array.from(new Set([...prev, ...ids])));
}
};
const getSelectedStatusText = () => {
if (selectedIds.length === 0) return 'No brands selected';
if (selectedIds.length === 1) return '1 brand selected';
return `${selectedIds.length} brands selected`;
};
//const userId = sessionStorage.getItem('USERID'); // dynamic user id
const handleSave = async () => {
const payload = {
userid: userId,
brands: brands
.filter((b) => selectedIds.includes(b.name))
.map((b) => ({
id: b.name,
name: b.name,
})),
};
try {
const res = await fetch('https://ebay.backend.data4autos.com/api/motorstate/brands/bulk-insert', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});
const data = await res.json();
setToast(`${data.message} (Code: ${data.code}, User: ${data.userid})`);
setTimeout(() => setToast(''), 4000);
} catch (error) {
console.error('Error saving brands:', error);
setToast('Failed to save collections. Please try again.');
setTimeout(() => setToast(''), 4000);
}
};
const getFilterStatusText = () => {
if (filteredBrands.length === brands.length && !search && !showDropshipOnly) {
return `Showing all ${brands.length} brands`;
}
let status = `Showing ${filteredBrands.length} of ${brands.length} brands`;
if (search && showDropshipOnly) {
status += ` matching "${search}" and dropship only`;
} else if (search) {
status += ` matching "${search}"`;
} else if (showDropshipOnly) {
status += ` (dropship only)`;
}
return status;
};
return (
<div className="bg-gradient-to-br from-[#00d1ff]/10 via-white to-[#00d1ff]/20">
{/* Sticky Header (below default Header) */}
<div
className={`sticky top-14 z-10 transition-all duration-300 ${isScrolled ? 'bg-white/95 backdrop-blur-md shadow-lg py-3' : 'bg-white/80 backdrop-blur-sm py-4'
}`}
>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div className="flex flex-col">
<h1 className="text-2xl font-bold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-[#00d1ff]">
Data4Autos MotorState Brands
</h1>
<p className="text-sm text-gray-500 mt-1">{getFilterStatusText()}</p>
<p className="text-sm font-medium text-[#00d1ff] mt-1">{getSelectedStatusText()}</p>
</div>
<div className="flex flex-col sm:flex-row gap-3 items-stretch sm:items-center">
<div className="relative flex-grow">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg className="h-5 w-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
clipRule="evenodd"
/>
</svg>
</div>
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search brands…"
className="block w-full pl-10 pr-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
/>
</div>
<div className="flex flex-wrap items-center gap-3">
{/* <label className="flex items-center gap-2 text-sm font-medium text-gray-700 cursor-pointer select-none">
<div className="relative">
<input
type="checkbox"
checked={showDropshipOnly}
onChange={() => setShowDropshipOnly(!showDropshipOnly)}
className="sr-only"
/>
<div className={`block w-10 h-6 rounded-full transition-colors ${showDropshipOnly ? 'bg-[#00d1ff]' : 'bg-gray-300'}`}></div>
<div
className={`absolute left-1 top-1 bg-white w-4 h-4 rounded-full transition-transform ${showDropshipOnly ? 'transform translate-x-4' : ''
}`}
></div>
</div>
Dropship Only
</label> */}
<label className="flex items-center gap-2 text-sm font-medium text-gray-700 cursor-pointer select-none">
<div className="relative">
<input type="checkbox" checked={allFilteredSelected} onChange={toggleSelectAll} className="sr-only" />
<div className={`block w-10 h-6 rounded-full transition-colors ${allFilteredSelected ? 'bg-[#00d1ff]' : 'bg-gray-300'}`}></div>
<div
className={`absolute left-1 top-1 bg-white w-4 h-4 rounded-full transition-transform ${allFilteredSelected ? 'transform translate-x-4' : ''
}`}
></div>
</div>
Select All
</label>
<button
onClick={handleSave}
className="px-5 py-2.5 bg-[#00d1ff] text-white font-medium rounded-lg hover:from-blue-700 hover:to-purple-700 transition-all duration-300 transform hover:-translate-y-0.5 disabled:opacity-50 disabled:transform-none disabled:cursor-not-allowed flex items-center gap-2 shadow-md hover:shadow-lg"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
</svg>
Save Collections
</button>
</div>
</div>
</div>
</div>
</div>
{/* Brand Grid */}
<div className={`pb-12 px-4 sm:px-6 lg:px-8 max-w-7xl mx-auto ${isScrolled ? 'pt-[100px]' : 'pt-16'}`}>
{filteredBrands.length > 0 ? (
<div className="grid grid-cols-1 xs:grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-5">
{[...filteredBrands]
.sort((a, b) => {
const aSelected = selectedIds.includes(a.name);
const bSelected = selectedIds.includes(b.name);
if (aSelected && !bSelected) return -1;
if (!aSelected && bSelected) return 1;
return 0;
})
.map((brand, index) => (
<div
key={brand.name}
className="bg-white rounded-xl shadow-md overflow-hidden hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1.5 relative group"
style={{ animationDelay: `${index * 0.05}s` }}
onClick={() => toggleSelect(brand.name)}
>
<div className="absolute top-3 right-3 z-10">
<label className="inline-flex items-center">
<input
type="checkbox"
checked={selectedIds.includes(brand.name)}
onChange={() => toggleSelect(brand.name)}
className="absolute opacity-0 h-0 w-0"
/>
<span
className={`checkmark w-6 h-6 rounded-md border-2 flex items-center justify-center transition-all ${selectedIds.includes(brand.name) ? 'bg-[#00d1ff] border-[#00d1ff]' : 'bg-white border-gray-300 group-hover:border-blue-400'
}`}
>
{selectedIds.includes(brand.name) && (
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="3" d="M5 13l4 4L19 7"></path>
</svg>
)}
</span>
</label>
</div>
<div className="p-5 flex flex-col items-center h-full">
<div className="w-28 h-28 flex items-center justify-center p-2 bg-gray-50 rounded-lg mb-4">
{/* <img
src={brand.name || 'https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png'}
alt={brand.name}
className="max-w-full max-h-full object-contain"
/> */}
<img
src={
brandLogos[brand.name.trim().toLowerCase()]?.logo ??
"https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png"
}
alt={brand.name}
className="max-w-full max-h-full object-contain"
/>
</div>
<p className="text-center font-medium text-gray-800 mt-auto">{brand.name}</p>
</div>
</div>
))}
</div>
) : (
<div className="text-center py-20">
<div className="inline-block p-4 bg-white rounded-xl shadow-md">
<svg className="w-16 h-16 mx-auto text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<h3 className="mt-4 text-xl font-medium text-gray-700">No brands found</h3>
<p className="mt-2 text-gray-500">Try adjusting your search query or filter settings</p>
</div>
</div>
)}
</div>
{/* Toast Notification */}
{toast && (
<div className="fixed bottom-6 right-6 bg-gradient-to-r from-green-500 to-emerald-600 text-white px-6 py-4 rounded-2xl shadow-2xl z-30 animate-fade-in-up">
<div className="flex items-center gap-3">
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
</svg>
<span className="font-medium">{toast}</span>
</div>
</div>
)}
{/* Custom Animations */}
<style jsx global>{`
@keyframes fadeInUp {
from {
opacity: 0;
transform: translate3d(0, 40px, 0);
}
to {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
.animate-fade-in-up {
animation: fadeInUp 0.5s ease-out;
}
.checkmark {
transition: all 0.2s ease;
}
`}</style>
</div>
);
}

View File

@ -0,0 +1,13 @@
// import { Metadata } from 'next';
// import React from 'react';
// export const metadata: Metadata = {
// title: 'Sales Admin',
// };
// const Sales = () => {
// return <div>starter page</div>;
// };
// export default Sales;

View File

@ -0,0 +1,90 @@
// // app/(defaults)/manage-brands/page.tsx
// import { Metadata } from 'next';
// import { getAccessToken } from '@/utils/apiHelper';
// import BrandsClient from './BrandsClient';
// export const metadata: Metadata = {
// title: 'Brands Admin',
// };
// type Brand = {
// id: string;
// name: string;
// logo?: string;
// dropship: boolean;
// };
// async function fetchBrands(accessToken: string): Promise<Brand[]> {
// const resp = await fetch('https://turn14.data4autos.com/v1/brands', {
// headers: { Authorization: `Bearer ${accessToken}` },
// cache: 'no-store',
// });
// if (!resp.ok) {
// throw new Error(`Failed to fetch brands: ${resp.statusText}`);
// }
// const data = await resp.json();
// return data.data || [];
// }
// export default async function BrandsPage() {
// try {
// const accessToken = await getAccessToken();
// const brands = accessToken ? await fetchBrands(accessToken) : [];
// return <BrandsClient brands={brands} />;
// } catch (error) {
// console.error('Error loading brands:', error);
// // You might want to handle this error more gracefully in your application
// return (
// <div>
// <div className="min-h-screen flex items-center justify-center bg-gray-100">
// <div className="bg-white p-8 rounded-xl shadow-md max-w-md w-full text-center">
// <h2 className="text-2xl font-bold text-red-600 mb-4">Error Loading Brands</h2>
// <p className="text-gray-600 mb-6">We encountered a problem while loading the brands. Please try again later.</p>
// <button onClick={() => window.location.reload()} className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
// Try Again
// </button>
// </div>
// </div>
// </div>
// );
// }
// }
// app/(defaults)/manage-brands/page.tsx
import { Metadata } from 'next';
import { getAccessToken } from '@/utils/apiHelper';
import BrandsClient from './BrandsClient';
export const metadata: Metadata = {
title: 'Brands Admin',
};
export default async function BrandsPage() {
try {
console.log()
const accessToken = await getAccessToken();
console.log('Access Token:', accessToken); // Debugging line
return <BrandsClient />;
} catch (error) {
console.error('Error loading brands:', error);
return (
<div className="p-8 flex items-center justify-center">
<div className="bg-white p-8 rounded-xl shadow-md max-w-md w-full text-center">
<h2 className="text-2xl font-bold text-red-600 mb-4">Error Loading Brands</h2>
<p className="text-gray-600 mb-6">We encountered a problem while loading the brands. Please try again later.</p>
<button
onClick={() => window.location.reload()}
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
Try Again
</button>
</div>
</div>
);
}
}

View File

@ -0,0 +1,966 @@
// pages/manage-brand-products.tsx
'use client';
import { getAccessToken } from '@/utils/apiHelper';
import { getAccessToken_client } from '@/utils/apiHelper_client';
import axios from 'axios';
import { useRouter } from 'next/navigation';
import React, { useEffect, useMemo, useState } from 'react';
interface Brand {
id: string;
brandid: string;
name: string;
logo?: string;
}
interface Item {
id: string;
attributes: {
product_name?: string;
brand?: string;
brand_id?: number | string;
thumbnail?: string;
part_number?: string;
category?: string;
subcategory?: string;
price?: string; // may come as string; we parse below
part_description?: string;
regular_stock?: boolean;
ltl_freight_required?: boolean;
is_clearance_item?: boolean;
is_air_freight_prohibited?: boolean;
files?: { url: string }[];
fitmmentTags?: any;
};
inventoryQuantity?: number;
}
const makes_list_raw = [
'Alfa Romeo',
'Ferrari',
'Dodge',
'Subaru',
'Toyota',
'Volkswagen',
'Volvo',
'Audi',
'BMW',
'Buick',
'Cadillac',
'Chevrolet',
'Chrysler',
'CX Automotive',
'Nissan',
'Ford',
'Hyundai',
'Infiniti',
'Lexus',
'Mercury',
'Mazda',
'Oldsmobile',
'Plymouth',
'Pontiac',
'Rolls-Royce',
'Eagle',
'Lincoln',
'Mercedes-Benz',
'GMC',
'Saab',
'Honda',
'Saturn',
'Mitsubishi',
'Isuzu',
'Jeep',
'AM General',
'Geo',
'Suzuki',
'E. P. Dutton, Inc.',
'Land Rover',
'PAS, Inc',
'Acura',
'Jaguar',
'Lotus',
'Grumman Olson',
'Porsche',
'American Motors Corporation',
'Kia',
'Lamborghini',
'Panoz Auto-Development',
'Maserati',
'Saleen',
'Aston Martin',
'Dabryan Coach Builders Inc',
'Federal Coach',
'Vector',
'Bentley',
'Daewoo',
'Qvale',
'Roush Performance',
'Autokraft Limited',
'Bertone',
'Panther Car Company Limited',
'Texas Coach Company',
'TVR Engineering Ltd',
'Morgan',
'MINI',
'Yugo',
'BMW Alpina',
'Renault',
'Bitter Gmbh and Co. Kg',
'Scion',
'Maybach',
'Lambda Control Systems',
'Merkur',
'Peugeot',
'Spyker',
'London Coach Co Inc',
'Hummer',
'Bugatti',
'Pininfarina',
'Shelby',
'Saleen Performance',
'smart',
'Tecstar, LP',
'Kenyon Corporation Of America',
'Avanti Motor Corporation',
'Bill Dovell Motor Car Company',
'Import Foreign Auto Sales Inc',
'S and S Coach Company E.p. Dutton',
'Superior Coaches Div E.p. Dutton',
'Vixen Motor Company',
'Volga Associated Automobile',
'Wallace Environmental',
'Import Trade Services',
'J.K. Motors',
'Panos',
'Quantum Technologies',
'London Taxi',
'Red Shift Ltd.',
'Ruf Automobile Gmbh',
'Excalibur Autos',
'Mahindra',
'VPG',
'Fiat',
'Sterling',
'Azure Dynamics',
'McLaren Automotive',
'Ram',
'CODA Automotive',
'Fisker',
'Tesla',
'Mcevoy Motors',
'BYD',
'ASC Incorporated',
'SRT',
'CCC Engineering',
'Mobility Ventures LLC',
'Pagani',
'Genesis',
'Karma',
'Koenigsegg',
'Aurora Cars Ltd',
'RUF Automobile',
'Dacia',
'STI',
'Daihatsu',
'Polestar',
'Kandi',
'Rivian',
'Lucid',
'JBA Motorcars, Inc.',
'Lordstown',
'Vinfast',
'INEOS Automotive',
'Bugatti Rimac',
'Grumman Allied Industries',
'Environmental Rsch and Devp Corp',
'Evans Automobiles',
'Laforza Automobile Inc',
'General Motors',
'Consulier Industries Inc',
'Goldacre',
'Isis Imports Ltd',
'PAS Inc - GMC'
];
const makes_list = makes_list_raw.sort();
interface ManageBrandProductsProps {
accessToken: string | null;
}
const toNumber = (val: unknown): number => {
if (typeof val === 'number') return val;
if (typeof val === 'string') {
const cleaned = val.replace(/[^0-9.\-]/g, '');
const n = parseFloat(cleaned);
return Number.isFinite(n) ? n : 0;
}
return 0;
};
const priceOf = (item: Item) => toNumber(item.attributes?.price);
const invOf = (item: Item) => toNumber(item.inventoryQuantity);
const ManageBrandProducts: React.FC<ManageBrandProductsProps> = () => {
const router = useRouter()
const [accessToken, setaccessToken] = useState<string>('');
const [brands, setBrands] = useState<Brand[]>([]);
const [itemsMap, setItemsMap] = useState<Record<string, Item[]>>({});
const [expandedBrand, setExpandedBrand] = useState<string | null>(null);
const [loadingMap, setLoadingMap] = useState<Record<string, boolean>>({});
const [filters, setFilters] = useState({
make: '',
invMin: 0,
invMax: 9999,
priceMin: 0,
});
// Added products (from /api/user-products)
const [addedIds, setAddedIds] = useState<Set<string>>(new Set());
const [addedPartNumbers, setAddedPartNumbers] = useState<Set<string>>(new Set());
const [payment, setPayment] = useState<any>(null);
const [Turn14Enabled, setTurn14Enabled] = useState<boolean | null>(null);
// UI/Sticky state
const [isScrolled, setIsScrolled] = useState(false);
// Global Search
const [searchText, setSearchText] = useState<string>('');
// Selection
const [selectedItems, setSelectedItems] = useState<Record<string, Item>>({});
// In-stock toggle (pill switch)
const [inStockOnly, setInStockOnly] = useState<boolean>(false);
// Toasts
const [toastActive, setToastActive] = useState(false);
const [toast, setToast] = useState('');
// NOTE: sessionStorage is available (client component)
const userId = typeof window !== 'undefined' ? sessionStorage.getItem('USERID') : null;
const EBAYSTOREID = typeof window !== 'undefined' ? sessionStorage.getItem('EBAYSTOREID') : null;
useEffect(() => {
const role = localStorage.getItem("user_role");
const sessionId = localStorage.getItem("payment_session");
// ✅ Admins and Partners can access directly (skip payment check)
if (role === "admin" || role === "partner") {
return;
}
// 🚫 If no payment session, redirect to pricing
if (!sessionId) {
router.push("/pricing");
return;
}
// ✅ Otherwise, check payment details
const fetchPaymentDetails = async () => {
try {
const res: any = await axios.get(
"https://ebay.backend.data4autos.com/api/payment/details",
{ params: { session_id: sessionId } }
);
setPayment(res.data.payment);
} catch (err) {
console.error("Error fetching payment details:", err);
}
};
fetchPaymentDetails();
}, [router]);
useEffect(() => {
const fetchUserBrands = async () => {
try {
const accessToken = await getAccessToken_client();
setaccessToken(accessToken || '');
console.log('Got access token:', accessToken);
const res = await fetch(`https://ebay.backend.data4autos.com/api/brands/${userId}`, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
});
const data = await res.json();
setBrands(data);
const userSelectedIds = data.map((b: any) => String(b.brandid));
setToast(`Loaded ${userSelectedIds.length} selected brands`);
setTimeout(() => setToast(''), 4000);
} catch (error) {
console.error('Error fetching brands:', error);
setToast('Failed to load user brands');
setTimeout(() => setToast(''), 4000);
}
};
if (userId) fetchUserBrands();
}, [userId]);
useEffect(() => {
// Simulate Turn14 check
const t = setTimeout(() => setTurn14Enabled(true), 500);
return () => clearTimeout(t);
}, []);
useEffect(() => {
const onScroll = () => setIsScrolled(window.scrollY > 10);
window.addEventListener('scroll', onScroll);
return () => window.removeEventListener('scroll', onScroll);
}, []);
const toggleBrandItems = async (brandId: string) => {
if (expandedBrand === brandId) {
setExpandedBrand(null);
return;
}
setExpandedBrand(brandId);
if (!itemsMap[brandId]) {
setLoadingMap((prev) => ({ ...prev, [brandId]: true }));
try {
const res = await fetch(`https://turn14.data4autos.com/v1/items/brandallitems/${brandId}`, {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
});
const data1 = await res.json();
setItemsMap((prev) => ({ ...prev, [brandId]: data1 }));
} catch (err) {
console.error(err);
}
setLoadingMap((prev) => ({ ...prev, [brandId]: false }));
}
};
const applyFitmentFilters = (items: Item[]) => {
return items.filter((item) => {
if (!filters.make) return true;
const tags = item.attributes.fitmmentTags || {};
return (
(Array.isArray(tags.make) && tags.make.some((m: string) => m?.toLowerCase() === filters.make.toLowerCase())) ||
item.attributes.product_name?.toLowerCase().includes(filters.make.toLowerCase())
);
});
};
const applyInventoryFilters = (items: Item[]) =>
items.filter((it) => {
const qty = invOf(it);
if (inStockOnly && qty <= 0) return false;
return qty >= filters.invMin && qty <= filters.invMax;
});
const applyPriceFilters = (items: Item[]) =>
items.filter((it) => priceOf(it) >= filters.priceMin);
// Broad query match
const itemMatchesQuery = (item: Item, q: string): boolean => {
if (!q) return true;
const hay = `${item.id} ${JSON.stringify(item.attributes ?? {}, (_k, v) => v ?? '', 2)}`.toLowerCase();
return hay.includes(q.toLowerCase());
};
const handleMakeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setFilters((f) => ({ ...f, make: e.target.value }));
};
const handleInvMinChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const v = Math.max(0, toNumber(e.target.value));
setFilters((f) => ({ ...f, invMin: v }));
};
const handleInvMaxChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const v = Math.max(0, toNumber(e.target.value));
setFilters((f) => ({ ...f, invMax: v || 9999 }));
};
const handlePriceMinChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const v = Math.max(0, toNumber(e.target.value));
setFilters((f) => ({ ...f, priceMin: v }));
};
const isSelected = (id: string) => Boolean(selectedItems[id]);
const toggleSelect = (item: Item) => {
setSelectedItems((prev) => {
const copy = { ...prev };
if (copy[item.id]) delete copy[item.id];
else copy[item.id] = item;
return copy;
});
};
const toggleSelectAllVisible = (visibleItems: Item[]) => {
setSelectedItems((prev) => {
const copy = { ...prev };
const allSelected = visibleItems.every((it) => copy[it.id]);
if (allSelected) {
// Clear only the visible ones
visibleItems.forEach((it) => {
if (copy[it.id]) delete copy[it.id];
});
} else {
visibleItems.forEach((it) => {
copy[it.id] = it;
});
}
return copy;
});
};
const clearVisibleSelection = (visibleItems: Item[]) => {
setSelectedItems((prev) => {
const copy = { ...prev };
visibleItems.forEach((it) => {
if (copy[it.id]) delete copy[it.id];
});
return copy;
});
};
const selectedCount = useMemo(() => Object.keys(selectedItems).length, [selectedItems]);
const selectedArray = useMemo(() => Object.values(selectedItems), [selectedItems]);
const quantityToList = (qty: number) => {
if (qty <= 1) return 1;
if (qty >= 2 && qty <= 4) return qty;
return 4;
// If you'd rather skip low/zero stock instead of listing 1, change the first line to: if (qty <= 1) return 0;
};
const handleAddSelectedProducts = async () => {
// const filteredForQueue = selectedArray.filter((it) => invOf(it) > 0);
// const payloadProducts = filteredForQueue.map((item) => ({
// ...item,
// quantityToList: quantityToList(invOf(item)),
// }));
const filteredForQueue = selectedArray.filter((it) => invOf(it) > 0);
const payloadProducts = filteredForQueue.map((item) => {
const a = item.attributes || {};
const bid = a.brand_id ?? (item as any).brand_id; // accept either location
const bname = a.brand ?? (item as any).brand;
// find logo by id first, then by name
const brandLogo =
(bid != null ? brandLogoById[String(bid)] : '') ||
(bname ? brandLogoByName[String(bname).toLowerCase()] : '') ||
'';
return {
...item,
quantityToList: quantityToList(invOf(item)),
// only NEW field you asked for:
brandLogo,
// (optional helpers if your backend wants structure)
brand: {
id: bid ?? '',
name: bname ?? '',
logo: brandLogo,
},
};
});
setToast(`Queued ${payloadProducts.length} product(s)`);
try {
const userId = typeof window !== 'undefined' ? sessionStorage.getItem('USERID') : null;
const saveRes = await fetch('https://ebay.backend.data4autos.com/api/ebay/finalize-publish', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userKey: EBAYSTOREID,
products: payloadProducts,
userId: userId,
}),
});
const saveData = await saveRes.json();
console.log('Save response:', saveData);
setToastActive(true);
// Merge newly queued items into "added" sets so the pill appears instantly
setAddedIds(prev => {
const next = new Set(prev);
payloadProducts.forEach((p) => {
if (p?.id) next.add(String(p.id));
});
return next;
});
setAddedPartNumbers(prev => {
const next = new Set(prev);
payloadProducts.forEach((p) => {
const pn = p?.attributes?.part_number;
if (pn) next.add(String(pn));
});
return next;
});
setTimeout(() => setToastActive(false), 3000);
} catch (e) {
console.error(e);
setToast('Failed to queue products');
}
};
if (Turn14Enabled === false) {
return (
<div className="p-6 text-center">
<h1 className="text-xl font-semibold">Turn14 isnt connected yet</h1>
<p className="text-gray-600">Please connect Turn14 to continue.</p>
</div>
);
}
const getHeaderStatus = () => {
if (!expandedBrand) return `Selected: ${selectedCount}`;
const base = itemsMap[expandedBrand] || [];
const afterMake = applyFitmentFilters(base);
const afterSearch = afterMake.filter((it) => itemMatchesQuery(it, searchText));
const afterInv = applyInventoryFilters(afterSearch);
const afterPrice = applyPriceFilters(afterInv);
return `Showing ${afterPrice.length} of ${base.length}`;
};
useEffect(() => {
const fetchAddedProducts = async () => {
if (!userId) return;
try {
// pull a big page; adjust if your API supports 'all'
const url = `https://ebay.backend.data4autos.com/api/user-products?userid=${encodeURIComponent(
userId
)}&page=1&pageSize=10000`;
const res = await fetch(url, { method: 'GET' });
const data = await res.json();
console.log('Added products response:', data);
const ids = new Set<string>();
const partnos = new Set<string>();
// Accept a few common field names from your mapping API
(data?.items ?? []).forEach((row: any) => {
if (row?.id) ids.add(String(row.id));
if (row?.sku) ids.add(String(row.sku)); // sometimes sku is what your UI uses as id
if (row?.partNumber) partnos.add(String(row.partNumber));
if (row?.attributes?.part_number) partnos.add(String(row.attributes.part_number));
});
setAddedIds(ids);
setAddedPartNumbers(partnos);
} catch (e) {
console.error('Failed to fetch added products:', e);
}
};
fetchAddedProducts();
}, [userId]);
// NEW: fast maps to find logo by id or name
const brandLogoById = useMemo(() => {
const m: Record<string, string> = {};
brands.forEach(b => { m[String(b.brandid)] = b.logo || ''; });
return m;
}, [brands]);
const brandLogoByName = useMemo(() => {
const m: Record<string, string> = {};
brands.forEach(b => { if (b.name) m[b.name.toLowerCase()] = b.logo || ''; });
return m;
}, [brands]);
return (
<div className="bg-gradient-to-br from-[#00d1ff]/10 via-white to-[#00d1ff]/20 min-h-screen">
{/* Sticky Header */}
<div
className={`sticky top-14 z-10 transition-all duration-300 ${isScrolled ? 'bg-white/95 backdrop-blur-md shadow-lg py-3' : 'bg-white/80 backdrop-blur-sm py-4'
}`}
>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div className="flex flex-col">
<h1 className="text-2xl font-bold bg-[#00d1ff] to-purple-600 bg-clip-text text-[#00d1ff]">
Data4Autos Turn14 Manage Brand Products
</h1>
<p className="text-sm text-gray-500 mt-1">{getHeaderStatus()}</p>
<p className="text-sm font-medium text-[#00d1ff] mt-1">
Selected: <span className="font-semibold">{selectedCount}</span>
</p>
</div>
<div className="flex flex-col sm:flex-row gap-3 items-stretch sm:items-center w-full md:w-auto">
{/* Search */}
<div className="relative flex-grow">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg className="h-5 w-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
clipRule="evenodd"
/>
</svg>
</div>
<input
type="text"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
placeholder="Search products… (name, part#, tags)"
className="block w-full pl-10 pr-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
/>
</div>
{/* In-stock-only pill toggle */}
<div className="flex flex-wrap items-center gap-3">
<label className="flex items-center gap-2 text-sm font-medium text-gray-700 cursor-pointer select-none">
<div className="relative">
<input
type="checkbox"
checked={inStockOnly}
onChange={() => setInStockOnly((v) => !v)}
className="sr-only"
/>
<div className={`block w-10 h-6 rounded-full transition-colors ${inStockOnly ? 'bg-[#00d1ff]' : 'bg-gray-300'}`}></div>
<div
className={`absolute left-1 top-1 bg-white w-4 h-4 rounded-full transition-transform ${inStockOnly ? 'transform translate-x-4' : ''
}`}
></div>
</div>
In Stock Only
</label>
<button
onClick={handleAddSelectedProducts}
disabled={selectedCount === 0}
className="px-5 py-2.5 bg-[#00d1ff] text-white font-medium rounded-lg hover:from-[#00d1ff] hover:to-purple-700 transition-all duration-300 transform hover:-translate-y-0.5 disabled:opacity-50 disabled:transform-none disabled:cursor-not-allowed flex items-center gap-2 shadow-md hover:shadow-lg"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
</svg>
Add {selectedCount} Product{selectedCount === 1 ? '' : 's'}
</button>
</div>
</div>
</div>
{/* Global Filters */}
<div className="mt-3 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3">
{/* Make */}
<label className="text-sm text-gray-700 flex items-center">
<span className="mr-2 whitespace-nowrap">Filter Make:</span>
<select
value={filters.make}
onChange={handleMakeChange}
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 w-full"
>
<option value="">All</option>
{makes_list.map((m) => (
<option key={m} value={m}>
{m}
</option>
))}
</select>
</label>
{/* Inventory Range */}
<div className="text-sm text-gray-700">
<div className="mb-1 font-medium">Inventory Range</div>
<div className="flex items-center gap-2">
<input
type="number"
min={0}
value={filters.invMin}
onChange={handleInvMinChange}
className="w-24 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="Min"
/>
<span>to</span>
<input
type="number"
min={0}
value={filters.invMax}
onChange={handleInvMaxChange}
className="w-24 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="Max"
/>
</div>
</div>
{/* Price Minimum (Slider + number) */}
<div className="text-sm text-gray-700">
<div className="mb-1 font-medium">Price Floor</div>
<div className="flex items-center gap-3">
<input
type="range"
min={0}
max={5000}
step={5}
value={filters.priceMin}
onChange={handlePriceMinChange}
className="w-full"
aria-label="Minimum price"
/>
<input
type="number"
min={0}
value={filters.priceMin}
onChange={handlePriceMinChange}
className="w-24 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div className="text-xs text-gray-500 mt-1">Showing items with price ${filters.priceMin}</div>
</div>
</div>
</div>
</div>
{/* Brands/Products */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pt-6 pb-16">
{brands.length === 0 ? (
<div className="flex items-center justify-center py-24 bg-gradient-to-b from-gray-50 to-gray-100">
<div className="relative text-center bg-white/80 backdrop-blur-md border border-white/40 shadow-2xl rounded-3xl p-10 transition-all duration-500 hover:scale-[1.02]">
{/* Icon Circle */}
<div className="w-20 h-20 mx-auto flex items-center justify-center rounded-full bg-[#00d1ff] shadow-lg">
<svg
className="w-10 h-10 text-white"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
</div>
{/* Text */}
<h3 className="mt-6 text-2xl font-semibold text-gray-800 tracking-tight">
No Brands Selected Yet
</h3>
<p className="mt-3 text-gray-500 max-w-sm mx-auto">
Add brands to view and explore their products here.
</p>
{/* Button */}
{/* <button className="mt-6 px-6 py-3 bg-gradient-to-r from-indigo-500 to-purple-600 text-white rounded-xl shadow-md hover:shadow-lg transition-transform duration-300 hover:scale-105">
Add Brand
</button> */}
{/* Decorative Blur Circle */}
<div className="absolute -top-10 -right-10 w-32 h-32 bg-[#00d1ff] rounded-full blur-3xl"></div>
<div className="absolute -bottom-10 -left-10 w-32 h-32 bg-[#00d1ff] rounded-full blur-3xl"></div>
</div>
</div>
) : (
brands.map((brand) => {
const baseItems = itemsMap[brand.brandid] || [];
const filteredFitment = applyFitmentFilters(baseItems);
const filteredSearch = filteredFitment.filter((it) => itemMatchesQuery(it, searchText));
const filteredInv = applyInventoryFilters(filteredSearch);
const filteredItems = applyPriceFilters(filteredInv);
const allVisibleSelected = filteredItems.length > 0 && filteredItems.every((it) => isSelected(it.id));
const anyVisibleSelected = filteredItems.some((it) => isSelected(it.id));
return (
<div key={brand.brandid} className="mb-6">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-3">
{brand.logo && (
<img src={brand.logo} alt={brand.name} className="h-8 w-8 object-contain rounded bg-white border" />
)}
<div className="text-sm">
<div className="font-semibold">{brand.name}</div>
<div className="text-gray-500">ID: {brand.brandid}</div>
</div>
</div>
<button
onClick={() => toggleBrandItems(brand.brandid)}
className="px-3 py-2 text-sm rounded-md border border-gray-300 hover:bg-gray-50"
>
{expandedBrand === brand.brandid ? 'Hide Products' : 'Show Products'}
</button>
</div>
{expandedBrand === brand.brandid && (
<div className="border border-gray-200 rounded-lg p-3 bg-white">
<div className="mb-3 flex items-center justify-between">
<div className="text-xs text-gray-600">
Showing <span className="font-semibold">{filteredItems.length}</span> of{' '}
<span className="font-semibold">{baseItems.length}</span>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => toggleSelectAllVisible(filteredItems)}
className="text-xs px-3 py-1.5 rounded border border-gray-300 hover:bg-gray-50"
>
{allVisibleSelected ? 'Unselect All Visible' : 'Select All Visible'}
</button>
{anyVisibleSelected && (
<button
onClick={() => clearVisibleSelection(filteredItems)}
className="text-xs px-3 py-1.5 rounded border border-gray-300 hover:bg-gray-50"
>
Clear Visible
</button>
)}
</div>
</div>
{loadingMap[brand.brandid] ? (
<div className="py-8 text-center text-gray-500">Loading products</div>
) : filteredItems.length === 0 ? (
<div className="py-8 text-center text-gray-500">No products match the current filters.</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">
{filteredItems.map((item) => {
const imgSrc =
item.attributes.thumbnail ||
item?.attributes?.files?.[0]?.url ||
'https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png';
const qty = invOf(item);
const price = priceOf(item);
const computedQty = quantityToList(qty);
const isAlreadyAdded =
(item?.id && addedIds.has(String(item.id))) ||
(item?.attributes?.part_number && addedPartNumbers.has(String(item.attributes.part_number)));
return (
<div key={item.id} className="border border-gray-200 rounded-lg p-3 hover:shadow-sm">
<div className="flex items-center justify-between mb-2">
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={isSelected(item.id)}
onChange={() => toggleSelect(item)}
className="h-4 w-4 rounded border-gray-300 text-[#00d1ff] focus:ring-blue-500"
/>
<span className="text-xs text-gray-600">Select</span>
</label>
<div className="flex items-center gap-2">
{isAlreadyAdded && (
<span className="text-[10px] px-2 py-0.5 rounded-full bg-green-100 text-green-700 border border-green-200">
Added
</span>
)}
<span className="text-[10px] px-2 py-0.5 rounded-full bg-slate-100 text-slate-700">
Will list: {computedQty}
</span>
</div>
</div>
<img
src={imgSrc}
alt={item.attributes.product_name || 'Product'}
className="w-full h-24 object-contain mb-2 bg-gray-50 rounded"
/>
<div className="text-sm font-medium line-clamp-2">
{item.attributes.product_name || 'Unnamed Product'}
</div>
<div className="text-xs text-gray-600 mt-1">
Part #: <span className="font-medium">{item.attributes.part_number || '-'}</span>
</div>
<div className="text-xs text-gray-600">
{item.attributes.category || '-'} &gt; {item.attributes.subcategory || '-'}
</div>
<div className="text-xs">
Price:{' '}
<span className="font-semibold">
{Number.isFinite(price) && price > 0 ? `$${price.toFixed(2)}` : '-'}
</span>
</div>
<div className="text-xs text-gray-700">
Inventory: <span className="font-semibold">{qty}</span>
</div>
{item.attributes.part_description && (
<p className="mt-2 text-xs text-gray-700 line-clamp-3">
{item.attributes.part_description}
</p>
)}
</div>
);
})}
</div>
)}
</div>
)}
</div>
);
})
)}
</div>
{/* Toasts */}
{toast && (
<div className="fixed bottom-6 right-6 bg-gradient-to-r from-slate-800 to-black text-white px-6 py-4 rounded-2xl shadow-2xl z-30 animate-fade-in-up">
<div className="flex items-center gap-3">
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
</svg>
<span className="font-medium">{toast}</span>
</div>
</div>
)}
{toastActive && (
<div className="fixed bottom-6 right-6 bg-gradient-to-r from-green-500 to-emerald-600 text-white px-6 py-4 rounded-2xl shadow-2xl z-30 animate-fade-in-up">
<div className="flex items-center gap-3">
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
</svg>
<span className="font-medium">Products added successfully!</span>
</div>
</div>
)}
{/* Tiny animation helper */}
<style jsx global>{`
@keyframes fadeInUp {
from {
opacity: 0;
transform: translate3d(0, 40px, 0);
}
to {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
.animate-fade-in-up {
animation: fadeInUp 0.5s ease-out;
}
`}</style>
</div>
);
};
export default ManageBrandProducts;

View File

@ -0,0 +1,288 @@
// pages/manage-brand-products.tsx
'use client';
import { getAccessToken } from '@/utils/apiHelper';
import React, { useEffect, useState } from 'react';
interface Brand {
id: string;
brandid: string;
name: string;
logo?: string;
}
interface Item {
id: string;
attributes: {
product_name?: string;
thumbnail?: string;
part_number?: string;
category?: string;
subcategory?: string;
price?: string;
part_description?: string;
regular_stock?: boolean;
ltl_freight_required?: boolean;
is_clearance_item?: boolean;
is_air_freight_prohibited?: boolean;
files?: { url: string }[];
fitmmentTags?: any;
};
inventoryQuantity?: number;
}
const makes_list_raw = [
'Alfa Romeo',
'Ferrari',
'Dodge',
'Subaru',
'Toyota',
'Volkswagen',
'Volvo',
'Audi',
'BMW',
'Buick',
'Cadillac',
'Chevrolet',
'Chrysler',
'CX Automotive',
'Nissan',
'Ford',
'Hyundai',
'Infiniti',
'Lexus',
'Mercury',
'Mazda',
'Oldsmobile',
'Plymouth',
'Pontiac',
'Rolls-Royce',
'Eagle',
'Lincoln',
'Mercedes-Benz',
'GMC',
'Saab',
'Honda',
'Saturn',
'Mitsubishi',
'Isuzu',
'Jeep',
];
const makes_list = makes_list_raw.sort();
const styles = {
gridContainer: {
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: '10px',
},
gridItem: { display: 'flex', flexDirection: 'column' },
gridFullWidthItem: { gridColumn: 'span 2', display: 'flex', flexDirection: 'column' },
};
interface ManageBrandProductsProps {
accessToken: string | null;
}
const ManageBrandProducts: React.FC<ManageBrandProductsProps> = ({ accessToken }) => {
const [brands, setBrands] = useState<Brand[]>([]); // Placeholder brands
// const [brands, setBrands] = useState<Brand[]>([]); // Uncomment to start with no brands
const [itemsMap, setItemsMap] = useState<Record<string, Item[]>>({});
const [expandedBrand, setExpandedBrand] = useState<string | null>(null);
const [loadingMap, setLoadingMap] = useState<Record<string, boolean>>({});
const [filters, setFilters] = useState({ make: '' });
const [Turn14Enabled, setTurn14Enabled] = useState<boolean | null>(null);
const [status, setStatus] = useState<string>('processing');
const [processId, setProcessId] = useState<string | null>(null);
const [results, setResults] = useState<Item[]>([]);
const [progress, setProgress] = useState(0);
const [totalProducts, setTotalProducts] = useState(0);
const [processedProducts, setProcessedProducts] = useState(0);
const [currentProduct, setCurrentProduct] = useState<Item | null>(null);
const [toastActive, setToastActive] = useState(false);
const [toast, setToast] = useState('');
const userId = sessionStorage.getItem('USERID');
const EBAYSTOREID = sessionStorage.getItem('EBAYSTOREID');
useEffect(() => {
const fetchUserBrands = async () => {
try {
const res = await fetch(`https://ebay.backend.data4autos.com/api/brands/${userId}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
const data = await res.json();
console.log('GET response:', data);
setBrands(data);
// Extract selected brand IDs from the response
const userSelectedIds = data.map((b: any) => String(b.brandid)); // brandid from your response
// Optional: show toast
setToast(`Loaded ${userSelectedIds.length} selected brands`);
setTimeout(() => setToast(''), 4000);
} catch (error) {
console.error('Error fetching brands:', error);
setToast('Failed to load user brands');
setTimeout(() => setToast(''), 4000);
}
};
fetchUserBrands();
}, []);
// Simulate Turn14 check
useEffect(() => {
setTimeout(() => setTurn14Enabled(true), 500); // Simulate API call
}, []);
const toggleBrandItems = async (brandId: string) => {
if (expandedBrand === brandId) {
setExpandedBrand(null);
return;
}
setExpandedBrand(brandId);
if (!itemsMap[brandId]) {
setLoadingMap((prev) => ({ ...prev, [brandId]: true }));
try {
const res = await fetch(`https://turn14.data4autos.com/v1/items/brandallitems/${brandId}`, {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
});
const data1 = await res.json();
console.log('GET response:', data1);
// TODO: Replace with real API call using Turn14 token
// const data: Item[] = Array.from({ length: 5 }).map((_, idx) => ({
// id: `${brandId}-item-${idx}`,
// attributes: {
// product_name: `Product ${idx}`,
// thumbnail: '',
// part_number: `PN-${idx}`,
// category: 'Cat',
// subcategory: 'Subcat',
// price: '100',
// part_description: 'Some description',
// regular_stock: true,
// ltl_freight_required: false,
// is_clearance_item: false,
// is_air_freight_prohibited: false,
// files: [],
// fitmmentTags: { make: ['Toyota'], model: [], year: [] },
// },
// inventoryQuantity: 10,
// }));
setItemsMap((prev) => ({ ...prev, [brandId]: data1 }));
} catch (err) {
console.error(err);
}
setLoadingMap((prev) => ({ ...prev, [brandId]: false }));
}
};
const applyFitmentFilters = (items: Item[]) => {
return items;
// return items.filter((item) => {
// if (!filters.make) return true;
// const tags = item.attributes.fitmmentTags || {};
// return tags.make?.includes(filters.make) || item.attributes.product_name?.includes(filters.make);
// });
};
const handleFilterChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setFilters({ ...filters, make: e.target.value });
};
const handleAction = () => {
// TODO: Implement action logic using Turn14 access token
console.log('TODO: Submit selected products');
setToastActive(true);
};
if (Turn14Enabled === false) {
return (
<div style={{ padding: 24, textAlign: 'center' }}>
<h1>Turn14 isnt connected yet</h1>
<p>Please connect Turn14 to continue.</p>
</div>
);
}
return (
<div style={{ padding: 24 }}>
<h1>Data4Autos Turn14 Manage Brand Products</h1>
{brands.length === 0 ? (
<p>No brands selected yet.</p>
) : (
brands.map((brand) => {
const filteredItems = applyFitmentFilters(itemsMap[brand.brandid] || []);
return (
<div key={brand.brandid} style={{ marginBottom: 24 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
<div>
<strong>{brand.name}</strong> (ID: {brand.brandid})
</div>
<button onClick={() => toggleBrandItems(brand.brandid)}>{expandedBrand === brand.brandid ? 'Hide Products' : 'Show Products'}</button>
</div>
{expandedBrand === brand.brandid && (
<div style={{ border: '1px solid #eee', padding: 12 }}>
<div style={{ marginBottom: 12 }}>
<label>
Filter Make:
<select value={filters.make} onChange={handleFilterChange}>
<option value="">All</option>
{makes_list.map((m) => (
<option key={m} value={m}>
{m}
</option>
))}
</select>
</label>
</div>
<button onClick={handleAction}>Add {filteredItems.length} Products to Store</button>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(250px, 1fr))', gap: 12, marginTop: 12 }}>
{filteredItems.map((item) => {
// console.log('Rendering item:', item);
// console.log(item?.attributes?.files?.[0]);
return (
<div key={item.id} style={{ border: '1px solid #ccc', padding: 12 }}>
<img
src={item?.attributes?.files?.[0]?.url || item.attributes.thumbnail || 'https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png'}
alt={item.attributes.product_name}
style={{ width: '100%', marginBottom: 8 }}
/>
<strong>{item.attributes.product_name}</strong>
<p>Part Number: {item.attributes.part_number}</p>
<p>
Category: {item.attributes.category} &gt; {item.attributes.subcategory}
</p>
<p>Price: ${item.attributes.price}</p>
<p>{item.attributes.part_description}</p>
</div>
);
})}
</div>
</div>
)}
</div>
);
})
)}
{toastActive && <div style={{ position: 'fixed', bottom: 12, right: 12, background: '#0c0', color: '#fff', padding: 12 }}>Products added successfully!</div>}
</div>
);
};
export default ManageBrandProducts;

View File

@ -0,0 +1,791 @@
// pages/manage-brand-products.tsx
'use client';
import { getAccessToken } from '@/utils/apiHelper';
import { getAccessToken_client } from '@/utils/apiHelper_client';
import axios from 'axios';
import { useRouter } from 'next/navigation';
import React, { useEffect, useMemo, useState } from 'react';
interface Brand {
id: string;
name: string;
logo?: string;
}
interface Product {
PartNumber: string;
Description: string;
Brand: string;
SuggestedRetail: string;
Cost: string;
Length: string;
Width: string;
Height: string;
Weight: string;
QtyAvail: number;
UPC: string;
Jobber: string;
AAIACode: string;
MapPrice: string;
VendorMSRP: string;
AirRestricted: string;
StateRestricted: string;
TruckFrtOnly: string;
ManufacturerPart: string;
ShipAlone: string;
Status: string;
MotorStateNotes: string;
CanadaRestricted: string;
AcquiredDate: string;
EmissionsWarning: string;
"Image URL": string;
"Category Level 1": string;
"Category Level 2": string;
"Category Level 3": string;
"Long Description(150)": string;
}
interface BrandLogoMap {
[key: string]: {
logo: string;
};
}
interface ProductsResponse {
meta: {
eta: string;
count: number;
};
data: Product[];
}
interface ManageBrandProductsProps {
accessToken: string | null;
}
const toNumber = (val: unknown): number => {
if (typeof val === 'number') return val;
if (typeof val === 'string') {
const cleaned = val.replace(/[^0-9.\-]/g, '');
const n = parseFloat(cleaned);
return Number.isFinite(n) ? n : 0;
}
return 0;
};
// Helper functions to work directly with Product
const priceOf = (product: Product) => toNumber(product.SuggestedRetail);
const invOf = (product: Product) => toNumber(product.QtyAvail);
const ManageBrandProducts: React.FC<ManageBrandProductsProps> = () => {
const router = useRouter();
const [accessToken, setaccessToken] = useState<string>('');
const [brands, setBrands] = useState<Brand[]>([]);
const [productsMap, setProductsMap] = useState<Record<string, ProductsResponse>>({});
const [expandedBrand, setExpandedBrand] = useState<string | null>(null);
const [loadingMap, setLoadingMap] = useState<Record<string, boolean>>({});
const [filters, setFilters] = useState({
make: '',
invMin: 0,
invMax: 99,
priceMin: 0,
});
const [addedIds, setAddedIds] = useState<Set<string>>(new Set());
const [addedPartNumbers, setAddedPartNumbers] = useState<Set<string>>(new Set());
const [payment, setPayment] = useState<any>(null);
const [Turn14Enabled, setTurn14Enabled] = useState<boolean | null>(null);
const [isScrolled, setIsScrolled] = useState(false);
const [searchText, setSearchText] = useState<string>('');
const [selectedItems, setSelectedItems] = useState<Record<string, Product>>({});
const [inStockOnly, setInStockOnly] = useState<boolean>(false);
const [toastActive, setToastActive] = useState(false);
const [toast, setToast] = useState('');
const userId = typeof window !== 'undefined' ? sessionStorage.getItem('USERID') : null;
const EBAYSTOREID = typeof window !== 'undefined' ? sessionStorage.getItem('EBAYSTOREID') : null;
useEffect(() => {
const role = localStorage.getItem("user_role");
const sessionId = localStorage.getItem("payment_session");
if (role === "admin" || role === "partner") {
return;
}
if (!sessionId) {
router.push("/pricing");
return;
}
const fetchPaymentDetails = async () => {
try {
const res: any = await axios.get(
"https://ebay.backend.data4autos.com/api/payment/details",
{ params: { session_id: sessionId } }
);
setPayment(res.data.payment);
} catch (err) {
console.error("Error fetching payment details:", err);
}
};
fetchPaymentDetails();
}, [router]);
useEffect(() => {
const fetchUserBrands = async () => {
try {
const accessToken = await getAccessToken_client();
setaccessToken(accessToken || '');
console.log('Got access token:', accessToken);
const res = await fetch(`https://ebay.backend.data4autos.com/api/motorstate/brands/${userId}`, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
});
const data = await res.json();
setBrands(data);
const userSelectedIds = data.map((b: any) => String(b.name));
setToast(`Loaded ${userSelectedIds.length} selected brands`);
setTimeout(() => setToast(''), 4000);
} catch (error) {
console.error('Error fetching brands:', error);
setToast('Failed to load user brands');
setTimeout(() => setToast(''), 4000);
}
};
if (userId) fetchUserBrands();
}, [userId]);
useEffect(() => {
const t = setTimeout(() => setTurn14Enabled(true), 500);
return () => clearTimeout(t);
}, []);
useEffect(() => {
const onScroll = () => setIsScrolled(window.scrollY > 10);
window.addEventListener('scroll', onScroll);
return () => window.removeEventListener('scroll', onScroll);
}, []);
const toggleBrandItems = async (name: string) => {
if (expandedBrand === name) {
setExpandedBrand(null);
return;
}
setExpandedBrand(name);
if (!productsMap[name]) {
setLoadingMap((prev) => ({ ...prev, [name]: true }));
try {
const res = await fetch(`https://motorstate.data4autos.com/api/data/products/${name}`, {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
});
const data: ProductsResponse = await res.json();
setProductsMap((prev) => ({ ...prev, [name]: data }));
} catch (err) {
console.error(err);
}
setLoadingMap((prev) => ({ ...prev, [name]: false }));
}
};
// Get products directly for brand
const getProductsForBrand = (name: string): Product[] => {
const response = productsMap[name];
if (!response?.data) return [];
return response.data;
};
const applyFitmentFilters = (products: Product[]) => {
return products.filter((product) => {
if (!filters.make) return true;
return product.Description?.toLowerCase().includes(filters.make.toLowerCase());
});
};
const applyInventoryFilters = (products: Product[]) =>
products.filter((product) => {
const qty = invOf(product);
if (inStockOnly && qty <= 0) return false;
return qty >= filters.invMin && qty <= filters.invMax;
});
const applyPriceFilters = (products: Product[]) =>
products.filter((product) => priceOf(product) >= filters.priceMin);
const productMatchesQuery = (product: Product, q: string): boolean => {
if (!q) return true;
const searchableText = `${product.PartNumber} ${product.Description} ${product.Brand} ${product["Category Level 1"]} ${product["Category Level 2"]} ${product["Long Description(150)"]}`.toLowerCase();
return searchableText.includes(q.toLowerCase());
};
const handleMakeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setFilters((f) => ({ ...f, make: e.target.value }));
};
const handleInvMinChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const v = Math.max(0, toNumber(e.target.value));
setFilters((f) => ({ ...f, invMin: v }));
};
const handleInvMaxChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const v = Math.max(0, toNumber(e.target.value));
setFilters((f) => ({ ...f, invMax: v || 9999 }));
};
const handlePriceMinChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const v = Math.max(0, toNumber(e.target.value));
setFilters((f) => ({ ...f, priceMin: v }));
};
const isSelected = (partNumber: string) => Boolean(selectedItems[partNumber]);
const toggleSelect = (product: Product) => {
setSelectedItems((prev) => {
console.log('Toggling selection for product:', product);
const copy = { ...prev };
if (copy[product.PartNumber]) delete copy[product.PartNumber];
else copy[product.PartNumber] = product;
return copy;
});
};
const toggleSelectAllVisible = (visibleProducts: Product[]) => {
setSelectedItems((prev) => {
const copy = { ...prev };
const allSelected = visibleProducts.every((product) => copy[product.PartNumber]);
if (allSelected) {
visibleProducts.forEach((product) => {
if (copy[product.PartNumber]) delete copy[product.PartNumber];
});
} else {
visibleProducts.forEach((product) => {
copy[product.PartNumber] = product;
});
}
return copy;
});
};
const clearVisibleSelection = (visibleProducts: Product[]) => {
setSelectedItems((prev) => {
const copy = { ...prev };
visibleProducts.forEach((product) => {
if (copy[product.PartNumber]) delete copy[product.PartNumber];
});
return copy;
});
};
const selectedCount = useMemo(() => Object.keys(selectedItems).length, [selectedItems]);
const selectedArray = useMemo(() => Object.values(selectedItems), [selectedItems]);
const quantityToList = (qty: number) => {
if (qty <= 1) return 1;
if (qty >= 2 && qty <= 4) return qty;
return 4;
};
const [brandLogos, setBrandLogos] = useState<BrandLogoMap>({});
useEffect(() => {
fetch("/data/brandMap.json")
.then(res => res.json())
.then(data => {
const normalized: BrandLogoMap = {};
Object.keys(data).forEach(key => {
const cleanKey = key.trim().toLowerCase();
normalized[cleanKey] = data[key];
});
setBrandLogos(normalized);
});
}, []);
const handleAddSelectedProducts = async () => {
console.log('Adding selected products:', selectedArray);
const filteredForQueue = selectedArray.filter((product) => invOf(product) > 0);
console.log('Selected products for queue:', filteredForQueue);
const payloadProducts = filteredForQueue.map((product) => {
const brandLogo = brandLogoByName[product.Brand?.toLowerCase()] || '';
return {
...product,
quantityToList: quantityToList(invOf(product)),
brandLogo,
brand: {
id: product.Brand,
name: product.Brand,
logo: brandLogo,
},
};
});
console.log('Payload products with full structure:', payloadProducts, userId, EBAYSTOREID);
setToast(`Queued ${payloadProducts.length} product(s)`);
try {
const userId = typeof window !== 'undefined' ? sessionStorage.getItem('USERID') : null;
const saveRes = await fetch('https://ebay.backend.data4autos.com/api/ebay/motorstate-finalize-publish', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userKey: EBAYSTOREID,
products: payloadProducts,
userId: userId,
}),
});
const saveData = await saveRes.json();
console.log('Save response:', saveData);
setToastActive(true);
setAddedIds(prev => {
const next = new Set(prev);
payloadProducts.forEach((p) => {
if (p?.PartNumber) next.add(String(p.PartNumber));
});
return next;
});
setAddedPartNumbers(prev => {
const next = new Set(prev);
payloadProducts.forEach((p) => {
if (p?.PartNumber) next.add(String(p.PartNumber));
});
return next;
});
setTimeout(() => setToastActive(false), 3000);
} catch (e) {
console.error(e);
setToast('Failed to queue products');
}
};
if (Turn14Enabled === false) {
return (
<div className="p-6 text-center">
<h1 className="text-xl font-semibold">Turn14 isn't connected yet</h1>
<p className="text-gray-600">Please connect Turn14 to continue.</p>
</div>
);
}
const getHeaderStatus = () => {
if (!expandedBrand) return `Selected: ${selectedCount}`;
const baseProducts = getProductsForBrand(expandedBrand);
const afterMake = applyFitmentFilters(baseProducts);
const afterSearch = afterMake.filter((product) => productMatchesQuery(product, searchText));
const afterInv = applyInventoryFilters(afterSearch);
const afterPrice = applyPriceFilters(afterInv);
return `Showing ${afterPrice.length} of ${baseProducts.length}`;
};
useEffect(() => {
const fetchAddedProducts = async () => {
if (!userId) return;
try {
const url = `https://ebay.backend.data4autos.com/api/user-products?userid=${encodeURIComponent(
userId
)}&page=1&pageSize=10000`;
const res = await fetch(url, { method: 'GET' });
const data = await res.json();
console.log('Added products response:', data);
const ids = new Set<string>();
const partnos = new Set<string>();
(data?.items ?? []).forEach((row: any) => {
if (row?.id) ids.add(String(row.id));
if (row?.sku) ids.add(String(row.sku));
if (row?.partNumber) partnos.add(String(row.partNumber));
if (row?.PartNumber) partnos.add(String(row.PartNumber));
});
setAddedIds(ids);
setAddedPartNumbers(partnos);
} catch (e) {
console.error('Failed to fetch added products:', e);
}
};
fetchAddedProducts();
}, [userId]);
const brandLogoByName = useMemo(() => {
const m: Record<string, string> = {};
brands.forEach(b => { if (b.name) m[b.name.toLowerCase()] = b.logo || ''; });
return m;
}, [brands]);
return (
<div className="bg-gradient-to-br from-[#00d1ff]/10 via-white to-[#00d1ff]/20 min-h-screen">
{/* Sticky Header */}
<div
className={`sticky top-14 z-10 transition-all duration-300 ${isScrolled ? 'bg-white/95 backdrop-blur-md shadow-lg py-3' : 'bg-white/80 backdrop-blur-sm py-4'
}`}
>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div className="flex flex-col">
<h1 className="text-2xl font-bold bg-[#00d1ff] to-purple-600 bg-clip-text text-[#00d1ff]">
Data4Autos Turn14 Manage Brand Products
</h1>
<p className="text-sm text-gray-500 mt-1">{getHeaderStatus()}</p>
<p className="text-sm font-medium text-[#00d1ff] mt-1">
Selected: <span className="font-semibold">{selectedCount}</span>
</p>
</div>
<div className="flex flex-col sm:flex-row gap-3 items-stretch sm:items-center w-full md:w-auto">
<div className="relative flex-grow">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg className="h-5 w-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
clipRule="evenodd"
/>
</svg>
</div>
<input
type="text"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
placeholder="Search products… (PartNumber, Description, Brand, Category)"
className="block w-full pl-10 pr-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
/>
</div>
<div className="flex flex-wrap items-center gap-3">
<label className="flex items-center gap-2 text-sm font-medium text-gray-700 cursor-pointer select-none">
<div className="relative">
<input
type="checkbox"
checked={inStockOnly}
onChange={() => setInStockOnly((v) => !v)}
className="sr-only"
/>
<div className={`block w-10 h-6 rounded-full transition-colors ${inStockOnly ? 'bg-[#00d1ff]' : 'bg-gray-300'}`}></div>
<div
className={`absolute left-1 top-1 bg-white w-4 h-4 rounded-full transition-transform ${inStockOnly ? 'transform translate-x-4' : ''
}`}
></div>
</div>
In Stock Only
</label>
<button
onClick={handleAddSelectedProducts}
disabled={selectedCount === 0}
className="px-5 py-2.5 bg-[#00d1ff] text-white font-medium rounded-lg hover:from-[#00d1ff] hover:to-purple-700 transition-all duration-300 transform hover:-translate-y-0.5 disabled:opacity-50 disabled:transform-none disabled:cursor-not-allowed flex items-center gap-2 shadow-md hover:shadow-lg"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
</svg>
Add {selectedCount} Product{selectedCount === 1 ? '' : 's'}
</button>
</div>
</div>
</div>
{/* Global Filters */}
<div className="mt-3 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3">
<div className="text-sm text-gray-700">
<div className="mb-1 font-medium">Filter Make</div>
<input
type="text"
value={filters.make}
onChange={(e) => setFilters(f => ({ ...f, make: e.target.value }))}
placeholder="Filter by description..."
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 w-full"
/>
</div>
<div className="text-sm text-gray-700">
<div className="mb-1 font-medium">Inventory Range</div>
<div className="flex items-center gap-2">
<input
type="number"
min={0}
value={filters.invMin}
onChange={handleInvMinChange}
className="w-24 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="Min"
/>
<span>to</span>
<input
type="number"
min={0}
value={filters.invMax}
onChange={handleInvMaxChange}
className="w-24 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="Max"
/>
</div>
</div>
<div className="text-sm text-gray-700">
<div className="mb-1 font-medium">Price Floor</div>
<div className="flex items-center gap-3">
<input
type="range"
min={0}
max={5000}
step={5}
value={filters.priceMin}
onChange={handlePriceMinChange}
className="w-full"
aria-label="Minimum price"
/>
<input
type="number"
min={0}
value={filters.priceMin}
onChange={handlePriceMinChange}
className="w-24 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div className="text-xs text-gray-500 mt-1">Showing items with price ${filters.priceMin}</div>
</div>
</div>
</div>
</div>
{/* Brands/Products */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pt-6 pb-16">
{brands.length === 0 ? (
<div className="flex items-center justify-center py-24 bg-gradient-to-b from-gray-50 to-gray-100">
<div className="relative text-center bg-white/80 backdrop-blur-md border border-white/40 shadow-2xl rounded-3xl p-10 transition-all duration-500 hover:scale-[1.02]">
<div className="w-20 h-20 mx-auto flex items-center justify-center rounded-full bg-[#00d1ff] shadow-lg">
<svg
className="w-10 h-10 text-white"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
</div>
<h3 className="mt-6 text-2xl font-semibold text-gray-800 tracking-tight">
No Brands Selected Yet
</h3>
<p className="mt-3 text-gray-500 max-w-sm mx-auto">
Add brands to view and explore their products here.
</p>
<div className="absolute -top-10 -right-10 w-32 h-32 bg-[#00d1ff] rounded-full blur-3xl"></div>
<div className="absolute -bottom-10 -left-10 w-32 h-32 bg-[#00d1ff] rounded-full blur-3xl"></div>
</div>
</div>
) : (
brands.map((brand) => {
const baseProducts = getProductsForBrand(brand.name);
const filteredFitment = applyFitmentFilters(baseProducts);
const filteredSearch = filteredFitment.filter((product) => productMatchesQuery(product, searchText));
const filteredInv = applyInventoryFilters(filteredSearch);
const filteredProducts = applyPriceFilters(filteredInv);
const allVisibleSelected = filteredProducts.length > 0 && filteredProducts.every((product) => isSelected(product.PartNumber));
const anyVisibleSelected = filteredProducts.some((product) => isSelected(product.PartNumber));
return (
<div key={brand.name} className="mb-6">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-3">
{brand.name && (
<img
src={
brandLogos[brand.name.trim().toLowerCase()]?.logo ??
"https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png"
}
alt={brand.name}
className="w-14 object-contain"
/>
)}
<div className="text-sm">
<div className="font-semibold">{brand.name}</div>
</div>
</div>
<button
onClick={() => toggleBrandItems(brand.name)}
className="px-3 py-2 text-sm rounded-md border border-gray-300 hover:bg-gray-50"
>
{expandedBrand === brand.name ? 'Hide Products' : 'Show Products'}
</button>
</div>
{expandedBrand === brand.name && (
<div className="border border-gray-200 rounded-lg p-3 bg-white">
<div className="mb-3 flex items-center justify-between">
<div className="text-xs text-gray-600">
Showing <span className="font-semibold">{filteredProducts.length}</span> of{' '}
<span className="font-semibold">{baseProducts.length}</span>
{productsMap[brand.name]?.meta && (
<span className="ml-2 text-gray-400">
(Total: {productsMap[brand.name].meta.count})
</span>
)}
</div>
<div className="flex items-center gap-2">
<button
onClick={() => toggleSelectAllVisible(filteredProducts)}
className="text-xs px-3 py-1.5 rounded border border-gray-300 hover:bg-gray-50"
>
{allVisibleSelected ? 'Unselect All Visible' : 'Select All Visible'}
</button>
{anyVisibleSelected && (
<button
onClick={() => clearVisibleSelection(filteredProducts)}
className="text-xs px-3 py-1.5 rounded border border-gray-300 hover:bg-gray-50"
>
Clear Visible
</button>
)}
</div>
</div>
{loadingMap[brand.name] ? (
<div className="py-8 text-center text-gray-500">Loading products</div>
) : filteredProducts.length === 0 ? (
<div className="py-8 text-center text-gray-500">No products match the current filters.</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">
{filteredProducts.map((product) => {
const imgSrc = product["Image URL"] || 'https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png';
const qty = invOf(product);
const price = priceOf(product);
const computedQty = quantityToList(qty);
const isAlreadyAdded =
(product?.PartNumber && addedIds.has(String(product.PartNumber))) ||
(product?.PartNumber && addedPartNumbers.has(String(product.PartNumber)));
return (
<div key={product.PartNumber} className="border border-gray-200 rounded-lg p-3 hover:shadow-sm">
<div className="flex items-center justify-between mb-2">
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={isSelected(product.PartNumber)}
onChange={() => toggleSelect(product)}
className="h-4 w-4 rounded border-gray-300 text-[#00d1ff] focus:ring-blue-500"
/>
<span className="text-xs text-gray-600">Select</span>
</label>
<div className="flex items-center gap-2">
{isAlreadyAdded && (
<span className="text-[10px] px-2 py-0.5 rounded-full bg-green-100 text-green-700 border border-green-200">
Added
</span>
)}
<span className="text-[10px] px-2 py-0.5 rounded-full bg-slate-100 text-slate-700">
Will list: {computedQty}
</span>
</div>
</div>
<img
src={imgSrc}
alt={product.Description || 'Product'}
className="w-full h-24 object-contain mb-2 bg-gray-50 rounded"
/>
<div className="text-sm font-medium line-clamp-2">
{product.Description || 'Unnamed Product'}
</div>
<div className="text-xs text-gray-600 mt-1">
Part #: <span className="font-medium">{product.PartNumber || '-'}</span>
</div>
<div className="text-xs text-gray-600">
Brand: <span className="font-medium">{product.Brand || '-'}</span>
</div>
<div className="text-xs text-gray-600">
{product["Category Level 1"] || '-'} &gt; {product["Category Level 2"] || '-'}
</div>
<div className="text-xs">
Price:{' '}
<span className="font-semibold">
{Number.isFinite(price) && price > 0 ? `$${price.toFixed(2)}` : '-'}
</span>
</div>
<div className="text-xs text-gray-700">
Inventory: <span className="font-semibold">{qty}</span>
</div>
{product["Long Description(150)"] && (
<p className="mt-2 text-xs text-gray-700 line-clamp-3">
{product["Long Description(150)"]}
</p>
)}
</div>
);
})}
</div>
)}
</div>
)}
</div>
);
})
)}
</div>
{/* Toasts */}
{toast && (
<div className="fixed bottom-6 right-6 bg-gradient-to-r from-slate-800 to-black text-white px-6 py-4 rounded-2xl shadow-2xl z-30 animate-fade-in-up">
<div className="flex items-center gap-3">
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
</svg>
<span className="font-medium">{toast}</span>
</div>
</div>
)}
{toastActive && (
<div className="fixed bottom-6 right-6 bg-gradient-to-r from-green-500 to-emerald-600 text-white px-6 py-4 rounded-2xl shadow-2xl z-30 animate-fade-in-up">
<div className="flex items-center gap-3">
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
</svg>
<span className="font-medium">Products added successfully!</span>
</div>
</div>
)}
<style jsx global>{`
@keyframes fadeInUp {
from {
opacity: 0;
transform: translate3d(0, 40px, 0);
}
to {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
.animate-fade-in-up {
animation: fadeInUp 0.5s ease-out;
}
`}</style>
</div>
);
};
export default ManageBrandProducts;

View File

@ -0,0 +1,460 @@
// pages/manage-brand-products.tsx
'use client';
import { getAccessToken } from '@/utils/apiHelper';
import React, { useEffect, useMemo, useState } from 'react';
interface Brand {
id: string;
brandid: string;
name: string;
logo?: string;
}
interface Item {
id: string;
attributes: {
product_name?: string;
thumbnail?: string;
part_number?: string;
category?: string;
subcategory?: string;
price?: string;
part_description?: string;
regular_stock?: boolean;
ltl_freight_required?: boolean;
is_clearance_item?: boolean;
is_air_freight_prohibited?: boolean;
files?: { url: string }[];
fitmmentTags?: any;
};
inventoryQuantity?: number;
}
const makes_list_raw = [
'Alfa Romeo', 'Ferrari', 'Dodge', 'Subaru', 'Toyota', 'Volkswagen', 'Volvo', 'Audi', 'BMW', 'Buick', 'Cadillac',
'Chevrolet', 'Chrysler', 'CX Automotive', 'Nissan', 'Ford', 'Hyundai', 'Infiniti', 'Lexus', 'Mercury', 'Mazda',
'Oldsmobile', 'Plymouth', 'Pontiac', 'Rolls-Royce', 'Eagle', 'Lincoln', 'Mercedes-Benz', 'GMC', 'Saab', 'Honda',
'Saturn', 'Mitsubishi', 'Isuzu', 'Jeep',
];
const makes_list = makes_list_raw.sort();
interface ManageBrandProductsProps {
accessToken: string | null;
}
const ManageBrandProducts: React.FC<ManageBrandProductsProps> = ({ accessToken }) => {
const [brands, setBrands] = useState<Brand[]>([]);
const [itemsMap, setItemsMap] = useState<Record<string, Item[]>>({});
const [expandedBrand, setExpandedBrand] = useState<string | null>(null);
const [loadingMap, setLoadingMap] = useState<Record<string, boolean>>({});
const [filters, setFilters] = useState({ make: '' });
const [Turn14Enabled, setTurn14Enabled] = useState<boolean | null>(null);
// UI/Sticky state
const [isScrolled, setIsScrolled] = useState(false);
// Global Search
const [searchText, setSearchText] = useState<string>('');
// Selection
const [selectedItems, setSelectedItems] = useState<Record<string, Item>>({});
// In-stock toggle (pill switch)
const [inStockOnly, setInStockOnly] = useState<boolean>(false);
// Toasts
const [toastActive, setToastActive] = useState(false);
const [toast, setToast] = useState('');
// NOTE: sessionStorage is available (client component)
const userId = typeof window !== 'undefined' ? sessionStorage.getItem('USERID') : null;
const EBAYSTOREID = typeof window !== 'undefined' ? sessionStorage.getItem('EBAYSTOREID') : null;
useEffect(() => {
const fetchUserBrands = async () => {
try {
const res = await fetch(`https://ebay.backend.data4autos.com/api/brands/${userId}`, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
});
const data = await res.json();
setBrands(data);
const userSelectedIds = data.map((b: any) => String(b.brandid));
setToast(`Loaded ${userSelectedIds.length} selected brands`);
setTimeout(() => setToast(''), 4000);
} catch (error) {
console.error('Error fetching brands:', error);
setToast('Failed to load user brands');
setTimeout(() => setToast(''), 4000);
}
};
if (userId) fetchUserBrands();
}, [userId]);
useEffect(() => {
// Simulate Turn14 check
const t = setTimeout(() => setTurn14Enabled(true), 500);
return () => clearTimeout(t);
}, []);
useEffect(() => {
const onScroll = () => setIsScrolled(window.scrollY > 10);
window.addEventListener('scroll', onScroll);
return () => window.removeEventListener('scroll', onScroll);
}, []);
const toggleBrandItems = async (brandId: string) => {
if (expandedBrand === brandId) {
setExpandedBrand(null);
return;
}
setExpandedBrand(brandId);
if (!itemsMap[brandId]) {
setLoadingMap((prev) => ({ ...prev, [brandId]: true }));
try {
const res = await fetch(`https://turn14.data4autos.com/v1/items/brandallitems/${brandId}`, {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
});
const data1 = await res.json();
setItemsMap((prev) => ({ ...prev, [brandId]: data1 }));
} catch (err) {
console.error(err);
}
setLoadingMap((prev) => ({ ...prev, [brandId]: false }));
}
};
const applyFitmentFilters = (items: Item[]) => {
return items.filter((item) => {
if (!filters.make) return true;
const tags = item.attributes.fitmmentTags || {};
return (
(Array.isArray(tags.make) && tags.make.some((m: string) => m?.toLowerCase() === filters.make.toLowerCase())) ||
item.attributes.product_name?.toLowerCase().includes(filters.make.toLowerCase())
);
});
};
// Broad query match
const itemMatchesQuery = (item: Item, q: string): boolean => {
if (!q) return true;
const hay = `${item.id} ${JSON.stringify(item.attributes ?? {}, (_k, v) => v ?? '', 2)}`.toLowerCase();
return hay.includes(q.toLowerCase());
};
const handleFilterChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setFilters({ ...filters, make: e.target.value });
};
// Selection handlers
const isSelected = (id: string) => Boolean(selectedItems[id]);
const toggleSelect = (item: Item) => {
setSelectedItems((prev) => {
const copy = { ...prev };
if (copy[item.id]) delete copy[item.id];
else copy[item.id] = item;
return copy;
});
};
const selectedCount = useMemo(() => Object.keys(selectedItems).length, [selectedItems]);
const selectedArray = useMemo(() => Object.values(selectedItems), [selectedItems]);
const handleAddSelectedProducts = async () => {
setToast(`Queued ${selectedArray.length} product(s)`);
try {
const saveRes = await fetch('https://ebay.backend.data4autos.com/api/ebay/finalize-publish', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userKey: EBAYSTOREID,
products: selectedArray,
}),
});
const saveData = await saveRes.json();
console.log('Save response:', saveData);
setToastActive(true);
setTimeout(() => setToastActive(false), 3000);
} catch (e) {
console.error(e);
setToast('Failed to queue products');
}
};
if (Turn14Enabled === false) {
return (
<div className="p-6 text-center">
<h1 className="text-xl font-semibold">Turn14 isnt connected yet</h1>
<p className="text-gray-600">Please connect Turn14 to continue.</p>
</div>
);
}
const getHeaderStatus = () => {
// Aggregate counts only for the expanded brand (for precise UX) OR overall
if (!expandedBrand) return `Selected: ${selectedCount}`;
const base = itemsMap[expandedBrand] || [];
const afterMake = applyFitmentFilters(base);
const afterSearch = afterMake.filter((it) => itemMatchesQuery(it, searchText));
const afterStock = afterSearch.filter((it) => (inStockOnly ? (it.inventoryQuantity ?? 0) > 0 : true));
return `Showing ${afterStock.length} of ${base.length}`;
};
return (
<div className="bg-gradient-to-br from-slate-50 to-slate-200 min-h-screen">
{/* Sticky Header (pretty UI) */}
<div
className={`sticky top-0 z-20 transition-all duration-300 ${
isScrolled ? 'bg-white/95 backdrop-blur-md shadow-lg py-3' : 'bg-white/80 backdrop-blur-sm py-4'
}`}
>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div className="flex flex-col">
<h1 className="text-2xl font-bold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
Data4Autos Turn14 Manage Brand Products
</h1>
<p className="text-sm text-gray-500 mt-1">{getHeaderStatus()}</p>
<p className="text-sm font-medium text-blue-600 mt-1">
Selected: <span className="font-semibold">{selectedCount}</span>
</p>
</div>
<div className="flex flex-col sm:flex-row gap-3 items-stretch sm:items-center w-full md:w-auto">
{/* Search */}
<div className="relative flex-grow">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg className="h-5 w-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
clipRule="evenodd"
/>
</svg>
</div>
<input
type="text"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
placeholder="Search products… (name, part#, tags)"
className="block w-full pl-10 pr-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
/>
</div>
{/* In-stock-only pill toggle */}
<div className="flex flex-wrap items-center gap-3">
<label className="flex items-center gap-2 text-sm font-medium text-gray-700 cursor-pointer select-none">
<div className="relative">
<input
type="checkbox"
checked={inStockOnly}
onChange={() => setInStockOnly((v) => !v)}
className="sr-only"
/>
<div className={`block w-10 h-6 rounded-full transition-colors ${inStockOnly ? 'bg-blue-600' : 'bg-gray-300'}`}></div>
<div
className={`absolute left-1 top-1 bg-white w-4 h-4 rounded-full transition-transform ${
inStockOnly ? 'transform translate-x-4' : ''
}`}
></div>
</div>
In Stock Only
</label>
<button
onClick={handleAddSelectedProducts}
disabled={selectedCount === 0}
className="px-5 py-2.5 bg-gradient-to-r from-blue-600 to-purple-600 text-white font-medium rounded-lg hover:from-blue-700 hover:to-purple-700 transition-all duration-300 transform hover:-translate-y-0.5 disabled:opacity-50 disabled:transform-none disabled:cursor-not-allowed flex items-center gap-2 shadow-md hover:shadow-lg"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
</svg>
Add {selectedCount} Product{selectedCount === 1 ? '' : 's'}
</button>
</div>
</div>
</div>
{/* Optional: Make filter in header (global) */}
<div className="mt-3">
<label className="text-sm text-gray-700">
Filter Make:{' '}
<select
value={filters.make}
onChange={handleFilterChange}
className="ml-2 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
<option value="">All</option>
{makes_list.map((m) => (
<option key={m} value={m}>
{m}
</option>
))}
</select>
</label>
</div>
</div>
</div>
{/* Brands/Products */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pt-6 pb-16">
{brands.length === 0 ? (
<div className="text-center py-20">
<div className="inline-block p-4 bg-white rounded-xl shadow-md">
<svg className="w-16 h-16 mx-auto text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<h3 className="mt-4 text-xl font-medium text-gray-700">No brands selected yet</h3>
<p className="mt-2 text-gray-500">Add brands to view their products.</p>
</div>
</div>
) : (
brands.map((brand) => {
const baseItems = itemsMap[brand.brandid] || [];
const filteredFitment = applyFitmentFilters(baseItems);
const filteredSearch = filteredFitment.filter((it) => itemMatchesQuery(it, searchText));
const filteredItems = filteredSearch.filter((it) => (inStockOnly ? (it.inventoryQuantity ?? 0) > 0 : true));
return (
<div key={brand.brandid} className="mb-6">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-3">
{brand.logo && (
<img src={brand.logo} alt={brand.name} className="h-8 w-8 object-contain rounded bg-white border" />
)}
<div className="text-sm">
<div className="font-semibold">{brand.name}</div>
<div className="text-gray-500">ID: {brand.brandid}</div>
</div>
</div>
<button
onClick={() => toggleBrandItems(brand.brandid)}
className="px-3 py-2 text-sm rounded-md border border-gray-300 hover:bg-gray-50"
>
{expandedBrand === brand.brandid ? 'Hide Products' : 'Show Products'}
</button>
</div>
{expandedBrand === brand.brandid && (
<div className="border border-gray-200 rounded-lg p-3 bg-white">
<div className="mb-3 text-xs text-gray-600">
Showing <span className="font-semibold">{filteredItems.length}</span> of{' '}
<span className="font-semibold">{baseItems.length}</span>
</div>
{loadingMap[brand.brandid] ? (
<div className="py-8 text-center text-gray-500">Loading products</div>
) : filteredItems.length === 0 ? (
<div className="py-8 text-center text-gray-500">No products match the current filters.</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">
{filteredItems.map((item) => {
const imgSrc =
item.attributes.thumbnail ||
item?.attributes?.files?.[0]?.url ||
'https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png';
return (
<div key={item.id} className="border border-gray-200 rounded-lg p-3 hover:shadow-sm">
{/* Select checkbox */}
<div className="flex items-center gap-2 mb-2">
<input
type="checkbox"
checked={isSelected(item.id)}
onChange={() => toggleSelect(item)}
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-xs text-gray-600">Select</span>
</div>
<img
src={imgSrc}
alt={item.attributes.product_name || 'Product'}
className="w-full h-24 object-contain mb-2 bg-gray-50 rounded"
/>
<div className="text-sm font-medium line-clamp-2">
{item.attributes.product_name || 'Unnamed Product'}
</div>
<div className="text-xs text-gray-600 mt-1">
Part #: <span className="font-medium">{item.attributes.part_number || '-'}</span>
</div>
<div className="text-xs text-gray-600">
{item.attributes.category || '-'} &gt; {item.attributes.subcategory || '-'}
</div>
<div className="text-xs">
Price:{' '}
<span className="font-semibold">
{item.attributes.price ? `$${item.attributes.price}` : '-'}
</span>
</div>
<div className="text-xs text-gray-700">
Inventory: <span className="font-semibold">{item.inventoryQuantity ?? 0}</span>
</div>
{item.attributes.part_description && (
<p className="mt-2 text-xs text-gray-700 line-clamp-3">
{item.attributes.part_description}
</p>
)}
</div>
);
})}
</div>
)}
</div>
)}
</div>
);
})
)}
</div>
{/* Toasts */}
{toast && (
<div className="fixed bottom-6 right-6 bg-gradient-to-r from-slate-800 to-black text-white px-6 py-4 rounded-2xl shadow-2xl z-30 animate-fade-in-up">
<div className="flex items-center gap-3">
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
</svg>
<span className="font-medium">{toast}</span>
</div>
</div>
)}
{toastActive && (
<div className="fixed bottom-6 right-6 bg-gradient-to-r from-green-500 to-emerald-600 text-white px-6 py-4 rounded-2xl shadow-2xl z-30 animate-fade-in-up">
<div className="flex items-center gap-3">
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
</svg>
<span className="font-medium">Products added successfully!</span>
</div>
</div>
)}
{/* Tiny animation helper */}
<style jsx global>{`
@keyframes fadeInUp {
from {
opacity: 0;
transform: translate3d(0, 40px, 0);
}
to {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
.animate-fade-in-up {
animation: fadeInUp 0.5s ease-out;
}
`}</style>
</div>
);
};
export default ManageBrandProducts;

View File

@ -0,0 +1,625 @@
// pages/manage-brand-products.tsx
'use client';
import { getAccessToken } from '@/utils/apiHelper';
import React, { useEffect, useMemo, useState } from 'react';
interface Brand {
id: string;
brandid: string;
name: string;
logo?: string;
}
interface Item {
id: string;
attributes: {
product_name?: string;
thumbnail?: string;
part_number?: string;
category?: string;
subcategory?: string;
price?: string; // may come as string; we parse below
part_description?: string;
regular_stock?: boolean;
ltl_freight_required?: boolean;
is_clearance_item?: boolean;
is_air_freight_prohibited?: boolean;
files?: { url: string }[];
fitmmentTags?: any;
};
inventoryQuantity?: number;
}
const makes_list_raw = [
'Alfa Romeo', 'Ferrari', 'Dodge', 'Subaru', 'Toyota', 'Volkswagen', 'Volvo', 'Audi', 'BMW', 'Buick', 'Cadillac',
'Chevrolet', 'Chrysler', 'CX Automotive', 'Nissan', 'Ford', 'Hyundai', 'Infiniti', 'Lexus', 'Mercury', 'Mazda',
'Oldsmobile', 'Plymouth', 'Pontiac', 'Rolls-Royce', 'Eagle', 'Lincoln', 'Mercedes-Benz', 'GMC', 'Saab', 'Honda',
'Saturn', 'Mitsubishi', 'Isuzu', 'Jeep',
];
const makes_list = makes_list_raw.sort();
interface ManageBrandProductsProps {
accessToken: string | null;
}
const toNumber = (val: unknown): number => {
if (typeof val === 'number') return val;
if (typeof val === 'string') {
const cleaned = val.replace(/[^0-9.\-]/g, '');
const n = parseFloat(cleaned);
return Number.isFinite(n) ? n : 0;
}
return 0;
};
const priceOf = (item: Item) => toNumber(item.attributes?.price);
const invOf = (item: Item) => toNumber(item.inventoryQuantity);
const ManageBrandProducts: React.FC<ManageBrandProductsProps> = ({ accessToken }) => {
const [brands, setBrands] = useState<Brand[]>([]);
const [itemsMap, setItemsMap] = useState<Record<string, Item[]>>({});
const [expandedBrand, setExpandedBrand] = useState<string | null>(null);
const [loadingMap, setLoadingMap] = useState<Record<string, boolean>>({});
const [filters, setFilters] = useState({
make: '',
invMin: 0,
invMax: 9999,
priceMin: 0,
});
const [Turn14Enabled, setTurn14Enabled] = useState<boolean | null>(null);
// UI/Sticky state
const [isScrolled, setIsScrolled] = useState(false);
// Global Search
const [searchText, setSearchText] = useState<string>('');
// Selection
const [selectedItems, setSelectedItems] = useState<Record<string, Item>>({});
// In-stock toggle (pill switch)
const [inStockOnly, setInStockOnly] = useState<boolean>(false);
// Toasts
const [toastActive, setToastActive] = useState(false);
const [toast, setToast] = useState('');
// NOTE: sessionStorage is available (client component)
const userId = typeof window !== 'undefined' ? sessionStorage.getItem('USERID') : null;
const EBAYSTOREID = typeof window !== 'undefined' ? sessionStorage.getItem('EBAYSTOREID') : null;
useEffect(() => {
const fetchUserBrands = async () => {
try {
const res = await fetch(`https://ebay.backend.data4autos.com/api/brands/${userId}`, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
});
const data = await res.json();
setBrands(data);
const userSelectedIds = data.map((b: any) => String(b.brandid));
setToast(`Loaded ${userSelectedIds.length} selected brands`);
setTimeout(() => setToast(''), 4000);
} catch (error) {
console.error('Error fetching brands:', error);
setToast('Failed to load user brands');
setTimeout(() => setToast(''), 4000);
}
};
if (userId) fetchUserBrands();
}, [userId]);
useEffect(() => {
// Simulate Turn14 check
const t = setTimeout(() => setTurn14Enabled(true), 500);
return () => clearTimeout(t);
}, []);
useEffect(() => {
const onScroll = () => setIsScrolled(window.scrollY > 10);
window.addEventListener('scroll', onScroll);
return () => window.removeEventListener('scroll', onScroll);
}, []);
const toggleBrandItems = async (brandId: string) => {
if (expandedBrand === brandId) {
setExpandedBrand(null);
return;
}
setExpandedBrand(brandId);
if (!itemsMap[brandId]) {
setLoadingMap((prev) => ({ ...prev, [brandId]: true }));
try {
const res = await fetch(`https://turn14.data4autos.com/v1/items/brandallitems/${brandId}`, {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
});
const data1 = await res.json();
setItemsMap((prev) => ({ ...prev, [brandId]: data1 }));
} catch (err) {
console.error(err);
}
setLoadingMap((prev) => ({ ...prev, [brandId]: false }));
}
};
const applyFitmentFilters = (items: Item[]) => {
return items.filter((item) => {
if (!filters.make) return true;
const tags = item.attributes.fitmmentTags || {};
return (
(Array.isArray(tags.make) && tags.make.some((m: string) => m?.toLowerCase() === filters.make.toLowerCase())) ||
item.attributes.product_name?.toLowerCase().includes(filters.make.toLowerCase())
);
});
};
const applyInventoryFilters = (items: Item[]) =>
items.filter((it) => {
const qty = invOf(it);
if (inStockOnly && qty <= 0) return false;
return qty >= filters.invMin && qty <= filters.invMax;
});
const applyPriceFilters = (items: Item[]) =>
items.filter((it) => priceOf(it) >= filters.priceMin);
// Broad query match
const itemMatchesQuery = (item: Item, q: string): boolean => {
if (!q) return true;
const hay = `${item.id} ${JSON.stringify(item.attributes ?? {}, (_k, v) => v ?? '', 2)}`.toLowerCase();
return hay.includes(q.toLowerCase());
};
const handleMakeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setFilters((f) => ({ ...f, make: e.target.value }));
};
const handleInvMinChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const v = Math.max(0, toNumber(e.target.value));
setFilters((f) => ({ ...f, invMin: v }));
};
const handleInvMaxChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const v = Math.max(0, toNumber(e.target.value));
setFilters((f) => ({ ...f, invMax: v || 9999 }));
};
const handlePriceMinChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const v = Math.max(0, toNumber(e.target.value));
setFilters((f) => ({ ...f, priceMin: v }));
};
const isSelected = (id: string) => Boolean(selectedItems[id]);
const toggleSelect = (item: Item) => {
setSelectedItems((prev) => {
const copy = { ...prev };
if (copy[item.id]) delete copy[item.id];
else copy[item.id] = item;
return copy;
});
};
const toggleSelectAllVisible = (visibleItems: Item[]) => {
setSelectedItems((prev) => {
const copy = { ...prev };
const allSelected = visibleItems.every((it) => copy[it.id]);
if (allSelected) {
// Clear only the visible ones
visibleItems.forEach((it) => {
if (copy[it.id]) delete copy[it.id];
});
} else {
visibleItems.forEach((it) => {
copy[it.id] = it;
});
}
return copy;
});
};
const clearVisibleSelection = (visibleItems: Item[]) => {
setSelectedItems((prev) => {
const copy = { ...prev };
visibleItems.forEach((it) => {
if (copy[it.id]) delete copy[it.id];
});
return copy;
});
};
const selectedCount = useMemo(() => Object.keys(selectedItems).length, [selectedItems]);
const selectedArray = useMemo(() => Object.values(selectedItems), [selectedItems]);
const quantityToList = (qty: number) => {
if (qty <= 1) return 1;
if (qty >= 2 && qty <= 8) return qty;
return 8;
// If you'd rather skip low/zero stock instead of listing 1, change the first line to: if (qty <= 1) return 0;
};
const handleAddSelectedProducts = async () => {
const filteredForQueue = selectedArray.filter((it) => invOf(it) > 0);
const payloadProducts = filteredForQueue.map((item) => ({
...item,
quantityToList: quantityToList(invOf(item)),
}));
setToast(`Queued ${payloadProducts.length} product(s)`);
try {
const saveRes = await fetch('https://ebay.backend.data4autos.com/api/ebay/finalize-publish', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userKey: EBAYSTOREID,
products: payloadProducts,
}),
});
const saveData = await saveRes.json();
console.log('Save response:', saveData);
setToastActive(true);
setTimeout(() => setToastActive(false), 3000);
} catch (e) {
console.error(e);
setToast('Failed to queue products');
}
};
if (Turn14Enabled === false) {
return (
<div className="p-6 text-center">
<h1 className="text-xl font-semibold">Turn14 isnt connected yet</h1>
<p className="text-gray-600">Please connect Turn14 to continue.</p>
</div>
);
}
const getHeaderStatus = () => {
if (!expandedBrand) return `Selected: ${selectedCount}`;
const base = itemsMap[expandedBrand] || [];
const afterMake = applyFitmentFilters(base);
const afterSearch = afterMake.filter((it) => itemMatchesQuery(it, searchText));
const afterInv = applyInventoryFilters(afterSearch);
const afterPrice = applyPriceFilters(afterInv);
return `Showing ${afterPrice.length} of ${base.length}`;
};
return (
<div className="bg-gradient-to-br from-slate-50 to-slate-200 min-h-screen">
{/* Sticky Header */}
<div
className={`sticky top-14 z-10 transition-all duration-300 ${
isScrolled ? 'bg-white/95 backdrop-blur-md shadow-lg py-3' : 'bg-white/80 backdrop-blur-sm py-4'
}`}
>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div className="flex flex-col">
<h1 className="text-2xl font-bold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
Data4Autos Turn14 Manage Brand Products
</h1>
<p className="text-sm text-gray-500 mt-1">{getHeaderStatus()}</p>
<p className="text-sm font-medium text-blue-600 mt-1">
Selected: <span className="font-semibold">{selectedCount}</span>
</p>
</div>
<div className="flex flex-col sm:flex-row gap-3 items-stretch sm:items-center w-full md:w-auto">
{/* Search */}
<div className="relative flex-grow">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg className="h-5 w-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
clipRule="evenodd"
/>
</svg>
</div>
<input
type="text"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
placeholder="Search products… (name, part#, tags)"
className="block w-full pl-10 pr-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
/>
</div>
{/* In-stock-only pill toggle */}
<div className="flex flex-wrap items-center gap-3">
<label className="flex items-center gap-2 text-sm font-medium text-gray-700 cursor-pointer select-none">
<div className="relative">
<input
type="checkbox"
checked={inStockOnly}
onChange={() => setInStockOnly((v) => !v)}
className="sr-only"
/>
<div className={`block w-10 h-6 rounded-full transition-colors ${inStockOnly ? 'bg-blue-600' : 'bg-gray-300'}`}></div>
<div
className={`absolute left-1 top-1 bg-white w-4 h-4 rounded-full transition-transform ${
inStockOnly ? 'transform translate-x-4' : ''
}`}
></div>
</div>
In Stock Only
</label>
<button
onClick={handleAddSelectedProducts}
disabled={selectedCount === 0}
className="px-5 py-2.5 bg-gradient-to-r from-blue-600 to-purple-600 text-white font-medium rounded-lg hover:from-blue-700 hover:to-purple-700 transition-all duration-300 transform hover:-translate-y-0.5 disabled:opacity-50 disabled:transform-none disabled:cursor-not-allowed flex items-center gap-2 shadow-md hover:shadow-lg"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
</svg>
Add {selectedCount} Product{selectedCount === 1 ? '' : 's'}
</button>
</div>
</div>
</div>
{/* Global Filters */}
<div className="mt-3 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3">
{/* Make */}
<label className="text-sm text-gray-700 flex items-center">
<span className="mr-2 whitespace-nowrap">Filter Make:</span>
<select
value={filters.make}
onChange={handleMakeChange}
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 w-full"
>
<option value="">All</option>
{makes_list.map((m) => (
<option key={m} value={m}>
{m}
</option>
))}
</select>
</label>
{/* Inventory Range */}
<div className="text-sm text-gray-700">
<div className="mb-1 font-medium">Inventory Range</div>
<div className="flex items-center gap-2">
<input
type="number"
min={0}
value={filters.invMin}
onChange={handleInvMinChange}
className="w-24 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="Min"
/>
<span>to</span>
<input
type="number"
min={0}
value={filters.invMax}
onChange={handleInvMaxChange}
className="w-24 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="Max"
/>
</div>
</div>
{/* Price Minimum (Slider + number) */}
<div className="text-sm text-gray-700">
<div className="mb-1 font-medium">Price Floor</div>
<div className="flex items-center gap-3">
<input
type="range"
min={0}
max={5000}
step={5}
value={filters.priceMin}
onChange={handlePriceMinChange}
className="w-full"
aria-label="Minimum price"
/>
<input
type="number"
min={0}
value={filters.priceMin}
onChange={handlePriceMinChange}
className="w-24 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div className="text-xs text-gray-500 mt-1">Showing items with price ${filters.priceMin}</div>
</div>
</div>
</div>
</div>
{/* Brands/Products */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pt-6 pb-16">
{brands.length === 0 ? (
<div className="text-center py-20">
<div className="inline-block p-4 bg-white rounded-xl shadow-md">
<svg className="w-16 h-16 mx-auto text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<h3 className="mt-4 text-xl font-medium text-gray-700">No brands selected yet</h3>
<p className="mt-2 text-gray-500">Add brands to view their products.</p>
</div>
</div>
) : (
brands.map((brand) => {
const baseItems = itemsMap[brand.brandid] || [];
const filteredFitment = applyFitmentFilters(baseItems);
const filteredSearch = filteredFitment.filter((it) => itemMatchesQuery(it, searchText));
const filteredInv = applyInventoryFilters(filteredSearch);
const filteredItems = applyPriceFilters(filteredInv);
const allVisibleSelected = filteredItems.length > 0 && filteredItems.every((it) => isSelected(it.id));
const anyVisibleSelected = filteredItems.some((it) => isSelected(it.id));
return (
<div key={brand.brandid} className="mb-6">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-3">
{brand.logo && (
<img src={brand.logo} alt={brand.name} className="h-8 w-8 object-contain rounded bg-white border" />
)}
<div className="text-sm">
<div className="font-semibold">{brand.name}</div>
<div className="text-gray-500">ID: {brand.brandid}</div>
</div>
</div>
<button
onClick={() => toggleBrandItems(brand.brandid)}
className="px-3 py-2 text-sm rounded-md border border-gray-300 hover:bg-gray-50"
>
{expandedBrand === brand.brandid ? 'Hide Products' : 'Show Products'}
</button>
</div>
{expandedBrand === brand.brandid && (
<div className="border border-gray-200 rounded-lg p-3 bg-white">
<div className="mb-3 flex items-center justify-between">
<div className="text-xs text-gray-600">
Showing <span className="font-semibold">{filteredItems.length}</span> of{' '}
<span className="font-semibold">{baseItems.length}</span>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => toggleSelectAllVisible(filteredItems)}
className="text-xs px-3 py-1.5 rounded border border-gray-300 hover:bg-gray-50"
>
{allVisibleSelected ? 'Unselect All Visible' : 'Select All Visible'}
</button>
{anyVisibleSelected && (
<button
onClick={() => clearVisibleSelection(filteredItems)}
className="text-xs px-3 py-1.5 rounded border border-gray-300 hover:bg-gray-50"
>
Clear Visible
</button>
)}
</div>
</div>
{loadingMap[brand.brandid] ? (
<div className="py-8 text-center text-gray-500">Loading products</div>
) : filteredItems.length === 0 ? (
<div className="py-8 text-center text-gray-500">No products match the current filters.</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">
{filteredItems.map((item) => {
const imgSrc =
item.attributes.thumbnail ||
item?.attributes?.files?.[0]?.url ||
'https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png';
const qty = invOf(item);
const price = priceOf(item);
const computedQty = quantityToList(qty);
return (
<div key={item.id} className="border border-gray-200 rounded-lg p-3 hover:shadow-sm">
{/* Select checkbox */}
<div className="flex items-center justify-between mb-2">
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={isSelected(item.id)}
onChange={() => toggleSelect(item)}
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-xs text-gray-600">Select</span>
</label>
<span className="text-[10px] px-2 py-0.5 rounded-full bg-slate-100 text-slate-700">
Will list: {computedQty}
</span>
</div>
<img
src={imgSrc}
alt={item.attributes.product_name || 'Product'}
className="w-full h-24 object-contain mb-2 bg-gray-50 rounded"
/>
<div className="text-sm font-medium line-clamp-2">
{item.attributes.product_name || 'Unnamed Product'}
</div>
<div className="text-xs text-gray-600 mt-1">
Part #: <span className="font-medium">{item.attributes.part_number || '-'}</span>
</div>
<div className="text-xs text-gray-600">
{item.attributes.category || '-'} &gt; {item.attributes.subcategory || '-'}
</div>
<div className="text-xs">
Price:{' '}
<span className="font-semibold">
{Number.isFinite(price) && price > 0 ? `$${price.toFixed(2)}` : '-'}
</span>
</div>
<div className="text-xs text-gray-700">
Inventory: <span className="font-semibold">{qty}</span>
</div>
{item.attributes.part_description && (
<p className="mt-2 text-xs text-gray-700 line-clamp-3">
{item.attributes.part_description}
</p>
)}
</div>
);
})}
</div>
)}
</div>
)}
</div>
);
})
)}
</div>
{/* Toasts */}
{toast && (
<div className="fixed bottom-6 right-6 bg-gradient-to-r from-slate-800 to-black text-white px-6 py-4 rounded-2xl shadow-2xl z-30 animate-fade-in-up">
<div className="flex items-center gap-3">
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
</svg>
<span className="font-medium">{toast}</span>
</div>
</div>
)}
{toastActive && (
<div className="fixed bottom-6 right-6 bg-gradient-to-r from-green-500 to-emerald-600 text-white px-6 py-4 rounded-2xl shadow-2xl z-30 animate-fade-in-up">
<div className="flex items-center gap-3">
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
</svg>
<span className="font-medium">Products added successfully!</span>
</div>
</div>
)}
{/* Tiny animation helper */}
<style jsx global>{`
@keyframes fadeInUp {
from {
opacity: 0;
transform: translate3d(0, 40px, 0);
}
to {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
.animate-fade-in-up {
animation: fadeInUp 0.5s ease-out;
}
`}</style>
</div>
);
};
export default ManageBrandProducts;

View File

@ -0,0 +1,638 @@
// pages/manage-brand-products.tsx
'use client';
import { getAccessToken } from '@/utils/apiHelper';
import { getAccessToken_client } from '@/utils/apiHelper_client';
import React, { useEffect, useMemo, useState } from 'react';
interface Brand {
id: string;
brandid: string;
name: string;
logo?: string;
}
interface Item {
id: string;
attributes: {
product_name?: string;
thumbnail?: string;
part_number?: string;
category?: string;
subcategory?: string;
price?: string; // may come as string; we parse below
part_description?: string;
regular_stock?: boolean;
ltl_freight_required?: boolean;
is_clearance_item?: boolean;
is_air_freight_prohibited?: boolean;
files?: { url: string }[];
fitmmentTags?: any;
};
inventoryQuantity?: number;
}
const makes_list_raw = [
'Alfa Romeo', 'Ferrari', 'Dodge', 'Subaru', 'Toyota', 'Volkswagen', 'Volvo', 'Audi', 'BMW', 'Buick', 'Cadillac',
'Chevrolet', 'Chrysler', 'CX Automotive', 'Nissan', 'Ford', 'Hyundai', 'Infiniti', 'Lexus', 'Mercury', 'Mazda',
'Oldsmobile', 'Plymouth', 'Pontiac', 'Rolls-Royce', 'Eagle', 'Lincoln', 'Mercedes-Benz', 'GMC', 'Saab', 'Honda',
'Saturn', 'Mitsubishi', 'Isuzu', 'Jeep',
];
const makes_list = makes_list_raw.sort();
interface ManageBrandProductsProps {
accessToken: string | null;
}
const toNumber = (val: unknown): number => {
if (typeof val === 'number') return val;
if (typeof val === 'string') {
const cleaned = val.replace(/[^0-9.\-]/g, '');
const n = parseFloat(cleaned);
return Number.isFinite(n) ? n : 0;
}
return 0;
};
const priceOf = (item: Item) => toNumber(item.attributes?.price);
const invOf = (item: Item) => toNumber(item.inventoryQuantity);
const ManageBrandProducts: React.FC<ManageBrandProductsProps> = () => {
const [accessToken, setaccessToken] = useState<string>('');
const [brands, setBrands] = useState<Brand[]>([]);
const [itemsMap, setItemsMap] = useState<Record<string, Item[]>>({});
const [expandedBrand, setExpandedBrand] = useState<string | null>(null);
const [loadingMap, setLoadingMap] = useState<Record<string, boolean>>({});
const [filters, setFilters] = useState({
make: '',
invMin: 0,
invMax: 9999,
priceMin: 0,
});
const [Turn14Enabled, setTurn14Enabled] = useState<boolean | null>(null);
// UI/Sticky state
const [isScrolled, setIsScrolled] = useState(false);
// Global Search
const [searchText, setSearchText] = useState<string>('');
// Selection
const [selectedItems, setSelectedItems] = useState<Record<string, Item>>({});
// In-stock toggle (pill switch)
const [inStockOnly, setInStockOnly] = useState<boolean>(false);
// Toasts
const [toastActive, setToastActive] = useState(false);
const [toast, setToast] = useState('');
// NOTE: sessionStorage is available (client component)
const userId = typeof window !== 'undefined' ? sessionStorage.getItem('USERID') : null;
const EBAYSTOREID = typeof window !== 'undefined' ? sessionStorage.getItem('EBAYSTOREID') : null;
useEffect(() => {
const fetchUserBrands = async () => {
try {
const accessToken = await getAccessToken_client();
setaccessToken(accessToken || '');
console.log('Got access token:', accessToken);
const res = await fetch(`https://ebay.backend.data4autos.com/api/brands/${userId}`, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
});
const data = await res.json();
setBrands(data);
const userSelectedIds = data.map((b: any) => String(b.brandid));
setToast(`Loaded ${userSelectedIds.length} selected brands`);
setTimeout(() => setToast(''), 4000);
} catch (error) {
console.error('Error fetching brands:', error);
setToast('Failed to load user brands');
setTimeout(() => setToast(''), 4000);
}
};
if (userId) fetchUserBrands();
}, [userId]);
useEffect(() => {
// Simulate Turn14 check
const t = setTimeout(() => setTurn14Enabled(true), 500);
return () => clearTimeout(t);
}, []);
useEffect(() => {
const onScroll = () => setIsScrolled(window.scrollY > 10);
window.addEventListener('scroll', onScroll);
return () => window.removeEventListener('scroll', onScroll);
}, []);
const toggleBrandItems = async (brandId: string) => {
if (expandedBrand === brandId) {
setExpandedBrand(null);
return;
}
setExpandedBrand(brandId);
if (!itemsMap[brandId]) {
setLoadingMap((prev) => ({ ...prev, [brandId]: true }));
try {
const res = await fetch(`https://turn14.data4autos.com/v1/items/brandallitems/${brandId}`, {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
});
const data1 = await res.json();
setItemsMap((prev) => ({ ...prev, [brandId]: data1 }));
} catch (err) {
console.error(err);
}
setLoadingMap((prev) => ({ ...prev, [brandId]: false }));
}
};
const applyFitmentFilters = (items: Item[]) => {
return items.filter((item) => {
if (!filters.make) return true;
const tags = item.attributes.fitmmentTags || {};
return (
(Array.isArray(tags.make) && tags.make.some((m: string) => m?.toLowerCase() === filters.make.toLowerCase())) ||
item.attributes.product_name?.toLowerCase().includes(filters.make.toLowerCase())
);
});
};
const applyInventoryFilters = (items: Item[]) =>
items.filter((it) => {
const qty = invOf(it);
if (inStockOnly && qty <= 0) return false;
return qty >= filters.invMin && qty <= filters.invMax;
});
const applyPriceFilters = (items: Item[]) =>
items.filter((it) => priceOf(it) >= filters.priceMin);
// Broad query match
const itemMatchesQuery = (item: Item, q: string): boolean => {
if (!q) return true;
const hay = `${item.id} ${JSON.stringify(item.attributes ?? {}, (_k, v) => v ?? '', 2)}`.toLowerCase();
return hay.includes(q.toLowerCase());
};
const handleMakeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setFilters((f) => ({ ...f, make: e.target.value }));
};
const handleInvMinChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const v = Math.max(0, toNumber(e.target.value));
setFilters((f) => ({ ...f, invMin: v }));
};
const handleInvMaxChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const v = Math.max(0, toNumber(e.target.value));
setFilters((f) => ({ ...f, invMax: v || 9999 }));
};
const handlePriceMinChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const v = Math.max(0, toNumber(e.target.value));
setFilters((f) => ({ ...f, priceMin: v }));
};
const isSelected = (id: string) => Boolean(selectedItems[id]);
const toggleSelect = (item: Item) => {
setSelectedItems((prev) => {
const copy = { ...prev };
if (copy[item.id]) delete copy[item.id];
else copy[item.id] = item;
return copy;
});
};
const toggleSelectAllVisible = (visibleItems: Item[]) => {
setSelectedItems((prev) => {
const copy = { ...prev };
const allSelected = visibleItems.every((it) => copy[it.id]);
if (allSelected) {
// Clear only the visible ones
visibleItems.forEach((it) => {
if (copy[it.id]) delete copy[it.id];
});
} else {
visibleItems.forEach((it) => {
copy[it.id] = it;
});
}
return copy;
});
};
const clearVisibleSelection = (visibleItems: Item[]) => {
setSelectedItems((prev) => {
const copy = { ...prev };
visibleItems.forEach((it) => {
if (copy[it.id]) delete copy[it.id];
});
return copy;
});
};
const selectedCount = useMemo(() => Object.keys(selectedItems).length, [selectedItems]);
const selectedArray = useMemo(() => Object.values(selectedItems), [selectedItems]);
const quantityToList = (qty: number) => {
if (qty <= 1) return 1;
if (qty >= 2 && qty <= 8) return qty;
return 8;
// If you'd rather skip low/zero stock instead of listing 1, change the first line to: if (qty <= 1) return 0;
};
const handleAddSelectedProducts = async () => {
const filteredForQueue = selectedArray.filter((it) => invOf(it) > 0);
const payloadProducts = filteredForQueue.map((item) => ({
...item,
quantityToList: quantityToList(invOf(item)),
}));
setToast(`Queued ${payloadProducts.length} product(s)`);
try {
const userId = typeof window !== 'undefined' ? sessionStorage.getItem('USERID') : null;
const saveRes = await fetch('https://ebay.backend.data4autos.com/api/ebay/finalize-publish', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userKey: EBAYSTOREID,
products: payloadProducts,
userId: userId,
}),
});
const saveData = await saveRes.json();
console.log('Save response:', saveData);
setToastActive(true);
setTimeout(() => setToastActive(false), 3000);
} catch (e) {
console.error(e);
setToast('Failed to queue products');
}
};
if (Turn14Enabled === false) {
return (
<div className="p-6 text-center">
<h1 className="text-xl font-semibold">Turn14 isnt connected yet</h1>
<p className="text-gray-600">Please connect Turn14 to continue.</p>
</div>
);
}
const getHeaderStatus = () => {
if (!expandedBrand) return `Selected: ${selectedCount}`;
const base = itemsMap[expandedBrand] || [];
const afterMake = applyFitmentFilters(base);
const afterSearch = afterMake.filter((it) => itemMatchesQuery(it, searchText));
const afterInv = applyInventoryFilters(afterSearch);
const afterPrice = applyPriceFilters(afterInv);
return `Showing ${afterPrice.length} of ${base.length}`;
};
return (
<div className="bg-gradient-to-br from-slate-50 to-slate-200 min-h-screen">
{/* Sticky Header */}
<div
className={`sticky top-14 z-10 transition-all duration-300 ${isScrolled ? 'bg-white/95 backdrop-blur-md shadow-lg py-3' : 'bg-white/80 backdrop-blur-sm py-4'
}`}
>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div className="flex flex-col">
<h1 className="text-2xl font-bold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
Data4Autos Turn14 Manage Brand Products
</h1>
<p className="text-sm text-gray-500 mt-1">{getHeaderStatus()}</p>
<p className="text-sm font-medium text-blue-600 mt-1">
Selected: <span className="font-semibold">{selectedCount}</span>
</p>
</div>
<div className="flex flex-col sm:flex-row gap-3 items-stretch sm:items-center w-full md:w-auto">
{/* Search */}
<div className="relative flex-grow">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg className="h-5 w-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
clipRule="evenodd"
/>
</svg>
</div>
<input
type="text"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
placeholder="Search products… (name, part#, tags)"
className="block w-full pl-10 pr-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
/>
</div>
{/* In-stock-only pill toggle */}
<div className="flex flex-wrap items-center gap-3">
<label className="flex items-center gap-2 text-sm font-medium text-gray-700 cursor-pointer select-none">
<div className="relative">
<input
type="checkbox"
checked={inStockOnly}
onChange={() => setInStockOnly((v) => !v)}
className="sr-only"
/>
<div className={`block w-10 h-6 rounded-full transition-colors ${inStockOnly ? 'bg-blue-600' : 'bg-gray-300'}`}></div>
<div
className={`absolute left-1 top-1 bg-white w-4 h-4 rounded-full transition-transform ${inStockOnly ? 'transform translate-x-4' : ''
}`}
></div>
</div>
In Stock Only
</label>
<button
onClick={handleAddSelectedProducts}
disabled={selectedCount === 0}
className="px-5 py-2.5 bg-gradient-to-r from-blue-600 to-purple-600 text-white font-medium rounded-lg hover:from-blue-700 hover:to-purple-700 transition-all duration-300 transform hover:-translate-y-0.5 disabled:opacity-50 disabled:transform-none disabled:cursor-not-allowed flex items-center gap-2 shadow-md hover:shadow-lg"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
</svg>
Add {selectedCount} Product{selectedCount === 1 ? '' : 's'}
</button>
</div>
</div>
</div>
{/* Global Filters */}
<div className="mt-3 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3">
{/* Make */}
<label className="text-sm text-gray-700 flex items-center">
<span className="mr-2 whitespace-nowrap">Filter Make:</span>
<select
value={filters.make}
onChange={handleMakeChange}
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 w-full"
>
<option value="">All</option>
{makes_list.map((m) => (
<option key={m} value={m}>
{m}
</option>
))}
</select>
</label>
{/* Inventory Range */}
<div className="text-sm text-gray-700">
<div className="mb-1 font-medium">Inventory Range</div>
<div className="flex items-center gap-2">
<input
type="number"
min={0}
value={filters.invMin}
onChange={handleInvMinChange}
className="w-24 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="Min"
/>
<span>to</span>
<input
type="number"
min={0}
value={filters.invMax}
onChange={handleInvMaxChange}
className="w-24 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="Max"
/>
</div>
</div>
{/* Price Minimum (Slider + number) */}
<div className="text-sm text-gray-700">
<div className="mb-1 font-medium">Price Floor</div>
<div className="flex items-center gap-3">
<input
type="range"
min={0}
max={5000}
step={5}
value={filters.priceMin}
onChange={handlePriceMinChange}
className="w-full"
aria-label="Minimum price"
/>
<input
type="number"
min={0}
value={filters.priceMin}
onChange={handlePriceMinChange}
className="w-24 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div className="text-xs text-gray-500 mt-1">Showing items with price ${filters.priceMin}</div>
</div>
</div>
</div>
</div>
{/* Brands/Products */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pt-6 pb-16">
{brands.length === 0 ? (
<div className="text-center py-20">
<div className="inline-block p-4 bg-white rounded-xl shadow-md">
<svg className="w-16 h-16 mx-auto text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<h3 className="mt-4 text-xl font-medium text-gray-700">No brands selected yet</h3>
<p className="mt-2 text-gray-500">Add brands to view their products.</p>
</div>
</div>
) : (
brands.map((brand) => {
const baseItems = itemsMap[brand.brandid] || [];
const filteredFitment = applyFitmentFilters(baseItems);
const filteredSearch = filteredFitment.filter((it) => itemMatchesQuery(it, searchText));
const filteredInv = applyInventoryFilters(filteredSearch);
const filteredItems = applyPriceFilters(filteredInv);
const allVisibleSelected = filteredItems.length > 0 && filteredItems.every((it) => isSelected(it.id));
const anyVisibleSelected = filteredItems.some((it) => isSelected(it.id));
return (
<div key={brand.brandid} className="mb-6">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-3">
{brand.logo && (
<img src={brand.logo} alt={brand.name} className="h-8 w-8 object-contain rounded bg-white border" />
)}
<div className="text-sm">
<div className="font-semibold">{brand.name}</div>
<div className="text-gray-500">ID: {brand.brandid}</div>
</div>
</div>
<button
onClick={() => toggleBrandItems(brand.brandid)}
className="px-3 py-2 text-sm rounded-md border border-gray-300 hover:bg-gray-50"
>
{expandedBrand === brand.brandid ? 'Hide Products' : 'Show Products'}
</button>
</div>
{expandedBrand === brand.brandid && (
<div className="border border-gray-200 rounded-lg p-3 bg-white">
<div className="mb-3 flex items-center justify-between">
<div className="text-xs text-gray-600">
Showing <span className="font-semibold">{filteredItems.length}</span> of{' '}
<span className="font-semibold">{baseItems.length}</span>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => toggleSelectAllVisible(filteredItems)}
className="text-xs px-3 py-1.5 rounded border border-gray-300 hover:bg-gray-50"
>
{allVisibleSelected ? 'Unselect All Visible' : 'Select All Visible'}
</button>
{anyVisibleSelected && (
<button
onClick={() => clearVisibleSelection(filteredItems)}
className="text-xs px-3 py-1.5 rounded border border-gray-300 hover:bg-gray-50"
>
Clear Visible
</button>
)}
</div>
</div>
{loadingMap[brand.brandid] ? (
<div className="py-8 text-center text-gray-500">Loading products</div>
) : filteredItems.length === 0 ? (
<div className="py-8 text-center text-gray-500">No products match the current filters.</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">
{filteredItems.map((item) => {
const imgSrc =
item.attributes.thumbnail ||
item?.attributes?.files?.[0]?.url ||
'https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png';
const qty = invOf(item);
const price = priceOf(item);
const computedQty = quantityToList(qty);
return (
<div key={item.id} className="border border-gray-200 rounded-lg p-3 hover:shadow-sm">
{/* Select checkbox */}
<div className="flex items-center justify-between mb-2">
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={isSelected(item.id)}
onChange={() => toggleSelect(item)}
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-xs text-gray-600">Select</span>
</label>
<span className="text-[10px] px-2 py-0.5 rounded-full bg-slate-100 text-slate-700">
Will list: {computedQty}
</span>
</div>
<img
src={imgSrc}
alt={item.attributes.product_name || 'Product'}
className="w-full h-24 object-contain mb-2 bg-gray-50 rounded"
/>
<div className="text-sm font-medium line-clamp-2">
{item.attributes.product_name || 'Unnamed Product'}
</div>
<div className="text-xs text-gray-600 mt-1">
Part #: <span className="font-medium">{item.attributes.part_number || '-'}</span>
</div>
<div className="text-xs text-gray-600">
{item.attributes.category || '-'} &gt; {item.attributes.subcategory || '-'}
</div>
<div className="text-xs">
Price:{' '}
<span className="font-semibold">
{Number.isFinite(price) && price > 0 ? `$${price.toFixed(2)}` : '-'}
</span>
</div>
<div className="text-xs text-gray-700">
Inventory: <span className="font-semibold">{qty}</span>
</div>
{item.attributes.part_description && (
<p className="mt-2 text-xs text-gray-700 line-clamp-3">
{item.attributes.part_description}
</p>
)}
</div>
);
})}
</div>
)}
</div>
)}
</div>
);
})
)}
</div>
{/* Toasts */}
{toast && (
<div className="fixed bottom-6 right-6 bg-gradient-to-r from-slate-800 to-black text-white px-6 py-4 rounded-2xl shadow-2xl z-30 animate-fade-in-up">
<div className="flex items-center gap-3">
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
</svg>
<span className="font-medium">{toast}</span>
</div>
</div>
)}
{toastActive && (
<div className="fixed bottom-6 right-6 bg-gradient-to-r from-green-500 to-emerald-600 text-white px-6 py-4 rounded-2xl shadow-2xl z-30 animate-fade-in-up">
<div className="flex items-center gap-3">
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
</svg>
<span className="font-medium">Products added successfully!</span>
</div>
</div>
)}
{/* Tiny animation helper */}
<style jsx global>{`
@keyframes fadeInUp {
from {
opacity: 0;
transform: translate3d(0, 40px, 0);
}
to {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
.animate-fade-in-up {
animation: fadeInUp 0.5s ease-out;
}
`}</style>
</div>
);
};
export default ManageBrandProducts;

View File

@ -0,0 +1,856 @@
// pages/manage-brand-products.tsx
'use client';
import { getAccessToken } from '@/utils/apiHelper';
import { getAccessToken_client } from '@/utils/apiHelper_client';
import React, { useEffect, useMemo, useState } from 'react';
interface Brand {
id: string;
brandid: string;
name: string;
logo?: string;
}
interface Item {
id: string;
attributes: {
product_name?: string;
thumbnail?: string;
part_number?: string;
category?: string;
subcategory?: string;
price?: string; // may come as string; we parse below
part_description?: string;
regular_stock?: boolean;
ltl_freight_required?: boolean;
is_clearance_item?: boolean;
is_air_freight_prohibited?: boolean;
files?: { url: string }[];
fitmmentTags?: any;
};
inventoryQuantity?: number;
}
const makes_list_raw = [
'Alfa Romeo',
'Ferrari',
'Dodge',
'Subaru',
'Toyota',
'Volkswagen',
'Volvo',
'Audi',
'BMW',
'Buick',
'Cadillac',
'Chevrolet',
'Chrysler',
'CX Automotive',
'Nissan',
'Ford',
'Hyundai',
'Infiniti',
'Lexus',
'Mercury',
'Mazda',
'Oldsmobile',
'Plymouth',
'Pontiac',
'Rolls-Royce',
'Eagle',
'Lincoln',
'Mercedes-Benz',
'GMC',
'Saab',
'Honda',
'Saturn',
'Mitsubishi',
'Isuzu',
'Jeep',
'AM General',
'Geo',
'Suzuki',
'E. P. Dutton, Inc.',
'Land Rover',
'PAS, Inc',
'Acura',
'Jaguar',
'Lotus',
'Grumman Olson',
'Porsche',
'American Motors Corporation',
'Kia',
'Lamborghini',
'Panoz Auto-Development',
'Maserati',
'Saleen',
'Aston Martin',
'Dabryan Coach Builders Inc',
'Federal Coach',
'Vector',
'Bentley',
'Daewoo',
'Qvale',
'Roush Performance',
'Autokraft Limited',
'Bertone',
'Panther Car Company Limited',
'Texas Coach Company',
'TVR Engineering Ltd',
'Morgan',
'MINI',
'Yugo',
'BMW Alpina',
'Renault',
'Bitter Gmbh and Co. Kg',
'Scion',
'Maybach',
'Lambda Control Systems',
'Merkur',
'Peugeot',
'Spyker',
'London Coach Co Inc',
'Hummer',
'Bugatti',
'Pininfarina',
'Shelby',
'Saleen Performance',
'smart',
'Tecstar, LP',
'Kenyon Corporation Of America',
'Avanti Motor Corporation',
'Bill Dovell Motor Car Company',
'Import Foreign Auto Sales Inc',
'S and S Coach Company E.p. Dutton',
'Superior Coaches Div E.p. Dutton',
'Vixen Motor Company',
'Volga Associated Automobile',
'Wallace Environmental',
'Import Trade Services',
'J.K. Motors',
'Panos',
'Quantum Technologies',
'London Taxi',
'Red Shift Ltd.',
'Ruf Automobile Gmbh',
'Excalibur Autos',
'Mahindra',
'VPG',
'Fiat',
'Sterling',
'Azure Dynamics',
'McLaren Automotive',
'Ram',
'CODA Automotive',
'Fisker',
'Tesla',
'Mcevoy Motors',
'BYD',
'ASC Incorporated',
'SRT',
'CCC Engineering',
'Mobility Ventures LLC',
'Pagani',
'Genesis',
'Karma',
'Koenigsegg',
'Aurora Cars Ltd',
'RUF Automobile',
'Dacia',
'STI',
'Daihatsu',
'Polestar',
'Kandi',
'Rivian',
'Lucid',
'JBA Motorcars, Inc.',
'Lordstown',
'Vinfast',
'INEOS Automotive',
'Bugatti Rimac',
'Grumman Allied Industries',
'Environmental Rsch and Devp Corp',
'Evans Automobiles',
'Laforza Automobile Inc',
'General Motors',
'Consulier Industries Inc',
'Goldacre',
'Isis Imports Ltd',
'PAS Inc - GMC'
];
const makes_list = makes_list_raw.sort();
interface ManageBrandProductsProps {
accessToken: string | null;
}
const toNumber = (val: unknown): number => {
if (typeof val === 'number') return val;
if (typeof val === 'string') {
const cleaned = val.replace(/[^0-9.\-]/g, '');
const n = parseFloat(cleaned);
return Number.isFinite(n) ? n : 0;
}
return 0;
};
const priceOf = (item: Item) => toNumber(item.attributes?.price);
const invOf = (item: Item) => toNumber(item.inventoryQuantity);
const ManageBrandProducts: React.FC<ManageBrandProductsProps> = () => {
const [accessToken, setaccessToken] = useState<string>('');
const [brands, setBrands] = useState<Brand[]>([]);
const [itemsMap, setItemsMap] = useState<Record<string, Item[]>>({});
const [expandedBrand, setExpandedBrand] = useState<string | null>(null);
const [loadingMap, setLoadingMap] = useState<Record<string, boolean>>({});
const [filters, setFilters] = useState({
make: '',
invMin: 0,
invMax: 9999,
priceMin: 0,
});
// Added products (from /api/user-products)
const [addedIds, setAddedIds] = useState<Set<string>>(new Set());
const [addedPartNumbers, setAddedPartNumbers] = useState<Set<string>>(new Set());
const [Turn14Enabled, setTurn14Enabled] = useState<boolean | null>(null);
// UI/Sticky state
const [isScrolled, setIsScrolled] = useState(false);
// Global Search
const [searchText, setSearchText] = useState<string>('');
// Selection
const [selectedItems, setSelectedItems] = useState<Record<string, Item>>({});
// In-stock toggle (pill switch)
const [inStockOnly, setInStockOnly] = useState<boolean>(false);
// Toasts
const [toastActive, setToastActive] = useState(false);
const [toast, setToast] = useState('');
// NOTE: sessionStorage is available (client component)
const userId = typeof window !== 'undefined' ? sessionStorage.getItem('USERID') : null;
const EBAYSTOREID = typeof window !== 'undefined' ? sessionStorage.getItem('EBAYSTOREID') : null;
useEffect(() => {
const fetchUserBrands = async () => {
try {
const accessToken = await getAccessToken_client();
setaccessToken(accessToken || '');
console.log('Got access token:', accessToken);
const res = await fetch(`https://ebay.backend.data4autos.com/api/brands/${userId}`, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
});
const data = await res.json();
setBrands(data);
const userSelectedIds = data.map((b: any) => String(b.brandid));
setToast(`Loaded ${userSelectedIds.length} selected brands`);
setTimeout(() => setToast(''), 4000);
} catch (error) {
console.error('Error fetching brands:', error);
setToast('Failed to load user brands');
setTimeout(() => setToast(''), 4000);
}
};
if (userId) fetchUserBrands();
}, [userId]);
useEffect(() => {
// Simulate Turn14 check
const t = setTimeout(() => setTurn14Enabled(true), 500);
return () => clearTimeout(t);
}, []);
useEffect(() => {
const onScroll = () => setIsScrolled(window.scrollY > 10);
window.addEventListener('scroll', onScroll);
return () => window.removeEventListener('scroll', onScroll);
}, []);
const toggleBrandItems = async (brandId: string) => {
if (expandedBrand === brandId) {
setExpandedBrand(null);
return;
}
setExpandedBrand(brandId);
if (!itemsMap[brandId]) {
setLoadingMap((prev) => ({ ...prev, [brandId]: true }));
try {
const res = await fetch(`https://turn14.data4autos.com/v1/items/brandallitems/${brandId}`, {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
});
const data1 = await res.json();
setItemsMap((prev) => ({ ...prev, [brandId]: data1 }));
} catch (err) {
console.error(err);
}
setLoadingMap((prev) => ({ ...prev, [brandId]: false }));
}
};
const applyFitmentFilters = (items: Item[]) => {
return items.filter((item) => {
if (!filters.make) return true;
const tags = item.attributes.fitmmentTags || {};
return (
(Array.isArray(tags.make) && tags.make.some((m: string) => m?.toLowerCase() === filters.make.toLowerCase())) ||
item.attributes.product_name?.toLowerCase().includes(filters.make.toLowerCase())
);
});
};
const applyInventoryFilters = (items: Item[]) =>
items.filter((it) => {
const qty = invOf(it);
if (inStockOnly && qty <= 0) return false;
return qty >= filters.invMin && qty <= filters.invMax;
});
const applyPriceFilters = (items: Item[]) =>
items.filter((it) => priceOf(it) >= filters.priceMin);
// Broad query match
const itemMatchesQuery = (item: Item, q: string): boolean => {
if (!q) return true;
const hay = `${item.id} ${JSON.stringify(item.attributes ?? {}, (_k, v) => v ?? '', 2)}`.toLowerCase();
return hay.includes(q.toLowerCase());
};
const handleMakeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setFilters((f) => ({ ...f, make: e.target.value }));
};
const handleInvMinChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const v = Math.max(0, toNumber(e.target.value));
setFilters((f) => ({ ...f, invMin: v }));
};
const handleInvMaxChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const v = Math.max(0, toNumber(e.target.value));
setFilters((f) => ({ ...f, invMax: v || 9999 }));
};
const handlePriceMinChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const v = Math.max(0, toNumber(e.target.value));
setFilters((f) => ({ ...f, priceMin: v }));
};
const isSelected = (id: string) => Boolean(selectedItems[id]);
const toggleSelect = (item: Item) => {
setSelectedItems((prev) => {
const copy = { ...prev };
if (copy[item.id]) delete copy[item.id];
else copy[item.id] = item;
return copy;
});
};
const toggleSelectAllVisible = (visibleItems: Item[]) => {
setSelectedItems((prev) => {
const copy = { ...prev };
const allSelected = visibleItems.every((it) => copy[it.id]);
if (allSelected) {
// Clear only the visible ones
visibleItems.forEach((it) => {
if (copy[it.id]) delete copy[it.id];
});
} else {
visibleItems.forEach((it) => {
copy[it.id] = it;
});
}
return copy;
});
};
const clearVisibleSelection = (visibleItems: Item[]) => {
setSelectedItems((prev) => {
const copy = { ...prev };
visibleItems.forEach((it) => {
if (copy[it.id]) delete copy[it.id];
});
return copy;
});
};
const selectedCount = useMemo(() => Object.keys(selectedItems).length, [selectedItems]);
const selectedArray = useMemo(() => Object.values(selectedItems), [selectedItems]);
const quantityToList = (qty: number) => {
if (qty <= 1) return 1;
if (qty >= 2 && qty <= 4) return qty;
return 4;
// If you'd rather skip low/zero stock instead of listing 1, change the first line to: if (qty <= 1) return 0;
};
const handleAddSelectedProducts = async () => {
const filteredForQueue = selectedArray.filter((it) => invOf(it) > 0);
const payloadProducts = filteredForQueue.map((item) => ({
...item,
quantityToList: quantityToList(invOf(item)),
}));
setToast(`Queued ${payloadProducts.length} product(s)`);
try {
const userId = typeof window !== 'undefined' ? sessionStorage.getItem('USERID') : null;
const saveRes = await fetch('https://ebay.backend.data4autos.com/api/ebay/finalize-publish', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userKey: EBAYSTOREID,
products: payloadProducts,
userId: userId,
}),
});
const saveData = await saveRes.json();
console.log('Save response:', saveData);
setToastActive(true);
// Merge newly queued items into "added" sets so the pill appears instantly
setAddedIds(prev => {
const next = new Set(prev);
payloadProducts.forEach((p) => {
if (p?.id) next.add(String(p.id));
});
return next;
});
setAddedPartNumbers(prev => {
const next = new Set(prev);
payloadProducts.forEach((p) => {
const pn = p?.attributes?.part_number;
if (pn) next.add(String(pn));
});
return next;
});
setTimeout(() => setToastActive(false), 3000);
} catch (e) {
console.error(e);
setToast('Failed to queue products');
}
};
if (Turn14Enabled === false) {
return (
<div className="p-6 text-center">
<h1 className="text-xl font-semibold">Turn14 isnt connected yet</h1>
<p className="text-gray-600">Please connect Turn14 to continue.</p>
</div>
);
}
const getHeaderStatus = () => {
if (!expandedBrand) return `Selected: ${selectedCount}`;
const base = itemsMap[expandedBrand] || [];
const afterMake = applyFitmentFilters(base);
const afterSearch = afterMake.filter((it) => itemMatchesQuery(it, searchText));
const afterInv = applyInventoryFilters(afterSearch);
const afterPrice = applyPriceFilters(afterInv);
return `Showing ${afterPrice.length} of ${base.length}`;
};
useEffect(() => {
const fetchAddedProducts = async () => {
if (!userId) return;
try {
// pull a big page; adjust if your API supports 'all'
const url = `https://ebay.backend.data4autos.com/api/user-products?userid=${encodeURIComponent(
userId
)}&page=1&pageSize=10000`;
const res = await fetch(url, { method: 'GET' });
const data = await res.json();
console.log('Added products response:', data);
const ids = new Set<string>();
const partnos = new Set<string>();
// Accept a few common field names from your mapping API
(data?.items ?? []).forEach((row: any) => {
if (row?.id) ids.add(String(row.id));
if (row?.sku) ids.add(String(row.sku)); // sometimes sku is what your UI uses as id
if (row?.partNumber) partnos.add(String(row.partNumber));
if (row?.attributes?.part_number) partnos.add(String(row.attributes.part_number));
});
setAddedIds(ids);
setAddedPartNumbers(partnos);
} catch (e) {
console.error('Failed to fetch added products:', e);
}
};
fetchAddedProducts();
}, [userId]);
return (
<div className="bg-gradient-to-br from-slate-50 to-slate-200 min-h-screen">
{/* Sticky Header */}
<div
className={`sticky top-14 z-10 transition-all duration-300 ${isScrolled ? 'bg-white/95 backdrop-blur-md shadow-lg py-3' : 'bg-white/80 backdrop-blur-sm py-4'
}`}
>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div className="flex flex-col">
<h1 className="text-2xl font-bold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
Data4Autos Turn14 Manage Brand Products
</h1>
<p className="text-sm text-gray-500 mt-1">{getHeaderStatus()}</p>
<p className="text-sm font-medium text-blue-600 mt-1">
Selected: <span className="font-semibold">{selectedCount}</span>
</p>
</div>
<div className="flex flex-col sm:flex-row gap-3 items-stretch sm:items-center w-full md:w-auto">
{/* Search */}
<div className="relative flex-grow">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg className="h-5 w-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
clipRule="evenodd"
/>
</svg>
</div>
<input
type="text"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
placeholder="Search products… (name, part#, tags)"
className="block w-full pl-10 pr-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
/>
</div>
{/* In-stock-only pill toggle */}
<div className="flex flex-wrap items-center gap-3">
<label className="flex items-center gap-2 text-sm font-medium text-gray-700 cursor-pointer select-none">
<div className="relative">
<input
type="checkbox"
checked={inStockOnly}
onChange={() => setInStockOnly((v) => !v)}
className="sr-only"
/>
<div className={`block w-10 h-6 rounded-full transition-colors ${inStockOnly ? 'bg-blue-600' : 'bg-gray-300'}`}></div>
<div
className={`absolute left-1 top-1 bg-white w-4 h-4 rounded-full transition-transform ${inStockOnly ? 'transform translate-x-4' : ''
}`}
></div>
</div>
In Stock Only
</label>
<button
onClick={handleAddSelectedProducts}
disabled={selectedCount === 0}
className="px-5 py-2.5 bg-gradient-to-r from-blue-600 to-purple-600 text-white font-medium rounded-lg hover:from-blue-700 hover:to-purple-700 transition-all duration-300 transform hover:-translate-y-0.5 disabled:opacity-50 disabled:transform-none disabled:cursor-not-allowed flex items-center gap-2 shadow-md hover:shadow-lg"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
</svg>
Add {selectedCount} Product{selectedCount === 1 ? '' : 's'}
</button>
</div>
</div>
</div>
{/* Global Filters */}
<div className="mt-3 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3">
{/* Make */}
<label className="text-sm text-gray-700 flex items-center">
<span className="mr-2 whitespace-nowrap">Filter Make:</span>
<select
value={filters.make}
onChange={handleMakeChange}
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 w-full"
>
<option value="">All</option>
{makes_list.map((m) => (
<option key={m} value={m}>
{m}
</option>
))}
</select>
</label>
{/* Inventory Range */}
<div className="text-sm text-gray-700">
<div className="mb-1 font-medium">Inventory Range</div>
<div className="flex items-center gap-2">
<input
type="number"
min={0}
value={filters.invMin}
onChange={handleInvMinChange}
className="w-24 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="Min"
/>
<span>to</span>
<input
type="number"
min={0}
value={filters.invMax}
onChange={handleInvMaxChange}
className="w-24 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="Max"
/>
</div>
</div>
{/* Price Minimum (Slider + number) */}
<div className="text-sm text-gray-700">
<div className="mb-1 font-medium">Price Floor</div>
<div className="flex items-center gap-3">
<input
type="range"
min={0}
max={5000}
step={5}
value={filters.priceMin}
onChange={handlePriceMinChange}
className="w-full"
aria-label="Minimum price"
/>
<input
type="number"
min={0}
value={filters.priceMin}
onChange={handlePriceMinChange}
className="w-24 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div className="text-xs text-gray-500 mt-1">Showing items with price ${filters.priceMin}</div>
</div>
</div>
</div>
</div>
{/* Brands/Products */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pt-6 pb-16">
{brands.length === 0 ? (
<div className="text-center py-20">
<div className="inline-block p-4 bg-white rounded-xl shadow-md">
<svg className="w-16 h-16 mx-auto text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<h3 className="mt-4 text-xl font-medium text-gray-700">No brands selected yet</h3>
<p className="mt-2 text-gray-500">Add brands to view their products.</p>
</div>
</div>
) : (
brands.map((brand) => {
const baseItems = itemsMap[brand.brandid] || [];
const filteredFitment = applyFitmentFilters(baseItems);
const filteredSearch = filteredFitment.filter((it) => itemMatchesQuery(it, searchText));
const filteredInv = applyInventoryFilters(filteredSearch);
const filteredItems = applyPriceFilters(filteredInv);
const allVisibleSelected = filteredItems.length > 0 && filteredItems.every((it) => isSelected(it.id));
const anyVisibleSelected = filteredItems.some((it) => isSelected(it.id));
return (
<div key={brand.brandid} className="mb-6">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-3">
{brand.logo && (
<img src={brand.logo} alt={brand.name} className="h-8 w-8 object-contain rounded bg-white border" />
)}
<div className="text-sm">
<div className="font-semibold">{brand.name}</div>
<div className="text-gray-500">ID: {brand.brandid}</div>
</div>
</div>
<button
onClick={() => toggleBrandItems(brand.brandid)}
className="px-3 py-2 text-sm rounded-md border border-gray-300 hover:bg-gray-50"
>
{expandedBrand === brand.brandid ? 'Hide Products' : 'Show Products'}
</button>
</div>
{expandedBrand === brand.brandid && (
<div className="border border-gray-200 rounded-lg p-3 bg-white">
<div className="mb-3 flex items-center justify-between">
<div className="text-xs text-gray-600">
Showing <span className="font-semibold">{filteredItems.length}</span> of{' '}
<span className="font-semibold">{baseItems.length}</span>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => toggleSelectAllVisible(filteredItems)}
className="text-xs px-3 py-1.5 rounded border border-gray-300 hover:bg-gray-50"
>
{allVisibleSelected ? 'Unselect All Visible' : 'Select All Visible'}
</button>
{anyVisibleSelected && (
<button
onClick={() => clearVisibleSelection(filteredItems)}
className="text-xs px-3 py-1.5 rounded border border-gray-300 hover:bg-gray-50"
>
Clear Visible
</button>
)}
</div>
</div>
{loadingMap[brand.brandid] ? (
<div className="py-8 text-center text-gray-500">Loading products</div>
) : filteredItems.length === 0 ? (
<div className="py-8 text-center text-gray-500">No products match the current filters.</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">
{filteredItems.map((item) => {
const imgSrc =
item.attributes.thumbnail ||
item?.attributes?.files?.[0]?.url ||
'https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png';
const qty = invOf(item);
const price = priceOf(item);
const computedQty = quantityToList(qty);
const isAlreadyAdded =
(item?.id && addedIds.has(String(item.id))) ||
(item?.attributes?.part_number && addedPartNumbers.has(String(item.attributes.part_number)));
return (
<div key={item.id} className="border border-gray-200 rounded-lg p-3 hover:shadow-sm">
<div className="flex items-center justify-between mb-2">
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={isSelected(item.id)}
onChange={() => toggleSelect(item)}
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-xs text-gray-600">Select</span>
</label>
<div className="flex items-center gap-2">
{isAlreadyAdded && (
<span className="text-[10px] px-2 py-0.5 rounded-full bg-green-100 text-green-700 border border-green-200">
Added
</span>
)}
<span className="text-[10px] px-2 py-0.5 rounded-full bg-slate-100 text-slate-700">
Will list: {computedQty}
</span>
</div>
</div>
<img
src={imgSrc}
alt={item.attributes.product_name || 'Product'}
className="w-full h-24 object-contain mb-2 bg-gray-50 rounded"
/>
<div className="text-sm font-medium line-clamp-2">
{item.attributes.product_name || 'Unnamed Product'}
</div>
<div className="text-xs text-gray-600 mt-1">
Part #: <span className="font-medium">{item.attributes.part_number || '-'}</span>
</div>
<div className="text-xs text-gray-600">
{item.attributes.category || '-'} &gt; {item.attributes.subcategory || '-'}
</div>
<div className="text-xs">
Price:{' '}
<span className="font-semibold">
{Number.isFinite(price) && price > 0 ? `$${price.toFixed(2)}` : '-'}
</span>
</div>
<div className="text-xs text-gray-700">
Inventory: <span className="font-semibold">{qty}</span>
</div>
{item.attributes.part_description && (
<p className="mt-2 text-xs text-gray-700 line-clamp-3">
{item.attributes.part_description}
</p>
)}
</div>
);
})}
</div>
)}
</div>
)}
</div>
);
})
)}
</div>
{/* Toasts */}
{toast && (
<div className="fixed bottom-6 right-6 bg-gradient-to-r from-slate-800 to-black text-white px-6 py-4 rounded-2xl shadow-2xl z-30 animate-fade-in-up">
<div className="flex items-center gap-3">
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
</svg>
<span className="font-medium">{toast}</span>
</div>
</div>
)}
{toastActive && (
<div className="fixed bottom-6 right-6 bg-gradient-to-r from-green-500 to-emerald-600 text-white px-6 py-4 rounded-2xl shadow-2xl z-30 animate-fade-in-up">
<div className="flex items-center gap-3">
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
</svg>
<span className="font-medium">Products added successfully!</span>
</div>
</div>
)}
{/* Tiny animation helper */}
<style jsx global>{`
@keyframes fadeInUp {
from {
opacity: 0;
transform: translate3d(0, 40px, 0);
}
to {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
.animate-fade-in-up {
animation: fadeInUp 0.5s ease-out;
}
`}</style>
</div>
);
};
export default ManageBrandProducts;

View File

@ -0,0 +1,823 @@
// pages/manage-brand-products.tsx
'use client';
import { getAccessToken } from '@/utils/apiHelper';
import { getAccessToken_client } from '@/utils/apiHelper_client';
import axios from 'axios';
import { useRouter } from 'next/navigation';
import React, { useEffect, useMemo, useState } from 'react';
interface Brand {
id: string;
name: string;
logo?: string;
}
interface Product {
PartNumber: string;
Description: string;
Brand: string;
SuggestedRetail: string;
Cost: string;
Length: string;
Width: string;
Height: string;
Weight: string;
QtyAvail: number;
UPC: string;
Jobber: string;
AAIACode: string;
MapPrice: string;
VendorMSRP: string;
AirRestricted: string;
StateRestricted: string;
TruckFrtOnly: string;
ManufacturerPart: string;
ShipAlone: string;
Status: string;
MotorStateNotes: string;
CanadaRestricted: string;
AcquiredDate: string;
EmissionsWarning: string;
"Image URL": string;
"Category Level 1": string;
"Category Level 2": string;
"Category Level 3": string;
"Long Description(150)": string;
}
interface ProductsResponse {
meta: {
eta: string;
count: number;
};
data: Product[];
}
interface Item {
id: string;
attributes: {
product_name?: string;
brand?: string;
brand_id?: number | string;
thumbnail?: string;
part_number?: string;
category?: string;
subcategory?: string;
price?: string;
part_description?: string;
regular_stock?: boolean;
ltl_freight_required?: boolean;
is_clearance_item?: boolean;
is_air_freight_prohibited?: boolean;
files?: { url: string }[];
fitmmentTags?: any;
};
inventoryQuantity?: number;
}
interface ManageBrandProductsProps {
accessToken: string | null;
}
const toNumber = (val: unknown): number => {
if (typeof val === 'number') return val;
if (typeof val === 'string') {
const cleaned = val.replace(/[^0-9.\-]/g, '');
const n = parseFloat(cleaned);
return Number.isFinite(n) ? n : 0;
}
return 0;
};
// Helper functions to convert Product to Item format for compatibility
const productToItem = (product: Product): Item => ({
id: product.PartNumber,
attributes: {
product_name: product.Description,
brand: product.Brand,
thumbnail: product["Image URL"],
part_number: product.PartNumber,
category: product["Category Level 1"],
subcategory: product["Category Level 2"],
price: product.SuggestedRetail,
part_description: product["Long Description(150)"],
},
inventoryQuantity: product.QtyAvail
});
const priceOf = (item: Item) => toNumber(item.attributes?.price);
const invOf = (item: Item) => toNumber(item.inventoryQuantity);
const ManageBrandProducts: React.FC<ManageBrandProductsProps> = () => {
const router = useRouter();
const [accessToken, setaccessToken] = useState<string>('');
const [brands, setBrands] = useState<Brand[]>([]);
const [productsMap, setProductsMap] = useState<Record<string, ProductsResponse>>({});
const [expandedBrand, setExpandedBrand] = useState<string | null>(null);
const [loadingMap, setLoadingMap] = useState<Record<string, boolean>>({});
const [filters, setFilters] = useState({
make: '',
invMin: 0,
invMax: 99,
priceMin: 0,
});
const [addedIds, setAddedIds] = useState<Set<string>>(new Set());
const [addedPartNumbers, setAddedPartNumbers] = useState<Set<string>>(new Set());
const [payment, setPayment] = useState<any>(null);
const [Turn14Enabled, setTurn14Enabled] = useState<boolean | null>(null);
const [isScrolled, setIsScrolled] = useState(false);
const [searchText, setSearchText] = useState<string>('');
const [selectedItems, setSelectedItems] = useState<Record<string, Item>>({});
const [inStockOnly, setInStockOnly] = useState<boolean>(false);
const [toastActive, setToastActive] = useState(false);
const [toast, setToast] = useState('');
const userId = typeof window !== 'undefined' ? sessionStorage.getItem('USERID') : null;
const EBAYSTOREID = typeof window !== 'undefined' ? sessionStorage.getItem('EBAYSTOREID') : null;
useEffect(() => {
const role = localStorage.getItem("user_role");
const sessionId = localStorage.getItem("payment_session");
if (role === "admin" || role === "partner") {
return;
}
if (!sessionId) {
router.push("/pricing");
return;
}
const fetchPaymentDetails = async () => {
try {
const res: any = await axios.get(
"https://ebay.backend.data4autos.com/api/payment/details",
{ params: { session_id: sessionId } }
);
setPayment(res.data.payment);
} catch (err) {
console.error("Error fetching payment details:", err);
}
};
fetchPaymentDetails();
}, [router]);
useEffect(() => {
const fetchUserBrands = async () => {
try {
const accessToken = await getAccessToken_client();
setaccessToken(accessToken || '');
console.log('Got access token:', accessToken);
const res = await fetch(`https://ebay.backend.data4autos.com/api/motorstate/brands/${userId}`, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
});
const data = await res.json();
setBrands(data);
const userSelectedIds = data.map((b: any) => String(b.name));
setToast(`Loaded ${userSelectedIds.length} selected brands`);
setTimeout(() => setToast(''), 4000);
} catch (error) {
console.error('Error fetching brands:', error);
setToast('Failed to load user brands');
setTimeout(() => setToast(''), 4000);
}
};
if (userId) fetchUserBrands();
}, [userId]);
useEffect(() => {
const t = setTimeout(() => setTurn14Enabled(true), 500);
return () => clearTimeout(t);
}, []);
useEffect(() => {
const onScroll = () => setIsScrolled(window.scrollY > 10);
window.addEventListener('scroll', onScroll);
return () => window.removeEventListener('scroll', onScroll);
}, []);
const toggleBrandItems = async (name: string) => {
if (expandedBrand === name) {
setExpandedBrand(null);
return;
}
setExpandedBrand(name);
if (!productsMap[name]) {
setLoadingMap((prev) => ({ ...prev, [name]: true }));
try {
const res = await fetch(`https://motorstate.data4autos.com/api/data/products/${name}`, {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
});
const data: ProductsResponse = await res.json();
setProductsMap((prev) => ({ ...prev, [name]: data }));
} catch (err) {
console.error(err);
}
setLoadingMap((prev) => ({ ...prev, [name]: false }));
}
};
// Convert products to items for filtering compatibility
const getItemsForBrand = (name: string): Item[] => {
const response = productsMap[name];
if (!response?.data) return [];
return response.data.map(productToItem);
};
const applyFitmentFilters = (items: Item[]) => {
return items.filter((item) => {
if (!filters.make) return true;
const tags = item.attributes.fitmmentTags || {};
return (
(Array.isArray(tags.make) && tags.make.some((m: string) => m?.toLowerCase() === filters.make.toLowerCase())) ||
item.attributes.product_name?.toLowerCase().includes(filters.make.toLowerCase())
);
});
};
const applyInventoryFilters = (items: Item[]) =>
items.filter((it) => {
const qty = invOf(it);
if (inStockOnly && qty <= 0) return false;
return qty >= filters.invMin && qty <= filters.invMax;
});
const applyPriceFilters = (items: Item[]) =>
items.filter((it) => priceOf(it) >= filters.priceMin);
const itemMatchesQuery = (item: Item, q: string): boolean => {
if (!q) return true;
const hay = `${item.id} ${JSON.stringify(item.attributes ?? {}, (_k, v) => v ?? '', 2)}`.toLowerCase();
return hay.includes(q.toLowerCase());
};
const handleMakeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setFilters((f) => ({ ...f, make: e.target.value }));
};
const handleInvMinChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const v = Math.max(0, toNumber(e.target.value));
setFilters((f) => ({ ...f, invMin: v }));
};
const handleInvMaxChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const v = Math.max(0, toNumber(e.target.value));
setFilters((f) => ({ ...f, invMax: v || 9999 }));
};
const handlePriceMinChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const v = Math.max(0, toNumber(e.target.value));
setFilters((f) => ({ ...f, priceMin: v }));
};
const isSelected = (id: string) => Boolean(selectedItems[id]);
const toggleSelect = (item: Item) => {
setSelectedItems((prev) => {
console.log('Toggling selection for item:', item);
const copy = { ...prev };
if (copy[item.id]) delete copy[item.id];
else copy[item.id] = item;
return copy;
});
};
const toggleSelectAllVisible = (visibleItems: Item[]) => {
setSelectedItems((prev) => {
const copy = { ...prev };
const allSelected = visibleItems.every((it) => copy[it.id]);
if (allSelected) {
visibleItems.forEach((it) => {
if (copy[it.id]) delete copy[it.id];
});
} else {
visibleItems.forEach((it) => {
copy[it.id] = it;
});
}
return copy;
});
};
const clearVisibleSelection = (visibleItems: Item[]) => {
setSelectedItems((prev) => {
const copy = { ...prev };
visibleItems.forEach((it) => {
if (copy[it.id]) delete copy[it.id];
});
return copy;
});
};
const selectedCount = useMemo(() => Object.keys(selectedItems).length, [selectedItems]);
const selectedArray = useMemo(() => Object.values(selectedItems), [selectedItems]);
const quantityToList = (qty: number) => {
if (qty <= 1) return 1;
if (qty >= 2 && qty <= 4) return qty;
return 4;
};
const handleAddSelectedProducts = async () => {
console.log('Adding selected products:', selectedArray);
const filteredForQueue = selectedArray.filter((it) => invOf(it) > 0);
console.log('Selected items for queue:', filteredForQueue);
const payloadProducts = filteredForQueue.map((item) => {
const a = item.attributes || {};
const bid = a.brand_id ?? (item as any).brand_id;
const bname = a.brand ?? (item as any).brand;
const brandLogo =
(bid != null ? brandLogoById[String(bid)] : '') ||
(bname ? brandLogoByName[String(bname).toLowerCase()] : '') ||
'';
return {
...item,
quantityToList: quantityToList(invOf(item)),
brandLogo,
brand: {
id: bid ?? '',
name: bname ?? '',
logo: brandLogo,
},
};
});
console.log('Payload products:', payloadProducts);
setToast(`Queued ${payloadProducts.length} product(s)`);
try {
const userId = typeof window !== 'undefined' ? sessionStorage.getItem('USERID') : null;
// const saveRes = await fetch('https://ebay.backend.data4autos.com/api/ebay/finalize-publish', {
// method: 'POST',
// headers: { 'Content-Type': 'application/json' },
// body: JSON.stringify({
// userKey: EBAYSTOREID,
// products: payloadProducts,
// userId: userId,
// }),
// });
// const saveData = await saveRes.json();
// console.log('Save response:', saveData);
setToastActive(true);
setAddedIds(prev => {
const next = new Set(prev);
payloadProducts.forEach((p) => {
if (p?.id) next.add(String(p.id));
});
return next;
});
setAddedPartNumbers(prev => {
const next = new Set(prev);
payloadProducts.forEach((p) => {
const pn = p?.attributes?.part_number;
if (pn) next.add(String(pn));
});
return next;
});
setTimeout(() => setToastActive(false), 3000);
} catch (e) {
console.error(e);
setToast('Failed to queue products');
}
};
if (Turn14Enabled === false) {
return (
<div className="p-6 text-center">
<h1 className="text-xl font-semibold">Turn14 isn't connected yet</h1>
<p className="text-gray-600">Please connect Turn14 to continue.</p>
</div>
);
}
const getHeaderStatus = () => {
if (!expandedBrand) return `Selected: ${selectedCount}`;
const baseItems = getItemsForBrand(expandedBrand);
const afterMake = applyFitmentFilters(baseItems);
const afterSearch = afterMake.filter((it) => itemMatchesQuery(it, searchText));
const afterInv = applyInventoryFilters(afterSearch);
const afterPrice = applyPriceFilters(afterInv);
return `Showing ${afterPrice.length} of ${baseItems.length}`;
};
useEffect(() => {
const fetchAddedProducts = async () => {
if (!userId) return;
try {
const url = `https://ebay.backend.data4autos.com/api/user-products?userid=${encodeURIComponent(
userId
)}&page=1&pageSize=10000`;
const res = await fetch(url, { method: 'GET' });
const data = await res.json();
console.log('Added products response:', data);
const ids = new Set<string>();
const partnos = new Set<string>();
(data?.items ?? []).forEach((row: any) => {
if (row?.id) ids.add(String(row.id));
if (row?.sku) ids.add(String(row.sku));
if (row?.partNumber) partnos.add(String(row.partNumber));
if (row?.attributes?.part_number) partnos.add(String(row.attributes.part_number));
});
setAddedIds(ids);
setAddedPartNumbers(partnos);
} catch (e) {
console.error('Failed to fetch added products:', e);
}
};
fetchAddedProducts();
}, [userId]);
const brandLogoById = useMemo(() => {
const m: Record<string, string> = {};
brands.forEach(b => { m[String(b.name)] = b.logo || ''; });
return m;
}, [brands]);
const brandLogoByName = useMemo(() => {
const m: Record<string, string> = {};
brands.forEach(b => { if (b.name) m[b.name.toLowerCase()] = b.logo || ''; });
return m;
}, [brands]);
return (
<div className="bg-gradient-to-br from-[#00d1ff]/10 via-white to-[#00d1ff]/20 min-h-screen">
{/* Sticky Header */}
<div
className={`sticky top-14 z-10 transition-all duration-300 ${isScrolled ? 'bg-white/95 backdrop-blur-md shadow-lg py-3' : 'bg-white/80 backdrop-blur-sm py-4'
}`}
>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div className="flex flex-col">
<h1 className="text-2xl font-bold bg-[#00d1ff] to-purple-600 bg-clip-text text-[#00d1ff]">
Data4Autos Turn14 Manage Brand Products
</h1>
<p className="text-sm text-gray-500 mt-1">{getHeaderStatus()}</p>
<p className="text-sm font-medium text-[#00d1ff] mt-1">
Selected: <span className="font-semibold">{selectedCount}</span>
</p>
</div>
<div className="flex flex-col sm:flex-row gap-3 items-stretch sm:items-center w-full md:w-auto">
<div className="relative flex-grow">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg className="h-5 w-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
clipRule="evenodd"
/>
</svg>
</div>
<input
type="text"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
placeholder="Search products… (name, part#, tags)"
className="block w-full pl-10 pr-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
/>
</div>
<div className="flex flex-wrap items-center gap-3">
<label className="flex items-center gap-2 text-sm font-medium text-gray-700 cursor-pointer select-none">
<div className="relative">
<input
type="checkbox"
checked={inStockOnly}
onChange={() => setInStockOnly((v) => !v)}
className="sr-only"
/>
<div className={`block w-10 h-6 rounded-full transition-colors ${inStockOnly ? 'bg-[#00d1ff]' : 'bg-gray-300'}`}></div>
<div
className={`absolute left-1 top-1 bg-white w-4 h-4 rounded-full transition-transform ${inStockOnly ? 'transform translate-x-4' : ''
}`}
></div>
</div>
In Stock Only
</label>
<button
onClick={handleAddSelectedProducts}
disabled={selectedCount === 0}
className="px-5 py-2.5 bg-[#00d1ff] text-white font-medium rounded-lg hover:from-[#00d1ff] hover:to-purple-700 transition-all duration-300 transform hover:-translate-y-0.5 disabled:opacity-50 disabled:transform-none disabled:cursor-not-allowed flex items-center gap-2 shadow-md hover:shadow-lg"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
</svg>
Add {selectedCount} Product{selectedCount === 1 ? '' : 's'}
</button>
</div>
</div>
</div>
{/* Global Filters */}
<div className="mt-3 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3">
{/* <label className="text-sm text-gray-700 flex items-center">
<span className="mr-2 whitespace-nowrap">Filter Make:</span>
<select
value={filters.make}
onChange={handleMakeChange}
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 w-full"
>
<option value="">All</option>
{makes_list.map((m) => (
<option key={m} value={m}>
{m}
</option>
))}
</select>
</label> */}
<div className="text-sm text-gray-700">
<div className="mb-1 font-medium">Inventory Range</div>
<div className="flex items-center gap-2">
<input
type="number"
min={0}
value={filters.invMin}
onChange={handleInvMinChange}
className="w-24 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="Min"
/>
<span>to</span>
<input
type="number"
min={0}
value={filters.invMax}
onChange={handleInvMaxChange}
className="w-24 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="Max"
/>
</div>
</div>
<div className="text-sm text-gray-700">
<div className="mb-1 font-medium">Price Floor</div>
<div className="flex items-center gap-3">
<input
type="range"
min={0}
max={5000}
step={5}
value={filters.priceMin}
onChange={handlePriceMinChange}
className="w-full"
aria-label="Minimum price"
/>
<input
type="number"
min={0}
value={filters.priceMin}
onChange={handlePriceMinChange}
className="w-24 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div className="text-xs text-gray-500 mt-1">Showing items with price ${filters.priceMin}</div>
</div>
</div>
</div>
</div>
{/* Brands/Products */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pt-6 pb-16">
{brands.length === 0 ? (
<div className="flex items-center justify-center py-24 bg-gradient-to-b from-gray-50 to-gray-100">
<div className="relative text-center bg-white/80 backdrop-blur-md border border-white/40 shadow-2xl rounded-3xl p-10 transition-all duration-500 hover:scale-[1.02]">
<div className="w-20 h-20 mx-auto flex items-center justify-center rounded-full bg-[#00d1ff] shadow-lg">
<svg
className="w-10 h-10 text-white"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
</div>
<h3 className="mt-6 text-2xl font-semibold text-gray-800 tracking-tight">
No Brands Selected Yet
</h3>
<p className="mt-3 text-gray-500 max-w-sm mx-auto">
Add brands to view and explore their products here.
</p>
<div className="absolute -top-10 -right-10 w-32 h-32 bg-[#00d1ff] rounded-full blur-3xl"></div>
<div className="absolute -bottom-10 -left-10 w-32 h-32 bg-[#00d1ff] rounded-full blur-3xl"></div>
</div>
</div>
) : (
brands.map((brand) => {
const baseItems = getItemsForBrand(brand.name);
const filteredFitment = applyFitmentFilters(baseItems);
const filteredSearch = filteredFitment.filter((it) => itemMatchesQuery(it, searchText));
const filteredInv = applyInventoryFilters(filteredSearch);
const filteredItems = applyPriceFilters(filteredInv);
const allVisibleSelected = filteredItems.length > 0 && filteredItems.every((it) => isSelected(it.id));
const anyVisibleSelected = filteredItems.some((it) => isSelected(it.id));
return (
<div key={brand.name} className="mb-6">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-3">
{brand.logo && (
<img src={brand.logo} alt={brand.name} className="h-8 w-8 object-contain rounded bg-white border" />
)}
<div className="text-sm">
<div className="font-semibold">{brand.name}</div>
</div>
</div>
<button
onClick={() => toggleBrandItems(brand.name)}
className="px-3 py-2 text-sm rounded-md border border-gray-300 hover:bg-gray-50"
>
{expandedBrand === brand.name ? 'Hide Products' : 'Show Products'}
</button>
</div>
{expandedBrand === brand.name && (
<div className="border border-gray-200 rounded-lg p-3 bg-white">
<div className="mb-3 flex items-center justify-between">
<div className="text-xs text-gray-600">
Showing <span className="font-semibold">{filteredItems.length}</span> of{' '}
<span className="font-semibold">{baseItems.length}</span>
{productsMap[brand.name]?.meta && (
<span className="ml-2 text-gray-400">
(Total: {productsMap[brand.name].meta.count})
</span>
)}
</div>
<div className="flex items-center gap-2">
<button
onClick={() => toggleSelectAllVisible(filteredItems)}
className="text-xs px-3 py-1.5 rounded border border-gray-300 hover:bg-gray-50"
>
{allVisibleSelected ? 'Unselect All Visible' : 'Select All Visible'}
</button>
{anyVisibleSelected && (
<button
onClick={() => clearVisibleSelection(filteredItems)}
className="text-xs px-3 py-1.5 rounded border border-gray-300 hover:bg-gray-50"
>
Clear Visible
</button>
)}
</div>
</div>
{loadingMap[brand.name] ? (
<div className="py-8 text-center text-gray-500">Loading products</div>
) : filteredItems.length === 0 ? (
<div className="py-8 text-center text-gray-500">No products match the current filters.</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">
{filteredItems.map((item) => {
const imgSrc = item.attributes.thumbnail || 'https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png';
const qty = invOf(item);
const price = priceOf(item);
const computedQty = quantityToList(qty);
const isAlreadyAdded =
(item?.id && addedIds.has(String(item.id))) ||
(item?.attributes?.part_number && addedPartNumbers.has(String(item.attributes.part_number)));
return (
<div key={item.id} className="border border-gray-200 rounded-lg p-3 hover:shadow-sm">
<div className="flex items-center justify-between mb-2">
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={isSelected(item.id)}
onChange={() => toggleSelect(item)}
className="h-4 w-4 rounded border-gray-300 text-[#00d1ff] focus:ring-blue-500"
/>
<span className="text-xs text-gray-600">Select</span>
</label>
<div className="flex items-center gap-2">
{isAlreadyAdded && (
<span className="text-[10px] px-2 py-0.5 rounded-full bg-green-100 text-green-700 border border-green-200">
Added
</span>
)}
<span className="text-[10px] px-2 py-0.5 rounded-full bg-slate-100 text-slate-700">
Will list: {computedQty}
</span>
</div>
</div>
<img
src={imgSrc}
alt={item.attributes.product_name || 'Product'}
className="w-full h-24 object-contain mb-2 bg-gray-50 rounded"
/>
<div className="text-sm font-medium line-clamp-2">
{item.attributes.product_name || 'Unnamed Product'}
</div>
<div className="text-xs text-gray-600 mt-1">
Part #: <span className="font-medium">{item.attributes.part_number || '-'}</span>
</div>
<div className="text-xs text-gray-600">
{item.attributes.category || '-'} &gt; {item.attributes.subcategory || '-'}
</div>
<div className="text-xs">
Price:{' '}
<span className="font-semibold">
{Number.isFinite(price) && price > 0 ? `$${price.toFixed(2)}` : '-'}
</span>
</div>
<div className="text-xs text-gray-700">
Inventory: <span className="font-semibold">{qty}</span>
</div>
{item.attributes.part_description && (
<p className="mt-2 text-xs text-gray-700 line-clamp-3">
{item.attributes.part_description}
</p>
)}
</div>
);
})}
</div>
)}
</div>
)}
</div>
);
})
)}
</div>
{/* Toasts */}
{toast && (
<div className="fixed bottom-6 right-6 bg-gradient-to-r from-slate-800 to-black text-white px-6 py-4 rounded-2xl shadow-2xl z-30 animate-fade-in-up">
<div className="flex items-center gap-3">
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
</svg>
<span className="font-medium">{toast}</span>
</div>
</div>
)}
{toastActive && (
<div className="fixed bottom-6 right-6 bg-gradient-to-r from-green-500 to-emerald-600 text-white px-6 py-4 rounded-2xl shadow-2xl z-30 animate-fade-in-up">
<div className="flex items-center gap-3">
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
</svg>
<span className="font-medium">Products added successfully!</span>
</div>
</div>
)}
<style jsx global>{`
@keyframes fadeInUp {
from {
opacity: 0;
transform: translate3d(0, 40px, 0);
}
to {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
.animate-fade-in-up {
animation: fadeInUp 0.5s ease-out;
}
`}</style>
</div>
);
};
export default ManageBrandProducts;

View File

@ -0,0 +1,269 @@
// pages/manage-brand-products.tsx
'use client';
import { getAccessToken } from '@/utils/apiHelper';
import React, { useEffect, useState } from 'react';
interface Brand {
id: string;
brandid: string;
name: string;
logo?: string;
}
interface Item {
id: string;
attributes: {
product_name?: string;
thumbnail?: string;
part_number?: string;
category?: string;
subcategory?: string;
price?: string;
part_description?: string;
regular_stock?: boolean;
ltl_freight_required?: boolean;
is_clearance_item?: boolean;
is_air_freight_prohibited?: boolean;
files?: { url: string }[];
fitmmentTags?: any;
};
inventoryQuantity?: number;
}
const makes_list_raw = [
'Alfa Romeo',
'Ferrari',
'Dodge',
'Subaru',
'Toyota',
'Volkswagen',
'Volvo',
'Audi',
'BMW',
'Buick',
'Cadillac',
'Chevrolet',
'Chrysler',
'CX Automotive',
'Nissan',
'Ford',
'Hyundai',
'Infiniti',
'Lexus',
'Mercury',
'Mazda',
'Oldsmobile',
'Plymouth',
'Pontiac',
'Rolls-Royce',
'Eagle',
'Lincoln',
'Mercedes-Benz',
'GMC',
'Saab',
'Honda',
'Saturn',
'Mitsubishi',
'Isuzu',
'Jeep',
];
const makes_list = makes_list_raw.sort();
const styles = {
gridContainer: {
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: '10px',
},
gridItem: { display: 'flex', flexDirection: 'column' },
gridFullWidthItem: { gridColumn: 'span 2', display: 'flex', flexDirection: 'column' },
};
interface ManageBrandProductsProps {
accessToken: string | null;
}
const ManageBrandProducts: React.FC<ManageBrandProductsProps> = ({ accessToken }) => {
const [brands, setBrands] = useState<Brand[]>([]); // Placeholder brands
// const [brands, setBrands] = useState<Brand[]>([]); // Uncomment to start with no brands
const [itemsMap, setItemsMap] = useState<Record<string, Item[]>>({});
const [expandedBrand, setExpandedBrand] = useState<string | null>(null);
const [loadingMap, setLoadingMap] = useState<Record<string, boolean>>({});
const [filters, setFilters] = useState({ make: '' });
const [Turn14Enabled, setTurn14Enabled] = useState<boolean | null>(null);
const [status, setStatus] = useState<string>('processing');
const [processId, setProcessId] = useState<string | null>(null);
const [results, setResults] = useState<Item[]>([]);
const [progress, setProgress] = useState(0);
const [totalProducts, setTotalProducts] = useState(0);
const [processedProducts, setProcessedProducts] = useState(0);
const [currentProduct, setCurrentProduct] = useState<Item | null>(null);
const [toastActive, setToastActive] = useState(false);
const [toast, setToast] = useState('');
const userId = sessionStorage.getItem('USERID');
const EBAYSTOREID = sessionStorage.getItem('EBAYSTOREID');
useEffect(() => {
const fetchUserBrands = async () => {
try {
const res = await fetch(`https://ebay.backend.data4autos.com/api/brands/${userId}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
const data = await res.json();
console.log('GET response:', data);
setBrands(data);
// Extract selected brand IDs from the response
const userSelectedIds = data.map((b: any) => String(b.brandid)); // brandid from your response
// Optional: show toast
setToast(`Loaded ${userSelectedIds.length} selected brands`);
setTimeout(() => setToast(''), 4000);
} catch (error) {
console.error('Error fetching brands:', error);
setToast('Failed to load user brands');
setTimeout(() => setToast(''), 4000);
}
};
fetchUserBrands();
}, []);
// Simulate Turn14 check
useEffect(() => {
setTimeout(() => setTurn14Enabled(true), 500); // Simulate API call
}, []);
const toggleBrandItems = async (brandId: string) => {
if (expandedBrand === brandId) {
setExpandedBrand(null);
return;
}
setExpandedBrand(brandId);
if (!itemsMap[brandId]) {
setLoadingMap((prev) => ({ ...prev, [brandId]: true }));
try {
const res = await fetch(`https://turn14.data4autos.com/v1/items/brandallitems/${brandId}`, {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
});
const data1 = await res.json();
console.log('GET response:', data1);
setItemsMap((prev) => ({ ...prev, [brandId]: data1 }));
} catch (err) {
console.error(err);
}
setLoadingMap((prev) => ({ ...prev, [brandId]: false }));
}
};
const applyFitmentFilters = (items: Item[]) => {
//return items;
return items.filter((item) => {
if (!filters.make) return true;
const tags = item.attributes.fitmmentTags || {};
return tags.make?.includes(filters.make) || item.attributes.product_name?.includes(filters.make);
});
};
const handleFilterChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setFilters({ ...filters, make: e.target.value });
};
const handleAction = () => {
// TODO: Implement action logic using Turn14 access token
console.log('TODO: Submit selected products');
setToastActive(true);
};
if (Turn14Enabled === false) {
return (
<div style={{ padding: 24, textAlign: 'center' }}>
<h1>Turn14 isnt connected yet</h1>
<p>Please connect Turn14 to continue.</p>
</div>
);
}
return (
<div style={{ padding: 24 }}>
<h1>Data4Autos Turn14 Manage Brand Products</h1>
{brands.length === 0 ? (
<p>No brands selected yet.</p>
) : (
brands.map((brand) => {
const filteredItems = applyFitmentFilters(itemsMap[brand.brandid] || []);
return (
<div key={brand.brandid} style={{ marginBottom: 24 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
<div>
<strong>{brand.name}</strong> (ID: {brand.brandid})
</div>
<button onClick={() => toggleBrandItems(brand.brandid)}>{expandedBrand === brand.brandid ? 'Hide Products' : 'Show Products'}</button>
</div>
{expandedBrand === brand.brandid && (
<div style={{ border: '1px solid #eee', padding: 12 }}>
<div style={{ marginBottom: 12 }}>
<label>
Filter Make:
<select value={filters.make} onChange={handleFilterChange}>
<option value="">All</option>
{makes_list.map((m) => (
<option key={m} value={m}>
{m}
</option>
))}
</select>
</label>
</div>
<button onClick={handleAction}>Add {filteredItems.length} Products to Store</button>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(250px, 1fr))', gap: 12, marginTop: 12 }}>
{filteredItems.map((item) => {
// console.log('Rendering item:', item);
// console.log(item?.attributes?.files?.[0]);
return (
<div key={item.id} style={{ border: '1px solid #ccc', padding: 12 }}>
<img
src={item?.attributes?.files?.[0]?.url || item.attributes.thumbnail || 'https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png'}
alt={item.attributes.product_name}
style={{ width: '100%', marginBottom: 8 }}
/>
<strong>{item.attributes.product_name}</strong>
<p>Part Number: {item.attributes.part_number}</p>
<p>
Category: {item.attributes.category} &gt; {item.attributes.subcategory}
</p>
<p>Price: ${item.attributes.price}</p>
<p>{item.attributes.part_description}</p>
</div>
);
})}
</div>
</div>
)}
</div>
);
})
)}
{toastActive && <div style={{ position: 'fixed', bottom: 12, right: 12, background: '#0c0', color: '#fff', padding: 12 }}>Products added successfully!</div>}
</div>
);
};
export default ManageBrandProducts;

View File

@ -0,0 +1,25 @@
import { Metadata } from 'next';
import React from 'react';
import ManageBrandProducts from './manage-brand-products';
import { getAccessToken } from '@/utils/apiHelper';
export const metadata: Metadata = {
title: 'Sales Admin',
};
const Sales = async () => {
const accessToken = await getAccessToken();
return (
<div>
<ManageBrandProducts accessToken={accessToken} />
</div>
);
// return <div style={{ padding: '20px' }}><div style={{ maxWidth: '1200px', margin: '0 auto' }}>
// <h1>Sales Admin</h1>
// <p>Manage your brand products and inventory here.</p>
// <ManageBrandProducts />
//
};
export default Sales;

199
app/(defaults)/page.tsx Normal file
View File

@ -0,0 +1,199 @@
'use client';
import React, { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import axios from 'axios';
const Home = () => {
const router = useRouter();
const [userId, setUserId] = useState<string | null>(null);
const [user, setUser] = useState<string | null>(null);
const [userDetails, setUserDetails] = useState<any>({});
const fetchUsers = async () => {
try {
const uid = localStorage.getItem("data4auto_uid");
const res: any = await axios.get(`https://ebay.backend.data4autos.com/api/motorstate/auth/users/${uid}`);
setUserDetails(res.data?.user || []);
} catch (err) {
console.error(err);
}
};
useEffect(() => {
fetchUsers();
}, []);
// Authentication check
useEffect(() => {
const uid = localStorage.getItem('data4auto_uid');
const userEmail = localStorage.getItem('d4a_email');
if (uid && userEmail) {
setUserId(uid);
setUser(userEmail);
} else {
axios
.get('https://ebay.backend.data4autos.com/api/auth/protected', {
withCredentials: true,
})
.then((res: any) => {
if (res?.data?.payment !== null) {
setUser(res.data.user?.email);
setUserId(res.data.user.userid);
localStorage.setItem('d4a_auth_uid', res.data.user.userid);
} else {
router.push('/pricing');
}
})
.catch(() => {
router.push('/login');
});
}
}, [router]);
if (!userId && !user) {
return (
<div className="flex h-screen items-center justify-center text-gray-600 text-lg">
🔐 Checking authentication...
</div>
);
}
const cards = [
{
id: 1,
title: '🎥 How to Access Your Store',
content: (
<div className="relative w-full h-56 rounded-2xl overflow-hidden shadow-lg">
<iframe
className="absolute top-0 left-0 w-full h-full rounded-2xl"
src="https://www.youtube.com/embed/g6qV2cQ2Fhw?si=CYpzlvUEahw0eUn7"
allowFullScreen
/>
</div>
),
},
{
id: 2,
title: '📊 About Data4Autos',
content: (
<p className="text-gray-700 leading-relaxed">
Data4Autos is an advanced automation platform for eCommerce sellers.
Connect, manage, and automate your Motor State and eBay stores effortlessly all in one place.
</p>
),
},
{
id: 3,
title: '⚙️ About Motor StateStateStateStateState Integration',
content: (
<p className="text-gray-700 leading-relaxed">
Sync your Motor StateState account to autgfhjklomatically import products, pricing, and stock updates.
</p>
),
},
{
id: 4,
title: '🛒 About eBay Integration',
content: (
<p className="text-gray-700 leading-relaxed">
Manage your eBay listing automation, stock updates, and orders from one dashboard.
</p>
),
},
{
id: 5,
title: '💰 Pricing & Plans',
content: (
<div className="flex flex-col items-center gap-2">
<p className="text-gray-700 text-center leading-relaxed">
Explore flexible pricing options designed for every business size.
</p>
<button
onClick={() => router.push('/pricing')}
className="mt-3 bg-[#0099cc] text-white px-6 py-2 rounded-full font-medium shadow-md hover:scale-105 transition"
>
View Pricing
</button>
</div>
),
},
{
id: 6,
title: '👤 User Details',
content: (
<div className="text-gray-700 space-y-2 text-center">
<p><strong>Name:</strong> {userDetails?.name}</p>
<p><strong>Email:</strong> {userDetails?.email}</p>
<p><strong>Phone:</strong> {userDetails?.phonenumber}</p>
</div>
),
},
];
const cardColors = [
"bg-gradient-to-br from-[#ffecd2] to-[#fcb69f]",
"bg-gradient-to-br from-[#a1c4fd] to-[#c2e9fb]",
"bg-gradient-to-br from-[#fbc2eb] to-[#a6c1ee]",
"bg-gradient-to-br from-[#d4fc79] to-[#96e6a1]",
"bg-gradient-to-br from-[#84fab0] to-[#8fd3f4]",
"bg-gradient-to-br from-[#fccb90] to-[#d57eeb]"
];
return (
<div className="min-h-screen bg-[#eef9ff] px-6 py-14">
{/* ⭐ HERO SECTION WITH IMAGE BACKGROUND ⭐ */}
<div className="w-full mx-auto mb-16">
<div className="relative overflow-hidden rounded-3xl shadow-xl h-[320px] md:h-[380px] lg:h-[420px]">
{/* Background Image */}
<img
src="https://data4autos.com/assets/img/contact/contact-banner-bg.webp"
className="absolute inset-0 w-full h-full object-cover"
/>
{/* Dark Overlay */}
<div className="absolute inset-0 bg-black/40"></div>
{/* Gradient Overlay */}
<div className="absolute inset-0 bg-gradient-to-r from-[#00d1ff99]/60 via-[#ffffff00] to-[#212529ab]"></div>
{/* Glow Blurs */}
<div className="absolute -top-10 -left-10 w-40 h-40 bg-white/20 blur-3xl rounded-full"></div>
<div className="absolute bottom-0 right-0 w-60 h-60 bg-blue-300/20 blur-3xl rounded-full"></div>
{/* Center Text */}
<div className="relative z-10 flex flex-col items-center justify-center h-full text-center px-6">
<h1 className="text-5xl font-extrabold text-white drop-shadow-lg">
Welcome Back, {userDetails?.name || "User"} 👋
</h1>
<p className="text-white/90 mt-4 text-lg max-w-2xl">
Manage your eBay & Motor State store with powerful automation tools.
</p>
</div>
</div>
</div>
{/* DASHBOARD CARDS */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-12 mx-auto">
{cards.map((card, index) => (
<div
key={card.id}
className={`relative overflow-hidden rounded-3xl p-8 shadow-lg hover:shadow-2xl hover:-translate-y-3 transition duration-500 ${cardColors[index]}`}
>
<h2 className="text-2xl font-extrabold mb-4 text-center">
{card.title}
</h2>
<div>{card.content}</div>
</div>
))}
</div>
</div>
);
};
export default Home;

View File

@ -0,0 +1,155 @@
'use client';
import React, { useEffect, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import axios from "axios";
import IconXCircle from "@/components/icon/icon-x-circle";
interface PaymentDetails {
email: string;
amount: number;
planId: string;
stripeSessionId: string;
status: string;
created_at: string;
}
const PaymentFailure: React.FC = () => {
const [payment, setPayment] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const router = useRouter();
const searchParams = useSearchParams();
const sessionId = searchParams.get("session_id"); // Stripe sends session_id in query
useEffect(() => {
if (!sessionId) {
setError("No session_id provided");
setLoading(false);
return;
}
const fetchPaymentDetails = async () => {
try {
const res:any = await axios.get(
`https://ebay.backend.data4autos.com/api/payment/details`,
{ params: { session_id: sessionId } }
);
setPayment(res.data.payment);
} catch (err: any) {
console.error(err);
setError(err.response?.data?.error || "Failed to fetch payment details");
} finally {
setLoading(false);
}
};
fetchPaymentDetails();
}, [sessionId]);
if (loading) {
return (
<div className="flex items-center justify-center min-h-[83vh] text-gray-500">
Loading payment details...
</div>
);
}
if (error) {
return (
<div className="flex items-center justify-center min-h-[83vh] text-red-500">
{error}
</div>
);
}
return (
<div className="flex flex-col items-center justify-center min-h-[83vh] bg-gradient-to-br from-red-50 via-white to-red-100 px-4">
{/* ❌ Error Icon */}
<div className="animate-bounce mb-6">
<IconXCircle className="w-24 h-24 text-red-500 drop-shadow-md" />
</div>
{/* ❌ Title */}
<h1 className="text-3xl font-bold text-gray-800 mb-2 text-center">
Payment Failed
</h1>
{/* ❌ Subtitle */}
<p className="text-gray-600 mb-6 text-center max-w-md">
Unfortunately, your payment could not be processed. Please try again or
contact support if the issue persists.
</p>
{/* ❌ Card with Payment Details */}
<div className="bg-white shadow-lg rounded-2xl p-6 w-full max-w-md border border-gray-100 transition-transform duration-300 hover:scale-[1.02] space-y-3">
<div className="flex justify-between items-start">
<span className="text-gray-500">Transaction ID:</span>
<span className="font-semibold text-gray-800 break-all max-w-[60%]">
{payment?.stripeSessionId}
</span>
</div>
<div className="flex justify-between items-start">
<span className="text-gray-500">Email:</span>
<span className="font-semibold text-gray-800 break-words max-w-[60%]">
{payment?.email}
</span>
</div>
<div className="flex justify-between items-start">
<span className="text-gray-500">Plan:</span>
<span className="font-semibold text-gray-800 break-words max-w-[60%]">
{payment?.planId}
</span>
</div>
<div className="flex justify-between items-start">
<span className="text-gray-500">Amount:</span>
<span className="font-semibold text-gray-800 break-words max-w-[60%]">
${payment?.amount / 100}
</span>
</div>
<div className="flex justify-between items-start">
<span className="text-gray-500">Payment Status:</span>
<span className="font-semibold text-red-600 break-words max-w-[60%]">
{payment?.status}
</span>
</div>
<div className="flex justify-between items-start">
<span className="text-gray-500">Date:</span>
<span className="font-semibold text-gray-800 break-words max-w-[60%]">
{new Date(payment?.created_at || "").toLocaleString()}
</span>
</div>
</div>
{/* ❌ Buttons */}
<div className="mt-8 flex flex-col sm:flex-row gap-4">
<button
onClick={() => router.push("/pricing")}
className="bg-red-500 hover:bg-red-600 text-white px-6 py-3 rounded-xl shadow-md font-medium transition-all duration-200 hover:shadow-lg"
>
Retry Payment
</button>
<button
onClick={() => router.push("/support")}
className="bg-gray-200 hover:bg-gray-300 text-gray-800 px-6 py-3 rounded-xl shadow-md font-medium transition-all duration-200 hover:shadow-lg"
>
Contact Support
</button>
</div>
{/* ❌ Footer Text */}
<p className="mt-6 text-sm text-gray-400 text-center">
Dont worry no amount has been deducted from your account.
</p>
</div>
);
};
export default PaymentFailure;

View File

@ -0,0 +1,150 @@
'use client';
import React, { useEffect, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import axios from "axios";
import IconCircleCheck from "@/components/icon/icon-circle-check";
interface PaymentDetails {
email: string;
amount: string;
plan: string;
stripeSessionId: string;
status: string;
createdAt: string;
}
const PaymentSuccess: React.FC = () => {
const [payment, setPayment] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const router = useRouter();
const searchParams = useSearchParams();
const sessionId = searchParams.get("session_id");
useEffect(() => {
if (!sessionId) {
setError("No session_id provided");
setLoading(false);
return;
}
const fetchPaymentDetails = async () => {
try {
const res: any = await axios.get(
`https://ebay.backend.data4autos.com/api/payment/details`,
{ params: { session_id: sessionId } }
);
setPayment(res.data.payment);
localStorage.setItem('payment_session', res.data.payment?.stripeSessionId);
} catch (err: any) {
console.error(err);
setError(err.response?.data?.error || "Failed to fetch payment details");
} finally {
setLoading(false);
}
};
fetchPaymentDetails();
}, [sessionId]);
if (loading) {
return (
<div className="flex items-center justify-center min-h-[83vh] text-gray-500">
Loading payment details...
</div>
);
}
if (error) {
return (
<div className="flex items-center justify-center min-h-[83vh] text-red-500">
{error}
</div>
);
}
return (
<div className="flex flex-col items-center justify-center min-h-[83vh] bg-gradient-to-br from-green-50 via-white to-green-100 px-4">
{/* ✅ Success Icon */}
<div className="animate-bounce mb-3">
<IconCircleCheck className="w-24 h-24 text-green-500 drop-shadow-md" />
</div>
{/* ✅ Title */}
<h1 className="text-3xl font-bold text-gray-800 mb-2 text-center">
Payment Successful 🎉
</h1>
{/* ✅ Subtitle */}
<p className="text-gray-600 mb-6 text-center max-w-md">
Thank you for your purchase! Your payment has been processed successfully.
</p>
{/* Card with Details */}
<div className="bg-white shadow-lg rounded-2xl p-6 w-full max-w-md border border-gray-100 transition-transform duration-300 hover:scale-[1.02] space-y-3">
<div className="flex justify-between items-start mb-3">
<span className="text-gray-500">Transaction ID:</span>
<span className="font-semibold text-gray-800 break-all max-w-[60%]">
{payment?.stripeSessionId}
</span>
</div>
<div className="flex justify-between items-start mb-3">
<span className="text-gray-500">Email:</span>
<span className="font-semibold text-gray-800 break-words max-w-[60%]">
{payment?.email}
</span>
</div>
<div className="flex justify-between items-start mb-3">
<span className="text-gray-500">Plan:</span>
<span className="font-semibold text-gray-800 break-words max-w-[60%]">
{payment?.plan}
</span>
</div>
<div className="flex justify-between items-start mb-3">
<span className="text-gray-500">Amount:</span>
<span className="font-semibold text-gray-800 break-words max-w-[60%]">
${payment?.amount}
</span>
</div>
<div className="flex justify-between items-start mb-3">
<span className="text-gray-500">Payment Status:</span>
<span className="font-semibold text-green-600 break-words max-w-[60%]">
{payment?.status}
</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">Subscription:</span>
<span className="font-semibold text-gray-800">
{payment?.startDate
? `${new Date(payment.startDate).toLocaleDateString()}${new Date(payment.endDate).toLocaleDateString()}`
: "Pending activation"}
</span>
</div>
</div>
{/* ✅ Button */}
<button
onClick={() => router.push("/")}
className="mt-8 bg-green-500 hover:bg-green-600 text-white px-6 py-3 rounded-xl shadow-md font-medium transition-all duration-200 hover:shadow-lg"
>
Go to Dashboard
</button>
{/* ✅ Footer Text */}
<p className="mt-6 text-sm text-gray-400 text-center">
You can view your payment history anytime from your dashboard.
</p>
</div>
);
};
export default PaymentSuccess;

View File

@ -0,0 +1,39 @@
import ComponentsPricingTableToggle from '@/components/pricing-table/components-pricing-table-toggle';
import IconArrowLeft from '@/components/icon/icon-arrow-left';
import PanelCodeHighlight from '@/components/panel-code-highlight';
import { Metadata } from 'next';
import Link from 'next/link';
import React from 'react';
export const metadata: Metadata = {
title: 'Pricing Table',
};
const PricingTable = () => {
return (
<div>
<ul className="flex space-x-2 rtl:space-x-reverse">
<li>
<Link href="/" className="text-primary hover:underline">
Dashboard
</Link>
</li>
<li className="before:content-['/'] ltr:before:mr-2 rtl:before:ml-2">
<span>Pricing Table</span>
</li>
</ul>
<div className="space-y-8 pt-5">
{/* Basic */}
{/* Toggle */}
<ComponentsPricingTableToggle />
{/* Animated */}
</div>
</div>
);
};
export default PricingTable;

View File

@ -0,0 +1,165 @@
"use client";
import { useEffect, useState } from "react";
import Settings from "./store-settings";
import { useRouter } from "next/navigation";
import axios from "axios";
const API_URL = "https://ebay.backend.data4autos.com/api/ebay/inventory/locations";
export default function Page() {
const router = useRouter()
const [locations, setLocations] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [selectedKey, setSelectedKey] = useState<string | null>(null);
const [showForm, setShowForm] = useState(false);
const [payment, setPayment] = useState<any>(null);
useEffect(() => {
const role = localStorage.getItem("user_role");
const sessionId = localStorage.getItem("payment_session");
// ✅ Admins and Partners can access directly (skip payment check)
if (role === "admin" || role === "partner") {
return;
}
// 🚫 If no payment session, redirect to pricing
if (!sessionId) {
router.push("/pricing");
return;
}
// ✅ Otherwise, check payment details
const fetchPaymentDetails = async () => {
try {
const res: any = await axios.get(
"https://ebay.backend.data4autos.com/api/payment/details",
{ params: { session_id: sessionId } }
);
setPayment(res.data.payment);
} catch (err) {
console.error("Error fetching payment details:", err);
}
};
fetchPaymentDetails();
}, [router]);
useEffect(() => {
const fetchLocations = async () => {
try {
const userId = typeof window !== 'undefined' ? sessionStorage.getItem('USERID') : null;
const EBAYSTOREID = typeof window !== 'undefined' ? sessionStorage.getItem('EBAYSTOREID') : null;
const res = await fetch(API_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ accessToken: "clientaccesstoken", userKey: EBAYSTOREID }),
});
const data = await res.json();
setLocations(data.locations || []);
} catch (err) {
console.error("❌ Error fetching locations:", err);
} finally {
setLoading(false);
}
};
fetchLocations();
}, []);
const handleSaveExisting = () => {
const selected = locations.find((loc) => loc.merchantLocationKey === selectedKey);
console.log("✅ Selected Location:", selected);
};
const handleSaveNew = (payload: any) => {
console.log("📦 New Location Payload:", payload);
};
if (loading)
return (
<div className="flex items-center justify-center min-h-[83vh] bg-gradient-to-br from-[#e0f7ff] to-white text-lg font-medium">
Loading eBay Locations...
</div>
);
return (
<div className="min-h-[83vh] flex items-center justify-center bg-gradient-to-br from-[#00d1ff]/10 via-white to-[#00d1ff]/20 p-2 md:p-8">
<div className="max-w-7xl mx-auto backdrop-blur-md border border-white/40 shadow-2xl rounded-3xl p-3 md:p-8 transition-all duration-500">
<h1 className="text-3xl font-bold text-center text-[#00d1ff] mb-3 drop-shadow-sm">
eBay Locations
</h1>
{!showForm ? (
<>
{/* Locations Grid */}
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{locations.map((loc, idx) => (
<div
key={idx}
onClick={() => setSelectedKey(loc.merchantLocationKey)}
className={`relative p-6 rounded-2xl border cursor-pointer transform transition-all duration-300 ${selectedKey === loc.merchantLocationKey
? "bg-gradient-to-br from-[#00d1ff] to-[#007bff] text-white shadow-lg scale-105"
: "bg-white hover:shadow-xl hover:-translate-y-1 border-gray-200"
}`}
>
<div className="flex justify-between items-center">
<h2 className="text-lg font-semibold">{loc.merchantLocationKey || "Unnamed Location"}</h2>
{selectedKey === loc.merchantLocationKey && (
<span className="text-xs bg-white/30 px-2 py-1 rounded-md font-medium">
Selected
</span>
)}
</div>
<p
className={`mt-2 text-sm ${selectedKey === loc.merchantLocationKey ? "text-white/90" : "text-gray-600"
}`}
>
📞 {loc.phone || "N/A"}
</p>
<p
className={`text-sm ${selectedKey === loc.merchantLocationKey ? "text-white/90" : "text-gray-600"
}`}
>
📍 {loc.location?.address?.addressLine1},{" "}
{loc.location?.address?.city},{" "}
{loc.location?.address?.stateOrProvince}{" "}
{loc.location?.address?.postalCode}
</p>
<div className="mt-4 text-xs opacity-70">
Key: {loc.merchantLocationKey}
</div>
</div>
))}
</div>
{/* Buttons */}
<div className="mt-10 flex flex-wrap justify-center gap-6">
<button
onClick={handleSaveExisting}
disabled={!selectedKey}
className={`px-6 py-3 rounded-full text-white font-semibold transition-all shadow-md ${selectedKey
? "bg-gradient-to-r from-[#00d1ff] to-[#007bff] hover:scale-105"
: "bg-gray-400 cursor-not-allowed"
}`}
>
Save Selected
</button>
<button
onClick={() => setShowForm(true)}
className="px-6 py-3 rounded-full font-semibold text-[#00d1ff] border border-[#00d1ff] hover:bg-[#00d1ff] hover:text-white transition-all shadow-sm"
>
+ Create New Location
</button>
</div>
</>
) : (
<Settings onSave={handleSaveNew} />
)}
</div>
</div>
);
}

View File

@ -0,0 +1,221 @@
"use client";
import { useState } from "react";
type Props = { onSave: (data: any) => void };
const DAYS = [
"MONDAY",
"TUESDAY",
"WEDNESDAY",
"THURSDAY",
"FRIDAY",
"SATURDAY",
"SUNDAY",
];
export default function Settings({ onSave }: Props) {
const [addressLine1, setAddressLine1] = useState("");
const [city, setCity] = useState("");
const [stateOrProvince, setStateOrProvince] = useState("");
const [postalCode, setPostalCode] = useState("");
const [country, setCountry] = useState("US");
const [name, setName] = useState("");
const [phone, setPhone] = useState("");
const [timeZoneId, setTimeZoneId] = useState("America/New_York");
const [open, setOpen] = useState("09:00:00");
const [close, setClose] = useState("18:00:00");
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const operatingHours = DAYS.map((day) => ({
dayOfWeekEnum: day,
intervals: [{ open, close }],
}));
const payload = {
location: {
address: { addressLine1, city, stateOrProvince, postalCode, country },
},
locationTypes: ["STORE"],
name,
phone,
operatingHours,
timeZoneId,
merchantLocationStatus: "ENABLED",
};
onSave(payload);
};
return (
<div className="min-h-screen flex items-center justify-center p-6">
<div className="w-full max-w-2xl bg-white border border-gray-100 rounded-3xl shadow-2xl p-10 relative overflow-hidden">
{/* Glow Accent */}
<div className="absolute top-0 left-0 w-full h-2 bg-[#00d1ff]" />
<h2 className="text-3xl font-bold text-center text-[#00d1ff] mb-8 tracking-tight">
Create New Location
</h2>
<form onSubmit={handleSubmit} className="space-y-6">
{/* Store Name */}
<div>
<label className="block text-sm font-semibold text-gray-700 mb-1">
Store Name *
</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
required
className="w-full border border-gray-200 rounded-xl p-3 focus:ring-2 focus:ring-[#00d1ff] focus:border-[#00d1ff] outline-none transition duration-300 shadow-sm"
placeholder="Enter store name"
/>
</div>
{/* Phone */}
<div>
<label className="block text-sm font-semibold text-gray-700 mb-1">
Phone *
</label>
<input
type="text"
value={phone}
onChange={(e) => setPhone(e.target.value)}
required
className="w-full border border-gray-200 rounded-xl p-3 focus:ring-2 focus:ring-[#00d1ff] focus:border-[#00d1ff] outline-none transition duration-300 shadow-sm"
placeholder="Enter phone number"
/>
</div>
{/* Address */}
<div>
<label className="block text-sm font-semibold text-gray-700 mb-1">
Address Line 1 *
</label>
<input
type="text"
value={addressLine1}
onChange={(e) => setAddressLine1(e.target.value)}
required
className="w-full border border-gray-200 rounded-xl p-3 focus:ring-2 focus:ring-[#00d1ff] focus:border-[#00d1ff] outline-none transition duration-300 shadow-sm"
placeholder="Enter address"
/>
</div>
{/* City / State */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-semibold text-gray-700 mb-1">
City *
</label>
<input
type="text"
value={city}
onChange={(e) => setCity(e.target.value)}
required
className="w-full border border-gray-200 rounded-xl p-3 focus:ring-2 focus:ring-[#00d1ff] focus:border-[#00d1ff] outline-none transition duration-300 shadow-sm"
placeholder="City"
/>
</div>
<div>
<label className="block text-sm font-semibold text-gray-700 mb-1">
State *
</label>
<input
type="text"
value={stateOrProvince}
onChange={(e) => setStateOrProvince(e.target.value)}
required
className="w-full border border-gray-200 rounded-xl p-3 focus:ring-2 focus:ring-[#00d1ff] focus:border-[#00d1ff] outline-none transition duration-300 shadow-sm"
placeholder="State"
/>
</div>
</div>
{/* Postal / Country */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-semibold text-gray-700 mb-1">
Postal Code *
</label>
<input
type="text"
value={postalCode}
onChange={(e) => setPostalCode(e.target.value)}
required
className="w-full border border-gray-200 rounded-xl p-3 focus:ring-2 focus:ring-[#00d1ff] focus:border-[#00d1ff] outline-none transition duration-300 shadow-sm"
placeholder="Postal Code"
/>
</div>
<div>
<label className="block text-sm font-semibold text-gray-700 mb-1">
Country *
</label>
<input
type="text"
value={country}
onChange={(e) => setCountry(e.target.value)}
required
className="w-full border border-gray-200 rounded-xl p-3 focus:ring-2 focus:ring-[#00d1ff] focus:border-[#00d1ff] outline-none transition duration-300 shadow-sm"
placeholder="Country Code (e.g. US)"
/>
</div>
</div>
{/* Time Zone */}
<div>
<label className="block text-sm font-semibold text-gray-700 mb-1">
Time Zone *
</label>
<input
type="text"
value={timeZoneId}
onChange={(e) => setTimeZoneId(e.target.value)}
required
className="w-full border border-gray-200 rounded-xl p-3 focus:ring-2 focus:ring-[#00d1ff] focus:border-[#00d1ff] outline-none transition duration-300 shadow-sm"
placeholder="e.g. America/New_York"
/>
</div>
{/* Hours */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-semibold text-gray-700 mb-1">
Open Time *
</label>
<input
type="time"
value={open}
onChange={(e) => setOpen(e.target.value)}
required
className="w-full border border-gray-200 rounded-xl p-3 focus:ring-2 focus:ring-[#00d1ff] focus:border-[#00d1ff] outline-none transition duration-300 shadow-sm"
/>
</div>
<div>
<label className="block text-sm font-semibold text-gray-700 mb-1">
Close Time *
</label>
<input
type="time"
value={close}
onChange={(e) => setClose(e.target.value)}
required
className="w-full border border-gray-200 rounded-xl p-3 focus:ring-2 focus:ring-[#00d1ff] focus:border-[#00d1ff] outline-none transition duration-300 shadow-sm"
/>
</div>
</div>
{/* Button */}
<button
type="submit"
className="w-full bg-[#00d1ff] text-white py-3 rounded-xl font-semibold shadow-lg hover:bg-[#00c2ed] hover:shadow-xl transition-all duration-300"
>
Save & Console JSON
</button>
</form>
</div>
</div>
);
}

View File

@ -0,0 +1,375 @@
'use client';
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import axios from 'axios';
export default function Turn14SettingsClient() {
const router = useRouter();
const [ftpHost, setFtpHost] = useState('ftp.motorstateftp.com');
const [ftpUsername, setFtpUsername] = useState('');
const [ftpPassword, setFtpPassword] = useState('');
const [message, setMessage] = useState('');
const [loading, setLoading] = useState(false);
const [statusLoaded, setStatusLoaded] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const [connectionStatus, setConnectionStatus] = useState<'idle' | 'success' | 'error'>('idle');
const [userId, setUserId] = useState<string | null>(null);
const [payment, setPayment] = useState<any>(null);
useEffect(() => {
// Check for user ID in cookies or localStorage
// Note: Cookies cannot be directly accessed client-side; you may need to sync with the server
const uid = localStorage.getItem('data4auto_uid');
if (uid) {
setUserId(uid);
} else {
router.push('/login');
}
}, [router]);
useEffect(() => {
const role = localStorage.getItem("user_role");
const sessionId = localStorage.getItem("payment_session");
// ✅ Admins and Partners can access directly (skip payment check)
if (role === "admin" || role === "partner") {
return;
}
// 🚫 If no payment session, redirect to pricing
if (!sessionId) {
router.push("/pricing");
return;
}
// ✅ Otherwise, check payment details
const fetchPaymentDetails = async () => {
try {
const res: any = await axios.get(
"https://ebay.backend.data4autos.com/api/payment/details",
{ params: { session_id: sessionId } }
);
setPayment(res.data.payment);
} catch (err) {
console.error("Error fetching payment details:", err);
}
};
fetchPaymentDetails();
}, [router]);
const read = async (r: Response) =>
r.headers.get('content-type')?.includes('application/json')
? r.json()
: r.text();
// Load FTP Status on Mount
useEffect(() => {
if (!userId) return;
console.log('Fetching existing FTP status...');
const fetchStatus = async () => {
setLoading(true);
try {
const res = await fetch('https://ebay.backend.data4autos.com/api/motorstate/auth/motorstate/status', {
// ^ make sure this path matches the Express route
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userid: userId }),
});
const data = await read(res);
if (!res.ok) {
throw new Error(
(typeof data === 'object' && (data?.message || data?.error)) ||
`Status failed (${res.status})`
);
}
if (data?.hasCredentials) {
setFtpHost(data.credentials.ftpHost || 'ftp.motorstateftp.com');
setFtpUsername(data.credentials.ftpUsername || '');
setFtpPassword(data.credentials.ftpPassword || '');
setMessage('✅ Existing FTP credentials loaded.');
setConnectionStatus('success');
} else {
setMessage(' No credentials saved yet.');
setConnectionStatus('idle');
}
} catch (e: any) {
console.error('Error fetching FTP status:', e);
setMessage(`${e?.message || 'Failed to fetch status.'}`);
setConnectionStatus('error');
} finally {
setLoading(false);
setStatusLoaded(true);
}
};
fetchStatus();
}, [userId]);
// Test and Save Credentials
const handleTestAndSave = async () => {
setMessage('');
if (!ftpUsername || !ftpPassword) {
setMessage('❌ Please enter FTP Username and Password.');
setConnectionStatus('error');
return;
}
if (!userId) {
setMessage('❌ User ID not found. Please log in again.');
setConnectionStatus('error');
return;
}
setLoading(true);
try {
setMessage('Testing FTP connection... Please wait.');
// Test the FTP connection
const testRes = await fetch('https://motorstate.data4autos.com/api/tokens', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
clientName: "motorstate",
host: ftpHost,
user: ftpUsername,
password: ftpPassword,
}),
});
const testData = await read(testRes);
if (!testData.ok) {
throw new Error(testData.message || 'FTP connection failed');
}
// If test is successful, save the credentials
setMessage('✅ FTP connection successful. Saving credentials...');
const saveRes = await fetch('https://ebay.backend.data4autos.com/api/motorstate/auth/motorstate/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userid: userId,
ftpHost,
ftpUsername,
ftpPassword,
clientName : "motorstate",
}),
});
const saveData = await read(saveRes);
if (!saveRes.ok) {
console.error('Save request failed:', saveRes.status, saveData);
throw new Error(
(typeof saveData === 'object' &&
(saveData?.message || saveData?.error)) ||
`Save failed (${saveRes.status})`
);
}
setMessage('✅ Credentials tested and saved successfully.');
setConnectionStatus('success');
} catch (e: any) {
console.error('Error in handleTestAndSave:', e);
setMessage(`${e?.message || 'Failed to test or save credentials.'}`);
setConnectionStatus('error');
} finally {
setLoading(false);
}
};
const getStatusColor = () => {
switch (connectionStatus) {
case 'success':
return 'bg-green-100 border-green-400 text-green-800';
case 'error':
return 'bg-red-100 border-red-400 text-red-800';
default:
return 'bg-blue-100 border-blue-400 text-blue-800';
}
};
const getStatusIcon = () => {
switch (connectionStatus) {
case 'success':
return (
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clipRule="evenodd"
/>
</svg>
);
case 'error':
return (
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
clipRule="evenodd"
/>
</svg>
);
default:
return (
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
clipRule="evenodd"
/>
</svg>
);
}
};
return (
<div className="min-h-[83vh] bg-gradient-to-br from-[#00d1ff]/10 via-white to-[#00d1ff]/20 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-2xl mx-auto bg-white border border-gray-200 rounded-2xl shadow-lg overflow-hidden">
<div className="px-6 py-8">
{/* Header Section */}
<div className="text-center mb-8">
<div className="inline-flex items-center justify-center w-16 h-16 bg-[#00d1ff]/10 rounded-2xl mb-4">
<svg
className="w-8 h-8 text-[#00d1ff]"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M13 10V3L4 14h7v7l9-11h-7z"
/>
</svg>
</div>
<h1 className="text-3xl font-bold text-gray-900 mb-1">
<span className="text-[#00d1ff]">MotorState FTP </span> Configuration
</h1>
<p className="text-gray-600 text-sm">
Manage your MotorState FTP credentials securely.
</p>
</div>
{/* Input Fields */}
<div className="space-y-6">
<div>
<label htmlFor="ftpHost" className="block text-sm font-semibold text-gray-700 mb-2">
FTP Host
</label>
<input
id="ftpHost"
type="text"
value={ftpHost}
onChange={(e) => setFtpHost(e.target.value)}
className="block w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#00d1ff] focus:border-[#00d1ff] transition-all"
placeholder="Enter your FTP Host (e.g., ftp.example.com)"
/>
</div>
<div>
<label htmlFor="ftpUsername" className="block text-sm font-semibold text-gray-700 mb-2">
FTP Username
</label>
<input
id="ftpUsername"
type="text"
value={ftpUsername}
onChange={(e) => setFtpUsername(e.target.value)}
className="block w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#00d1ff] focus:border-[#00d1ff] transition-all"
placeholder="Enter your FTP Username"
/>
</div>
<div>
<label htmlFor="ftpPassword" className="block text-sm font-semibold text-gray-700 mb-2">
FTP Password
</label>
<div className="relative">
<input
id="ftpPassword"
type={showPassword ? 'text' : 'password'}
value={ftpPassword}
onChange={(e) => setFtpPassword(e.target.value)}
className="block w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#00d1ff] focus:border-[#00d1ff] transition-all pr-10"
placeholder="Enter your FTP Password"
/>
<button
type="button"
className="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-[#00d1ff]"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? '🙈' : '👁️'}
</button>
</div>
</div>
{/* Test and Save Button */}
<button
onClick={handleTestAndSave}
disabled={loading}
className="w-full flex justify-center items-center py-3 px-4 border border-transparent rounded-lg shadow-md text-sm font-medium text-white bg-[#00d1ff] hover:bg-[#00bce5] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#00d1ff] transition-all"
>
{loading ? (
<>
<svg
className="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
Testing...
</>
) : (
'Test and Save'
)}
</button>
{/* Status Message */}
{statusLoaded && message && (
<div
className={`mt-4 p-4 rounded-lg border ${getStatusColor()} flex items-start space-x-3`}
>
<div className="flex-shrink-0">{getStatusIcon()}</div>
<p className="text-sm font-medium">{message}</p>
</div>
)}
{/* Tips Section */}
<div className="bg-[#00d1ff]/5 p-4 rounded-lg border border-[#00d1ff]/20 mt-6">
<h3 className="text-sm font-semibold text-[#00d1ff] mb-2">
💡 Connection Tips
</h3>
<ul className="text-xs text-gray-600 space-y-1">
<li> Ensure your FTP credentials are valid and active.</li>
<li> The default FTP host is ftp.motorstateftp.com.</li>
<li> Your credentials will be tested before saving.</li>
</ul>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,12 @@
// app/(defaults)/turn14-settings/page.tsx
import type { Metadata } from 'next';
import Turn14SettingsClient from './Turn14SettingsClient';
export const metadata: Metadata = {
title: 'Turn14 Settings',
};
export default function Page() {
return <Turn14SettingsClient />;
}

119
app/api/auth/login/route.ts Normal file
View File

@ -0,0 +1,119 @@
import { NextResponse } from 'next/server';
const AUTH_API_BASE = process.env.AUTH_API_BASE ?? 'https://ebay.backend.data4autos.com';
const SESSION_MAX_AGE_S = 30 * 60; // 30 minutes in seconds
// Utility to extract userId from a nested object
function extractUserId(obj: any): string | undefined {
if (!obj || typeof obj !== 'object') return undefined;
// Try common keys at the top level
const commonKeys = ['userid', 'userId', 'id', 'uuid', '_id', 'user_id'];
for (const key of commonKeys) {
const value = obj[key];
if (typeof value === 'string' && value.trim()) return value;
if (typeof value === 'number') return String(value);
}
// Check common container keys
const containers = ['user', 'data', 'profile', 'result'];
for (const container of containers) {
const value = obj[container];
const found = extractUserId(value);
if (found) return found;
}
// Recursive scan of all nested objects
for (const value of Object.values(obj)) {
if (value && typeof value === 'object') {
const found = extractUserId(value);
if (found) return found;
}
}
return undefined;
}
export async function POST(req: Request) {
try {
// Parse request body
const body = await req.json();
console.log('[login] Request body at', new Date().toISOString(), ':', JSON.stringify(body, null, 2));
// Proxy request to upstream API
const upstream = await fetch(`${AUTH_API_BASE}/api/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
// Parse upstream response
const contentType = upstream.headers.get('content-type') ?? 'application/json';
let data: any;
if (contentType.includes('application/json')) {
data = await upstream.json();
} else {
data = await upstream.text();
}
console.log('[login] Upstream response at', new Date().toISOString(), ':', JSON.stringify(data, null, 2));
// Prepare response
const res = NextResponse.json(data, { status: upstream.status });
if (!upstream.ok) {
console.error('[login] Upstream request failed with status:', upstream.status, 'Response:', JSON.stringify(data, null, 2));
return res; // Return early if upstream fails
}
// Extract token
const token =
typeof data === 'object'
? data.token || data.accessToken || data.access_token || '1'
: '1';
console.log('[login] Extracted token:', token);
// Extract userId
const userId = typeof data === 'object' ? extractUserId(data) : undefined;
console.log('[login] Extracted userId:', userId);
if (!userId) {
console.warn('[login] Could not extract userId from upstream payload:', JSON.stringify(data, null, 2));
return NextResponse.json(
{ message: 'Login successful but user ID not found in response' },
{ status: 200 }
);
}
// Define cookie options
const cookieOptions = {
httpOnly: true,
sameSite: 'lax' as const,
secure: process.env.NODE_ENV === 'production', // Allow non-secure cookies in development
path: '/',
maxAge: SESSION_MAX_AGE_S,
};
// Set cookies
try {
res.cookies.set('d4a_session', token, cookieOptions);
res.cookies.set('d4a_exp', String(Date.now() + SESSION_MAX_AGE_S * 1000), cookieOptions);
res.cookies.set('d4a_uid', userId, cookieOptions);
console.log('[login] Set cookies: d4a_session, d4a_exp, d4a_uid (value:', userId, ')');
} catch (cookieError) {
console.error('[login] Error setting cookies at', new Date().toISOString(), ':', cookieError);
return NextResponse.json(
{ message: 'Login successful but failed to set cookies' },
{ status: 200 }
);
}
return res;
} catch (error) {
console.error('[login] Error at', new Date().toISOString(), ':', error);
return NextResponse.json({ message: 'Internal server error' }, { status: 500 });
}
}
export async function GET() {
console.log('[login] Received GET request');
return NextResponse.json({ ok: true }, { status: 200 });
}

View File

@ -0,0 +1,43 @@
// import { NextResponse } from 'next/server';
// import { cookies } from 'next/headers';
// export async function POST() {
// try {
// // Get the cookie store
// const cookieStore = cookies();
// // Delete the d4a_uid cookie
// cookieStore.delete('d4a_uid');
// // Return a successful response
// return NextResponse.json({ message: 'Logged out successfully' }, { status: 200 });
// } catch (error) {
// console.error('Logout error:', error);
// // Return an error response if something goes wrong
// return NextResponse.json({ message: 'Logout failed' }, { status: 500 });
// }
// }
import { NextResponse } from 'next/server';
import { cookies } from 'next/headers';
export async function POST() {
try {
const cookieStore = cookies();
const cookieOptions = {
path: '/',
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
sameSite: 'lax' as const,
expires: new Date(0), // Set to a past date to expire the cookie
};
// Expire cookies to delete them
cookieStore.set('d4a_session', '', cookieOptions);
cookieStore.set('d4a_exp', '', cookieOptions);
cookieStore.set('d4a_uid', '', cookieOptions);
return NextResponse.json({ message: 'Logged out successfully' }, { status: 200 });
} catch (error) {
console.error('Logout error:', error);
return NextResponse.json({ message: 'Logout failed' }, { status: 500 });
}
}

View File

@ -0,0 +1,26 @@
import { NextResponse } from 'next/server';
const AUTH_API_BASE = process.env.AUTH_API_BASE ?? 'https://ebay.backend.data4autos.com';
export async function POST(req: Request) {
const body = await req.json();
// Forward to your backend
const upstream = await fetch(`${AUTH_API_BASE}/api/auth/signup`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
const contentType = upstream.headers.get('content-type') ?? 'application/json';
const buf = await upstream.arrayBuffer();
return new Response(buf, {
status: upstream.status,
headers: { 'content-type': contentType },
});
}
export async function GET() {
return NextResponse.json({ ok: true });
}

View File

@ -0,0 +1,14 @@
import { cookies } from 'next/headers';
import { NextResponse } from 'next/server';
export async function GET() {
if (process.env.NODE_ENV === 'production') {
return new NextResponse('Not found', { status: 404 });
}
const c = cookies();
return NextResponse.json({
d4a_uid: c.get('d4a_uid')?.value ?? null,
d4a_session_set: !!c.get('d4a_session'),
d4a_exp: c.get('d4a_exp')?.value ?? null,
});
}

View File

@ -0,0 +1,111 @@
import { NextResponse } from 'next/server';
import { cookies } from 'next/headers';
const AUTH_API_BASE = process.env.AUTH_API_BASE ?? 'https://ebay.backend.data4autos.com';
const TURN14_TOKEN_URL = 'https://turn14.data4autos.com/v1/auth/token';
export async function POST(req: Request) {
console.log('[turn14/save] Received POST request at', new Date().toISOString());
// Get user ID from cookies
const cookieStore = cookies();
const uid = cookieStore.get('d4a_uid')?.value;
if (!uid) {
console.error('[turn14/save] Missing d4a_uid cookie');
return NextResponse.json(
{ code: 'UNAUTHORIZED', message: 'User ID missing. Please login.' },
{ status: 401 }
);
}
console.log('[turn14/save] Found d4a_uid:', uid);
try {
const body = await req.json();
console.log('[turn14/save] Request body:', JSON.stringify(body, null, 2));
const { turn14clientid, turn14clientsecret } = body;
if (!turn14clientid || !turn14clientsecret) {
console.error('[turn14/save] Missing clientid or secret');
return NextResponse.json(
{
code: 'BAD_REQUEST',
message: 'turn14clientid and turn14clientsecret are required',
},
{ status: 400 }
);
}
// Optional: Fetch token from Turn14 (uncomment if needed)
/*
console.log('[turn14/save] Fetching token from Turn14');
const tokenResp = await fetch(TURN14_TOKEN_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'client_credentials',
client_id: turn14clientid,
client_secret: turn14clientsecret,
}),
});
const text = await tokenResp.text();
if (!tokenResp.ok) {
console.error('[turn14/save] Token fetch failed:', tokenResp.status, text);
return new Response(text || 'Failed to get Turn14 token', {
status: tokenResp.status,
headers: { 'content-type': tokenResp.headers.get('content-type') ?? 'text/plain' },
});
}
const json = JSON.parse(text);
const turn14accesstoken = json?.access_token;
const turn14expiresin = String(json?.expires_in ?? '3600');
if (!turn14accesstoken) {
console.error('[turn14/save] Missing access_token in Turn14 response');
return NextResponse.json(
{ code: 'TOKEN_ERROR', message: 'Turn14 response missing access_token' },
{ status: 502 }
);
}
*/
const payload = {
userid: uid,
turn14clientid,
turn14clientsecret,
// turn14accesstoken, // Uncomment if token fetching is enabled
// turn14expiresin, // Uncomment if token fetching is enabled
};
console.log('[turn14/save] Sending payload to upstream:', JSON.stringify(payload, null, 2));
const upstream = await fetch(`${AUTH_API_BASE}/api/auth/turn14/save`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
console.log('[turn14/save] Upstream response status:', upstream.status);
const contentType = upstream.headers.get('content-type') ?? 'application/json';
const buf = await upstream.arrayBuffer();
return new Response(buf, {
status: upstream.status,
headers: { 'content-type': contentType },
});
} catch (error) {
console.error('[turn14/save] Error at', new Date().toISOString(), ':', error);
return NextResponse.json(
{ code: 'SERVER_ERROR', message: 'Internal server error' },
{ status: 500 }
);
}
}
export async function GET() {
console.log('[turn14/save] Received GET request at', new Date().toISOString());
return NextResponse.json({ ok: true });
}

View File

@ -0,0 +1,24 @@
// app/api/turn14/token/route.ts
export async function POST(req: Request) {
const body = await req.json();
const upstream = await fetch('https://turn14.data4autos.com/v1/auth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
const contentType = upstream.headers.get('content-type') ?? 'application/json';
const buf = await upstream.arrayBuffer();
return new Response(buf, {
status: upstream.status,
headers: { 'content-type': contentType },
});
}
export async function GET() {
return new Response(JSON.stringify({ ok: true }), {
headers: { 'content-type': 'application/json' },
});
}

View File

@ -0,0 +1,92 @@
import { cookies } from 'next/headers';
import { NextResponse } from 'next/server';
const AUTH_API_BASE = process.env.AUTH_API_BASE ?? 'https://ebay.backend.data4autos.com';
const TURN14_TOKEN_URL = 'https://turn14.data4autos.com/v1/auth/token';
export async function POST(req: Request) {
//console.log('Received POST request to /api/turn14/update-token');
const uid = cookies().get('d4a_uid')?.value;
if (!uid) {
console.log('Missing d4a_uid cookie');
return NextResponse.json({ code: 'UNAUTHORIZED', message: 'User id missing. Please login.' }, { status: 401 });
}
const body = await req.json();
//console.log('Request body:', body);
let {
turn14accesstoken,
turn14expiresin,
turn14clientid,
turn14clientsecret,
} = body ?? {};
if (!turn14accesstoken) {
if (!turn14clientid || !turn14clientsecret) {
console.log('Missing clientid or secret');
return NextResponse.json(
{ code: 'BAD_REQUEST', message: 'Provide token+expiresin OR clientId+secret to fetch token' },
{ status: 400 }
);
}
console.log('Fetching token from Turn14');
const tokenResp = await fetch(TURN14_TOKEN_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'client_credentials',
client_id: turn14clientid,
client_secret: turn14clientsecret,
}),
});
const text = await tokenResp.text();
if (!tokenResp.ok) {
console.log('Token fetch failed:', tokenResp.status, text);
return new Response(text || 'Failed to get Turn14 token', {
status: tokenResp.status,
headers: { 'content-type': tokenResp.headers.get('content-type') ?? 'text/plain' },
});
}
const json = JSON.parse(text);
turn14accesstoken = json?.access_token;
turn14expiresin = String(json?.expires_in ?? '3600');
if (!turn14accesstoken) {
console.log('Missing access_token in Turn14 response');
return NextResponse.json({ code: 'TOKEN_ERROR', message: 'Turn14 response missing access_token' }, { status: 502 });
}
}
const payload = {
userid: uid,
turn14accesstoken,
turn14expiresin: String(turn14expiresin ?? '3600'),
};
//console.log('Sending payload to upstream:', payload);
const upstream = await fetch(`${AUTH_API_BASE}/api/auth/turn14/update-token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
console.log('Upstream response:', upstream.status);
const contentType = upstream.headers.get('content-type') ?? 'application/json';
const buf = await upstream.arrayBuffer();
return new Response(buf, { status: upstream.status, headers: { 'content-type': contentType } });
}
export async function GET() {
console.log('Received GET request to /api/turn14/update-token');
return NextResponse.json({ ok: true });
}
/* import { NextResponse } from 'next/server';
export async function POST(req: Request) {
console.log('Received POST request to /api/turn14/update-token');
return NextResponse.json({ message: 'POST received' });
}
export async function GET() {
console.log('Received GET request to /api/turn14/update-token');
return NextResponse.json({ ok: true });
} */

BIN
app/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

28
app/layout.tsx Normal file
View File

@ -0,0 +1,28 @@
import ProviderComponent from '@/components/layouts/provider-component';
import 'react-perfect-scrollbar/dist/css/styles.css';
import '../styles/tailwind.css';
import { Metadata } from 'next';
import { Nunito } from 'next/font/google';
export const metadata: Metadata = {
title: {
template: 'Data4Autos',
default: 'Data4Autos',
},
};
const nunito = Nunito({
weight: ['400', '500', '600', '700', '800'],
subsets: ['latin'],
display: 'swap',
variable: '--font-nunito',
});
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body className={nunito.variable}>
<ProviderComponent>{children}</ProviderComponent>
</body>
</html>
);
}

8
app/loading.tsx Normal file
View File

@ -0,0 +1,8 @@
import Loading from '@/components/layouts/loading';
import React from 'react';
const loading = () => {
return <Loading />;
};
export default loading;

26
app/not-found.tsx Normal file
View File

@ -0,0 +1,26 @@
import { Metadata } from 'next';
import Link from 'next/link';
import React from 'react';
export const metadata: Metadata = {
title: 'Error 404',
};
const NotFound = () => {
return (
<div className="relative flex min-h-screen items-center justify-center overflow-hidden">
<div className="px-6 py-16 text-center font-semibold before:container before:absolute before:left-1/2 before:aspect-square before:-translate-x-1/2 before:rounded-full before:bg-[linear-gradient(180deg,#4361EE_0%,rgba(67,97,238,0)_50.73%)] before:opacity-10 md:py-20">
<div className="relative">
<img src="/assets/images/error/404-dark.svg" alt="404" className="dark-img mx-auto -mt-10 w-full max-w-xs object-cover md:-mt-14 md:max-w-xl" />
<img src="/assets/images/error/404-light.svg" alt="404" className="light-img mx-auto -mt-10 w-full max-w-xs object-cover md:-mt-14 md:max-w-xl" />
<p className="mt-5 text-base dark:text-white">The page you requested was not found!</p>
<Link href="/" className="btn btn-gradient mx-auto !mt-7 w-max border-0 uppercase shadow-none">
Home
</Link>
</div>
</div>
</div>
);
};
export default NotFound;

View File

@ -0,0 +1,152 @@
'use client';
import React, { Fragment } from 'react';
import { Dialog, Transition, TransitionChild, DialogPanel } from '@headlessui/react';
import IconX from '@/components/icon/icon-x';
interface EditProfileModalProps {
isOpen: boolean;
onClose: () => void;
params: any;
onChange: (e: any) => void;
onSave: () => void;
}
const EditProfileModal: React.FC<EditProfileModalProps> = ({
isOpen,
onClose,
params,
onChange,
onSave,
}) => {
return (
<Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" open={isOpen} onClose={onClose} className="relative z-50">
<TransitionChild
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-[black]/60" />
</TransitionChild>
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center px-4 py-8">
<TransitionChild
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<DialogPanel className="panel w-full max-w-lg overflow-hidden rounded-lg border-0 p-0 text-black dark:text-white-dark">
{/* Close Button */}
<button
type="button"
onClick={onClose}
className="absolute top-4 text-gray-400 hover:text-gray-800 ltr:right-4 rtl:left-4"
>
<IconX />
</button>
{/* Header */}
<div className="bg-[#00d1ff] py-3 text-lg font-medium ltr:pl-5 rtl:pr-5 dark:bg-[#121c2c]">
Update Profile
</div>
{/* Form */}
<div className="p-5">
<form noValidate>
<div className="mb-4">
<label>Name</label>
<input
id="name"
type="text"
className="form-input"
value={params.name}
onChange={onChange}
placeholder="Enter full name"
/>
</div>
<div className="mb-4">
<label>Email</label>
<input
id="email"
type="email"
className="form-input"
value={params.email}
onChange={onChange}
placeholder="Enter email"
/>
</div>
<div className="mb-4">
<label>Phone Number</label>
<input
id="phonenumber"
type="text"
className="form-input"
value={params.phonenumber}
onChange={onChange}
placeholder="Enter phone number"
/>
</div>
{/* <div className="mb-4">
<label>Company Name</label>
<input
id="companyname"
type="text"
className="form-input"
value={params.companyname}
onChange={onChange}
placeholder="Enter company name"
/>
</div> */}
{/* ✅ Role Display (Read-only) */}
{/* <div className="mb-4">
<label>Role</label>
<input
id="role"
type="hidden"
className="form-input bg-gray-100 cursor-not-allowed text-gray-700"
value={params.role}
readOnly
/>
</div> */}
<div className="mt-6 flex justify-end">
<button
type="button"
className="btn btn-outline-danger"
onClick={onClose}
>
Cancel
</button>
<button
type="button"
className="btn btn-primary ltr:ml-4 rtl:mr-4"
onClick={onSave}
>
Save Changes
</button>
</div>
</form>
</div>
</DialogPanel>
</TransitionChild>
</div>
</div>
</Dialog>
</Transition>
);
};
export default EditProfileModal;

View File

@ -0,0 +1,52 @@
"use client";
import React, { useState } from "react";
import axios from "axios";
export default function ForgotPasswordForm() {
const [email, setEmail] = useState("");
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState("");
const handleForgot = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setMessage("");
try {
const res = await axios.post("https://ebay.backend.data4autos.com/api/auth/forgot-password", { email });
setMessage(" Weve emailed you a reset link. Please click the link for reset password");
} catch (err: any) {
console.error(err);
setMessage("Something went wrong. Try again.");
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleForgot} className="space-y-5">
<div>
<label className="block text-sm font-medium text-white mb-1">
Email
</label>
<input
type="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
className="form-input ps-10 placeholder:text-white-dark"
placeholder="you@example.com"
/>
</div>
<button
type="submit"
disabled={loading}
className="btn w-full border-0 uppercase shadow-[0_10px_20px_-10px_rgba(25,212,251,0.44)] disabled:cursor-not-allowed disabled:opacity-70 !mt-6 bg-[linear-gradient(135deg,#0EA5E9_0%,#19D4FB_50%,#67E8F9_100%)] text-white hover:bg-[linear-gradient(135deg,#67E8F9_0%,#19D4FB_50%,#0EA5E9_100%)]"
>
{loading ? "Sending..." : "Send Reset Link"}
</button>
{message && <p className="mt-2 text-center text-sm font-semibold text-primary">{message}</p>}
</form>
);
}

View File

@ -0,0 +1,172 @@
'use client';
import React, { useState } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import IconLockDots from '@/components/icon/icon-lock-dots';
import IconMail from '@/components/icon/icon-mail';
import { Eye, EyeOff } from "lucide-react";
const ComponentsAuthLoginForm = () => {
const router = useRouter();
const searchParams = useSearchParams();
const nextUrl = searchParams.get('next') || '/';
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [loading, setLoading] = useState(false);
const [err, setErr] = useState<string | null>(null);
const [msg, setMsg] = useState<string | null>(null);
const submitForm = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setErr(null);
setMsg(null);
if (!email || !password) {
setErr('Please enter email and password.');
return;
}
setLoading(true);
try {
// ✅ Call your Next.js API (same origin), which sets d4a_uid, d4a_session, d4a_exp cookies
const res = await fetch('https://ebay.backend.data4autos.com/api/motorstate/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
// credentials not required for same-origin, but harmless:
credentials: 'same-origin',
body: JSON.stringify({ email, password }),
});
const contentType = res.headers.get('content-type') || '';
const data = contentType.includes('application/json') ? await res.json() : await res.text();
console.log("data data", data)
try {
console.log('data:', data.store);
sessionStorage.setItem('USERID', data.userid);
sessionStorage.setItem('EBAYSTOREID', data.store.urlPath);
localStorage.setItem('data4auto_uid', data.userid);
localStorage.setItem('d4a_email', data.email);
localStorage.setItem('user_role', data.role);
data?.payment?.stripeSessionId && localStorage.setItem('payment_session', data?.payment?.stripeSessionId);
localStorage.setItem("token", "true"); // ✅ Prevents redirect issue
console.log('set sessionStorage USERID');
} catch {
console.log('no sessionStorage');
}
if (!res.ok) {
throw new Error((typeof data === 'object' && (data?.message || data?.error)) || `Login failed (${res.status})`);
}
// (DEV ONLY) quick check that cookies were set:
if (process.env.NODE_ENV !== 'production') {
try {
const who = await fetch('/api/debug/whoami', { cache: 'no-store' }).then((r) => r.json());
console.log('whoami:', who);
} catch { }
}
setMsg('Login successful!');
setTimeout(() => router.push(nextUrl), 500);
} catch (e: any) {
setErr(e?.message || 'Something went wrong. Please try again.');
} finally {
setLoading(false);
}
};
return (
<form className="space-y-5 dark:text-white" onSubmit={submitForm}>
{/* Alerts */}
{err && <div className="rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700">{err}</div>}
{msg && <div className="rounded-md border border-green-200 bg-green-50 px-3 py-2 text-sm text-green-700">{msg}</div>}
{/* Email */}
<div>
<label htmlFor="Email">Email</label>
<div className="relative text-white-dark">
<input
id="Email"
type="email"
placeholder="Enter Email"
className="form-input ps-10 placeholder:text-white-dark"
value={email}
onChange={(e) => setEmail(e.target.value)}
autoComplete="email"
/>
<span className="absolute start-4 top-1/2 -translate-y-1/2">
<IconMail fill={true} />
</span>
</div>
</div>
{/* Password */}
{/* Password */}
<div>
<label htmlFor="Password">Password</label>
<div className="relative text-white-dark">
<input
id="Password"
type={showPassword ? "text" : "password"} // 👈 toggle between text/password
placeholder="Enter Password"
className="form-input ps-10 pe-10 placeholder:text-white-dark"
value={password}
onChange={(e) => setPassword(e.target.value)}
autoComplete="current-password"
/>
{/* Lock icon (left) */}
<span className="absolute start-4 top-1/2 -translate-y-1/2">
<IconLockDots fill={true} />
</span>
{/* 👁️ Eye toggle (right) */}
<button
type="button"
className="absolute end-3 top-1/2 -translate-y-1/2 text-gray-400"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
</button>
</div>
</div>
<div className="text-end mt-4">
<a href="/forgot-password" className="text-sm text-[#19d4fb] hover:underline">
Forgot Password?
</a>
</div>
{/* Optional newsletter */}
{/* <div>
<label className="flex cursor-pointer items-center">
<input type="checkbox" className="form-checkbox bg-white dark:bg-black" />
<span className="text-white-dark">Subscribe to weekly newsletter</span>
</label>
</div> */}
{/* <button
type="submit"
disabled={loading}
className="btn btn-gradient !mt-6 w-full border-0 uppercase shadow-[0_10px_20px_-10px_rgba(67,97,238,0.44)] disabled:cursor-not-allowed disabled:opacity-70"
>
{loading ? 'Signing in…' : 'Sign in'}
</button> */}
<button
type="submit"
disabled={loading}
className="btn w-full border-0 uppercase shadow-[0_10px_20px_-10px_rgba(25,212,251,0.44)] disabled:cursor-not-allowed disabled:opacity-70 !mt-6 bg-[linear-gradient(135deg,#0EA5E9_0%,#19D4FB_50%,#67E8F9_100%)] text-white hover:bg-[linear-gradient(135deg,#67E8F9_0%,#19D4FB_50%,#0EA5E9_100%)]"
>
{loading ? 'Signing in…' : 'Sign in'}
</button>
</form>
);
};
export default ComponentsAuthLoginForm;

View File

@ -0,0 +1,57 @@
'use client';
import IconLockDots from '@/components/icon/icon-lock-dots';
import IconMail from '@/components/icon/icon-mail';
import IconUser from '@/components/icon/icon-user';
import { useRouter } from 'next/navigation';
import React from 'react';
const ComponentsAuthRegisterForm = () => {
const router = useRouter();
const submitForm = (e: any) => {
e.preventDefault();
router.push('/');
};
return (
<form className="space-y-5 dark:text-white" onSubmit={submitForm}>
<div>
<label htmlFor="Name">Name</label>
<div className="relative text-white-dark">
<input id="Name" type="text" placeholder="Enter Name" className="form-input ps-10 placeholder:text-white-dark" />
<span className="absolute start-4 top-1/2 -translate-y-1/2">
<IconUser fill={true} />
</span>
</div>
</div>
<div>
<label htmlFor="Email">Email</label>
<div className="relative text-white-dark">
<input id="Email" type="email" placeholder="Enter Email" className="form-input ps-10 placeholder:text-white-dark" />
<span className="absolute start-4 top-1/2 -translate-y-1/2">
<IconMail fill={true} />
</span>
</div>
</div>
<div>
<label htmlFor="Password">Password</label>
<div className="relative text-white-dark">
<input id="Password" type="password" placeholder="Enter Password" className="form-input ps-10 placeholder:text-white-dark" />
<span className="absolute start-4 top-1/2 -translate-y-1/2">
<IconLockDots fill={true} />
</span>
</div>
</div>
{/* <div>
<label className="flex cursor-pointer items-center">
<input type="checkbox" className="form-checkbox bg-white dark:bg-black" />
<span className="text-white-dark">Subscribe to weekly newsletter</span>
</label>
</div> */}
<button type="submit" className="btn btn-gradient !mt-6 w-full border-0 uppercase shadow-[0_10px_20px_-10px_rgba(67,97,238,0.44)]">
Sign Up
</button>
</form>
);
};
export default ComponentsAuthRegisterForm;

View File

@ -0,0 +1,208 @@
'use client';
import React, { useState } from 'react';
import { useRouter } from 'next/navigation';
import IconLockDots from '@/components/icon/icon-lock-dots';
import IconMail from '@/components/icon/icon-mail';
import IconUser from '@/components/icon/icon-user';
import { Eye, EyeOff } from "lucide-react";
const API_BASE =
process.env.NEXT_PUBLIC_API_BASE_URL?.replace(/\/$/, '') ||
'https://ebay.backend.data4autos.com';
// 'https://ebay.backend.data4autos.com';
const ComponentsAuthRegisterForm = () => {
const router = useRouter();
const [name, setName] = useState('');
const [phonenumber, setPhonenumber] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [subscribe, setSubscribe] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const [loading, setLoading] = useState(false);
const [err, setErr] = useState<string | null>(null);
const [msg, setMsg] = useState<string | null>(null);
const submitForm = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setErr(null);
setMsg(null);
if (!name || !email || !password || !phonenumber) {
setErr('Please fill in all fields.');
return;
}
setLoading(true);
try {
const res = await fetch(`${API_BASE}/api/motorstate/auth/signup`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
// If your API sets cookies, uncomment:
// credentials: 'include',
body: JSON.stringify({
name,
email,
password,
phonenumber,
// You can also send `subscribe` if your API supports it
// subscribe,
}),
});
const contentType = res.headers.get('content-type') || '';
let data: any = null;
if (contentType.includes('application/json')) {
data = await res.json();
} else {
data = await res.text();
}
if (!res.ok) {
const message =
(typeof data === 'object' && (data?.message || data?.error)) ||
`Signup failed (${res.status})`;
throw new Error(message);
}
setMsg(
(typeof data === 'object' && (data?.message || 'Signup successful!')) ||
'Signup successful!'
);
// Redirect to login (or wherever you like)
setTimeout(() => router.push('/login'), 1000);
} catch (e: any) {
setErr(e?.message || 'Something went wrong. Please try again.');
} finally {
setLoading(false);
}
};
return (
<form className="space-y-3 p-4 dark:text-white" onSubmit={submitForm}>
{/* Name */}
<div>
<label htmlFor="Name">Name</label>
<div className="relative text-white-dark">
<input
id="Name"
type="text"
placeholder="Enter Name"
className="form-input ps-10 placeholder:text-white-dark"
value={name}
onChange={(e) => setName(e.target.value)}
autoComplete="name"
/>
<span className="absolute start-4 top-1/2 -translate-y-1/2">
<IconUser fill={true} />
</span>
</div>
</div>
{/* Email */}
<div>
<label htmlFor="Email">Email</label>
<div className="relative text-white-dark">
<input
id="Email"
type="email"
placeholder="Enter Email"
className="form-input ps-10 placeholder:text-white-dark"
value={email}
onChange={(e) => setEmail(e.target.value)}
autoComplete="email"
/>
<span className="absolute start-4 top-1/2 -translate-y-1/2">
<IconMail fill={true} />
</span>
</div>
</div>
{/* Password */}
<div>
<label htmlFor="Password">Password</label>
<div className="relative text-white-dark">
<input
id="Password"
type={showPassword ? "text" : "password"}
placeholder="Enter Password"
className="form-input ps-10 placeholder:text-white-dark"
value={password}
onChange={(e) => setPassword(e.target.value)}
autoComplete="new-password"
/>
<span className="absolute start-4 top-1/2 -translate-y-1/2">
<IconLockDots fill={true} />
</span>
{/* 👁️ Eye toggle (right) */}
<button
type="button"
className="absolute end-3 top-1/2 -translate-y-1/2 text-gray-400"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
</button>
</div>
</div>
{/* Phone Number */}
<div>
<label htmlFor="PhoneNumber">Phone Number</label>
<div className="relative text-white-dark">
<input
id="PhoneNumber"
type="tel"
placeholder="Enter Phone Number"
className="form-input ps-10 placeholder:text-white-dark"
value={phonenumber}
onChange={(e) => setPhonenumber(e.target.value)}
autoComplete="tel"
/>
{/* Simple emoji icon to match the padded layout without adding a new component */}
<span className="absolute start-4 top-1/2 -translate-y-1/2">📞</span>
</div>
</div>
{/* Subscribe */}
{/* <div>
<label className="flex cursor-pointer items-center">
<input
type="checkbox"
className="form-checkbox bg-white dark:bg-black"
checked={subscribe}
onChange={(e) => setSubscribe(e.target.checked)}
/>
<span className="text-white-dark">Subscribe to weekly newsletter</span>
</label>
</div> */}
{/* Alerts */}
{err && (
<div className="rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700">
{err}
</div>
)}
{msg && (
<div className="rounded-md border border-green-200 bg-green-50 px-3 py-2 text-sm text-green-700">
{msg}
</div>
)}
{/* Submit */}
<button
type="submit"
disabled={loading}
className="btn w-full border-0 uppercase shadow-[0_10px_20px_-10px_rgba(25,212,251,0.44)] disabled:cursor-not-allowed disabled:opacity-70 !mt-6 bg-[linear-gradient(135deg,#0EA5E9_0%,#19D4FB_50%,#67E8F9_100%)] text-white hover:bg-[linear-gradient(135deg,#67E8F9_0%,#19D4FB_50%,#0EA5E9_100%)]"
>
{loading ? 'Creating account…' : 'Sign Up'}
</button>
</form>
);
};
export default ComponentsAuthRegisterForm;

View File

@ -0,0 +1,68 @@
"use client";
import React, { useState } from "react";
import axios from "axios";
import { useSearchParams } from "next/navigation";
export default function ResetPasswordForm() {
const searchParams = useSearchParams();
// ✅ token and email are read from the URL: /reset-password?email=...&token=...
// const email = searchParams.get("email") || "";
const token = searchParams.get("token") || "";
const [newPassword, setNewPassword] = useState("");
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState("");
const handleReset = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setMessage("");
try {
await axios.post("https://ebay.backend.data4autos.com/api/auth/reset-password", {
token, // ✅ use token from URL
newPassword,
});
setMessage("✅ Your password has been successfully reset.");
} catch (err) {
console.error(err);
setMessage("❌ Reset failed. Check the link or try again.");
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleReset} className="space-y-5">
<div>
<label className="block text-sm font-medium text-white mb-1">
New Password
</label>
<input
type="password"
required
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
className="form-input ps-10 placeholder:text-white-dark"
placeholder="********"
/>
</div>
<button
type="submit"
disabled={loading}
className="btn w-full border-0 uppercase shadow-[0_10px_20px_-10px_rgba(25,212,251,0.44)] disabled:cursor-not-allowed disabled:opacity-70 !mt-6 bg-[linear-gradient(135deg,#0EA5E9_0%,#19D4FB_50%,#67E8F9_100%)] text-white hover:bg-[linear-gradient(135deg,#67E8F9_0%,#19D4FB_50%,#0EA5E9_100%)]"
>
{loading ? "Resetting..." : "Reset Password"}
</button>
{message && (
<p className="mt-2 text-center text-sm font-semibold text-primary">
{message}
</p>
)}
</form>
);
}

View File

@ -0,0 +1,169 @@
"use client";
import React, { useState, useEffect } from "react";
import IconLockDots from "@/components/icon/icon-lock-dots";
import { useRouter } from "next/navigation";
import axios from "axios";
interface ChangePasswordResponse {
message?: string;
error?: string;
}
const ComponentsAuthUnlockForm = () => {
const router = useRouter();
const [currentPassword, setCurrentPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const [success, setSuccess] = useState("");
const [userEmail, setUserEmail] = useState<string | null>(null);
const [redirecting, setRedirecting] = useState(false);
// ✅ Load user email & verify auth
useEffect(() => {
// 🧠 Prevent running this effect after password update redirect starts
if (redirecting) return;
const token = localStorage.getItem("token");
const email = localStorage.getItem("d4a_email");
if (!token) {
setError("You are not logged in. Redirecting to login...");
setTimeout(() => router.push("/login"), 1500);
} else {
setUserEmail(email);
}
}, [router, redirecting]);
const submitForm = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
setSuccess("");
const token = localStorage.getItem("token");
const email = localStorage.getItem("d4a_email");
if (!token || !email) {
setError("Session expired or missing. Please log in again.");
return;
}
if (!currentPassword || !newPassword) {
setError("Please fill all fields.");
return;
}
setLoading(true);
try {
const res = await axios.post<ChangePasswordResponse>(
`https://ebay.backend.data4autos.com/api/auth/change-password`,
{ email, currentPassword, newPassword },
{
headers: { Authorization: `Bearer ${token}` },
}
);
setSuccess(res.data.message || "Password updated successfully!");
console.log("Password updated successfully, redirecting to login in 2 seconds...");
setRedirecting(true); // ✅ prevents useEffect from running again
setTimeout(() => {
localStorage.removeItem("token");
router.push("/login");
}, 1200);
} catch (err: any) {
console.error("Change Password Error:", err);
const msg =
err.response?.data?.error ||
err.response?.data?.message ||
err.message ||
"Something went wrong. Please try again.";
if (msg.toLowerCase().includes("expired")) {
setError("Session expired. Please log in again.");
setTimeout(() => router.push("/login"), 1500);
} else {
setError(msg);
}
} finally {
setLoading(false);
}
};
return (
<form className="space-y-5" onSubmit={submitForm}>
{/* 🧩 User Info Header */}
<div className="mb-10 flex items-center">
<div className="flex h-16 w-16 items-end justify-center overflow-hidden rounded-full bg-[#00AB55] ltr:mr-4 rtl:ml-4">
<img
src="/assets/images/auth/user.jpg"
className="w-full object-cover"
alt="user"
/>
</div>
<div className="flex-1">
<h4 className="text-2xl dark:text-white text-[#19d4fb]">{userEmail || "User"}</h4>
{/* <p className="text-white">
Enter your password to change your credentials
</p> */}
</div>
</div>
{/* 🔒 Current Password */}
<div>
<label htmlFor="currentPassword" className="dark:text-white">
Current Password
</label>
<div className="relative text-white-dark">
<input
id="currentPassword"
type="password"
placeholder="Enter Current Password"
className="form-input ps-10 placeholder:text-white-dark"
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
required
/>
<span className="absolute start-4 top-1/2 -translate-y-1/2">
<IconLockDots fill={true} />
</span>
</div>
</div>
{/* 🔑 New Password */}
<div>
<label htmlFor="newPassword" className="dark:text-white">
New Password
</label>
<div className="relative text-white-dark">
<input
id="newPassword"
type="password"
placeholder="Enter New Password"
className="form-input ps-10 placeholder:text-white-dark"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
required
/>
<span className="absolute start-4 top-1/2 -translate-y-1/2">
<IconLockDots fill={true} />
</span>
</div>
</div>
{error && <p className="text-red-500 text-sm">{error}</p>}
{success && <p className="text-green-500 text-sm">{success}</p>}
<button
type="submit"
disabled={loading}
className="btn w-full border-0 uppercase shadow-[0_10px_20px_-10px_rgba(25,212,251,0.44)] disabled:cursor-not-allowed disabled:opacity-70 !mt-6 bg-[linear-gradient(135deg,#0EA5E9_0%,#19D4FB_50%,#67E8F9_100%)] text-white hover:bg-[linear-gradient(135deg,#67E8F9_0%,#19D4FB_50%,#0EA5E9_100%)]"
>
{loading ? "UPDATING…" : "CHANGE PASSWORD"}
</button>
</form>
);
};
export default ComponentsAuthUnlockForm;

View File

@ -0,0 +1,94 @@
'use client';
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
type SubscriptionState = {
active: boolean; // true if paid subscription is active
trialEndsAt?: string | null; // ISO string when the trial ends
};
type SubscriptionContextValue = {
state: SubscriptionState;
isTrialActive: boolean;
daysLeftInTrial: number;
isEntitled: boolean; // active OR valid trial
startTrial: (days?: number) => void;
endTrial: () => void;
purchase: () => void; // placeholder “pay” success
cancel: () => void; // cancels paid sub (trial untouched)
reset: () => void; // dev helper
};
const STORAGE_KEYS = {
ACTIVE: 'SUB_ACTIVE',
TRIAL_END: 'SUB_TRIAL_ENDS_AT',
};
const SubscriptionContext = createContext<SubscriptionContextValue | null>(null);
export function SubscriptionProvider({ children }: { children: React.ReactNode }) {
const [active, setActive] = useState<boolean>(false);
const [trialEndsAt, setTrialEndsAt] = useState<string | null>(null);
// Load from localStorage
useEffect(() => {
try {
const a = localStorage.getItem(STORAGE_KEYS.ACTIVE);
const t = localStorage.getItem(STORAGE_KEYS.TRIAL_END);
setActive(a === '1');
setTrialEndsAt(t || null);
} catch {}
}, []);
// Persist changes
useEffect(() => {
try {
localStorage.setItem(STORAGE_KEYS.ACTIVE, active ? '1' : '0');
if (trialEndsAt) localStorage.setItem(STORAGE_KEYS.TRIAL_END, trialEndsAt);
else localStorage.removeItem(STORAGE_KEYS.TRIAL_END);
} catch {}
}, [active, trialEndsAt]);
const now = Date.now();
const trialMsLeft = useMemo(() => {
if (!trialEndsAt) return 0;
const end = new Date(trialEndsAt).getTime();
return Math.max(0, end - now);
}, [trialEndsAt, now]);
const daysLeftInTrial = Math.ceil(trialMsLeft / (1000 * 60 * 60 * 24));
const isTrialActive = trialMsLeft > 0;
const isEntitled = active || isTrialActive;
const startTrial = (days = 7) => {
const end = new Date(Date.now() + days * 24 * 60 * 60 * 1000).toISOString();
setTrialEndsAt(end);
};
const endTrial = () => setTrialEndsAt(null);
// Placeholder purchase: mark as active
const purchase = () => setActive(true);
// Cancel paid sub (keeps trial data as-is)
const cancel = () => setActive(false);
const reset = () => {
setActive(false);
setTrialEndsAt(null);
};
return (
<SubscriptionContext.Provider
value={{ state: { active, trialEndsAt }, isTrialActive, daysLeftInTrial, isEntitled, startTrial, endTrial, purchase, cancel, reset }}
>
{children}
</SubscriptionContext.Provider>
);
}
export function useSubscription() {
const ctx = useContext(SubscriptionContext);
if (!ctx) throw new Error('useSubscription must be used within SubscriptionProvider');
return ctx;
}

View File

@ -0,0 +1,58 @@
'use client';
import React from 'react';
import { useSubscription } from './subscription-context';
type Props = {
children: React.ReactNode;
title?: string; // optional feature/page title
allowTrialCta?: boolean; // show Start Trial button
};
const SubscriptionGate: React.FC<Props> = ({ children, title = 'This feature', allowTrialCta = true }) => {
const { isEntitled, isTrialActive, daysLeftInTrial, startTrial, purchase } = useSubscription();
if (isEntitled) {
return <>{children}</>;
}
return (
<div className="max-w-3xl mx-auto py-16 px-6">
<div className="bg-white rounded-2xl shadow-lg p-8 border">
<h2 className="text-2xl font-bold mb-2">{title} requires a subscription</h2>
<p className="text-gray-600 mb-6">
Unlock access to all tools. New users get a <span className="font-semibold">7-day free trial</span>.
</p>
{isTrialActive && (
<div className="mb-6 text-sm text-emerald-700 bg-emerald-50 border border-emerald-200 rounded-lg px-4 py-3">
Your trial is active <span className="font-semibold">{daysLeftInTrial}</span> day(s) left.
</div>
)}
<div className="flex flex-wrap gap-3">
{allowTrialCta && !isTrialActive && (
<button
onClick={() => startTrial(7)}
className="px-5 py-2.5 rounded-lg bg-slate-900 text-white hover:bg-black transition"
>
Start 7-day Free Trial
</button>
)}
<button
onClick={purchase}
className="px-5 py-2.5 rounded-lg bg-gradient-to-r from-blue-600 to-purple-600 text-white hover:from-blue-700 hover:to-purple-700 transition"
>
Purchase Subscription
</button>
</div>
<p className="text-xs text-gray-500 mt-4">
You can manage your plan anytime in <span className="font-medium">Account Settings Subscription</span>.
</p>
</div>
</div>
);
};
export default SubscriptionGate;

View File

@ -0,0 +1,92 @@
'use client';
import React from 'react';
import { useSubscription } from './subscription-context';
const SubscriptionPanel: React.FC = () => {
const { state, isTrialActive, daysLeftInTrial, isEntitled, startTrial, endTrial, purchase, cancel, reset } = useSubscription();
return (
<div className="mt-6 bg-white rounded-xl border shadow-sm p-6">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold">Subscription</h3>
<p className="text-sm text-gray-600">Manage your Data4Autos subscription & free trial.</p>
</div>
<div className="text-right">
<div className="text-sm">
Status:{' '}
{state.active ? (
<span className="inline-block px-2 py-0.5 rounded bg-emerald-50 text-emerald-700 border border-emerald-200">
Active
</span>
) : (
<span className="inline-block px-2 py-0.5 rounded bg-gray-100 text-gray-700 border">
Inactive
</span>
)}
</div>
{isTrialActive && (
<div className="text-xs text-emerald-700">
Trial: {daysLeftInTrial} day(s) left
</div>
)}
</div>
</div>
<div className="mt-5 flex flex-wrap gap-3">
{!state.active && !isTrialActive && (
<button
onClick={() => startTrial(7)}
className="px-4 py-2 rounded-lg bg-slate-900 text-white hover:bg-black transition"
>
Start 7-day Free Trial
</button>
)}
{isTrialActive && (
<button
onClick={endTrial}
className="px-4 py-2 rounded-lg border hover:bg-gray-50 transition"
>
End Trial
</button>
)}
{!state.active && (
<button
onClick={purchase}
className="px-4 py-2 rounded-lg bg-gradient-to-r from-blue-600 to-purple-600 text-white hover:from-blue-700 hover:to-purple-700 transition"
>
Purchase Subscription
</button>
)}
{state.active && (
<button
onClick={cancel}
className="px-4 py-2 rounded-lg border hover:bg-gray-50 transition"
>
Cancel Subscription
</button>
)}
{/* Dev helper (remove in production) */}
<button
onClick={reset}
className="px-4 py-2 rounded-lg border border-red-300 text-red-700 hover:bg-red-50 transition"
>
Reset (Dev)
</button>
</div>
{!isEntitled && (
<p className="text-xs text-gray-500 mt-3">
Subscription is required to access premium pages. You can still start a 7-day free trial.
</p>
)}
</div>
);
};
export default SubscriptionPanel;

View File

@ -0,0 +1,237 @@
'use client';
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import Link from 'next/link';
import IconDownload from '@/components/icon/icon-download';
import IconEdit from '@/components/icon/icon-edit';
import IconPlus from '@/components/icon/icon-plus';
import IconPrinter from '@/components/icon/icon-printer';
import IconSend from '@/components/icon/icon-send';
import { formatCreatedAtWithEnd } from '@/utils/commonFunction.utils';
import { useRouter } from 'next/navigation';
const ComponentsAppsInvoicePreview = () => {
const [payment, setPayment] = useState<any>(null);
const router = useRouter();
// 🧩 Print invoice
const exportTable = () => {
window.print();
};
// 🧩 Fetch payment data
useEffect(() => {
const sessionId = localStorage.getItem('payment_session');
if (!sessionId) {
router.push('/pricing');
return;
}
const fetchPayment = async () => {
try {
const res: any = await axios.get(
'https://ebay.backend.data4autos.com/api/payment/details',
{ params: { session_id: sessionId } }
);
setPayment(res.data.payment);
} catch (error) {
console.error('Error fetching payment details:', error);
}
};
fetchPayment();
}, [router]);
// 🧩 Invoice line items (example if your backend doesn't send itemized data)
const items = [
{
id: 1,
title: payment?.plan || 'Subscription Plan',
quantity: 1,
price: payment?.amount || '0',
amount: payment?.amount || '0',
},
];
const columns = [
{ key: 'id', label: 'S.NO' },
{ key: 'title', label: 'PLAN' },
{ key: 'quantity', label: 'QTY' },
{ key: 'price', label: 'PRICE', class: 'ltr:text-right rtl:text-left' },
{ key: 'amount', label: 'AMOUNT', class: 'ltr:text-right rtl:text-left' },
];
return (
<div className="p-6">
{/* ========= INVOICE CARD ========= */}
<div className="panel bg-white shadow-md rounded-xl p-6">
{/* Header */}
<div className="flex flex-wrap justify-between gap-4 px-4">
<div className="text-2xl font-semibold uppercase">Invoice</div>
<div className="shrink-0">
<img src="/assets/images/logo_dark.png" alt="logo" className="w-64" />
</div>
</div>
<div className="px-4 text-right text-gray-500">
<div className="mt-6 space-y-1">
<div>Data4Autos</div>
<div>sales@data4autos.com</div>
<div>
+1-647-679-7651</div>
</div>
</div>
<hr className="my-6 border-gray-200" />
{/* ========= INVOICE DETAILS ========= */}
<div className="flex flex-col justify-between gap-6 lg:flex-row">
<div className="flex-1">
<div className="space-y-1 text-gray-500">
<div>Issue For:</div>
<div className="font-semibold text-black">{payment?.email || '—'}</div>
<div>Customer ID: {payment?.userid || '—'}</div>
<div>Plan: {payment?.plan || '—'}</div>
</div>
</div>
<div className="flex flex-col justify-between gap-6 sm:flex-row lg:w-2/3">
<div className="sm:w-1/2 lg:w-2/5">
<div className="mb-2 flex justify-between">
<span className="text-gray-500">Invoice :</span>
<span>#{payment?.id || '—'}</span>
</div>
<div className="mb-2 flex justify-between">
<span className="text-gray-500">Issue Date :</span>
<span>
{payment?.createdAt
? new Date(payment.createdAt).toLocaleDateString('en-GB', {
day: '2-digit',
month: 'short',
year: 'numeric',
})
: '—'}
</span>
</div>
<div className="mb-2 flex justify-between">
<span className="text-gray-500">Valid Till :</span>
<span>{formatCreatedAtWithEnd(payment?.createdAt)?.split(' - ')[1]}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">Status :</span>
<span
className={`font-medium ${payment?.status === 'succeeded' ? 'text-green-600' : 'text-yellow-700'
}`}
>
{payment?.status === 'succeeded' ? 'Paid' : payment?.status || '—'}
</span>
</div>
</div>
<div className="sm:w-1/2 lg:w-2/5">
<div className="mb-2 flex justify-between">
<span className="text-gray-500">Bank Name:</span>
<span>Bank of Canada</span>
</div>
<div className="mb-2 flex justify-between">
<span className="text-gray-500">Account No:</span>
<span>1234567890</span>
</div>
{/* <div className="mb-2 flex justify-between">
<span className="text-gray-500">SWIFT Code:</span>
<span>S58K796</span>
</div> */}
<div className="mb-2 flex justify-between">
<span className="text-gray-500">Country:</span>
<span>Canada</span>
</div>
</div>
</div>
</div>
{/* ========= ITEMS TABLE ========= */}
<div className="table-responsive mt-6">
<table className="table-striped w-full border-collapse">
<thead>
<tr className="bg-gray-100 text-gray-700">
{columns.map((column) => (
<th key={column.key} className={`p-3 text-left ${column.class || ''}`}>
{column.label}
</th>
))}
</tr>
</thead>
<tbody>
{items.map((item) => (
<tr key={item.id} className="border-t">
<td className="p-3">{item.id}</td>
<td className="p-3">{item.title}</td>
<td className="p-3">{item.quantity}</td>
<td className="p-3 text-right">${item.price}</td>
<td className="p-3 text-right">${item.amount}</td>
</tr>
))}
</tbody>
</table>
</div>
{/* ========= TOTAL ========= */}
<div className="mt-6 grid grid-cols-1 sm:grid-cols-2 px-4">
<div></div>
<div className="space-y-2 text-right">
<div className="flex justify-between">
<span>Subtotal</span>
<span>${payment?.amount || '0'}</span>
</div>
<div className="flex justify-between">
<span>Tax</span>
<span>$0</span>
</div>
<div className="flex justify-between font-semibold text-lg">
<span>Grand Total</span>
<span>${payment?.amount || '0'}</span>
</div>
</div>
</div>
</div>
{/* ========= ACTION BUTTONS ========= */}
<div className="mt-6 flex flex-wrap items-center justify-center gap-4 lg:justify-end">
{/* <button type="button" className="btn btn-info gap-2">
<IconSend />
Send Invoice
</button> */}
<button
type="button"
className="btn btn-primary bg-[#00d1ff] border-[#00d1ff] gap-2 hover:bg-[#00b8e6] transition"
onClick={exportTable}
>
<IconPrinter />
Print
</button>
{/* <button type="button" className="btn btn-success gap-2">
<IconDownload />
Download
</button>
<Link href="/apps/invoice/add" className="btn btn-secondary gap-2">
<IconPlus />
Create
</Link>
<Link href="/apps/invoice/edit" className="btn btn-warning gap-2">
<IconEdit />
Edit
</Link> */}
</div>
</div >
);
};
export default ComponentsAppsInvoicePreview;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,57 @@
'use client';
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
import { usePopper } from 'react-popper';
const Dropdown = (props: any, forwardedRef: any) => {
const [visibility, setVisibility] = useState<any>(false);
const referenceRef = useRef<any>();
const popperRef = useRef<any>();
const { styles, attributes } = usePopper(referenceRef.current, popperRef.current, {
placement: props.placement || 'bottom-end',
modifiers: [
{
name: 'offset',
options: {
offset: props.offset || [0],
},
},
],
});
const handleDocumentClick = (event: any) => {
if (referenceRef.current.contains(event.target) || popperRef.current.contains(event.target)) {
return;
}
setVisibility(false);
};
useEffect(() => {
document.addEventListener('mousedown', handleDocumentClick);
return () => {
document.removeEventListener('mousedown', handleDocumentClick);
};
}, []);
useImperativeHandle(forwardedRef, () => ({
close() {
setVisibility(false);
},
}));
return (
<>
<button ref={referenceRef} type="button" className={props.btnClassName} onClick={() => setVisibility(!visibility)}>
{props.button}
</button>
<div ref={popperRef} style={styles.popper} {...attributes.popper} className="z-50" onClick={() => setVisibility(!visibility)}>
{visibility && props.children}
</div>
</>
);
};
export default forwardRef(Dropdown);

57
components/dropdown.tsx Normal file
View File

@ -0,0 +1,57 @@
'use client';
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
import { usePopper } from 'react-popper';
const Dropdown = (props: any, forwardedRef: any) => {
const [visibility, setVisibility] = useState<any>(false);
const referenceRef = useRef<any>();
const popperRef = useRef<any>();
const { styles, attributes } = usePopper(referenceRef.current, popperRef.current, {
placement: props.placement || 'bottom-end',
modifiers: [
{
name: 'offset',
options: {
offset: props.offset || [0],
},
},
],
});
const handleDocumentClick = (event: any) => {
if (referenceRef.current.contains(event.target) || popperRef.current.contains(event.target)) {
return;
}
setVisibility(false);
};
useEffect(() => {
document.addEventListener('mousedown', handleDocumentClick);
return () => {
document.removeEventListener('mousedown', handleDocumentClick);
};
}, []);
useImperativeHandle(forwardedRef, () => ({
close() {
setVisibility(false);
},
}));
return (
<>
<button ref={referenceRef} type="button" className={props.btnClassName} onClick={() => setVisibility(!visibility)}>
{props.button}
</button>
<div ref={popperRef} style={styles.popper} {...attributes.popper} className="z-50" onClick={() => setVisibility(!visibility)}>
{visibility && props.children}
</div>
</>
);
};
export default forwardRef(Dropdown);

21
components/highlight.tsx Normal file
View File

@ -0,0 +1,21 @@
import 'highlight.js/styles/monokai-sublime.css';
import hightlight from 'highlight.js';
import { PropsWithChildren, useEffect, useRef } from 'react';
const CodeHighlight = ({ children }: PropsWithChildren) => {
const highlightElement = useRef<any>(null);
useEffect(() => {
if (highlightElement?.current) {
hightlight.highlightElement(highlightElement.current.querySelector('pre'));
}
}, []);
return (
<div ref={highlightElement} className="highlight-el">
{children}
</div>
);
};
export default CodeHighlight;

47
components/home/home.tsx Normal file
View File

@ -0,0 +1,47 @@
// 'use client';
// import React, { useEffect } from 'react';
// const Home = () => {
// useEffect(() => {
// console.log('Home component mounted on client');
// }, []);
// return <div>Welcome To Data4Autos Turn14 and eBay Integration</div>;
// };
// export default Home;
'use client';
import React, { useState, useEffect } from 'react';
import { useCookies } from 'react-cookie'; // If using 'react-cookie'
import { useRouter } from 'next/navigation';
const Home = () => {
const [cookies] = useCookies(['d4a_uid']); // Use correct cookie name
const [user, setUser] = useState(null);
const router = useRouter();
useEffect(() => {
const uid = cookies['d4a_uid'] || null; // Read cookie
if (!uid) {
router.push('/login'); // Redirect if not authenticated
} else {
setUser(uid); // Set user if authenticated
}
}, [cookies, router]);
if (!user) {
// Optional: Prevent flash of protected content
return <div>Checking authentication, please wait...</div>;
}
return (
<div>
Welcome To Data4Auto: Turn14 and eBay Integration
</div>
);
};
export default Home;

View File

@ -0,0 +1,43 @@
import { FC } from 'react';
interface IconAirplayProps {
className?: string;
fill?: boolean;
}
const IconAirplay: FC<IconAirplayProps> = ({ className, fill = false }) => {
return (
<>
{!fill ? (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<path
opacity="0.5"
d="M7.142 18.9706C5.18539 18.8995 3.99998 18.6568 3.17157 17.8284C2 16.6569 2 14.7712 2 11C2 7.22876 2 5.34315 3.17157 4.17157C4.34315 3 6.22876 3 10 3H14C17.7712 3 19.6569 3 20.8284 4.17157C22 5.34315 22 7.22876 22 11C22 14.7712 22 16.6569 20.8284 17.8284C20.0203 18.6366 18.8723 18.8873 17 18.965"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
/>
<path
d="M9.94955 16.0503C10.8806 15.1192 11.3461 14.6537 11.9209 14.6234C11.9735 14.6206 12.0261 14.6206 12.0787 14.6234C12.6535 14.6537 13.119 15.1192 14.0501 16.0503C16.0759 18.0761 17.0888 19.089 16.8053 19.963C16.7809 20.0381 16.7506 20.1112 16.7147 20.1815C16.2973 21 14.8648 21 11.9998 21C9.13482 21 7.70233 21 7.28489 20.1815C7.249 20.1112 7.21873 20.0381 7.19436 19.963C6.91078 19.089 7.92371 18.0761 9.94955 16.0503Z"
stroke="currentColor"
strokeWidth="1.5"
/>
</svg>
) : (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<path
opacity="0.5"
d="M14 3H10C6.22876 3 4.34315 3 3.17157 4.17157C2 5.34315 2 7.22876 2 11C2 14.7712 2 16.6569 3.17157 17.8284C4.34315 19 6.22876 19 10 19H14C17.7712 19 19.6569 19 20.8284 17.8284C22 16.6569 22 14.7712 22 11C22 7.22876 22 5.34315 20.8284 4.17157C19.6569 3 17.7712 3 14 3Z"
fill="currentColor"
/>
<path
d="M9.94955 16.0503C10.8806 15.1192 11.3461 14.6537 11.9209 14.6234C11.9735 14.6206 12.0261 14.6206 12.0787 14.6234C12.6535 14.6537 13.119 15.1192 14.0501 16.0503C16.0759 18.0761 17.0888 19.089 16.8053 19.963C16.7809 20.0381 16.7506 20.1112 16.7147 20.1815C16.2973 21 14.8648 21 11.9998 21C9.13482 21 7.70233 21 7.28489 20.1815C7.249 20.1112 7.21873 20.0381 7.19436 19.963C6.91078 19.089 7.92371 18.0761 9.94955 16.0503Z"
fill="currentColor"
/>
</svg>
)}
</>
);
};
export default IconAirplay;

View File

@ -0,0 +1,31 @@
import { FC } from 'react';
interface IconArchiveProps {
className?: string;
}
const IconArchive: FC<IconArchiveProps> = ({ className }) => {
return (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<path
d="M9 12C9 11.5341 9 11.3011 9.07612 11.1173C9.17761 10.8723 9.37229 10.6776 9.61732 10.5761C9.80109 10.5 10.0341 10.5 10.5 10.5H13.5C13.9659 10.5 14.1989 10.5 14.3827 10.5761C14.6277 10.6776 14.8224 10.8723 14.9239 11.1173C15 11.3011 15 11.5341 15 12C15 12.4659 15 12.6989 14.9239 12.8827C14.8224 13.1277 14.6277 13.3224 14.3827 13.4239C14.1989 13.5 13.9659 13.5 13.5 13.5H10.5C10.0341 13.5 9.80109 13.5 9.61732 13.4239C9.37229 13.3224 9.17761 13.1277 9.07612 12.8827C9 12.6989 9 12.4659 9 12Z"
stroke="currentColor"
strokeWidth="1.5"
/>
<path
opacity="0.5"
d="M20.5 7V13C20.5 16.7712 20.5 18.6569 19.3284 19.8284C18.1569 21 16.2712 21 12.5 21H11.5C7.72876 21 5.84315 21 4.67157 19.8284C3.5 18.6569 3.5 16.7712 3.5 13V7"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
/>
<path
d="M2 5C2 4.05719 2 3.58579 2.29289 3.29289C2.58579 3 3.05719 3 4 3H20C20.9428 3 21.4142 3 21.7071 3.29289C22 3.58579 22 4.05719 22 5C22 5.94281 22 6.41421 21.7071 6.70711C21.4142 7 20.9428 7 20 7H4C3.05719 7 2.58579 7 2.29289 6.70711C2 6.41421 2 5.94281 2 5Z"
stroke="currentColor"
strokeWidth="1.5"
/>
</svg>
);
};
export default IconArchive;

View File

@ -0,0 +1,16 @@
import { FC } from 'react';
interface IconArrowBackwardProps {
className?: string;
}
const IconArrowBackward: FC<IconArrowBackwardProps> = ({ className }) => {
return (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<path d="M9.5 7L4.5 12L9.5 17" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path opacity="0.5" d="M4.5 12L14.5 12C16.1667 12 19.5 13 19.5 17" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
</svg>
);
};
export default IconArrowBackward;

View File

@ -0,0 +1,16 @@
import { FC } from 'react';
interface IconArrowForwardProps {
className?: string;
}
const IconArrowForward: FC<IconArrowForwardProps> = ({ className }) => {
return (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<path d="M14.5 7L19.5 12L14.5 17" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path opacity="0.5" d="M19.5 12L9.5 12C7.83333 12 4.5 13 4.5 17" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
</svg>
);
};
export default IconArrowForward;

View File

@ -0,0 +1,15 @@
import { FC } from 'react';
interface IconArrowLeftProps {
className?: string;
}
const IconArrowLeft: FC<IconArrowLeftProps> = ({ className }) => {
return (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<path d="M4 12H20M20 12L14 6M20 12L14 18" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
};
export default IconArrowLeft;

View File

@ -0,0 +1,18 @@
import { FC } from 'react';
interface IconArrowWaveLeftUpProps {
className?: string;
}
const IconArrowWaveLeftUp: FC<IconArrowWaveLeftUpProps> = ({ className }) => {
return (
<svg width="111" height="22" viewBox="0 0 116 22" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<path
d="M0.645796 11.44C0.215273 11.6829 0.0631375 12.2287 0.305991 12.6593C0.548845 13.0898 1.09472 13.2419 1.52525 12.9991L0.645796 11.44ZM49.0622 20.4639L48.9765 19.5731L48.9765 19.5731L49.0622 20.4639ZM115.315 2.33429L105.013 3.14964L110.87 11.6641L115.315 2.33429ZM1.52525 12.9991C10.3971 7.99452 17.8696 10.3011 25.3913 13.8345C29.1125 15.5825 32.9505 17.6894 36.8117 19.2153C40.7121 20.7566 44.7862 21.7747 49.148 21.3548L48.9765 19.5731C45.0058 19.9553 41.2324 19.0375 37.4695 17.5505C33.6675 16.0481 30.0265 14.0342 26.1524 12.2143C18.4834 8.61181 10.3 5.99417 0.645796 11.44L1.52525 12.9991ZM49.148 21.3548C52.4593 21.0362 54.7308 19.6545 56.4362 17.7498C58.1039 15.8872 59.2195 13.5306 60.2695 11.3266C61.3434 9.07217 62.3508 6.97234 63.8065 5.35233C65.2231 3.77575 67.0736 2.6484 69.8869 2.40495L69.7326 0.62162C66.4361 0.906877 64.1742 2.26491 62.475 4.15595C60.8148 6.00356 59.703 8.35359 58.6534 10.5568C57.5799 12.8105 56.5678 14.9194 55.1027 16.5557C53.6753 18.1499 51.809 19.3005 48.9765 19.5731L49.148 21.3548ZM69.8869 2.40495C72.2392 2.2014 75.0889 2.61953 78.2858 3.35001C81.4816 4.08027 84.9116 5.09374 88.4614 6.04603C91.9873 6.99189 95.6026 7.86868 99.0694 8.28693C102.533 8.70483 105.908 8.67299 108.936 7.75734L108.418 6.04396C105.72 6.85988 102.621 6.91239 99.2838 6.50981C95.9496 6.10757 92.4363 5.25904 88.9252 4.31715C85.4382 3.38169 81.9229 2.34497 78.6845 1.60499C75.4471 0.865243 72.3735 0.393097 69.7326 0.62162L69.8869 2.40495Z"
fill="currentColor"
/>
</svg>
);
};
export default IconArrowWaveLeftUp;

View File

@ -0,0 +1,21 @@
import { FC } from 'react';
interface IconAtProps {
className?: string;
}
const IconAt: FC<IconAtProps> = ({ className }) => {
return (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<path
d="M12 18C8.68629 18 6 15.3137 6 12C6 8.68629 8.68629 6 12 6C15.3137 6 18 8.68629 18 12C18 12.7215 17.8726 13.4133 17.6392 14.054C17.5551 14.285 17.4075 14.4861 17.2268 14.6527L17.1463 14.727C16.591 15.2392 15.7573 15.3049 15.1288 14.8858C14.6735 14.5823 14.4 14.0713 14.4 13.5241V12M14.4 12C14.4 13.3255 13.3255 14.4 12 14.4C10.6745 14.4 9.6 13.3255 9.6 12C9.6 10.6745 10.6745 9.6 12 9.6C13.3255 9.6 14.4 10.6745 14.4 12Z"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
/>
<path opacity="0.5" d="M2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12Z" stroke="currentColor" strokeWidth="1.5" />
</svg>
);
};
export default IconAt;

View File

@ -0,0 +1,28 @@
import { FC } from 'react';
interface IconAwardProps {
className?: string;
}
const IconAward: FC<IconAwardProps> = ({ className }) => {
return (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<path
opacity="0.5"
d="M11.1459 7.02251C11.5259 6.34084 11.7159 6 12 6C12.2841 6 12.4741 6.34084 12.8541 7.02251L12.9524 7.19887C13.0603 7.39258 13.1143 7.48944 13.1985 7.55334C13.2827 7.61725 13.3875 7.64097 13.5972 7.68841L13.7881 7.73161C14.526 7.89857 14.895 7.98205 14.9828 8.26432C15.0706 8.54659 14.819 8.84072 14.316 9.42898L14.1858 9.58117C14.0429 9.74833 13.9714 9.83191 13.9392 9.93531C13.9071 10.0387 13.9179 10.1502 13.9395 10.3733L13.9592 10.5763C14.0352 11.3612 14.0733 11.7536 13.8435 11.9281C13.6136 12.1025 13.2682 11.9435 12.5773 11.6254L12.3986 11.5431C12.2022 11.4527 12.1041 11.4075 12 11.4075C11.8959 11.4075 11.7978 11.4527 11.6014 11.5431L11.4227 11.6254C10.7318 11.9435 10.3864 12.1025 10.1565 11.9281C9.92674 11.7536 9.96476 11.3612 10.0408 10.5763L10.0605 10.3733C10.0821 10.1502 10.0929 10.0387 10.0608 9.93531C10.0286 9.83191 9.95713 9.74833 9.81418 9.58117L9.68403 9.42898C9.18097 8.84072 8.92945 8.54659 9.01723 8.26432C9.10501 7.98205 9.47396 7.89857 10.2119 7.73161L10.4028 7.68841C10.6125 7.64097 10.7173 7.61725 10.8015 7.55334C10.8857 7.48944 10.9397 7.39258 11.0476 7.19887L11.1459 7.02251Z"
stroke="currentColor"
strokeWidth="1.5"
/>
<path d="M19 9C19 12.866 15.866 16 12 16C8.13401 16 5 12.866 5 9C5 5.13401 8.13401 2 12 2C15.866 2 19 5.13401 19 9Z" stroke="currentColor" strokeWidth="1.5" />
<path
opacity="0.5"
d="M7.35111 15L6.71424 17.323C6.0859 19.6148 5.77173 20.7607 6.19097 21.3881C6.3379 21.6079 6.535 21.7844 6.76372 21.9008C7.41635 22.2331 8.42401 21.7081 10.4393 20.658C11.1099 20.3086 11.4452 20.1339 11.8014 20.0959C11.9335 20.0818 12.0665 20.0818 12.1986 20.0959C12.5548 20.1339 12.8901 20.3086 13.5607 20.658C15.576 21.7081 16.5837 22.2331 17.2363 21.9008C17.465 21.7844 17.6621 21.6079 17.809 21.3881C18.2283 20.7607 17.9141 19.6148 17.2858 17.323L16.6489 15"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
/>
</svg>
);
};
export default IconAward;

View File

@ -0,0 +1,18 @@
import { FC } from 'react';
interface IconBarChartProps {
className?: string;
}
const IconBarChart: FC<IconBarChartProps> = ({ className }) => {
return (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<path d="M22 22H2" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
<path opacity="0.5" d="M21 22V14.5C21 13.6716 20.3284 13 19.5 13H16.5C15.6716 13 15 13.6716 15 14.5V22" stroke="currentColor" strokeWidth="1.5" />
<path d="M15 22V5C15 3.58579 15 2.87868 14.5607 2.43934C14.1213 2 13.4142 2 12 2C10.5858 2 9.87868 2 9.43934 2.43934C9 2.87868 9 3.58579 9 5V22" stroke="currentColor" strokeWidth="1.5" />
<path opacity="0.5" d="M9 22V9.5C9 8.67157 8.32843 8 7.5 8H4.5C3.67157 8 3 8.67157 3 9.5V22" stroke="currentColor" strokeWidth="1.5" />
</svg>
);
};
export default IconBarChart;

View File

@ -0,0 +1,21 @@
import { FC } from 'react';
interface IconBellBingProps {
className?: string;
}
const IconBellBing: FC<IconBellBingProps> = ({ className }) => {
return (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<path
d="M19.0001 9.7041V9C19.0001 5.13401 15.8661 2 12.0001 2C8.13407 2 5.00006 5.13401 5.00006 9V9.7041C5.00006 10.5491 4.74995 11.3752 4.28123 12.0783L3.13263 13.8012C2.08349 15.3749 2.88442 17.5139 4.70913 18.0116C9.48258 19.3134 14.5175 19.3134 19.291 18.0116C21.1157 17.5139 21.9166 15.3749 20.8675 13.8012L19.7189 12.0783C19.2502 11.3752 19.0001 10.5491 19.0001 9.7041Z"
stroke="currentColor"
strokeWidth="1.5"
/>
<path d="M7.5 19C8.15503 20.7478 9.92246 22 12 22C14.0775 22 15.845 20.7478 16.5 19" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
<path d="M12 6V10" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
</svg>
);
};
export default IconBellBing;

View File

@ -0,0 +1,20 @@
import { FC } from 'react';
interface IconBellProps {
className?: string;
}
const IconBell: FC<IconBellProps> = ({ className }) => {
return (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<path
d="M19.0001 9.7041V9C19.0001 5.13401 15.8661 2 12.0001 2C8.13407 2 5.00006 5.13401 5.00006 9V9.7041C5.00006 10.5491 4.74995 11.3752 4.28123 12.0783L3.13263 13.8012C2.08349 15.3749 2.88442 17.5139 4.70913 18.0116C9.48258 19.3134 14.5175 19.3134 19.291 18.0116C21.1157 17.5139 21.9166 15.3749 20.8675 13.8012L19.7189 12.0783C19.2502 11.3752 19.0001 10.5491 19.0001 9.7041Z"
stroke="currentColor"
strokeWidth="1.5"
/>
<path opacity="0.5" d="M7.5 19C8.15503 20.7478 9.92246 22 12 22C14.0775 22 15.845 20.7478 16.5 19" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
</svg>
);
};
export default IconBell;

View File

@ -0,0 +1,22 @@
import { FC } from 'react';
interface IconBinanceProps {
className?: string;
}
const IconBinance: FC<IconBinanceProps> = ({ className }) => {
return (
<svg width="100%" height="100%" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" className={className}>
<g id="Icon">
<circle cx="512" cy="512" r="512" style={{fill: '#f3ba2f'}} />
<path className="st1 fill-white" d="M404.9 468 512 360.9l107.1 107.2 62.3-62.3L512 236.3 342.6 405.7z" />
<path transform="rotate(-45.001 298.629 511.998)" className="st1 fill-white" d="M254.6 467.9h88.1V556h-88.1z" />
<path className="st1 fill-white" d="M404.9 556 512 663.1l107.1-107.2 62.4 62.3h-.1L512 787.7 342.6 618.3l-.1-.1z" />
<path transform="rotate(-45.001 725.364 512.032)" className="st1 fill-white" d="M681.3 468h88.1v88.1h-88.1z" />
<path className="st1 fill-white" d="M575.2 512 512 448.7l-46.7 46.8-5.4 5.3-11.1 11.1-.1.1.1.1 63.2 63.2 63.2-63.3z" />
</g>
</svg>
);
};
export default IconBinance;

View File

@ -0,0 +1,42 @@
import { FC } from 'react';
interface IconBitcoinProps {
className?: string;
}
const IconBitcoin: FC<IconBitcoinProps> = ({ className }) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
xmlSpace="preserve"
width="100%"
height="100%"
version="1.1"
shapeRendering="geometricPrecision"
textRendering="geometricPrecision"
imageRendering="optimizeQuality"
fillRule="evenodd"
clipRule="evenodd"
viewBox="0 0 4091.27 4091.73"
className={className}
>
<g id="Layer_x0020_1">
<metadata id="CorelCorpID_0Corel-Layer" />
<g id="_1421344023328">
<path
fill="#F7931A"
fillRule="nonzero"
d="M4030.06 2540.77c-273.24,1096.01 -1383.32,1763.02 -2479.46,1489.71 -1095.68,-273.24 -1762.69,-1383.39 -1489.33,-2479.31 273.12,-1096.13 1383.2,-1763.19 2479,-1489.95 1096.06,273.24 1763.03,1383.51 1489.76,2479.57l0.02 -0.02z"
/>
<path
fill="white"
fillRule="nonzero"
d="M2947.77 1754.38c40.72,-272.26 -166.56,-418.61 -450,-516.24l91.95 -368.8 -224.5 -55.94 -89.51 359.09c-59.02,-14.72 -119.63,-28.59 -179.87,-42.34l90.16 -361.46 -224.36 -55.94 -92 368.68c-48.84,-11.12 -96.81,-22.11 -143.35,-33.69l0.26 -1.16 -309.59 -77.31 -59.72 239.78c0,0 166.56,38.18 163.05,40.53 90.91,22.69 107.35,82.87 104.62,130.57l-104.74 420.15c6.26,1.59 14.38,3.89 23.34,7.49 -7.49,-1.86 -15.46,-3.89 -23.73,-5.87l-146.81 588.57c-11.11,27.62 -39.31,69.07 -102.87,53.33 2.25,3.26 -163.17,-40.72 -163.17,-40.72l-111.46 256.98 292.15 72.83c54.35,13.63 107.61,27.89 160.06,41.3l-92.9 373.03 224.24 55.94 92 -369.07c61.26,16.63 120.71,31.97 178.91,46.43l-91.69 367.33 224.51 55.94 92.89 -372.33c382.82,72.45 670.67,43.24 791.83,-303.02 97.63,-278.78 -4.86,-439.58 -206.26,-544.44 146.69,-33.83 257.18,-130.31 286.64,-329.61l-0.07 -0.05zm-512.93 719.26c-69.38,278.78 -538.76,128.08 -690.94,90.29l123.28 -494.2c152.17,37.99 640.17,113.17 567.67,403.91zm69.43 -723.3c-63.29,253.58 -453.96,124.75 -580.69,93.16l111.77 -448.21c126.73,31.59 534.85,90.55 468.94,355.05l-0.02 0z"
/>
</g>
</g>
</svg>
);
};
export default IconBitcoin;

View File

@ -0,0 +1,25 @@
import { FC } from 'react';
interface IconBoltProps {
className?: string;
}
const IconBolt: FC<IconBoltProps> = ({ className }) => {
return (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<path
d="M13.926 9.70541C13.5474 9.33386 13.5474 8.74151 13.5474 7.55682V7.24712C13.5474 3.96249 13.5474 2.32018 12.6241 2.03721C11.7007 1.75425 10.711 3.09327 8.73167 5.77133L5.66953 9.91436C4.3848 11.6526 3.74244 12.5217 4.09639 13.205C4.10225 13.2164 4.10829 13.2276 4.1145 13.2387C4.48945 13.9117 5.59888 13.9117 7.81775 13.9117C9.05079 13.9117 9.6673 13.9117 10.054 14.2754"
stroke="currentColor"
strokeWidth="1.5"
/>
<path
opacity="0.5"
d="M13.9259 9.70557L13.9459 9.72481C14.3326 10.0885 14.9492 10.0885 16.1822 10.0885C18.4011 10.0885 19.5105 10.0885 19.8854 10.7615C19.8917 10.7726 19.8977 10.7838 19.9036 10.7951C20.2575 11.4785 19.6151 12.3476 18.3304 14.0858L15.2682 18.2288C13.2888 20.9069 12.2991 22.2459 11.3758 21.9629C10.4524 21.68 10.4524 20.0376 10.4525 16.753L10.4525 16.4434C10.4525 15.2587 10.4525 14.6663 10.074 14.2948L10.054 14.2755"
stroke="currentColor"
strokeWidth="1.5"
/>
</svg>
);
};
export default IconBolt;

View File

@ -0,0 +1,24 @@
import { FC } from 'react';
interface IconBookProps {
className?: string;
}
const IconBook: FC<IconBookProps> = ({ className }) => {
return (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<path
d="M4 8C4 5.17157 4 3.75736 4.87868 2.87868C5.75736 2 7.17157 2 10 2H14C16.8284 2 18.2426 2 19.1213 2.87868C20 3.75736 20 5.17157 20 8V16C20 18.8284 20 20.2426 19.1213 21.1213C18.2426 22 16.8284 22 14 22H10C7.17157 22 5.75736 22 4.87868 21.1213C4 20.2426 4 18.8284 4 16V8Z"
stroke="currentColor"
strokeWidth="1.5"
/>
<path
opacity="0.5"
d="M6.12132 16.1022L5.92721 15.3778L6.12132 16.1022ZM3.27556 18.0294C3.16835 18.4295 3.40579 18.8408 3.80589 18.948C4.20599 19.0552 4.61724 18.8178 4.72444 18.4177L3.27556 18.0294ZM6.25 16C6.25 16.4142 6.58579 16.75 7 16.75C7.41421 16.75 7.75 16.4142 7.75 16H6.25ZM7.75 2.5C7.75 2.08579 7.41421 1.75 7 1.75C6.58579 1.75 6.25 2.08579 6.25 2.5H7.75ZM7.89778 16.75H19.8978V15.25H7.89778V16.75ZM7.89778 15.25C7.01609 15.25 6.42812 15.2436 5.92721 15.3778L6.31543 16.8267C6.57752 16.7564 6.91952 16.75 7.89778 16.75V15.25ZM5.92721 15.3778C4.63311 15.7245 3.62231 16.7353 3.27556 18.0294L4.72444 18.4177C4.9325 17.6412 5.53898 17.0347 6.31543 16.8267L5.92721 15.3778ZM7.75 16V2.5H6.25V16H7.75Z"
fill="currentColor"
/>
</svg>
);
};
export default IconBook;

View File

@ -0,0 +1,21 @@
import { FC } from 'react';
interface IconBookmarkProps {
className?: string;
bookmark?: boolean;
}
const IconBookmark: FC<IconBookmarkProps> = ({ className, bookmark = true }) => {
return (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<path
d="M21 16.0909V11.0975C21 6.80891 21 4.6646 19.682 3.3323C18.364 2 16.2426 2 12 2C7.75736 2 5.63604 2 4.31802 3.3323C3 4.6646 3 6.80891 3 11.0975V16.0909C3 19.1875 3 20.7358 3.73411 21.4123C4.08421 21.735 4.52615 21.9377 4.99692 21.9915C5.98402 22.1045 7.13673 21.0849 9.44216 19.0458C10.4612 18.1445 10.9708 17.6938 11.5603 17.5751C11.8506 17.5166 12.1494 17.5166 12.4397 17.5751C13.0292 17.6938 13.5388 18.1445 14.5578 19.0458C16.8633 21.0849 18.016 22.1045 19.0031 21.9915C19.4739 21.9377 19.9158 21.735 20.2659 21.4123C21 20.7358 21 19.1875 21 16.0909Z"
stroke="currentColor"
strokeWidth="1.5"
/>
{bookmark && <path opacity="0.5" d="M15 6H9" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />}
</svg>
);
};
export default IconBookmark;

View File

@ -0,0 +1,42 @@
import { FC } from 'react';
interface IconBoxProps {
className?: string;
fill?: boolean;
}
const IconBox: FC<IconBoxProps> = ({ className, fill = false }) => {
return (
<>
{fill ? (
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<path
d="M11.2296 27.4907C13.5704 28.7191 14.7408 29.3333 15.9998 29.3333V16L3.51719 9.43018C3.49882 9.45971 3.48077 9.48953 3.46303 9.51965C2.6665 10.8723 2.6665 12.5555 2.6665 15.922V16.078C2.6665 19.4444 2.6665 21.1277 3.46303 22.4803C4.25956 23.833 5.69401 24.5858 8.5629 26.0913L11.2296 27.4907Z"
fill="currentColor"
/>
<path
opacity="0.7"
d="M23.4367 5.90853L20.77 4.50913C18.4292 3.28071 17.2588 2.6665 15.9997 2.6665C14.7407 2.6665 13.5703 3.28071 11.2295 4.50912L8.56279 5.90853C5.75778 7.38053 4.32404 8.13292 3.51709 9.43002L15.9997 15.9998L28.4824 9.43002C27.6754 8.13292 26.2417 7.38054 23.4367 5.90853Z"
fill="currentColor"
/>
<path
opacity="0.5"
d="M28.5368 9.51965C28.5191 9.48953 28.501 9.45971 28.4826 9.43018L16 16V29.3333C17.259 29.3333 18.4294 28.7191 20.7703 27.4907L23.4369 26.0913C26.3058 24.5858 27.7403 23.833 28.5368 22.4803C29.3333 21.1277 29.3333 19.4444 29.3333 16.078V15.922C29.3333 12.5555 29.3333 10.8723 28.5368 9.51965Z"
fill="currentColor"
/>
</svg>
) : (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<path
d="M15.5777 3.38197L17.5777 4.43152C19.7294 5.56066 20.8052 6.12523 21.4026 7.13974C22 8.15425 22 9.41667 22 11.9415V12.0585C22 14.5833 22 15.8458 21.4026 16.8603C20.8052 17.8748 19.7294 18.4393 17.5777 19.5685L15.5777 20.618C13.8221 21.5393 12.9443 22 12 22C11.0557 22 10.1779 21.5393 8.42229 20.618L6.42229 19.5685C4.27063 18.4393 3.19479 17.8748 2.5974 16.8603C2 15.8458 2 14.5833 2 12.0585V11.9415C2 9.41667 2 8.15425 2.5974 7.13974C3.19479 6.12523 4.27063 5.56066 6.42229 4.43152L8.42229 3.38197C10.1779 2.46066 11.0557 2 12 2C12.9443 2 13.8221 2.46066 15.5777 3.38197Z"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
/>
<path opacity="0.5" d="M21 7.5L12 12M12 12L3 7.5M12 12V21.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
</svg>
)}
</>
);
};
export default IconBox;

View File

@ -0,0 +1,22 @@
import { FC } from 'react';
interface IconCalendarProps {
className?: string;
}
const IconCalendar: FC<IconCalendarProps> = ({ className }) => {
return (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<path
d="M2 12C2 8.22876 2 6.34315 3.17157 5.17157C4.34315 4 6.22876 4 10 4H14C17.7712 4 19.6569 4 20.8284 5.17157C22 6.34315 22 8.22876 22 12V14C22 17.7712 22 19.6569 20.8284 20.8284C19.6569 22 17.7712 22 14 22H10C6.22876 22 4.34315 22 3.17157 20.8284C2 19.6569 2 17.7712 2 14V12Z"
stroke="currentColor"
strokeWidth="1.5"
/>
<path opacity="0.5" d="M7 4V2.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
<path opacity="0.5" d="M17 4V2.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
<path opacity="0.5" d="M2 9H22" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
</svg>
);
};
export default IconCalendar;

View File

@ -0,0 +1,22 @@
import { FC } from 'react';
interface IconCameraProps {
className?: string;
}
const IconCamera: FC<IconCameraProps> = ({ className }) => {
return (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<circle cx="12" cy="13" r="3" stroke="currentColor" strokeWidth="1.5" />
<path
opacity="0.5"
d="M9.77778 21H14.2222C17.3433 21 18.9038 21 20.0248 20.2646C20.51 19.9462 20.9267 19.5371 21.251 19.0607C22 17.9601 22 16.4279 22 13.3636C22 10.2994 22 8.76721 21.251 7.6666C20.9267 7.19014 20.51 6.78104 20.0248 6.46268C19.3044 5.99013 18.4027 5.82123 17.022 5.76086C16.3631 5.76086 15.7959 5.27068 15.6667 4.63636C15.4728 3.68489 14.6219 3 13.6337 3H10.3663C9.37805 3 8.52715 3.68489 8.33333 4.63636C8.20412 5.27068 7.63685 5.76086 6.978 5.76086C5.59733 5.82123 4.69555 5.99013 3.97524 6.46268C3.48995 6.78104 3.07328 7.19014 2.74902 7.6666C2 8.76721 2 10.2994 2 13.3636C2 16.4279 2 17.9601 2.74902 19.0607C3.07328 19.5371 3.48995 19.9462 3.97524 20.2646C5.09624 21 6.65675 21 9.77778 21Z"
stroke="currentColor"
strokeWidth="1.5"
/>
<path d="M19 10H18" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
</svg>
);
};
export default IconCamera;

View File

@ -0,0 +1,15 @@
import { FC } from 'react';
interface IconCaretDownProps {
className?: string;
}
const IconCaretDown: FC<IconCaretDownProps> = ({ className }) => {
return (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<path d="M19 9L12 15L5 9" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
};
export default IconCaretDown;

View File

@ -0,0 +1,34 @@
import { FC } from 'react';
interface IconCaretsDownProps {
className?: string;
fill?: boolean;
}
const IconCaretsDown: FC<IconCaretsDownProps> = ({ className, fill = false }) => {
return (
<>
{!fill ? (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<path d="M19 11L12 17L5 11" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path opacity="0.5" d="M19 7L12 13L5 7" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
) : (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<path
opacity="0.5"
d="M5.00004 6.25C4.68618 6.25 4.40551 6.44543 4.29662 6.73979C4.18773 7.03415 4.27364 7.36519 4.51194 7.56944L11.5119 13.5694C11.7928 13.8102 12.2073 13.8102 12.4881 13.5694L19.4881 7.56944C19.7264 7.36519 19.8123 7.03415 19.7035 6.73979C19.5946 6.44543 19.3139 6.25 19 6.25H5.00004Z"
fill="currentColor"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M4.43057 10.5119C4.70014 10.1974 5.17361 10.161 5.48811 10.4306L12 16.0122L18.5119 10.4306C18.8264 10.161 19.2999 10.1974 19.5695 10.5119C19.839 10.8264 19.8026 11.2999 19.4881 11.5695L12.4881 17.5695C12.2072 17.8102 11.7928 17.8102 11.5119 17.5695L4.51192 11.5695C4.19743 11.2999 4.161 10.8264 4.43057 10.5119Z"
fill="currentColor"
/>
</svg>
)}
</>
);
};
export default IconCaretsDown;

View File

@ -0,0 +1,28 @@
import { FC } from 'react';
interface IconCashBanknotesProps {
className?: string;
}
const IconCashBanknotes: FC<IconCashBanknotesProps> = ({ className }) => {
return (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<path
d="M2 10C2 7.17157 2 5.75736 2.87868 4.87868C3.75736 4 5.17157 4 8 4H13C15.8284 4 17.2426 4 18.1213 4.87868C19 5.75736 19 7.17157 19 10C19 12.8284 19 14.2426 18.1213 15.1213C17.2426 16 15.8284 16 13 16H8C5.17157 16 3.75736 16 2.87868 15.1213C2 14.2426 2 12.8284 2 10Z"
stroke="currentColor"
strokeWidth="1.5"
/>
<path
opacity="0.5"
d="M19.0003 7.07617C19.9754 7.17208 20.6317 7.38885 21.1216 7.87873C22.0003 8.75741 22.0003 10.1716 22.0003 13.0001C22.0003 15.8285 22.0003 17.2427 21.1216 18.1214C20.2429 19.0001 18.8287 19.0001 16.0003 19.0001H11.0003C8.17187 19.0001 6.75766 19.0001 5.87898 18.1214C5.38909 17.6315 5.17233 16.9751 5.07642 16"
stroke="currentColor"
strokeWidth="1.5"
/>
<path d="M13 10C13 11.3807 11.8807 12.5 10.5 12.5C9.11929 12.5 8 11.3807 8 10C8 8.61929 9.11929 7.5 10.5 7.5C11.8807 7.5 13 8.61929 13 10Z" stroke="currentColor" strokeWidth="1.5" />
<path opacity="0.5" d="M16 12L16 8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
<path opacity="0.5" d="M5 12L5 8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
</svg>
);
};
export default IconCashBanknotes;

View File

@ -0,0 +1,23 @@
import { FC } from 'react';
interface IconChartSquareProps {
className?: string;
}
const IconChartSquare: FC<IconChartSquareProps> = ({ className }) => {
return (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<path
opacity="0.5"
d="M2 12C2 7.28595 2 4.92893 3.46447 3.46447C4.92893 2 7.28595 2 12 2C16.714 2 19.0711 2 20.5355 3.46447C22 4.92893 22 7.28595 22 12C22 16.714 22 19.0711 20.5355 20.5355C19.0711 22 16.714 22 12 22C7.28595 22 4.92893 22 3.46447 20.5355C2 19.0711 2 16.714 2 12Z"
stroke="currentColor"
strokeWidth="1.5"
/>
<path d="M7 18L7 15" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
<path d="M17 18V9" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
<path d="M12 18V12" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
</svg>
);
};
export default IconChartSquare;

View File

@ -0,0 +1,24 @@
import { FC } from 'react';
interface IconChatDotProps {
className?: string;
}
const IconChatDot: FC<IconChatDotProps> = ({ className }) => {
return (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<g opacity="0.5">
<path d="M9 12C9 12.5523 8.55228 13 8 13C7.44772 13 7 12.5523 7 12C7 11.4477 7.44772 11 8 11C8.55228 11 9 11.4477 9 12Z" fill="currentColor" />
<path d="M13 12C13 12.5523 12.5523 13 12 13C11.4477 13 11 12.5523 11 12C11 11.4477 11.4477 11 12 11C12.5523 11 13 11.4477 13 12Z" fill="currentColor" />
<path d="M17 12C17 12.5523 16.5523 13 16 13C15.4477 13 15 12.5523 15 12C15 11.4477 15.4477 11 16 11C16.5523 11 17 11.4477 17 12Z" fill="currentColor" />
</g>
<path
d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 13.5997 2.37562 15.1116 3.04346 16.4525C3.22094 16.8088 3.28001 17.2161 3.17712 17.6006L2.58151 19.8267C2.32295 20.793 3.20701 21.677 4.17335 21.4185L6.39939 20.8229C6.78393 20.72 7.19121 20.7791 7.54753 20.9565C8.88837 21.6244 10.4003 22 12 22Z"
stroke="currentColor"
strokeWidth="1.5"
/>
</svg>
);
};
export default IconChatDot;

Some files were not shown because too many files have changed in this diff Show More