mirror of
https://gitea.gofwd.group/sean/gunbuilder-next-tailwind.git
synced 2025-12-05 18:46:45 -05:00
nextauth working.
This commit is contained in:
237
package-lock.json
generated
237
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
46
src/app/account/forgot-password/page.tsx
Normal file
46
src/app/account/forgot-password/page.tsx
Normal file
@@ -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 (
|
||||
<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">
|
||||
<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">
|
||||
Enter your email address and we'll send you a link to reset your password.<br/>
|
||||
<span className="text-primary font-semibold">(This feature is not yet implemented.)</span>
|
||||
</p>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<input
|
||||
type="email"
|
||||
required
|
||||
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"
|
||||
value={email}
|
||||
onChange={e => setEmail(e.target.value)}
|
||||
disabled={submitted}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full btn btn-primary"
|
||||
disabled={submitted}
|
||||
>
|
||||
{submitted ? 'Check your email' : 'Send reset link'}
|
||||
</button>
|
||||
</form>
|
||||
<div className="mt-6 text-center">
|
||||
<Link href="/account/login" className="text-primary-600 hover:underline text-sm">Back to login</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
43
src/app/account/layout.tsx
Normal file
43
src/app/account/layout.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function AccountLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
{/* Simple navbar with back button */}
|
||||
<nav className="bg-white dark:bg-neutral-900 shadow-sm">
|
||||
<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 items-center">
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 dark:text-gray-300 hover:text-gray-700 dark:hover:text-gray-100 focus:outline-none transition"
|
||||
>
|
||||
<svg
|
||||
className="h-5 w-5 mr-1"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||
</svg>
|
||||
Back
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Main content */}
|
||||
<main className="flex-1">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
168
src/app/account/login/page.tsx
Normal file
168
src/app/account/login/page.tsx
Normal file
@@ -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 (
|
||||
<div className="min-h-screen flex">
|
||||
{/* Left side image or illustration */}
|
||||
<div className="hidden lg:block relative w-0 flex-1 bg-gray-900">
|
||||
{/* You can replace this with your own image or illustration */}
|
||||
<img
|
||||
className="absolute inset-0 h-full w-full object-cover opacity-80"
|
||||
src="/window.svg"
|
||||
alt="Login visual"
|
||||
/>
|
||||
</div>
|
||||
{/* 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="mx-auto w-full max-w-md space-y-8">
|
||||
<div>
|
||||
<h2 className="mt-6 text-3xl font-extrabold text-gray-900 dark:text-white">Sign in to your account</h2>
|
||||
<p className="mt-2 text-sm text-gray-600 dark:text-gray-300">
|
||||
Or{' '}
|
||||
<Link href="/account/register" className="font-medium text-primary-600 hover:text-primary-500">
|
||||
Sign Up For Free
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
|
||||
<input type="hidden" name="remember" value="true" />
|
||||
<div className="rounded-md shadow-sm -space-y-px">
|
||||
<div>
|
||||
<label htmlFor="email-address" className="sr-only">
|
||||
Email address
|
||||
</label>
|
||||
<input
|
||||
id="email-address"
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
required
|
||||
value={email}
|
||||
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"
|
||||
placeholder="Email address"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="password" className="sr-only">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
required
|
||||
value={password}
|
||||
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"
|
||||
placeholder="Password"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="text-red-600 text-sm mt-2">{error}</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
id="remember-me"
|
||||
name="remember-me"
|
||||
type="checkbox"
|
||||
className="checkbox checkbox-primary"
|
||||
disabled={loading}
|
||||
/>
|
||||
<label htmlFor="remember-me" className="ml-2 block text-sm text-gray-900 dark:text-gray-300">
|
||||
Remember me
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="text-sm">
|
||||
<Link href="/account/forgot-password" className="font-medium text-primary-600 hover:text-primary-500">
|
||||
Forgot your password?
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full btn btn-primary text-white font-medium text-sm py-2 px-4 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Signing in...' : 'Sign in'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Social login buttons */}
|
||||
<div className="mt-6">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-gray-300 dark:border-neutral-700" />
|
||||
</div>
|
||||
<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">
|
||||
Or continue with
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 flex justify-center">
|
||||
<button
|
||||
type="button"
|
||||
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"
|
||||
disabled={loading}
|
||||
>
|
||||
<span className="sr-only">Sign in with Google</span>
|
||||
{/* Google Icon Placeholder */}
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M21.35 11.1H12v2.8h5.35c-.23 1.2-1.4 3.5-5.35 3.5-3.22 0-5.85-2.67-5.85-5.9s2.63-5.9 5.85-5.9c1.83 0 3.06.78 3.76 1.44l2.57-2.5C17.09 3.59 14.77 2.5 12 2.5 6.75 2.5 2.5 6.75 2.5 12s4.25 9.5 9.5 9.5c5.47 0 9.09-3.84 9.09-9.25 0-.62-.07-1.08-.16-1.55z" /></svg>
|
||||
<span className="ml-2">Sign in with Google</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
34
src/app/account/profile/page.tsx
Normal file
34
src/app/account/profile/page.tsx
Normal file
@@ -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 <div className="flex justify-center items-center h-64">Loading...</div>;
|
||||
}
|
||||
|
||||
if (!session?.user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-xl mx-auto mt-12 p-6 bg-white dark:bg-neutral-900 rounded shadow">
|
||||
<h1 className="text-2xl font-bold mb-4">Profile</h1>
|
||||
<div className="space-y-2">
|
||||
<div><span className="font-semibold">Name:</span> {session.user.name || 'N/A'}</div>
|
||||
<div><span className="font-semibold">Email:</span> {session.user.email}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
94
src/app/account/register/page.tsx
Normal file
94
src/app/account/register/page.tsx
Normal file
@@ -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 (
|
||||
<div className="min-h-screen flex items-center justify-center bg-white dark:bg-neutral-900">
|
||||
<div className="w-full max-w-md bg-white dark:bg-neutral-900 rounded-lg shadow p-8">
|
||||
<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">
|
||||
<input
|
||||
type="email"
|
||||
required
|
||||
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"
|
||||
value={email}
|
||||
onChange={e => setEmail(e.target.value)}
|
||||
disabled={loading}
|
||||
/>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
required
|
||||
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"
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
disabled={loading}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute inset-y-0 right-2 flex items-center text-xs text-gray-500 dark:text-gray-300"
|
||||
tabIndex={-1}
|
||||
onClick={() => setShowPassword(v => !v)}
|
||||
>
|
||||
{showPassword ? 'Hide' : 'Show'}
|
||||
</button>
|
||||
</div>
|
||||
{error && <div className="text-red-600 text-sm">{error}</div>}
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full btn btn-primary text-white font-medium text-sm py-2 px-4 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Creating account...' : 'Create Account'}
|
||||
</button>
|
||||
</form>
|
||||
<div className="mt-6 text-center">
|
||||
<Link href="/account/login" className="text-primary-600 hover:underline text-sm">Already have an account? Sign in</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
70
src/app/api/auth/[...nextauth]/route.ts
Normal file
70
src/app/api/auth/[...nextauth]/route.ts
Normal file
@@ -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 }
|
||||
27
src/app/api/register/route.ts
Normal file
27
src/app/api/register/route.ts
Normal file
@@ -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 });
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 (
|
||||
<html lang="en" suppressHydrationWarning data-theme="pew">
|
||||
<body className={`${inter.className} antialiased`}>
|
||||
<ThemeProvider>
|
||||
<div className="min-h-screen bg-neutral-50 dark:bg-neutral-900 transition-colors duration-200">
|
||||
<Navbar />
|
||||
{children}
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
<Providers>
|
||||
{children}
|
||||
</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -668,7 +668,7 @@ export default function Home() {
|
||||
} else if (matchingComponent && !selectedParts[matchingComponent.id]) {
|
||||
return (
|
||||
<button
|
||||
className="btn btn-primary btn-sm"
|
||||
className="btn btn-neutral btn-sm flex items-center gap-1"
|
||||
onClick={() => {
|
||||
selectPartForComponent(matchingComponent.id, {
|
||||
id: part.id,
|
||||
@@ -681,7 +681,8 @@ export default function Home() {
|
||||
router.push('/build');
|
||||
}}
|
||||
>
|
||||
Add
|
||||
<span className="text-lg leading-none">+</span>
|
||||
<span className="text-xs font-normal">to build</span>
|
||||
</button>
|
||||
);
|
||||
} else {
|
||||
|
||||
17
src/components/AuthProvider.tsx
Normal file
17
src/components/AuthProvider.tsx
Normal file
@@ -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}</>;
|
||||
}
|
||||
@@ -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: <ThemeSwitcher />,
|
||||
},
|
||||
];
|
||||
function isMenuItem(item: unknown): item is MenuItem {
|
||||
return typeof item === 'object' && item !== null && 'label' in item;
|
||||
}
|
||||
const menuItems = rawMenuItems.filter(isMenuItem);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Top Bar */}
|
||||
<div className="w-full bg-[#4B6516] text-white h-10 flex items-center justify-between px-4 sm:px-8">
|
||||
<div className="w-full bg-[#4B6516] text-white h-10 flex items-center justify-between px-4 sm:px-8 relative z-20">
|
||||
<Link href="/" className="font-bold text-lg tracking-tight hover:underline focus:underline">Pew Builder</Link>
|
||||
<UserCircleIcon className="h-7 w-7 text-white opacity-80" />
|
||||
<div className="relative">
|
||||
{loading ? null : session?.user ? (
|
||||
<>
|
||||
<button
|
||||
className="flex items-center gap-2 focus:outline-none focus:ring-2 focus:ring-white/70 rounded-full px-3 py-1 hover:bg-[#3a4d12] transition font-semibold"
|
||||
aria-haspopup="true"
|
||||
aria-expanded={menuOpen}
|
||||
onClick={() => setMenuOpen((v) => !v)}
|
||||
>
|
||||
My Profile
|
||||
{session.user.name && (
|
||||
<span className="hidden sm:inline text-white font-medium text-sm">({session.user.name})</span>
|
||||
)}
|
||||
</button>
|
||||
{menuOpen && (
|
||||
<div
|
||||
className="absolute right-0 mt-2 w-56 bg-white dark:bg-neutral-800 rounded-md shadow-lg ring-1 ring-black/10 dark:ring-white/10 focus:outline-none divide-y divide-neutral-100 dark:divide-neutral-700 animate-fade-in"
|
||||
tabIndex={-1}
|
||||
onBlur={() => setMenuOpen(false)}
|
||||
>
|
||||
<div className="py-1">
|
||||
{session?.user && (
|
||||
<div className="px-4 py-2 text-xs text-neutral-500 dark:text-neutral-400 border-b border-neutral-100 dark:border-neutral-700">
|
||||
{session.user.email}
|
||||
</div>
|
||||
)}
|
||||
{menuItems.map((item, idx) =>
|
||||
item.custom ? (
|
||||
<div key={idx} className="px-4 py-2 flex items-center justify-between">
|
||||
<span className="text-sm text-neutral-700 dark:text-neutral-200">Theme</span>
|
||||
{item.custom}
|
||||
</div>
|
||||
) : item.href ? (
|
||||
<Link
|
||||
key={idx}
|
||||
href={item.href}
|
||||
className={`block px-4 py-2 text-sm rounded transition-colors ${
|
||||
item.active
|
||||
? 'bg-primary/10 text-primary font-semibold'
|
||||
: 'text-neutral-700 dark:text-neutral-200 hover:bg-neutral-100 dark:hover:bg-neutral-700'
|
||||
}`}
|
||||
onClick={item.onClick}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
) : (
|
||||
<button
|
||||
key={idx}
|
||||
className="block w-full text-left px-4 py-2 text-sm text-neutral-700 dark:text-neutral-200 hover:bg-neutral-100 dark:hover:bg-neutral-700 rounded transition-colors"
|
||||
onClick={item.onClick}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
className="btn btn-sm btn-primary font-semibold px-4 py-1"
|
||||
onClick={() => signIn()}
|
||||
>
|
||||
Sign In
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Subnav */}
|
||||
@@ -42,15 +157,11 @@ export default function Navbar() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Right: Sign In + Search */}
|
||||
{/* Right: Search */}
|
||||
<div className="flex items-center space-x-4">
|
||||
<Link href="/signin" className="btn btn-sm btn-ghost text-sm font-medium">
|
||||
Sign In
|
||||
</Link>
|
||||
<button className="p-2 rounded-full hover:bg-neutral-100 dark:hover:bg-neutral-700 transition-colors">
|
||||
<MagnifyingGlassIcon className="h-5 w-5 text-neutral-700 dark:text-neutral-200" />
|
||||
</button>
|
||||
<ThemeSwitcher />
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
15
src/components/NavigationWrapper.tsx
Normal file
15
src/components/NavigationWrapper.tsx
Normal file
@@ -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 <Navbar />;
|
||||
}
|
||||
@@ -120,11 +120,18 @@ export default function ProductCard({ product, onAdd, added }: ProductCardProps)
|
||||
</Link>
|
||||
{onAdd && (
|
||||
<button
|
||||
className="btn btn-accent btn-sm ml-2"
|
||||
className="btn btn-neutral btn-sm ml-2 flex items-center gap-1"
|
||||
onClick={onAdd}
|
||||
disabled={added}
|
||||
>
|
||||
{added ? 'Added!' : 'Add'}
|
||||
{added ? (
|
||||
'Added!'
|
||||
) : (
|
||||
<>
|
||||
<span className="text-lg leading-none">+</span>
|
||||
<span className="text-xs font-normal">to build</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
21
src/components/Providers.tsx
Normal file
21
src/components/Providers.tsx
Normal file
@@ -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 (
|
||||
<SessionProvider>
|
||||
<AuthProvider>
|
||||
<ThemeProvider>
|
||||
<div className="min-h-screen bg-neutral-50 dark:bg-neutral-900 transition-colors duration-200">
|
||||
<NavigationWrapper />
|
||||
{children}
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
</AuthProvider>
|
||||
</SessionProvider>
|
||||
);
|
||||
}
|
||||
16
src/store/useAuthStore.ts
Normal file
16
src/store/useAuthStore.ts
Normal file
@@ -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<AuthStore>((set) => ({
|
||||
session: null,
|
||||
isLoading: true,
|
||||
setSession: (session) => set({ session }),
|
||||
setLoading: (isLoading) => set({ isLoading }),
|
||||
}));
|
||||
Reference in New Issue
Block a user