diff --git a/public/keepsimple_/assets/longevity/dna-white-bg-upd.gif b/public/keepsimple_/assets/longevity/dna-white-bg-upd.gif deleted file mode 100644 index a2bdb91..0000000 Binary files a/public/keepsimple_/assets/longevity/dna-white-bg-upd.gif and /dev/null differ diff --git a/public/keepsimple_/assets/longevity/dna-white-bg-upd.mp4 b/public/keepsimple_/assets/longevity/dna-white-bg-upd.mp4 deleted file mode 100644 index d1822ac..0000000 Binary files a/public/keepsimple_/assets/longevity/dna-white-bg-upd.mp4 and /dev/null differ diff --git a/public/keepsimple_/assets/longevity/dna-white-bg.mp4 b/public/keepsimple_/assets/longevity/dna-white-bg.mp4 deleted file mode 100644 index 2a4718c..0000000 Binary files a/public/keepsimple_/assets/longevity/dna-white-bg.mp4 and /dev/null differ diff --git a/public/keepsimple_/assets/longevity/dna.mp4 b/public/keepsimple_/assets/longevity/dna.mp4 deleted file mode 100644 index e1da447..0000000 Binary files a/public/keepsimple_/assets/longevity/dna.mp4 and /dev/null differ diff --git a/public/keepsimple_/assets/longevity/dna/blue.mp4 b/public/keepsimple_/assets/longevity/dna/blue.mp4 new file mode 100644 index 0000000..4f21344 Binary files /dev/null and b/public/keepsimple_/assets/longevity/dna/blue.mp4 differ diff --git a/public/keepsimple_/assets/longevity/dna-1.mp4 b/public/keepsimple_/assets/longevity/dna/default.mp4 similarity index 100% rename from public/keepsimple_/assets/longevity/dna-1.mp4 rename to public/keepsimple_/assets/longevity/dna/default.mp4 diff --git a/public/keepsimple_/assets/longevity/dna/red-and-blue.mp4 b/public/keepsimple_/assets/longevity/dna/red-and-blue.mp4 new file mode 100644 index 0000000..3cfdae7 Binary files /dev/null and b/public/keepsimple_/assets/longevity/dna/red-and-blue.mp4 differ diff --git a/public/keepsimple_/assets/longevity/dna/red.mp4 b/public/keepsimple_/assets/longevity/dna/red.mp4 new file mode 100644 index 0000000..c10b303 Binary files /dev/null and b/public/keepsimple_/assets/longevity/dna/red.mp4 differ diff --git a/public/keepsimple_/assets/longevity/study/mobile-charts/book-notes-chart.webp b/public/keepsimple_/assets/longevity/study/mobile-charts/book-notes-chart.webp new file mode 100644 index 0000000..55fbc38 Binary files /dev/null and b/public/keepsimple_/assets/longevity/study/mobile-charts/book-notes-chart.webp differ diff --git a/public/keepsimple_/assets/longevity/study/mobile-charts/daily-work.webp b/public/keepsimple_/assets/longevity/study/mobile-charts/daily-work.webp new file mode 100644 index 0000000..ec7ab2f Binary files /dev/null and b/public/keepsimple_/assets/longevity/study/mobile-charts/daily-work.webp differ diff --git a/public/keepsimple_/assets/longevity/study/mobile-charts/data-chart.webp b/public/keepsimple_/assets/longevity/study/mobile-charts/data-chart.webp new file mode 100644 index 0000000..cecefdc Binary files /dev/null and b/public/keepsimple_/assets/longevity/study/mobile-charts/data-chart.webp differ diff --git a/public/keepsimple_/assets/longevity/study/mobile-charts/memory-retention.webp b/public/keepsimple_/assets/longevity/study/mobile-charts/memory-retention.webp new file mode 100644 index 0000000..50d9205 Binary files /dev/null and b/public/keepsimple_/assets/longevity/study/mobile-charts/memory-retention.webp differ diff --git a/public/keepsimple_/assets/longevity/study/mobile-charts/research-task.webp b/public/keepsimple_/assets/longevity/study/mobile-charts/research-task.webp new file mode 100644 index 0000000..cecefdc Binary files /dev/null and b/public/keepsimple_/assets/longevity/study/mobile-charts/research-task.webp differ diff --git a/src/assets/icons/longevity/Borders.tsx b/src/assets/icons/longevity/Borders.tsx new file mode 100644 index 0000000..e2fa5c0 --- /dev/null +++ b/src/assets/icons/longevity/Borders.tsx @@ -0,0 +1,17 @@ +const Borders = ({ className }) => ( + + + +); + +export default Borders; diff --git a/src/assets/icons/longevity/LearnMoreIcon.tsx b/src/assets/icons/longevity/LearnMoreIcon.tsx new file mode 100644 index 0000000..d38ccb7 --- /dev/null +++ b/src/assets/icons/longevity/LearnMoreIcon.tsx @@ -0,0 +1,20 @@ +export const LearnMoreIcon = () => ( + + + + +); + +export default LearnMoreIcon; diff --git a/src/assets/icons/longevity/Study/CloseIcon.tsx b/src/assets/icons/longevity/Study/CloseIcon.tsx new file mode 100644 index 0000000..837ffc0 --- /dev/null +++ b/src/assets/icons/longevity/Study/CloseIcon.tsx @@ -0,0 +1,42 @@ +export const StudyCloseIcon = () => ( + + + + + + + + + + +); diff --git a/src/components/longevity/BorderedPill/BorderedPill.module.scss b/src/components/longevity/BorderedPill/BorderedPill.module.scss new file mode 100644 index 0000000..2f7b539 --- /dev/null +++ b/src/components/longevity/BorderedPill/BorderedPill.module.scss @@ -0,0 +1,49 @@ +.root { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + background-color: #ffffff66; + + width: 100%; + max-width: 320px; + min-height: 42px; + padding: 9px 28px; + border: 0; + cursor: pointer; + + .border { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 0; + } + + .content { + position: relative; + z-index: 1; + display: flex; + gap: 12px; + font-size: 16px; + + svg { + padding-top: 3px; + } + + .label { + padding-top: 2px; + } + } +} + +.white { + color: #fff; + + svg { + path { + fill: #fff; + } + } +} diff --git a/src/components/longevity/BorderedPill/BorderedPill.tsx b/src/components/longevity/BorderedPill/BorderedPill.tsx new file mode 100644 index 0000000..8a73803 --- /dev/null +++ b/src/components/longevity/BorderedPill/BorderedPill.tsx @@ -0,0 +1,35 @@ +import React, { ElementType } from 'react'; +import cn from 'classnames'; + +import type { BorderedPillProps } from './BorderedPill.types'; + +import Borders from '@icons/longevity/Borders'; + +import styles from './BorderedPill.module.scss'; + +export function BorderedPill({ + as, + text, + leftIcon, + children, + className, + isWhite, + ...rest +}: BorderedPillProps) { + const Component = (as ?? 'button') as ElementType; + + return ( + + + + {leftIcon ? {leftIcon} : null} + {text ? {text} : children} + + + ); +} diff --git a/src/components/longevity/BorderedPill/BorderedPill.types.ts b/src/components/longevity/BorderedPill/BorderedPill.types.ts new file mode 100644 index 0000000..def41d0 --- /dev/null +++ b/src/components/longevity/BorderedPill/BorderedPill.types.ts @@ -0,0 +1,25 @@ +import type { ElementType, ReactNode } from 'react'; + +type CommonProps = { + as?: T; + text?: string; + leftIcon?: ReactNode; + className?: string; + isWhite?: boolean; + children?: ReactNode; +}; + +type ButtonOnlyProps = { + onClick?: React.ButtonHTMLAttributes['onClick']; + type?: 'button' | 'submit' | 'reset'; + disabled?: boolean; +}; + +type NonButtonProps = { + onClick?: never; + type?: never; + disabled?: never; +}; + +export type BorderedPillProps = + CommonProps & (T extends 'button' ? ButtonOnlyProps : NonButtonProps); diff --git a/src/components/longevity/BorderedPill/index.ts b/src/components/longevity/BorderedPill/index.ts new file mode 100644 index 0000000..599df19 --- /dev/null +++ b/src/components/longevity/BorderedPill/index.ts @@ -0,0 +1,3 @@ +import { BorderedPill } from './BorderedPill'; + +export default BorderedPill; diff --git a/src/components/longevity/DietResults/DietResults.module.scss b/src/components/longevity/DietResults/DietResults.module.scss index 9f8c45f..5ee91d8 100644 --- a/src/components/longevity/DietResults/DietResults.module.scss +++ b/src/components/longevity/DietResults/DietResults.module.scss @@ -19,6 +19,10 @@ opacity: 60%; cursor: pointer; + &:hover { + opacity: 1; + } + &.active { opacity: 1; @@ -38,7 +42,7 @@ } .selected { - font-size: 8px; + font-size: 12px; color: #fff; text-transform: uppercase; max-width: 100px; @@ -69,22 +73,18 @@ transform: translateY(0); } - .defaultLabel { - font-size: 8px; - text-transform: uppercase; - left: 0; - font-family: Aboreto-Regular, sans-serif; - font-weight: 400; - background-repeat: no-repeat; - background-size: cover; - bottom: 5px; + .tooltip { + background-color: #f2e7d6; color: #000000d9; - width: 127px; - position: absolute; - background-image: url('/keepsimple_/assets/longevity/diet/default-label-bg.png'); - height: 11px; - text-align: center; - padding-top: 3px; + font-size: 14px; + margin-top: -40px; + z-index: 788; + padding: 2.5px 9px; + border-radius: unset; + + div { + display: none; + } } } diff --git a/src/components/longevity/DietResults/DietResults.tsx b/src/components/longevity/DietResults/DietResults.tsx index 954748d..25d3b44 100644 --- a/src/components/longevity/DietResults/DietResults.tsx +++ b/src/components/longevity/DietResults/DietResults.tsx @@ -1,6 +1,7 @@ import { FC } from 'react'; import Image from 'next/image'; import cn from 'classnames'; +import { Tooltip as ReactTooltip } from 'react-tooltip'; import { DietResultsProps } from './DietResults.types'; @@ -10,16 +11,30 @@ const DietResults: FC = ({ id, scaleLevels, setSelectedHealthyOptionId, + whatToEatItemNamesAndIds, + setIsIconClicked, }) => { + const getSelectedHealthOptionName = (id: number) => { + const selectedOption = whatToEatItemNamesAndIds.find( + (option: any) => option.id === id, + ); + return selectedOption ? selectedOption.name : null; + }; + return (
{scaleLevels.map((level, index) => (
setSelectedHealthyOptionId(level.id)} + onClick={() => { + setSelectedHealthyOptionId(level.id); + setIsIconClicked(false); + requestAnimationFrame(() => setIsIconClicked(true)); + }} key={level.id} className={cn(styles.item, { [styles.active]: id === level.id, })} + data-tooltip-id={level.id.toString()} > = ({ [styles.active]: id === level.id, })} > - selected state + Your Diet - {index === 0 && ( - Borderline “OK” Foods - )} + + + {getSelectedHealthOptionName(index + 1)} + +
))}
diff --git a/src/components/longevity/DietResults/DietResults.types.tsx b/src/components/longevity/DietResults/DietResults.types.tsx index 20d7d4f..6997db6 100644 --- a/src/components/longevity/DietResults/DietResults.types.tsx +++ b/src/components/longevity/DietResults/DietResults.types.tsx @@ -1,5 +1,14 @@ export type DietResultsProps = { id?: number; + setIsIconClicked?: (isClicked: boolean) => void; + whatToEatItemNamesAndIds: { + name: string; + id: number; + }[]; + selectedHealthOption: { + name: string; + id: number; + }; setSelectedHealthyOptionId?: (id: number) => void; scaleLevels?: { id: number; diff --git a/src/components/longevity/FlipCard/FlipCard.module.scss b/src/components/longevity/FlipCard/FlipCard.module.scss index c3057b1..48a63a1 100644 --- a/src/components/longevity/FlipCard/FlipCard.module.scss +++ b/src/components/longevity/FlipCard/FlipCard.module.scss @@ -3,22 +3,19 @@ background-size: cover; background-repeat: no-repeat; max-width: 948px; - height: 531px; + height: 550px; text-align: center; display: flex; - justify-content: center; align-items: center; flex-direction: column; margin-bottom: 30px; - padding: 10px; .pageSwitcherFlip { position: absolute; - bottom: 120px; - right: 5px; - left: 93%; + bottom: 20px; z-index: 55; cursor: pointer; + right: 12px; } .headline { @@ -61,7 +58,7 @@ } .diamond { - width: 13px; + width: 10px; height: 10px; background-color: #df382e; transform: rotate(45deg); @@ -76,14 +73,12 @@ .painText { margin: 0 30px; + width: 604px; p { margin: 0; } } - - .divider { - } } .hacksFlipCard { @@ -128,8 +123,12 @@ height: auto; padding: 0; + .pageSwitcherFlip { + display: none; + } + .diamond { - width: 19px; + width: 10px; } .headline { @@ -144,9 +143,22 @@ font-size: 16px; } + .painText, + .hacksFlipCard { + p { + width: 267px; + } + } + + .painText { + margin: 0 10px; + width: unset; + } + .chartWrapper { .chartTitle { font-size: 14px; + padding: 16px 0 32px 0; } .chart { diff --git a/src/components/longevity/FlipCard/FlipCard.tsx b/src/components/longevity/FlipCard/FlipCard.tsx index 7e8cce2..be30698 100644 --- a/src/components/longevity/FlipCard/FlipCard.tsx +++ b/src/components/longevity/FlipCard/FlipCard.tsx @@ -18,6 +18,7 @@ const FlipCard: FC = ({ isHacks, setSwitchPage, switchPage, + chartWidth, }) => { return (
= ({ {chartTitle void; switchPage?: boolean; + chartWidth?: number; }; diff --git a/src/components/longevity/Loader/Loader.module.scss b/src/components/longevity/Loader/Loader.module.scss new file mode 100644 index 0000000..cd9b956 --- /dev/null +++ b/src/components/longevity/Loader/Loader.module.scss @@ -0,0 +1,111 @@ +$dot-count: 26; +$dot-size: 6px; +$dot-space: 10px; +$dot-start: (($dot-count / 2 + 1) * ($dot-size + $dot-space)) / 2; + +$animation-time: 2s; +$animation-distance: 18px; +.wrapper { + display: flex; + width: 100vw; + height: 100vh; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.35); + align-items: center; + justify-content: center; + z-index: 9999; +} + +.loader { + position: relative; + z-index: 9999; + + .dot { + animation-name: movement; + animation-duration: $animation-time; + animation-iteration-count: infinite; + animation-timing-function: ease-in-out; + height: $dot-size; + position: absolute; + top: -#{$dot-size}; + transform: translate3d(0, -#{$animation-distance}, 0) scale(1); + width: $dot-size; + + @for $i from 1 through $dot-count { + $dot-move: ceil($i / 2); + $dot-pos: $dot-start - (($dot-size + $dot-space) * $dot-move); + + $animation-delay: -#{$i * 0.1}s; + @if $i % 2 == 0 { + $animation-delay: -#{($i * 0.1) + ($animation-time / 2)}; + } + + &:nth-of-type(#{$i}) { + animation-delay: $animation-delay; + left: $dot-pos; + + &::before { + animation-delay: $animation-delay; + } + } + } + + &::before { + animation-name: size-opacity; + animation-duration: $animation-time; + animation-iteration-count: infinite; + animation-timing-function: ease; + background: #e1d0b1; + border-radius: 50%; + content: ''; + display: block; + height: 100%; + width: 100%; + } + + &:nth-of-type(even)::before { + background-color: #604f3d; + box-shadow: inset 0 0 4px darken(#111111, 10%); + } + } +} + +@keyframes movement { + 0% { + transform: translate3d(0, -#{$animation-distance}, 0); + z-index: 0; + } + 50% { + transform: translate3d(0, #{$animation-distance}, 0); + z-index: 10; + } + 100% { + transform: translate3d(0, -#{$animation-distance}, 0); + z-index: -5; + } +} + +@keyframes size-opacity { + 0% { + opacity: 1; + transform: scale(1); + } + 25% { + transform: scale(1.5); + } + 50% { + opacity: 1; + } + 75% { + opacity: 0.35; + transform: scale(0.5); + } + 100% { + opacity: 1; + transform: scale(1); + } +} diff --git a/src/components/longevity/Loader/Loader.tsx b/src/components/longevity/Loader/Loader.tsx new file mode 100644 index 0000000..a6106ef --- /dev/null +++ b/src/components/longevity/Loader/Loader.tsx @@ -0,0 +1,17 @@ +import { FC } from 'react'; + +import styles from './Loader.module.scss'; + +const Loader: FC = () => { + return ( +
+
+ {Array.from({ length: 26 }).map((_, i) => ( +
+ ))} +
+
+ ); +}; + +export default Loader; diff --git a/src/components/longevity/Loader/index.ts b/src/components/longevity/Loader/index.ts new file mode 100644 index 0000000..45ded85 --- /dev/null +++ b/src/components/longevity/Loader/index.ts @@ -0,0 +1,3 @@ +import Loader from './Loader'; + +export default Loader; diff --git a/src/components/longevity/MainInfoSection/MainInfoSection.module.scss b/src/components/longevity/MainInfoSection/MainInfoSection.module.scss index 1ae90c7..494da01 100644 --- a/src/components/longevity/MainInfoSection/MainInfoSection.module.scss +++ b/src/components/longevity/MainInfoSection/MainInfoSection.module.scss @@ -11,6 +11,10 @@ margin: 40px 0 16px 0; display: flex; + .wrapper { + position: relative; + } + .heading { padding-bottom: 24px; } @@ -35,6 +39,7 @@ letter-spacing: -5px; color: #ce2128; margin-left: 60px; + z-index: 555; } } diff --git a/src/components/longevity/MainInfoSection/MainInfoSection.tsx b/src/components/longevity/MainInfoSection/MainInfoSection.tsx index 2ed5d98..4fcbb0f 100644 --- a/src/components/longevity/MainInfoSection/MainInfoSection.tsx +++ b/src/components/longevity/MainInfoSection/MainInfoSection.tsx @@ -1,8 +1,10 @@ -import { FC } from 'react'; +import { FC, useContext } from 'react'; import cn from 'classnames'; +import Image from 'next/image'; import Heading from '@components/Heading'; import BasicStats from '@components/longevity/BasicStats'; +import { GlobalContext } from '@components/Context/GlobalContext'; import { MainInfoSectionProps } from './MainInfoSection.types'; @@ -20,18 +22,27 @@ const MainInfoSection: FC = ({ basicStatsTitle, isIntroPage, }) => { + const { setHeroReady } = useContext(GlobalContext); + return (
-
+ Background setHeroReady(true)} + /> +
{ const router = useRouter(); const [openNav, setOpenNav] = useState(false); const [openSubNav, setOpenSubNav] = useState(false); + const isInternal = (path: string) => path.startsWith('/'); + const toggleNavbar = () => { setOpenNav(false); }; @@ -93,18 +98,36 @@ const MobileNavigation: FC = () => { }; // TODO - Get back to this - // const getNextNavItemName = (nav1, nav2) => { - // const allNavItems = [...nav1, ...nav2]; - // const currentIndex = allNavItems.findIndex(i => i.path === router.pathname); - // const nextItem = allNavItems[currentIndex + 1]; - // - // if (currentIndex === -1) return ''; - // if (currentIndex === allNavItems.length - 1) { - // return allNavItems[0].name; - // } - // return nextItem ? nextItem.name : ''; - // }; - // const nextPathname = getNextNavItemName(navItems, subNavItems); + + const buildNavOrder = (nav1: any[], nav2: any[]) => { + const out: any[] = []; + + for (const item of nav1) { + if (item.hasNoUrl) { + out.push(...nav2); + continue; + } + + if (!isInternal(item.path)) continue; + out.push(item); + } + return out; + }; + + const getNextNavItem = (nav1: any[], nav2: any[] = []) => { + const pathname = router.pathname; + const ordered = buildNavOrder(nav1, nav2); + + const currentIndex = ordered.findIndex(i => i.path === pathname); + if (currentIndex === -1) return null; + + const nextIndex = (currentIndex + 1) % ordered.length; + const next = ordered[nextIndex]; + + return next ? { name: next.name, path: next.path } : null; + }; + + const nextPathname = getNextNavItem(navItems, subNavItems); return ( <> @@ -205,7 +228,12 @@ const MobileNavigation: FC = () => { ))} - {/**/} + + + Next:{' '} + {nextPathname.name} + + ); }; diff --git a/src/components/longevity/ProgressBar/ProgressBar.module.scss b/src/components/longevity/ProgressBar/ProgressBar.module.scss index 18dd1ce..8462a87 100644 --- a/src/components/longevity/ProgressBar/ProgressBar.module.scss +++ b/src/components/longevity/ProgressBar/ProgressBar.module.scss @@ -111,3 +111,11 @@ margin-top: 10px; font-weight: 700; } + +@media (max-width: 956px) { + .wrapper { + .container { + touch-action: none; + } + } +} diff --git a/src/components/longevity/ProgressBar/ProgressBar.tsx b/src/components/longevity/ProgressBar/ProgressBar.tsx index b127f1c..b3ea86a 100644 --- a/src/components/longevity/ProgressBar/ProgressBar.tsx +++ b/src/components/longevity/ProgressBar/ProgressBar.tsx @@ -14,7 +14,6 @@ const ProgressBar: FC = ({ activityLevels, }) => { const trackRef = useRef(null); - const [isDragging, setIsDragging] = useState(false); const selectedMinutes = useMemo(() => stops[stopIndex], [stops, stopIndex]); @@ -49,27 +48,34 @@ const ProgressBar: FC = ({ [getClosestIndexFromClientX], ); - const onPointerDownThumb = (e: React.PointerEvent) => { + const onTrackClick = (e: React.MouseEvent) => { + jumpTo(e.clientX); + }; + + const onPointerDown = (e: React.PointerEvent) => { e.preventDefault(); - (e.currentTarget as HTMLButtonElement).setPointerCapture(e.pointerId); + setIsDragging(true); + + e.currentTarget.setPointerCapture(e.pointerId); + + jumpTo(e.clientX); }; - const onPointerMove = (e: React.PointerEvent) => { + const onPointerMove = (e: React.PointerEvent) => { if (!isDragging) return; + e.preventDefault(); jumpTo(e.clientX); }; - const onPointerUp = (e: React.PointerEvent) => { + const onPointerUp = (e: React.PointerEvent) => { setIsDragging(false); try { - (e.currentTarget as HTMLButtonElement).releasePointerCapture(e.pointerId); + e.currentTarget.releasePointerCapture(e.pointerId); } catch {} }; - const onTrackClick = (e: React.MouseEvent) => { - jumpTo(e.clientX); - }; + const onPointerCancel = () => setIsDragging(false); return (
@@ -86,7 +92,15 @@ const ProgressBar: FC = ({ ))}
-
+
diff --git a/src/components/longevity/StudySection/StudySection.module.scss b/src/components/longevity/StudySection/StudySection.module.scss index deefd77..c5e3d4f 100644 --- a/src/components/longevity/StudySection/StudySection.module.scss +++ b/src/components/longevity/StudySection/StudySection.module.scss @@ -2,6 +2,8 @@ .studySection { max-width: 948px; + margin-bottom: 40px; + position: relative; } .cardContainer { @@ -29,6 +31,7 @@ .showFlipCard { animation: showFlipCardAndScaleUp 0.6s forwards; + z-index: 33; } .hideFlipCard { @@ -70,7 +73,7 @@ .mainContent { padding: 28px 40px; background-image: url('/keepsimple_/assets/longevity/study/study-bg.png'); - height: 483px; + height: 427px; background-size: cover; .description { @@ -114,11 +117,9 @@ } .pageSwitcher { - position: relative; - bottom: 70px; - right: 5px; - left: 93%; - z-index: 5; + position: absolute; + bottom: 20px; + right: 12px; cursor: pointer; } @@ -175,8 +176,11 @@ background-position: right !important; } } + .hacksModalBody { display: flex !important; + flex-direction: column; + justify-content: space-around; } } diff --git a/src/components/longevity/StudySection/StudySection.tsx b/src/components/longevity/StudySection/StudySection.tsx index 448df90..44695a1 100644 --- a/src/components/longevity/StudySection/StudySection.tsx +++ b/src/components/longevity/StudySection/StudySection.tsx @@ -9,6 +9,10 @@ import { StudySectionProps } from './StudySection.types'; import FlipCard from '@components/longevity/FlipCard'; import { useIsWidthLessThan } from '@hooks/useScreenSize'; import Modal from '@components/Modal'; +import { BorderedPill } from '@components/longevity/BorderedPill/BorderedPill'; + +import LearnMoreIcon from '@icons/longevity/LearnMoreIcon'; +import { StudyCloseIcon } from '@icons/longevity/Study/CloseIcon'; import styles from './StudySection.module.scss'; @@ -20,18 +24,23 @@ const StudySection: FC = ({ flippedCardChartTitle, flippedCardSubText, flippedCardChart, + flippedCardChartMobile, flippedCardPainText, hacksQuote, backsBackgroundImageUrl, quoteAuthor, + chartWidth, }) => { - const [switchPage, setSwitchPage] = useState(false); + const [switchPage, setSwitchPage] = useState(null); const isMobile = useIsWidthLessThan(965); const [openModal, setOpenModal] = useState(false); + const chartImage = flippedCardChartMobile + ? flippedCardChartMobile + : flippedCardChart; const headlineBg = isHacks ? '/keepsimple_/assets/longevity/study/hacks.png' : '/keepsimple_/assets/longevity/study-headline-bg.png'; - + //explain to learn doesn't have it return ( <>
@@ -39,7 +48,7 @@ const StudySection: FC = ({
@@ -67,38 +76,34 @@ const StudySection: FC = ({ dangerouslySetInnerHTML={{ __html: description || '' }} className={styles.description} /> - {isMobile && ( + {isMobile && flippedCardChart && (
- + leftIcon={} + />
)} + {flippedCardChart && ( + {'Page { + setSwitchPage(!switchPage); + }} + /> + )}
- {'Page setSwitchPage(!switchPage)} - />
{!isMobile && (
= ({ quoteAuthor={quoteAuthor} switchPage={switchPage} setSwitchPage={setSwitchPage} + chartWidth={chartWidth} />
)} @@ -132,12 +138,19 @@ const StudySection: FC = ({ + } + onClick={() => setOpenModal(false)} + isWhite={isHacks} /> )} diff --git a/src/components/longevity/StudySection/StudySection.types.ts b/src/components/longevity/StudySection/StudySection.types.ts index 1bdd62b..8468260 100644 --- a/src/components/longevity/StudySection/StudySection.types.ts +++ b/src/components/longevity/StudySection/StudySection.types.ts @@ -7,7 +7,9 @@ export type StudySectionProps = { flippedCardHeadline?: string; flippedCardPainText?: string; flippedCardChart?: string; + flippedCardChartMobile?: string; hacksQuote?: string; quoteAuthor?: string; backsBackgroundImageUrl?: string; + chartWidth?: number; }; diff --git a/src/components/longevity/Table/Table.module.scss b/src/components/longevity/Table/Table.module.scss index 1243496..1d90204 100644 --- a/src/components/longevity/Table/Table.module.scss +++ b/src/components/longevity/Table/Table.module.scss @@ -139,3 +139,24 @@ } } } + +@media (max-width: 1140px) { + .table { + width: 739px; + + .header { + .headerRow { + .headline { + padding: unset; + } + } + } + + .tr { + &::after { + right: 0; + left: unset; + } + } + } +} diff --git a/src/components/longevity/WhatToEatOrAvoid/WhatToEatOrAvoid.module.scss b/src/components/longevity/WhatToEatOrAvoid/WhatToEatOrAvoid.module.scss index 12a95f2..8c68f68 100644 --- a/src/components/longevity/WhatToEatOrAvoid/WhatToEatOrAvoid.module.scss +++ b/src/components/longevity/WhatToEatOrAvoid/WhatToEatOrAvoid.module.scss @@ -156,6 +156,14 @@ } } +.selected { + user-select: text; +} + +.whatToEatSection { + cursor: pointer; +} + @media (max-width: 965px) { .whatToEatOrAvoid { padding: 12px; diff --git a/src/components/longevity/WhatToEatOrAvoid/WhatToEatOrAvoid.tsx b/src/components/longevity/WhatToEatOrAvoid/WhatToEatOrAvoid.tsx index e9b4439..f957b99 100644 --- a/src/components/longevity/WhatToEatOrAvoid/WhatToEatOrAvoid.tsx +++ b/src/components/longevity/WhatToEatOrAvoid/WhatToEatOrAvoid.tsx @@ -42,7 +42,21 @@ const WhatToEatOrAvoid: FC = ({ }, []); return ( -
+
{ + const selectedText = window.getSelection?.()?.toString() ?? ''; + if (selectedText.trim().length > 0) return; + + if (setSelectedHealthyOptionId && id) { + setSelectedHealthyOptionId(id); + e.stopPropagation(); + } + }} + >
= ({ Your diet )}
{ - 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 ? (
- -
-
{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 ( {showLoader && !isSmallScreen && ( @@ -247,6 +294,7 @@ function App({ Component, pageProps: { session, ...pageProps } }: TApp) { + {overlayOn && } {isCookieStateLoaded && !cookieBoxIsSeen && ( )} diff --git a/src/styles/globals.scss b/src/styles/globals.scss index d0f754c..cff7960 100644 --- a/src/styles/globals.scss +++ b/src/styles/globals.scss @@ -27,6 +27,9 @@ html { border-left: 1px solid #fafafa; } } +body.noScroll { + overflow: hidden; +} html::-webkit-scrollbar-thumb { background: rgba(40, 88, 123, 0.5);