2026-06-29 06:02:08 +00:00

460 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useEffect, useState } from 'react';
import type { ReactElement } from 'react';
import Head from 'next/head';
import BaseButton from '../components/BaseButton';
import CardBox from '../components/CardBox';
import BaseIcon from "../components/BaseIcon";
import { mdiInformation, mdiEye, mdiEyeOff } from '@mdi/js';
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 } from '../helpers/pexels'
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 [contentPosition] = useState<'left' | 'right' | 'background'>('left');
const [showPassword, setShowPassword] = useState(false);
const { currentUser, isFetching, errorMessage, token, notify:notifyState } = useAppSelector(
(state) => state.auth,
);
const [initialValues, setInitialValues] = React.useState({ email:'admin@flatlogic.com',
password: 'fc6e39e3',
remember: true })
const title = 'Review Flow'
const appHighlights = [
'Automated review requests after payments, jobs, or service milestones.',
'Customer, business, transaction, and delivery follow-up data in one admin workspace.',
'Dashboards, CRM records, payment events, email logs, and admin controls already built in.',
];
const competitorAdvantages = [
{
title: 'Built around review operations',
description:
'Review Flow combines CRM records, payments, follow-up, review requests, and reputation workflows in one focused system.',
},
{
title: 'Designed for logistics teams',
description:
'Transportation teams can manage businesses, customers, transactions, payment events, and review requests without jumping tools.',
},
{
title: 'More value in the base plan',
description:
'The $65 Base plan includes review automation, widgets, social proof, analytics, AI replies, referrals, and the app tools already available.',
},
];
const pricingPlans = [
{
name: 'Base',
price: '$65',
description:
'Best for businesses that want full review growth tools plus the core Review Flow admin system.',
sections: [
{
title: 'Review automation',
features: [
'Automate review requests and follow-up reminders.',
'Manually send review requests.',
'Personalize review request SMS and email messaging.',
'Personalize review invite links.',
'Monitor reviews across the web.',
'New review notifications and opportunities reports.',
],
},
{
title: 'Widgets, referrals, and social proof',
features: [
'Showcase reviews on your website with social proof widgets.',
'Collect reviews and leads with widgets for your website.',
'Microsite that showcases your reviews and generates leads.',
'Automate sharing of reviews to your social media accounts.',
'Share referral link on social media.',
],
},
{
title: 'Insights, AI, and team motivation',
features: [
'Easily respond to customer reviews with AI-generated replies.',
'Gain review insights and trending topics.',
'Campaign insights and analytics.',
'Encourage friendly competition with staff leaderboards.',
'Connect to 1000s of business apps.',
],
},
{
title: 'Existing Review Flow tools included',
features: [
'Review Flow workspace for creating, scheduling, and tracking review requests.',
'Business, customer, transaction, and delivery follow-up records.',
'Webhook connectors for Stripe, PayPal, Square, Shopify, and WooCommerce workflows.',
'Payment events, email delivery logs, and cron run monitoring.',
'Admin dashboard with users, roles, permissions, profile, and API documentation access.',
],
},
],
},
{
name: 'Pro',
price: '$99',
description:
'Best for growing teams that want every Base feature plus booking, referral, gifting, competitor, and advanced AI tools.',
sections: [
{
title: 'Everything in Base',
features: [
'Includes all Base review automation, widgets, referrals, analytics, AI replies, social sharing, integrations, and existing app tools.',
'Advanced workflow management.',
'Priority setup support.',
],
},
{
title: 'Booking reminders',
features: [
'Automate repeat booking reminders and follow-ups.',
'Personalize booking reminder SMS and email messaging.',
],
},
{
title: 'Referral automation',
features: [
'Automate customer referral requests and follow-ups.',
'Personalize referral request SMS and email messaging.',
'Personalize referral invite links.',
],
},
{
title: 'Gifting and loyalty',
features: [
'Delight your loyal customers with gift automations.',
'Automate gifting for new customers.',
],
},
{
title: 'Competitor intelligence and advanced feedback',
features: [
'Gain competitor review and SEO insights.',
'Track competitor topics and gain valuable competitive intel.',
'Competitor topic insights include topics for your business.',
'Automate review replies with AI.',
'Collect deeper, more actionable customer feedback with NPS Surveys.',
],
},
],
},
];
// Fetch Pexels image
useEffect( () => {
async function fetchData() {
const image = await getPexelsImage()
setIllustrationImage(image);
}
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('/dashboard');
}
}, [currentUser?.id, router]);
// Show error message if there is one
useEffect(() => {
if (errorMessage){
notify('error', errorMessage)
}
}, [errorMessage])
// Show notification if there is one
useEffect(() => {
if (notifyState?.showNotification) {
notify('success', notifyState?.textNotification)
dispatch(resetAction());
}
}, [notifyState?.showNotification])
const togglePasswordVisibility = () => {
setShowPassword(!showPassword);
};
const handleSubmit = async (value) => {
const {remember, ...rest} = value
await dispatch(loginUser(rest));
};
const setLogin = (target: HTMLElement) => {
setInitialValues(prev => ({
...prev,
email : target.innerText.trim(),
password: target.dataset.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>
</div>
</div>
)
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>
<SectionFullScreen bg='violet'>
<div className={`flex ${contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'} min-h-screen w-full`}>
{contentPosition !== 'background' ? imageBlock(illustrationImage) : 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 text-gray-500 justify-between'>
<div>
<p className='mb-2'>Use{' '}
<code className={`cursor-pointer ${textColor} `}
data-password="fc6e39e3"
onClick={(e) => setLogin(e.target)}>admin@flatlogic.com</code>{' / '}
<code className={`${textColor}`}>fc6e39e3</code>{' / '}
to login as Admin</p>
<p>Use <code
className={`cursor-pointer ${textColor} `}
data-password="874c3b951385"
onClick={(e) => setLogin(e.target)}>client@hello.com</code>{' / '}
<code className={`${textColor}`}>874c3b951385</code>{' / '}
to login as User</p>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w='w-16'
h='h-16'
size={48}
path={mdiInformation}
/>
</div>
</div>
</CardBox>
<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='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>
<div className={'flex justify-between'}>
<FormCheckRadio type='checkbox' label='Remember'>
<Field type='checkbox' name='remember' />
</FormCheckRadio>
<Link className={`${textColor} text-blue-600`} href={'/forgot'}>
Forgot password?
</Link>
</div>
<BaseDivider />
<BaseButtons>
<BaseButton
className={'w-full'}
type='submit'
label={isFetching ? 'Loading...' : 'Login'}
color='info'
disabled={isFetching}
/>
</BaseButtons>
<br />
<p className={'text-center'}>
Dont have an account yet?{' '}
<Link className={`${textColor}`} href={'/register'}>
New Account
</Link>
</p>
</Form>
</Formik>
</CardBox>
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
<div className='space-y-8'>
<div>
<p className='text-sm font-semibold uppercase tracking-[0.2em] text-blue-600'>About Us</p>
<h3 className='mt-2 text-3xl font-semibold text-gray-900 dark:text-white'>
Review management built for transportation teams.
</h3>
<p className='mt-4 text-base leading-7 text-gray-600 dark:text-slate-300'>
Review Flow helps logistics and transportation businesses turn completed jobs, payments,
and customer interactions into organized review requests. Your team can manage customer
records, monitor follow-up, and keep reputation-building work moving from one secure
admin panel.
</p>
</div>
<div className='grid gap-3 md:grid-cols-3'>
{appHighlights.map((highlight) => (
<div
key={highlight}
className='rounded-2xl border border-blue-100 bg-blue-50/70 p-4 text-sm leading-6 text-blue-900 dark:border-dark-700 dark:bg-dark-800 dark:text-slate-200'
>
{highlight}
</div>
))}
</div>
<div>
<h4 className='text-xl font-semibold text-gray-900 dark:text-white'>Why we&apos;re better</h4>
<div className='mt-4 grid gap-4 md:grid-cols-3'>
{competitorAdvantages.map((item) => (
<div key={item.title} className='rounded-2xl border border-gray-200 p-4 dark:border-dark-700'>
<h5 className='font-semibold text-gray-900 dark:text-white'>{item.title}</h5>
<p className='mt-2 text-sm leading-6 text-gray-600 dark:text-slate-300'>
{item.description}
</p>
</div>
))}
</div>
</div>
<div>
<div className='flex flex-col justify-between gap-2 md:flex-row md:items-end'>
<div>
<p className='text-sm font-semibold uppercase tracking-[0.2em] text-blue-600'>Pricing</p>
<h4 className='mt-2 text-2xl font-semibold text-gray-900 dark:text-white'>Simple monthly plans</h4>
</div>
<p className='text-sm text-gray-500 dark:text-slate-400'>Upgrade when your review workflow grows.</p>
</div>
<div className='mt-4 grid gap-4 md:grid-cols-2'>
{pricingPlans.map((plan) => (
<div
key={plan.name}
className='rounded-2xl border border-gray-200 bg-white p-5 shadow-sm dark:border-dark-700 dark:bg-dark-900'
>
<div className='flex items-start justify-between gap-3'>
<div>
<h5 className='text-lg font-semibold text-gray-900 dark:text-white'>{plan.name}</h5>
<p className='mt-1 text-sm leading-6 text-gray-600 dark:text-slate-300'>{plan.description}</p>
</div>
<div className='text-right'>
<span className='text-3xl font-bold text-blue-600'>{plan.price}</span>
<span className='block text-xs text-gray-500 dark:text-slate-400'>/month</span>
</div>
</div>
<div className='mt-5 space-y-5'>
{plan.sections.map((section) => (
<div key={section.title}>
<h6 className='text-sm font-semibold uppercase tracking-wide text-gray-900 dark:text-white'>
{section.title}
</h6>
<ul className='mt-2 space-y-2 text-sm text-gray-700 dark:text-slate-300'>
{section.features.map((feature) => (
<li key={feature} className='flex gap-2'>
<span className='font-semibold text-blue-600'></span>
<span>{feature}</span>
</li>
))}
</ul>
</div>
))}
</div>
</div>
))}
</div>
</div>
</div>
</CardBox>
</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>
);
}
Login.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};