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 (
+
+ );
+}
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) =>