diff --git a/frontend/src/components/FormField.tsx b/frontend/src/components/FormField.tsx
index 6cb83e0..a48f9a4 100644
--- a/frontend/src/components/FormField.tsx
+++ b/frontend/src/components/FormField.tsx
@@ -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) => {
)}
- {Children.map(props.children, (child: ReactElement, index) => (
+ {Children.map(props.children, (child: ReactElement, index) => {
+ const childClassName = (child.props as { className?: string })?.className || ''
+
+ return (
{cloneElement(child as ReactElement<{ className?: string }>, {
- className: `${controlClassName} ${icons[index] ? 'pl-10' : ''}`,
+ className: `${controlClassName} ${childClassName} ${icons[index] ? 'pl-10' : ''}`.trim(),
})}
{icons[index] && (
{
/>
)}
- ))}
+ )
+ })}
{props.help && (
{props.help}
diff --git a/frontend/src/pages/forgot.tsx b/frontend/src/pages/forgot.tsx
index 39b0193..f3cd002 100644
--- a/frontend/src/pages/forgot.tsx
+++ b/frontend/src/pages/forgot.tsx
@@ -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 (
<>
- {getPageTitle('Login')}
+ {getPageTitle('Forgot password')}
-
- handleSubmit(values)}
- >
-
+
>
);
diff --git a/frontend/src/pages/login.tsx b/frontend/src/pages/login.tsx
index fa2bf62..f978095 100644
--- a/frontend/src/pages/login.tsx
+++ b/frontend/src/pages/login.tsx
@@ -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(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) => (
-
-
-
Photo
- by {image?.photographer} on Pexels
+ const registeredMessage = useMemo(() => {
+ if (!registeredOrg) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+ Your {registeredRole || 'customer'} account was created for {registeredOrg}.
+
+ {registeredTenants.length ? (
+
+
Linked tenants
+
+ {registeredTenants.map((tenantName) => (
+
+ {tenantName}
+
+ ))}
+
+
+ ) : (
+
No linked tenant is shown for that organization yet.
+ )}
+
+ Admin and Super Admin accounts are provisioned internally, not through self-signup.
+
-
- )
-
- const videoBlock = (video) => {
- if (video?.video_files?.length > 0) {
- return (
-
-
-
-
)
- }
- };
+
+
+ );
+ }, [registeredOrg, registeredRole, registeredTenants]);
return (
-
-
-
{getPageTitle('Login')}
-
+ <>
+
+
{getPageTitle('Login')}
+
-
-
- {contentType === 'image' && contentPosition !== 'background' ? imageBlock(illustrationImage) : null}
- {contentType === 'video' && contentPosition !== 'background' ? videoBlock(illustrationVideo) : null}
-
-
-
-
- {title}
-
-
-
-
-
Use{' '}
- setLogin(e.target)}>super_admin@flatlogic.com{' / '}
- 946cafba{' / '}
- to login as Super Admin
-
-
Use{' '}
- setLogin(e.target)}>admin@flatlogic.com{' / '}
- 946cafba{' / '}
- to login as Admin
-
Use setLogin(e.target)}>john@doe.com{' / '}
- bc40952c93f4{' / '}
- to login as Concierge
-
Use setLogin(e.target)}>client@hello.com{' / '}
- bc40952c93f4{' / '}
- to login as Customer
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Portal access
+
+
+
+ {title}
+
+
+ Sign in to the private workspace used for booking intake, guest coordination, and billing
+ follow-through.
+
-
-
- {registeredOrg ? (
-
-
-
-
-
- Your {registeredRole || 'customer'} account was created for {registeredOrg}.
-
- {registeredTenants.length ? (
-
-
Linked tenants
-
- {registeredTenants.map((tenantName) => (
-
- {tenantName}
-
- ))}
-
-
- ) : (
-
- No linked tenant is shown for that organization yet.
-
- )}
-
- Admin and Super Admin accounts are provisioned internally, not through self-signup.
-
+
+ {ACCESS_CHIPS.map((chip) => (
+
+ {chip}
+
+ ))}
+
+
+
+
+
+
+ Operator view
+
+
Multi-tenant workspace
+
+
+
+ Service posture
+
+
Calm, selective, precise
+
+
+
+ Execution model
+
+
Request to reservation to billing
+
+
+
+
+
+
+
+ Demo account access
+
+
+
+ {DEMO_ACCOUNTS.map((account) => {
+ const isSelected =
+ initialValues.email === account.email && initialValues.password === account.password;
+
+ return (
+
+ );
+ })}
+
+
+
+
+
+ The public experience stays polished and restrained. Operational depth appears only where it
+ belongs: inside the authenticated workspace.
+
+
+
+
+
+
+ {registeredMessage}
+
+
+
+
+
Member sign in
+
Welcome back
+
+ Enter your credentials to open the Gracey workspace.
+
+
+ {ACCESS_CHIPS.map((chip) => (
+
+ {chip}
+
+ ))}
+
+
+
+
+
+
+
+
+
© 2026 {title}. © All rights reserved
+
+
+ Privacy Policy
+
+
+ Terms of Use
+
+
+
+
-
-
-
© 2026 {title}. © All rights reserved
-
- Privacy Policy
-
-
-
+
+
+
+ >
);
}
diff --git a/frontend/src/pages/register.tsx b/frontend/src/pages/register.tsx
index 68ba80f..a955a16 100644
--- a/frontend/src/pages/register.tsx
+++ b/frontend/src/pages/register.tsx
@@ -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
([]);
const [selectedOrganization, setSelectedOrganization] = React.useState(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() {
{getPageTitle('Register')}
-
-
- handleSubmit(values)}
- >
-
-
+
+
+
+
+
-
+
+
+
+
+
+
+
Customer access
+
Create your account
+
+ Set your portal credentials and attach them to the right organization before your first stay
+ request begins.
+
+
+ {REGISTER_CHIPS.map((chip) => (
+
+ {chip}
+
+ ))}
+
+
+
+ {selectedOrganizationRecord ? (
+
+
+ Account context
+
+
+ This signup creates a Customer account inside{' '}
+ {selectedOrganizationRecord.name}.
+
+
+ ) : null}
+
+
handleSubmit(values)}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Already have credentials?
+
+ Return to Login
+
+
+
+
+
+
+
+
+
© 2026 {TITLE}. © All rights reserved
+
+
+ Privacy Policy
+
+
+ Terms of Use
+
+
+
+
+
+
+
+
>
);