diff --git a/next.config.ts b/next.config.ts
index 0904350..862135b 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -2,14 +2,7 @@ import type { NextConfig } from "next";
const nextConfig: NextConfig = {
images: {
- remotePatterns: [
- {
- protocol: 'https',
- hostname: 'placehold.co',
- port: '',
- pathname: '/**',
- },
- ],
+ remotePatterns: [],
},
};
diff --git a/package-lock.json b/package-lock.json
index 3c23630..a0699c4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,12 +8,15 @@
"name": "pew-builder-nextjs",
"version": "0.1.0",
"dependencies": {
+ "@auth/core": "^0.34.2",
"@headlessui/react": "^2.2.4",
"@heroicons/react": "^2.2.0",
"daisyui": "^4.7.3",
"next": "15.3.4",
+ "next-auth": "^4.24.11",
"react": "^19.0.0",
- "react-dom": "^19.0.0"
+ "react-dom": "^19.0.0",
+ "zustand": "^5.0.6"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
@@ -41,6 +44,46 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/@auth/core": {
+ "version": "0.34.2",
+ "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.34.2.tgz",
+ "integrity": "sha512-KywHKRgLiF3l7PLyL73fjLSIBe1YNcA6sMeew4yMP6cfCWGXZrkkXd32AjRi1hlJ9nvovUBGZHvbn+LijO6ZeQ==",
+ "license": "ISC",
+ "dependencies": {
+ "@panva/hkdf": "^1.1.1",
+ "@types/cookie": "0.6.0",
+ "cookie": "0.6.0",
+ "jose": "^5.1.3",
+ "oauth4webapi": "^2.10.4",
+ "preact": "10.11.3",
+ "preact-render-to-string": "5.2.3"
+ },
+ "peerDependencies": {
+ "@simplewebauthn/browser": "^9.0.1",
+ "@simplewebauthn/server": "^9.0.2",
+ "nodemailer": "^6.8.0"
+ },
+ "peerDependenciesMeta": {
+ "@simplewebauthn/browser": {
+ "optional": true
+ },
+ "@simplewebauthn/server": {
+ "optional": true
+ },
+ "nodemailer": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.27.6",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz",
+ "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@emnapi/core": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz",
@@ -1048,6 +1091,15 @@
"node": ">=12.4.0"
}
},
+ "node_modules/@panva/hkdf": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz",
+ "integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@@ -1223,6 +1275,12 @@
"tslib": "^2.4.0"
}
},
+ "node_modules/@types/cookie": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
+ "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
+ "license": "MIT"
+ },
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -1258,7 +1316,7 @@
"version": "19.1.8",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz",
"integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.0.2"
@@ -2504,6 +2562,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/cookie": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
+ "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -2545,7 +2612,7 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
"node_modules/culori": {
@@ -4448,6 +4515,15 @@
"jiti": "lib/jiti-cli.mjs"
}
},
+ "node_modules/jose": {
+ "version": "5.10.0",
+ "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz",
+ "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -4803,6 +4879,56 @@
}
}
},
+ "node_modules/next-auth": {
+ "version": "4.24.11",
+ "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.11.tgz",
+ "integrity": "sha512-pCFXzIDQX7xmHFs4KVH4luCjaCbuPRtZ9oBUjUhOk84mZ9WVPf94n87TxYI4rSRf9HmfHEF8Yep3JrYDVOo3Cw==",
+ "license": "ISC",
+ "dependencies": {
+ "@babel/runtime": "^7.20.13",
+ "@panva/hkdf": "^1.0.2",
+ "cookie": "^0.7.0",
+ "jose": "^4.15.5",
+ "oauth": "^0.9.15",
+ "openid-client": "^5.4.0",
+ "preact": "^10.6.3",
+ "preact-render-to-string": "^5.1.19",
+ "uuid": "^8.3.2"
+ },
+ "peerDependencies": {
+ "@auth/core": "0.34.2",
+ "next": "^12.2.5 || ^13 || ^14 || ^15",
+ "nodemailer": "^6.6.5",
+ "react": "^17.0.2 || ^18 || ^19",
+ "react-dom": "^17.0.2 || ^18 || ^19"
+ },
+ "peerDependenciesMeta": {
+ "@auth/core": {
+ "optional": true
+ },
+ "nodemailer": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/next-auth/node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/next-auth/node_modules/jose": {
+ "version": "4.15.9",
+ "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz",
+ "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
"node_modules/next/node_modules/postcss": {
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
@@ -4858,6 +4984,21 @@
"node": ">=0.10.0"
}
},
+ "node_modules/oauth": {
+ "version": "0.9.15",
+ "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz",
+ "integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==",
+ "license": "MIT"
+ },
+ "node_modules/oauth4webapi": {
+ "version": "2.17.0",
+ "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-2.17.0.tgz",
+ "integrity": "sha512-lbC0Z7uzAFNFyzEYRIC+pkSVvDHJTbEW+dYlSBAlCYDe6RxUkJ26bClhk8ocBZip1wfI9uKTe0fm4Ib4RHn6uQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -4991,6 +5132,60 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/oidc-token-hash": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.1.0.tgz",
+ "integrity": "sha512-y0W+X7Ppo7oZX6eovsRkuzcSM40Bicg2JEJkDJ4irIt1wsYAP5MLSNv+QAogO8xivMffw/9OvV3um1pxXgt1uA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^10.13.0 || >=12.0.0"
+ }
+ },
+ "node_modules/openid-client": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.1.tgz",
+ "integrity": "sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==",
+ "license": "MIT",
+ "dependencies": {
+ "jose": "^4.15.9",
+ "lru-cache": "^6.0.0",
+ "object-hash": "^2.2.0",
+ "oidc-token-hash": "^5.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
+ "node_modules/openid-client/node_modules/jose": {
+ "version": "4.15.9",
+ "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz",
+ "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
+ "node_modules/openid-client/node_modules/lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/openid-client/node_modules/object-hash": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz",
+ "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -5284,6 +5479,28 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/preact": {
+ "version": "10.11.3",
+ "resolved": "https://registry.npmjs.org/preact/-/preact-10.11.3.tgz",
+ "integrity": "sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/preact"
+ }
+ },
+ "node_modules/preact-render-to-string": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.3.tgz",
+ "integrity": "sha512-aPDxUn5o3GhWdtJtW0svRC2SS/l8D9MAgo2+AWml+BhDImb27ALf04Q2d+AHqUUOc6RdSXFIBVa2gxzgMKgtZA==",
+ "license": "MIT",
+ "dependencies": {
+ "pretty-format": "^3.8.0"
+ },
+ "peerDependencies": {
+ "preact": ">=10"
+ }
+ },
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -5294,6 +5511,12 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/pretty-format": {
+ "version": "3.8.0",
+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz",
+ "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==",
+ "license": "MIT"
+ },
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -6591,6 +6814,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/uuid": {
+ "version": "8.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -6801,6 +7033,12 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
+ "node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "license": "ISC"
+ },
"node_modules/yaml": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz",
@@ -6826,6 +7064,35 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
+ },
+ "node_modules/zustand": {
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.6.tgz",
+ "integrity": "sha512-ihAqNeUVhe0MAD+X8M5UzqyZ9k3FFZLBTtqo6JLPwV53cbRB/mJwBI0PxcIgqhBBHlEs8G45OTDTMq3gNcLq3A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=18.0.0",
+ "immer": ">=9.0.6",
+ "react": ">=18.0.0",
+ "use-sync-external-store": ">=1.2.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "use-sync-external-store": {
+ "optional": true
+ }
+ }
}
}
}
diff --git a/package.json b/package.json
index 8e01a43..f3ae57f 100644
--- a/package.json
+++ b/package.json
@@ -9,12 +9,15 @@
"lint": "next lint"
},
"dependencies": {
+ "@auth/core": "^0.34.2",
"@headlessui/react": "^2.2.4",
"@heroicons/react": "^2.2.0",
"daisyui": "^4.7.3",
"next": "15.3.4",
+ "next-auth": "^4.24.11",
"react": "^19.0.0",
- "react-dom": "^19.0.0"
+ "react-dom": "^19.0.0",
+ "zustand": "^5.0.6"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
diff --git a/src/app/account/forgot-password/page.tsx b/src/app/account/forgot-password/page.tsx
new file mode 100644
index 0000000..e3d9662
--- /dev/null
+++ b/src/app/account/forgot-password/page.tsx
@@ -0,0 +1,46 @@
+'use client';
+import Link from 'next/link';
+import { useState } from 'react';
+
+export default function ForgotPasswordPage() {
+ const [email, setEmail] = useState('');
+ const [submitted, setSubmitted] = useState(false);
+
+ function handleSubmit(e: React.FormEvent) {
+ e.preventDefault();
+ setSubmitted(true);
+ }
+
+ return (
+
+
+
Forgot your password?
+
+ Enter your email address and we'll send you a link to reset your password.
+ (This feature is not yet implemented.)
+
+
+
+ Back to login
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/app/account/layout.tsx b/src/app/account/layout.tsx
new file mode 100644
index 0000000..0664ca6
--- /dev/null
+++ b/src/app/account/layout.tsx
@@ -0,0 +1,43 @@
+import Link from 'next/link';
+
+export default function AccountLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+
+ {/* Simple navbar with back button */}
+
+
+ {/* Main content */}
+
+ {children}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/app/account/login/page.tsx b/src/app/account/login/page.tsx
new file mode 100644
index 0000000..76a17d3
--- /dev/null
+++ b/src/app/account/login/page.tsx
@@ -0,0 +1,168 @@
+'use client';
+
+import { useState } from 'react';
+import { useRouter, useSearchParams } from 'next/navigation';
+import { signIn } from 'next-auth/react';
+import Link from 'next/link';
+
+export default function LoginPage() {
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState('');
+ const router = useRouter();
+ const searchParams = useSearchParams();
+
+ async function handleSubmit(e: React.FormEvent) {
+ e.preventDefault();
+ setLoading(true);
+ setError('');
+ const res = await signIn('credentials', {
+ redirect: false,
+ email,
+ password,
+ callbackUrl: searchParams.get('callbackUrl') || '/',
+ });
+ setLoading(false);
+ if (res?.error) {
+ setError('Invalid email or password');
+ } else if (res?.ok) {
+ router.push(res.url || '/');
+ }
+ }
+
+ async function handleGoogle() {
+ setLoading(true);
+ await signIn('google', { callbackUrl: searchParams.get('callbackUrl') || '/' });
+ setLoading(false);
+ }
+
+ return (
+
+ {/* Left side image or illustration */}
+
+ {/* You can replace this with your own image or illustration */}
+

+
+ {/* Right side form */}
+
+
+
+
Sign in to your account
+
+ Or{' '}
+
+ Sign Up For Free
+
+
+
+
+
+ {/* Social login buttons */}
+
+
+
+
+
+ Or continue with
+
+
+
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/app/account/profile/page.tsx b/src/app/account/profile/page.tsx
new file mode 100644
index 0000000..5f4023d
--- /dev/null
+++ b/src/app/account/profile/page.tsx
@@ -0,0 +1,34 @@
+'use client';
+
+import { useSession } from 'next-auth/react';
+import { useRouter } from 'next/navigation';
+import { useEffect } from 'react';
+
+export default function ProfilePage() {
+ const { data: session, status } = useSession();
+ const router = useRouter();
+
+ useEffect(() => {
+ if (status === 'unauthenticated') {
+ router.replace('/account/login');
+ }
+ }, [status, router]);
+
+ if (status === 'loading') {
+ return Loading...
;
+ }
+
+ if (!session?.user) {
+ return null;
+ }
+
+ return (
+
+
Profile
+
+
Name: {session.user.name || 'N/A'}
+
Email: {session.user.email}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/app/account/register/page.tsx b/src/app/account/register/page.tsx
new file mode 100644
index 0000000..d95af7e
--- /dev/null
+++ b/src/app/account/register/page.tsx
@@ -0,0 +1,94 @@
+'use client';
+
+import { useState } from 'react';
+import { useRouter } from 'next/navigation';
+import Link from 'next/link';
+import { signIn } from 'next-auth/react';
+
+export default function RegisterPage() {
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [showPassword, setShowPassword] = useState(false);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState('');
+ const router = useRouter();
+
+ async function handleSubmit(e: React.FormEvent) {
+ e.preventDefault();
+ setLoading(true);
+ setError('');
+ const res = await fetch('/api/register', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ email, password }),
+ });
+ const data = await res.json();
+ if (!res.ok) {
+ setLoading(false);
+ setError(data.error || 'Registration failed');
+ return;
+ }
+ // Auto-login after registration
+ const signInRes = await signIn('credentials', {
+ redirect: false,
+ email,
+ password,
+ callbackUrl: '/account/profile',
+ });
+ setLoading(false);
+ if (signInRes?.ok) {
+ router.push('/');
+ } else {
+ router.push('/account/login?registered=1');
+ }
+ }
+
+ return (
+
+
+
Create your account
+
+
+ Already have an account? Sign in
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts
new file mode 100644
index 0000000..724b52c
--- /dev/null
+++ b/src/app/api/auth/[...nextauth]/route.ts
@@ -0,0 +1,70 @@
+import NextAuth from 'next-auth';
+import GoogleProvider from 'next-auth/providers/google';
+import CredentialsProvider from 'next-auth/providers/credentials';
+
+// In-memory user store (for demo only)
+type User = { email: string; password: string };
+declare global {
+ // eslint-disable-next-line no-var
+ var _users: User[] | undefined;
+}
+const users: User[] = global._users || (global._users = []);
+
+const handler = NextAuth({
+ providers: [
+ GoogleProvider({
+ clientId: process.env.GOOGLE_CLIENT_ID ?? '',
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET ?? '',
+ }),
+ CredentialsProvider({
+ name: 'Credentials',
+ credentials: {
+ email: { label: "Email", type: "email" },
+ password: { label: "Password", type: "password" }
+ },
+ async authorize(credentials) {
+ if (!credentials?.email || !credentials?.password) return null;
+ // Check in-memory user store
+ const user = users.find(
+ (u) => u.email === credentials.email && u.password === credentials.password
+ );
+ if (user) {
+ return {
+ id: user.email,
+ email: user.email,
+ name: user.email.split('@')[0],
+ };
+ }
+ // For demo, still allow the test user
+ if (credentials.email === "test@example.com" && credentials.password === "password") {
+ return {
+ id: "1",
+ email: credentials.email,
+ name: "Test User",
+ };
+ }
+ return null;
+ }
+ }),
+ ],
+ pages: {
+ signIn: '/account/login',
+ // signUp: '/account/register', // Uncomment when register page is ready
+ // error: '/account/error', // Uncomment when error page is ready
+ },
+ callbacks: {
+ async session({ session, token }) {
+ // Add any additional user data to the session here
+ return session;
+ },
+ async jwt({ token, user }) {
+ // Add any additional user data to the JWT here
+ if (user) {
+ token.id = user.id;
+ }
+ return token;
+ },
+ },
+})
+
+export { handler as GET, handler as POST }
\ No newline at end of file
diff --git a/src/app/api/register/route.ts b/src/app/api/register/route.ts
new file mode 100644
index 0000000..4b61305
--- /dev/null
+++ b/src/app/api/register/route.ts
@@ -0,0 +1,27 @@
+import { NextResponse } from 'next/server';
+
+// In-memory user store (for demo only)
+type User = { email: string; password: string };
+declare global {
+ // eslint-disable-next-line no-var
+ var _users: User[] | undefined;
+}
+const users: User[] = global._users || (global._users = []);
+
+export async function POST(req: Request) {
+ const { email, password } = await req.json();
+
+ if (!email || !password) {
+ return NextResponse.json({ error: 'Email and password are required.' }, { status: 400 });
+ }
+
+ // Check if user already exists
+ const existing = users.find((u) => u.email === email);
+ if (existing) {
+ return NextResponse.json({ error: 'User already exists.' }, { status: 400 });
+ }
+
+ // Add new user
+ users.push({ email, password });
+ return NextResponse.json({ success: true });
+}
\ No newline at end of file
diff --git a/src/app/build/page.tsx b/src/app/build/page.tsx
index 9ee7ee4..a3aca2f 100644
--- a/src/app/build/page.tsx
+++ b/src/app/build/page.tsx
@@ -4,6 +4,10 @@ import { useState } from 'react';
import Link from 'next/link';
import React from 'react';
import SearchInput from '@/components/SearchInput';
+import RestrictionAlert from '@/components/RestrictionAlert';
+import { useBuildStore } from '@/store/useBuildStore';
+import { mockProducts } from '@/mock/product';
+import { Dialog } from '@headlessui/react';
// AR-15 Build Requirements grouped by main categories
const buildGroups = [
@@ -215,12 +219,60 @@ const categories = ["All", "Upper", "Lower", "Accessory"];
type SortField = 'name' | 'category' | 'estimatedPrice' | 'status';
type SortDirection = 'asc' | 'desc';
+// Map checklist component categories to product categories for filtering
+const getProductCategory = (componentCategory: string): string => {
+ const categoryMap: Record = {
+ 'Upper': 'Upper Receiver', // Default to Upper Receiver for Upper category
+ 'Lower': 'Lower Receiver', // Default to Lower Receiver for Lower category
+ 'Accessory': 'Magazine', // Default to Magazine for Accessory category
+ };
+
+ return categoryMap[componentCategory] || 'Magazine';
+};
+
+// Map specific checklist components to product categories
+const getProductCategoryForComponent = (componentName: string): string => {
+ const componentMap: Record = {
+ // Upper components
+ 'Upper Receiver': 'Upper Receiver',
+ 'Barrel': 'Barrel',
+ 'Bolt Carrier Group (BCG)': 'BCG',
+ 'Charging Handle': 'Charging Handle',
+ 'Gas Block': 'Gas Block',
+ 'Gas Tube': 'Gas Tube',
+ 'Handguard': 'Handguard',
+ 'Muzzle Device': 'Muzzle Device',
+
+ // Lower components
+ 'Lower Receiver': 'Lower Receiver',
+ 'Trigger': 'Trigger',
+ 'Trigger Guard': 'Lower Receiver',
+ 'Pistol Grip': 'Lower Receiver',
+ 'Buffer Tube': 'Lower Receiver',
+ 'Buffer': 'Lower Receiver',
+ 'Buffer Spring': 'Lower Receiver',
+ 'Stock': 'Stock',
+
+ // Accessories
+ 'Magazine': 'Magazine',
+ 'Sights': 'Magazine',
+ };
+
+ return componentMap[componentName] || 'Lower Receiver';
+};
+
+export { buildGroups };
export default function BuildPage() {
const [sortField, setSortField] = useState('name');
const [sortDirection, setSortDirection] = useState('asc');
const [selectedCategory, setSelectedCategory] = useState('All');
const [searchTerm, setSearchTerm] = useState('');
+ const selectedParts = useBuildStore((state) => state.selectedParts);
+ const removePartForComponent = useBuildStore((state) => state.removePartForComponent);
+ const clearBuild = useBuildStore((state) => state.clearBuild);
+ const [showClearModal, setShowClearModal] = useState(false);
+
// Filter components
const filteredComponents = allComponents.filter(component => {
if (selectedCategory !== 'All' && component.category !== selectedCategory) {
@@ -278,36 +330,66 @@ export default function BuildPage() {
const getStatusColor = (status: string) => {
switch (status) {
- case 'completed':
- return 'bg-green-100 text-green-800';
- case 'pending':
- return 'bg-yellow-100 text-yellow-800';
- case 'in-progress':
- return 'bg-blue-100 text-blue-800';
- default:
- return 'bg-gray-100 text-gray-800';
+ case 'completed': return 'bg-green-100 text-green-800';
+ case 'in-progress': return 'bg-yellow-100 text-yellow-800';
+ case 'pending': return 'bg-gray-100 text-gray-800';
+ default: return 'bg-gray-100 text-gray-800';
}
};
const totalEstimatedCost = sortedComponents.reduce((sum, component) => sum + component.estimatedPrice, 0);
- const completedCount = sortedComponents.filter(component => component.status === 'completed').length;
+ const completedCount = sortedComponents.filter(component => selectedParts[component.id]).length;
+ const actualTotalCost = sortedComponents.reduce((sum, component) => {
+ const selected = selectedParts[component.id];
+ if (selected && selected.offers) {
+ return sum + Math.min(...selected.offers.map(offer => offer.price));
+ }
+ return sum;
+ }, 0);
const hasActiveFilters = selectedCategory !== 'All' || searchTerm;
+ // Check for restricted parts in the build
+ const getRestrictedParts = () => {
+ const restrictedParts: Array<{ part: any; restriction: string }> = [];
+
+ Object.values(selectedParts).forEach(selectedPart => {
+ if (selectedPart) {
+ const product = mockProducts.find(p => p.id === selectedPart.id);
+ if (product?.restrictions) {
+ const restrictions = product.restrictions;
+ if (restrictions.nfa) restrictedParts.push({ part: product, restriction: 'NFA' });
+ if (restrictions.sbr) restrictedParts.push({ part: product, restriction: 'SBR' });
+ if (restrictions.suppressor) restrictedParts.push({ part: product, restriction: 'Suppressor' });
+ if (restrictions.stateRestrictions && restrictions.stateRestrictions.length > 0) {
+ restrictedParts.push({ part: product, restriction: 'State Restrictions' });
+ }
+ }
+ }
+ });
+
+ return restrictedParts;
+ };
+
+ const restrictedParts = getRestrictedParts();
+ const hasNFAItems = restrictedParts.some(rp => rp.restriction === 'NFA');
+ const hasSuppressors = restrictedParts.some(rp => rp.restriction === 'Suppressor');
+ const hasStateRestrictions = restrictedParts.some(rp => rp.restriction === 'State Restrictions');
+ const [showRestrictionAlerts, setShowRestrictionAlerts] = useState(true);
+
return (
{/* Page Title */}
-
AR-15 Build Checklist
-
Track your build progress and find required components
+
Plan Your Build
{/* Build Summary */}
-
+
{allComponents.length}
Total Components
@@ -320,26 +402,138 @@ export default function BuildPage() {
{allComponents.length - completedCount}
Remaining
-
-
${totalEstimatedCost}
-
Estimated Cost
+
+
+
${actualTotalCost.toFixed(2)}
+
Total Cost
+
+
+ {/* Clear Build Modal */}
+
+
+ {/* Restriction Alerts */}
+ {restrictedParts.length > 0 && (
+
+
+
+
+
+
+ {restrictedParts.length} restriction{restrictedParts.length > 1 ? 's' : ''} detected
+
+
+
+
+
+ {showRestrictionAlerts && (
+
+ {hasNFAItems && (
+
+
+
π
+
+
NFA Items in Your Build
+
+ Your build contains items that require National Firearms Act registration.
+
+
+
+
+ )}
+ {hasSuppressors && (
+
+
+
π
+
+
Suppressor in Your Build
+
+ Sound suppressor requires NFA registration. Processing times: 6-12 months.
+
+
+
+
+ )}
+ {hasStateRestrictions && (
+
+
+
πΊοΈ
+
+
State Restrictions Apply
+
+ Some items may be restricted in certain states. Verify local laws.
+
+
+
+
+ )}
+
+ )}
+
+
+ )}
+
{/* Search and Filters */}
-
+
{/* Filters Row */}
-
+
{/* Category Dropdown */}
-
+
{/* Status Filter */}
-
+
-
{/* Sort by */}
-
+
handleSort(e.target.value as SortField)}
- className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white text-gray-900"
+ className="w-full px-3 py-1.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white text-gray-900 text-sm"
>
@@ -376,8 +570,8 @@ export default function BuildPage() {
{/* Clear Filters */}
-
-