integrate meditations
This commit is contained in:
gofnnp 2025-06-24 16:19:03 +04:00
parent 12836b372d
commit 8d456c5d0a
24 changed files with 662 additions and 5 deletions

View File

@ -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"
}
}

View File

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

View File

@ -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 (
<section className={styles.container}>
<BreathPage id={id} resultId={result?.id} />
</section>
);
}

View File

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

View File

@ -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 (
<section className={styles.container}>
<BreathResultPage id={resultId} action={action} />
</section>
);
}

View File

@ -0,0 +1,9 @@
import { FullScreenModalProvider } from "@/providers/fullscreen-blur-modal-provider";
export default function BreathLayout({
children,
}: {
children: React.ReactNode;
}) {
return <FullScreenModalProvider>{children}</FullScreenModalProvider>;
}

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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(<StartBreathModalChild handleBegin={handleBegin} />);
}, [handleBegin, openModal]);
return <></>;
}

View File

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

View File

@ -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 (
<div className={styles.loadingContainer}>
<Spinner />
</div>
);
}
if (animationState === "result") {
return (
<>
<Typography
as="h1"
size="xl"
weight="semiBold"
color="white"
className={styles.title}
>
{action?.title}
</Typography>
<Typography
as="p"
size="lg"
align="left"
color="white"
className={styles.result}
>
{data?.result}
</Typography>
</>
);
}
return (
<>
{animationState === "preview" && (
<Typography
as="h2"
size="xl"
color="white"
className={clsx(styles.breathRelax, styles.textPosition)}
>
{t("breath_relax")}
</Typography>
)}
{animationState === "breath" && (
<>
<div className={styles.textPosition}>
<Typography
as="h2"
size="xl"
color="white"
className={styles.breathIn}
>
{t("breath_in")}
</Typography>
</div>
<div className={styles.textPosition}>
<Typography
as="h2"
size="xl"
color="white"
className={styles.breathOut}
>
{t("breath_out")}
</Typography>
</div>
</>
)}
</>
);
}

View File

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

View File

@ -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 (
<section className={styles.container}>
<div className={styles.text}>
<Typography
as="h4"
weight="semiBold"
color="white"
className={styles.title}
>
{t("title")
.split("")
.map((symbol, index) => (
<span
className={styles.symbol}
style={{ animationDelay: `${index * 0.05}s` }}
key={index}
>
{symbol}
</span>
))}
</Typography>
<Typography as="h4" className={styles.subtitle}>
{t("subtitle")
.split("")
.map((symbol, index) => (
<span
className={styles.symbol}
style={{
animationDelay: `${
(t("title").split("").length + index) * 0.05
}s`,
}}
key={index}
>
{symbol}
</span>
))}
</Typography>
</div>
<Button className={styles.button} onClick={handleBegin}>
<Typography weight="bold" color="white">
{t("button")}
</Typography>
</Button>
</section>
);
}
export default StartBreathModalChild;

View File

@ -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";

View File

@ -0,0 +1,5 @@
export {
default as CompatibilityActionFieldsForm,
CompatibilityActionFieldsFormSkeleton,
} from "./CompatibilityActionFieldsForm/CompatibilityActionFieldsForm";
export { default as CompatibilityResultPage } from "./CompatibilityResultPage/CompatibilityResultPage";

View File

@ -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({
<Section title="Meditations" contentClassName={styles.sectionContent}>
<Grid columns={columns} className={styles.grid}>
{meditations.map(meditation => (
<MeditationCard key={meditation._id} {...meditation} />
<Link href={ROUTES.breath(meditation._id)} key={meditation._id}>
<MeditationCard key={meditation._id} {...meditation} />
</Link>
))}
</Grid>
</Section>

View File

@ -0,0 +1 @@
export { default as PalmistryResultPage } from "./PalmistryResultPage/PalmistryResultPage";

View File

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

View File

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

View File

@ -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 (
<div
style={style}
className={clsx(styles.modal, className, isOpen && styles.open)}
>
<div className={clsx(styles.content, classNameContent)}>{children}</div>
</div>
);
}

View File

@ -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";

View File

@ -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<React.ReactNode>(null);
const openModal = useCallback((modalContent: React.ReactNode) => {
setContent(modalContent);
setIsOpen(true);
}, []);
const closeModal = useCallback(() => {
setIsOpen(false);
}, []);
return (
<FullScreenModalContext.Provider value={{ openModal, closeModal, isOpen }}>
{children}
<FullScreenBlurModal isOpen={isOpen}>{content}</FullScreenBlurModal>
</FullScreenModalContext.Provider>
);
};

View File

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