mirror of
https://gitea.gofwd.group/sean/gunbuilder-next-tailwind.git
synced 2025-12-06 02:56:45 -05:00
fucking pos. data is working but tailwind is fucked
This commit is contained in:
10
drizzle.config.ts
Normal file
10
drizzle.config.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import type { Config } from "drizzle-kit";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
schema: "./src/db/schema.ts",
|
||||||
|
out: "./drizzle/migrations",
|
||||||
|
driver: "pg",
|
||||||
|
dbCredentials: {
|
||||||
|
connectionString: process.env.DATABASE_URL!,
|
||||||
|
},
|
||||||
|
} satisfies Config;
|
||||||
8
next.config.js
Normal file
8
next.config.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
images: {
|
||||||
|
domains: ["www.brownells.com"],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = nextConfig;
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import type { NextConfig } from "next";
|
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
|
||||||
images: {
|
|
||||||
remotePatterns: [],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default nextConfig;
|
|
||||||
2759
package-lock.json
generated
2759
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
19
package.json
19
package.json
@@ -12,23 +12,26 @@
|
|||||||
"@auth/core": "^0.34.2",
|
"@auth/core": "^0.34.2",
|
||||||
"@headlessui/react": "^2.2.4",
|
"@headlessui/react": "^2.2.4",
|
||||||
"@heroicons/react": "^2.2.0",
|
"@heroicons/react": "^2.2.0",
|
||||||
|
"autoprefixer": "^10.4.21",
|
||||||
"daisyui": "^4.7.3",
|
"daisyui": "^4.7.3",
|
||||||
"next": "15.3.4",
|
"drizzle-kit": "^0.31.4",
|
||||||
|
"drizzle-orm": "^0.44.2",
|
||||||
|
"next": "^14.2.30",
|
||||||
"next-auth": "^4.24.11",
|
"next-auth": "^4.24.11",
|
||||||
"react": "^19.0.0",
|
"pg": "^8.16.3",
|
||||||
"react-dom": "^19.0.0",
|
"react": "18.2.0",
|
||||||
|
"react-dom": "18.2.0",
|
||||||
|
"tailwindcss": "^3.4.3",
|
||||||
"zustand": "^5.0.6"
|
"zustand": "^5.0.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/pg": "^8.15.4",
|
||||||
"@types/react-dom": "^19",
|
"@types/react": "^18.2.0",
|
||||||
"autoprefixer": "^10.4.21",
|
"@types/react-dom": "^18.2.0",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "15.3.4",
|
"eslint-config-next": "15.3.4",
|
||||||
"postcss": "^8.5.6",
|
|
||||||
"tailwindcss": "^3.4.4",
|
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,4 +3,4 @@ module.exports = {
|
|||||||
tailwindcss: {},
|
tailwindcss: {},
|
||||||
autoprefixer: {},
|
autoprefixer: {},
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
@@ -13,7 +13,7 @@ export default function ForgotPasswordPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-1 items-center justify-center min-h-[60vh]">
|
<div className="flex flex-1 items-center justify-center min-h-[60vh]">
|
||||||
<div className="w-full max-w-md bg-white dark:bg-neutral-900 rounded-lg shadow p-8">
|
<div className="w-full max-w-md bg-white dark:bg-zinc-900 rounded-lg shadow p-8">
|
||||||
<h1 className="text-2xl font-bold mb-4 text-gray-900 dark:text-white">Forgot your password?</h1>
|
<h1 className="text-2xl font-bold mb-4 text-gray-900 dark:text-white">Forgot your password?</h1>
|
||||||
<p className="mb-6 text-gray-600 dark:text-gray-300 text-sm">
|
<p className="mb-6 text-gray-600 dark:text-gray-300 text-sm">
|
||||||
Enter your email address and we'll send you a link to reset your password.<br/>
|
Enter your email address and we'll send you a link to reset your password.<br/>
|
||||||
@@ -24,7 +24,7 @@ export default function ForgotPasswordPage() {
|
|||||||
type="email"
|
type="email"
|
||||||
required
|
required
|
||||||
placeholder="Email address"
|
placeholder="Email address"
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-neutral-700 rounded-md bg-white dark:bg-neutral-800 text-gray-900 dark:text-white focus:outline-none focus:ring-primary-500 focus:border-primary-500"
|
className="w-full px-3 py-2 border border-gray-300 dark:border-zinc-700 rounded-md bg-white dark:bg-zinc-800 text-gray-900 dark:text-white focus:outline-none focus:ring-primary-500 focus:border-primary-500"
|
||||||
value={email}
|
value={email}
|
||||||
onChange={e => setEmail(e.target.value)}
|
onChange={e => setEmail(e.target.value)}
|
||||||
disabled={submitted}
|
disabled={submitted}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export default function AccountLayout({
|
|||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex flex-col">
|
<div className="min-h-screen flex flex-col">
|
||||||
{/* Simple navbar with back button */}
|
{/* Simple navbar with back button */}
|
||||||
<nav className="bg-white dark:bg-neutral-900 shadow-sm">
|
<nav className="bg-white dark:bg-zinc-900 shadow-sm">
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div className="flex justify-start h-16">
|
<div className="flex justify-start h-16">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export default function LoginPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* Right side form */}
|
{/* Right side form */}
|
||||||
<div className="flex-1 flex flex-col justify-center py-12 px-4 sm:px-6 lg:px-20 xl:px-24 bg-white dark:bg-neutral-900 min-h-screen">
|
<div className="flex-1 flex flex-col justify-center py-12 px-4 sm:px-6 lg:px-20 xl:px-24 bg-white dark:bg-zinc-900 min-h-screen">
|
||||||
<div className="mx-auto w-full max-w-md space-y-8">
|
<div className="mx-auto w-full max-w-md space-y-8">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="mt-6 text-3xl font-extrabold text-gray-900 dark:text-white">Sign in to your account</h2>
|
<h2 className="mt-6 text-3xl font-extrabold text-gray-900 dark:text-white">Sign in to your account</h2>
|
||||||
@@ -75,7 +75,7 @@ export default function LoginPage() {
|
|||||||
required
|
required
|
||||||
value={email}
|
value={email}
|
||||||
onChange={e => setEmail(e.target.value)}
|
onChange={e => setEmail(e.target.value)}
|
||||||
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 dark:border-neutral-700 placeholder-gray-500 dark:placeholder-neutral-400 text-gray-900 dark:text-white bg-white dark:bg-neutral-800 rounded-t-md focus:outline-none focus:ring-primary-500 focus:border-primary-500 focus:z-10 sm:text-sm"
|
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 dark:border-zinc-700 placeholder-gray-500 dark:placeholder-zinc-400 text-gray-900 dark:text-white bg-white dark:bg-zinc-800 rounded-t-md focus:outline-none focus:ring-primary-500 focus:border-primary-500 focus:z-10 sm:text-sm"
|
||||||
placeholder="Email address"
|
placeholder="Email address"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
/>
|
/>
|
||||||
@@ -92,7 +92,7 @@ export default function LoginPage() {
|
|||||||
required
|
required
|
||||||
value={password}
|
value={password}
|
||||||
onChange={e => setPassword(e.target.value)}
|
onChange={e => setPassword(e.target.value)}
|
||||||
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 dark:border-neutral-700 placeholder-gray-500 dark:placeholder-neutral-400 text-gray-900 dark:text-white bg-white dark:bg-neutral-800 rounded-b-md focus:outline-none focus:ring-primary-500 focus:border-primary-500 focus:z-10 sm:text-sm"
|
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 dark:border-zinc-700 placeholder-gray-500 dark:placeholder-zinc-400 text-gray-900 dark:text-white bg-white dark:bg-zinc-800 rounded-b-md focus:outline-none focus:ring-primary-500 focus:border-primary-500 focus:z-10 sm:text-sm"
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
/>
|
/>
|
||||||
@@ -139,10 +139,10 @@ export default function LoginPage() {
|
|||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="absolute inset-0 flex items-center">
|
<div className="absolute inset-0 flex items-center">
|
||||||
<div className="w-full border-t border-gray-300 dark:border-neutral-700" />
|
<div className="w-full border-t border-gray-300 dark:border-zinc-700" />
|
||||||
</div>
|
</div>
|
||||||
<div className="relative flex justify-center text-sm">
|
<div className="relative flex justify-center text-sm">
|
||||||
<span className="px-2 bg-white dark:bg-neutral-900 text-gray-500 dark:text-gray-400">
|
<span className="px-2 bg-white dark:bg-zinc-900 text-gray-500 dark:text-zinc-400">
|
||||||
Or continue with
|
Or continue with
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -151,7 +151,7 @@ export default function LoginPage() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleGoogle}
|
onClick={handleGoogle}
|
||||||
className="btn btn-outline flex justify-center items-center py-2 px-4 border border-gray-300 dark:border-neutral-700 rounded-md shadow-sm bg-white dark:bg-neutral-800 text-sm font-medium text-gray-500 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-neutral-700 transition"
|
className="btn btn-outline flex justify-center items-center py-2 px-4 border border-gray-300 dark:border-zinc-700 rounded-md shadow-sm bg-white dark:bg-zinc-800 text-sm font-medium text-gray-500 dark:text-zinc-300 hover:bg-gray-50 dark:hover:bg-zinc-700 transition"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
<span className="sr-only">Sign in with Google</span>
|
<span className="sr-only">Sign in with Google</span>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export default function ProfilePage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-xl mx-auto mt-12 p-6 bg-white dark:bg-neutral-900 rounded shadow">
|
<div className="max-w-xl mx-auto mt-12 p-6 bg-white dark:bg-zinc-900 rounded shadow">
|
||||||
<h1 className="text-2xl font-bold mb-4">Profile</h1>
|
<h1 className="text-2xl font-bold mb-4">Profile</h1>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div><span className="font-semibold">Name:</span> {session.user.name || 'N/A'}</div>
|
<div><span className="font-semibold">Name:</span> {session.user.name || 'N/A'}</div>
|
||||||
|
|||||||
@@ -44,15 +44,15 @@ export default function RegisterPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center bg-white dark:bg-neutral-900">
|
<div className="min-h-screen flex items-center justify-center bg-white dark:bg-zinc-900">
|
||||||
<div className="w-full max-w-md bg-white dark:bg-neutral-900 rounded-lg shadow p-8">
|
<div className="w-full max-w-md bg-white dark:bg-zinc-900 rounded-lg shadow p-8">
|
||||||
<h1 className="text-2xl font-bold mb-4 text-gray-900 dark:text-white">Create your account</h1>
|
<h1 className="text-2xl font-bold mb-4 text-gray-900 dark:text-white">Create your account</h1>
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
required
|
required
|
||||||
placeholder="Email address"
|
placeholder="Email address"
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-neutral-700 rounded-md bg-white dark:bg-neutral-800 text-gray-900 dark:text-white focus:outline-none focus:ring-primary-500 focus:border-primary-500"
|
className="w-full px-3 py-2 border border-gray-300 dark:border-zinc-700 rounded-md bg-white dark:bg-zinc-800 text-gray-900 dark:text-white focus:outline-none focus:ring-primary-500 focus:border-primary-500"
|
||||||
value={email}
|
value={email}
|
||||||
onChange={e => setEmail(e.target.value)}
|
onChange={e => setEmail(e.target.value)}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
@@ -62,7 +62,7 @@ export default function RegisterPage() {
|
|||||||
type={showPassword ? 'text' : 'password'}
|
type={showPassword ? 'text' : 'password'}
|
||||||
required
|
required
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-neutral-700 rounded-md bg-white dark:bg-neutral-800 text-gray-900 dark:text-white focus:outline-none focus:ring-primary-500 focus:border-primary-500 pr-10"
|
className="w-full px-3 py-2 border border-gray-300 dark:border-zinc-700 rounded-md bg-white dark:bg-zinc-800 text-gray-900 dark:text-white focus:outline-none focus:ring-primary-500 focus:border-primary-500 pr-10"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={e => setPassword(e.target.value)}
|
onChange={e => setPassword(e.target.value)}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
|
|||||||
7
src/app/api/brands/route.ts
Normal file
7
src/app/api/brands/route.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { db } from "@/db";
|
||||||
|
import { brands } from "@/db/schema";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
const allBrands = await db.select().from(brands);
|
||||||
|
return Response.json({ success: true, data: allBrands });
|
||||||
|
}
|
||||||
7
src/app/api/categories/route.ts
Normal file
7
src/app/api/categories/route.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { db } from "@/db";
|
||||||
|
import { componentType } from "@/db/schema";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
const allComponentTypes = await db.select().from(componentType);
|
||||||
|
return Response.json({ success: true, data: allComponentTypes });
|
||||||
|
}
|
||||||
58
src/app/api/products/[slug]/route.ts
Normal file
58
src/app/api/products/[slug]/route.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { db } from '@/db';
|
||||||
|
import { bb_products } from '@/db/schema';
|
||||||
|
|
||||||
|
function slugify(name: string): string {
|
||||||
|
return name
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]+/g, '-')
|
||||||
|
.replace(/(^-|-$)+/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
req: Request,
|
||||||
|
{ params }: { params: { slug: string } }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const allProducts = await db.select().from(bb_products);
|
||||||
|
const mapped = allProducts.map((item: any) => ({
|
||||||
|
id: item.uuid,
|
||||||
|
name: item.productName,
|
||||||
|
slug: slugify(item.productName),
|
||||||
|
description: item.shortDescription || item.longDescription || '',
|
||||||
|
longDescription: item.longDescription,
|
||||||
|
image_url: item.imageUrl || item.thumbUrl || '/window.svg',
|
||||||
|
images: [item.imageUrl, item.thumbUrl].filter(Boolean),
|
||||||
|
brand: {
|
||||||
|
id: item.brandName || 'unknown',
|
||||||
|
name: item.brandName || 'Unknown',
|
||||||
|
logo: item.brandLogoImage || '',
|
||||||
|
},
|
||||||
|
category: {
|
||||||
|
id: item.category || 'unknown',
|
||||||
|
name: item.category || 'Unknown',
|
||||||
|
},
|
||||||
|
subcategory: item.subcategory,
|
||||||
|
offers: [
|
||||||
|
{
|
||||||
|
price: parseFloat(item.salePrice || item.retailPrice || '0'),
|
||||||
|
url: item.buyLink || '',
|
||||||
|
vendor: {
|
||||||
|
name: 'Brownells',
|
||||||
|
logo: '',
|
||||||
|
},
|
||||||
|
inStock: true,
|
||||||
|
shipping: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
restrictions: {},
|
||||||
|
}));
|
||||||
|
const found = mapped.find((p: any) => p.slug === params.slug);
|
||||||
|
if (found) {
|
||||||
|
return Response.json({ success: true, product: found });
|
||||||
|
} else {
|
||||||
|
return Response.json({ success: false, error: 'Not found' }, { status: 404 });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return Response.json({ success: false, error: String(error) }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/app/api/test-products/route.ts
Normal file
24
src/app/api/test-products/route.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { db } from "@/db";
|
||||||
|
import { bb_products } from "@/db/schema";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const allProducts = await db.select().from(bb_products).limit(50);
|
||||||
|
const mapped = allProducts.map((item: any) => ({
|
||||||
|
id: item.uuid,
|
||||||
|
name: item.productName,
|
||||||
|
slug: slugify(item.productName),
|
||||||
|
...item
|
||||||
|
}));
|
||||||
|
return Response.json({ success: true, data: mapped });
|
||||||
|
} catch (error) {
|
||||||
|
return Response.json({ success: false, error: String(error) }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function slugify(name: string): string {
|
||||||
|
return name
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]+/g, '-')
|
||||||
|
.replace(/(^-|-$)+/g, '');
|
||||||
|
}
|
||||||
58
src/app/globals copy.css
Normal file
58
src/app/globals copy.css
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
@apply transition-colors duration-200;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer components {
|
||||||
|
/* Custom scrollbar for webkit browsers */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
@apply bg-zinc-100 dark:bg-zinc-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
@apply bg-zinc-300 dark:bg-zinc-600 rounded-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
@apply bg-zinc-400 dark:bg-zinc-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Focus styles for better accessibility */
|
||||||
|
.focus-ring {
|
||||||
|
@apply focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 dark:focus:ring-offset-zinc-900;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card styles */
|
||||||
|
.card {
|
||||||
|
@apply bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg shadow-sm hover:shadow-md transition-shadow duration-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button styles */
|
||||||
|
/* Removed custom .btn-primary to avoid DaisyUI conflict */
|
||||||
|
|
||||||
|
/* Input styles */
|
||||||
|
.input-field {
|
||||||
|
@apply w-full px-3 py-2 border border-zinc-300 dark:border-zinc-600 rounded-lg bg-white dark:bg-zinc-800 text-zinc-900 dark:text-white placeholder-zinc-500 dark:placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary transition-colors duration-200;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float {
|
||||||
|
0%, 100% { transform: translateY(0); }
|
||||||
|
50% { transform: translateY(-8px); }
|
||||||
|
}
|
||||||
|
.animate-float {
|
||||||
|
animation: float 4s ease-in-out infinite;
|
||||||
|
}
|
||||||
@@ -1,50 +1,3 @@
|
|||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
@layer base {
|
|
||||||
html {
|
|
||||||
scroll-behavior: smooth;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
@apply transition-colors duration-200;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@layer components {
|
|
||||||
/* Custom scrollbar for webkit browsers */
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
|
||||||
@apply bg-neutral-100 dark:bg-neutral-800;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
@apply bg-neutral-300 dark:bg-neutral-600 rounded-full;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
|
||||||
@apply bg-neutral-400 dark:bg-neutral-500;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Focus styles for better accessibility */
|
|
||||||
.focus-ring {
|
|
||||||
@apply focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 dark:focus:ring-offset-neutral-900;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Card styles */
|
|
||||||
.card {
|
|
||||||
@apply bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-lg shadow-sm hover:shadow-md transition-shadow duration-200;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Button styles */
|
|
||||||
/* Removed custom .btn-primary to avoid DaisyUI conflict */
|
|
||||||
|
|
||||||
/* Input styles */
|
|
||||||
.input-field {
|
|
||||||
@apply w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg bg-white dark:bg-neutral-800 text-neutral-900 dark:text-white placeholder-neutral-500 dark:placeholder-neutral-400 focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary transition-colors duration-200;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -29,8 +29,7 @@ export default function LandingPage() {
|
|||||||
A better way to plan your next build
|
A better way to plan your next build
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-8 text-pretty text-lg font-medium text-gray-500 sm:text-xl/8">
|
<p className="mt-8 text-pretty text-lg font-medium text-gray-500 sm:text-xl/8">
|
||||||
Anim aute id magna aliqua ad ad non deserunt sunt. Qui irure qui lorem cupidatat commodo. Elit sunt amet
|
Welcome to Pew Builder – the modern way to discover, compare, and assemble firearm parts. Start your build, explore top brands, and find the perfect components for your next project.
|
||||||
fugiat veniam occaecat fugiat aliqua. Anim aute id magna aliqua ad ad non deserunt sunt.
|
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-10 flex items-top gap-x-6">
|
<div className="mt-10 flex items-top gap-x-6">
|
||||||
<Link
|
<Link
|
||||||
@@ -42,11 +41,12 @@ export default function LandingPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Right: Product Image */}
|
{/* Right: Product Image */}
|
||||||
<div className="mt-16 sm:mt-24 lg:mt-0 lg:shrink-0 lg:grow items-top flex justify-center">
|
<div className="mt-16 sm:mt-24 lg:mt-0 lg:shrink-0 lg:grow items-top flex justify-center group">
|
||||||
<img
|
<img
|
||||||
alt="AR-15 Lower Receiver"
|
alt="AR-15 Lower Receiver"
|
||||||
src="https://i.imgur.com/IK8FbaI.png"
|
src="https://i.imgur.com/IK8FbaI.png"
|
||||||
className="max-w-md w-full h-auto object-contain rounded-xl"
|
className="max-w-md w-full h-auto object-contain rounded-xl transition-transform duration-500 ease-in-out animate-float group-hover:-translate-y-2"
|
||||||
|
style={{ willChange: 'transform' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
198
src/app/parts/categoryMapping.ts
Normal file
198
src/app/parts/categoryMapping.ts
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
// Auto-generated mapping from product feed categories (all_categories.csv)
|
||||||
|
// to standardized builder component types (component_type.csv)
|
||||||
|
// Refine as needed for your builder logic
|
||||||
|
export const categoryToComponentType: Record<string, string> = {
|
||||||
|
"Sporting Bolt Action Centerfire Rifles": "N/A",
|
||||||
|
"HANDGUN SIGHTS": "N/A",
|
||||||
|
"Synthetic Holsters": "N/A",
|
||||||
|
"Scope Mounts": "Accessories",
|
||||||
|
"Short Barrel Shotguns": "N/A",
|
||||||
|
"Polymer Centerfire Conceal Carry Pistols": "N/A",
|
||||||
|
"LESS LETHAL AMMO": "N/A",
|
||||||
|
"LESS LETHAL PISTOL": "N/A",
|
||||||
|
"Rifle/Shotgun Combos": "N/A",
|
||||||
|
"Leveraction Shotguns": "N/A",
|
||||||
|
"Specialty Pistols": "N/A",
|
||||||
|
"Miscellaneous Accessories": "N/A",
|
||||||
|
"LESS LETHAL ACCESSORIES": "N/A",
|
||||||
|
"Tactical Rimfire Semi-Auto Rifles": "N/A",
|
||||||
|
"Sporting Semi-Auto Rimfire Rifles": "N/A",
|
||||||
|
"Lower Receivers": "Lower Receiver",
|
||||||
|
"Handgun Magazines": "Magazine",
|
||||||
|
"Non-Magnified Optic Mounts": "Accessories",
|
||||||
|
"Magnified Tactical Optics": "Accessories",
|
||||||
|
"BOLT ACTION SHOTGUN": "N/A",
|
||||||
|
"Sporting Semi-Auto Shotguns": "N/A",
|
||||||
|
"Metal Frame Centerfire Pistols": "N/A",
|
||||||
|
"THERMAL OPTICS": "Accessories",
|
||||||
|
"Sporting Leveraction Rimfire Rifles": "N/A",
|
||||||
|
"Pump Rimfire Rifles": "N/A",
|
||||||
|
"TACTICAL CENTERFIRE SEMI-AUTO PISTOLS": "N/A",
|
||||||
|
"Single Action Centerfire Revolvers": "N/A",
|
||||||
|
"Range Finders": "N/A",
|
||||||
|
"Metal Frame Rimfire Pistols": "N/A",
|
||||||
|
"Sporting Bolt Action Rimfire Rifles": "N/A",
|
||||||
|
"Side by Side Shotguns": "N/A",
|
||||||
|
"Lasers and Lights": "N/A",
|
||||||
|
"Tactical Pump Shotguns": "N/A",
|
||||||
|
"Binoculars": "N/A",
|
||||||
|
"Double Action Centerfire Revolvers": "N/A",
|
||||||
|
"Tactical Semi-Auto Shotguns": "N/A",
|
||||||
|
"Scopes": "Accessories",
|
||||||
|
"Silencer Mounts": "N/A",
|
||||||
|
"Double Action Centrfire Conceal Revolver": "N/A",
|
||||||
|
"Sporting Semi-Auto Centerfire Rifles": "N/A",
|
||||||
|
"FIRE CONTROL UNIT": "N/A",
|
||||||
|
"Spotting Scopes": "N/A",
|
||||||
|
"Single Shot Centerfire Rifles": "N/A",
|
||||||
|
"Derringers": "N/A",
|
||||||
|
"Pump Centerfire Rifles": "N/A",
|
||||||
|
"Double Action Rimfire Revolvers": "N/A",
|
||||||
|
"Tactical Centerfire Semi-Auto Rifles": "N/A",
|
||||||
|
"Handgun Accessories": "N/A",
|
||||||
|
"Sporting Leveraction Centerfire Rifles": "N/A",
|
||||||
|
"Single Shot Shotguns": "N/A",
|
||||||
|
"Polymer Rimfire Pistols": "N/A",
|
||||||
|
"LESS LETHAL RIFLE": "N/A",
|
||||||
|
"Short Barrel Rifles": "N/A",
|
||||||
|
"Black Powder Guns": "N/A",
|
||||||
|
"Over/Under Shotguns": "N/A",
|
||||||
|
"TACTICAL RIMFIRE SEMI-AUTO PISTOL": "N/A",
|
||||||
|
"Non-Magnified Optic Accessories": "N/A",
|
||||||
|
"Scope Accessories": "N/A",
|
||||||
|
"Scope Rings": "N/A",
|
||||||
|
"Rimfire Silencers": "N/A",
|
||||||
|
"Non-Magnified Optics": "N/A",
|
||||||
|
"Metal Frame Centerfire Conceal Pistols": "N/A",
|
||||||
|
"LONG GUN SIGHTS": "N/A",
|
||||||
|
"UPPER RECEIVERS": "Upper Receiver",
|
||||||
|
"Double Action Rimfire Conceal Revolvers": "N/A",
|
||||||
|
"Rifle Magazines": "Magazine",
|
||||||
|
"Rifle Accessories": "N/A",
|
||||||
|
"Silencer Pistons": "N/A",
|
||||||
|
"Shotgun Silencers": "N/A",
|
||||||
|
"Tactical Bolt Action Rifles": "N/A",
|
||||||
|
"Centerfire Ammo": "N/A",
|
||||||
|
"Single Action Rimfire Revolvers": "N/A",
|
||||||
|
"Leather Holsters": "N/A",
|
||||||
|
"AR Style Centerfire Rifles": "N/A",
|
||||||
|
"Centerfire Pistol Silencers": "N/A",
|
||||||
|
"Single Shot Rimfire Rifles": "N/A",
|
||||||
|
"Silencer Accessories": "N/A",
|
||||||
|
"Sporting Pump Shotguns": "N/A",
|
||||||
|
"Single Shot Handguns": "N/A",
|
||||||
|
"Centerfire Rifle Silencers": "N/A",
|
||||||
|
"Polymer Centerfire Pistols": "N/A",
|
||||||
|
"Magnified Tactical Optic Mounts": "N/A",
|
||||||
|
"SHOTGUN MAGAZINES": "Magazine",
|
||||||
|
"BLACK POWDER FIREARMS (ATF CONTROLLED)": "N/A"
|
||||||
|
};
|
||||||
|
|
||||||
|
// List of standardized builder component types (from component_type.csv)
|
||||||
|
export const standardizedComponentTypes = [
|
||||||
|
"Upper Receiver",
|
||||||
|
"Barrel",
|
||||||
|
"Muzzle Device",
|
||||||
|
"Lower Receiver",
|
||||||
|
"Safety",
|
||||||
|
"Trigger",
|
||||||
|
"Gas Tube",
|
||||||
|
"Gas Block",
|
||||||
|
"Grips",
|
||||||
|
"Handguards",
|
||||||
|
"Charging Handle",
|
||||||
|
"Bolt Carrier Group",
|
||||||
|
"Magazine",
|
||||||
|
"Buffer Assembly",
|
||||||
|
"Buffer Tube",
|
||||||
|
"Foregrips",
|
||||||
|
"Lower Parts Kit",
|
||||||
|
"Accessories"
|
||||||
|
];
|
||||||
|
|
||||||
|
// Hybrid mapping function: prefer subcategory, fallback to category
|
||||||
|
export function mapToBuilderType(category: string, subcategory: string): string {
|
||||||
|
if (standardizedComponentTypes.includes(subcategory)) {
|
||||||
|
return subcategory;
|
||||||
|
}
|
||||||
|
if (standardizedComponentTypes.includes(category)) {
|
||||||
|
return category;
|
||||||
|
}
|
||||||
|
return "N/A";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Builder category hierarchy for filters
|
||||||
|
export const builderCategories = [
|
||||||
|
{
|
||||||
|
id: "upper-parts",
|
||||||
|
name: "Upper Parts",
|
||||||
|
subcategories: [
|
||||||
|
{ id: "complete-upper", name: "Complete Upper Receiver" },
|
||||||
|
{ id: "stripped-upper", name: "Stripped Upper Receiver" },
|
||||||
|
{ id: "barrel", name: "Barrel" },
|
||||||
|
{ id: "gas-block", name: "Gas Block" },
|
||||||
|
{ id: "gas-tube", name: "Gas Tube" },
|
||||||
|
{ id: "handguard", name: "Handguard / Rail" },
|
||||||
|
{ id: "bcg", name: "Bolt Carrier Group (BCG)" },
|
||||||
|
{ id: "charging-handle", name: "Charging Handle" },
|
||||||
|
{ id: "muzzle-device", name: "Muzzle Device" },
|
||||||
|
{ id: "forward-assist", name: "Forward Assist" },
|
||||||
|
{ id: "dust-cover", name: "Dust Cover" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "lower-parts",
|
||||||
|
name: "Lower Parts",
|
||||||
|
subcategories: [
|
||||||
|
{ id: "complete-lower", name: "Complete Lower Receiver" },
|
||||||
|
{ id: "stripped-lower", name: "Stripped Lower Receiver" },
|
||||||
|
{ id: "lower-parts-kit", name: "Lower Parts Kit" },
|
||||||
|
{ id: "trigger", name: "Trigger / Fire Control Group" },
|
||||||
|
{ id: "buffer-tube", name: "Buffer Tube Assembly" },
|
||||||
|
{ id: "buffer-spring", name: "Buffer & Spring" },
|
||||||
|
{ id: "stock", name: "Stock / Brace" },
|
||||||
|
{ id: "pistol-grip", name: "Pistol Grip" },
|
||||||
|
{ id: "trigger-guard", name: "Trigger Guard" },
|
||||||
|
{ id: "ambi-controls", name: "Ambidextrous Controls" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "accessories",
|
||||||
|
name: "Accessories",
|
||||||
|
subcategories: [
|
||||||
|
{ id: "optics", name: "Optics & Sights" },
|
||||||
|
{ id: "sling-mounts", name: "Sling Mounts / QD Points" },
|
||||||
|
{ id: "slings", name: "Slings" },
|
||||||
|
{ id: "grips-bipods", name: "Vertical Grips / Bipods" },
|
||||||
|
{ id: "lights", name: "Weapon Lights" },
|
||||||
|
{ id: "magazines", name: "Magazines" },
|
||||||
|
{ id: "optic-mounts", name: "Optic Mounts / Rings" },
|
||||||
|
{ id: "suppressors", name: "Suppressors / Adapters" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "kits-bundles",
|
||||||
|
name: "Kits / Bundles",
|
||||||
|
subcategories: [
|
||||||
|
{ id: "rifle-kit", name: "Rifle Kit" },
|
||||||
|
{ id: "pistol-kit", name: "Pistol Kit" },
|
||||||
|
{ id: "upper-kit", name: "Upper Build Kit" },
|
||||||
|
{ id: "lower-kit", name: "Lower Build Kit" },
|
||||||
|
{ id: "kit-80", name: "80% Build Kit" },
|
||||||
|
{ id: "receiver-set", name: "Matched Receiver Set" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Example subcategory mapping (expand as needed)
|
||||||
|
export const subcategoryMapping: Record<string, string> = {
|
||||||
|
"Rifle Barrels": "barrel",
|
||||||
|
"Bolt Carrier Groups": "bcg",
|
||||||
|
"Handguards & Rails": "handguard",
|
||||||
|
"Suppressors": "suppressors",
|
||||||
|
"Receivers": "complete-upper", // or "stripped-upper" if you want to split
|
||||||
|
"Triggers": "trigger",
|
||||||
|
"Rifle Stocks": "stock",
|
||||||
|
"Buttstocks": "stock",
|
||||||
|
// ...add more mappings as needed
|
||||||
|
};
|
||||||
@@ -9,24 +9,45 @@ import ProductCard from '@/components/ProductCard';
|
|||||||
import RestrictionAlert from '@/components/RestrictionAlert';
|
import RestrictionAlert from '@/components/RestrictionAlert';
|
||||||
import Tooltip from '@/components/Tooltip';
|
import Tooltip from '@/components/Tooltip';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { mockProducts } from '@/mock/product';
|
|
||||||
import type { Product } from '@/mock/product';
|
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { useBuildStore } from '@/store/useBuildStore';
|
import { useBuildStore } from '@/store/useBuildStore';
|
||||||
import { buildGroups } from '../build/page';
|
import { buildGroups } from '../build/page';
|
||||||
|
import { categoryToComponentType, standardizedComponentTypes, mapToBuilderType, builderCategories, subcategoryMapping } from './categoryMapping';
|
||||||
|
|
||||||
// Extract unique values for dropdowns
|
// Product type (copied from mock/product for type safety)
|
||||||
const categories = ['All', ...Array.from(new Set(mockProducts.map(part => part.category.name)))];
|
type Product = {
|
||||||
const brands = ['All', ...Array.from(new Set(mockProducts.map(part => part.brand.name)))];
|
id: string;
|
||||||
const vendors = ['All', ...Array.from(new Set(mockProducts.flatMap(part => part.offers.map(offer => offer.vendor.name))))];
|
name: string;
|
||||||
|
description: string;
|
||||||
|
longDescription?: string;
|
||||||
|
image_url: string;
|
||||||
|
images?: string[];
|
||||||
|
brand: { id: string; name: string; logo?: string };
|
||||||
|
category: { id: string; name: string };
|
||||||
|
subcategory?: string;
|
||||||
|
offers: Array<{
|
||||||
|
price: number;
|
||||||
|
url: string;
|
||||||
|
vendor: { name: string; logo?: string };
|
||||||
|
inStock?: boolean;
|
||||||
|
shipping?: string;
|
||||||
|
}>;
|
||||||
|
restrictions?: {
|
||||||
|
nfa?: boolean;
|
||||||
|
sbr?: boolean;
|
||||||
|
suppressor?: boolean;
|
||||||
|
stateRestrictions?: string[];
|
||||||
|
};
|
||||||
|
slug: string;
|
||||||
|
};
|
||||||
|
|
||||||
// Restrictions for filter dropdown
|
// Restrictions for filter dropdown
|
||||||
const restrictionOptions = [
|
const restrictionOptions = [
|
||||||
'All',
|
{ value: '', label: 'All Restrictions' },
|
||||||
'NFA',
|
{ value: 'NFA', label: 'NFA' },
|
||||||
'SBR',
|
{ value: 'SBR', label: 'SBR' },
|
||||||
'Suppressor',
|
{ value: 'Suppressor', label: 'Suppressor' },
|
||||||
'State Restrictions',
|
{ value: 'State Restrictions', label: 'State Restrictions' },
|
||||||
];
|
];
|
||||||
|
|
||||||
type SortField = 'name' | 'category' | 'price';
|
type SortField = 'name' | 'category' | 'price';
|
||||||
@@ -104,23 +125,23 @@ const Dropdown = ({
|
|||||||
label: string;
|
label: string;
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
options: string[];
|
options: { value: string; label: string }[];
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Listbox value={value} onChange={onChange}>
|
<Listbox value={value} onChange={onChange}>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Listbox.Label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
|
<Listbox.Label className="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-1">
|
||||||
{label}
|
{label}
|
||||||
</Listbox.Label>
|
</Listbox.Label>
|
||||||
<Listbox.Button className="relative w-full cursor-default rounded-lg bg-white dark:bg-neutral-800 py-1.5 pl-3 pr-10 text-left shadow-sm ring-1 ring-inset ring-neutral-300 dark:ring-neutral-600 focus:outline-none focus:ring-2 focus:ring-primary-500 sm:text-sm">
|
<Listbox.Button className="relative w-full cursor-default rounded-lg bg-white dark:bg-zinc-800 py-1.5 pl-3 pr-10 text-left shadow-sm ring-1 ring-inset ring-zinc-300 dark:ring-zinc-600 focus:outline-none focus:ring-2 focus:ring-primary-500 sm:text-sm">
|
||||||
<span className="block truncate text-neutral-900 dark:text-white">
|
<span className="block truncate text-zinc-900 dark:text-white">
|
||||||
{value || placeholder}
|
{value || placeholder}
|
||||||
</span>
|
</span>
|
||||||
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
||||||
<ChevronUpDownIcon
|
<ChevronUpDownIcon
|
||||||
className="h-4 w-4 text-neutral-400"
|
className="h-4 w-4 text-zinc-400"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
@@ -130,7 +151,7 @@ const Dropdown = ({
|
|||||||
leave="transition ease-in duration-100"
|
leave="transition ease-in duration-100"
|
||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
className="absolute z-20 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white dark:bg-neutral-800 py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
|
className="absolute z-20 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white dark:bg-zinc-800 py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
|
||||||
>
|
>
|
||||||
<Listbox.Options>
|
<Listbox.Options>
|
||||||
{options.map((option, optionIdx) => (
|
{options.map((option, optionIdx) => (
|
||||||
@@ -138,15 +159,15 @@ const Dropdown = ({
|
|||||||
key={optionIdx}
|
key={optionIdx}
|
||||||
className={({ active }) =>
|
className={({ active }) =>
|
||||||
`relative cursor-default select-none py-2 pl-10 pr-4 ${
|
`relative cursor-default select-none py-2 pl-10 pr-4 ${
|
||||||
active ? 'bg-primary-100 dark:bg-primary-900 text-primary-900 dark:text-primary-100' : 'text-neutral-900 dark:text-white'
|
active ? 'bg-primary-100 dark:bg-primary-900 text-primary-900 dark:text-primary-100' : 'text-zinc-900 dark:text-white'
|
||||||
}`
|
}`
|
||||||
}
|
}
|
||||||
value={option}
|
value={option.value}
|
||||||
>
|
>
|
||||||
{({ selected }) => (
|
{({ selected }) => (
|
||||||
<>
|
<>
|
||||||
<span className={`block truncate ${selected ? 'font-medium' : 'font-normal'}`}>
|
<span className={`block truncate ${selected ? 'font-medium' : 'font-normal'}`}>
|
||||||
{option}
|
{option.label}
|
||||||
</span>
|
</span>
|
||||||
{selected ? (
|
{selected ? (
|
||||||
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-primary-600 dark:text-primary-400">
|
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-primary-600 dark:text-primary-400">
|
||||||
@@ -203,41 +224,19 @@ const getComponentCategory = (productCategory: string): string => {
|
|||||||
|
|
||||||
// Map product categories to specific checklist component names
|
// Map product categories to specific checklist component names
|
||||||
const getMatchingComponentName = (productCategory: string): string => {
|
const getMatchingComponentName = (productCategory: string): string => {
|
||||||
const componentMap: Record<string, string> = {
|
return categoryToComponentType[productCategory] || '';
|
||||||
'Upper Receiver': 'Upper Receiver',
|
|
||||||
'Barrel': 'Barrel',
|
|
||||||
'BCG': 'Bolt Carrier Group (BCG)',
|
|
||||||
'Bolt Carrier Group': 'Bolt Carrier Group (BCG)',
|
|
||||||
'Charging Handle': 'Charging Handle',
|
|
||||||
'Gas Block': 'Gas Block',
|
|
||||||
'Gas Tube': 'Gas Tube',
|
|
||||||
'Handguard': 'Handguard',
|
|
||||||
'Muzzle Device': 'Muzzle Device',
|
|
||||||
'Suppressor': 'Muzzle Device', // Suppressors go to Muzzle Device component
|
|
||||||
|
|
||||||
'Lower Receiver': 'Lower Receiver',
|
|
||||||
'Trigger': 'Trigger',
|
|
||||||
'Trigger Guard': 'Trigger Guard',
|
|
||||||
'Pistol Grip': 'Pistol Grip',
|
|
||||||
'Buffer Tube': 'Buffer Tube',
|
|
||||||
'Buffer': 'Buffer',
|
|
||||||
'Buffer Spring': 'Buffer Spring',
|
|
||||||
'Stock': 'Stock',
|
|
||||||
|
|
||||||
'Magazine': 'Magazine',
|
|
||||||
'Sights': 'Sights',
|
|
||||||
'Optic': 'Sights',
|
|
||||||
'Scope': 'Sights',
|
|
||||||
'Red Dot': 'Sights',
|
|
||||||
};
|
|
||||||
|
|
||||||
return componentMap[productCategory] || '';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [selectedCategory, setSelectedCategory] = useState('All');
|
const [products, setProducts] = useState<Product[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Filters
|
||||||
|
const [selectedCategoryId, setSelectedCategoryId] = useState('all');
|
||||||
|
const [selectedSubcategoryId, setSelectedSubcategoryId] = useState('all');
|
||||||
const [selectedBrand, setSelectedBrand] = useState('All');
|
const [selectedBrand, setSelectedBrand] = useState('All');
|
||||||
const [selectedVendor, setSelectedVendor] = useState('All');
|
const [selectedVendor, setSelectedVendor] = useState('All');
|
||||||
const [priceRange, setPriceRange] = useState('');
|
const [priceRange, setPriceRange] = useState('');
|
||||||
@@ -252,17 +251,85 @@ export default function Home() {
|
|||||||
const selectedParts = useBuildStore((state) => state.selectedParts);
|
const selectedParts = useBuildStore((state) => state.selectedParts);
|
||||||
const removePartForComponent = useBuildStore((state) => state.removePartForComponent);
|
const removePartForComponent = useBuildStore((state) => state.removePartForComponent);
|
||||||
|
|
||||||
|
// Fetch live data from /api/test-products
|
||||||
|
useEffect(() => {
|
||||||
|
setLoading(true);
|
||||||
|
fetch('/api/test-products')
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success && Array.isArray(data.data)) {
|
||||||
|
// Map API data to Product type
|
||||||
|
const mapped: Product[] = data.data.slice(0, 50).map((item: any) => ({
|
||||||
|
id: item.uuid,
|
||||||
|
name: item.productName,
|
||||||
|
description: item.shortDescription || item.longDescription || '',
|
||||||
|
longDescription: item.longDescription,
|
||||||
|
image_url: item.imageUrl || item.thumbUrl || '/window.svg',
|
||||||
|
images: [item.imageUrl, item.thumbUrl].filter(Boolean),
|
||||||
|
brand: {
|
||||||
|
id: item.brandName || 'unknown',
|
||||||
|
name: item.brandName || 'Unknown',
|
||||||
|
logo: item.brandLogoImage || '',
|
||||||
|
},
|
||||||
|
category: {
|
||||||
|
id: item.category || 'unknown',
|
||||||
|
name: item.category || 'Unknown',
|
||||||
|
},
|
||||||
|
subcategory: item.subcategory,
|
||||||
|
offers: [
|
||||||
|
{
|
||||||
|
price: parseFloat(item.salePrice || item.retailPrice || '0'),
|
||||||
|
url: item.buyLink || '',
|
||||||
|
vendor: {
|
||||||
|
name: 'Brownells', // Static for now, or parse from buyLink if needed
|
||||||
|
logo: '',
|
||||||
|
},
|
||||||
|
inStock: true,
|
||||||
|
shipping: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
restrictions: {}, // Could infer from department/category if needed
|
||||||
|
slug: item.slug || '',
|
||||||
|
}));
|
||||||
|
setProducts(mapped);
|
||||||
|
// Log unique categories for mapping
|
||||||
|
const uniqueCategories = Array.from(new Set(mapped.map(p => p.category.name)));
|
||||||
|
console.log('Unique categories from live data:', uniqueCategories);
|
||||||
|
} else {
|
||||||
|
setError('No data returned from API');
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
setError(String(err));
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Extract unique values for dropdowns from live data
|
||||||
|
const categories = [{ id: 'all', name: 'All Categories' }, ...builderCategories.map(cat => ({ id: cat.id, name: cat.name }))];
|
||||||
|
const brands = [{ value: 'All', label: 'All Brands' }, ...Array.from(new Set(products.map(part => part.brand.name))).map(name => ({ value: name, label: name }))];
|
||||||
|
const vendors = [{ value: 'All', label: 'All Vendors' }, ...Array.from(new Set(products.flatMap(part => part.offers.map(offer => offer.vendor.name)))).map(name => ({ value: name, label: name }))];
|
||||||
|
|
||||||
// Read category from URL parameter on page load
|
// Read category from URL parameter on page load
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const categoryParam = searchParams.get('category');
|
const categoryParam = searchParams.get('category');
|
||||||
if (categoryParam && categories.includes(categoryParam)) {
|
if (categoryParam && categories.some(c => c.id === categoryParam)) {
|
||||||
setSelectedCategory(categoryParam);
|
setSelectedCategoryId(categoryParam);
|
||||||
}
|
}
|
||||||
}, [searchParams]);
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [searchParams, categories.map(c => c.id).join(',')]);
|
||||||
|
|
||||||
|
const selectedCategory = builderCategories.find(cat => cat.id === selectedCategoryId);
|
||||||
|
const subcategoryOptions = selectedCategory
|
||||||
|
? [{ id: 'all', name: 'All Subcategories' }, ...selectedCategory.subcategories]
|
||||||
|
: [];
|
||||||
|
|
||||||
// Filter parts based on selected criteria
|
// Filter parts based on selected criteria
|
||||||
const filteredParts = mockProducts.filter(part => {
|
const filteredParts = products.filter(part => {
|
||||||
const matchesCategory = selectedCategory === 'All' || part.category.name === selectedCategory;
|
const mappedSubcat = subcategoryMapping[part.subcategory || ''];
|
||||||
|
const matchesCategory = selectedCategoryId === 'all' || (selectedCategory && selectedCategory.subcategories.some(sub => sub.id === mappedSubcat));
|
||||||
|
const matchesSubcategory = selectedSubcategoryId === 'all' || mappedSubcat === selectedSubcategoryId;
|
||||||
const matchesBrand = selectedBrand === 'All' || part.brand.name === selectedBrand;
|
const matchesBrand = selectedBrand === 'All' || part.brand.name === selectedBrand;
|
||||||
const matchesVendor = selectedVendor === 'All' || part.offers.some(offer => offer.vendor.name === selectedVendor);
|
const matchesVendor = selectedVendor === 'All' || part.offers.some(offer => offer.vendor.name === selectedVendor);
|
||||||
const matchesSearch = !searchTerm ||
|
const matchesSearch = !searchTerm ||
|
||||||
@@ -270,14 +337,10 @@ export default function Home() {
|
|||||||
part.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
part.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
part.brand.name.toLowerCase().includes(searchTerm.toLowerCase());
|
part.brand.name.toLowerCase().includes(searchTerm.toLowerCase());
|
||||||
|
|
||||||
// Restriction filter logic
|
// Restriction filter logic (no real data, so always true)
|
||||||
let matchesRestriction = true;
|
let matchesRestriction = true;
|
||||||
if (selectedRestriction) {
|
if (selectedRestriction) {
|
||||||
if (selectedRestriction === 'NFA') matchesRestriction = !!part.restrictions?.nfa;
|
matchesRestriction = false;
|
||||||
else if (selectedRestriction === 'SBR') matchesRestriction = !!part.restrictions?.sbr;
|
|
||||||
else if (selectedRestriction === 'Suppressor') matchesRestriction = !!part.restrictions?.suppressor;
|
|
||||||
else if (selectedRestriction === 'State Restrictions') matchesRestriction = !!(part.restrictions?.stateRestrictions && part.restrictions.stateRestrictions.length > 0);
|
|
||||||
else matchesRestriction = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Price range filtering
|
// Price range filtering
|
||||||
@@ -300,13 +363,12 @@ export default function Home() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return matchesCategory && matchesBrand && matchesVendor && matchesSearch && matchesPrice && matchesRestriction;
|
return matchesCategory && matchesSubcategory && matchesBrand && matchesVendor && matchesSearch && matchesPrice && matchesRestriction;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sort parts
|
// Sort parts
|
||||||
const sortedParts = [...filteredParts].sort((a, b) => {
|
const sortedParts = [...filteredParts].sort((a, b) => {
|
||||||
let aValue: any, bValue: any;
|
let aValue: any, bValue: any;
|
||||||
|
|
||||||
if (sortField === 'price') {
|
if (sortField === 'price') {
|
||||||
aValue = Math.min(...a.offers.map(offer => offer.price));
|
aValue = Math.min(...a.offers.map(offer => offer.price));
|
||||||
bValue = Math.min(...b.offers.map(offer => offer.price));
|
bValue = Math.min(...b.offers.map(offer => offer.price));
|
||||||
@@ -317,7 +379,6 @@ export default function Home() {
|
|||||||
aValue = a.name.toLowerCase();
|
aValue = a.name.toLowerCase();
|
||||||
bValue = b.name.toLowerCase();
|
bValue = b.name.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sortDirection === 'asc') {
|
if (sortDirection === 'asc') {
|
||||||
return aValue > bValue ? 1 : -1;
|
return aValue > bValue ? 1 : -1;
|
||||||
} else {
|
} else {
|
||||||
@@ -342,7 +403,8 @@ export default function Home() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const clearFilters = () => {
|
const clearFilters = () => {
|
||||||
setSelectedCategory('All');
|
setSelectedCategoryId('all');
|
||||||
|
setSelectedSubcategoryId('all');
|
||||||
setSelectedBrand('All');
|
setSelectedBrand('All');
|
||||||
setSelectedVendor('All');
|
setSelectedVendor('All');
|
||||||
setSearchTerm('');
|
setSearchTerm('');
|
||||||
@@ -350,7 +412,7 @@ export default function Home() {
|
|||||||
setSelectedRestriction('');
|
setSelectedRestriction('');
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasActiveFilters = selectedCategory !== 'All' || selectedBrand !== 'All' || selectedVendor !== 'All' || searchTerm || priceRange || selectedRestriction;
|
const hasActiveFilters = selectedCategoryId !== 'all' || selectedBrand !== 'All' || selectedVendor !== 'All' || searchTerm || priceRange || selectedRestriction;
|
||||||
|
|
||||||
// RestrictionBadge for table view (show NFA/SBR/Suppressor/State)
|
// RestrictionBadge for table view (show NFA/SBR/Suppressor/State)
|
||||||
const getRestrictionFlags = (restrictions?: Product['restrictions']) => {
|
const getRestrictionFlags = (restrictions?: Product['restrictions']) => {
|
||||||
@@ -367,22 +429,31 @@ export default function Home() {
|
|||||||
setTimeout(() => setAddedPartIds((prev) => prev.filter((id) => id !== part.id)), 1500);
|
setTimeout(() => setAddedPartIds((prev) => prev.filter((id) => id !== part.id)), 1500);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (products.length) {
|
||||||
|
const uniqueCategories = Array.from(new Set(products.map(p => p.category?.name)));
|
||||||
|
const uniqueSubcategories = Array.from(new Set(products.map(p => p.subcategory)));
|
||||||
|
console.log('Unique categories:', uniqueCategories);
|
||||||
|
console.log('Unique subcategories:', uniqueSubcategories);
|
||||||
|
}
|
||||||
|
}, [products]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="min-h-screen bg-neutral-50 dark:bg-neutral-900">
|
<main className="min-h-screen bg-zinc-50 dark:bg-zinc-900">
|
||||||
{/* Page Title */}
|
{/* Page Title */}
|
||||||
<div className="bg-white dark:bg-neutral-800 border-b border-neutral-200 dark:border-neutral-700">
|
<div className="bg-white dark:bg-zinc-800 border-b border-zinc-200 dark:border-zinc-700">
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||||
<h1 className="text-3xl font-bold text-neutral-900 dark:text-white">
|
<h1 className="text-3xl font-bold text-zinc-900 dark:text-white">
|
||||||
Parts Catalog
|
Parts Catalog
|
||||||
{selectedCategory !== 'All' && (
|
{selectedCategory && selectedCategoryId !== 'all' && (
|
||||||
<span className="text-primary-600 dark:text-primary-400 ml-2 text-2xl">
|
<span className="text-primary-600 dark:text-primary-400 ml-2 text-2xl">
|
||||||
- {selectedCategory}
|
- {selectedCategory.name}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-neutral-600 dark:text-neutral-400 mt-2">
|
<p className="text-zinc-600 dark:text-zinc-400 mt-2">
|
||||||
{selectedCategory !== 'All'
|
{selectedCategory && selectedCategoryId !== 'all'
|
||||||
? `Showing ${selectedCategory} parts for your build`
|
? `Showing ${selectedCategory.name} parts for your build`
|
||||||
: 'Browse and filter firearm parts for your build'
|
: 'Browse and filter firearm parts for your build'
|
||||||
}
|
}
|
||||||
</p>
|
</p>
|
||||||
@@ -390,7 +461,7 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Search and Filters */}
|
{/* Search and Filters */}
|
||||||
<div className="bg-white dark:bg-neutral-800 border-b border-neutral-200 dark:border-neutral-700">
|
<div className="bg-white dark:bg-zinc-800 border-b border-zinc-200 dark:border-zinc-700">
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-3">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-3">
|
||||||
{/* Search Row */}
|
{/* Search Row */}
|
||||||
<div className="mb-3 flex justify-end">
|
<div className="mb-3 flex justify-end">
|
||||||
@@ -410,7 +481,7 @@ export default function Home() {
|
|||||||
setIsSearchExpanded(false);
|
setIsSearchExpanded(false);
|
||||||
setSearchTerm('');
|
setSearchTerm('');
|
||||||
}}
|
}}
|
||||||
className="p-2 text-neutral-500 hover:text-neutral-700 dark:text-neutral-400 dark:hover:text-neutral-200 transition-colors flex-shrink-0"
|
className="p-2 text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-200 transition-colors flex-shrink-0"
|
||||||
aria-label="Close search"
|
aria-label="Close search"
|
||||||
>
|
>
|
||||||
<XMarkIcon className="h-5 w-5" />
|
<XMarkIcon className="h-5 w-5" />
|
||||||
@@ -419,7 +490,7 @@ export default function Home() {
|
|||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsSearchExpanded(true)}
|
onClick={() => setIsSearchExpanded(true)}
|
||||||
className="p-2 text-neutral-500 hover:text-neutral-700 dark:text-neutral-400 dark:hover:text-neutral-200 transition-colors rounded-lg hover:bg-neutral-100 dark:hover:bg-neutral-700"
|
className="p-2 text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-200 transition-colors rounded-lg hover:bg-zinc-100 dark:hover:bg-zinc-700"
|
||||||
aria-label="Open search"
|
aria-label="Open search"
|
||||||
>
|
>
|
||||||
<MagnifyingGlassIcon className="h-5 w-5" />
|
<MagnifyingGlassIcon className="h-5 w-5" />
|
||||||
@@ -434,13 +505,26 @@ export default function Home() {
|
|||||||
<div className="col-span-1">
|
<div className="col-span-1">
|
||||||
<Dropdown
|
<Dropdown
|
||||||
label="Category"
|
label="Category"
|
||||||
value={selectedCategory}
|
value={selectedCategoryId}
|
||||||
onChange={setSelectedCategory}
|
onChange={setSelectedCategoryId}
|
||||||
options={categories}
|
options={categories.map(c => ({ value: c.id, label: c.name }))}
|
||||||
placeholder="All categories"
|
placeholder="All categories"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Subcategory Dropdown (only if a category is selected) */}
|
||||||
|
{selectedCategory && selectedCategoryId !== 'all' && (
|
||||||
|
<div className="col-span-1">
|
||||||
|
<Dropdown
|
||||||
|
label="Subcategory"
|
||||||
|
value={selectedSubcategoryId}
|
||||||
|
onChange={setSelectedSubcategoryId}
|
||||||
|
options={subcategoryOptions.map(s => ({ value: s.id, label: s.name }))}
|
||||||
|
placeholder="All subcategories"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Brand Dropdown */}
|
{/* Brand Dropdown */}
|
||||||
<div className="col-span-1">
|
<div className="col-span-1">
|
||||||
<Dropdown
|
<Dropdown
|
||||||
@@ -467,11 +551,11 @@ export default function Home() {
|
|||||||
<div className="col-span-1">
|
<div className="col-span-1">
|
||||||
<Listbox value={priceRange} onChange={setPriceRange}>
|
<Listbox value={priceRange} onChange={setPriceRange}>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Listbox.Label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
|
<Listbox.Label className="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-1">
|
||||||
Price Range
|
Price Range
|
||||||
</Listbox.Label>
|
</Listbox.Label>
|
||||||
<Listbox.Button className="relative w-full cursor-default rounded-lg bg-white dark:bg-neutral-800 py-1.5 pl-3 pr-10 text-left shadow-sm ring-1 ring-inset ring-neutral-300 dark:ring-neutral-600 focus:outline-none focus:ring-2 focus:ring-primary-500 sm:text-sm">
|
<Listbox.Button className="relative w-full cursor-default rounded-lg bg-white dark:bg-zinc-800 py-1.5 pl-3 pr-10 text-left shadow-sm ring-1 ring-inset ring-zinc-300 dark:ring-zinc-600 focus:outline-none focus:ring-2 focus:ring-primary-500 sm:text-sm">
|
||||||
<span className="block truncate text-neutral-900 dark:text-white">
|
<span className="block truncate text-zinc-900 dark:text-white">
|
||||||
{priceRange === '' ? 'Select price range' :
|
{priceRange === '' ? 'Select price range' :
|
||||||
priceRange === 'under-100' ? 'Under $100' :
|
priceRange === 'under-100' ? 'Under $100' :
|
||||||
priceRange === '100-300' ? '$100 - $300' :
|
priceRange === '100-300' ? '$100 - $300' :
|
||||||
@@ -480,7 +564,7 @@ export default function Home() {
|
|||||||
</span>
|
</span>
|
||||||
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
||||||
<ChevronUpDownIcon
|
<ChevronUpDownIcon
|
||||||
className="h-4 w-4 text-neutral-400"
|
className="h-4 w-4 text-zinc-400"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
@@ -490,7 +574,7 @@ export default function Home() {
|
|||||||
leave="transition ease-in duration-100"
|
leave="transition ease-in duration-100"
|
||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
className="absolute z-20 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white dark:bg-neutral-800 py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
|
className="absolute z-20 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white dark:bg-zinc-800 py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
|
||||||
>
|
>
|
||||||
<Listbox.Options>
|
<Listbox.Options>
|
||||||
{[
|
{[
|
||||||
@@ -504,7 +588,7 @@ export default function Home() {
|
|||||||
key={optionIdx}
|
key={optionIdx}
|
||||||
className={({ active }) =>
|
className={({ active }) =>
|
||||||
`relative cursor-default select-none py-2 pl-10 pr-4 ${
|
`relative cursor-default select-none py-2 pl-10 pr-4 ${
|
||||||
active ? 'bg-primary-100 dark:bg-primary-900 text-primary-900 dark:text-primary-100' : 'text-neutral-900 dark:text-white'
|
active ? 'bg-primary-100 dark:bg-primary-900 text-primary-900 dark:text-primary-100' : 'text-zinc-900 dark:text-white'
|
||||||
}`
|
}`
|
||||||
}
|
}
|
||||||
value={option.value}
|
value={option.value}
|
||||||
@@ -548,7 +632,7 @@ export default function Home() {
|
|||||||
className={`w-full px-3 py-1.5 rounded-lg transition-colors flex items-center justify-center gap-1.5 text-sm ${
|
className={`w-full px-3 py-1.5 rounded-lg transition-colors flex items-center justify-center gap-1.5 text-sm ${
|
||||||
hasActiveFilters
|
hasActiveFilters
|
||||||
? 'bg-accent-600 hover:bg-accent-700 dark:bg-accent-500 dark:hover:bg-accent-600 text-white'
|
? 'bg-accent-600 hover:bg-accent-700 dark:bg-accent-500 dark:hover:bg-accent-600 text-white'
|
||||||
: 'bg-neutral-200 dark:bg-neutral-700 text-neutral-400 dark:text-neutral-500 cursor-not-allowed'
|
: 'bg-zinc-200 dark:bg-zinc-700 text-zinc-400 dark:text-zinc-500 cursor-not-allowed'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<XMarkIcon className="h-3.5 w-3.5" />
|
<XMarkIcon className="h-3.5 w-3.5" />
|
||||||
@@ -563,18 +647,19 @@ export default function Home() {
|
|||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
{/* View Toggle and Results Count */}
|
{/* View Toggle and Results Count */}
|
||||||
<div className="flex justify-between items-center mb-6">
|
<div className="flex justify-between items-center mb-6">
|
||||||
<div className="text-sm text-neutral-700 dark:text-neutral-300">
|
<div className="text-sm text-zinc-700 dark:text-zinc-300">
|
||||||
Showing {sortedParts.length} of {mockProducts.length} parts
|
{loading ? 'Loading...' : `Showing ${sortedParts.length} of ${products.length} parts`}
|
||||||
{hasActiveFilters && (
|
{hasActiveFilters && !loading && (
|
||||||
<span className="ml-2 text-primary-600 dark:text-primary-400">
|
<span className="ml-2 text-primary-600 dark:text-primary-400">
|
||||||
(filtered)
|
(filtered)
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{error && <span className="ml-2 text-red-500">{error}</span>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* View Toggle */}
|
{/* View Toggle */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-sm text-neutral-600 dark:text-neutral-400">View:</span>
|
<span className="text-sm text-zinc-600 dark:text-zinc-400">View:</span>
|
||||||
<div className="btn-group">
|
<div className="btn-group">
|
||||||
<button
|
<button
|
||||||
className={`btn btn-sm ${viewMode === 'table' ? 'btn-active' : ''}`}
|
className={`btn btn-sm ${viewMode === 'table' ? 'btn-active' : ''}`}
|
||||||
@@ -596,19 +681,19 @@ export default function Home() {
|
|||||||
|
|
||||||
{/* Table View */}
|
{/* Table View */}
|
||||||
{viewMode === 'table' && (
|
{viewMode === 'table' && (
|
||||||
<div className="bg-white dark:bg-neutral-800 shadow-sm rounded-lg overflow-hidden border border-neutral-200 dark:border-neutral-700">
|
<div className="bg-white dark:bg-zinc-800 shadow-sm rounded-lg overflow-hidden border border-zinc-200 dark:border-zinc-700">
|
||||||
<div className="overflow-x-auto max-h-screen overflow-y-auto">
|
<div className="overflow-x-auto max-h-screen overflow-y-auto">
|
||||||
<table className="min-w-full divide-y divide-neutral-200 dark:divide-neutral-700">
|
<table className="min-w-full divide-y divide-zinc-200 dark:divide-zinc-700">
|
||||||
<thead className="bg-neutral-50 dark:bg-neutral-700 sticky top-0 z-10 shadow-sm">
|
<thead className="bg-zinc-50 dark:bg-zinc-700 sticky top-0 z-10 shadow-sm">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-neutral-500 dark:text-neutral-300 uppercase tracking-wider">
|
<th className="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-300 uppercase tracking-wider">
|
||||||
Product
|
Product
|
||||||
</th>
|
</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-neutral-500 dark:text-neutral-300 uppercase tracking-wider">
|
<th className="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-300 uppercase tracking-wider">
|
||||||
Category
|
Category
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
className="px-6 py-3 text-left text-xs font-medium text-neutral-500 dark:text-neutral-300 uppercase tracking-wider cursor-pointer hover:bg-neutral-100 dark:hover:bg-neutral-600"
|
className="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-300 uppercase tracking-wider cursor-pointer hover:bg-zinc-100 dark:hover:bg-zinc-600"
|
||||||
onClick={() => handleSort('price')}
|
onClick={() => handleSort('price')}
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-1">
|
<div className="flex items-center space-x-1">
|
||||||
@@ -616,23 +701,22 @@ export default function Home() {
|
|||||||
<span className="text-sm">{getSortIcon('price')}</span>
|
<span className="text-sm">{getSortIcon('price')}</span>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-neutral-500 dark:text-neutral-300 uppercase tracking-wider">
|
<th className="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-300 uppercase tracking-wider">
|
||||||
Actions
|
Actions
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="bg-white dark:bg-neutral-800 divide-y divide-neutral-200 dark:divide-neutral-700">
|
<tbody className="bg-white dark:bg-zinc-800 divide-y divide-zinc-200 dark:divide-zinc-700">
|
||||||
{sortedParts.map((part) => (
|
{sortedParts.map((part) => (
|
||||||
<tr key={part.id} className="hover:bg-neutral-50 dark:hover:bg-neutral-700 transition-colors">
|
<tr key={part.id} className="hover:bg-zinc-50 dark:hover:bg-zinc-700 transition-colors">
|
||||||
<td className="px-6 py-4 whitespace-nowrap flex items-center gap-3 min-w-[180px]">
|
<td className="px-0 py-2 flex items-center gap-2 align-top">
|
||||||
<div className="w-12 h-12 flex-shrink-0 rounded bg-neutral-100 dark:bg-neutral-700 overflow-hidden flex items-center justify-center border border-neutral-200 dark:border-neutral-700">
|
<div className="w-12 h-12 flex-shrink-0 rounded bg-zinc-100 dark:bg-zinc-700 overflow-hidden flex items-center justify-center border border-zinc-200 dark:border-zinc-700">
|
||||||
<Image src={Array.isArray(part.images) && (part.images as string[]).length > 0 ? (part.images as string[])[0] : '/window.svg'} alt={part.name} width={48} height={48} className="object-contain w-12 h-12" />
|
<Image src={Array.isArray(part.images) && (part.images as string[]).length > 0 ? (part.images as string[])[0] : '/window.svg'} alt={part.name} width={48} height={48} className="object-contain w-12 h-12" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="max-w-md break-words whitespace-normal">
|
||||||
<Link href={`/products/${part.id}`} className="text-sm font-semibold text-primary hover:underline dark:text-primary-400">
|
<Link href={`/products/${part.slug}`} className="text-sm font-semibold text-primary hover:underline dark:text-primary-400">
|
||||||
{part.name}
|
{part.name}
|
||||||
</Link>
|
</Link>
|
||||||
<div className="text-xs text-neutral-500 dark:text-neutral-400">{part.brand.name}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
@@ -641,7 +725,7 @@ export default function Home() {
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
<div className="text-sm font-semibold text-neutral-900 dark:text-white">
|
<div className="text-sm font-semibold text-zinc-900 dark:text-white">
|
||||||
${Math.min(...part.offers.map(offer => offer.price)).toFixed(2)}
|
${Math.min(...part.offers.map(offer => offer.price)).toFixed(2)}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -687,7 +771,7 @@ export default function Home() {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<span className="text-xs text-gray-400">Part Selected</span>
|
<span className="inline-flex items-center gap-1 px-2 py-1 rounded bg-gray-200 text-gray-500 text-xs">N/A</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
})()}
|
})()}
|
||||||
@@ -699,19 +783,16 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Table Footer */}
|
{/* Table Footer */}
|
||||||
<div className="bg-neutral-50 dark:bg-neutral-700 px-6 py-3 border-t border-neutral-200 dark:border-neutral-600">
|
<div className="bg-zinc-50 dark:bg-zinc-700 px-6 py-3 border-t border-zinc-200 dark:border-zinc-600">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="text-sm text-neutral-700 dark:text-neutral-300">
|
<div className="text-sm text-zinc-700 dark:text-zinc-300">
|
||||||
Showing {sortedParts.length} of {mockProducts.length} parts
|
Showing {sortedParts.length} of {products.length} parts
|
||||||
{hasActiveFilters && (
|
{hasActiveFilters && (
|
||||||
<span className="ml-2 text-primary-600 dark:text-primary-400">
|
<span className="ml-2 text-primary-600 dark:text-primary-400">
|
||||||
(filtered)
|
(filtered)
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-neutral-500 dark:text-neutral-400">
|
|
||||||
Total Value: ${sortedParts.reduce((sum, part) => sum + Math.min(...part.offers.map(offer => offer.price)), 0).toFixed(2)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -728,8 +809,8 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Compact Restriction Legend */}
|
{/* Compact Restriction Legend */}
|
||||||
<div className="mt-8 pt-4 border-t border-neutral-200 dark:border-neutral-700">
|
<div className="mt-8 pt-4 border-t border-zinc-200 dark:border-zinc-700">
|
||||||
<div className="flex items-center justify-center gap-4 text-xs text-neutral-500 dark:text-neutral-400">
|
<div className="flex items-center justify-center gap-4 text-xs text-zinc-500 dark:text-zinc-400">
|
||||||
<span className="font-medium">Restrictions:</span>
|
<span className="font-medium">Restrictions:</span>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<div className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-medium bg-red-600 text-white">🔒NFA</div>
|
<div className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-medium bg-red-600 text-white">🔒NFA</div>
|
||||||
|
|||||||
@@ -1,23 +1,77 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useParams } from 'next/navigation';
|
import { useParams } from 'next/navigation';
|
||||||
import { mockProducts } from '@/mock/product';
|
|
||||||
import RestrictionAlert from '@/components/RestrictionAlert';
|
import RestrictionAlert from '@/components/RestrictionAlert';
|
||||||
import { StarIcon } from '@heroicons/react/20/solid';
|
import { StarIcon } from '@heroicons/react/20/solid';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { useBuildStore } from '@/store/useBuildStore';
|
import { useBuildStore } from '@/store/useBuildStore';
|
||||||
|
|
||||||
|
// Product type (copied from /parts/page.tsx for type safety)
|
||||||
|
type Product = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
longDescription?: string;
|
||||||
|
image_url: string;
|
||||||
|
images?: string[];
|
||||||
|
brand: { id: string; name: string; logo?: string };
|
||||||
|
category: { id: string; name: string };
|
||||||
|
subcategory?: string;
|
||||||
|
offers: Array<{
|
||||||
|
price: number;
|
||||||
|
url: string;
|
||||||
|
vendor: { name: string; logo?: string };
|
||||||
|
inStock?: boolean;
|
||||||
|
shipping?: string;
|
||||||
|
}>;
|
||||||
|
restrictions?: {
|
||||||
|
nfa?: boolean;
|
||||||
|
sbr?: boolean;
|
||||||
|
suppressor?: boolean;
|
||||||
|
stateRestrictions?: string[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export default function ProductDetailsPage() {
|
export default function ProductDetailsPage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const productId = params.id as string;
|
const slug = params.slug as string;
|
||||||
|
const [product, setProduct] = useState<Product | null>(null);
|
||||||
const product = mockProducts.find(p => p.id === productId);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [selectedImageIndex, setSelectedImageIndex] = useState(0);
|
const [selectedImageIndex, setSelectedImageIndex] = useState(0);
|
||||||
const [selectedOffer, setSelectedOffer] = useState(0);
|
const [selectedOffer, setSelectedOffer] = useState(0);
|
||||||
const [addSuccess, setAddSuccess] = useState(false);
|
const [addSuccess, setAddSuccess] = useState(false);
|
||||||
const selectPartForComponent = useBuildStore((state) => state.selectPartForComponent);
|
const selectPartForComponent = useBuildStore((state) => state.selectPartForComponent);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLoading(true);
|
||||||
|
fetch(`/api/products/${slug}`)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then((data: any) => {
|
||||||
|
if (data.success && data.product) {
|
||||||
|
setProduct(data.product);
|
||||||
|
} else {
|
||||||
|
setError('No data returned from API');
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch((err: any) => {
|
||||||
|
setError(String(err));
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}, [slug]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto px-4 py-8">
|
||||||
|
<div className="alert alert-info">
|
||||||
|
<span>Loading product...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!product) {
|
if (!product) {
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-4 py-8">
|
<div className="container mx-auto px-4 py-8">
|
||||||
@@ -28,19 +82,15 @@ export default function ProductDetailsPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use images array if present, otherwise fallback to image_url
|
|
||||||
const allImages = product.images && product.images.length > 0
|
const allImages = product.images && product.images.length > 0
|
||||||
? product.images
|
? product.images
|
||||||
: [product.image_url];
|
: [product.image_url];
|
||||||
const lowestPrice = Math.min(...product.offers.map(o => o.price));
|
const lowestPrice = Math.min(...product.offers.map(o => o.price));
|
||||||
const highestPrice = Math.max(...product.offers.map(o => o.price));
|
const highestPrice = Math.max(...product.offers.map(o => o.price));
|
||||||
const averageRating = product.reviews
|
|
||||||
? product.reviews.reduce((acc, review) => acc + review.rating, 0) / product.reviews.length
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
const handleAddToBuild = () => {
|
const handleAddToBuild = () => {
|
||||||
// Map category to component ID
|
// Map category to component ID (can be improved to match /parts logic)
|
||||||
const categoryToComponentMap: Record<string, string> = {
|
const categoryToComponentMap = {
|
||||||
'Barrel': 'barrel',
|
'Barrel': 'barrel',
|
||||||
'Upper Receiver': 'upper',
|
'Upper Receiver': 'upper',
|
||||||
'Suppressor': 'suppressor',
|
'Suppressor': 'suppressor',
|
||||||
@@ -60,9 +110,7 @@ export default function ProductDetailsPage() {
|
|||||||
'Magazine': 'magazine',
|
'Magazine': 'magazine',
|
||||||
'Sights': 'sights'
|
'Sights': 'sights'
|
||||||
};
|
};
|
||||||
|
const componentId = (categoryToComponentMap as Record<string, string>)[product.category.name] || product.category.id;
|
||||||
const componentId = categoryToComponentMap[product.category.name] || product.category.id;
|
|
||||||
|
|
||||||
selectPartForComponent(componentId, {
|
selectPartForComponent(componentId, {
|
||||||
id: product.id,
|
id: product.id,
|
||||||
name: product.name,
|
name: product.name,
|
||||||
@@ -100,7 +148,7 @@ export default function ProductDetailsPage() {
|
|||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||||
{/* Product Images */}
|
{/* Product Images */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="aspect-square bg-neutral-100 dark:bg-neutral-800 rounded-lg overflow-hidden">
|
<div className="aspect-square bg-zinc-100 dark:bg-zinc-800 rounded-lg overflow-hidden">
|
||||||
<Image
|
<Image
|
||||||
src={allImages[selectedImageIndex]}
|
src={allImages[selectedImageIndex]}
|
||||||
alt={product.name}
|
alt={product.name}
|
||||||
@@ -119,7 +167,7 @@ export default function ProductDetailsPage() {
|
|||||||
className={`flex-shrink-0 w-20 h-20 rounded-lg overflow-hidden border-2 ${
|
className={`flex-shrink-0 w-20 h-20 rounded-lg overflow-hidden border-2 ${
|
||||||
selectedImageIndex === index
|
selectedImageIndex === index
|
||||||
? 'border-primary-500'
|
? 'border-primary-500'
|
||||||
: 'border-neutral-200 dark:border-neutral-700'
|
: 'border-zinc-200 dark:border-zinc-700'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
@@ -149,17 +197,17 @@ export default function ProductDetailsPage() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm text-neutral-600 dark:text-neutral-400">
|
<div className="text-sm text-zinc-600 dark:text-zinc-400">
|
||||||
{product.brand.name}
|
{product.brand.name}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-neutral-600 dark:text-neutral-400">
|
<div className="text-sm text-zinc-600 dark:text-zinc-400">
|
||||||
{product.category.name}
|
{product.category.name}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Product Name */}
|
{/* Product Name */}
|
||||||
<h1 className="text-3xl font-bold text-neutral-900 dark:text-white">
|
<h1 className="text-3xl font-bold text-zinc-900 dark:text-white">
|
||||||
{product.name}
|
{product.name}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
@@ -169,40 +217,19 @@ export default function ProductDetailsPage() {
|
|||||||
${lowestPrice.toFixed(2)}
|
${lowestPrice.toFixed(2)}
|
||||||
</div>
|
</div>
|
||||||
{lowestPrice !== highestPrice && (
|
{lowestPrice !== highestPrice && (
|
||||||
<div className="text-lg text-neutral-600 dark:text-neutral-400">
|
<div className="text-lg text-zinc-600 dark:text-zinc-400">
|
||||||
- ${highestPrice.toFixed(2)}
|
- ${highestPrice.toFixed(2)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="text-sm text-neutral-500">
|
<div className="text-sm text-zinc-500">
|
||||||
from {product.offers.length} vendor{product.offers.length > 1 ? 's' : ''}
|
from {product.offers.length} vendor{product.offers.length > 1 ? 's' : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Reviews */}
|
|
||||||
{product.reviews && product.reviews.length > 0 && (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="flex items-center">
|
|
||||||
{[1, 2, 3, 4, 5].map((star) => (
|
|
||||||
<StarIcon
|
|
||||||
key={star}
|
|
||||||
className={`h-5 w-5 ${
|
|
||||||
star <= averageRating
|
|
||||||
? 'text-yellow-400'
|
|
||||||
: 'text-neutral-300 dark:text-neutral-600'
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<span className="text-sm text-neutral-600 dark:text-neutral-400">
|
|
||||||
{averageRating.toFixed(1)} ({product.reviews.length} reviews)
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Description */}
|
{/* Description */}
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-semibold mb-2">Description</h3>
|
<h3 className="text-lg font-semibold mb-2">Description</h3>
|
||||||
<p className="text-neutral-700 dark:text-neutral-300">
|
<p className="text-zinc-700 dark:text-zinc-300">
|
||||||
{product.longDescription || product.description}
|
{product.longDescription || product.description}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -222,54 +249,6 @@ export default function ProductDetailsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Specifications */}
|
|
||||||
{product.specifications && (
|
|
||||||
<div className="mt-12">
|
|
||||||
<h2 className="text-2xl font-bold mb-6">Specifications</h2>
|
|
||||||
<div className="card">
|
|
||||||
<div className="card-body">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
{product.specifications.weight && (
|
|
||||||
<div>
|
|
||||||
<span className="font-semibold">Weight:</span> {product.specifications.weight}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{product.specifications.length && (
|
|
||||||
<div>
|
|
||||||
<span className="font-semibold">Length:</span> {product.specifications.length}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{product.specifications.material && (
|
|
||||||
<div>
|
|
||||||
<span className="font-semibold">Material:</span> {product.specifications.material}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{product.specifications.finish && (
|
|
||||||
<div>
|
|
||||||
<span className="font-semibold">Finish:</span> {product.specifications.finish}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{product.specifications.caliber && (
|
|
||||||
<div>
|
|
||||||
<span className="font-semibold">Caliber:</span> {product.specifications.caliber}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{product.specifications.compatibility && (
|
|
||||||
<div className="md:col-span-2">
|
|
||||||
<span className="font-semibold">Compatibility:</span>
|
|
||||||
<div className="flex flex-wrap gap-2 mt-1">
|
|
||||||
{product.specifications.compatibility.map((comp, index) => (
|
|
||||||
<span key={index} className="badge badge-outline">{comp}</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Vendor Offers */}
|
{/* Vendor Offers */}
|
||||||
<div className="mt-12">
|
<div className="mt-12">
|
||||||
<h2 className="text-2xl font-bold mb-6">Where to Buy</h2>
|
<h2 className="text-2xl font-bold mb-6">Where to Buy</h2>
|
||||||
@@ -291,7 +270,7 @@ export default function ProductDetailsPage() {
|
|||||||
<div>
|
<div>
|
||||||
<div className="font-semibold">{offer.vendor.name}</div>
|
<div className="font-semibold">{offer.vendor.name}</div>
|
||||||
{offer.shipping && (
|
{offer.shipping && (
|
||||||
<div className="text-sm text-neutral-600 dark:text-neutral-400">
|
<div className="text-sm text-zinc-600 dark:text-zinc-400">
|
||||||
{offer.shipping}
|
{offer.shipping}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -323,52 +302,6 @@ export default function ProductDetailsPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Reviews */}
|
|
||||||
{product.reviews && product.reviews.length > 0 && (
|
|
||||||
<div className="mt-12">
|
|
||||||
<h2 className="text-2xl font-bold mb-6">Customer Reviews</h2>
|
|
||||||
<div className="space-y-4">
|
|
||||||
{product.reviews.map((review) => (
|
|
||||||
<div key={review.id} className="card">
|
|
||||||
<div className="card-body">
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{[1, 2, 3, 4, 5].map((star) => (
|
|
||||||
<StarIcon
|
|
||||||
key={star}
|
|
||||||
className={`h-4 w-4 ${
|
|
||||||
star <= review.rating
|
|
||||||
? 'text-yellow-400'
|
|
||||||
: 'text-neutral-300 dark:text-neutral-600'
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-neutral-600 dark:text-neutral-400">
|
|
||||||
{new Date(review.date).toLocaleDateString()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="font-semibold mb-1">{review.user}</div>
|
|
||||||
<p className="text-neutral-700 dark:text-neutral-300">{review.comment}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Compatibility */}
|
|
||||||
{product.compatibility && product.compatibility.length > 0 && (
|
|
||||||
<div className="mt-12">
|
|
||||||
<h2 className="text-2xl font-bold mb-6">Compatible Parts</h2>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{product.compatibility.map((part, index) => (
|
|
||||||
<span key={index} className="badge badge-primary">{part}</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
28
src/app/test-products/page.tsx
Normal file
28
src/app/test-products/page.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export default function TestProductsPage() {
|
||||||
|
const [products, setProducts] = useState<any[]>([]);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch('/api/test-products')
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) setProducts(data.data);
|
||||||
|
else setError(data.error || "Unknown error");
|
||||||
|
})
|
||||||
|
.catch(err => setError(String(err)));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-2xl mx-auto py-10">
|
||||||
|
<h1 className="text-2xl font-bold mb-4">Test Products API</h1>
|
||||||
|
{error && <div className="text-red-500 mb-4">Error: {error}</div>}
|
||||||
|
<pre className="bg-gray-100 p-4 rounded overflow-x-auto text-xs">
|
||||||
|
{JSON.stringify(products, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ export default function Providers({ children }: { children: React.ReactNode }) {
|
|||||||
<SessionProvider>
|
<SessionProvider>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<div className="min-h-screen bg-neutral-50 dark:bg-neutral-900 transition-colors duration-200">
|
<div className="min-h-screen bg-zinc-50 dark:bg-zinc-900 transition-colors duration-200">
|
||||||
<NavigationWrapper />
|
<NavigationWrapper />
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
6
src/db/index.ts
Normal file
6
src/db/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { drizzle } from "drizzle-orm/node-postgres";
|
||||||
|
import { Pool } from "pg";
|
||||||
|
import * as schema from "./schema";
|
||||||
|
|
||||||
|
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
||||||
|
export const db = drizzle(pool, { schema });
|
||||||
558
src/db/schema.ts
Normal file
558
src/db/schema.ts
Normal file
@@ -0,0 +1,558 @@
|
|||||||
|
import { pgTableCreator, integer, varchar, text, numeric, timestamp, unique, check, date, boolean, uuid, bigint, real, doublePrecision, primaryKey, pgView, index, serial } from "drizzle-orm/pg-core";
|
||||||
|
import { relations, sql } from "drizzle-orm";
|
||||||
|
import { DATABASE_PREFIX as prefix } from "@/lib/constants";
|
||||||
|
|
||||||
|
export const pgTable = pgTableCreator((name) => (prefix == "" || prefix == null) ? name: `${prefix}_${name}`);
|
||||||
|
///
|
||||||
|
export const products = pgTable("products", {
|
||||||
|
id: integer().primaryKey().generatedAlwaysAsIdentity({ name: "products_id_seq", startWith: 1, increment: 1, minValue: 1, maxValue: 2147483647, cache: 1 }),
|
||||||
|
name: varchar({ length: 255 }).notNull(),
|
||||||
|
description: text().notNull(),
|
||||||
|
price: numeric().notNull(),
|
||||||
|
resellerId: integer("reseller_id").notNull(),
|
||||||
|
categoryId: integer("category_id").notNull(),
|
||||||
|
stockQty: integer("stock_qty").default(0),
|
||||||
|
updatedAt: timestamp("updated_at", { mode: 'string' }).defaultNow().notNull(),
|
||||||
|
createdAt: timestamp("created_at", { mode: 'string' }).defaultNow().notNull(),
|
||||||
|
deletedAt: timestamp("deleted_at", { mode: 'string' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const categories = pgTable("categories", {
|
||||||
|
id: integer().primaryKey().generatedAlwaysAsIdentity({ name: "categories_id_seq", startWith: 1, increment: 1, minValue: 1, maxValue: 2147483647, cache: 1 }),
|
||||||
|
name: varchar({ length: 100 }).notNull(),
|
||||||
|
parentCategoryId: integer("parent_category_id"),
|
||||||
|
updatedAt: timestamp("updated_at", { mode: 'string' }).defaultNow().notNull(),
|
||||||
|
createdAt: timestamp("created_at", { mode: 'string' }).defaultNow().notNull(),
|
||||||
|
deletedAt: timestamp("deleted_at", { mode: 'string' }),
|
||||||
|
uuid: uuid().defaultRandom(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const productFeeds = pgTable("product_feeds", {
|
||||||
|
id: integer().primaryKey().generatedAlwaysAsIdentity({ name: "productfeeds_id_seq", startWith: 1, increment: 1, minValue: 1, maxValue: 2147483647, cache: 1 }),
|
||||||
|
resellerId: integer("reseller_id").notNull(),
|
||||||
|
feedUrl: varchar("feed_url", { length: 255 }).notNull(),
|
||||||
|
lastUpdate: timestamp("last_update", { mode: 'string' }),
|
||||||
|
updatedAt: timestamp("updated_at", { mode: 'string' }).defaultNow().notNull(),
|
||||||
|
createdAt: timestamp("created_at", { mode: 'string' }).defaultNow().notNull(),
|
||||||
|
deletedAt: timestamp("deleted_at", { mode: 'string' }),
|
||||||
|
uuid: uuid().defaultRandom(),
|
||||||
|
}, (table) => {
|
||||||
|
return {
|
||||||
|
productFeedsUuidUnique: unique("product_feeds_uuid_unique").on(table.uuid),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const userActivityLog = pgTable("user_activity_log", {
|
||||||
|
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||||
|
id: bigint({ mode: "number" }).primaryKey().generatedAlwaysAsIdentity({ name: "user_activity_id_seq", startWith: 1, increment: 1, minValue: 1, maxValue: 2147483647, cache: 1 }),
|
||||||
|
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||||
|
userId: bigint("user_id", { mode: "number" }).notNull(),
|
||||||
|
activity: text().notNull(),
|
||||||
|
timestamp: timestamp({ mode: 'string' }).default(sql`CURRENT_TIMESTAMP`),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const brands = pgTable("brands", {
|
||||||
|
id: integer().primaryKey().generatedAlwaysAsIdentity({ name: "brands_id_seq", startWith: 1, increment: 1, minValue: 1, maxValue: 2147483647, cache: 1 }),
|
||||||
|
name: varchar({ length: 100 }).notNull(),
|
||||||
|
updatedAt: timestamp("updated_at", { mode: 'string' }).defaultNow().notNull(),
|
||||||
|
createdAt: timestamp("created_at", { mode: 'string' }).defaultNow().notNull(),
|
||||||
|
deletedAt: timestamp("deleted_at", { mode: 'string' }),
|
||||||
|
uuid: uuid().defaultRandom(),
|
||||||
|
}, (table) => {
|
||||||
|
return {
|
||||||
|
brandsUuidUnique: unique("brands_uuid_unique").on(table.uuid),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export const manufacturer = pgTable("manufacturer", {
|
||||||
|
id: integer().primaryKey().generatedAlwaysAsIdentity({ name: "manufacturer_id_seq", startWith: 1, increment: 1, minValue: 1, maxValue: 2147483647, cache: 1 }),
|
||||||
|
name: varchar({ length: 100 }).notNull(),
|
||||||
|
updatedAt: timestamp("updated_at", { mode: 'string' }).defaultNow().notNull(),
|
||||||
|
createdAt: timestamp("created_at", { mode: 'string' }).defaultNow().notNull(),
|
||||||
|
deletedAt: timestamp("deleted_at", { mode: 'string' }),
|
||||||
|
uuid: uuid().defaultRandom(),
|
||||||
|
}, (table) => {
|
||||||
|
return {
|
||||||
|
manufacturerUuidUnique: unique("manufacturer_uuid_unique").on(table.uuid),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const states = pgTable("states", {
|
||||||
|
id: integer().primaryKey().generatedByDefaultAsIdentity({ name: "states_id_seq", startWith: 1, increment: 1, minValue: 1, maxValue: 2147483647, cache: 1 }),
|
||||||
|
state: varchar({ length: 50 }),
|
||||||
|
abbreviation: varchar({ length: 50 }),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const componentType = pgTable("component_type", {
|
||||||
|
id: integer().primaryKey().generatedAlwaysAsIdentity({ name: "component_type_id_seq", startWith: 1, increment: 1, minValue: 1, maxValue: 2147483647, cache: 1 }),
|
||||||
|
name: varchar({ length: 100 }).notNull(),
|
||||||
|
updatedAt: timestamp("updated_at", { mode: 'string' }).defaultNow().notNull(),
|
||||||
|
createdAt: timestamp("created_at", { mode: 'string' }).defaultNow().notNull(),
|
||||||
|
deletedAt: timestamp("deleted_at", { mode: 'string' }),
|
||||||
|
uuid: uuid().defaultRandom(),
|
||||||
|
}, (table) => {
|
||||||
|
return {
|
||||||
|
componentTypeUuidUnique: unique("component_type_uuid_unique").on(table.uuid),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const aeroPrecision = pgTable("aero_precision", {
|
||||||
|
sku: text().primaryKey().notNull(),
|
||||||
|
manufacturerId: text("manufacturer_id"),
|
||||||
|
brandName: text("brand_name"),
|
||||||
|
productName: text("product_name"),
|
||||||
|
longDescription: text("long_description"),
|
||||||
|
shortDescription: text("short_description"),
|
||||||
|
department: text(),
|
||||||
|
category: text(),
|
||||||
|
subcategory: text(),
|
||||||
|
thumbUrl: text("thumb_url"),
|
||||||
|
imageUrl: text("image_url"),
|
||||||
|
buyLink: text("buy_link"),
|
||||||
|
keywords: text(),
|
||||||
|
reviews: text(),
|
||||||
|
retailPrice: numeric("retail_price"),
|
||||||
|
salePrice: numeric("sale_price"),
|
||||||
|
brandPageLink: text("brand_page_link"),
|
||||||
|
brandLogoImage: text("brand_logo_image"),
|
||||||
|
productPageViewTracking: text("product_page_view_tracking"),
|
||||||
|
variantsXml: text("variants_xml"),
|
||||||
|
mediumImageUrl: text("medium_image_url"),
|
||||||
|
productContentWidget: text("product_content_widget"),
|
||||||
|
googleCategorization: text("google_categorization"),
|
||||||
|
itemBasedCommission: text("item_based_commission"),
|
||||||
|
uuid: uuid().defaultRandom(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const compartment = pgTable("compartment", {
|
||||||
|
id: uuid().defaultRandom().primaryKey().notNull(),
|
||||||
|
name: varchar({ length: 100 }).notNull(),
|
||||||
|
description: varchar({ length: 300 }),
|
||||||
|
updatedAt: timestamp("updated_at", { mode: 'string' }).defaultNow().notNull(),
|
||||||
|
createdAt: timestamp("created_at", { mode: 'string' }).defaultNow().notNull(),
|
||||||
|
deletedAt: timestamp("deleted_at", { mode: 'string' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const builds = pgTable("builds", {
|
||||||
|
id: integer().primaryKey().generatedAlwaysAsIdentity({ name: "build_id_seq", startWith: 1, increment: 1, minValue: 1, maxValue: 2147483647, cache: 1 }),
|
||||||
|
accountId: integer("account_id").notNull(),
|
||||||
|
name: varchar({ length: 255 }).notNull(),
|
||||||
|
description: text(),
|
||||||
|
updatedAt: timestamp("updated_at", { mode: 'string' }).defaultNow().notNull(),
|
||||||
|
createdAt: timestamp("created_at", { mode: 'string' }).defaultNow().notNull(),
|
||||||
|
deletedAt: timestamp("deleted_at", { mode: 'string' }),
|
||||||
|
uuid: uuid().defaultRandom(),
|
||||||
|
}, (table) => {
|
||||||
|
return {
|
||||||
|
buildsUuidUnique: unique("builds_uuid_unique").on(table.uuid),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const bb_products = pgTable("bb_products", {
|
||||||
|
uuid: uuid().defaultRandom().primaryKey().notNull(),
|
||||||
|
upc: varchar("UPC", { length: 100 }),
|
||||||
|
sku: varchar("SKU", { length: 50 }),
|
||||||
|
manufacturerId: varchar("MANUFACTURER_ID", { length: 50 }),
|
||||||
|
brandName: varchar("BRAND_NAME", { length: 50 }),
|
||||||
|
productName: varchar("PRODUCT_NAME", { length: 255 }),
|
||||||
|
longDescription: text("LONG_DESCRIPTION"),
|
||||||
|
shortDescription: varchar("SHORT_DESCRIPTION", { length: 500 }),
|
||||||
|
department: varchar("DEPARTMENT", { length: 100 }),
|
||||||
|
category: varchar("CATEGORY", { length: 100 }),
|
||||||
|
subcategory: varchar("SUBCATEGORY", { length: 100 }),
|
||||||
|
thumbUrl: varchar("THUMB_URL", { length: 500 }),
|
||||||
|
imageUrl: varchar("IMAGE_URL", { length: 500 }),
|
||||||
|
buyLink: varchar("BUY_LINK", { length: 500 }),
|
||||||
|
keywords: varchar("KEYWORDS", { length: 500 }),
|
||||||
|
reviews: varchar("REVIEWS", { length: 500 }),
|
||||||
|
retailPrice: varchar("RETAIL_PRICE", { length: 50 }),
|
||||||
|
salePrice: varchar("SALE_PRICE", { length: 50 }),
|
||||||
|
brandPageLink: varchar("BRAND_PAGE_LINK", { length: 500 }),
|
||||||
|
brandLogoImage: varchar("BRAND_LOGO_IMAGE", { length: 500 }),
|
||||||
|
productPageViewTracking: varchar("PRODUCT_PAGE_VIEW_TRACKING", { length: 500 }),
|
||||||
|
parentGroupId: varchar("PARENT_GROUP_ID", { length: 200 }),
|
||||||
|
fineline: varchar("FINELINE", { length: 200 }),
|
||||||
|
superfineline: varchar("SUPERFINELINE", { length: 200 }),
|
||||||
|
modelnumber: varchar("MODELNUMBER", { length: 100 }),
|
||||||
|
caliber: varchar("CALIBER", { length: 200 }),
|
||||||
|
mediumImageUrl: varchar("MEDIUM_IMAGE_URL", { length: 500 }),
|
||||||
|
productContentWidget: varchar("PRODUCT_CONTENT_WIDGET", { length: 500 }),
|
||||||
|
googleCategorization: varchar("GOOGLE_CATEGORIZATION", { length: 500 }),
|
||||||
|
itemBasedCommission: varchar("ITEM_BASED_COMMISSION", { length: 500 }),
|
||||||
|
itemBasedCommissionRate: varchar("ITEM_BASED_COMMISSION RATE", { length: 50 }),
|
||||||
|
updatedAt: timestamp("updated_at", { mode: 'string' }).defaultNow().notNull(),
|
||||||
|
createdAt: timestamp("created_at", { mode: 'string' }).defaultNow().notNull(),
|
||||||
|
deletedAt: timestamp("deleted_at", { mode: 'string' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const psa_old = pgTable("psa_old", {
|
||||||
|
sku: varchar("SKU", { length: 50 }),
|
||||||
|
manufacturerId: varchar("MANUFACTURER_ID", { length: 50 }),
|
||||||
|
brandName: varchar("BRAND_NAME", { length: 50 }),
|
||||||
|
productName: varchar("PRODUCT_NAME", { length: 255 }),
|
||||||
|
longDescription: text("LONG_DESCRIPTION"),
|
||||||
|
shortDescription: varchar("SHORT_DESCRIPTION", { length: 50 }),
|
||||||
|
department: varchar("DEPARTMENT", { length: 50 }),
|
||||||
|
category: varchar("CATEGORY", { length: 50 }),
|
||||||
|
subcategory: varchar("SUBCATEGORY", { length: 50 }),
|
||||||
|
thumbUrl: varchar("THUMB_URL", { length: 50 }),
|
||||||
|
imageUrl: varchar("IMAGE_URL", { length: 50 }),
|
||||||
|
buyLink: varchar("BUY_LINK", { length: 128 }),
|
||||||
|
keywords: varchar("KEYWORDS", { length: 50 }),
|
||||||
|
reviews: varchar("REVIEWS", { length: 50 }),
|
||||||
|
retailPrice: real("RETAIL_PRICE"),
|
||||||
|
salePrice: real("SALE_PRICE"),
|
||||||
|
brandPageLink: varchar("BRAND_PAGE_LINK", { length: 50 }),
|
||||||
|
brandLogoImage: varchar("BRAND_LOGO_IMAGE", { length: 50 }),
|
||||||
|
productPageViewTracking: varchar("PRODUCT_PAGE_VIEW_TRACKING", { length: 256 }),
|
||||||
|
parentGroupId: varchar("PARENT_GROUP_ID", { length: 50 }),
|
||||||
|
fineline: varchar("FINELINE", { length: 50 }),
|
||||||
|
superfineline: varchar("SUPERFINELINE", { length: 200 }),
|
||||||
|
modelnumber: varchar("MODELNUMBER", { length: 50 }),
|
||||||
|
caliber: varchar("CALIBER", { length: 200 }),
|
||||||
|
upc: varchar("UPC", { length: 100 }),
|
||||||
|
mediumImageUrl: varchar("MEDIUM_IMAGE_URL", { length: 50 }),
|
||||||
|
productContentWidget: varchar("PRODUCT_CONTENT_WIDGET", { length: 256 }),
|
||||||
|
googleCategorization: varchar("GOOGLE_CATEGORIZATION", { length: 50 }),
|
||||||
|
itemBasedCommission: varchar("ITEM_BASED_COMMISSION", { length: 50 }),
|
||||||
|
uuid: uuid().defaultRandom(),
|
||||||
|
});
|
||||||
|
export const psa = pgTable("psa", {
|
||||||
|
id: integer().primaryKey().generatedAlwaysAsIdentity({ name: "psa_id_seq", startWith: 1, increment: 1, minValue: 1, maxValue: 2147483647, cache: 1 }),
|
||||||
|
sku: varchar("SKU", { length: 50 }),
|
||||||
|
manufacturerId: varchar("MANUFACTURER_ID", { length: 50 }),
|
||||||
|
brandName: varchar("BRAND_NAME", { length: 50 }),
|
||||||
|
productName: varchar("PRODUCT_NAME", { length: 255 }),
|
||||||
|
longDescription: text("LONG_DESCRIPTION"),
|
||||||
|
shortDescription: varchar("SHORT_DESCRIPTION", { length: 50 }),
|
||||||
|
department: varchar("DEPARTMENT", { length: 50 }),
|
||||||
|
category: varchar("CATEGORY", { length: 50 }),
|
||||||
|
subcategory: varchar("SUBCATEGORY", { length: 50 }),
|
||||||
|
thumbUrl: varchar("THUMB_URL", { length: 50 }),
|
||||||
|
imageUrl: varchar("IMAGE_URL", { length: 50 }),
|
||||||
|
buyLink: varchar("BUY_LINK", { length: 128 }),
|
||||||
|
keywords: varchar("KEYWORDS", { length: 50 }),
|
||||||
|
reviews: varchar("REVIEWS", { length: 50 }),
|
||||||
|
retailPrice: real("RETAIL_PRICE"),
|
||||||
|
salePrice: real("SALE_PRICE"),
|
||||||
|
brandPageLink: varchar("BRAND_PAGE_LINK", { length: 50 }),
|
||||||
|
brandLogoImage: varchar("BRAND_LOGO_IMAGE", { length: 50 }),
|
||||||
|
productPageViewTracking: varchar("PRODUCT_PAGE_VIEW_TRACKING", { length: 256 }),
|
||||||
|
parentGroupId: varchar("PARENT_GROUP_ID", { length: 50 }),
|
||||||
|
fineline: varchar("FINELINE", { length: 50 }),
|
||||||
|
superfineline: varchar("SUPERFINELINE", { length: 200 }),
|
||||||
|
modelnumber: varchar("MODELNUMBER", { length: 50 }),
|
||||||
|
caliber: varchar("CALIBER", { length: 200 }),
|
||||||
|
upc: varchar("UPC", { length: 100 }),
|
||||||
|
mediumImageUrl: varchar("MEDIUM_IMAGE_URL", { length: 50 }),
|
||||||
|
productContentWidget: varchar("PRODUCT_CONTENT_WIDGET", { length: 256 }),
|
||||||
|
googleCategorization: varchar("GOOGLE_CATEGORIZATION", { length: 50 }),
|
||||||
|
itemBasedCommission: varchar("ITEM_BASED_COMMISSION", { length: 50 }),
|
||||||
|
uuid: uuid().defaultRandom(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const lipseycatalog = pgTable("lipseycatalog", {
|
||||||
|
id: integer().primaryKey().generatedAlwaysAsIdentity({ name: "lipseycatalog_id_seq", startWith: 1, increment: 1, minValue: 1, maxValue: 2147483647, cache: 1 }),
|
||||||
|
itemno: varchar({ length: 20 }).notNull(),
|
||||||
|
description1: text(),
|
||||||
|
description2: text(),
|
||||||
|
upc: varchar({ length: 20 }),
|
||||||
|
manufacturermodelno: varchar({ length: 30 }),
|
||||||
|
msrp: doublePrecision(),
|
||||||
|
model: text(),
|
||||||
|
calibergauge: text(),
|
||||||
|
manufacturer: text(),
|
||||||
|
type: text(),
|
||||||
|
action: text(),
|
||||||
|
barrellength: text(),
|
||||||
|
capacity: text(),
|
||||||
|
finish: text(),
|
||||||
|
overalllength: text(),
|
||||||
|
receiver: text(),
|
||||||
|
safety: text(),
|
||||||
|
sights: text(),
|
||||||
|
stockframegrips: text(),
|
||||||
|
magazine: text(),
|
||||||
|
weight: text(),
|
||||||
|
imagename: text(),
|
||||||
|
chamber: text(),
|
||||||
|
drilledandtapped: text(),
|
||||||
|
rateoftwist: text(),
|
||||||
|
itemtype: text(),
|
||||||
|
additionalfeature1: text(),
|
||||||
|
additionalfeature2: text(),
|
||||||
|
additionalfeature3: text(),
|
||||||
|
shippingweight: text(),
|
||||||
|
boundbookmanufacturer: text(),
|
||||||
|
boundbookmodel: text(),
|
||||||
|
boundbooktype: text(),
|
||||||
|
nfathreadpattern: text(),
|
||||||
|
nfaattachmentmethod: text(),
|
||||||
|
nfabaffletype: text(),
|
||||||
|
silencercanbedisassembled: text(),
|
||||||
|
silencerconstructionmaterial: text(),
|
||||||
|
nfadbreduction: text(),
|
||||||
|
silenceroutsidediameter: text(),
|
||||||
|
nfaform3Caliber: text(),
|
||||||
|
opticmagnification: text(),
|
||||||
|
maintubesize: text(),
|
||||||
|
adjustableobjective: text(),
|
||||||
|
objectivesize: text(),
|
||||||
|
opticadjustments: text(),
|
||||||
|
illuminatedreticle: text(),
|
||||||
|
reticle: text(),
|
||||||
|
exclusive: text(),
|
||||||
|
quantity: varchar({ length: 10 }).default(sql`NULL`),
|
||||||
|
allocated: text(),
|
||||||
|
onsale: text(),
|
||||||
|
price: doublePrecision(),
|
||||||
|
currentprice: doublePrecision(),
|
||||||
|
retailmap: doublePrecision(),
|
||||||
|
fflrequired: text(),
|
||||||
|
sotrequired: text(),
|
||||||
|
exclusivetype: text(),
|
||||||
|
scopecoverincluded: text(),
|
||||||
|
special: text(),
|
||||||
|
sightstype: text(),
|
||||||
|
case: text(),
|
||||||
|
choke: text(),
|
||||||
|
dbreduction: text(),
|
||||||
|
family: text(),
|
||||||
|
finishtype: text(),
|
||||||
|
frame: text(),
|
||||||
|
griptype: varchar({ length: 30 }),
|
||||||
|
handgunslidematerial: text(),
|
||||||
|
countryoforigin: varchar({ length: 4 }),
|
||||||
|
itemlength: text(),
|
||||||
|
itemwidth: text(),
|
||||||
|
itemheight: text(),
|
||||||
|
packagelength: doublePrecision(),
|
||||||
|
packagewidth: doublePrecision(),
|
||||||
|
packageheight: doublePrecision(),
|
||||||
|
itemgroup: varchar({ length: 40 }),
|
||||||
|
updatedAt: timestamp("updated_at", { mode: 'string' }).defaultNow().notNull(),
|
||||||
|
createdAt: timestamp("created_at", { mode: 'string' }).defaultNow().notNull(),
|
||||||
|
deletedAt: timestamp("deleted_at", { mode: 'string' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const buildsComponents = pgTable("builds_components", {
|
||||||
|
id: integer().primaryKey().generatedAlwaysAsIdentity({ name: "build_components_id_seq", startWith: 1, increment: 1, minValue: 1, maxValue: 2147483647, cache: 1 }),
|
||||||
|
buildId: integer("build_id").notNull(),
|
||||||
|
productId: integer("product_id").notNull(),
|
||||||
|
updatedAt: timestamp("updated_at", { mode: 'string' }).defaultNow().notNull(),
|
||||||
|
createdAt: timestamp("created_at", { mode: 'string' }).defaultNow().notNull(),
|
||||||
|
deletedAt: timestamp("deleted_at", { mode: 'string' }),
|
||||||
|
uuid: uuid().defaultRandom(),
|
||||||
|
}, (table) => {
|
||||||
|
return {
|
||||||
|
buildsComponentsUuidUnique: unique("builds_components_uuid_unique").on(table.uuid),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const balResellers = pgTable("bal_resellers", {
|
||||||
|
id: integer().primaryKey().generatedAlwaysAsIdentity({ name: "resellers_id_seq", startWith: 1, increment: 1, minValue: 1, maxValue: 2147483647, cache: 1 }),
|
||||||
|
name: varchar({ length: 100 }).notNull(),
|
||||||
|
websiteUrl: varchar("website_url", { length: 255 }),
|
||||||
|
contactEmail: varchar("contact_email", { length: 100 }),
|
||||||
|
updatedAt: timestamp("updated_at", { mode: 'string' }).defaultNow().notNull(),
|
||||||
|
createdAt: timestamp("created_at", { mode: 'string' }).defaultNow().notNull(),
|
||||||
|
deletedAt: timestamp("deleted_at", { mode: 'string' }),
|
||||||
|
uuid: uuid().defaultRandom(),
|
||||||
|
}, (table) => {
|
||||||
|
return {
|
||||||
|
balResellersUuidUnique: unique("bal_resellers_uuid_unique").on(table.uuid),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const verificationTokens = pgTable("verificationTokens", {
|
||||||
|
identifier: varchar("identifier").notNull(),
|
||||||
|
token: varchar("token").notNull(),
|
||||||
|
expires: timestamp("expires").notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const authenticator = pgTable("authenticator", {
|
||||||
|
credentialId: text().notNull(),
|
||||||
|
userId: text().notNull(),
|
||||||
|
providerAccountId: text().notNull(),
|
||||||
|
credentialPublicKey: text().notNull(),
|
||||||
|
counter: integer().notNull(),
|
||||||
|
credentialDeviceType: text().notNull(),
|
||||||
|
credentialBackedUp: boolean().notNull(),
|
||||||
|
transports: text(),
|
||||||
|
}, (table) => {
|
||||||
|
return {
|
||||||
|
authenticatorUserIdCredentialIdPk: primaryKey({ columns: [table.credentialId, table.userId], name: "authenticator_userId_credentialID_pk"}),
|
||||||
|
authenticatorCredentialIdUnique: unique("authenticator_credentialID_unique").on(table.credentialId),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const accounts = pgTable("accounts", {
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
uuid: uuid("uuid").defaultRandom(),
|
||||||
|
userId: uuid("user_id").notNull(),
|
||||||
|
type: varchar("type").notNull(),
|
||||||
|
provider: text().notNull(),
|
||||||
|
providerAccountId: varchar("provider_account_id").notNull(),
|
||||||
|
refreshToken: text("refresh_token"),
|
||||||
|
accessToken: text("access_token"),
|
||||||
|
expiresAt: integer("expires_at"),
|
||||||
|
tokenType: varchar("token_type"),
|
||||||
|
idToken: text("id_token"),
|
||||||
|
sessionState: varchar("session_state"),
|
||||||
|
scope: text(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/* export const vw_accounts = pgView("vw_accounts", {
|
||||||
|
uuid: uuid().defaultRandom(),
|
||||||
|
userId: text("user_id").notNull(),
|
||||||
|
type: text().notNull(),
|
||||||
|
provider: text().notNull(),
|
||||||
|
providerAccountId: text("provider_account_id").notNull(),
|
||||||
|
refreshToken: text("refresh_token"),
|
||||||
|
accessToken: text("access_token"),
|
||||||
|
expiresAt: integer("expires_at"),
|
||||||
|
tokenType: text("token_type"),
|
||||||
|
scope: text(),
|
||||||
|
idToken: text("id_token"),
|
||||||
|
sessionState: text("session_state"),
|
||||||
|
first_name: text("first_name"),
|
||||||
|
last_name: text("last_name"),
|
||||||
|
|
||||||
|
},) */
|
||||||
|
|
||||||
|
/* From here down is the authentication library Lusia tables */
|
||||||
|
|
||||||
|
export const users = pgTable("users",
|
||||||
|
{
|
||||||
|
id: varchar("id", { length: 21 }).primaryKey(),
|
||||||
|
name: varchar("name"),
|
||||||
|
username: varchar({ length: 50 }),
|
||||||
|
discordId: varchar("discord_id", { length: 255 }).unique(),
|
||||||
|
email: varchar("email", { length: 255 }).unique().notNull(),
|
||||||
|
emailVerified: boolean("email_verified").default(false).notNull(),
|
||||||
|
hashedPassword: varchar("hashed_password", { length: 255 }),
|
||||||
|
first_name: varchar("first_name", { length: 50 }),
|
||||||
|
last_name: varchar("last_name", { length: 50 }),
|
||||||
|
full_name: varchar("full_name", { length: 50 }),
|
||||||
|
profilePicture: varchar("profile_picture", { length: 255 }),
|
||||||
|
image: text("image"),
|
||||||
|
dateOfBirth: date("date_of_birth"),
|
||||||
|
phoneNumber: varchar("phone_number", { length: 20 }),
|
||||||
|
createdAt: timestamp("created_at", { mode: 'string' }).default(sql`CURRENT_TIMESTAMP`),
|
||||||
|
updatedAt: timestamp("updated_at", { mode: 'string' }).default(sql`CURRENT_TIMESTAMP`),
|
||||||
|
isAdmin: boolean("is_admin").default(false),
|
||||||
|
lastLogin: timestamp("last_login", { mode: 'string' }),
|
||||||
|
buildPrivacySetting: text("build_privacy_setting").default('public'),
|
||||||
|
uuid: uuid().defaultRandom(),
|
||||||
|
avatar: varchar("avatar", { length: 255 }),
|
||||||
|
stripeSubscriptionId: varchar("stripe_subscription_id", { length: 191 }),
|
||||||
|
stripePriceId: varchar("stripe_price_id", { length: 191 }),
|
||||||
|
stripeCustomerId: varchar("stripe_customer_id", { length: 191 }),
|
||||||
|
stripeCurrentPeriodEnd: timestamp("stripe_current_period_end"),
|
||||||
|
}, (table) => ({
|
||||||
|
usersUsernameKey: unique("users_username_key").on(table.username),
|
||||||
|
usersEmailKey: unique("users_email_key").on(table.email),
|
||||||
|
usersBuildPrivacySettingCheck: check("users_build_privacy_setting_check", sql`build_privacy_setting = ANY (ARRAY['private'::text, 'public'::text])`),
|
||||||
|
emailIdx: index("user_email_idx").on(table.email),
|
||||||
|
discordIdx: index("user_discord_idx").on(table.discordId),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
export type User = typeof users.$inferSelect;
|
||||||
|
export type NewUser = typeof users.$inferInsert;
|
||||||
|
|
||||||
|
export const sessions = pgTable(
|
||||||
|
"sessions",
|
||||||
|
{
|
||||||
|
id: varchar("id", { length: 255 }).primaryKey(),
|
||||||
|
userId: varchar("user_id", { length: 21 }).notNull(),
|
||||||
|
expiresAt: timestamp("expires_at", { 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) => ({
|
||||||
|
userIdx: index("session_user_idx").on(t.userId),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const emailVerificationCodes = pgTable(
|
||||||
|
"email_verification_codes",
|
||||||
|
{
|
||||||
|
id: serial("id").primaryKey(),
|
||||||
|
userId: varchar("user_id", { length: 21 }).unique().notNull(),
|
||||||
|
email: varchar("email", { length: 255 }).notNull(),
|
||||||
|
code: varchar("code", { length: 8 }).notNull(),
|
||||||
|
expiresAt: timestamp("expires_at", { withTimezone: true, mode: "date" }).notNull(),
|
||||||
|
},
|
||||||
|
(t) => ({
|
||||||
|
userIdx: index("verification_code_user_idx").on(t.userId),
|
||||||
|
emailIdx: index("verification_code_email_idx").on(t.email),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const passwordResetTokens = pgTable(
|
||||||
|
"password_reset_tokens",
|
||||||
|
{
|
||||||
|
id: varchar("id", { length: 40 }).primaryKey(),
|
||||||
|
userId: varchar("user_id", { length: 21 }).notNull(),
|
||||||
|
expiresAt: timestamp("expires_at", { withTimezone: true, mode: "date" }).notNull(),
|
||||||
|
},
|
||||||
|
(t) => ({
|
||||||
|
userIdx: index("password_token_user_idx").on(t.userId),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const posts = pgTable(
|
||||||
|
"posts",
|
||||||
|
{
|
||||||
|
id: varchar("id", { length: 15 }).primaryKey(),
|
||||||
|
userId: varchar("user_id", { length: 255 }).notNull(),
|
||||||
|
title: varchar("title", { length: 255 }).notNull(),
|
||||||
|
excerpt: varchar("excerpt", { length: 255 }).notNull(),
|
||||||
|
content: text("content").notNull(),
|
||||||
|
status: varchar("status", { length: 10, enum: ["draft", "published"] })
|
||||||
|
.default("draft")
|
||||||
|
.notNull(),
|
||||||
|
tags: varchar("tags", { length: 255 }),
|
||||||
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||||
|
updatedAt: timestamp("updated_at", { mode: "date" }).$onUpdate(() => new Date()),
|
||||||
|
},
|
||||||
|
(t) => ({
|
||||||
|
userIdx: index("post_user_idx").on(t.userId),
|
||||||
|
createdAtIdx: index("post_created_at_idx").on(t.createdAt),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const postRelations = relations(posts, ({ one }) => ({
|
||||||
|
user: one(users, {
|
||||||
|
fields: [posts.userId],
|
||||||
|
references: [users.id],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export type Post = typeof posts.$inferSelect;
|
||||||
|
export type NewPost = typeof posts.$inferInsert;
|
||||||
|
|
||||||
|
export const vwUserSessions = pgView("vw_user_sessions", { id: varchar({ length: 255 }),
|
||||||
|
userId: varchar("user_id", { length: 21 }),
|
||||||
|
uId: varchar("u_id", { length: 21 }),
|
||||||
|
uEmail: varchar("u_email", { length: 255 }),
|
||||||
|
expiresAt: timestamp("expires_at", { withTimezone: true, mode: 'string' }),
|
||||||
|
createdAt: timestamp("created_at", { mode: 'string' }),
|
||||||
|
updatedAt: timestamp("updated_at", { mode: 'string' }),
|
||||||
|
}).existing();
|
||||||
|
//as(sql`SELECT s.id, s.user_id, u.id AS u_id, u.email AS u_email, s.expires_at, s.created_at, s.updated_at FROM sessions s, users u WHERE s.user_id::text = u.id::text`);
|
||||||
|
|
||||||
|
// Default Drizzle File
|
||||||
|
|
||||||
|
// import { pgTable, serial, text, integer, timestamp } from "drizzle-orm/pg-core";
|
||||||
|
|
||||||
|
// export const products = pgTable("products", {
|
||||||
|
// id: serial("id").primaryKey(),
|
||||||
|
// name: text("name").notNull(),
|
||||||
|
// description: text("description"),
|
||||||
|
// price: integer("price"),
|
||||||
|
// createdAt: timestamp("created_at").defaultNow(),
|
||||||
|
// // Add more fields as needed
|
||||||
|
// });
|
||||||
40
src/lib/constants.ts
Normal file
40
src/lib/constants.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
|
||||||
|
const constants = {
|
||||||
|
APP_NAME: 'Ballistic Builder',
|
||||||
|
SITE_NAME: 'Ballistic Builder',
|
||||||
|
COMPANY_NAME: 'Forward Group, LLC',
|
||||||
|
COMPANY_URL: 'https://goforward.group',
|
||||||
|
AUTHOR: 'Forward Group, LLC',
|
||||||
|
META_KEYWORDS: 'Pew Pew',
|
||||||
|
META_DESCRIPTION: 'Pow Pow',
|
||||||
|
DESCRIPTION: 'Developed by Forward Group, LLC',
|
||||||
|
PJAM_RAINIER: 'https://api.pepperjamnetwork.com/20120402/publisher/creative/product?apiKey=17c11367569cc10dce51e6a5900d0c7c8b390c9cb2d2cecc25b3ed53a3b8649b&format=json&programIds=8713',
|
||||||
|
PJAM_BARRETTA: 'https://api.pepperjamnetwork.com/20120402/publisher/creative/product?apiKey=17c11367569cc10dce51e6a5900d0c7c8b390c9cb2d2cecc25b3ed53a3b8649b&format=json&programIds=8342'
|
||||||
|
};
|
||||||
|
|
||||||
|
export default constants;
|
||||||
|
|
||||||
|
|
||||||
|
export enum SITE_CONT_TYPE {
|
||||||
|
CONTACTUS = "CONTACTUS",
|
||||||
|
PRIVACYPOLICY = "PP",
|
||||||
|
PERSONALINFOPOLICY = "PIP",
|
||||||
|
FAQ = "FAQ",
|
||||||
|
TERMSOFSERVICE = "TOS",
|
||||||
|
ABOUTUS="ABOUTUS",
|
||||||
|
DISCLOSURE="DISCLOSURE"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const APP_TITLE = "Ballistics Builder";
|
||||||
|
export const DATABASE_PREFIX = "";
|
||||||
|
export const TEST_DB_PREFIX = "test_acme";
|
||||||
|
export const EMAIL_SENDER = '"Ballistics Builder" <don@goforward.group>';
|
||||||
|
|
||||||
|
export enum Paths {
|
||||||
|
Home = "/",
|
||||||
|
Login = "/login",
|
||||||
|
Signup = "/signup",
|
||||||
|
Dashboard = "/dashboard",
|
||||||
|
VerifyEmail = "/verify-email",
|
||||||
|
ResetPassword = "/reset-password",
|
||||||
|
}
|
||||||
@@ -1,120 +1,8 @@
|
|||||||
console.log('DaisyUI plugin loaded');
|
|
||||||
/** @type {import('tailwindcss').Config} */
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
content: [
|
content: [
|
||||||
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
|
"./src/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
|
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
|
|
||||||
],
|
],
|
||||||
darkMode: 'class',
|
theme: { extend: {} },
|
||||||
theme: {
|
plugins: [],
|
||||||
extend: {
|
}
|
||||||
colors: {
|
|
||||||
// Secondary accent colors
|
|
||||||
accent: {
|
|
||||||
50: '#fef2f2',
|
|
||||||
100: '#fee2e2',
|
|
||||||
200: '#fecaca',
|
|
||||||
300: '#fca5a5',
|
|
||||||
400: '#f87171',
|
|
||||||
500: '#ef4444',
|
|
||||||
600: '#dc2626',
|
|
||||||
700: '#b91c1c',
|
|
||||||
800: '#991b1b',
|
|
||||||
900: '#7f1d1d',
|
|
||||||
950: '#450a0a',
|
|
||||||
},
|
|
||||||
// Neutral grays with warm undertones
|
|
||||||
neutral: {
|
|
||||||
50: '#fafafa',
|
|
||||||
100: '#f5f5f5',
|
|
||||||
200: '#e5e5e5',
|
|
||||||
300: '#d4d4d4',
|
|
||||||
400: '#a3a3a3',
|
|
||||||
500: '#737373',
|
|
||||||
600: '#525252',
|
|
||||||
700: '#404040',
|
|
||||||
800: '#262626',
|
|
||||||
900: '#171717',
|
|
||||||
950: '#0a0a0a',
|
|
||||||
},
|
|
||||||
// Success colors
|
|
||||||
success: {
|
|
||||||
50: '#f0fdf4',
|
|
||||||
100: '#dcfce7',
|
|
||||||
200: '#bbf7d0',
|
|
||||||
300: '#86efac',
|
|
||||||
400: '#4ade80',
|
|
||||||
500: '#22c55e',
|
|
||||||
600: '#16a34a',
|
|
||||||
700: '#15803d',
|
|
||||||
800: '#166534',
|
|
||||||
900: '#14532d',
|
|
||||||
950: '#052e16',
|
|
||||||
},
|
|
||||||
// Warning colors
|
|
||||||
warning: {
|
|
||||||
50: '#fffbeb',
|
|
||||||
100: '#fef3c7',
|
|
||||||
200: '#fde68a',
|
|
||||||
300: '#fcd34d',
|
|
||||||
400: '#fbbf24',
|
|
||||||
500: '#f59e0b',
|
|
||||||
600: '#d97706',
|
|
||||||
700: '#b45309',
|
|
||||||
800: '#92400e',
|
|
||||||
900: '#78350f',
|
|
||||||
950: '#451a03',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fontFamily: {
|
|
||||||
sans: ['Inter', 'system-ui', 'sans-serif'],
|
|
||||||
display: ['Inter', 'system-ui', 'sans-serif'],
|
|
||||||
},
|
|
||||||
animation: {
|
|
||||||
'fade-in': 'fadeIn 0.5s ease-in-out',
|
|
||||||
'slide-up': 'slideUp 0.3s ease-out',
|
|
||||||
},
|
|
||||||
keyframes: {
|
|
||||||
fadeIn: {
|
|
||||||
'0%': { opacity: '0' },
|
|
||||||
'100%': { opacity: '1' },
|
|
||||||
},
|
|
||||||
slideUp: {
|
|
||||||
'0%': { transform: 'translateY(10px)', opacity: '0' },
|
|
||||||
'100%': { transform: 'translateY(0)', opacity: '1' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: [require('daisyui')],
|
|
||||||
daisyui: {
|
|
||||||
themes: [
|
|
||||||
{
|
|
||||||
pew: {
|
|
||||||
primary: '#4B6516', // Olive/army green
|
|
||||||
'primary-content': '#fff',
|
|
||||||
accent: '#181C20', // Dark navy for CTA/footer
|
|
||||||
'accent-content': '#fff',
|
|
||||||
neutral: '#222',
|
|
||||||
'base-100': '#fff',
|
|
||||||
'base-200': '#f5f6fa',
|
|
||||||
'base-300': '#e5e7eb',
|
|
||||||
info: '#3ABFF8',
|
|
||||||
success: '#36D399',
|
|
||||||
warning: '#FBBD23',
|
|
||||||
error: '#F87272',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'dark',
|
|
||||||
],
|
|
||||||
darkTheme: "dark",
|
|
||||||
base: true,
|
|
||||||
styled: true,
|
|
||||||
utils: true,
|
|
||||||
logs: false,
|
|
||||||
rtl: false,
|
|
||||||
prefix: '',
|
|
||||||
// 'pew' is the default theme
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|||||||
Reference in New Issue
Block a user