190 lines
8.5 KiB
TypeScript
190 lines
8.5 KiB
TypeScript
import React, { useEffect } from 'react';
|
|
import type { ReactElement } from 'react';
|
|
import Head from 'next/head';
|
|
import Link from 'next/link';
|
|
import { useRouter } from 'next/router';
|
|
import { Formik, Form, Field } from 'formik';
|
|
import { mdiAccount, mdiAsterisk, mdiShieldCheck, mdiArrowLeft } from '@mdi/js';
|
|
import BaseButton from '../components/BaseButton';
|
|
import CardBox from '../components/CardBox';
|
|
import SectionFullScreen from '../components/SectionFullScreen';
|
|
import LayoutGuest from '../layouts/Guest';
|
|
import FormField from '../components/FormField';
|
|
import BaseIcon from '../components/BaseIcon';
|
|
import { useAppDispatch, useAppSelector } from '../stores/hooks';
|
|
import { loginUser, resetAction, findMe } from '../stores/authSlice';
|
|
import { getPageTitle } from '../config';
|
|
|
|
const validate = (values: any) => {
|
|
const errors: any = {};
|
|
if (!values.email) {
|
|
errors.email = 'Required';
|
|
} else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(values.email)) {
|
|
errors.email = 'Invalid email address';
|
|
}
|
|
if (!values.password) {
|
|
errors.password = 'Required';
|
|
}
|
|
return errors;
|
|
};
|
|
|
|
export default function LoginPage() {
|
|
const router = useRouter();
|
|
const dispatch = useAppDispatch();
|
|
const { isFetching, token } = useAppSelector((state) => state.auth);
|
|
const { action, businessId } = router.query;
|
|
const isClaiming = action === 'claim';
|
|
|
|
const title = 'Crafted Network'
|
|
|
|
useEffect(() => {
|
|
if (token) {
|
|
dispatch(findMe());
|
|
// If claiming, redirect back to business details after login
|
|
if (isClaiming && businessId) {
|
|
router.push(`/public/businesses-details?id=${businessId}`);
|
|
} else {
|
|
router.push('/dashboard');
|
|
}
|
|
}
|
|
}, [token, dispatch, isClaiming, businessId, router]);
|
|
|
|
useEffect(() => {
|
|
dispatch(resetAction());
|
|
}, [dispatch]);
|
|
|
|
const handleSubmit = async (values: any) => {
|
|
const { email, password } = values;
|
|
const rest = { email, password };
|
|
await dispatch(loginUser(rest));
|
|
};
|
|
|
|
return (
|
|
<SectionFullScreen bg="white">
|
|
<Head>
|
|
<title>{getPageTitle('Login')} | {title}</title>
|
|
</Head>
|
|
|
|
<div className="flex flex-col lg:flex-row min-h-screen w-full overflow-hidden">
|
|
{/* Left Side: Visual/Branding */}
|
|
<div className="hidden lg:flex lg:w-1/2 bg-slate-900 items-center justify-center p-12 relative overflow-hidden">
|
|
<div className="absolute top-0 left-0 w-full h-full opacity-20">
|
|
<div className="absolute top-1/4 left-1/4 w-96 h-96 bg-emerald-500 blur-[120px] rounded-full"></div>
|
|
<div className="absolute bottom-1/4 right-1/4 w-96 h-96 bg-blue-600 blur-[120px] rounded-full"></div>
|
|
</div>
|
|
|
|
<div className="relative z-10 text-center space-y-8 max-w-md">
|
|
<div className="inline-flex p-4 rounded-3xl bg-emerald-500 shadow-2xl shadow-emerald-500/20 rotate-3">
|
|
<BaseIcon path={mdiShieldCheck} size={48} className="text-slate-900" />
|
|
</div>
|
|
<h1 className="text-5xl font-black text-white tracking-tight">
|
|
The <span className="text-emerald-400">Crafted</span> Network
|
|
</h1>
|
|
<p className="text-xl text-slate-400 leading-relaxed font-medium">
|
|
Join the world's most trusted network for verified professional services.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right Side: Login Form */}
|
|
<div className="w-full lg:w-1/2 flex items-center justify-center p-6 md:p-12 bg-white">
|
|
<div className="w-full max-w-md space-y-10 animate-fade-in">
|
|
{/* Back Button */}
|
|
<Link href="/" className="inline-flex items-center text-sm font-bold text-slate-400 hover:text-emerald-600 transition-colors group">
|
|
<BaseIcon path={mdiArrowLeft} size={20} className="mr-2 group-hover:-translate-x-1 transition-transform" />
|
|
Back to Home
|
|
</Link>
|
|
|
|
<div className="space-y-4">
|
|
<h2 className="text-4xl font-black text-slate-900 tracking-tight">Welcome Back</h2>
|
|
|
|
{isClaiming ? (
|
|
<div className="bg-emerald-50 p-6 rounded-2xl border border-emerald-100 flex items-start space-x-4">
|
|
<div className="p-2 bg-emerald-100 rounded-xl text-emerald-600">
|
|
<BaseIcon path={mdiShieldCheck} size={24} />
|
|
</div>
|
|
<div className="space-y-1">
|
|
<h4 className="font-bold text-emerald-900">Verify Ownership</h4>
|
|
<p className="text-emerald-700 text-sm">Please login or create an account to verify ownership and take control of your business profile.</p>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<p className="text-slate-500">Log in to manage your professional presence and track your requests.</p>
|
|
)}
|
|
</div>
|
|
|
|
<CardBox className="border-none shadow-none p-0">
|
|
<Formik
|
|
initialValues={{ email: '', password: '' }}
|
|
validate={validate}
|
|
onSubmit={handleSubmit}
|
|
>
|
|
{({ errors, touched }) => (
|
|
<Form className="space-y-6">
|
|
<FormField label="Email Address" labelColor="text-slate-900 font-bold" help={touched.email && errors.email ? (errors.email as string) : "Your registered professional email"}>
|
|
<Field
|
|
name="email"
|
|
type="email"
|
|
placeholder="alex@example.com"
|
|
className={`w-full px-5 py-4 bg-slate-50 border-2 ${touched.email && errors.email ? 'border-red-500' : 'border-slate-100'} rounded-2xl focus:border-emerald-500 focus:ring-0 transition-all font-medium text-slate-900`}
|
|
/>
|
|
</FormField>
|
|
|
|
<FormField label="Password" labelColor="text-slate-900 font-bold" help={touched.password && errors.password ? (errors.password as string) : "Security first — keep it safe"}>
|
|
<Field
|
|
name="password"
|
|
type="password"
|
|
placeholder="••••••••"
|
|
className={`w-full px-5 py-4 bg-slate-50 border-2 ${touched.password && errors.password ? 'border-red-500' : 'border-slate-100'} rounded-2xl focus:border-emerald-500 focus:ring-0 transition-all font-medium text-slate-900`}
|
|
/>
|
|
</FormField>
|
|
|
|
<div className="flex items-center justify-between text-sm font-bold pt-2">
|
|
<Link href="/forgot-password" size="sm" className="text-emerald-600 hover:text-emerald-500 transition-colors">
|
|
Forgot Password?
|
|
</Link>
|
|
</div>
|
|
|
|
<BaseButton
|
|
type="submit"
|
|
color="emerald"
|
|
label={isFetching ? 'Verifying...' : 'Login to Dashboard'}
|
|
className="w-full py-5 rounded-2xl font-black text-lg shadow-xl shadow-emerald-500/20 active:scale-[0.98] transition-all"
|
|
disabled={isFetching}
|
|
/>
|
|
</Form>
|
|
)}
|
|
</Formik>
|
|
|
|
<div className="pt-10 text-center space-y-6">
|
|
<div className="relative">
|
|
<div className="absolute inset-0 flex items-center">
|
|
<div className="w-full border-t border-slate-100"></div>
|
|
</div>
|
|
<div className="relative flex justify-center text-xs uppercase font-bold tracking-widest text-slate-400">
|
|
<span className="bg-white px-4">New to the Network?</span>
|
|
</div>
|
|
</div>
|
|
|
|
<Link
|
|
href={isClaiming ? `/register?action=claim&businessId=${businessId}` : "/register"}
|
|
className="block w-full py-4 px-6 rounded-2xl bg-slate-50 border-2 border-slate-100 text-slate-900 font-bold hover:bg-slate-100 transition-all text-center"
|
|
>
|
|
Create Professional Account
|
|
</Link>
|
|
|
|
<p className='text-xs font-medium text-slate-400 pt-8'>
|
|
© 2026 <span>{title}</span>. All rights reserved. Professional Directory Platform.
|
|
</p>
|
|
</div>
|
|
</CardBox>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</SectionFullScreen>
|
|
);
|
|
}
|
|
|
|
LoginPage.getLayout = function getLayout(page: ReactElement) {
|
|
return page;
|
|
}; |