diff --git a/package-lock.json b/package-lock.json
index d5d9c8a..a0699c4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,10 +8,12 @@
"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",
"zustand": "^5.0.6"
@@ -42,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",
@@ -1049,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",
@@ -1224,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",
@@ -2505,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",
@@ -4449,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",
@@ -4804,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",
@@ -4859,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",
@@ -4992,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",
@@ -5285,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",
@@ -5295,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",
@@ -6592,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",
@@ -6802,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",
diff --git a/package.json b/package.json
index 027521e..f3ae57f 100644
--- a/package.json
+++ b/package.json
@@ -9,10 +9,12 @@
"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",
"zustand": "^5.0.6"
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/globals.css b/src/app/globals.css
index f559d12..b2c7231 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -42,9 +42,6 @@
/* Button styles */
/* Removed custom .btn-primary to avoid DaisyUI conflict */
- .btn-secondary {
- @apply bg-neutral-100 hover:bg-neutral-200 dark:bg-neutral-700 dark:hover:bg-neutral-600 text-neutral-700 dark:text-neutral-300 font-medium py-2 px-4 rounded-lg transition-colors duration-200 focus-ring;
- }
/* Input styles */
.input-field {
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 9d5d7e7..2ceb6e1 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,8 +1,7 @@
import "./globals.css";
import type { Metadata } from "next";
import { Inter } from "next/font/google";
-import Navbar from "@/components/Navbar";
-import { ThemeProvider } from "@/components/ThemeProvider";
+import Providers from "@/components/Providers";
const inter = Inter({ subsets: ["latin"] });
@@ -19,12 +18,9 @@ export default function RootLayout({
return (
-
-
-
- {children}
-
-
+
+ {children}
+
);
diff --git a/src/app/parts/page.tsx b/src/app/parts/page.tsx
index 22530a5..eef5d9a 100644
--- a/src/app/parts/page.tsx
+++ b/src/app/parts/page.tsx
@@ -668,7 +668,7 @@ export default function Home() {
} else if (matchingComponent && !selectedParts[matchingComponent.id]) {
return (
);
} else {
diff --git a/src/components/AuthProvider.tsx b/src/components/AuthProvider.tsx
new file mode 100644
index 0000000..c8c4f49
--- /dev/null
+++ b/src/components/AuthProvider.tsx
@@ -0,0 +1,17 @@
+'use client';
+
+import { useEffect } from 'react';
+import { useSession } from 'next-auth/react';
+import { useAuthStore } from '@/store/useAuthStore';
+
+export function AuthProvider({ children }: { children: React.ReactNode }) {
+ const { data: session, status } = useSession();
+ const { setSession, setLoading } = useAuthStore();
+
+ useEffect(() => {
+ setSession(session);
+ setLoading(status === 'loading');
+ }, [session, status, setSession, setLoading]);
+
+ return <>{children}>;
+}
\ No newline at end of file
diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx
index 2608dec..49c68f4 100644
--- a/src/components/Navbar.tsx
+++ b/src/components/Navbar.tsx
@@ -3,10 +3,23 @@
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import ThemeSwitcher from './ThemeSwitcher';
-import { MagnifyingGlassIcon, UserCircleIcon } from '@heroicons/react/24/outline';
+import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
+import { useSession, signIn, signOut } from 'next-auth/react';
+import { useState, ReactNode } from 'react';
+
+interface MenuItem {
+ label: string;
+ href?: string;
+ active?: boolean;
+ onClick?: () => void;
+ custom?: ReactNode;
+}
export default function Navbar() {
const pathname = usePathname();
+ const { data: session, status } = useSession();
+ const loading = status === 'loading';
+ const [menuOpen, setMenuOpen] = useState(false);
const navItems = [
{ href: '/parts', label: 'Parts Catalog' },
@@ -14,12 +27,114 @@ export default function Navbar() {
{ href: '/my-builds', label: 'My Builds' },
];
+ // Dropdown menu items
+ const rawMenuItems = [
+ session?.user
+ ? {
+ label: 'Profile',
+ href: '/account/profile',
+ active: pathname === '/account/profile',
+ onClick: () => setMenuOpen(false),
+ }
+ : undefined,
+ session?.user
+ ? {
+ label: 'Sign Out',
+ onClick: () => {
+ setMenuOpen(false);
+ signOut({ callbackUrl: '/' });
+ },
+ }
+ : {
+ label: 'Sign In',
+ onClick: () => {
+ setMenuOpen(false);
+ signIn();
+ },
+ },
+ {
+ label: 'Theme',
+ custom: ,
+ },
+ ];
+ function isMenuItem(item: unknown): item is MenuItem {
+ return typeof item === 'object' && item !== null && 'label' in item;
+ }
+ const menuItems = rawMenuItems.filter(isMenuItem);
+
return (
<>
{/* Top Bar */}
-
+
Pew Builder
-
+
+ {loading ? null : session?.user ? (
+ <>
+
+ {menuOpen && (
+
setMenuOpen(false)}
+ >
+
+ {session?.user && (
+
+ {session.user.email}
+
+ )}
+ {menuItems.map((item, idx) =>
+ item.custom ? (
+
+ Theme
+ {item.custom}
+
+ ) : item.href ? (
+
+ {item.label}
+
+ ) : (
+
+ )
+ )}
+
+
+ )}
+ >
+ ) : (
+
+ )}
+
{/* Subnav */}
@@ -42,15 +157,11 @@ export default function Navbar() {
))}
- {/* Right: Sign In + Search */}
+ {/* Right: Search */}
-
- Sign In
-
-
diff --git a/src/components/NavigationWrapper.tsx b/src/components/NavigationWrapper.tsx
new file mode 100644
index 0000000..99b2080
--- /dev/null
+++ b/src/components/NavigationWrapper.tsx
@@ -0,0 +1,15 @@
+'use client';
+
+import { usePathname } from 'next/navigation';
+import Navbar from './Navbar';
+
+export default function NavigationWrapper() {
+ const pathname = usePathname();
+ const isAccountPage = pathname?.startsWith('/account');
+
+ if (isAccountPage) {
+ return null;
+ }
+
+ return ;
+}
\ No newline at end of file
diff --git a/src/components/ProductCard.tsx b/src/components/ProductCard.tsx
index 85f7284..105ba22 100644
--- a/src/components/ProductCard.tsx
+++ b/src/components/ProductCard.tsx
@@ -120,11 +120,18 @@ export default function ProductCard({ product, onAdd, added }: ProductCardProps)
{onAdd && (
)}
diff --git a/src/components/Providers.tsx b/src/components/Providers.tsx
new file mode 100644
index 0000000..3ecd704
--- /dev/null
+++ b/src/components/Providers.tsx
@@ -0,0 +1,21 @@
+'use client';
+
+import { SessionProvider } from 'next-auth/react';
+import { AuthProvider } from './AuthProvider';
+import { ThemeProvider } from './ThemeProvider';
+import NavigationWrapper from './NavigationWrapper';
+
+export default function Providers({ children }: { children: React.ReactNode }) {
+ return (
+
+
+
+
+
+ {children}
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/store/useAuthStore.ts b/src/store/useAuthStore.ts
new file mode 100644
index 0000000..c359787
--- /dev/null
+++ b/src/store/useAuthStore.ts
@@ -0,0 +1,16 @@
+import { create } from 'zustand';
+import { Session } from 'next-auth';
+
+interface AuthStore {
+ session: Session | null;
+ isLoading: boolean;
+ setSession: (session: Session | null) => void;
+ setLoading: (isLoading: boolean) => void;
+}
+
+export const useAuthStore = create((set) => ({
+ session: null,
+ isLoading: true,
+ setSession: (session) => set({ session }),
+ setLoading: (isLoading) => set({ isLoading }),
+}));
\ No newline at end of file