{
- if (setSelectedHealthyOptionId && id) {
- setSelectedHealthyOptionId(id);
- }
- }}
role="checkbox"
aria-checked={selectedHealthyOptionId === Number(1)}
tabIndex={0}
diff --git a/src/components/longevity/YourDiet/YourDiet.module.scss b/src/components/longevity/YourDiet/YourDiet.module.scss
index f9d6242..4806850 100644
--- a/src/components/longevity/YourDiet/YourDiet.module.scss
+++ b/src/components/longevity/YourDiet/YourDiet.module.scss
@@ -10,8 +10,6 @@
max-width: 828px;
flex-direction: column;
align-items: center;
- // TODO: add when user selects a diet
- //@extend .animate-fadeIn;
.wrapper {
margin-top: 48px;
@@ -20,7 +18,7 @@
gap: 80px;
.icon {
- filter: drop-shadow(0px 0px 18px rgba(53, 35, 13, 0.4));
+ filter: drop-shadow(0px 0px 18px rgba(53, 35, 13, 0.2));
}
p {
@@ -52,6 +50,10 @@
}
}
+.active {
+ @extend .animate-fadeIn;
+}
+
@media (max-width: 965px) {
.selectedDiet {
max-width: unset;
diff --git a/src/components/longevity/YourDiet/YourDiet.tsx b/src/components/longevity/YourDiet/YourDiet.tsx
index be466f1..ef7a223 100644
--- a/src/components/longevity/YourDiet/YourDiet.tsx
+++ b/src/components/longevity/YourDiet/YourDiet.tsx
@@ -1,5 +1,6 @@
import { FC } from 'react';
import Image from 'next/image';
+import cn from 'classnames';
import Heading from '@components/Heading';
@@ -9,7 +10,12 @@ import { YourDietProps } from './YourDiet.types';
import styles from './YourDiet.module.scss';
-const YourDiet: FC
= ({ id, scaleLevels }) => {
+const YourDiet: FC = ({
+ id,
+ scaleLevels,
+ isIconClicked,
+ selectedHealthOptionName,
+}) => {
const selectedDiet = scaleLevels.find((level: any) => level.id === id);
const isMobile = useIsWidthLessThan(956);
const backgroundImageUrl = isMobile
@@ -23,13 +29,15 @@ const YourDiet: FC = ({ id, scaleLevels }) => {
return (
= ({ locale, data }) => {
- const [selectedHealthyOptionId, setSelectedHealthyOptionId] = useState(3);
-
+ const [selectedHealthyOptionId, setSelectedHealthyOptionId] = useState(1);
+ const [isIconClicked, setIsIconClicked] = useState(false);
const foodFacts = data['food science facts that most influenced my choices'];
const items = data['what not to eat'];
+ const getSelectedHealthOptionName = (id: number) => {
+ const selectedOption = data?.['what to eat']?.find(
+ (option: any) => option.id === id,
+ );
+ return selectedOption ? selectedOption['product name'] : null;
+ };
+ const whatToEatItemNamesAndIds = () => {
+ return (
+ data?.['what to eat']?.map((option: any) => ({
+ id: option.id,
+ name: option['product name'],
+ })) || []
+ );
+ };
+
+ const selectedHealthOption = {
+ id: selectedHealthyOptionId,
+ name: getSelectedHealthOptionName(selectedHealthyOptionId),
+ };
const whatNotToEat = items?.map((item, index) => ({
...item,
@@ -100,8 +119,16 @@ const DietLayout: FC = ({ locale, data }) => {
scaleLevels={scaleLevels}
id={selectedHealthyOptionId}
setSelectedHealthyOptionId={setSelectedHealthyOptionId}
+ selectedHealthOption={selectedHealthOption}
+ whatToEatItemNamesAndIds={whatToEatItemNamesAndIds()}
+ setIsIconClicked={setIsIconClicked}
+ />
+
-
= {
+ default: '/keepsimple_/assets/longevity/dna/default.mp4',
+ red: '/keepsimple_/assets/longevity/dna/red.mp4',
+ blue: '/keepsimple_/assets/longevity/dna/blue.mp4',
+ 'red-and-blue': '/keepsimple_/assets/longevity/dna/red-and-blue.mp4',
+};
+
+function pickLayer(pathname: string): LayerKey {
+ const base = '/tools/longevity-protocol';
+ if (!pathname.startsWith(base)) return 'default';
+
+ const rest = pathname.slice(base.length);
+ if (rest === '/about-project' || rest === '' || rest === '/')
+ return 'default';
+ if (rest === '/environment') return 'blue';
+ if (rest === '/results') return 'red-and-blue';
+ if (rest === '/habits' || rest.startsWith('/habits/')) return 'red';
+ return 'default';
+}
+
export default function Layout({ children }: { children: React.ReactNode }) {
const router = useRouter();
+
const sectionRef = useRef(null);
+ const canvasRef = useRef(null);
+ const videoLayerRef = useRef(null);
+
+ const videosRef = useRef>>(
+ {},
+ );
+
const [isLongevityProtocolPage, setIsLongevityProtocolPage] = useState(false);
+ const [activeLayer, setActiveLayer] = useState('default');
+
+ const [transitionsOn, setTransitionsOn] = useState(false);
+ const [canvasVisible, setCanvasVisible] = useState(true);
+
const isMobile = useIsWidthLessThan(956);
useEffect(() => {
@@ -24,16 +59,44 @@ export default function Layout({ children }: { children: React.ReactNode }) {
}
}, [router.pathname]);
- const videoRef = useRef(null);
- const canvasRef = useRef(null);
- const videoLayerRef = useRef(null);
+ useLayoutEffect(() => {
+ if (!router.pathname.startsWith('/tools/longevity-protocol')) return;
+
+ const initial = pickLayer(router.pathname);
+ setTransitionsOn(false);
+ setCanvasVisible(true);
+ setActiveLayer(initial);
+
+ const id = requestAnimationFrame(() => setTransitionsOn(true));
+ return () => cancelAnimationFrame(id);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ useLayoutEffect(() => {
+ if (!isLongevityProtocolPage) return;
+
+ const next = pickLayer(router.pathname);
+ if (next === activeLayer) return;
+
+ setCanvasVisible(false);
+
+ const FADE_MS = 250;
+ const t = window.setTimeout(() => {
+ setActiveLayer(next);
+ setCanvasVisible(true);
+ }, FADE_MS);
+
+ return () => window.clearTimeout(t);
+ }, [router.pathname, isLongevityProtocolPage, activeLayer]);
+
useEffect(() => {
if (!isLongevityProtocolPage) return;
const layer = videoLayerRef.current;
const canvas = canvasRef.current;
- const video = videoRef.current;
- if (!layer || !canvas || !video) return;
+ if (!layer || !canvas) return;
+
+ const getActiveVideo = () => videosRef.current[activeLayer] ?? null;
const ctx = canvas.getContext('2d', { alpha: true });
if (!ctx) return;
@@ -48,20 +111,40 @@ export default function Layout({ children }: { children: React.ReactNode }) {
const dpr = Math.min(2, window.devicePixelRatio || 1);
+ const canvasCSS = 175;
const getSizes = () => {
- const h = Math.max(1, Math.ceil(layer.getBoundingClientRect().height));
- const w = Math.max(1, Math.ceil(canvas.getBoundingClientRect().width));
+ const rect = layer.getBoundingClientRect();
+ const h = Math.max(1, Math.ceil(rect.height));
+ const w = canvasCSS;
return { w, h };
};
- const resize = () => {
- const { w, h } = getSizes();
+ let needsResize = true;
+ const scheduleResize = () => {
+ needsResize = true;
+ };
+
+ const applyResizeIfNeeded = () => {
+ if (!needsResize) return;
+ needsResize = false;
+
+ const rect = layer.getBoundingClientRect();
+ if (rect.height <= 2 || layer.offsetParent === null) {
+ needsResize = true;
+ return;
+ }
+
+ const w = canvasCSS;
+ const h = Math.max(1, Math.ceil(rect.height));
canvas.style.width = `${w}px`;
canvas.style.height = `${h}px`;
- canvas.width = Math.max(1, Math.round(w * dpr));
- canvas.height = Math.max(1, Math.round(h * dpr));
+ const bw = Math.max(1, Math.round(w * dpr));
+ const bh = Math.max(1, Math.round(h * dpr));
+
+ if (canvas.width !== bw) canvas.width = bw;
+ if (canvas.height !== bh) canvas.height = bh;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
};
@@ -69,27 +152,33 @@ export default function Layout({ children }: { children: React.ReactNode }) {
const draw = () => {
if (stopped) return;
- if (!visible) {
+ if (!visible || layer.offsetParent === null) {
raf = requestAnimationFrame(draw);
return;
}
+ applyResizeIfNeeded();
+
const { w, h } = getSizes();
+ const video = getActiveVideo();
+
+ if (!video) {
+ raf = requestAnimationFrame(draw);
+ return;
+ }
const vw = video.videoWidth;
const vh = video.videoHeight;
- if (!vw || !vh || video.paused || video.ended) {
+ if (!vw || !vh || video.paused) {
raf = requestAnimationFrame(draw);
return;
}
const scale = w / vw;
-
const tileH = Math.max(1, Math.round(vh * scale));
ctx.clearRect(0, 0, w, h);
-
for (let y = 0; y < h + tileH; y += tileH) {
ctx.drawImage(video, 0, y, w, tileH);
}
@@ -97,7 +186,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
raf = requestAnimationFrame(draw);
};
- const ro = new ResizeObserver(() => resize());
+ const ro = new ResizeObserver(() => scheduleResize());
ro.observe(layer);
const io = new IntersectionObserver(
@@ -108,24 +197,12 @@ export default function Layout({ children }: { children: React.ReactNode }) {
);
io.observe(layer);
- const onMeta = () => {
- resize();
- video.play().catch(() => {});
- };
- video.addEventListener('loadedmetadata', onMeta);
-
const onVis = () => {
- if (document.visibilityState === 'visible') {
- resize();
- video.play().catch(() => {});
- }
+ if (document.visibilityState === 'visible') scheduleResize();
};
document.addEventListener('visibilitychange', onVis);
- const onRouteDone = () => requestAnimationFrame(resize);
- router.events.on('routeChangeComplete', onRouteDone);
-
- resize();
+ scheduleResize();
raf = requestAnimationFrame(draw);
return () => {
@@ -133,11 +210,18 @@ export default function Layout({ children }: { children: React.ReactNode }) {
cancelAnimationFrame(raf);
ro.disconnect();
io.disconnect();
- video.removeEventListener('loadedmetadata', onMeta);
document.removeEventListener('visibilitychange', onVis);
- router.events.off('routeChangeComplete', onRouteDone);
};
- }, [isLongevityProtocolPage, router.events]);
+ }, [isLongevityProtocolPage, activeLayer]);
+
+ useEffect(() => {
+ if (!isLongevityProtocolPage) return;
+
+ (Object.keys(SOURCES) as LayerKey[]).forEach(k => {
+ const v = videosRef.current[k];
+ v?.play?.().catch(() => {});
+ });
+ }, [isLongevityProtocolPage]);
return (
<>
@@ -153,30 +237,40 @@ export default function Layout({ children }: { children: React.ReactNode }) {
)
) : null}
+
{isLongevityProtocolPage ? (
-
-
+ {(Object.keys(SOURCES) as LayerKey[]).map(k => (
+
-
{children}
) : (
diff --git a/src/layouts/SleepLayout/SleepLayout.tsx b/src/layouts/SleepLayout/SleepLayout.tsx
index 4c08102..6afbd72 100644
--- a/src/layouts/SleepLayout/SleepLayout.tsx
+++ b/src/layouts/SleepLayout/SleepLayout.tsx
@@ -1,5 +1,4 @@
-import { FC, useRef, useState } from 'react';
-import html2canvas from 'html2canvas';
+import { FC, useState } from 'react';
import Image from 'next/image';
import Modal from '@components/Modal';
@@ -16,30 +15,13 @@ import styles from './SleepLayout.module.scss';
const SleepLayout: FC = ({ locale, data, supplements }) => {
const isMobile = useIsWidthLessThan(1140);
- const tableRef = useRef(null);
// TODO: move to constants
const imgPath = '/keepsimple_/assets/longevity/sleep/';
const [open, setOpen] = useState(false);
- const [imgSrc, setImgSrc] = useState('');
-
- const makeTableImage = async () => {
- if (!tableRef.current) return;
-
- await new Promise(r => setTimeout(r, 50));
-
- const canvas = await html2canvas(tableRef.current, {
- backgroundColor: '#fff',
- scale: Math.min(2, window.devicePixelRatio || 1),
- useCORS: true,
- });
-
- setImgSrc(canvas.toDataURL('image/png'));
- };
const handleOpen = async () => {
setOpen(true);
- await makeTableImage();
};
// TODO: move to constants
@@ -86,24 +68,6 @@ const SleepLayout: FC = ({ locale, data, supplements }) => {
description={data['used devices']}
headlineBackgroundImageUrl={`${imgPath}used-devices-header.png`}
/>
- {isMobile && (
-
- )}
{isMobile && (
<>
{open && (
- setOpen(false)}>
- setOpen(false)} fullSizeMobile>
+
)}
diff --git a/src/layouts/StudyLayout/StudyLayout.tsx b/src/layouts/StudyLayout/StudyLayout.tsx
index 1fef197..e13e6dc 100644
--- a/src/layouts/StudyLayout/StudyLayout.tsx
+++ b/src/layouts/StudyLayout/StudyLayout.tsx
@@ -22,6 +22,7 @@ const StudyLayout: FC = ({ data, locale }) => {
flippedCardHeadline={data.books?.['flipped card headline']}
flippedCardPainText={data.books?.['flipped card pain caption']}
flippedCardChart={`${process.env.NEXT_PUBLIC_STRAPI}${data?.['books flipped card image']?.data?.attributes?.url}`}
+ chartWidth={387}
/>
= ({ data, locale }) => {
flippedCardHeadline={data['book notes']?.['flipped card headline']}
flippedCardPainText={data['book notes']?.['flipped card pain caption']}
flippedCardChart={`${process.env.NEXT_PUBLIC_STRAPI}${data?.['books notes flipped card image']?.data?.attributes?.url}`}
+ chartWidth={387}
+ flippedCardChartMobile={
+ '/keepsimple_/assets/longevity/study/mobile-charts/book-notes-chart.webp'
+ }
/>
= ({ data, locale }) => {
flippedCardHeadline={data['daily work']?.['flipped card headline']}
flippedCardPainText={data['daily work']?.['flipped card pain caption']}
flippedCardChart={`${process.env.NEXT_PUBLIC_STRAPI}${data?.['daily work flipped card image']?.data?.attributes?.url}`}
+ flippedCardChartMobile={
+ '/keepsimple_/assets/longevity/study/mobile-charts/daily-work.webp'
+ }
+ chartWidth={810}
/>
= ({ data, locale }) => {
data['research tasks']?.['flipped card pain caption']
}
flippedCardChart={`${process.env.NEXT_PUBLIC_STRAPI}${data?.['research tasks flipped card image']?.data?.attributes?.url}`}
+ chartWidth={590}
+ flippedCardChartMobile={
+ '/keepsimple_/assets/longevity/study/mobile-charts/research-task.webp'
+ }
/>
= ({ data, locale }) => {
flippedCardHeadline={data.data?.['flipped card headline']}
flippedCardPainText={data.data?.['flipped card pain caption']}
flippedCardChart={`${process.env.NEXT_PUBLIC_STRAPI}${data?.['data flipped card image']?.data?.attributes?.url}`}
+ chartWidth={390}
+ flippedCardChartMobile={
+ '/keepsimple_/assets/longevity/study/mobile-charts/data-chart.webp'
+ }
/>
= ({ data, locale }) => {
quoteAuthor={data.hacks?.['hacks card quote author']}
flippedCardChart={`${process.env.NEXT_PUBLIC_STRAPI}${data?.['hacks flipped card image']?.data?.attributes?.url}`}
backsBackgroundImageUrl={`${process.env.NEXT_PUBLIC_STRAPI}${data?.['hacks flipped card image']?.data?.attributes?.url}`}
+ chartWidth={390}
/>
);
diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx
index fbd1384..69a479c 100644
--- a/src/pages/_app.tsx
+++ b/src/pages/_app.tsx
@@ -12,6 +12,7 @@ import Layout from '@layouts/Layout';
import { GlobalContext } from '@components/Context/GlobalContext';
import Box from 'src/components/Box';
+import Loader from '@components/longevity/Loader';
import { authenticate } from '@api/auth';
import mixpanel, { initMixpanel, trackPageView } from '../../lib/mixpanel';
@@ -33,9 +34,13 @@ function App({ Component, pageProps: { session, ...pageProps } }: TApp) {
const loadingTimer = useRef(null);
const [accountData, setAccountData] = useState(null);
const [token, setToken] = useState(null);
+ const [heroReady, setHeroReady] = useState(true);
+ const [routeLoading, setRouteLoading] = useState(false);
+ const [longevityTransition, setLongevityTransition] = useState(false);
const isIndexingOn = process.env.NEXT_PUBLIC_INDEXING === 'on';
const isProduction = process.env.NEXT_PUBLIC_ENV === 'prod';
+ const longevityBaseUrl = '/tools/longevity-protocol';
const { initUseMobile } = useMobile()[0];
const { events } = useRouter();
const { setIsVisible } = useSpinner()[0];
@@ -218,6 +223,44 @@ function App({ Component, pageProps: { session, ...pageProps } }: TApp) {
}
}, [accountData?.id, accountData?.createdAt]);
+ const clean = (url: string) =>
+ url.split('?')[0].split('#')[0].replace(/\/+$/, '');
+ const isLongevityUrl = (url: string) =>
+ clean(url).startsWith(longevityBaseUrl);
+
+ useEffect(() => {
+ const onStart = (url: string) => {
+ const fromLongevity = isLongevityUrl(router.asPath);
+ const toLongevity = isLongevityUrl(url);
+
+ const shouldGate = fromLongevity && toLongevity;
+
+ setLongevityTransition(shouldGate);
+
+ if (shouldGate) {
+ setHeroReady(false);
+ setRouteLoading(true);
+ }
+ };
+
+ const onDone = () => {
+ setRouteLoading(false);
+ setLongevityTransition(false);
+ };
+
+ router.events.on('routeChangeStart', onStart);
+ router.events.on('routeChangeComplete', onDone);
+ router.events.on('routeChangeError', onDone);
+
+ return () => {
+ router.events.off('routeChangeStart', onStart);
+ router.events.off('routeChangeComplete', onDone);
+ router.events.off('routeChangeError', onDone);
+ };
+ }, [router]);
+
+ const overlayOn = longevityTransition && (routeLoading || !heroReady);
+
return (