From 41e55404bf0c38cf5639fd81a6e8264cd155d7a8 Mon Sep 17 00:00:00 2001 From: Sean S Date: Mon, 30 Jun 2025 06:36:03 -0400 Subject: [PATCH] nextauth working. --- package-lock.json | 237 +++++++++++++++++++++++ package.json | 2 + src/app/account/forgot-password/page.tsx | 46 +++++ src/app/account/layout.tsx | 43 ++++ src/app/account/login/page.tsx | 168 ++++++++++++++++ src/app/account/profile/page.tsx | 34 ++++ src/app/account/register/page.tsx | 94 +++++++++ src/app/api/auth/[...nextauth]/route.ts | 70 +++++++ src/app/api/register/route.ts | 27 +++ src/app/globals.css | 3 - src/app/layout.tsx | 12 +- src/app/parts/page.tsx | 5 +- src/components/AuthProvider.tsx | 17 ++ src/components/Navbar.tsx | 127 +++++++++++- src/components/NavigationWrapper.tsx | 15 ++ src/components/ProductCard.tsx | 11 +- src/components/Providers.tsx | 21 ++ src/store/useAuthStore.ts | 16 ++ 18 files changed, 925 insertions(+), 23 deletions(-) create mode 100644 src/app/account/forgot-password/page.tsx create mode 100644 src/app/account/layout.tsx create mode 100644 src/app/account/login/page.tsx create mode 100644 src/app/account/profile/page.tsx create mode 100644 src/app/account/register/page.tsx create mode 100644 src/app/api/auth/[...nextauth]/route.ts create mode 100644 src/app/api/register/route.ts create mode 100644 src/components/AuthProvider.tsx create mode 100644 src/components/NavigationWrapper.tsx create mode 100644 src/components/Providers.tsx create mode 100644 src/store/useAuthStore.ts 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.) +

+
+ setEmail(e.target.value)} + disabled={submitted} + /> + +
+
+ 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 */} + Login visual +
+ {/* Right side form */} +
+
+
+

Sign in to your account

+

+ Or{' '} + + Sign Up For Free + +

+
+
+ +
+
+ + 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" + placeholder="Email address" + disabled={loading} + /> +
+
+ + 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" + placeholder="Password" + disabled={loading} + /> +
+
+ + {error && ( +
{error}
+ )} + +
+
+ + +
+ +
+ + Forgot your password? + +
+
+ +
+ +
+
+ + {/* 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

+
+ setEmail(e.target.value)} + disabled={loading} + /> +
+ setPassword(e.target.value)} + disabled={loading} + /> + +
+ {error &&
{error}
} + +
+
+ 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