Autosave: 20260404-164039
This commit is contained in:
parent
d8c8294cc5
commit
77b3bcf3a6
@ -34,7 +34,7 @@ const FormField = ({ icons = [], ...props }: Props) => {
|
||||
}
|
||||
|
||||
const controlClassName = [
|
||||
`px-3 py-2 max-w-full border-gray-300 dark:border-dark-700 ${corners} w-full dark:placeholder-gray-400`,
|
||||
`px-3 py-2 max-w-full w-full ${corners} border-gray-300 text-gray-900 placeholder:text-gray-400 dark:border-dark-700 dark:text-white dark:placeholder-gray-400`,
|
||||
`${focusRing}`,
|
||||
props.hasTextareaHeight ? 'h-24' : 'h-12',
|
||||
props.isBorderless ? 'border-0' : 'border',
|
||||
@ -54,10 +54,13 @@ const FormField = ({ icons = [], ...props }: Props) => {
|
||||
</label>
|
||||
)}
|
||||
<div className={`${elementWrapperClass}`}>
|
||||
{Children.map(props.children, (child: ReactElement, index) => (
|
||||
{Children.map(props.children, (child: ReactElement, index) => {
|
||||
const childClassName = (child.props as { className?: string })?.className || ''
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
{cloneElement(child as ReactElement<{ className?: string }>, {
|
||||
className: `${controlClassName} ${icons[index] ? 'pl-10' : ''}`,
|
||||
className: `${controlClassName} ${childClassName} ${icons[index] ? 'pl-10' : ''}`.trim(),
|
||||
})}
|
||||
{icons[index] && (
|
||||
<BaseIcon
|
||||
@ -68,7 +71,8 @@ const FormField = ({ icons = [], ...props }: Props) => {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{props.help && (
|
||||
<div className='text-xs text-gray-500 dark:text-dark-600 mt-1'>{props.help}</div>
|
||||
|
||||
@ -1,77 +1,217 @@
|
||||
import React from 'react';
|
||||
import type { ReactElement } from 'react';
|
||||
import { ToastContainer, toast } from 'react-toastify';
|
||||
import Head from 'next/head';
|
||||
import Link from 'next/link';
|
||||
import { Field, Form, Formik } from 'formik';
|
||||
import { ToastContainer, toast } from 'react-toastify';
|
||||
import axios from 'axios';
|
||||
|
||||
import BaseButton from '../components/BaseButton';
|
||||
import BaseButtons from '../components/BaseButtons';
|
||||
import BaseDivider from '../components/BaseDivider';
|
||||
import CardBox from '../components/CardBox';
|
||||
import FormField from '../components/FormField';
|
||||
import SectionFullScreen from '../components/SectionFullScreen';
|
||||
import LayoutGuest from '../layouts/Guest';
|
||||
import { Field, Form, Formik } from 'formik';
|
||||
import FormField from '../components/FormField';
|
||||
import BaseDivider from '../components/BaseDivider';
|
||||
import BaseButtons from '../components/BaseButtons';
|
||||
import { useRouter } from 'next/router';
|
||||
import { getPageTitle } from '../config';
|
||||
import axios from "axios";
|
||||
import { useAppSelector } from '../stores/hooks';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
type ForgotValues = {
|
||||
email: string;
|
||||
};
|
||||
|
||||
const TITLE = 'Gracey Corporate Stay Portal';
|
||||
const RECOVERY_CHIPS = ['Booking access', 'Service access', 'Billing access'];
|
||||
|
||||
const AUTH_INPUT_CLASS = '!bg-white !text-stone-900 placeholder:!text-stone-400 dark:!bg-white dark:!text-stone-900 dark:placeholder:!text-stone-400';
|
||||
|
||||
export default function Forgot() {
|
||||
const [loading, setLoading] = React.useState(false)
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const router = useRouter();
|
||||
const notify = (type, msg) => toast( msg, {type});
|
||||
const textColor = useAppSelector((state) => state.style.linkColor);
|
||||
const notify = (type, msg) => toast(msg, { type, position: 'bottom-center' });
|
||||
|
||||
const handleSubmit = async (value: ForgotValues) => {
|
||||
setLoading(true);
|
||||
|
||||
const handleSubmit = async (value) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const { data: response } = await axios.post('/auth/send-password-reset-email', value);
|
||||
setLoading(false)
|
||||
notify('success', 'Please check your email for verification link');
|
||||
await axios.post('/auth/send-password-reset-email', value);
|
||||
notify('success', 'Please check your email for a password reset link.');
|
||||
setTimeout(async () => {
|
||||
await router.push('/login')
|
||||
}, 3000)
|
||||
await router.push('/login');
|
||||
}, 3000);
|
||||
} catch (error) {
|
||||
setLoading(false)
|
||||
console.log('error: ', error)
|
||||
notify('error', 'Something was wrong. Try again')
|
||||
console.error('Password reset email request failed:', error);
|
||||
notify('error', 'Something was wrong. Try again.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{getPageTitle('Login')}</title>
|
||||
<title>{getPageTitle('Forgot password')}</title>
|
||||
</Head>
|
||||
|
||||
<SectionFullScreen bg='violet'>
|
||||
<CardBox className='w-11/12 md:w-7/12 lg:w-6/12 xl:w-4/12'>
|
||||
<Formik
|
||||
initialValues={{
|
||||
email: '',
|
||||
}}
|
||||
onSubmit={(values) => handleSubmit(values)}
|
||||
>
|
||||
<Form>
|
||||
<FormField label='Email' help='Please enter your email'>
|
||||
<Field name='email' />
|
||||
</FormField>
|
||||
<div className='min-h-screen w-full bg-gradient-to-br from-[#fbf6ee] via-[#f3eadb] to-[#dfc8a5] px-3 py-4 text-stone-900 sm:px-5 sm:py-6 lg:px-8 lg:py-8'>
|
||||
<div className='mx-auto flex min-h-[calc(100vh-2rem)] w-full max-w-7xl flex-col overflow-hidden rounded-[2rem] border border-stone-200/90 bg-white/75 shadow-[0_30px_80px_rgba(68,44,23,0.12)] backdrop-blur md:grid md:grid-cols-[1.04fr_0.96fr]'>
|
||||
<div className='relative overflow-hidden border-b border-stone-200/80 bg-[linear-gradient(180deg,rgba(255,250,243,0.98),rgba(243,233,216,0.98))] px-5 py-6 sm:px-8 sm:py-8 lg:px-12 lg:py-10 md:border-b-0 md:border-r'>
|
||||
<div className='absolute inset-0 bg-[radial-gradient(circle_at_top_right,rgba(199,152,87,0.18),transparent_28%),radial-gradient(circle_at_bottom_left,rgba(255,255,255,0.85),transparent_34%)]' />
|
||||
|
||||
<BaseDivider />
|
||||
<div className='relative z-10 flex h-full flex-col justify-between gap-6'>
|
||||
<div className='space-y-6 sm:space-y-8'>
|
||||
<div className='space-y-4'>
|
||||
<p className='text-[11px] font-semibold uppercase tracking-[0.34em] text-stone-500'>
|
||||
Credential recovery
|
||||
</p>
|
||||
<div className='space-y-3'>
|
||||
<h1 className='max-w-2xl text-3xl font-semibold leading-tight text-stone-950 sm:text-[2.55rem]'>
|
||||
Restore access without losing your operating context.
|
||||
</h1>
|
||||
<p className='max-w-2xl text-sm leading-7 text-stone-700 sm:text-base'>
|
||||
Request a reset link for the email you use in Gracey and return to the workspace with a fresh
|
||||
password.
|
||||
</p>
|
||||
</div>
|
||||
<div className='flex flex-wrap gap-2'>
|
||||
{RECOVERY_CHIPS.map((chip) => (
|
||||
<span
|
||||
key={chip}
|
||||
className='inline-flex items-center rounded-full border border-stone-200 bg-white/80 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-stone-600'
|
||||
>
|
||||
{chip}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BaseButtons>
|
||||
<BaseButton
|
||||
type='submit'
|
||||
label={loading ? 'Loading...' : 'Submit' }
|
||||
color='info'
|
||||
/>
|
||||
<BaseButton
|
||||
href={'/login'}
|
||||
label={'Login'}
|
||||
color='info'
|
||||
/>
|
||||
</BaseButtons>
|
||||
</Form>
|
||||
</Formik>
|
||||
</CardBox>
|
||||
<div className='grid gap-3 rounded-[1.75rem] border border-amber-200/70 bg-white/60 p-4 sm:grid-cols-3 sm:p-5'>
|
||||
<div>
|
||||
<p className='text-[11px] font-semibold uppercase tracking-[0.22em] text-amber-800'>
|
||||
Delivery path
|
||||
</p>
|
||||
<p className='mt-2 text-sm text-stone-700'>Email-based reset flow</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className='text-[11px] font-semibold uppercase tracking-[0.22em] text-amber-800'>
|
||||
Access posture
|
||||
</p>
|
||||
<p className='mt-2 text-sm text-stone-700'>Private and credential-safe</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className='text-[11px] font-semibold uppercase tracking-[0.22em] text-amber-800'>
|
||||
Return path
|
||||
</p>
|
||||
<p className='mt-2 text-sm text-stone-700'>Back to portal login</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='rounded-[1.75rem] border border-stone-200/90 bg-white/55 p-5'>
|
||||
<div className='flex items-center gap-3'>
|
||||
<span className='h-px w-10 bg-amber-400/80' />
|
||||
<p className='text-xs font-semibold uppercase tracking-[0.28em] text-stone-600'>Recovery notes</p>
|
||||
</div>
|
||||
|
||||
<div className='mt-4 space-y-4 text-sm leading-7 text-stone-700'>
|
||||
<p>
|
||||
Use the same email address you rely on for booking, service, or billing access inside the
|
||||
workspace.
|
||||
</p>
|
||||
<p>
|
||||
Once the request is submitted, Gracey sends a time-sensitive link so you can create a new
|
||||
password and re-enter the portal securely.
|
||||
</p>
|
||||
<p className='text-stone-600'>
|
||||
If you no longer have access to the original inbox, contact an internal administrator before
|
||||
attempting another reset.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='rounded-[1.5rem] border border-stone-200/90 bg-white/55 p-4 text-sm leading-6 text-stone-600'>
|
||||
Recovery links are delivered only to recognized account emails and are intended for authorized
|
||||
operators and customers.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center justify-center bg-[linear-gradient(180deg,rgba(255,255,255,0.74),rgba(255,250,242,0.96))] px-4 py-6 sm:px-8 sm:py-8 lg:px-10 lg:py-10'>
|
||||
<div className='w-full max-w-xl space-y-5 sm:space-y-6'>
|
||||
<CardBox className='border border-stone-200/90 bg-white/95 text-stone-950 shadow-[0_22px_55px_rgba(60,38,20,0.08)] dark:!border-stone-200 dark:!bg-white dark:!text-stone-950'>
|
||||
<div className='space-y-5 sm:space-y-6'>
|
||||
<div className='space-y-3'>
|
||||
<p className='text-xs font-semibold uppercase tracking-[0.28em] text-stone-500'>Password reset</p>
|
||||
<h2 className='text-3xl font-semibold text-stone-950'>Restore your password</h2>
|
||||
<p className='text-sm leading-6 text-stone-600'>
|
||||
Enter your email and we’ll send the next secure step to restore access to the Gracey portal.
|
||||
</p>
|
||||
<div className='flex flex-wrap gap-2 pt-1'>
|
||||
{RECOVERY_CHIPS.map((chip) => (
|
||||
<span
|
||||
key={chip}
|
||||
className='inline-flex items-center rounded-full border border-stone-200 bg-stone-50 px-3 py-1 text-[11px] font-medium text-stone-600'
|
||||
>
|
||||
{chip}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Formik
|
||||
initialValues={{
|
||||
email: '',
|
||||
}}
|
||||
onSubmit={(values) => handleSubmit(values)}
|
||||
>
|
||||
<Form>
|
||||
<FormField label='Email' help='Please enter your email'>
|
||||
<Field type='email' name='email' className={AUTH_INPUT_CLASS} />
|
||||
</FormField>
|
||||
|
||||
<BaseDivider />
|
||||
|
||||
<BaseButtons className='w-full' type='justify-center' classAddon='mb-3' mb='mb-0'>
|
||||
<BaseButton
|
||||
className='w-full justify-center border-[#4d3929] bg-[#4d3929] text-white hover:bg-[#3d2d21] focus:ring-amber-300'
|
||||
type='submit'
|
||||
label={loading ? 'Loading...' : 'Send reset link'}
|
||||
color='white'
|
||||
disabled={loading}
|
||||
/>
|
||||
</BaseButtons>
|
||||
|
||||
<div className='flex flex-col items-center gap-3 text-center sm:flex-row sm:justify-between sm:text-left'>
|
||||
<p className='text-sm text-stone-600'>Remembered your password?</p>
|
||||
<Link className={`${textColor} text-sm font-medium`} href='/login'>
|
||||
Return to Login
|
||||
</Link>
|
||||
</div>
|
||||
</Form>
|
||||
</Formik>
|
||||
</div>
|
||||
</CardBox>
|
||||
|
||||
<div className='flex flex-col items-center justify-center gap-3 border-t border-stone-200 pt-1 text-center text-sm text-stone-600 sm:flex-row'>
|
||||
<p>© 2026 {TITLE}. © All rights reserved</p>
|
||||
<div className='flex flex-wrap items-center justify-center gap-4'>
|
||||
<Link className={`${textColor} text-sm`} href='/privacy-policy/'>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
<Link className={`${textColor} text-sm`} href='/terms-of-use/'>
|
||||
Terms of Use
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SectionFullScreen>
|
||||
|
||||
<ToastContainer />
|
||||
</>
|
||||
);
|
||||
|
||||
@ -1,327 +1,378 @@
|
||||
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import type { ReactElement } from 'react';
|
||||
import Head from 'next/head';
|
||||
import Link from 'next/link';
|
||||
import { Field, Form, Formik } from 'formik';
|
||||
import { ToastContainer, toast } from 'react-toastify';
|
||||
import { mdiEye, mdiEyeOff, mdiInformation } from '@mdi/js';
|
||||
|
||||
import BaseButton from '../components/BaseButton';
|
||||
import BaseButtons from '../components/BaseButtons';
|
||||
import BaseDivider from '../components/BaseDivider';
|
||||
import BaseIcon from '../components/BaseIcon';
|
||||
import CardBox from '../components/CardBox';
|
||||
import BaseIcon from "../components/BaseIcon";
|
||||
import { mdiInformation, mdiEye, mdiEyeOff } from '@mdi/js';
|
||||
import FormCheckRadio from '../components/FormCheckRadio';
|
||||
import FormField from '../components/FormField';
|
||||
import SectionFullScreen from '../components/SectionFullScreen';
|
||||
import LayoutGuest from '../layouts/Guest';
|
||||
import { Field, Form, Formik } from 'formik';
|
||||
import FormField from '../components/FormField';
|
||||
import FormCheckRadio from '../components/FormCheckRadio';
|
||||
import BaseDivider from '../components/BaseDivider';
|
||||
import BaseButtons from '../components/BaseButtons';
|
||||
import { useRouter } from 'next/router';
|
||||
import { getPageTitle } from '../config';
|
||||
import { findMe, loginUser, resetAction } from '../stores/authSlice';
|
||||
import { useAppDispatch, useAppSelector } from '../stores/hooks';
|
||||
import Link from 'next/link';
|
||||
import {toast, ToastContainer} from "react-toastify";
|
||||
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels'
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
type LoginValues = {
|
||||
email: string;
|
||||
password: string;
|
||||
remember: boolean;
|
||||
};
|
||||
|
||||
type DemoAccount = {
|
||||
email: string;
|
||||
password: string;
|
||||
role: string;
|
||||
accent: string;
|
||||
summary: string;
|
||||
};
|
||||
|
||||
const DEMO_ACCOUNTS: DemoAccount[] = [
|
||||
{
|
||||
email: 'super_admin@flatlogic.com',
|
||||
password: '946cafba',
|
||||
role: 'Super Admin',
|
||||
accent: 'border-stone-300 bg-stone-900 text-white',
|
||||
summary: 'Global workspace control across organizations and roles.',
|
||||
},
|
||||
{
|
||||
email: 'admin@flatlogic.com',
|
||||
password: '946cafba',
|
||||
role: 'Admin',
|
||||
accent: 'border-amber-200 bg-amber-50 text-stone-900',
|
||||
summary: 'Day-to-day operating visibility with organization context.',
|
||||
},
|
||||
{
|
||||
email: 'john@doe.com',
|
||||
password: 'bc40952c93f4',
|
||||
role: 'Concierge',
|
||||
accent: 'border-stone-200 bg-white text-stone-900',
|
||||
summary: 'Service coordination, reservation readiness, and updates.',
|
||||
},
|
||||
{
|
||||
email: 'client@hello.com',
|
||||
password: 'bc40952c93f4',
|
||||
role: 'Customer',
|
||||
accent: 'border-amber-200 bg-white text-stone-900',
|
||||
summary: 'Customer-side request access and stay visibility.',
|
||||
},
|
||||
];
|
||||
|
||||
const ACCESS_CHIPS = ['Super Admin', 'Admin', 'Concierge', 'Customer'];
|
||||
|
||||
const AUTH_INPUT_CLASS = '!bg-white !text-stone-900 placeholder:!text-stone-400 dark:!bg-white dark:!text-stone-900 dark:placeholder:!text-stone-400';
|
||||
|
||||
const DEFAULT_LOGIN_VALUES: LoginValues = {
|
||||
email: DEMO_ACCOUNTS[0].email,
|
||||
password: DEMO_ACCOUNTS[0].password,
|
||||
remember: true,
|
||||
};
|
||||
|
||||
export default function Login() {
|
||||
const router = useRouter();
|
||||
const dispatch = useAppDispatch();
|
||||
const textColor = useAppSelector((state) => state.style.linkColor);
|
||||
const iconsColor = useAppSelector((state) => state.style.iconsColor);
|
||||
const notify = (type, msg) => toast(msg, { type });
|
||||
const [ illustrationImage, setIllustrationImage ] = useState({
|
||||
src: undefined,
|
||||
photographer: undefined,
|
||||
photographer_url: undefined,
|
||||
})
|
||||
const [ illustrationVideo, setIllustrationVideo ] = useState({video_files: []})
|
||||
const [contentType, setContentType] = useState('image');
|
||||
const [contentPosition, setContentPosition] = useState('right');
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const { currentUser, isFetching, errorMessage, token, notify:notifyState } = useAppSelector(
|
||||
const { currentUser, isFetching, errorMessage, token, notify: notifyState } = useAppSelector(
|
||||
(state) => state.auth,
|
||||
);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [initialValues, setInitialValues] = useState<LoginValues>(DEFAULT_LOGIN_VALUES);
|
||||
|
||||
const notify = (type, msg) => toast(msg, { type });
|
||||
const title = 'Gracey Corporate Stay Portal';
|
||||
|
||||
const registeredOrg = typeof router.query.registeredOrg === 'string' ? router.query.registeredOrg : '';
|
||||
const registeredRole = typeof router.query.registeredRole === 'string' ? router.query.registeredRole : '';
|
||||
const registeredTenants =
|
||||
typeof router.query.registeredTenants === 'string'
|
||||
? router.query.registeredTenants.split(' | ').filter(Boolean)
|
||||
: [];
|
||||
const [initialValues, setInitialValues] = React.useState({ email:'super_admin@flatlogic.com',
|
||||
password: '946cafba',
|
||||
remember: true })
|
||||
|
||||
const title = 'Gracey Corporate Stay Portal'
|
||||
|
||||
// Fetch Pexels image/video
|
||||
useEffect( () => {
|
||||
async function fetchData() {
|
||||
const image = await getPexelsImage()
|
||||
const video = await getPexelsVideo()
|
||||
setIllustrationImage(image);
|
||||
setIllustrationVideo(video);
|
||||
}
|
||||
fetchData();
|
||||
}, []);
|
||||
// Fetch user data
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
dispatch(findMe());
|
||||
}
|
||||
}, [token, dispatch]);
|
||||
// Redirect to dashboard if user is logged in
|
||||
|
||||
useEffect(() => {
|
||||
if (currentUser?.id) {
|
||||
router.push('/command-center');
|
||||
}
|
||||
}, [currentUser?.id, router]);
|
||||
// Show error message if there is one
|
||||
|
||||
useEffect(() => {
|
||||
if (errorMessage){
|
||||
notify('error', errorMessage)
|
||||
if (errorMessage) {
|
||||
notify('error', errorMessage);
|
||||
}
|
||||
}, [errorMessage]);
|
||||
|
||||
}, [errorMessage])
|
||||
// Show notification if there is one
|
||||
useEffect(() => {
|
||||
if (notifyState?.showNotification) {
|
||||
notify('success', notifyState?.textNotification)
|
||||
dispatch(resetAction());
|
||||
}
|
||||
}, [notifyState?.showNotification])
|
||||
if (notifyState?.showNotification) {
|
||||
notify('success', notifyState?.textNotification);
|
||||
dispatch(resetAction());
|
||||
}
|
||||
}, [notifyState?.showNotification, notifyState?.textNotification, dispatch]);
|
||||
|
||||
const togglePasswordVisibility = () => {
|
||||
setShowPassword(!showPassword);
|
||||
};
|
||||
|
||||
const handleSubmit = async (value) => {
|
||||
const {remember, ...rest} = value
|
||||
const handleSubmit = async (value: LoginValues) => {
|
||||
const { remember, ...rest } = value;
|
||||
await dispatch(loginUser(rest));
|
||||
};
|
||||
|
||||
const setLogin = (target: HTMLElement) => {
|
||||
setInitialValues(prev => ({
|
||||
...prev,
|
||||
email : target.innerText.trim(),
|
||||
password: target.dataset.password ?? '',
|
||||
}));
|
||||
const chooseLogin = (email: string, password: string) => {
|
||||
setInitialValues((prev) => ({
|
||||
...prev,
|
||||
email,
|
||||
password,
|
||||
}));
|
||||
};
|
||||
|
||||
const imageBlock = (image) => (
|
||||
<div className="hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3"
|
||||
style={{
|
||||
backgroundImage: `${image ? `url(${image.src?.original})` : 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'}`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'left center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
}}>
|
||||
<div className="flex justify-center w-full bg-blue-300/20">
|
||||
<a className="text-[8px]" href={image?.photographer_url} target="_blank" rel="noreferrer">Photo
|
||||
by {image?.photographer} on Pexels</a>
|
||||
const registeredMessage = useMemo(() => {
|
||||
if (!registeredOrg) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<CardBox className='border border-amber-200 bg-amber-50/90 text-stone-900 shadow-none'>
|
||||
<div className='flex items-start gap-3'>
|
||||
<BaseIcon className='mt-1 text-amber-700' size={20} path={mdiInformation} />
|
||||
<div className='min-w-0 space-y-2'>
|
||||
<p className='font-semibold'>
|
||||
Your {registeredRole || 'customer'} account was created for {registeredOrg}.
|
||||
</p>
|
||||
{registeredTenants.length ? (
|
||||
<div>
|
||||
<p className='text-xs font-semibold uppercase tracking-[0.18em] text-amber-800'>Linked tenants</p>
|
||||
<div className='mt-2 flex flex-wrap gap-2'>
|
||||
{registeredTenants.map((tenantName) => (
|
||||
<span
|
||||
key={tenantName}
|
||||
className='inline-flex items-center rounded-full border border-amber-200 bg-white/90 px-3 py-1 text-xs font-medium text-stone-900'
|
||||
>
|
||||
{tenantName}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className='text-sm text-stone-700'>No linked tenant is shown for that organization yet.</p>
|
||||
)}
|
||||
<p className='text-xs text-stone-700'>
|
||||
Admin and Super Admin accounts are provisioned internally, not through self-signup.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const videoBlock = (video) => {
|
||||
if (video?.video_files?.length > 0) {
|
||||
return (
|
||||
<div className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'>
|
||||
<video
|
||||
className='absolute top-0 left-0 w-full h-full object-cover'
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
>
|
||||
<source src={video.video_files[0]?.link} type='video/mp4'/>
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
<div className='flex justify-center w-full bg-blue-300/20 z-10'>
|
||||
<a
|
||||
className='text-[8px]'
|
||||
href={video.user.url}
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
Video by {video.user.name} on Pexels
|
||||
</a>
|
||||
</div>
|
||||
</div>)
|
||||
}
|
||||
};
|
||||
</div>
|
||||
</CardBox>
|
||||
);
|
||||
}, [registeredOrg, registeredRole, registeredTenants]);
|
||||
|
||||
return (
|
||||
<div style={contentPosition === 'background' ? {
|
||||
backgroundImage: `${
|
||||
illustrationImage
|
||||
? `url(${illustrationImage.src?.original})`
|
||||
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
|
||||
}`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'left center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
} : {}}>
|
||||
<Head>
|
||||
<title>{getPageTitle('Login')}</title>
|
||||
</Head>
|
||||
<>
|
||||
<Head>
|
||||
<title>{getPageTitle('Login')}</title>
|
||||
</Head>
|
||||
|
||||
<SectionFullScreen bg='violet'>
|
||||
<div className={`flex ${contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'} min-h-screen w-full`}>
|
||||
{contentType === 'image' && contentPosition !== 'background' ? imageBlock(illustrationImage) : null}
|
||||
{contentType === 'video' && contentPosition !== 'background' ? videoBlock(illustrationVideo) : null}
|
||||
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
|
||||
|
||||
<CardBox id="loginRoles" className='w-full md:w-3/5 lg:w-2/3'>
|
||||
|
||||
<h2 className="text-4xl font-semibold my-4">{title}</h2>
|
||||
|
||||
<div className='flex flex-row justify-between'>
|
||||
<div>
|
||||
|
||||
<p className='mb-2'>Use{' '}
|
||||
<code className={`cursor-pointer ${textColor} `}
|
||||
data-password="946cafba"
|
||||
onClick={(e) => setLogin(e.target)}>super_admin@flatlogic.com</code>{' / '}
|
||||
<code className={`${textColor}`}>946cafba</code>{' / '}
|
||||
to login as Super Admin</p>
|
||||
|
||||
<p className='mb-2'>Use{' '}
|
||||
<code className={`cursor-pointer ${textColor} `}
|
||||
data-password="946cafba"
|
||||
onClick={(e) => setLogin(e.target)}>admin@flatlogic.com</code>{' / '}
|
||||
<code className={`${textColor}`}>946cafba</code>{' / '}
|
||||
to login as Admin</p>
|
||||
<p className='mb-2'>Use <code
|
||||
className={`cursor-pointer ${textColor} `}
|
||||
data-password="bc40952c93f4"
|
||||
onClick={(e) => setLogin(e.target)}>john@doe.com</code>{' / '}
|
||||
<code className={`${textColor}`}>bc40952c93f4</code>{' / '}
|
||||
to login as Concierge</p>
|
||||
<p>Use <code
|
||||
className={`cursor-pointer ${textColor} `}
|
||||
data-password="bc40952c93f4"
|
||||
onClick={(e) => setLogin(e.target)}>client@hello.com</code>{' / '}
|
||||
<code className={`${textColor}`}>bc40952c93f4</code>{' / '}
|
||||
to login as Customer</p>
|
||||
</div>
|
||||
<div>
|
||||
<BaseIcon
|
||||
className={`${iconsColor}`}
|
||||
w='w-16'
|
||||
h='h-16'
|
||||
size={48}
|
||||
path={mdiInformation}
|
||||
/>
|
||||
</div>
|
||||
<SectionFullScreen bg='violet'>
|
||||
<div className='min-h-screen w-full bg-gradient-to-br from-[#fbf6ee] via-[#f3eadb] to-[#dfc8a5] px-3 py-4 text-stone-900 sm:px-5 sm:py-6 lg:px-8 lg:py-8'>
|
||||
<div className='mx-auto flex min-h-[calc(100vh-2rem)] w-full max-w-7xl flex-col overflow-hidden rounded-[2rem] border border-stone-200/90 bg-white/75 shadow-[0_30px_80px_rgba(68,44,23,0.12)] backdrop-blur md:grid md:grid-cols-[1.06fr_0.94fr]'>
|
||||
<div className='relative overflow-hidden border-b border-stone-200/80 bg-[linear-gradient(180deg,rgba(255,250,243,0.98),rgba(243,233,216,0.98))] px-5 py-6 sm:px-8 sm:py-8 lg:px-12 lg:py-10 md:border-b-0 md:border-r'>
|
||||
<div className='absolute inset-0 bg-[radial-gradient(circle_at_top_right,rgba(199,152,87,0.18),transparent_28%),radial-gradient(circle_at_bottom_left,rgba(255,255,255,0.85),transparent_34%)]' />
|
||||
|
||||
<div className='relative z-10 flex h-full flex-col justify-between gap-6'>
|
||||
<div className='space-y-6 sm:space-y-8'>
|
||||
<div className='space-y-4'>
|
||||
<p className='text-[11px] font-semibold uppercase tracking-[0.34em] text-stone-500'>
|
||||
Portal access
|
||||
</p>
|
||||
<div className='space-y-3'>
|
||||
<h1 className='max-w-2xl text-3xl font-semibold leading-tight text-stone-950 sm:text-[2.6rem]'>
|
||||
{title}
|
||||
</h1>
|
||||
<p className='max-w-2xl text-sm leading-7 text-stone-700 sm:text-base'>
|
||||
Sign in to the private workspace used for booking intake, guest coordination, and billing
|
||||
follow-through.
|
||||
</p>
|
||||
</div>
|
||||
</CardBox>
|
||||
|
||||
{registeredOrg ? (
|
||||
<CardBox className='w-full border border-blue-200 bg-blue-50 text-blue-950 shadow-none md:w-3/5 lg:w-2/3 dark:border-blue-900 dark:bg-blue-950/40 dark:text-blue-100'>
|
||||
<div className='flex items-start gap-3'>
|
||||
<BaseIcon className='mt-1 text-blue-700 dark:text-blue-200' size={20} path={mdiInformation} />
|
||||
<div className='min-w-0 space-y-2'>
|
||||
<p className='font-semibold'>
|
||||
Your {registeredRole || 'customer'} account was created for {registeredOrg}.
|
||||
</p>
|
||||
{registeredTenants.length ? (
|
||||
<div>
|
||||
<p className='text-xs font-semibold uppercase tracking-[0.18em] text-blue-800 dark:text-blue-200'>Linked tenants</p>
|
||||
<div className='mt-2 flex flex-wrap gap-2'>
|
||||
{registeredTenants.map((tenantName) => (
|
||||
<span
|
||||
key={tenantName}
|
||||
className='inline-flex items-center rounded-full border border-blue-200 bg-white/80 px-3 py-1 text-xs font-medium text-blue-900 dark:border-blue-800 dark:bg-blue-900/40 dark:text-blue-100'
|
||||
>
|
||||
{tenantName}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className='text-sm text-blue-800 dark:text-blue-100'>
|
||||
No linked tenant is shown for that organization yet.
|
||||
</p>
|
||||
)}
|
||||
<p className='text-xs text-blue-800 dark:text-blue-100'>
|
||||
Admin and Super Admin accounts are provisioned internally, not through self-signup.
|
||||
</p>
|
||||
<div className='flex flex-wrap gap-2'>
|
||||
{ACCESS_CHIPS.map((chip) => (
|
||||
<span
|
||||
key={chip}
|
||||
className='inline-flex items-center rounded-full border border-stone-200 bg-white/80 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-stone-600'
|
||||
>
|
||||
{chip}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='grid gap-3 rounded-[1.75rem] border border-amber-200/70 bg-white/60 p-4 sm:grid-cols-3 sm:p-5'>
|
||||
<div>
|
||||
<p className='text-[11px] font-semibold uppercase tracking-[0.22em] text-amber-800'>
|
||||
Operator view
|
||||
</p>
|
||||
<p className='mt-2 text-sm text-stone-700'>Multi-tenant workspace</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className='text-[11px] font-semibold uppercase tracking-[0.22em] text-amber-800'>
|
||||
Service posture
|
||||
</p>
|
||||
<p className='mt-2 text-sm text-stone-700'>Calm, selective, precise</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className='text-[11px] font-semibold uppercase tracking-[0.22em] text-amber-800'>
|
||||
Execution model
|
||||
</p>
|
||||
<p className='mt-2 text-sm text-stone-700'>Request to reservation to billing</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='space-y-4'>
|
||||
<div className='flex items-center gap-3'>
|
||||
<span className='h-px w-10 bg-amber-400/80' />
|
||||
<p className='text-xs font-semibold uppercase tracking-[0.28em] text-stone-600'>
|
||||
Demo account access
|
||||
</p>
|
||||
</div>
|
||||
<div className='grid gap-3 lg:grid-cols-2'>
|
||||
{DEMO_ACCOUNTS.map((account) => {
|
||||
const isSelected =
|
||||
initialValues.email === account.email && initialValues.password === account.password;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={account.email}
|
||||
type='button'
|
||||
onClick={() => chooseLogin(account.email, account.password)}
|
||||
className={`rounded-[1.5rem] border p-4 text-left transition duration-150 hover:-translate-y-0.5 hover:shadow-lg ${account.accent} ${
|
||||
isSelected ? 'ring-2 ring-amber-400/70' : 'ring-1 ring-transparent'
|
||||
}`}
|
||||
>
|
||||
<div className='flex flex-wrap items-center justify-between gap-2'>
|
||||
<p className='text-[11px] font-semibold uppercase tracking-[0.25em] text-current/70'>
|
||||
{account.role}
|
||||
</p>
|
||||
<span className='rounded-full border border-current/15 px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-current/70'>
|
||||
Tap to fill
|
||||
</span>
|
||||
</div>
|
||||
<p className='mt-3 break-all text-sm font-medium'>{account.email}</p>
|
||||
<p className='mt-2 font-mono text-sm opacity-80'>{account.password}</p>
|
||||
<p className='mt-3 text-xs leading-5 opacity-75'>{account.summary}</p>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='rounded-[1.5rem] border border-stone-200/90 bg-white/55 p-4 text-sm leading-6 text-stone-600'>
|
||||
The public experience stays polished and restrained. Operational depth appears only where it
|
||||
belongs: inside the authenticated workspace.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center justify-center bg-[linear-gradient(180deg,rgba(255,255,255,0.74),rgba(255,250,242,0.96))] px-4 py-6 sm:px-8 sm:py-8 lg:px-10 lg:py-10'>
|
||||
<div className='w-full max-w-xl space-y-5 sm:space-y-6'>
|
||||
{registeredMessage}
|
||||
|
||||
<CardBox className='border border-stone-200/90 bg-white/95 text-stone-950 shadow-[0_22px_55px_rgba(60,38,20,0.08)] dark:!border-stone-200 dark:!bg-white dark:!text-stone-950'>
|
||||
<div className='space-y-5 sm:space-y-6'>
|
||||
<div className='space-y-3'>
|
||||
<p className='text-xs font-semibold uppercase tracking-[0.28em] text-stone-500'>Member sign in</p>
|
||||
<h2 className='text-3xl font-semibold text-stone-950'>Welcome back</h2>
|
||||
<p className='text-sm leading-6 text-stone-600'>
|
||||
Enter your credentials to open the Gracey workspace.
|
||||
</p>
|
||||
<div className='flex flex-wrap gap-2 pt-1'>
|
||||
{ACCESS_CHIPS.map((chip) => (
|
||||
<span
|
||||
key={chip}
|
||||
className='inline-flex items-center rounded-full border border-stone-200 bg-stone-50 px-3 py-1 text-[11px] font-medium text-stone-600'
|
||||
>
|
||||
{chip}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Formik initialValues={initialValues} enableReinitialize onSubmit={handleSubmit}>
|
||||
<Form>
|
||||
<FormField label='Login' help='Please enter your login'>
|
||||
<Field name='email' className={AUTH_INPUT_CLASS} />
|
||||
</FormField>
|
||||
|
||||
<div className='relative'>
|
||||
<FormField label='Password' help='Please enter your password'>
|
||||
<Field name='password' type={showPassword ? 'text' : 'password'} className={AUTH_INPUT_CLASS} />
|
||||
</FormField>
|
||||
<button
|
||||
type='button'
|
||||
className='absolute bottom-8 right-0 flex items-center pr-3 text-stone-500 transition hover:text-stone-800'
|
||||
onClick={() => setShowPassword((prev) => !prev)}
|
||||
>
|
||||
<BaseIcon size={20} path={showPassword ? mdiEyeOff : mdiEye} />
|
||||
</button>
|
||||
</div>
|
||||
</CardBox>
|
||||
) : null}
|
||||
|
||||
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
enableReinitialize
|
||||
onSubmit={(values) => handleSubmit(values)}
|
||||
>
|
||||
<Form>
|
||||
<FormField
|
||||
label='Login'
|
||||
help='Please enter your login'>
|
||||
<Field name='email' />
|
||||
</FormField>
|
||||
<div className='flex flex-col gap-3 text-sm sm:flex-row sm:items-center sm:justify-between'>
|
||||
<FormCheckRadio type='checkbox' label='Remember'>
|
||||
<Field type='checkbox' name='remember' />
|
||||
</FormCheckRadio>
|
||||
|
||||
<div className='relative'>
|
||||
<FormField
|
||||
label='Password'
|
||||
help='Please enter your password'>
|
||||
<Field name='password' type={showPassword ? 'text' : 'password'} />
|
||||
</FormField>
|
||||
<div
|
||||
className='absolute bottom-8 right-0 pr-3 flex items-center cursor-pointer'
|
||||
onClick={togglePasswordVisibility}
|
||||
>
|
||||
<BaseIcon
|
||||
className='text-gray-500 hover:text-gray-700'
|
||||
size={20}
|
||||
path={showPassword ? mdiEyeOff : mdiEye}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Link className={`${textColor} text-sm font-medium`} href='/forgot'>
|
||||
Forgot password?
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className={'flex justify-between'}>
|
||||
<FormCheckRadio type='checkbox' label='Remember'>
|
||||
<Field type='checkbox' name='remember' />
|
||||
</FormCheckRadio>
|
||||
<BaseDivider />
|
||||
|
||||
<Link className={`${textColor} text-blue-600`} href={'/forgot'}>
|
||||
Forgot password?
|
||||
</Link>
|
||||
</div>
|
||||
<BaseButtons className='w-full' type='justify-center' classAddon='mb-3' mb='mb-0'>
|
||||
<BaseButton
|
||||
className='w-full justify-center border-[#4d3929] bg-[#4d3929] text-white hover:bg-[#3d2d21] focus:ring-amber-300'
|
||||
type='submit'
|
||||
label={isFetching ? 'Loading...' : 'Login'}
|
||||
color='white'
|
||||
disabled={isFetching}
|
||||
/>
|
||||
</BaseButtons>
|
||||
|
||||
<BaseDivider />
|
||||
|
||||
<BaseButtons>
|
||||
<BaseButton
|
||||
className={'w-full'}
|
||||
type='submit'
|
||||
label={isFetching ? 'Loading...' : 'Login'}
|
||||
color='info'
|
||||
disabled={isFetching}
|
||||
/>
|
||||
</BaseButtons>
|
||||
<br />
|
||||
<p className={'text-center'}>
|
||||
Don’t have an account yet?{' '}
|
||||
<Link className={`${textColor}`} href={'/register'}>
|
||||
New Account
|
||||
</Link>
|
||||
</p>
|
||||
</Form>
|
||||
<p className='mt-4 text-center text-sm text-stone-600'>
|
||||
Don’t have an account yet?{' '}
|
||||
<Link className={`${textColor} font-medium`} href='/register'>
|
||||
New Account
|
||||
</Link>
|
||||
</p>
|
||||
</Form>
|
||||
</Formik>
|
||||
</div>
|
||||
</CardBox>
|
||||
|
||||
<div className='flex flex-col items-center justify-center gap-3 border-t border-stone-200 pt-1 text-center text-sm text-stone-600 sm:flex-row'>
|
||||
<p>© 2026 {title}. © All rights reserved</p>
|
||||
<div className='flex flex-wrap items-center justify-center gap-4'>
|
||||
<Link className={`${textColor} text-sm`} href='/privacy-policy/'>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
<Link className={`${textColor} text-sm`} href='/terms-of-use/'>
|
||||
Terms of Use
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SectionFullScreen>
|
||||
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
|
||||
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. © All rights reserved</p>
|
||||
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
</div>
|
||||
<ToastContainer />
|
||||
</div>
|
||||
</SectionFullScreen>
|
||||
|
||||
<ToastContainer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -1,20 +1,22 @@
|
||||
import React from 'react';
|
||||
import type { ReactElement } from 'react';
|
||||
import { ToastContainer, toast } from 'react-toastify';
|
||||
import Head from 'next/head';
|
||||
import BaseButton from '../components/BaseButton';
|
||||
import CardBox from '../components/CardBox';
|
||||
import SectionFullScreen from '../components/SectionFullScreen';
|
||||
import LayoutGuest from '../layouts/Guest';
|
||||
import Link from 'next/link';
|
||||
import { Field, Form, Formik } from 'formik';
|
||||
import FormField from '../components/FormField';
|
||||
import BaseDivider from '../components/BaseDivider';
|
||||
import BaseButtons from '../components/BaseButtons';
|
||||
import { useRouter } from 'next/router';
|
||||
import { getPageTitle } from '../config';
|
||||
import { ToastContainer, toast } from 'react-toastify';
|
||||
import Select from 'react-select';
|
||||
import axios from 'axios';
|
||||
import ConnectedEntityNotice from '../components/ConnectedEntityNotice';
|
||||
|
||||
import BaseButton from '../components/BaseButton';
|
||||
import BaseButtons from '../components/BaseButtons';
|
||||
import BaseDivider from '../components/BaseDivider';
|
||||
import CardBox from '../components/CardBox';
|
||||
import FormField from '../components/FormField';
|
||||
import SectionFullScreen from '../components/SectionFullScreen';
|
||||
import LayoutGuest from '../layouts/Guest';
|
||||
import { getPageTitle } from '../config';
|
||||
import { useAppSelector } from '../stores/hooks';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
type PublicTenantOption = {
|
||||
id: string;
|
||||
@ -40,11 +42,23 @@ type OrganizationSelectOption = {
|
||||
organization: PublicOrganizationOption;
|
||||
};
|
||||
|
||||
type RegisterValues = {
|
||||
email: string;
|
||||
password: string;
|
||||
confirm: string;
|
||||
};
|
||||
|
||||
const TITLE = 'Gracey Corporate Stay Portal';
|
||||
const REGISTER_CHIPS = ['Customer self-signup', 'Organization-linked access', 'Internal admin provisioning'];
|
||||
|
||||
const AUTH_INPUT_CLASS = '!bg-white !text-stone-900 placeholder:!text-stone-400 dark:!bg-white dark:!text-stone-900 dark:placeholder:!text-stone-400';
|
||||
|
||||
export default function Register() {
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [organizations, setOrganizations] = React.useState<PublicOrganizationOption[]>([]);
|
||||
const [selectedOrganization, setSelectedOrganization] = React.useState<OrganizationSelectOption | null>(null);
|
||||
const router = useRouter();
|
||||
const textColor = useAppSelector((state) => state.style.linkColor);
|
||||
const notify = (type, msg) => toast(msg, { type, position: 'bottom-center' });
|
||||
|
||||
React.useEffect(() => {
|
||||
@ -69,7 +83,7 @@ export default function Register() {
|
||||
const selectedOrganizationRecord = selectedOrganization?.organization || null;
|
||||
const selectedTenants = selectedOrganizationRecord?.linkedTenants || [];
|
||||
|
||||
const handleSubmit = async (value) => {
|
||||
const handleSubmit = async (value: RegisterValues) => {
|
||||
if (!selectedOrganization) {
|
||||
notify('error', 'Please select your organization before creating the account.');
|
||||
return;
|
||||
@ -114,87 +128,216 @@ export default function Register() {
|
||||
<title>{getPageTitle('Register')}</title>
|
||||
</Head>
|
||||
|
||||
<SectionFullScreen bg="violet">
|
||||
<CardBox className="w-11/12 md:w-7/12 lg:w-6/12 xl:w-4/12">
|
||||
<Formik
|
||||
initialValues={{
|
||||
email: '',
|
||||
password: '',
|
||||
confirm: '',
|
||||
}}
|
||||
onSubmit={(values) => handleSubmit(values)}
|
||||
>
|
||||
<Form>
|
||||
<label className="mb-2 block font-bold">Organization</label>
|
||||
<SectionFullScreen bg='violet'>
|
||||
<div className='min-h-screen w-full bg-gradient-to-br from-[#fbf6ee] via-[#f3eadb] to-[#dfc8a5] px-3 py-4 text-stone-900 sm:px-5 sm:py-6 lg:px-8 lg:py-8'>
|
||||
<div className='mx-auto flex min-h-[calc(100vh-2rem)] w-full max-w-7xl flex-col overflow-hidden rounded-[2rem] border border-stone-200/90 bg-white/75 shadow-[0_30px_80px_rgba(68,44,23,0.12)] backdrop-blur md:grid md:grid-cols-[1.04fr_0.96fr]'>
|
||||
<div className='relative overflow-hidden border-b border-stone-200/80 bg-[linear-gradient(180deg,rgba(255,250,243,0.98),rgba(243,233,216,0.98))] px-5 py-6 sm:px-8 sm:py-8 lg:px-12 lg:py-10 md:border-b-0 md:border-r'>
|
||||
<div className='absolute inset-0 bg-[radial-gradient(circle_at_top_right,rgba(199,152,87,0.18),transparent_28%),radial-gradient(circle_at_bottom_left,rgba(255,255,255,0.85),transparent_34%)]' />
|
||||
|
||||
<Select<OrganizationSelectOption, false>
|
||||
classNames={{
|
||||
control: () => 'px-1 mb-4 py-2',
|
||||
}}
|
||||
value={selectedOrganization}
|
||||
onChange={(option) => setSelectedOrganization(option)}
|
||||
options={options}
|
||||
placeholder="Select organization..."
|
||||
/>
|
||||
|
||||
{selectedOrganizationRecord ? (
|
||||
<ConnectedEntityNotice
|
||||
title="Account context"
|
||||
description={
|
||||
<div className="space-y-3">
|
||||
<p>
|
||||
Self-signup creates a <span className="font-semibold">Customer</span> account inside{' '}
|
||||
<span className="font-semibold">{selectedOrganizationRecord.name}</span>.
|
||||
</p>
|
||||
<div>
|
||||
<p className="mb-2 text-xs font-semibold uppercase tracking-[0.18em] text-blue-800 dark:text-blue-200">
|
||||
Linked tenant preview
|
||||
</p>
|
||||
{selectedTenants.length ? (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedTenants.map((tenant) => (
|
||||
<span
|
||||
key={tenant.id}
|
||||
className="inline-flex items-center rounded-full border border-blue-200 bg-white/80 px-3 py-1 text-xs font-medium text-blue-900 dark:border-blue-800 dark:bg-blue-900/40 dark:text-blue-100"
|
||||
>
|
||||
{tenant.name || 'Unnamed tenant'}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-blue-800 dark:text-blue-100">
|
||||
No linked tenant is visible for this organization yet.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-blue-800 dark:text-blue-100">
|
||||
Admin and Super Admin accounts are created internally by an existing privileged user.
|
||||
<div className='relative z-10 flex h-full flex-col justify-between gap-6'>
|
||||
<div className='space-y-6 sm:space-y-8'>
|
||||
<div className='space-y-4'>
|
||||
<p className='text-[11px] font-semibold uppercase tracking-[0.34em] text-stone-500'>
|
||||
Customer enrollment
|
||||
</p>
|
||||
<div className='space-y-3'>
|
||||
<h1 className='max-w-2xl text-3xl font-semibold leading-tight text-stone-950 sm:text-[2.55rem]'>
|
||||
Create portal access with the right organization attached.
|
||||
</h1>
|
||||
<p className='max-w-2xl text-sm leading-7 text-stone-700 sm:text-base'>
|
||||
Register a customer account for Gracey and connect it to the correct organization before the
|
||||
first stay request is submitted.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
<div className='flex flex-wrap gap-2'>
|
||||
{REGISTER_CHIPS.map((chip) => (
|
||||
<span
|
||||
key={chip}
|
||||
className='inline-flex items-center rounded-full border border-stone-200 bg-white/80 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-stone-600'
|
||||
>
|
||||
{chip}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FormField label="Email" help="Please enter your email">
|
||||
<Field type="email" name="email" />
|
||||
</FormField>
|
||||
<FormField label="Password" help="Please enter your password">
|
||||
<Field type="password" name="password" />
|
||||
</FormField>
|
||||
<FormField label="Confirm Password" help="Please confirm your password">
|
||||
<Field type="password" name="confirm" />
|
||||
</FormField>
|
||||
<div className='grid gap-3 rounded-[1.75rem] border border-amber-200/70 bg-white/60 p-4 sm:grid-cols-3 sm:p-5'>
|
||||
<div>
|
||||
<p className='text-[11px] font-semibold uppercase tracking-[0.22em] text-amber-800'>
|
||||
Audience
|
||||
</p>
|
||||
<p className='mt-2 text-sm text-stone-700'>Customer-side account setup</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className='text-[11px] font-semibold uppercase tracking-[0.22em] text-amber-800'>
|
||||
Organization scope
|
||||
</p>
|
||||
<p className='mt-2 text-sm text-stone-700'>Attach access to the right company</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className='text-[11px] font-semibold uppercase tracking-[0.22em] text-amber-800'>
|
||||
Admin note
|
||||
</p>
|
||||
<p className='mt-2 text-sm text-stone-700'>Admin roles stay internally provisioned</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BaseDivider />
|
||||
<div className='rounded-[1.75rem] border border-stone-200/90 bg-white/55 p-5'>
|
||||
<div className='flex items-center gap-3'>
|
||||
<span className='h-px w-10 bg-amber-400/80' />
|
||||
<p className='text-xs font-semibold uppercase tracking-[0.28em] text-stone-600'>Enrollment notes</p>
|
||||
</div>
|
||||
|
||||
<BaseButtons>
|
||||
<BaseButton type="submit" label={loading ? 'Loading...' : 'Register'} color="info" />
|
||||
<BaseButton href={'/login'} label={'Login'} color="info" />
|
||||
</BaseButtons>
|
||||
</Form>
|
||||
</Formik>
|
||||
</CardBox>
|
||||
<div className='mt-4 space-y-4 text-sm leading-7 text-stone-700'>
|
||||
<p>
|
||||
Select the organization that should own the account. This keeps customer access tied to the
|
||||
right operating context from the start.
|
||||
</p>
|
||||
<p>
|
||||
Linked tenants, if available, become visible after account creation so the login experience
|
||||
stays consistent with the workspace structure.
|
||||
</p>
|
||||
<p className='text-stone-600'>
|
||||
Need admin or concierge access instead? Those roles should be created internally rather than
|
||||
through public signup.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='rounded-[1.5rem] border border-stone-200/90 bg-white/55 p-4 text-sm leading-6 text-stone-600'>
|
||||
Gracey keeps public account creation narrow on purpose: clear audience, clear tenant context, and a
|
||||
quieter path into the authenticated workspace.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center justify-center bg-[linear-gradient(180deg,rgba(255,255,255,0.74),rgba(255,250,242,0.96))] px-4 py-6 sm:px-8 sm:py-8 lg:px-10 lg:py-10'>
|
||||
<div className='w-full max-w-xl space-y-5 sm:space-y-6'>
|
||||
<CardBox className='border border-stone-200/90 bg-white/95 text-stone-950 shadow-[0_22px_55px_rgba(60,38,20,0.08)] dark:!border-stone-200 dark:!bg-white dark:!text-stone-950'>
|
||||
<div className='space-y-5 sm:space-y-6'>
|
||||
<div className='space-y-3'>
|
||||
<p className='text-xs font-semibold uppercase tracking-[0.28em] text-stone-500'>Customer access</p>
|
||||
<h2 className='text-3xl font-semibold text-stone-950'>Create your account</h2>
|
||||
<p className='text-sm leading-6 text-stone-600'>
|
||||
Set your portal credentials and attach them to the right organization before your first stay
|
||||
request begins.
|
||||
</p>
|
||||
<div className='flex flex-wrap gap-2 pt-1'>
|
||||
{REGISTER_CHIPS.map((chip) => (
|
||||
<span
|
||||
key={chip}
|
||||
className='inline-flex items-center rounded-full border border-stone-200 bg-stone-50 px-3 py-1 text-[11px] font-medium text-stone-600'
|
||||
>
|
||||
{chip}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedOrganizationRecord ? (
|
||||
<div className='rounded-[1.25rem] border border-amber-200/80 bg-amber-50/80 p-4 text-sm text-stone-700'>
|
||||
<p className='text-[11px] font-semibold uppercase tracking-[0.22em] text-amber-800'>
|
||||
Account context
|
||||
</p>
|
||||
<p className='mt-2 leading-6'>
|
||||
This signup creates a <span className='font-semibold'>Customer</span> account inside{' '}
|
||||
<span className='font-semibold'>{selectedOrganizationRecord.name}</span>.
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<Formik
|
||||
initialValues={{
|
||||
email: '',
|
||||
password: '',
|
||||
confirm: '',
|
||||
}}
|
||||
onSubmit={(values) => handleSubmit(values)}
|
||||
>
|
||||
<Form>
|
||||
<div className='mb-4'>
|
||||
<label className='mb-2 block text-sm font-semibold text-stone-700'>Organization</label>
|
||||
<Select<OrganizationSelectOption, false>
|
||||
classNames={{
|
||||
control: ({ isFocused }) =>
|
||||
`min-h-[42px] rounded-lg border bg-white px-2 py-1 text-sm shadow-none ${
|
||||
isFocused
|
||||
? 'border-stone-900 ring-1 ring-stone-900'
|
||||
: 'border-stone-300 hover:border-stone-400'
|
||||
}`,
|
||||
menu: () => 'overflow-hidden rounded-xl border border-stone-200 bg-white shadow-xl',
|
||||
menuList: () => 'p-2',
|
||||
option: ({ isFocused, isSelected }) =>
|
||||
`cursor-pointer rounded-lg px-3 py-2 text-sm ${
|
||||
isSelected
|
||||
? 'bg-stone-900 text-white'
|
||||
: isFocused
|
||||
? 'bg-amber-50 text-stone-900'
|
||||
: 'text-stone-700'
|
||||
}`,
|
||||
placeholder: () => 'text-stone-400',
|
||||
singleValue: () => 'text-stone-900',
|
||||
indicatorSeparator: () => 'hidden',
|
||||
dropdownIndicator: () => 'text-stone-500',
|
||||
}}
|
||||
value={selectedOrganization}
|
||||
onChange={(option) => setSelectedOrganization(option)}
|
||||
options={options}
|
||||
placeholder='Select organization...'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField label='Email' help='Please enter your email'>
|
||||
<Field type='email' name='email' className={AUTH_INPUT_CLASS} />
|
||||
</FormField>
|
||||
<FormField label='Password' help='Please enter your password'>
|
||||
<Field type='password' name='password' className={AUTH_INPUT_CLASS} />
|
||||
</FormField>
|
||||
<FormField label='Confirm Password' help='Please confirm your password'>
|
||||
<Field type='password' name='confirm' className={AUTH_INPUT_CLASS} />
|
||||
</FormField>
|
||||
|
||||
<BaseDivider />
|
||||
|
||||
<BaseButtons className='w-full' type='justify-center' classAddon='mb-3' mb='mb-0'>
|
||||
<BaseButton
|
||||
className='w-full justify-center border-[#4d3929] bg-[#4d3929] text-white hover:bg-[#3d2d21] focus:ring-amber-300'
|
||||
type='submit'
|
||||
label={loading ? 'Loading...' : 'Register'}
|
||||
color='white'
|
||||
disabled={loading}
|
||||
/>
|
||||
</BaseButtons>
|
||||
|
||||
<div className='flex flex-col items-center gap-3 text-center sm:flex-row sm:justify-between sm:text-left'>
|
||||
<p className='text-sm text-stone-600'>Already have credentials?</p>
|
||||
<Link className={`${textColor} text-sm font-medium`} href='/login'>
|
||||
Return to Login
|
||||
</Link>
|
||||
</div>
|
||||
</Form>
|
||||
</Formik>
|
||||
</div>
|
||||
</CardBox>
|
||||
|
||||
<div className='flex flex-col items-center justify-center gap-3 border-t border-stone-200 pt-1 text-center text-sm text-stone-600 sm:flex-row'>
|
||||
<p>© 2026 {TITLE}. © All rights reserved</p>
|
||||
<div className='flex flex-wrap items-center justify-center gap-4'>
|
||||
<Link className={`${textColor} text-sm`} href='/privacy-policy/'>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
<Link className={`${textColor} text-sm`} href='/terms-of-use/'>
|
||||
Terms of Use
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SectionFullScreen>
|
||||
|
||||
<ToastContainer />
|
||||
</>
|
||||
);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user