Autosave: 20260501-154034
This commit is contained in:
parent
0e22f33482
commit
068b3eea03
186
frontend/src/components/MobileBottomNav.tsx
Normal file
186
frontend/src/components/MobileBottomNav.tsx
Normal file
@ -0,0 +1,186 @@
|
||||
import {
|
||||
mdiAccountCircleOutline,
|
||||
mdiChartTimelineVariant,
|
||||
mdiHeartPulse,
|
||||
mdiLeafCircle,
|
||||
mdiMenu,
|
||||
mdiTargetVariant,
|
||||
mdiViewDashboardOutline,
|
||||
mdiWaterOutline,
|
||||
} from '@mdi/js'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { hasCompletedApexOnboarding } from '../helpers/apexOnboarding'
|
||||
import { hasPermission } from '../helpers/userPermissions'
|
||||
import { useAppSelector } from '../stores/hooks'
|
||||
import BaseIcon from './BaseIcon'
|
||||
|
||||
type Props = {
|
||||
onMenuClick: () => void
|
||||
isMenuOpen: boolean
|
||||
}
|
||||
|
||||
type MobileNavItem = {
|
||||
key: string
|
||||
href?: string
|
||||
label: string
|
||||
icon: string
|
||||
matchers: string[]
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
function isRouteActive(pathname: string, matchers: string[]) {
|
||||
return matchers.some(
|
||||
(matcher) => pathname === matcher || pathname.startsWith(`${matcher}/`),
|
||||
)
|
||||
}
|
||||
|
||||
function getItemClass(active: boolean, isPulse: boolean) {
|
||||
if (isPulse) {
|
||||
return active
|
||||
? 'bg-[#4A6741] text-white shadow-md dark:bg-emerald-700 dark:text-white'
|
||||
: 'bg-[#EDF4EA] text-[#4A6741] dark:bg-dark-800 dark:text-emerald-300'
|
||||
}
|
||||
|
||||
return active
|
||||
? 'bg-[#EDF4EA] text-[#4A6741] dark:bg-dark-800 dark:text-emerald-300'
|
||||
: 'text-[#7E725E] dark:text-slate-400'
|
||||
}
|
||||
|
||||
export default function MobileBottomNav({ onMenuClick, isMenuOpen }: Props) {
|
||||
const router = useRouter()
|
||||
const { currentUser } = useAppSelector((state) => state.auth)
|
||||
const [onboardingComplete, setOnboardingComplete] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setOnboardingComplete(hasCompletedApexOnboarding(currentUser?.id))
|
||||
}, [currentUser?.id])
|
||||
|
||||
const focusItem: MobileNavItem =
|
||||
onboardingComplete && hasPermission(currentUser, 'READ_GOALS')
|
||||
? {
|
||||
key: 'goals',
|
||||
href: '/goals/goals-list',
|
||||
label: 'Goals',
|
||||
icon: mdiTargetVariant,
|
||||
matchers: ['/goals'],
|
||||
}
|
||||
: {
|
||||
key: 'setup',
|
||||
href: '/onboarding',
|
||||
label: 'Setup',
|
||||
icon: mdiAccountCircleOutline,
|
||||
matchers: ['/onboarding'],
|
||||
}
|
||||
|
||||
let trackingItem: MobileNavItem = {
|
||||
key: 'today',
|
||||
href: '/daily-pulse',
|
||||
label: 'Today',
|
||||
icon: mdiChartTimelineVariant,
|
||||
matchers: ['/daily-pulse'],
|
||||
}
|
||||
|
||||
if (hasPermission(currentUser, 'READ_HEALTH_METRICS')) {
|
||||
trackingItem = {
|
||||
key: 'vitals',
|
||||
href: '/health_metrics/health_metrics-list',
|
||||
label: 'Vitals',
|
||||
icon: mdiChartTimelineVariant,
|
||||
matchers: ['/health_metrics'],
|
||||
}
|
||||
} else if (hasPermission(currentUser, 'READ_CONDITIONS')) {
|
||||
trackingItem = {
|
||||
key: 'care',
|
||||
href: '/conditions/conditions-list',
|
||||
label: 'Care',
|
||||
icon: mdiLeafCircle,
|
||||
matchers: ['/conditions'],
|
||||
}
|
||||
} else if (hasPermission(currentUser, 'READ_HYDRATION_LOGS')) {
|
||||
trackingItem = {
|
||||
key: 'hydrate',
|
||||
href: '/hydration_logs/hydration_logs-list',
|
||||
label: 'Hydrate',
|
||||
icon: mdiWaterOutline,
|
||||
matchers: ['/hydration_logs'],
|
||||
}
|
||||
}
|
||||
|
||||
const navItems: MobileNavItem[] = [
|
||||
{
|
||||
key: 'home',
|
||||
href: '/dashboard',
|
||||
label: 'Home',
|
||||
icon: mdiViewDashboardOutline,
|
||||
matchers: ['/dashboard'],
|
||||
},
|
||||
{
|
||||
key: 'pulse',
|
||||
href: '/daily-pulse',
|
||||
label: 'Pulse',
|
||||
icon: mdiHeartPulse,
|
||||
matchers: ['/daily-pulse'],
|
||||
},
|
||||
focusItem,
|
||||
trackingItem,
|
||||
{
|
||||
key: 'menu',
|
||||
label: 'Menu',
|
||||
icon: mdiMenu,
|
||||
matchers: [],
|
||||
onClick: onMenuClick,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="fixed inset-x-0 bottom-0 z-30 px-3 pb-3 pt-2 lg:hidden">
|
||||
<div className="mx-auto flex max-w-md items-center gap-1 rounded-3xl border border-stone-200 bg-[#FFF9F0]/95 p-2 shadow-lg backdrop-blur-md dark:border-dark-700 dark:bg-dark-900/95">
|
||||
{navItems.map((item) => {
|
||||
const isActive =
|
||||
item.key === 'menu'
|
||||
? isMenuOpen
|
||||
: isRouteActive(router.pathname, item.matchers)
|
||||
const isPulse = item.key === 'pulse'
|
||||
const sharedClassName = [
|
||||
'flex min-w-0 flex-1 flex-col items-center justify-center gap-1 rounded-2xl px-1 py-2 text-[11px] font-semibold transition-all duration-150',
|
||||
getItemClass(isActive, isPulse),
|
||||
].join(' ')
|
||||
const content = (
|
||||
<>
|
||||
<BaseIcon path={item.icon} size={isPulse ? 22 : 20} />
|
||||
<span className="truncate text-center leading-none">{item.label}</span>
|
||||
</>
|
||||
)
|
||||
|
||||
if (item.href) {
|
||||
return (
|
||||
<Link
|
||||
key={item.key}
|
||||
href={item.href}
|
||||
aria-current={isActive ? 'page' : undefined}
|
||||
className={sharedClassName}
|
||||
>
|
||||
{content}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
key={item.key}
|
||||
type="button"
|
||||
aria-pressed={isActive}
|
||||
aria-label={item.label}
|
||||
className={sharedClassName}
|
||||
onClick={item.onClick}
|
||||
>
|
||||
{content}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -1,6 +1,5 @@
|
||||
import React, {useEffect, useRef} from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
|
||||
import BaseDivider from './BaseDivider'
|
||||
import BaseIcon from './BaseIcon'
|
||||
|
||||
88
frontend/src/helpers/apexOnboarding.ts
Normal file
88
frontend/src/helpers/apexOnboarding.ts
Normal file
@ -0,0 +1,88 @@
|
||||
export type ApexOnboardingRecordIds = {
|
||||
physicalId?: string;
|
||||
lifestyleId?: string;
|
||||
goalId?: string;
|
||||
conditionId?: string;
|
||||
};
|
||||
|
||||
export type ApexOnboardingDraft<T = Record<string, unknown>> = {
|
||||
version: number;
|
||||
lastSavedAt?: string;
|
||||
completedAt?: string;
|
||||
form?: Partial<T>;
|
||||
recordIds?: ApexOnboardingRecordIds;
|
||||
};
|
||||
|
||||
const APEX_ONBOARDING_VERSION = 1;
|
||||
|
||||
export function getApexOnboardingStorageKey(userId?: string | null) {
|
||||
return userId
|
||||
? `apex-brain:onboarding:v${APEX_ONBOARDING_VERSION}:${userId}`
|
||||
: '';
|
||||
}
|
||||
|
||||
export function readApexOnboardingDraft<
|
||||
T = Record<string, unknown>,
|
||||
>(userId?: string | null): ApexOnboardingDraft<T> | null {
|
||||
if (typeof window === 'undefined' || !userId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const storageKey = getApexOnboardingStorageKey(userId);
|
||||
|
||||
try {
|
||||
const rawValue = window.localStorage.getItem(storageKey);
|
||||
|
||||
if (!rawValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsedValue = JSON.parse(rawValue);
|
||||
|
||||
if (!parsedValue || typeof parsedValue !== 'object') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return parsedValue as ApexOnboardingDraft<T>;
|
||||
} catch (error) {
|
||||
console.error('Failed to read APEX BRAIN onboarding draft:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function saveApexOnboardingDraft<T>(
|
||||
userId: string | null | undefined,
|
||||
draft: Omit<ApexOnboardingDraft<T>, 'version'>,
|
||||
) {
|
||||
if (typeof window === 'undefined' || !userId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const storageKey = getApexOnboardingStorageKey(userId);
|
||||
const nextDraft: ApexOnboardingDraft<T> = {
|
||||
version: APEX_ONBOARDING_VERSION,
|
||||
...draft,
|
||||
lastSavedAt: draft.lastSavedAt ?? new Date().toISOString(),
|
||||
};
|
||||
|
||||
try {
|
||||
window.localStorage.setItem(storageKey, JSON.stringify(nextDraft));
|
||||
} catch (error) {
|
||||
console.error('Failed to save APEX BRAIN onboarding draft:', error);
|
||||
}
|
||||
}
|
||||
|
||||
export function markApexOnboardingCompleted<T>(
|
||||
userId: string | null | undefined,
|
||||
draft: Omit<ApexOnboardingDraft<T>, 'version'>,
|
||||
) {
|
||||
saveApexOnboardingDraft<T>(userId, {
|
||||
...draft,
|
||||
completedAt: draft.completedAt ?? new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
export function hasCompletedApexOnboarding(userId?: string | null) {
|
||||
const draft = readApexOnboardingDraft(userId);
|
||||
return Boolean(draft?.completedAt);
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
import React, { ReactNode, useEffect } from 'react'
|
||||
import { useState } from 'react'
|
||||
import jwt from 'jsonwebtoken';
|
||||
import type { ReactNode } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import jwt from 'jsonwebtoken'
|
||||
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
|
||||
import menuAside from '../menuAside'
|
||||
import menuNavBar from '../menuNavBar'
|
||||
@ -9,66 +9,88 @@ import NavBar from '../components/NavBar'
|
||||
import NavBarItemPlain from '../components/NavBarItemPlain'
|
||||
import AsideMenu from '../components/AsideMenu'
|
||||
import FooterBar from '../components/FooterBar'
|
||||
import MobileBottomNav from '../components/MobileBottomNav'
|
||||
import { useAppDispatch, useAppSelector } from '../stores/hooks'
|
||||
import Search from '../components/Search';
|
||||
import Search from '../components/Search'
|
||||
import { useRouter } from 'next/router'
|
||||
import {findMe, logoutUser} from "../stores/authSlice";
|
||||
|
||||
import {hasPermission} from "../helpers/userPermissions";
|
||||
|
||||
import { findMe, logoutUser } from '../stores/authSlice'
|
||||
import { humanize } from '../helpers/humanize'
|
||||
import { hasPermission } from '../helpers/userPermissions'
|
||||
|
||||
type Props = {
|
||||
children: ReactNode
|
||||
|
||||
permission?: string
|
||||
|
||||
}
|
||||
|
||||
export default function LayoutAuthenticated({
|
||||
children,
|
||||
|
||||
permission
|
||||
|
||||
}: Props) {
|
||||
function getMobilePageTitle(pathname: string) {
|
||||
const knownRoutes = [
|
||||
{ prefix: '/dashboard', title: 'Home' },
|
||||
{ prefix: '/daily-pulse', title: 'Daily Pulse' },
|
||||
{ prefix: '/onboarding', title: 'Onboarding' },
|
||||
{ prefix: '/goals', title: 'Goals' },
|
||||
{ prefix: '/health_metrics', title: 'Vitals' },
|
||||
{ prefix: '/conditions', title: 'Care' },
|
||||
{ prefix: '/hydration_logs', title: 'Hydration' },
|
||||
{ prefix: '/mood_logs', title: 'Mood' },
|
||||
{ prefix: '/sleep_logs', title: 'Sleep' },
|
||||
{ prefix: '/profile', title: 'Profile' },
|
||||
]
|
||||
|
||||
const matchedRoute = knownRoutes.find((route) => pathname.startsWith(route.prefix))
|
||||
|
||||
if (matchedRoute) {
|
||||
return matchedRoute.title
|
||||
}
|
||||
|
||||
const [firstSegment] = pathname.split('/').filter(Boolean)
|
||||
|
||||
if (!firstSegment) {
|
||||
return 'Home'
|
||||
}
|
||||
|
||||
return humanize(firstSegment.replace(/-/g, ' ').replace(/_/g, ' '))
|
||||
}
|
||||
|
||||
export default function LayoutAuthenticated({ children, permission }: Props) {
|
||||
const dispatch = useAppDispatch()
|
||||
const router = useRouter()
|
||||
const { token, currentUser } = useAppSelector((state) => state.auth)
|
||||
const bgColor = useAppSelector((state) => state.style.bgLayoutColor);
|
||||
const bgColor = useAppSelector((state) => state.style.bgLayoutColor)
|
||||
const darkMode = useAppSelector((state) => state.style.darkMode)
|
||||
const [isAsideMobileExpanded, setIsAsideMobileExpanded] = useState(false)
|
||||
const [isAsideLgActive, setIsAsideLgActive] = useState(false)
|
||||
|
||||
let localToken
|
||||
if (typeof window !== 'undefined') {
|
||||
// Perform localStorage action
|
||||
localToken = localStorage.getItem('token')
|
||||
}
|
||||
|
||||
const isTokenValid = () => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) return;
|
||||
const date = new Date().getTime() / 1000;
|
||||
const data = jwt.decode(token);
|
||||
if (!data) return;
|
||||
return date < data.exp;
|
||||
};
|
||||
const tokenValue = localStorage.getItem('token')
|
||||
|
||||
if (!tokenValue) return
|
||||
|
||||
const date = new Date().getTime() / 1000
|
||||
const data = jwt.decode(tokenValue)
|
||||
|
||||
if (!data) return
|
||||
|
||||
return date < data.exp
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(findMe());
|
||||
dispatch(findMe())
|
||||
if (!isTokenValid()) {
|
||||
dispatch(logoutUser());
|
||||
router.push('/login');
|
||||
dispatch(logoutUser())
|
||||
router.push('/login')
|
||||
}
|
||||
}, [token, localToken]);
|
||||
}, [token, localToken])
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (!permission || !currentUser) return;
|
||||
if (!permission || !currentUser) return
|
||||
|
||||
if (!hasPermission(currentUser, permission)) router.push('/error');
|
||||
}, [currentUser, permission]);
|
||||
|
||||
|
||||
const darkMode = useAppSelector((state) => state.style.darkMode)
|
||||
|
||||
const [isAsideMobileExpanded, setIsAsideMobileExpanded] = useState(false)
|
||||
const [isAsideLgActive, setIsAsideLgActive] = useState(false)
|
||||
if (!hasPermission(currentUser, permission)) router.push('/error')
|
||||
}, [currentUser, permission])
|
||||
|
||||
useEffect(() => {
|
||||
const handleRouteChangeStart = () => {
|
||||
@ -78,22 +100,20 @@ export default function LayoutAuthenticated({
|
||||
|
||||
router.events.on('routeChangeStart', handleRouteChangeStart)
|
||||
|
||||
// If the component is unmounted, unsubscribe
|
||||
// from the event with the `off` method:
|
||||
return () => {
|
||||
router.events.off('routeChangeStart', handleRouteChangeStart)
|
||||
}
|
||||
}, [router.events, dispatch])
|
||||
|
||||
|
||||
const layoutAsidePadding = 'xl:pl-60'
|
||||
const mobilePageTitle = getMobilePageTitle(router.pathname)
|
||||
|
||||
return (
|
||||
<div className={`${darkMode ? 'dark' : ''} overflow-hidden lg:overflow-visible`}>
|
||||
<div
|
||||
className={`${layoutAsidePadding} ${
|
||||
isAsideMobileExpanded ? 'ml-60 lg:ml-0' : ''
|
||||
} pt-14 min-h-screen w-screen transition-position lg:w-auto ${bgColor} dark:bg-dark-800 dark:text-slate-100`}
|
||||
} pt-14 pb-24 min-h-screen w-screen transition-position lg:w-auto lg:pb-0 ${bgColor} dark:bg-dark-800 dark:text-slate-100`}
|
||||
>
|
||||
<NavBar
|
||||
menu={menuNavBar}
|
||||
@ -105,13 +125,23 @@ export default function LayoutAuthenticated({
|
||||
>
|
||||
<BaseIcon path={isAsideMobileExpanded ? mdiBackburger : mdiForwardburger} size="24" />
|
||||
</NavBarItemPlain>
|
||||
<div className="flex min-w-0 flex-1 items-center px-1 lg:hidden">
|
||||
<div className="min-w-0">
|
||||
<div className="truncate text-[10px] font-semibold uppercase tracking-[0.28em] text-[#8A7C66] dark:text-slate-400">
|
||||
APEX BRAIN
|
||||
</div>
|
||||
<div className="truncate text-sm font-semibold text-[#1C1C1C] dark:text-white">
|
||||
{mobilePageTitle}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<NavBarItemPlain
|
||||
display="hidden lg:flex xl:hidden"
|
||||
onClick={() => setIsAsideLgActive(true)}
|
||||
>
|
||||
<BaseIcon path={mdiMenu} size="24" />
|
||||
</NavBarItemPlain>
|
||||
<NavBarItemPlain useMargin>
|
||||
<NavBarItemPlain display="hidden lg:flex" useMargin>
|
||||
<Search />
|
||||
</NavBarItemPlain>
|
||||
</NavBar>
|
||||
@ -122,7 +152,13 @@ export default function LayoutAuthenticated({
|
||||
onAsideLgClose={() => setIsAsideLgActive(false)}
|
||||
/>
|
||||
{children}
|
||||
<FooterBar>Hand-crafted & Made with ❤️</FooterBar>
|
||||
<div className="hidden lg:block">
|
||||
<FooterBar>Hand-crafted & Made with ❤️</FooterBar>
|
||||
</div>
|
||||
<MobileBottomNav
|
||||
isMenuOpen={isAsideMobileExpanded}
|
||||
onMenuClick={() => setIsAsideMobileExpanded((currentValue) => !currentValue)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -5,7 +5,17 @@ const menuAside: MenuAsideItem[] = [
|
||||
{
|
||||
href: '/dashboard',
|
||||
icon: icon.mdiViewDashboardOutline,
|
||||
label: 'Dashboard',
|
||||
label: 'Home',
|
||||
},
|
||||
{
|
||||
href: '/daily-pulse',
|
||||
label: 'Daily Pulse',
|
||||
icon: 'mdiHeartPulse' in icon ? icon['mdiHeartPulse' as keyof typeof icon] : icon.mdiChartTimelineVariant,
|
||||
},
|
||||
{
|
||||
href: '/onboarding',
|
||||
label: 'Onboarding',
|
||||
icon: icon.mdiAccount,
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
1399
frontend/src/pages/daily-pulse.tsx
Normal file
1399
frontend/src/pages/daily-pulse.tsx
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,166 +1,399 @@
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import type { ReactElement } from 'react';
|
||||
import {
|
||||
mdiArrowTopRight,
|
||||
mdiCheckCircleOutline,
|
||||
mdiHeartPulse,
|
||||
mdiLeafCircle,
|
||||
mdiTargetVariant,
|
||||
mdiTrophyOutline,
|
||||
mdiWaterOutline,
|
||||
mdiWeatherNight,
|
||||
} from '@mdi/js';
|
||||
import Head from 'next/head';
|
||||
import Link from 'next/link';
|
||||
import BaseButton from '../components/BaseButton';
|
||||
import CardBox from '../components/CardBox';
|
||||
import SectionFullScreen from '../components/SectionFullScreen';
|
||||
import LayoutGuest from '../layouts/Guest';
|
||||
import BaseDivider from '../components/BaseDivider';
|
||||
import BaseButtons from '../components/BaseButtons';
|
||||
import type { ReactElement } from 'react';
|
||||
import BaseIcon from '../components/BaseIcon';
|
||||
import { getPageTitle } from '../config';
|
||||
import { useAppSelector } from '../stores/hooks';
|
||||
import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
|
||||
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
|
||||
import LayoutGuest from '../layouts/Guest';
|
||||
|
||||
const promiseCards = [
|
||||
{
|
||||
title: 'Daily Pulse check-ins',
|
||||
description: 'Save water, mood, sleep, and weight in one elegant flow that already connects to the app’s deeper record screens.',
|
||||
icon: mdiHeartPulse,
|
||||
},
|
||||
{
|
||||
title: 'Premium daily ring',
|
||||
description: 'Get a calm summary of what has been captured today, with a clear score that encourages consistency without overwhelm.',
|
||||
icon: mdiTrophyOutline,
|
||||
},
|
||||
{
|
||||
title: 'Goals in context',
|
||||
description: 'Keep active goals visible beside today’s inputs so actions feel connected to long-term outcomes.',
|
||||
icon: mdiTargetVariant,
|
||||
},
|
||||
];
|
||||
|
||||
const featureChips = [
|
||||
'Fitness',
|
||||
'Nutrition',
|
||||
'Medical',
|
||||
'Mental Wellness',
|
||||
'Sleep',
|
||||
'Hydration',
|
||||
'Longevity',
|
||||
'Family & Teams',
|
||||
'Health Vault',
|
||||
'Reports & Exports',
|
||||
];
|
||||
|
||||
const workflowSteps = [
|
||||
{
|
||||
step: '01',
|
||||
title: 'Capture your essentials',
|
||||
description: 'Log hydration, mood, sleep, and weight from one mobile-first workspace.',
|
||||
icon: mdiWaterOutline,
|
||||
},
|
||||
{
|
||||
step: '02',
|
||||
title: 'See your score instantly',
|
||||
description: 'The Daily Pulse ring turns raw logging into a clear signal for the day.',
|
||||
icon: mdiCheckCircleOutline,
|
||||
},
|
||||
{
|
||||
step: '03',
|
||||
title: 'Drill into full records',
|
||||
description: 'Jump directly into detailed CRUD screens for trends, edits, exports, and history.',
|
||||
icon: mdiArrowTopRight,
|
||||
},
|
||||
];
|
||||
|
||||
export default function Starter() {
|
||||
const [illustrationImage, setIllustrationImage] = useState({
|
||||
src: undefined,
|
||||
photographer: undefined,
|
||||
photographer_url: undefined,
|
||||
})
|
||||
const [illustrationVideo, setIllustrationVideo] = useState({video_files: []})
|
||||
const [contentType, setContentType] = useState('video');
|
||||
const [contentPosition, setContentPosition] = useState('right');
|
||||
const textColor = useAppSelector((state) => state.style.linkColor);
|
||||
|
||||
const title = 'APEX BRAIN'
|
||||
|
||||
// Fetch Pexels image/video
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
const image = await getPexelsImage();
|
||||
const video = await getPexelsVideo();
|
||||
setIllustrationImage(image);
|
||||
setIllustrationVideo(video);
|
||||
}
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
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>
|
||||
);
|
||||
|
||||
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>)
|
||||
}
|
||||
};
|
||||
|
||||
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('Starter Page')}</title>
|
||||
<title>{getPageTitle('APEX BRAIN')}</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 className='w-full md:w-3/5 lg:w-2/3'>
|
||||
<CardBoxComponentTitle title="Welcome to your APEX BRAIN app!"/>
|
||||
|
||||
<div className="space-y-3">
|
||||
<p className='text-center text-gray-500'>This is a React.js/Node.js app generated by the <a className={`${textColor}`} href="https://flatlogic.com/generator">Flatlogic Web App Generator</a></p>
|
||||
<p className='text-center text-gray-500'>For guides and documentation please check
|
||||
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p>
|
||||
<div style={{ backgroundColor: '#F5F0E8', color: '#1C1C1C' }}>
|
||||
<header className="relative overflow-hidden border-b" style={{ borderColor: '#E1D6C5' }}>
|
||||
<div
|
||||
className="absolute inset-x-0 top-0 h-[28rem]"
|
||||
style={{
|
||||
background:
|
||||
'radial-gradient(circle at top left, rgba(201,168,76,0.18) 0%, rgba(201,168,76,0) 48%), radial-gradient(circle at top right, rgba(74,103,65,0.16) 0%, rgba(74,103,65,0) 44%)',
|
||||
}}
|
||||
/>
|
||||
<div className="relative mx-auto max-w-7xl px-6 py-6 lg:px-10">
|
||||
<nav className="flex flex-wrap items-center justify-between gap-4">
|
||||
<Link href="/" className="inline-flex items-center gap-3 text-lg font-semibold tracking-[0.08em]" style={{ color: '#1C1C1C' }}>
|
||||
<span
|
||||
className="flex h-12 w-12 items-center justify-center rounded-2xl"
|
||||
style={{ backgroundColor: '#4A6741', color: '#F5F0E8' }}
|
||||
>
|
||||
<BaseIcon path={mdiTrophyOutline} size={24} />
|
||||
</span>
|
||||
APEX BRAIN
|
||||
</Link>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-3 text-sm font-semibold">
|
||||
<Link href="/daily-pulse" className="rounded-full border px-4 py-2 transition hover:-translate-y-0.5" style={{ borderColor: '#D6C8B4', color: '#4A6741' }}>
|
||||
Daily Pulse
|
||||
</Link>
|
||||
<Link href="/onboarding" className="rounded-full border px-4 py-2 transition hover:-translate-y-0.5" style={{ borderColor: '#D6C8B4', color: '#4A6741' }}>
|
||||
Onboarding
|
||||
</Link>
|
||||
<Link href="/dashboard" className="rounded-full border px-4 py-2 transition hover:-translate-y-0.5" style={{ borderColor: '#D6C8B4', color: '#4A6741' }}>
|
||||
Dashboard
|
||||
</Link>
|
||||
<Link href="/login" className="rounded-full px-4 py-2 text-white transition hover:-translate-y-0.5" style={{ backgroundColor: '#4A6741' }}>
|
||||
Login
|
||||
</Link>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div className="grid gap-12 pb-14 pt-12 lg:grid-cols-[1.1fr_0.9fr] lg:items-center lg:pb-20 lg:pt-20">
|
||||
<div>
|
||||
<div
|
||||
className="mb-5 inline-flex items-center gap-2 rounded-full border px-4 py-2 text-sm font-semibold"
|
||||
style={{ borderColor: '#D6C8B4', color: '#4A6741', backgroundColor: 'rgba(255,255,255,0.56)' }}
|
||||
>
|
||||
<BaseIcon path={mdiLeafCircle} size={18} />
|
||||
Your Complete Health Operating System
|
||||
</div>
|
||||
<h1 className="max-w-3xl text-5xl font-semibold tracking-tight md:text-6xl" style={{ color: '#1C1C1C' }}>
|
||||
The warm, premium wellness home for your entire life.
|
||||
</h1>
|
||||
<p className="mt-6 max-w-2xl text-lg leading-8" style={{ color: '#4C5544' }}>
|
||||
APEX BRAIN brings health, fitness, medical tracking, habits, sleep, hydration, and longevity into one calm, trustworthy experience — with a polished first slice you can use right now.
|
||||
</p>
|
||||
|
||||
<div className="mt-8 flex flex-wrap items-center gap-4">
|
||||
<Link
|
||||
href="/register"
|
||||
className="inline-flex items-center justify-center rounded-full px-6 py-3 text-sm font-semibold text-white transition hover:-translate-y-0.5"
|
||||
style={{ backgroundColor: '#4A6741' }}
|
||||
>
|
||||
Create My Profile
|
||||
</Link>
|
||||
<Link
|
||||
href="/login"
|
||||
className="inline-flex items-center justify-center rounded-full border px-6 py-3 text-sm font-semibold transition hover:-translate-y-0.5"
|
||||
style={{ borderColor: '#D6C8B4', color: '#4A6741', backgroundColor: 'rgba(255,255,255,0.66)' }}
|
||||
>
|
||||
I Already Have an Account
|
||||
</Link>
|
||||
<Link href="/onboarding" className="text-sm font-semibold" style={{ color: '#4A6741' }}>
|
||||
Continue guided onboarding →
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="mt-10 grid gap-4 sm:grid-cols-3">
|
||||
<div className="rounded-[24px] border p-4" style={{ borderColor: '#E1D6C5', backgroundColor: 'rgba(255,255,255,0.72)' }}>
|
||||
<div className="text-xs font-semibold uppercase tracking-[0.18em]" style={{ color: '#7E725E' }}>
|
||||
Aesthetics
|
||||
</div>
|
||||
<p className="mt-3 text-sm leading-6" style={{ color: '#4C5544' }}>
|
||||
Cream backgrounds, forest green accents, and soft gold highlights for a grounded, luxury feel.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-[24px] border p-4" style={{ borderColor: '#E1D6C5', backgroundColor: 'rgba(255,255,255,0.72)' }}>
|
||||
<div className="text-xs font-semibold uppercase tracking-[0.18em]" style={{ color: '#7E725E' }}>
|
||||
Mobile-first
|
||||
</div>
|
||||
<p className="mt-3 text-sm leading-6" style={{ color: '#4C5544' }}>
|
||||
Designed to feel just as refined on iPhone, Android, and desktop screens.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-[24px] border p-4" style={{ borderColor: '#E1D6C5', backgroundColor: 'rgba(255,255,255,0.72)' }}>
|
||||
<div className="text-xs font-semibold uppercase tracking-[0.18em]" style={{ color: '#7E725E' }}>
|
||||
First live slice
|
||||
</div>
|
||||
<p className="mt-3 text-sm leading-6" style={{ color: '#4C5544' }}>
|
||||
Daily Pulse is implemented now as the first real workflow, not just a mockup.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="lg:justify-self-end">
|
||||
<div className="mx-auto max-w-md rounded-[34px] border p-4 shadow-[0_30px_80px_rgba(74,103,65,0.12)]" style={{ borderColor: '#D8CBB7', backgroundColor: 'rgba(255,255,255,0.7)' }}>
|
||||
<div className="rounded-[30px] border p-5" style={{ borderColor: '#E1D6C5', backgroundColor: '#FFF9F0' }}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-xs font-semibold uppercase tracking-[0.18em]" style={{ color: '#7E725E' }}>
|
||||
Daily Pulse preview
|
||||
</div>
|
||||
<div className="mt-2 text-2xl font-semibold" style={{ color: '#1C1C1C' }}>
|
||||
75 / 100 today
|
||||
</div>
|
||||
</div>
|
||||
<span className="rounded-full px-3 py-1 text-xs font-semibold uppercase tracking-[0.16em]" style={{ backgroundColor: 'rgba(74,103,65,0.12)', color: '#2F5A35' }}>
|
||||
live now
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 grid gap-3 sm:grid-cols-2">
|
||||
<div className="rounded-[22px] border p-4" style={{ borderColor: '#E5DAC9', backgroundColor: 'rgba(255,255,255,0.72)' }}>
|
||||
<div className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#4A6741' }}>
|
||||
<BaseIcon path={mdiWaterOutline} size={20} />
|
||||
Hydration
|
||||
</div>
|
||||
<div className="mt-3 text-2xl font-semibold">1,250 ml</div>
|
||||
<p className="mt-2 text-sm" style={{ color: '#6A6A55' }}>2 entries captured today</p>
|
||||
</div>
|
||||
<div className="rounded-[22px] border p-4" style={{ borderColor: '#E5DAC9', backgroundColor: 'rgba(255,255,255,0.72)' }}>
|
||||
<div className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#4A6741' }}>
|
||||
<BaseIcon path={mdiWeatherNight} size={20} />
|
||||
Sleep
|
||||
</div>
|
||||
<div className="mt-3 text-2xl font-semibold">7h 38m</div>
|
||||
<p className="mt-2 text-sm" style={{ color: '#6A6A55' }}>Quality 8 / 10</p>
|
||||
</div>
|
||||
<div className="rounded-[22px] border p-4" style={{ borderColor: '#E5DAC9', backgroundColor: 'rgba(255,255,255,0.72)' }}>
|
||||
<div className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#4A6741' }}>
|
||||
<BaseIcon path={mdiHeartPulse} size={20} />
|
||||
Mood
|
||||
</div>
|
||||
<div className="mt-3 text-2xl font-semibold">8 / 10</div>
|
||||
<p className="mt-2 text-sm" style={{ color: '#6A6A55' }}>Calm • Focused • Optimistic</p>
|
||||
</div>
|
||||
<div className="rounded-[22px] border p-4" style={{ borderColor: '#E5DAC9', backgroundColor: 'rgba(255,255,255,0.72)' }}>
|
||||
<div className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#4A6741' }}>
|
||||
<BaseIcon path={mdiTargetVariant} size={20} />
|
||||
Goal focus
|
||||
</div>
|
||||
<div className="mt-3 text-lg font-semibold">Build better daily habits</div>
|
||||
<p className="mt-2 text-sm" style={{ color: '#6A6A55' }}>One ritual connected to the rest of the app.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Link href="/daily-pulse" className="mt-5 inline-flex items-center gap-2 text-sm font-semibold" style={{ color: '#4A6741' }}>
|
||||
Open the Daily Pulse slice
|
||||
<BaseIcon path={mdiArrowTopRight} size={18} />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BaseButtons>
|
||||
<BaseButton
|
||||
href='/login'
|
||||
label='Login'
|
||||
color='info'
|
||||
className='w-full'
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
</BaseButtons>
|
||||
</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>
|
||||
<main>
|
||||
<section className="mx-auto max-w-7xl px-6 py-16 lg:px-10 lg:py-20">
|
||||
<div className="max-w-3xl">
|
||||
<div className="text-sm font-semibold uppercase tracking-[0.2em]" style={{ color: '#7E725E' }}>
|
||||
FIRST DELIVERY
|
||||
</div>
|
||||
<h2 className="mt-4 text-4xl font-semibold tracking-tight" style={{ color: '#1C1C1C' }}>
|
||||
A complete first win, not just a landing page.
|
||||
</h2>
|
||||
<p className="mt-4 text-lg leading-8" style={{ color: '#4C5544' }}>
|
||||
The initial APEX BRAIN experience is centered around a premium Daily Pulse workflow: create inputs, see a summary ring, review recent activity, and jump into full detail screens.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className="mt-10 grid gap-5 lg:grid-cols-3">
|
||||
{promiseCards.map((card) => (
|
||||
<div key={card.title} className="rounded-[28px] border p-6 shadow-sm" style={{ borderColor: '#E1D6C5', backgroundColor: '#FFF9F0' }}>
|
||||
<div className="flex h-14 w-14 items-center justify-center rounded-2xl" style={{ backgroundColor: '#F1E7D8', color: '#4A6741' }}>
|
||||
<BaseIcon path={card.icon} size={26} />
|
||||
</div>
|
||||
<h3 className="mt-5 text-xl font-semibold" style={{ color: '#1C1C1C' }}>
|
||||
{card.title}
|
||||
</h3>
|
||||
<p className="mt-3 text-sm leading-7" style={{ color: '#4C5544' }}>
|
||||
{card.description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="border-y" style={{ borderColor: '#E1D6C5', backgroundColor: '#EFE6DB' }}>
|
||||
<div className="mx-auto max-w-7xl px-6 py-16 lg:px-10 lg:py-20">
|
||||
<div className="grid gap-10 lg:grid-cols-[0.9fr_1.1fr] lg:items-center">
|
||||
<div>
|
||||
<div className="text-sm font-semibold uppercase tracking-[0.2em]" style={{ color: '#7E725E' }}>
|
||||
How the slice works
|
||||
</div>
|
||||
<h2 className="mt-4 text-4xl font-semibold tracking-tight" style={{ color: '#1C1C1C' }}>
|
||||
One calm ritual that already feels like a real health product.
|
||||
</h2>
|
||||
<p className="mt-4 text-lg leading-8" style={{ color: '#4C5544' }}>
|
||||
Instead of scattering the experience across dozens of unfinished screens, the first release centers attention on the one journey users repeat every day.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
{workflowSteps.map((item) => (
|
||||
<div key={item.step} className="rounded-[28px] border p-5" style={{ borderColor: '#D6C8B4', backgroundColor: '#FFF9F0' }}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm font-semibold uppercase tracking-[0.18em]" style={{ color: '#7E725E' }}>
|
||||
{item.step}
|
||||
</div>
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-2xl" style={{ backgroundColor: '#F1E7D8', color: '#4A6741' }}>
|
||||
<BaseIcon path={item.icon} size={20} />
|
||||
</div>
|
||||
</div>
|
||||
<h3 className="mt-5 text-lg font-semibold" style={{ color: '#1C1C1C' }}>
|
||||
{item.title}
|
||||
</h3>
|
||||
<p className="mt-3 text-sm leading-7" style={{ color: '#4C5544' }}>
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mx-auto max-w-7xl px-6 py-16 lg:px-10 lg:py-20">
|
||||
<div className="max-w-3xl">
|
||||
<div className="text-sm font-semibold uppercase tracking-[0.2em]" style={{ color: '#7E725E' }}>
|
||||
What comes next
|
||||
</div>
|
||||
<h2 className="mt-4 text-4xl font-semibold tracking-tight" style={{ color: '#1C1C1C' }}>
|
||||
The foundation is designed to expand into the full APEX BRAIN vision.
|
||||
</h2>
|
||||
<p className="mt-4 text-lg leading-8" style={{ color: '#4C5544' }}>
|
||||
The app already has the building blocks for a much broader health platform. Daily Pulse is the first polished slice that can grow into the complete ecosystem below.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 flex flex-wrap gap-3">
|
||||
{featureChips.map((chip) => (
|
||||
<span key={chip} className="rounded-full border px-4 py-2 text-sm font-semibold" style={{ borderColor: '#D6C8B4', color: '#4A6741', backgroundColor: '#FFF9F0' }}>
|
||||
{chip}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-10 rounded-[32px] border p-8" style={{ borderColor: '#D8CBB7', backgroundColor: '#FFF9F0' }}>
|
||||
<div className="flex flex-col gap-6 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div>
|
||||
<div className="text-sm font-semibold uppercase tracking-[0.2em]" style={{ color: '#7E725E' }}>
|
||||
Ready to continue?
|
||||
</div>
|
||||
<h3 className="mt-3 text-3xl font-semibold" style={{ color: '#1C1C1C' }}>
|
||||
Start with the branded slice, then tell us which area should expand next.
|
||||
</h3>
|
||||
<p className="mt-3 max-w-2xl text-base leading-7" style={{ color: '#4C5544' }}>
|
||||
You can create an account, start the guided onboarding flow, sign in to the admin interface, or go straight to Daily Pulse once your setup feels right.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Link
|
||||
href="/onboarding"
|
||||
className="inline-flex items-center justify-center rounded-full px-6 py-3 text-sm font-semibold text-white transition hover:-translate-y-0.5"
|
||||
style={{ backgroundColor: '#4A6741' }}
|
||||
>
|
||||
Start onboarding
|
||||
</Link>
|
||||
<Link
|
||||
href="/daily-pulse"
|
||||
className="inline-flex items-center justify-center rounded-full border px-6 py-3 text-sm font-semibold transition hover:-translate-y-0.5"
|
||||
style={{ borderColor: '#D6C8B4', color: '#4A6741' }}
|
||||
>
|
||||
Open Daily Pulse
|
||||
</Link>
|
||||
<Link
|
||||
href="/login"
|
||||
className="inline-flex items-center justify-center rounded-full border px-6 py-3 text-sm font-semibold transition hover:-translate-y-0.5"
|
||||
style={{ borderColor: '#D6C8B4', color: '#4A6741' }}
|
||||
>
|
||||
Admin Login
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer className="border-t" style={{ borderColor: '#E1D6C5' }}>
|
||||
<div className="mx-auto flex max-w-7xl flex-col gap-3 px-6 py-6 text-sm lg:flex-row lg:items-center lg:justify-between lg:px-10" style={{ color: '#6A6A55' }}>
|
||||
<p>© 2026 APEX BRAIN. Calm, premium health management in one place.</p>
|
||||
<div className="flex flex-wrap items-center gap-4 font-semibold">
|
||||
<Link href="/register" style={{ color: '#4A6741' }}>
|
||||
Create profile
|
||||
</Link>
|
||||
<Link href="/login" style={{ color: '#4A6741' }}>
|
||||
Login
|
||||
</Link>
|
||||
<Link href="/onboarding" style={{ color: '#4A6741' }}>
|
||||
Onboarding
|
||||
</Link>
|
||||
<Link href="/dashboard" style={{ color: '#4A6741' }}>
|
||||
Dashboard
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Starter.getLayout = function getLayout(page: ReactElement) {
|
||||
return <LayoutGuest>{page}</LayoutGuest>;
|
||||
};
|
||||
|
||||
|
||||
@ -21,6 +21,7 @@ import { useAppDispatch, useAppSelector } from '../stores/hooks';
|
||||
import Link from 'next/link';
|
||||
import {toast, ToastContainer} from "react-toastify";
|
||||
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels'
|
||||
import { hasCompletedApexOnboarding } from '../helpers/apexOnboarding';
|
||||
|
||||
export default function Login() {
|
||||
const router = useRouter();
|
||||
@ -64,10 +65,15 @@ export default function Login() {
|
||||
}, [token, dispatch]);
|
||||
// Redirect to dashboard if user is logged in
|
||||
useEffect(() => {
|
||||
if (currentUser?.id) {
|
||||
router.push('/dashboard');
|
||||
if (!currentUser?.id) {
|
||||
return;
|
||||
}
|
||||
}, [currentUser?.id, router]);
|
||||
|
||||
const shouldStartOnboarding =
|
||||
!hasCompletedApexOnboarding(currentUser.id) && !currentUser.firstName;
|
||||
|
||||
router.push(shouldStartOnboarding ? '/onboarding' : '/dashboard');
|
||||
}, [currentUser?.firstName, currentUser?.id, router]);
|
||||
// Show error message if there is one
|
||||
useEffect(() => {
|
||||
if (errorMessage){
|
||||
|
||||
1626
frontend/src/pages/onboarding.tsx
Normal file
1626
frontend/src/pages/onboarding.tsx
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user