mirror of
https://gitea.gofwd.group/sean/gunbuilder-next-tailwind.git
synced 2025-12-06 02:56:45 -05:00
/admin and with working with db. data is pulling from db
This commit is contained in:
46
src/app/(main)/account/forgot-password/page.tsx
Normal file
46
src/app/(main)/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-zinc-900 rounded-lg shadow p-8">
|
||||
<h1 className="text-2xl font-bold mb-4 text-gray-900 dark:text-white">Forgot your password?</h1>
|
||||
<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-zinc-700 rounded-md bg-white dark:bg-zinc-800 text-gray-900 dark:text-white focus:outline-none focus:ring-primary-500 focus:border-primary-500"
|
||||
value={email}
|
||||
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/(main)/account/layout.tsx
Normal file
43
src/app/(main)/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-zinc-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/(main)/account/login/page.tsx
Normal file
168
src/app/(main)/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-zinc-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-zinc-700 placeholder-gray-500 dark:placeholder-zinc-400 text-gray-900 dark:text-white bg-white dark:bg-zinc-800 rounded-t-md focus:outline-none focus:ring-primary-500 focus:border-primary-500 focus:z-10 sm:text-sm"
|
||||
placeholder="Email address"
|
||||
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-zinc-700 placeholder-gray-500 dark:placeholder-zinc-400 text-gray-900 dark:text-white bg-white dark:bg-zinc-800 rounded-b-md focus:outline-none focus:ring-primary-500 focus:border-primary-500 focus:z-10 sm:text-sm"
|
||||
placeholder="Password"
|
||||
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-zinc-700" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-sm">
|
||||
<span className="px-2 bg-white dark:bg-zinc-900 text-gray-500 dark:text-zinc-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-zinc-700 rounded-md shadow-sm bg-white dark:bg-zinc-800 text-sm font-medium text-gray-500 dark:text-zinc-300 hover:bg-gray-50 dark:hover:bg-zinc-700 transition"
|
||||
disabled={loading}
|
||||
>
|
||||
<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/(main)/account/profile/page.tsx
Normal file
34
src/app/(main)/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-zinc-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/(main)/account/register/page.tsx
Normal file
94
src/app/(main)/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-zinc-900">
|
||||
<div className="w-full max-w-md bg-white dark:bg-zinc-900 rounded-lg shadow p-8">
|
||||
<h1 className="text-2xl font-bold mb-4 text-gray-900 dark:text-white">Create your account</h1>
|
||||
<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-zinc-700 rounded-md bg-white dark:bg-zinc-800 text-gray-900 dark:text-white focus:outline-none focus:ring-primary-500 focus:border-primary-500"
|
||||
value={email}
|
||||
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-zinc-700 rounded-md bg-white dark:bg-zinc-800 text-gray-900 dark:text-white focus:outline-none focus:ring-primary-500 focus:border-primary-500 pr-10"
|
||||
value={password}
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user