mirror of
https://gitea.gofwd.group/sean/gunbuilder-next-tailwind.git
synced 2025-12-05 18:46:45 -05:00
/admin and with working with db. data is pulling from db
This commit is contained in:
10
check_hash_pw.js
Normal file
10
check_hash_pw.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
const bcrypt = require('bcryptjs');
|
||||||
|
|
||||||
|
// Replace with your plaintext password and hash
|
||||||
|
const plaintextPassword = 'newpassword'; // <-- put your real password here
|
||||||
|
const hash = '$2b$10$n78/VuxwnDoOemWoqjVKnunz5PZy7SisG3VUhsPtQXKEEnMej6TWK';
|
||||||
|
|
||||||
|
bcrypt.compare(plaintextPassword, hash, (err, result) => {
|
||||||
|
if (err) throw err;
|
||||||
|
console.log('Password matches hash?', result); // true if matches, false if not
|
||||||
|
});
|
||||||
12
hash-password.js
Normal file
12
hash-password.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
const bcrypt = require('bcryptjs');
|
||||||
|
|
||||||
|
const password = process.argv[2];
|
||||||
|
if (!password) {
|
||||||
|
console.error('Usage: node hash-password.js <password>');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
bcrypt.hash(password, 10, (err, hash) => {
|
||||||
|
if (err) throw err;
|
||||||
|
console.log('Hashed password:', hash);
|
||||||
|
});
|
||||||
16
middleware.ts
Normal file
16
middleware.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { NextResponse, NextRequest } from 'next/server';
|
||||||
|
import { getToken } from 'next-auth/jwt';
|
||||||
|
|
||||||
|
export async function middleware(request: NextRequest) {
|
||||||
|
if (request.nextUrl.pathname.startsWith('/admin')) {
|
||||||
|
const token = await getToken({ req: request, secret: process.env.NEXTAUTH_SECRET });
|
||||||
|
if (!token || !token.isAdmin) {
|
||||||
|
return NextResponse.redirect(new URL('/account/login', request.url));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NextResponse.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
matcher: ['/admin/:path*'],
|
||||||
|
};
|
||||||
100
package-lock.json
generated
100
package-lock.json
generated
@@ -9,12 +9,16 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@auth/core": "^0.34.2",
|
"@auth/core": "^0.34.2",
|
||||||
|
"@auth/drizzle-adapter": "^1.10.0",
|
||||||
"@headlessui/react": "^2.2.4",
|
"@headlessui/react": "^2.2.4",
|
||||||
"@heroicons/react": "^2.2.0",
|
"@heroicons/react": "^2.2.0",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
|
"bcryptjs": "^3.0.2",
|
||||||
"daisyui": "^4.7.3",
|
"daisyui": "^4.7.3",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"drizzle-kit": "^0.31.4",
|
"drizzle-kit": "^0.31.4",
|
||||||
"drizzle-orm": "^0.44.2",
|
"drizzle-orm": "^0.44.2",
|
||||||
|
"lucide-react": "^0.525.0",
|
||||||
"next": "^14.2.30",
|
"next": "^14.2.30",
|
||||||
"next-auth": "^4.24.11",
|
"next-auth": "^4.24.11",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
@@ -75,6 +79,75 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@auth/drizzle-adapter": {
|
||||||
|
"version": "1.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@auth/drizzle-adapter/-/drizzle-adapter-1.10.0.tgz",
|
||||||
|
"integrity": "sha512-3MKsdAINTfvV4QKev8PFMNG93HJEUHh9sggDXnmUmriFogRf8qLvgqnPsTlfUyWcLwTzzrrYjeu8CGM+4IxHwQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@auth/core": "0.40.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@auth/drizzle-adapter/node_modules/@auth/core": {
|
||||||
|
"version": "0.40.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@auth/core/-/core-0.40.0.tgz",
|
||||||
|
"integrity": "sha512-n53uJE0RH5SqZ7N1xZoMKekbHfQgjd0sAEyUbE+IYJnmuQkbvuZnXItCU7d+i7Fj8VGOgqvNO7Mw4YfBTlZeQw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@panva/hkdf": "^1.2.1",
|
||||||
|
"jose": "^6.0.6",
|
||||||
|
"oauth4webapi": "^3.3.0",
|
||||||
|
"preact": "10.24.3",
|
||||||
|
"preact-render-to-string": "6.5.11"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@simplewebauthn/browser": "^9.0.1",
|
||||||
|
"@simplewebauthn/server": "^9.0.2",
|
||||||
|
"nodemailer": "^6.8.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@simplewebauthn/browser": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@simplewebauthn/server": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"nodemailer": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@auth/drizzle-adapter/node_modules/jose": {
|
||||||
|
"version": "6.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/jose/-/jose-6.0.11.tgz",
|
||||||
|
"integrity": "sha512-QxG7EaliDARm1O1S8BGakqncGT9s25bKL1WSf6/oa17Tkqwi8D2ZNglqCF+DsYF88/rV66Q/Q2mFAy697E1DUg==",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/panva"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@auth/drizzle-adapter/node_modules/oauth4webapi": {
|
||||||
|
"version": "3.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.5.3.tgz",
|
||||||
|
"integrity": "sha512-2bnHosmBLAQpXNBLOvaJMyMkr4Yya5ohE5Q9jqyxiN+aa7GFCzvDN1RRRMrp0NkfqRR2MTaQNkcSUCCjILD9oQ==",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/panva"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@auth/drizzle-adapter/node_modules/preact": {
|
||||||
|
"version": "10.24.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz",
|
||||||
|
"integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/preact"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@auth/drizzle-adapter/node_modules/preact-render-to-string": {
|
||||||
|
"version": "6.5.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.5.11.tgz",
|
||||||
|
"integrity": "sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"preact": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/runtime": {
|
"node_modules/@babel/runtime": {
|
||||||
"version": "7.27.6",
|
"version": "7.27.6",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz",
|
||||||
@@ -2505,6 +2578,14 @@
|
|||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/bcryptjs": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==",
|
||||||
|
"bin": {
|
||||||
|
"bcrypt": "bin/bcrypt"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/binary-extensions": {
|
"node_modules/binary-extensions": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||||
@@ -2890,6 +2971,16 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/date-fns": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/kossnocorp"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.1",
|
"version": "4.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||||
@@ -4898,6 +4989,15 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lucide-react": {
|
||||||
|
"version": "0.525.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.525.0.tgz",
|
||||||
|
"integrity": "sha512-Tm1txJ2OkymCGkvwoHt33Y2JpN5xucVq1slHcgE6Lk0WjDfjgKWor5CdVER8U6DvcfMwh4M8XxmpTiyzfmfDYQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/math-intrinsics": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
|
|||||||
@@ -10,12 +10,16 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@auth/core": "^0.34.2",
|
"@auth/core": "^0.34.2",
|
||||||
|
"@auth/drizzle-adapter": "^1.10.0",
|
||||||
"@headlessui/react": "^2.2.4",
|
"@headlessui/react": "^2.2.4",
|
||||||
"@heroicons/react": "^2.2.0",
|
"@heroicons/react": "^2.2.0",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
|
"bcryptjs": "^3.0.2",
|
||||||
"daisyui": "^4.7.3",
|
"daisyui": "^4.7.3",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"drizzle-kit": "^0.31.4",
|
"drizzle-kit": "^0.31.4",
|
||||||
"drizzle-orm": "^0.44.2",
|
"drizzle-orm": "^0.44.2",
|
||||||
|
"lucide-react": "^0.525.0",
|
||||||
"next": "^14.2.30",
|
"next": "^14.2.30",
|
||||||
"next-auth": "^4.24.11",
|
"next-auth": "^4.24.11",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
|
|||||||
20
src/app/(main)/layout.tsx
Normal file
20
src/app/(main)/layout.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import "../globals.css";
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
import { Inter } from "next/font/google";
|
||||||
|
import Providers from "@/components/Providers";
|
||||||
|
|
||||||
|
const inter = Inter({ subsets: ["latin"] });
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Pew Builder - Firearm Parts Catalog & Build Management",
|
||||||
|
description: "Professional firearm parts catalog and AR-15 build management system",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function MainAppLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<Providers>
|
||||||
|
|
||||||
|
{children}
|
||||||
|
</Providers>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import BetaTester from "../components/BetaTester";
|
import BetaTester from "@/components/BetaTester";
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
export default function LandingPage() {
|
export default function LandingPage() {
|
||||||
259
src/app/admin/AdminNavbar.tsx
Normal file
259
src/app/admin/AdminNavbar.tsx
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
"use client";
|
||||||
|
import { useState, ReactNode } from 'react';
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogBackdrop,
|
||||||
|
DialogPanel,
|
||||||
|
Menu,
|
||||||
|
MenuButton,
|
||||||
|
MenuItem,
|
||||||
|
MenuItems,
|
||||||
|
TransitionChild,
|
||||||
|
} from '@headlessui/react';
|
||||||
|
import {
|
||||||
|
Bars3Icon,
|
||||||
|
BellIcon,
|
||||||
|
CalendarIcon,
|
||||||
|
ChartPieIcon,
|
||||||
|
Cog6ToothIcon,
|
||||||
|
DocumentDuplicateIcon,
|
||||||
|
FolderIcon,
|
||||||
|
HomeIcon,
|
||||||
|
UsersIcon,
|
||||||
|
XMarkIcon,
|
||||||
|
} from '@heroicons/react/24/outline';
|
||||||
|
import { ChevronDownIcon, MagnifyingGlassIcon } from '@heroicons/react/20/solid';
|
||||||
|
|
||||||
|
const navigation = [
|
||||||
|
{ name: 'Dashboard', href: '/admin', icon: HomeIcon },
|
||||||
|
{ name: 'Users', href: '/admin/users', icon: UsersIcon },
|
||||||
|
{ name: 'Category Mapping', href: '/admin/category-mapping', icon: ChartPieIcon },
|
||||||
|
// { name: 'Settings', href: '/admin/settings', icon: Cog6ToothIcon }, // optional/future
|
||||||
|
];
|
||||||
|
const userNavigation = [
|
||||||
|
{ name: 'Your profile', href: '#' },
|
||||||
|
{ name: 'Sign out', href: '#' },
|
||||||
|
];
|
||||||
|
|
||||||
|
function classNames(...classes: string[]) {
|
||||||
|
return classes.filter(Boolean).join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AdminNavbar({ children }: { children: ReactNode }) {
|
||||||
|
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<Dialog open={sidebarOpen} onClose={setSidebarOpen} className="relative z-50 lg:hidden">
|
||||||
|
<DialogBackdrop
|
||||||
|
transition
|
||||||
|
className="fixed inset-0 bg-gray-900/80 transition-opacity duration-300 ease-linear data-[closed]:opacity-0"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="fixed inset-0 flex">
|
||||||
|
<DialogPanel
|
||||||
|
transition
|
||||||
|
className="relative mr-16 flex w-full max-w-xs flex-1 transform transition duration-300 ease-in-out data-[closed]:-translate-x-full"
|
||||||
|
>
|
||||||
|
<TransitionChild>
|
||||||
|
<div className="absolute left-full top-0 flex w-16 justify-center pt-5 duration-300 ease-in-out data-[closed]:opacity-0">
|
||||||
|
<button type="button" onClick={() => setSidebarOpen(false)} className="-m-2.5 p-2.5">
|
||||||
|
<span className="sr-only">Close sidebar</span>
|
||||||
|
<XMarkIcon aria-hidden="true" className="size-6 text-white" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</TransitionChild>
|
||||||
|
|
||||||
|
{/* Sidebar component */}
|
||||||
|
<div className="flex grow flex-col gap-y-5 overflow-y-auto bg-white px-6 pb-4">
|
||||||
|
<div className="flex h-16 shrink-0 items-center">
|
||||||
|
<img
|
||||||
|
alt="Your Company"
|
||||||
|
src="https://tailwindcss.com/plus-assets/img/logos/mark.svg?color=indigo&shade=600"
|
||||||
|
className="h-8 w-auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<nav className="flex flex-1 flex-col">
|
||||||
|
<ul role="list" className="flex flex-1 flex-col gap-y-7">
|
||||||
|
<li>
|
||||||
|
<ul role="list" className="-mx-2 space-y-1">
|
||||||
|
{navigation.map((item) => (
|
||||||
|
<li key={item.name}>
|
||||||
|
<a
|
||||||
|
href={item.href}
|
||||||
|
className={classNames(
|
||||||
|
pathname === item.href
|
||||||
|
? 'bg-gray-50 text-indigo-600'
|
||||||
|
: 'text-gray-700 hover:bg-gray-50 hover:text-indigo-600',
|
||||||
|
'group flex gap-x-3 rounded-md p-2 text-sm/6 font-semibold',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<item.icon
|
||||||
|
aria-hidden="true"
|
||||||
|
className={classNames(
|
||||||
|
pathname === item.href ? 'text-indigo-600' : 'text-gray-400 group-hover:text-indigo-600',
|
||||||
|
'size-6 shrink-0',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{item.name}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li className="mt-auto">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="group -mx-2 flex gap-x-3 rounded-md p-2 text-sm/6 font-semibold text-gray-700 hover:bg-gray-50 hover:text-indigo-600"
|
||||||
|
>
|
||||||
|
<Cog6ToothIcon
|
||||||
|
aria-hidden="true"
|
||||||
|
className="size-6 shrink-0 text-gray-400 group-hover:text-indigo-600"
|
||||||
|
/>
|
||||||
|
Settings
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</DialogPanel>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Static sidebar for desktop */}
|
||||||
|
<div className="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-72 lg:flex-col">
|
||||||
|
{/* Sidebar component */}
|
||||||
|
<div className="flex grow flex-col gap-y-5 overflow-y-auto border-r border-gray-200 bg-white px-6 pb-4">
|
||||||
|
<div className="flex h-16 shrink-0 items-center">
|
||||||
|
<img
|
||||||
|
alt="Your Company"
|
||||||
|
src="https://tailwindcss.com/plus-assets/img/logos/mark.svg?color=indigo&shade=600"
|
||||||
|
className="h-8 w-auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<nav className="flex flex-1 flex-col">
|
||||||
|
<ul role="list" className="flex flex-1 flex-col gap-y-7">
|
||||||
|
<li>
|
||||||
|
<ul role="list" className="-mx-2 space-y-1">
|
||||||
|
{navigation.map((item) => (
|
||||||
|
<li key={item.name}>
|
||||||
|
<a
|
||||||
|
href={item.href}
|
||||||
|
className={classNames(
|
||||||
|
pathname === item.href
|
||||||
|
? 'bg-gray-50 text-indigo-600'
|
||||||
|
: 'text-gray-700 hover:bg-gray-50 hover:text-indigo-600',
|
||||||
|
'group flex gap-x-3 rounded-md p-2 text-sm/6 font-semibold',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<item.icon
|
||||||
|
aria-hidden="true"
|
||||||
|
className={classNames(
|
||||||
|
pathname === item.href ? 'text-indigo-600' : 'text-gray-400 group-hover:text-indigo-600',
|
||||||
|
'size-6 shrink-0',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{item.name}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li className="mt-auto">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="group -mx-2 flex gap-x-3 rounded-md p-2 text-sm/6 font-semibold text-gray-700 hover:bg-gray-50 hover:text-indigo-600"
|
||||||
|
>
|
||||||
|
<Cog6ToothIcon
|
||||||
|
aria-hidden="true"
|
||||||
|
className="size-6 shrink-0 text-gray-400 group-hover:text-indigo-600"
|
||||||
|
/>
|
||||||
|
Settings
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="lg:pl-72">
|
||||||
|
<div className="sticky top-0 z-40 flex h-16 shrink-0 items-center gap-x-4 border-b border-gray-200 bg-white px-4 shadow-sm sm:gap-x-6 sm:px-6 lg:px-8">
|
||||||
|
<button type="button" onClick={() => setSidebarOpen(true)} className="-m-2.5 p-2.5 text-gray-700 lg:hidden">
|
||||||
|
<span className="sr-only">Open sidebar</span>
|
||||||
|
<Bars3Icon aria-hidden="true" className="size-6" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Separator */}
|
||||||
|
<div aria-hidden="true" className="h-6 w-px bg-gray-200 lg:hidden" />
|
||||||
|
|
||||||
|
<div className="flex flex-1 gap-x-4 self-stretch lg:gap-x-6">
|
||||||
|
<form action="#" method="GET" className="grid flex-1 grid-cols-1">
|
||||||
|
<input
|
||||||
|
name="search"
|
||||||
|
type="search"
|
||||||
|
placeholder="Search"
|
||||||
|
aria-label="Search"
|
||||||
|
className="col-start-1 row-start-1 block size-full bg-white pl-8 text-base text-gray-900 outline-none placeholder:text-gray-400 sm:text-sm/6"
|
||||||
|
/>
|
||||||
|
<MagnifyingGlassIcon
|
||||||
|
aria-hidden="true"
|
||||||
|
className="pointer-events-none col-start-1 row-start-1 size-5 self-center text-gray-400"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
<div className="flex items-center gap-x-4 lg:gap-x-6">
|
||||||
|
<button type="button" className="-m-2.5 p-2.5 text-gray-400 hover:text-gray-500">
|
||||||
|
<span className="sr-only">View notifications</span>
|
||||||
|
<BellIcon aria-hidden="true" className="size-6" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Separator */}
|
||||||
|
<div aria-hidden="true" className="hidden lg:block lg:h-6 lg:w-px lg:bg-gray-200" />
|
||||||
|
|
||||||
|
{/* Profile dropdown */}
|
||||||
|
<Menu as="div" className="relative">
|
||||||
|
<MenuButton className="relative flex items-center">
|
||||||
|
<span className="absolute -inset-1.5" />
|
||||||
|
<span className="sr-only">Open user menu</span>
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
|
||||||
|
className="size-8 rounded-full bg-gray-50"
|
||||||
|
/>
|
||||||
|
<span className="hidden lg:flex lg:items-center">
|
||||||
|
<span aria-hidden="true" className="ml-4 text-sm/6 font-semibold text-gray-900">
|
||||||
|
Tom Cook
|
||||||
|
</span>
|
||||||
|
<ChevronDownIcon aria-hidden="true" className="ml-2 size-5 text-gray-400" />
|
||||||
|
</span>
|
||||||
|
</MenuButton>
|
||||||
|
<MenuItems
|
||||||
|
transition
|
||||||
|
className="absolute right-0 z-10 mt-2.5 w-32 origin-top-right rounded-md bg-white py-2 shadow-lg ring-1 ring-gray-900/5 transition focus:outline-none data-[closed]:scale-95 data-[closed]:transform data-[closed]:opacity-0 data-[enter]:duration-100 data-[leave]:duration-75 data-[enter]:ease-out data-[leave]:ease-in"
|
||||||
|
>
|
||||||
|
{userNavigation.map((item) => (
|
||||||
|
<MenuItem key={item.name}>
|
||||||
|
<a
|
||||||
|
href={item.href}
|
||||||
|
className="block px-3 py-1 text-sm/6 text-gray-900 data-[focus]:bg-gray-50 data-[focus]:outline-none"
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</a>
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</MenuItems>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<main className="py-10">
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
166
src/app/admin/category-mapping/page.tsx
Normal file
166
src/app/admin/category-mapping/page.tsx
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
"use client";
|
||||||
|
import { useEffect, useState, ChangeEvent, FormEvent } from "react";
|
||||||
|
|
||||||
|
type Mapping = {
|
||||||
|
id: number;
|
||||||
|
feedname: string;
|
||||||
|
affiliatecategory: string;
|
||||||
|
buildercategoryid: number;
|
||||||
|
notes?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MappingForm = {
|
||||||
|
feedname: string;
|
||||||
|
affiliatecategory: string;
|
||||||
|
buildercategoryid: string;
|
||||||
|
notes?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function CategoryMappingAdmin() {
|
||||||
|
const [mappings, setMappings] = useState<Mapping[]>([]);
|
||||||
|
const [form, setForm] = useState<MappingForm>({ feedname: "", affiliatecategory: "", buildercategoryid: "", notes: "" });
|
||||||
|
const [editingId, setEditingId] = useState<number | null>(null);
|
||||||
|
const [editForm, setEditForm] = useState<MappingForm>({ feedname: "", affiliatecategory: "", buildercategoryid: "", notes: "" });
|
||||||
|
const [productCategories, setProductCategories] = useState<any[]>([]);
|
||||||
|
|
||||||
|
// Fetch all mappings
|
||||||
|
const fetchMappings = async () => {
|
||||||
|
const res = await fetch("/api/category-mapping");
|
||||||
|
const data = await res.json();
|
||||||
|
setMappings(data.data || []);
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchProductCategories = async () => {
|
||||||
|
const res = await fetch("/api/product-categories");
|
||||||
|
const data = await res.json();
|
||||||
|
setProductCategories(data.data || []);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchMappings();
|
||||||
|
fetchProductCategories();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Add new mapping
|
||||||
|
const handleAdd = async (e: FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
await fetch("/api/category-mapping", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
...form,
|
||||||
|
buildercategoryid: parseInt(form.buildercategoryid, 10)
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
setForm({ feedname: "", affiliatecategory: "", buildercategoryid: "", notes: "" });
|
||||||
|
fetchMappings();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Edit mapping
|
||||||
|
const handleEdit = (mapping: Mapping) => {
|
||||||
|
setEditingId(mapping.id);
|
||||||
|
setEditForm({ ...mapping, buildercategoryid: mapping.buildercategoryid.toString() });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdate = async (e: FormEvent<HTMLButtonElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
await fetch("/api/category-mapping", {
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ ...editForm, buildercategoryid: parseInt(editForm.buildercategoryid, 10), id: editingId }),
|
||||||
|
});
|
||||||
|
setEditingId(null);
|
||||||
|
fetchMappings();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Delete mapping
|
||||||
|
const handleDelete = async (id: number) => {
|
||||||
|
await fetch("/api/category-mapping", {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ id }),
|
||||||
|
});
|
||||||
|
fetchMappings();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-3xl mx-auto py-8">
|
||||||
|
<h1 className="text-2xl font-bold mb-4">Affiliate Category Mapping Admin</h1>
|
||||||
|
<div className="mb-8">
|
||||||
|
<h2 className="text-xl font-semibold mb-2">AR15 Category Hierarchy</h2>
|
||||||
|
<table className="min-w-full border text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-zinc-100">
|
||||||
|
<th className="border px-2 py-1">ID</th>
|
||||||
|
<th className="border px-2 py-1">Category</th>
|
||||||
|
<th className="border px-2 py-1">Parent</th>
|
||||||
|
<th className="border px-2 py-1">Type</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{productCategories
|
||||||
|
.filter(cat => cat.type === 'AR15')
|
||||||
|
.map(cat => {
|
||||||
|
const parent = productCategories.find(p => p.id === cat.parent_category_id);
|
||||||
|
return (
|
||||||
|
<tr key={cat.id}>
|
||||||
|
<td className="border px-2 py-1">{cat.id}</td>
|
||||||
|
<td className="border px-2 py-1">{cat.name}</td>
|
||||||
|
<td className="border px-2 py-1">{parent ? parent.name : 'Top Level'}</td>
|
||||||
|
<td className="border px-2 py-1">{cat.type}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<form onSubmit={handleAdd} className="flex gap-2 mb-6">
|
||||||
|
<input className="border px-2 py-1 rounded w-32" placeholder="Feed Name" value={form.feedname} onChange={e => setForm(f => ({ ...f, feedname: e.target.value }))} required />
|
||||||
|
<input className="border px-2 py-1 rounded w-48" placeholder="Affiliate Category" value={form.affiliatecategory} onChange={e => setForm(f => ({ ...f, affiliatecategory: e.target.value }))} required />
|
||||||
|
<input className="border px-2 py-1 rounded w-40" placeholder="Builder Category ID" value={form.buildercategoryid} onChange={e => setForm(f => ({ ...f, buildercategoryid: e.target.value }))} required />
|
||||||
|
<input className="border px-2 py-1 rounded w-32" placeholder="Notes" value={form.notes} onChange={e => setForm(f => ({ ...f, notes: e.target.value }))} />
|
||||||
|
<button className="bg-blue-600 text-white px-3 py-1 rounded" type="submit">Add</button>
|
||||||
|
</form>
|
||||||
|
<table className="min-w-full border text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-zinc-100">
|
||||||
|
<th className="border px-2 py-1">Feed Name</th>
|
||||||
|
<th className="border px-2 py-1">Affiliate Category</th>
|
||||||
|
<th className="border px-2 py-1">Builder Category ID</th>
|
||||||
|
<th className="border px-2 py-1">Notes</th>
|
||||||
|
<th className="border px-2 py-1">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{mappings.map((m) => (
|
||||||
|
<tr key={m.id}>
|
||||||
|
{editingId === m.id ? (
|
||||||
|
<>
|
||||||
|
<td className="border px-2 py-1"><input className="border px-1 rounded w-28" value={editForm.feedname} onChange={e => setEditForm(f => ({ ...f, feedname: e.target.value }))} /></td>
|
||||||
|
<td className="border px-2 py-1"><input className="border px-1 rounded w-40" value={editForm.affiliatecategory} onChange={e => setEditForm(f => ({ ...f, affiliatecategory: e.target.value }))} /></td>
|
||||||
|
<td className="border px-2 py-1"><input className="border px-1 rounded w-32" value={editForm.buildercategoryid} onChange={e => setEditForm(f => ({ ...f, buildercategoryid: e.target.value }))} /></td>
|
||||||
|
<td className="border px-2 py-1"><input className="border px-1 rounded w-24" value={editForm.notes} onChange={e => setEditForm(f => ({ ...f, notes: e.target.value }))} /></td>
|
||||||
|
<td className="border px-2 py-1">
|
||||||
|
<button className="bg-green-600 text-white px-2 py-1 rounded mr-1" onClick={handleUpdate}>Save</button>
|
||||||
|
<button className="bg-gray-400 text-white px-2 py-1 rounded" onClick={() => setEditingId(null)}>Cancel</button>
|
||||||
|
</td>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<td className="border px-2 py-1">{m.feedname}</td>
|
||||||
|
<td className="border px-2 py-1">{m.affiliatecategory}</td>
|
||||||
|
<td className="border px-2 py-1">{m.buildercategoryid}</td>
|
||||||
|
<td className="border px-2 py-1">{m.notes}</td>
|
||||||
|
<td className="border px-2 py-1">
|
||||||
|
<button className="bg-yellow-500 text-white px-2 py-1 rounded mr-1" onClick={() => handleEdit(m)}>Edit</button>
|
||||||
|
<button className="bg-red-600 text-white px-2 py-1 rounded" onClick={() => handleDelete(m.id)}>Delete</button>
|
||||||
|
</td>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
9
src/app/admin/layout.tsx
Normal file
9
src/app/admin/layout.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import AdminNavbar from './AdminNavbar';
|
||||||
|
|
||||||
|
export default async function AdminLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<AdminNavbar>
|
||||||
|
{children}
|
||||||
|
</AdminNavbar>
|
||||||
|
);
|
||||||
|
}
|
||||||
278
src/app/admin/page.tsx
Normal file
278
src/app/admin/page.tsx
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import {
|
||||||
|
Users,
|
||||||
|
ShoppingCart,
|
||||||
|
TrendingUp,
|
||||||
|
AlertTriangle,
|
||||||
|
CheckCircle,
|
||||||
|
Clock,
|
||||||
|
BarChart3,
|
||||||
|
Activity,
|
||||||
|
Calendar,
|
||||||
|
Settings
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
export default function AdminDashboard() {
|
||||||
|
const [selectedPeriod, setSelectedPeriod] = useState('7d');
|
||||||
|
|
||||||
|
// Sample data - in a real app, this would come from your database
|
||||||
|
const stats = {
|
||||||
|
totalUsers: 1247,
|
||||||
|
activeUsers: 892,
|
||||||
|
totalProducts: 15420,
|
||||||
|
totalBuilds: 3421,
|
||||||
|
revenue: 45678,
|
||||||
|
growth: 12.5,
|
||||||
|
pendingApprovals: 23,
|
||||||
|
systemAlerts: 2
|
||||||
|
};
|
||||||
|
|
||||||
|
const recentActivity = [
|
||||||
|
{ id: 1, user: 'John Doe', action: 'Created new build', time: '2 minutes ago', type: 'build' },
|
||||||
|
{ id: 2, user: 'Jane Smith', action: 'Updated profile', time: '5 minutes ago', type: 'profile' },
|
||||||
|
{ id: 3, user: 'Mike Johnson', action: 'Added product to cart', time: '8 minutes ago', type: 'cart' },
|
||||||
|
{ id: 4, user: 'Sarah Wilson', action: 'Completed purchase', time: '12 minutes ago', type: 'purchase' },
|
||||||
|
{ id: 5, user: 'Admin User', action: 'Updated category mapping', time: '15 minutes ago', type: 'admin' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const topProducts = [
|
||||||
|
{ name: 'AR-15 Lower Receiver', sales: 156, revenue: 12480 },
|
||||||
|
{ name: 'Tactical Scope', sales: 89, revenue: 17800 },
|
||||||
|
{ name: 'Gun Case', sales: 234, revenue: 11700 },
|
||||||
|
{ name: 'Ammo Storage', sales: 67, revenue: 3350 },
|
||||||
|
{ name: 'Cleaning Kit', sales: 189, revenue: 5670 }
|
||||||
|
];
|
||||||
|
|
||||||
|
const systemStatus = [
|
||||||
|
{ service: 'Database', status: 'healthy', uptime: '99.9%' },
|
||||||
|
{ service: 'API', status: 'healthy', uptime: '99.8%' },
|
||||||
|
{ service: 'File Storage', status: 'warning', uptime: '98.5%' },
|
||||||
|
{ service: 'Email Service', status: 'healthy', uptime: '99.7%' }
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-8">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex justify-between items-center mb-8">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900">Admin Dashboard</h1>
|
||||||
|
<p className="text-gray-600 mt-1">Welcome back! Here's what's happening with your platform.</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<select
|
||||||
|
value={selectedPeriod}
|
||||||
|
onChange={(e) => setSelectedPeriod(e.target.value)}
|
||||||
|
className="border border-gray-300 rounded-md px-3 py-2 text-sm"
|
||||||
|
>
|
||||||
|
<option value="24h">Last 24 hours</option>
|
||||||
|
<option value="7d">Last 7 days</option>
|
||||||
|
<option value="30d">Last 30 days</option>
|
||||||
|
<option value="90d">Last 90 days</option>
|
||||||
|
</select>
|
||||||
|
<button className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 transition-colors">
|
||||||
|
<Settings className="w-4 h-4 inline mr-2" />
|
||||||
|
Settings
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats Cards */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||||
|
<div className="bg-white rounded-lg shadow p-6">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="p-2 bg-blue-100 rounded-lg">
|
||||||
|
<Users className="w-6 h-6 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
<div className="ml-4">
|
||||||
|
<p className="text-sm font-medium text-gray-600">Total Users</p>
|
||||||
|
<p className="text-2xl font-bold text-gray-900">{stats.totalUsers.toLocaleString()}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 flex items-center text-sm">
|
||||||
|
<TrendingUp className="w-4 h-4 text-green-500 mr-1" />
|
||||||
|
<span className="text-green-600">+{stats.growth}%</span>
|
||||||
|
<span className="text-gray-500 ml-1">from last month</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-lg shadow p-6">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="p-2 bg-green-100 rounded-lg">
|
||||||
|
<ShoppingCart className="w-6 h-6 text-green-600" />
|
||||||
|
</div>
|
||||||
|
<div className="ml-4">
|
||||||
|
<p className="text-sm font-medium text-gray-600">Total Products</p>
|
||||||
|
<p className="text-2xl font-bold text-gray-900">{stats.totalProducts.toLocaleString()}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 flex items-center text-sm">
|
||||||
|
<CheckCircle className="w-4 h-4 text-green-500 mr-1" />
|
||||||
|
<span className="text-green-600">Active</span>
|
||||||
|
<span className="text-gray-500 ml-1">in catalog</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-lg shadow p-6">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="p-2 bg-purple-100 rounded-lg">
|
||||||
|
<BarChart3 className="w-6 h-6 text-purple-600" />
|
||||||
|
</div>
|
||||||
|
<div className="ml-4">
|
||||||
|
<p className="text-sm font-medium text-gray-600">Total Builds</p>
|
||||||
|
<p className="text-2xl font-bold text-gray-900">{stats.totalBuilds.toLocaleString()}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 flex items-center text-sm">
|
||||||
|
<Activity className="w-4 h-4 text-blue-500 mr-1" />
|
||||||
|
<span className="text-blue-600">{stats.activeUsers}</span>
|
||||||
|
<span className="text-gray-500 ml-1">active users</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-lg shadow p-6">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="p-2 bg-orange-100 rounded-lg">
|
||||||
|
<TrendingUp className="w-6 h-6 text-orange-600" />
|
||||||
|
</div>
|
||||||
|
<div className="ml-4">
|
||||||
|
<p className="text-sm font-medium text-gray-600">Revenue</p>
|
||||||
|
<p className="text-2xl font-bold text-gray-900">${stats.revenue.toLocaleString()}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 flex items-center text-sm">
|
||||||
|
<TrendingUp className="w-4 h-4 text-green-500 mr-1" />
|
||||||
|
<span className="text-green-600">+8.2%</span>
|
||||||
|
<span className="text-gray-500 ml-1">from last month</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Alerts and Notifications */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
|
||||||
|
<div className="lg:col-span-2">
|
||||||
|
<div className="bg-white rounded-lg shadow">
|
||||||
|
<div className="p-6 border-b border-gray-200">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900">Recent Activity</h3>
|
||||||
|
</div>
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
{recentActivity.map((activity) => (
|
||||||
|
<div key={activity.id} className="flex items-center space-x-3">
|
||||||
|
<div className={`w-2 h-2 rounded-full ${
|
||||||
|
activity.type === 'build' ? 'bg-blue-500' :
|
||||||
|
activity.type === 'profile' ? 'bg-green-500' :
|
||||||
|
activity.type === 'cart' ? 'bg-orange-500' :
|
||||||
|
activity.type === 'purchase' ? 'bg-purple-500' :
|
||||||
|
'bg-gray-500'
|
||||||
|
}`} />
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="text-sm font-medium text-gray-900">{activity.user}</p>
|
||||||
|
<p className="text-sm text-gray-500">{activity.action}</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-400">{activity.time}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Pending Actions */}
|
||||||
|
<div className="bg-white rounded-lg shadow">
|
||||||
|
<div className="p-6 border-b border-gray-200">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900">Pending Actions</h3>
|
||||||
|
</div>
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<AlertTriangle className="w-5 h-5 text-yellow-500 mr-3" />
|
||||||
|
<span className="text-sm font-medium">Approvals Needed</span>
|
||||||
|
</div>
|
||||||
|
<span className="bg-yellow-100 text-yellow-800 text-xs font-medium px-2 py-1 rounded-full">
|
||||||
|
{stats.pendingApprovals}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<AlertTriangle className="w-5 h-5 text-red-500 mr-3" />
|
||||||
|
<span className="text-sm font-medium">System Alerts</span>
|
||||||
|
</div>
|
||||||
|
<span className="bg-red-100 text-red-800 text-xs font-medium px-2 py-1 rounded-full">
|
||||||
|
{stats.systemAlerts}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* System Status */}
|
||||||
|
<div className="bg-white rounded-lg shadow">
|
||||||
|
<div className="p-6 border-b border-gray-200">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900">System Status</h3>
|
||||||
|
</div>
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="space-y-3">
|
||||||
|
{systemStatus.map((service, index) => (
|
||||||
|
<div key={index} className="flex items-center justify-between">
|
||||||
|
<span className="text-sm font-medium text-gray-900">{service.service}</span>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className={`w-2 h-2 rounded-full ${
|
||||||
|
service.status === 'healthy' ? 'bg-green-500' : 'bg-yellow-500'
|
||||||
|
}`} />
|
||||||
|
<span className="text-xs text-gray-500">{service.uptime}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Top Products */}
|
||||||
|
<div className="bg-white rounded-lg shadow">
|
||||||
|
<div className="p-6 border-b border-gray-200">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900">Top Products</h3>
|
||||||
|
</div>
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="min-w-full divide-y divide-gray-200">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Product
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Sales
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Revenue
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="bg-white divide-y divide-gray-200">
|
||||||
|
{topProducts.map((product, index) => (
|
||||||
|
<tr key={index}>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||||
|
{product.name}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||||
|
{product.sales}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||||
|
${product.revenue.toLocaleString()}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
183
src/app/admin/users/page.tsx
Normal file
183
src/app/admin/users/page.tsx
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { db } from '@/db';
|
||||||
|
import { users } from '@/db/schema';
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
|
||||||
|
async function getUsers() {
|
||||||
|
try {
|
||||||
|
const allUsers = await db.query.users.findMany({
|
||||||
|
columns: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
name: true,
|
||||||
|
first_name: true,
|
||||||
|
last_name: true,
|
||||||
|
isAdmin: true,
|
||||||
|
emailVerified: true,
|
||||||
|
createdAt: true,
|
||||||
|
lastLogin: true,
|
||||||
|
buildPrivacySetting: true
|
||||||
|
},
|
||||||
|
orderBy: (users, { desc }) => [desc(users.createdAt)]
|
||||||
|
});
|
||||||
|
return allUsers;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching users:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function AdminUsersPage() {
|
||||||
|
const usersList = await getUsers();
|
||||||
|
|
||||||
|
const adminCount = usersList.filter(user => user.isAdmin).length;
|
||||||
|
const verifiedCount = usersList.filter(user => user.emailVerified).length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-8">
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<h1 className="text-2xl font-bold">Users</h1>
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
Total: {usersList.length} | Admins: {adminCount} | Verified: {verifiedCount}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats Cards */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
||||||
|
<div className="bg-white rounded-lg shadow p-4">
|
||||||
|
<div className="text-2xl font-bold text-blue-600">{usersList.length}</div>
|
||||||
|
<div className="text-sm text-gray-500">Total Users</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white rounded-lg shadow p-4">
|
||||||
|
<div className="text-2xl font-bold text-purple-600">{adminCount}</div>
|
||||||
|
<div className="text-sm text-gray-500">Admins</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white rounded-lg shadow p-4">
|
||||||
|
<div className="text-2xl font-bold text-green-600">{verifiedCount}</div>
|
||||||
|
<div className="text-sm text-gray-500">Verified</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white rounded-lg shadow p-4">
|
||||||
|
<div className="text-2xl font-bold text-orange-600">{usersList.length - verifiedCount}</div>
|
||||||
|
<div className="text-sm text-gray-500">Unverified</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-lg shadow overflow-hidden">
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="min-w-full divide-y divide-gray-200">
|
||||||
|
<thead className="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
User
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Email
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Status
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Role
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Joined
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Last Login
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Privacy
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Actions
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="bg-white divide-y divide-gray-200">
|
||||||
|
{usersList.map((user) => (
|
||||||
|
<tr key={user.id} className="hover:bg-gray-50">
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="flex-shrink-0 h-10 w-10">
|
||||||
|
<div className="h-10 w-10 rounded-full bg-gray-300 flex items-center justify-center">
|
||||||
|
<span className="text-sm font-medium text-gray-700">
|
||||||
|
{user.first_name?.[0] || user.last_name?.[0] || user.email[0].toUpperCase()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="ml-4">
|
||||||
|
<div className="text-sm font-medium text-gray-900">
|
||||||
|
{user.first_name && user.last_name
|
||||||
|
? `${user.first_name} ${user.last_name}`
|
||||||
|
: user.name || 'No name'
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
ID: {user.id}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
|
<div className="text-sm text-gray-900">{user.email}</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
|
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${
|
||||||
|
user.emailVerified
|
||||||
|
? 'bg-green-100 text-green-800'
|
||||||
|
: 'bg-yellow-100 text-yellow-800'
|
||||||
|
}`}>
|
||||||
|
{user.emailVerified ? 'Verified' : 'Unverified'}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
|
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${
|
||||||
|
user.isAdmin
|
||||||
|
? 'bg-purple-100 text-purple-800'
|
||||||
|
: 'bg-gray-100 text-gray-800'
|
||||||
|
}`}>
|
||||||
|
{user.isAdmin ? 'Admin' : 'User'}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||||
|
{user.createdAt ? format(new Date(user.createdAt), 'MMM d, yyyy') : 'N/A'}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||||
|
{user.lastLogin ? format(new Date(user.lastLogin), 'MMM d, yyyy') : 'Never'}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
|
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${
|
||||||
|
user.buildPrivacySetting === 'public'
|
||||||
|
? 'bg-blue-100 text-blue-800'
|
||||||
|
: 'bg-orange-100 text-orange-800'
|
||||||
|
}`}>
|
||||||
|
{user.buildPrivacySetting || 'public'}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<button className="text-indigo-600 hover:text-indigo-900">
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
<button className="text-red-600 hover:text-red-900">
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{usersList.length === 0 && (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<div className="text-gray-500">No users found</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,16 +1,14 @@
|
|||||||
import NextAuth from 'next-auth';
|
import NextAuth from 'next-auth';
|
||||||
import GoogleProvider from 'next-auth/providers/google';
|
import GoogleProvider from 'next-auth/providers/google';
|
||||||
import CredentialsProvider from 'next-auth/providers/credentials';
|
import CredentialsProvider from 'next-auth/providers/credentials';
|
||||||
|
import { DrizzleAdapter } from '@auth/drizzle-adapter';
|
||||||
// In-memory user store (for demo only)
|
import { db } from '@/db';
|
||||||
type User = { email: string; password: string };
|
import { users } from '@/db/schema';
|
||||||
declare global {
|
import bcrypt from 'bcryptjs';
|
||||||
// eslint-disable-next-line no-var
|
|
||||||
var _users: User[] | undefined;
|
|
||||||
}
|
|
||||||
const users: User[] = global._users || (global._users = []);
|
|
||||||
|
|
||||||
const handler = NextAuth({
|
const handler = NextAuth({
|
||||||
|
adapter: DrizzleAdapter(db),
|
||||||
|
session: { strategy: 'jwt' },
|
||||||
providers: [
|
providers: [
|
||||||
GoogleProvider({
|
GoogleProvider({
|
||||||
clientId: process.env.GOOGLE_CLIENT_ID ?? '',
|
clientId: process.env.GOOGLE_CLIENT_ID ?? '',
|
||||||
@@ -23,25 +21,16 @@ const handler = NextAuth({
|
|||||||
password: { label: "Password", type: "password" }
|
password: { label: "Password", type: "password" }
|
||||||
},
|
},
|
||||||
async authorize(credentials) {
|
async authorize(credentials) {
|
||||||
|
console.log('Credentials received:', credentials);
|
||||||
if (!credentials?.email || !credentials?.password) return null;
|
if (!credentials?.email || !credentials?.password) return null;
|
||||||
// Check in-memory user store
|
// Query the real users table using Drizzle
|
||||||
const user = users.find(
|
const foundUser = await db.query.users.findFirst({
|
||||||
(u) => u.email === credentials.email && u.password === credentials.password
|
where: (u, { eq }) => eq(u.email, credentials.email),
|
||||||
);
|
});
|
||||||
if (user) {
|
console.log('User found:', foundUser);
|
||||||
return {
|
if (foundUser && foundUser.hashedPassword && await bcrypt.compare(credentials.password, foundUser.hashedPassword)) {
|
||||||
id: user.email,
|
console.log('Returning user:', foundUser);
|
||||||
email: user.email,
|
return foundUser;
|
||||||
name: user.email.split('@')[0],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// For demo, still allow the test user
|
|
||||||
if (credentials.email === "test@example.com" && credentials.password === "password") {
|
|
||||||
return {
|
|
||||||
id: "1",
|
|
||||||
email: credentials.email,
|
|
||||||
name: "Test User",
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -53,17 +42,25 @@ const handler = NextAuth({
|
|||||||
// error: '/account/error', // Uncomment when error page is ready
|
// error: '/account/error', // Uncomment when error page is ready
|
||||||
},
|
},
|
||||||
callbacks: {
|
callbacks: {
|
||||||
async session({ session, token }) {
|
async session({ session, user, token }) {
|
||||||
// Add any additional user data to the session here
|
console.log('Session callback - user:', user);
|
||||||
|
console.log('Session callback - token:', token);
|
||||||
|
if (session.user) {
|
||||||
|
(session.user as any).isAdmin = (user as any)?.isAdmin ?? token?.isAdmin ?? false;
|
||||||
|
console.log('Session callback - final isAdmin:', (session.user as any).isAdmin);
|
||||||
|
}
|
||||||
return session;
|
return session;
|
||||||
},
|
},
|
||||||
async jwt({ token, user }) {
|
async jwt({ token, user }) {
|
||||||
// Add any additional user data to the JWT here
|
console.log('JWT callback - user:', user);
|
||||||
if (user) {
|
console.log('JWT callback - token before:', token);
|
||||||
token.id = user.id;
|
if (user && typeof user === 'object' && 'isAdmin' in user) {
|
||||||
|
(token as any).isAdmin = (user as any).isAdmin;
|
||||||
|
console.log('JWT callback - setting isAdmin to:', (user as any).isAdmin);
|
||||||
}
|
}
|
||||||
|
console.log('JWT callback - token after:', token);
|
||||||
return token;
|
return token;
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
17
src/app/api/auth/check-admin/route.ts
Normal file
17
src/app/api/auth/check-admin/route.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { getToken } from 'next-auth/jwt';
|
||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
const token = await getToken({
|
||||||
|
req: request,
|
||||||
|
secret: process.env.NEXTAUTH_SECRET
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Check Admin API - Token:', token);
|
||||||
|
console.log('Check Admin API - isAdmin:', token?.isAdmin);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
isAdmin: token?.isAdmin || false,
|
||||||
|
token: token
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import { db } from "@/db";
|
import { NextResponse } from 'next/server';
|
||||||
import { componentType } from "@/db/schema";
|
import { db } from '@/db';
|
||||||
|
import { categories } from '@/db/schema';
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
const allComponentTypes = await db.select().from(componentType);
|
const allCategories = await db.select().from(categories);
|
||||||
return Response.json({ success: true, data: allComponentTypes });
|
return NextResponse.json({ success: true, data: allCategories });
|
||||||
}
|
}
|
||||||
66
src/app/api/category-mapping/route.ts
Normal file
66
src/app/api/category-mapping/route.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { db } from '@/db';
|
||||||
|
import { affiliateCategoryMap } from '@/db/schema';
|
||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
|
||||||
|
// GET: List all mappings
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const mappings = await db.select().from(affiliateCategoryMap);
|
||||||
|
return NextResponse.json({ success: true, data: mappings });
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('GET /api/category-mapping error:', error);
|
||||||
|
return NextResponse.json({ success: false, error: error.message || 'Unknown error', data: [] }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST: Create a new mapping
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await req.json();
|
||||||
|
const { feedname, affiliatecategory, buildercategoryid, notes } = body;
|
||||||
|
if (!feedname || !affiliatecategory || !buildercategoryid) {
|
||||||
|
return NextResponse.json({ success: false, error: 'Missing required fields' }, { status: 400 });
|
||||||
|
}
|
||||||
|
const [inserted] = await db.insert(affiliateCategoryMap).values({ feedname, affiliatecategory, buildercategoryid, notes }).returning();
|
||||||
|
return NextResponse.json({ success: true, data: inserted });
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('POST /api/category-mapping error:', error);
|
||||||
|
return NextResponse.json({ success: false, error: error.message || 'Unknown error' }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT: Update a mapping
|
||||||
|
export async function PUT(req: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await req.json();
|
||||||
|
const { id, feedname, affiliatecategory, buildercategoryid, notes } = body;
|
||||||
|
if (!id) {
|
||||||
|
return NextResponse.json({ success: false, error: 'Missing id' }, { status: 400 });
|
||||||
|
}
|
||||||
|
const [updated] = await db.update(affiliateCategoryMap)
|
||||||
|
.set({ feedname, affiliatecategory, buildercategoryid, notes })
|
||||||
|
.where(eq(affiliateCategoryMap.id, id))
|
||||||
|
.returning();
|
||||||
|
return NextResponse.json({ success: true, data: updated });
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('PUT /api/category-mapping error:', error);
|
||||||
|
return NextResponse.json({ success: false, error: error.message || 'Unknown error' }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE: Remove a mapping
|
||||||
|
export async function DELETE(req: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await req.json();
|
||||||
|
const { id } = body;
|
||||||
|
if (!id) {
|
||||||
|
return NextResponse.json({ success: false, error: 'Missing id' }, { status: 400 });
|
||||||
|
}
|
||||||
|
await db.delete(affiliateCategoryMap).where(eq(affiliateCategoryMap.id, id));
|
||||||
|
return NextResponse.json({ success: true });
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('DELETE /api/category-mapping error:', error);
|
||||||
|
return NextResponse.json({ success: false, error: error.message || 'Unknown error' }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
8
src/app/api/product-categories/route.ts
Normal file
8
src/app/api/product-categories/route.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { db } from '@/db';
|
||||||
|
import { product_categories } from '@/db/schema';
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
const allCategories = await db.select().from(product_categories);
|
||||||
|
return NextResponse.json({ success: true, data: allCategories });
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Inter } from "next/font/google";
|
import { Inter } from "next/font/google";
|
||||||
import Providers from "@/components/Providers";
|
|
||||||
|
|
||||||
const inter = Inter({ subsets: ["latin"] });
|
const inter = Inter({ subsets: ["latin"] });
|
||||||
|
|
||||||
@@ -18,9 +17,7 @@ export default function RootLayout({
|
|||||||
return (
|
return (
|
||||||
<html lang="en" suppressHydrationWarning data-theme="pew">
|
<html lang="en" suppressHydrationWarning data-theme="pew">
|
||||||
<body className={`${inter.className} antialiased`}>
|
<body className={`${inter.className} antialiased`}>
|
||||||
<Providers>
|
{children}
|
||||||
{children}
|
|
||||||
</Providers>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -424,7 +424,7 @@ export const accounts = pgTable("accounts", {
|
|||||||
|
|
||||||
/* From here down is the authentication library Lusia tables */
|
/* From here down is the authentication library Lusia tables */
|
||||||
|
|
||||||
export const users = pgTable("users",
|
export const users = pgTable("user",
|
||||||
{
|
{
|
||||||
id: varchar("id", { length: 21 }).primaryKey(),
|
id: varchar("id", { length: 21 }).primaryKey(),
|
||||||
name: varchar("name"),
|
name: varchar("name"),
|
||||||
@@ -462,21 +462,16 @@ export const users = pgTable("users",
|
|||||||
export type User = typeof users.$inferSelect;
|
export type User = typeof users.$inferSelect;
|
||||||
export type NewUser = typeof users.$inferInsert;
|
export type NewUser = typeof users.$inferInsert;
|
||||||
|
|
||||||
export const sessions = pgTable(
|
export const session = pgTable(
|
||||||
"sessions",
|
"session",
|
||||||
{
|
{
|
||||||
id: varchar("id", { length: 255 }).primaryKey(),
|
sessionToken: varchar("sessionToken", { length: 255 }).primaryKey(),
|
||||||
userId: varchar("user_id", { length: 21 }).notNull(),
|
userId: varchar("userId", { length: 21 }).notNull(),
|
||||||
expiresAt: timestamp("expires_at", { withTimezone: true, mode: "date" }).notNull(),
|
expires: timestamp("expires", { withTimezone: true, mode: "date" }).notNull(),
|
||||||
createdAt: timestamp("created_at", { mode: 'string' }).default(sql`CURRENT_TIMESTAMP`),
|
}
|
||||||
updatedAt: timestamp("updated_at", { mode: 'string' }).default(sql`CURRENT_TIMESTAMP`),
|
);
|
||||||
},
|
|
||||||
(t) => ({
|
export const emailVerificationCodes = pgTable(
|
||||||
userIdx: index("session_user_idx").on(t.userId),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
export const emailVerificationCodes = pgTable(
|
|
||||||
"email_verification_codes",
|
"email_verification_codes",
|
||||||
{
|
{
|
||||||
id: serial("id").primaryKey(),
|
id: serial("id").primaryKey(),
|
||||||
@@ -555,4 +550,22 @@ export const sessions = pgTable(
|
|||||||
// price: integer("price"),
|
// price: integer("price"),
|
||||||
// createdAt: timestamp("created_at").defaultNow(),
|
// createdAt: timestamp("created_at").defaultNow(),
|
||||||
// // Add more fields as needed
|
// // Add more fields as needed
|
||||||
// });
|
// });
|
||||||
|
|
||||||
|
export const affiliateCategoryMap = pgTable("affiliate_category_map", {
|
||||||
|
id: integer().primaryKey().generatedAlwaysAsIdentity({ name: "affiliate_category_map_id_seq", startWith: 1, increment: 1 }),
|
||||||
|
feedname: varchar("feedname", { length: 100 }).notNull(),
|
||||||
|
affiliatecategory: varchar("affiliatecategory", { length: 255 }).notNull(),
|
||||||
|
buildercategoryid: integer("buildercategoryid").notNull(),
|
||||||
|
notes: varchar("notes", { length: 255 }),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const product_categories = pgTable("product_categories", {
|
||||||
|
id: integer().primaryKey().generatedAlwaysAsIdentity({ name: "product_categories_id_seq", startWith: 1, increment: 1 }),
|
||||||
|
name: varchar({ length: 100 }).notNull(),
|
||||||
|
parent_category_id: integer("parent_category_id"),
|
||||||
|
type: varchar({ length: 50 }),
|
||||||
|
sort_order: integer("sort_order"),
|
||||||
|
created_at: timestamp("created_at", { mode: 'string' }).defaultNow(),
|
||||||
|
updated_at: timestamp("updated_at", { mode: 'string' }).defaultNow(),
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user