Autosave: 20260501-154034

This commit is contained in:
Flatlogic Bot 2026-05-01 15:40:33 +00:00
parent 0e22f33482
commit 068b3eea03
10 changed files with 5768 additions and 3536 deletions

View 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>
)
}

View File

@ -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'

View 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);
}

View File

@ -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>
)

View File

@ -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,
},
{

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -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 apps 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 todays 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>;
};

View File

@ -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){

File diff suppressed because it is too large Load Diff