From 8d456c5d0a9c387f2cc8db80125954f735be3f46 Mon Sep 17 00:00:00 2001 From: gofnnp Date: Tue, 24 Jun 2025 16:19:03 +0400 Subject: [PATCH] main integrate meditations --- messages/en.json | 10 ++ .../(core)/breath/[id]/page.module.scss | 10 ++ src/app/[locale]/(core)/breath/[id]/page.tsx | 23 +++ .../[id]/result/[resultId]/page.module.scss | 10 ++ .../breath/[id]/result/[resultId]/page.tsx | 21 +++ src/app/[locale]/(core)/breath/layout.tsx | 9 ++ .../(core)/compatibility/[id]/page.tsx | 5 +- .../[id]/result/[resultId]/page.tsx | 2 +- .../(core)/palmistry/result/[id]/page.tsx | 2 +- .../domains/breath/BreathPage/BreathPage.tsx | 30 ++++ .../BreathResultPage.module.scss | 105 ++++++++++++++ .../BreathResultPage/BreathResultPage.tsx | 136 ++++++++++++++++++ .../StartBreathModalChild.module.scss | 53 +++++++ .../StartBreathModalChild.tsx | 62 ++++++++ src/components/domains/breath/index.ts | 3 + src/components/domains/compatibility/index.ts | 5 + .../MeditationSection/MeditationSection.tsx | 6 +- src/components/domains/palmistry/index.ts | 1 + src/components/ui/Card/Card.module.scss | 1 + .../FullScreenBlurModal.module.scss | 68 +++++++++ .../FullScreenBlurModal.tsx | 41 ++++++ src/components/ui/index.ts | 1 + .../fullscreen-blur-modal-provider.tsx | 58 ++++++++ src/shared/constants/client-routes.ts | 5 + 24 files changed, 662 insertions(+), 5 deletions(-) create mode 100644 src/app/[locale]/(core)/breath/[id]/page.module.scss create mode 100644 src/app/[locale]/(core)/breath/[id]/page.tsx create mode 100644 src/app/[locale]/(core)/breath/[id]/result/[resultId]/page.module.scss create mode 100644 src/app/[locale]/(core)/breath/[id]/result/[resultId]/page.tsx create mode 100644 src/app/[locale]/(core)/breath/layout.tsx create mode 100644 src/components/domains/breath/BreathPage/BreathPage.tsx create mode 100644 src/components/domains/breath/BreathResultPage/BreathResultPage.module.scss create mode 100644 src/components/domains/breath/BreathResultPage/BreathResultPage.tsx create mode 100644 src/components/domains/breath/StartBreathModalChild/StartBreathModalChild.module.scss create mode 100644 src/components/domains/breath/StartBreathModalChild/StartBreathModalChild.tsx create mode 100644 src/components/domains/breath/index.ts create mode 100644 src/components/domains/compatibility/index.ts create mode 100644 src/components/domains/palmistry/index.ts create mode 100644 src/components/ui/FullScreenBlurModal/FullScreenBlurModal.module.scss create mode 100644 src/components/ui/FullScreenBlurModal/FullScreenBlurModal.tsx create mode 100644 src/providers/fullscreen-blur-modal-provider.tsx diff --git a/messages/en.json b/messages/en.json index 20403d9..55bb284 100644 --- a/messages/en.json +++ b/messages/en.json @@ -201,5 +201,15 @@ "PalmistryResult": { "title": "Your Personality Type", "error": "Something went wrong. Please try again later." + }, + "Breath": { + "title": "Stop and breathe to help you relax and focus on what really matters.", + "subtitle": "Breathing practice will help improve your aura. Breath in the positive energy, breathe out the negative...", + "button": "BEGIN" + }, + "BreathResult": { + "breath_relax": "Breath & Relax", + "breath_in": "Breath in", + "breath_out": "Breath out" } } diff --git a/src/app/[locale]/(core)/breath/[id]/page.module.scss b/src/app/[locale]/(core)/breath/[id]/page.module.scss new file mode 100644 index 0000000..aab7c1d --- /dev/null +++ b/src/app/[locale]/(core)/breath/[id]/page.module.scss @@ -0,0 +1,10 @@ +.container { + width: 100dvw; + height: calc(100dvh - 56px); + background-color: #000; + position: absolute; + overflow: hidden; + left: 0; + top: 56px; + padding: 16px 16px 64px; +} diff --git a/src/app/[locale]/(core)/breath/[id]/page.tsx b/src/app/[locale]/(core)/breath/[id]/page.tsx new file mode 100644 index 0000000..657b03b --- /dev/null +++ b/src/app/[locale]/(core)/breath/[id]/page.tsx @@ -0,0 +1,23 @@ +import { BreathPage } from "@/components/domains/breath"; +import { startGeneration } from "@/entities/generations/api"; + +import styles from "./page.module.scss"; + +export default async function Breath({ + params, +}: { + params: Promise<{ id: string }>; +}) { + // const { id } = await params; + await params; + const id = "684a07f6ca4395c285362d4f"; + const result = await startGeneration({ + actionType: "palm", + actionId: id, + }); + return ( +
+ +
+ ); +} diff --git a/src/app/[locale]/(core)/breath/[id]/result/[resultId]/page.module.scss b/src/app/[locale]/(core)/breath/[id]/result/[resultId]/page.module.scss new file mode 100644 index 0000000..aab7c1d --- /dev/null +++ b/src/app/[locale]/(core)/breath/[id]/result/[resultId]/page.module.scss @@ -0,0 +1,10 @@ +.container { + width: 100dvw; + height: calc(100dvh - 56px); + background-color: #000; + position: absolute; + overflow: hidden; + left: 0; + top: 56px; + padding: 16px 16px 64px; +} diff --git a/src/app/[locale]/(core)/breath/[id]/result/[resultId]/page.tsx b/src/app/[locale]/(core)/breath/[id]/result/[resultId]/page.tsx new file mode 100644 index 0000000..71185ef --- /dev/null +++ b/src/app/[locale]/(core)/breath/[id]/result/[resultId]/page.tsx @@ -0,0 +1,21 @@ +import { BreathResultPage } from "@/components/domains/breath"; +import { loadPalms } from "@/entities/dashboard/loaders"; + +import styles from "./page.module.scss"; + +export default async function BreathResult({ + params, +}: { + params: Promise<{ id: string; resultId: string }>; +}) { + const { id, resultId } = await params; + // const actions = await loadMeditations(); + const actions = await loadPalms(); + const action = actions?.find(action => action._id === id); + + return ( +
+ +
+ ); +} diff --git a/src/app/[locale]/(core)/breath/layout.tsx b/src/app/[locale]/(core)/breath/layout.tsx new file mode 100644 index 0000000..d2cd127 --- /dev/null +++ b/src/app/[locale]/(core)/breath/layout.tsx @@ -0,0 +1,9 @@ +import { FullScreenModalProvider } from "@/providers/fullscreen-blur-modal-provider"; + +export default function BreathLayout({ + children, +}: { + children: React.ReactNode; +}) { + return {children}; +} diff --git a/src/app/[locale]/(core)/compatibility/[id]/page.tsx b/src/app/[locale]/(core)/compatibility/[id]/page.tsx index ca9727a..3a992a6 100644 --- a/src/app/[locale]/(core)/compatibility/[id]/page.tsx +++ b/src/app/[locale]/(core)/compatibility/[id]/page.tsx @@ -1,9 +1,10 @@ import { Suspense, use } from "react"; import { useTranslations } from "next-intl"; -import CompatibilityActionFieldsForm, { +import { + CompatibilityActionFieldsForm, CompatibilityActionFieldsFormSkeleton, -} from "@/components/domains/compatibility/CompatibilityActionFieldsForm/CompatibilityActionFieldsForm"; +} from "@/components/domains/compatibility"; import { Typography } from "@/components/ui"; import { loadCompatibilityActionFields } from "@/entities/compatibilityActionFields/loaders"; import { loadCompatibility } from "@/entities/dashboard/loaders"; diff --git a/src/app/[locale]/(core)/compatibility/[id]/result/[resultId]/page.tsx b/src/app/[locale]/(core)/compatibility/[id]/result/[resultId]/page.tsx index 3391227..9c64690 100644 --- a/src/app/[locale]/(core)/compatibility/[id]/result/[resultId]/page.tsx +++ b/src/app/[locale]/(core)/compatibility/[id]/result/[resultId]/page.tsx @@ -1,6 +1,6 @@ import { use } from "react"; -import CompatibilityResultPage from "@/components/domains/compatibility/CompatibilityResultPage/CompatibilityResultPage"; +import { CompatibilityResultPage } from "@/components/domains/compatibility"; import { Typography } from "@/components/ui"; import { loadCompatibility } from "@/entities/dashboard/loaders"; diff --git a/src/app/[locale]/(core)/palmistry/result/[id]/page.tsx b/src/app/[locale]/(core)/palmistry/result/[id]/page.tsx index 21455fc..6fb2dbe 100644 --- a/src/app/[locale]/(core)/palmistry/result/[id]/page.tsx +++ b/src/app/[locale]/(core)/palmistry/result/[id]/page.tsx @@ -1,4 +1,4 @@ -import PalmistryResultPage from "@/components/domains/palmistry/PalmistryResultPage/PalmistryResultPage"; +import { PalmistryResultPage } from "@/components/domains/palmistry"; import { Typography } from "@/components/ui"; import { loadPalms } from "@/entities/dashboard/loaders"; import { startGeneration } from "@/entities/generations/api"; diff --git a/src/components/domains/breath/BreathPage/BreathPage.tsx b/src/components/domains/breath/BreathPage/BreathPage.tsx new file mode 100644 index 0000000..b772d86 --- /dev/null +++ b/src/components/domains/breath/BreathPage/BreathPage.tsx @@ -0,0 +1,30 @@ +"use client"; + +import { useCallback, useEffect } from "react"; +import { useRouter } from "next/navigation"; + +import { useFullScreenModal } from "@/providers/fullscreen-blur-modal-provider"; +import { ROUTES } from "@/shared/constants/client-routes"; + +import { StartBreathModalChild } from ".."; + +interface BreathPageProps { + id: string; + resultId: string; +} + +export default function BreathPage({ id, resultId }: BreathPageProps) { + const router = useRouter(); + const { openModal, closeModal } = useFullScreenModal(); + + const handleBegin = useCallback(() => { + router.push(ROUTES.breathResult(id, resultId)); + closeModal(); + }, [closeModal, id, resultId, router]); + + useEffect(() => { + openModal(); + }, [handleBegin, openModal]); + + return <>; +} diff --git a/src/components/domains/breath/BreathResultPage/BreathResultPage.module.scss b/src/components/domains/breath/BreathResultPage/BreathResultPage.module.scss new file mode 100644 index 0000000..7989b7a --- /dev/null +++ b/src/components/domains/breath/BreathResultPage/BreathResultPage.module.scss @@ -0,0 +1,105 @@ +.title { + line-height: 30px; +} + +.result { + line-height: 25px; + margin-top: 24px; +} + +.loadingContainer { + display: flex; + justify-content: center; + align-items: center; + height: 100%; + width: 100%; +} + +.breathRelax { + animation-name: breath-relax; + animation-duration: 10s; + animation-iteration-count: 1; + animation-timing-function: linear; + animation-fill-mode: forwards; +} + +.textPosition { + position: absolute; + bottom: calc(50dvh - 56px); + left: 50%; + transform: translateX(-50%); +} + +.breathIn { + animation-name: breath-in; + animation-duration: 10s; + animation-iteration-count: infinite; + animation-timing-function: linear; +} + +.breathOut { + animation-name: breath-out; + animation-duration: 10s; + animation-iteration-count: infinite; + animation-timing-function: linear; +} + +@keyframes breath-relax { + 0% { + opacity: 0; + } + 5% { + opacity: 0; + } + 10% { + opacity: 1; + } + 95% { + opacity: 1; + } + 100% { + scale: 1; + opacity: 0; + } +} + +@keyframes breath-in { + 0% { + opacity: 0; + scale: 1; + } + 10% { + opacity: 1; + } + 40% { + opacity: 1; + } + 50% { + opacity: 0; + scale: 2; + } + 100% { + opacity: 0; + } +} + +@keyframes breath-out { + 0% { + opacity: 0; + scale: 1; + } + 50% { + opacity: 0; + scale: 2; + } + 60% { + opacity: 1; + } + 90% { + opacity: 1; + } + 100% { + opacity: 0; + scale: 1; + } +} diff --git a/src/components/domains/breath/BreathResultPage/BreathResultPage.tsx b/src/components/domains/breath/BreathResultPage/BreathResultPage.tsx new file mode 100644 index 0000000..793a173 --- /dev/null +++ b/src/components/domains/breath/BreathResultPage/BreathResultPage.tsx @@ -0,0 +1,136 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useTranslations } from "next-intl"; +import clsx from "clsx"; + +import { Spinner, Typography } from "@/components/ui"; +import { Action } from "@/entities/dashboard/types"; +import { useGenerationPolling } from "@/hooks/generation/useGenerationPolling"; +import { useToast } from "@/providers/toast-provider"; + +import styles from "./BreathResultPage.module.scss"; + +interface BreathResultPageProps { + id: string; + action: Action | undefined; +} + +export default function BreathResultPage({ + id, + action, +}: BreathResultPageProps) { + const t = useTranslations("BreathResult"); + const { data, error, isLoading } = useGenerationPolling(id); + const { addToast } = useToast(); + + const [animationState, setAnimationState] = useState< + "preview" | "breath" | "result" + >("preview"); + + useEffect(() => { + if (animationState === "preview") { + const previewTimeOut = setTimeout(() => { + setAnimationState("breath"); + }, 10_000); + + return () => { + clearTimeout(previewTimeOut); + }; + } + + if (animationState === "breath") { + const breathTimeOut = setTimeout(() => { + setAnimationState("result"); + }, 40_000); + + return () => { + clearTimeout(breathTimeOut); + }; + } + }, [animationState]); + + useEffect(() => { + if (error) { + addToast({ + variant: "error", + message: t("error"), + duration: 5000, + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [error]); + + if (isLoading && animationState === "result") { + return ( +
+ +
+ ); + } + + if (animationState === "result") { + return ( + <> + + {action?.title} + + + {data?.result} + + + ); + } + + return ( + <> + {animationState === "preview" && ( + + {t("breath_relax")} + + )} + + {animationState === "breath" && ( + <> +
+ + {t("breath_in")} + +
+
+ + {t("breath_out")} + +
+ + )} + + ); +} diff --git a/src/components/domains/breath/StartBreathModalChild/StartBreathModalChild.module.scss b/src/components/domains/breath/StartBreathModalChild/StartBreathModalChild.module.scss new file mode 100644 index 0000000..cef5227 --- /dev/null +++ b/src/components/domains/breath/StartBreathModalChild/StartBreathModalChild.module.scss @@ -0,0 +1,53 @@ +.container { + width: 100%; + height: 100%; + background-color: #0000009e; + + display: flex; + position: relative; + flex-direction: column; + align-items: center; + justify-content: flex-start; + flex: 1 1; + overflow: hidden; + padding: 52px 32px; +} + +.title { + font-size: 18px; + line-height: 1.5; +} + +.subtitle { + font-size: 18px; + line-height: 1.5; + color: #c2c2c3; + margin-top: 24px; +} + +.button { + padding: 16px 32px; + position: fixed; + bottom: 172px; + left: 50%; + transform: translateX(-50%); + width: fit-content; +} + +.symbol { + opacity: 0; + will-change: opacity; + animation-name: appearance; + animation-timing-function: ease; + animation-duration: 0.3s; + animation-fill-mode: forwards; +} + +@keyframes appearance { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} diff --git a/src/components/domains/breath/StartBreathModalChild/StartBreathModalChild.tsx b/src/components/domains/breath/StartBreathModalChild/StartBreathModalChild.tsx new file mode 100644 index 0000000..00069c0 --- /dev/null +++ b/src/components/domains/breath/StartBreathModalChild/StartBreathModalChild.tsx @@ -0,0 +1,62 @@ +import { useTranslations } from "next-intl"; + +import { Button, Typography } from "@/components/ui"; + +import styles from "./StartBreathModalChild.module.scss"; + +interface IStartBreathModalChildProps { + handleBegin: () => void; +} + +function StartBreathModalChild({ handleBegin }: IStartBreathModalChildProps) { + const t = useTranslations("Breath"); + + return ( +
+
+ + {t("title") + .split("") + .map((symbol, index) => ( + + {symbol} + + ))} + + + {t("subtitle") + .split("") + .map((symbol, index) => ( + + {symbol} + + ))} + +
+ +
+ ); +} + +export default StartBreathModalChild; diff --git a/src/components/domains/breath/index.ts b/src/components/domains/breath/index.ts new file mode 100644 index 0000000..78cd845 --- /dev/null +++ b/src/components/domains/breath/index.ts @@ -0,0 +1,3 @@ +export { default as BreathPage } from "./BreathPage/BreathPage"; +export { default as BreathResultPage } from "./BreathResultPage/BreathResultPage"; +export { default as StartBreathModalChild } from "./StartBreathModalChild/StartBreathModalChild"; diff --git a/src/components/domains/compatibility/index.ts b/src/components/domains/compatibility/index.ts new file mode 100644 index 0000000..a0aeac6 --- /dev/null +++ b/src/components/domains/compatibility/index.ts @@ -0,0 +1,5 @@ +export { + default as CompatibilityActionFieldsForm, + CompatibilityActionFieldsFormSkeleton, +} from "./CompatibilityActionFieldsForm/CompatibilityActionFieldsForm"; +export { default as CompatibilityResultPage } from "./CompatibilityResultPage/CompatibilityResultPage"; diff --git a/src/components/domains/dashboard/sections/MeditationSection/MeditationSection.tsx b/src/components/domains/dashboard/sections/MeditationSection/MeditationSection.tsx index bc5af5e..d953183 100644 --- a/src/components/domains/dashboard/sections/MeditationSection/MeditationSection.tsx +++ b/src/components/domains/dashboard/sections/MeditationSection/MeditationSection.tsx @@ -1,7 +1,9 @@ import { use } from "react"; +import Link from "next/link"; import { Grid, Section, Skeleton } from "@/components/ui"; import { Action } from "@/entities/dashboard/types"; +import { ROUTES } from "@/shared/constants/client-routes"; import { MeditationCard } from "../../cards"; @@ -19,7 +21,9 @@ export default function MeditationSection({
{meditations.map(meditation => ( - + + + ))}
diff --git a/src/components/domains/palmistry/index.ts b/src/components/domains/palmistry/index.ts new file mode 100644 index 0000000..439f787 --- /dev/null +++ b/src/components/domains/palmistry/index.ts @@ -0,0 +1 @@ +export { default as PalmistryResultPage } from "./PalmistryResultPage/PalmistryResultPage"; diff --git a/src/components/ui/Card/Card.module.scss b/src/components/ui/Card/Card.module.scss index 8b9b4fc..2d6a203 100644 --- a/src/components/ui/Card/Card.module.scss +++ b/src/components/ui/Card/Card.module.scss @@ -6,4 +6,5 @@ 0px 10px 15px 0px rgba(0, 0, 0, 0.1), 0px 4px 6px 0px rgba(0, 0, 0, 0.1); padding: 16px; + cursor: pointer; } diff --git a/src/components/ui/FullScreenBlurModal/FullScreenBlurModal.module.scss b/src/components/ui/FullScreenBlurModal/FullScreenBlurModal.module.scss new file mode 100644 index 0000000..0840ffc --- /dev/null +++ b/src/components/ui/FullScreenBlurModal/FullScreenBlurModal.module.scss @@ -0,0 +1,68 @@ +.modal { + width: 100%; + height: 100dvh; + position: fixed; + top: 0; + left: 0; + z-index: 8888; + /* background-color: #000; */ + opacity: 0; + -webkit-transition: opacity 3s ease; + -moz-transition: opacity 3s ease; + -ms-transition: opacity 3s ease; + -o-transition: opacity 3s ease; + transition: opacity 3s ease; + will-change: opacity; + animation: disappearance 3s ease; + animation-fill-mode: forwards; + pointer-events: none; +} + +.open { + opacity: 1; + animation: appearance 3s ease; + animation-fill-mode: forwards; + pointer-events: auto; +} + +.content { + width: 100%; + height: 100%; + animation: appearance-content 3s ease; + animation-fill-mode: forwards; +} + +@keyframes appearance { + 0% { + -webkit-backdrop-filter: blur(0); + backdrop-filter: blur(0); + pointer-events: none; + } + 100% { + -webkit-backdrop-filter: blur(50px); + backdrop-filter: blur(50px); + pointer-events: auto; + } +} + +@keyframes appearance-content { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +@keyframes disappearance { + 0% { + -webkit-backdrop-filter: blur(50px); + backdrop-filter: blur(50px); + pointer-events: auto; + } + 100% { + -webkit-backdrop-filter: blur(0); + backdrop-filter: blur(0); + pointer-events: none; + } +} diff --git a/src/components/ui/FullScreenBlurModal/FullScreenBlurModal.tsx b/src/components/ui/FullScreenBlurModal/FullScreenBlurModal.tsx new file mode 100644 index 0000000..b7d7ca5 --- /dev/null +++ b/src/components/ui/FullScreenBlurModal/FullScreenBlurModal.tsx @@ -0,0 +1,41 @@ +"use client"; + +import { useEffect } from "react"; +import clsx from "clsx"; + +import styles from "./FullScreenBlurModal.module.scss"; + +interface FullScreenBlurModalProps { + className?: string; + classNameContent?: string; + style?: React.CSSProperties; + children: React.ReactNode; + isOpen: boolean; +} + +export default function FullScreenBlurModal({ + className = "", + classNameContent = "", + children, + style, + isOpen, +}: FullScreenBlurModalProps) { + useEffect(() => { + if (isOpen) { + document.body.classList.add("no-scroll"); + } + + return () => { + document.body.classList.remove("no-scroll"); + }; + }, [isOpen]); + + return ( +
+
{children}
+
+ ); +} diff --git a/src/components/ui/index.ts b/src/components/ui/index.ts index 0160b78..a25f831 100644 --- a/src/components/ui/index.ts +++ b/src/components/ui/index.ts @@ -2,6 +2,7 @@ export { default as Button } from "./Button/Button"; export { default as Card } from "./Card/Card"; export { default as CircleArrow } from "./CircleArrow/CircleArrow"; export { default as EmailInput } from "./EmailInput/EmailInput"; +export { default as FullScreenBlurModal } from "./FullScreenBlurModal/FullScreenBlurModal"; export { default as GPTAnimationText } from "./GPTAnimationText/GPTAnimationText"; export { default as Grid } from "./Grid/Grid"; export { default as Icon } from "./Icon/Icon"; diff --git a/src/providers/fullscreen-blur-modal-provider.tsx b/src/providers/fullscreen-blur-modal-provider.tsx new file mode 100644 index 0000000..e7510b6 --- /dev/null +++ b/src/providers/fullscreen-blur-modal-provider.tsx @@ -0,0 +1,58 @@ +"use client"; + +import React, { + createContext, + ReactNode, + useCallback, + useContext, + useState, +} from "react"; + +import { FullScreenBlurModal } from "@/components/ui"; + +interface FullScreenModalContextType { + openModal: (content: React.ReactNode) => void; + closeModal: () => void; + isOpen: boolean; +} + +const FullScreenModalContext = createContext< + FullScreenModalContextType | undefined +>(undefined); + +export const useFullScreenModal = () => { + const context = useContext(FullScreenModalContext); + if (!context) { + throw new Error( + "useFullScreenModal must be used within FullScreenModalProvider" + ); + } + return context; +}; + +interface FullScreenModalProviderProps { + children: ReactNode; +} + +export const FullScreenModalProvider = ({ + children, +}: FullScreenModalProviderProps) => { + const [isOpen, setIsOpen] = useState(false); + const [content, setContent] = useState(null); + + const openModal = useCallback((modalContent: React.ReactNode) => { + setContent(modalContent); + setIsOpen(true); + }, []); + + const closeModal = useCallback(() => { + setIsOpen(false); + }, []); + + return ( + + {children} + {content} + + ); +}; diff --git a/src/shared/constants/client-routes.ts b/src/shared/constants/client-routes.ts index 3f105a2..edea7e4 100644 --- a/src/shared/constants/client-routes.ts +++ b/src/shared/constants/client-routes.ts @@ -10,6 +10,11 @@ const createRoute = (segments: string[]): string => { export const ROUTES = { home: () => createRoute([]), + // Breath + breath: (id: string) => createRoute(["breath", id]), + breathResult: (id: string, resultId: string) => + createRoute(["breath", id, "result", resultId]), + // Compatibility compatibility: (id: string) => createRoute(["compatibility", id]), compatibilityResult: (id: string, resultId: string) =>