compatibility & palm generations & add zustand & config prettier eslint
This commit is contained in:
gofnnp 2025-06-23 00:46:11 +04:00
parent 665b7bad90
commit 67f4dfdf3d
238 changed files with 6700 additions and 4284 deletions

View File

@ -1,7 +1,12 @@
{ {
"semi": true, "semi": true,
"trailingComma": "es5",
"singleQuote": false,
"printWidth": 80,
"tabWidth": 2, "tabWidth": 2,
"printWidth": 100, "useTabs": false,
"singleQuote": true, "bracketSpacing": true,
"trailingComma": "es5" "bracketSameLine": false,
"arrowParens": "avoid",
"endOfLine": "lf"
} }

34
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,34 @@
{
"editor.formatOnSave": false,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
// "source.organizeImports": "explicit"
},
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact"
],
// "typescript.preferences.importModuleSpecifier": "relative",
"editor.defaultFormatter": "esbenp.prettier-vscode",
"files.associations": {
"*.module.scss": "scss"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"eslint.workingDirectories": ["."],
"eslint.format.enable": true,
"editor.tabSize": 2,
"editor.insertSpaces": true
}

View File

@ -1,4 +1,4 @@
version: '3.8' version: "3.8"
services: services:
app: app:

View File

@ -1,9 +1,11 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc"; import { FlatCompat } from "@eslint/eslintrc";
import eslintPluginImport from "eslint-plugin-import"; import eslintPluginImport from "eslint-plugin-import";
import eslintPluginUnused from "eslint-plugin-unused-imports"; import eslintPluginReact from "eslint-plugin-react";
import eslintPluginReactHooks from "eslint-plugin-react-hooks";
import eslintPluginSort from "eslint-plugin-simple-import-sort"; import eslintPluginSort from "eslint-plugin-simple-import-sort";
import eslintPluginUnused from "eslint-plugin-unused-imports";
import { dirname } from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename); const __dirname = dirname(__filename);
@ -20,12 +22,14 @@ const eslintConfig = [
import: eslintPluginImport, import: eslintPluginImport,
"unused-imports": eslintPluginUnused, "unused-imports": eslintPluginUnused,
"simple-import-sort": eslintPluginSort, "simple-import-sort": eslintPluginSort,
react: eslintPluginReact,
"react-hooks": eslintPluginReactHooks,
}, },
rules: { rules: {
/* неиспользуемые переменные и импорты */ /* неиспользуемые переменные и импорты */
"@typescript-eslint/no-unused-vars": [ "@typescript-eslint/no-unused-vars": [
"error", "warn",
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" }, { argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
], ],
"unused-imports/no-unused-imports": "error", "unused-imports/no-unused-imports": "error",
@ -53,6 +57,52 @@ const eslintConfig = [
}, },
], ],
"simple-import-sort/exports": "error", "simple-import-sort/exports": "error",
/* React правила */
"react/jsx-uses-react": "off", // не нужно в React 17+
"react/react-in-jsx-scope": "off", // не нужно в React 17+
"react/prop-types": "off", // используем TypeScript
"react/display-name": "warn",
"react/jsx-key": "error",
"react/jsx-no-duplicate-props": "error",
"react/jsx-no-undef": "error",
// "react/no-array-index-key": "warn",
"react/no-danger": "warn",
"react/no-deprecated": "error",
"react/no-direct-mutation-state": "error",
"react/no-find-dom-node": "error",
"react/no-is-mounted": "error",
"react/no-render-return-value": "error",
"react/no-string-refs": "error",
"react/no-unescaped-entities": "warn",
"react/no-unknown-property": "error",
"react/no-unsafe": "warn",
"react/self-closing-comp": "error",
/* React Hooks правила */
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
/* TypeScript правила */
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-non-null-assertion": "warn",
"@typescript-eslint/no-var-requires": "error",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-empty-function": "warn",
"@typescript-eslint/no-inferrable-types": "error",
/* Общие правила */
"no-console": "warn",
"no-debugger": "error",
"no-alert": "warn",
"no-var": "error",
"prefer-const": "error",
"no-unused-expressions": "error",
"no-duplicate-imports": "error",
"no-multiple-empty-lines": ["error", { max: 2 }],
"eol-last": "error",
"no-trailing-spaces": "error",
}, },
}, },
]; ];

View File

@ -54,7 +54,8 @@
"subscription_status": "Subscription Status", "subscription_status": "Subscription Status",
"subscription_status_value": { "subscription_status_value": {
"ACTIVE": "Active", "ACTIVE": "Active",
"CANCELLED": "Cancels on <date>" "CANCELLED": "Cancels on <date>",
"PAST_DUE": "Past due"
}, },
"billing_period": "Billing Period", "billing_period": "Billing Period",
"billing_period_value": { "billing_period_value": {
@ -172,5 +173,29 @@
"SubscriptionStopped": { "SubscriptionStopped": {
"title": "Подписка остановлена успешно!", "title": "Подписка остановлена успешно!",
"icon": "🎉" "icon": "🎉"
},
"DatePicker": {
"year": "YYYY",
"month": "MM",
"day": "DD"
},
"TimePicker": {
"hour": "HH",
"minute": "MM",
"period": "AM/PM"
},
"Compatibility": {
"title": "Your Personality Type",
"description": "Please input your data to create the report.",
"button": "Continue",
"error": "Something went wrong. Please try again later."
},
"CompatibilityResult": {
"title": "Your Personality Type",
"error": "Something went wrong. Please try again later."
},
"PalmistryResult": {
"title": "Your Personality Type",
"error": "Something went wrong. Please try again later."
} }
} }

38
package-lock.json generated
View File

@ -18,7 +18,8 @@
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"sass": "^1.89.2", "sass": "^1.89.2",
"server-only": "^0.0.1", "server-only": "^0.0.1",
"zod": "^3.25.64" "zod": "^3.25.64",
"zustand": "^5.0.5"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3", "@eslint/eslintrc": "^3",
@ -28,6 +29,8 @@
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "15.3.3", "eslint-config-next": "15.3.3",
"eslint-plugin-import": "^2.31.0", "eslint-plugin-import": "^2.31.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-unused-imports": "^4.1.4", "eslint-plugin-unused-imports": "^4.1.4",
"prettier": "^3.5.3", "prettier": "^3.5.3",
@ -1343,7 +1346,7 @@
"version": "19.1.8", "version": "19.1.8",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz",
"integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==", "integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==",
"dev": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"csstype": "^3.0.2" "csstype": "^3.0.2"
@ -2441,7 +2444,7 @@
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"dev": true, "devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/damerau-levenshtein": { "node_modules/damerau-levenshtein": {
@ -6006,6 +6009,35 @@
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
} }
},
"node_modules/zustand": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.5.tgz",
"integrity": "sha512-mILtRfKW9xM47hqxGIxCv12gXusoY/xTSHBYApXozR0HmQv299whhBeeAcRy+KrPPybzosvJBCOmVjq6x12fCg==",
"license": "MIT",
"engines": {
"node": ">=12.20.0"
},
"peerDependencies": {
"@types/react": ">=18.0.0",
"immer": ">=9.0.6",
"react": ">=18.0.0",
"use-sync-external-store": ">=1.2.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
},
"use-sync-external-store": {
"optional": true
}
}
} }
} }
} }

View File

@ -7,7 +7,10 @@
"build": "next build", "build": "next build",
"start": "next start -p 3001", "start": "next start -p 3001",
"lint": "next lint", "lint": "next lint",
"lint:fix": "next lint --fix" "lint:fix": "next lint --fix",
"format": "prettier --write .",
"format:check": "prettier --check .",
"type-check": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"@lottiefiles/dotlottie-react": "^0.14.1", "@lottiefiles/dotlottie-react": "^0.14.1",
@ -20,7 +23,8 @@
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"sass": "^1.89.2", "sass": "^1.89.2",
"server-only": "^0.0.1", "server-only": "^0.0.1",
"zod": "^3.25.64" "zod": "^3.25.64",
"zustand": "^5.0.5"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3", "@eslint/eslintrc": "^3",
@ -30,6 +34,8 @@
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "15.3.3", "eslint-config-next": "15.3.3",
"eslint-plugin-import": "^2.31.0", "eslint-plugin-import": "^2.31.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-unused-imports": "^4.1.4", "eslint-plugin-unused-imports": "^4.1.4",
"prettier": "^3.5.3", "prettier": "^3.5.3",

View File

@ -0,0 +1,6 @@
.coreError {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}

View File

@ -0,0 +1,34 @@
"use client";
import { useEffect } from "react";
import { Button, Typography } from "@/components/ui";
import styles from "./error.module.scss";
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
// eslint-disable-next-line no-console
console.error(error);
}, [error]);
return (
<div className={styles.coreError}>
<Typography as="h2" size="xl" weight="bold">
Something went wrong!
</Typography>
<Typography as="p" align="center">
{error.message}
</Typography>
<Button onClick={() => reset()}>
<Typography color="white">Try again</Typography>
</Button>
</div>
);
}

View File

@ -0,0 +1,11 @@
.header {
margin-bottom: 24px;
& > .title {
line-height: 30px;
}
& > .description {
line-height: 25px;
}
}

View File

@ -0,0 +1,43 @@
import { Suspense, use } from "react";
import { useTranslations } from "next-intl";
import CompatibilityActionFieldsForm, {
CompatibilityActionFieldsFormSkeleton,
} from "@/components/domains/compatibility/CompatibilityActionFieldsForm/CompatibilityActionFieldsForm";
import { Typography } from "@/components/ui";
import { loadCompatibilityActionFields } from "@/entities/compatibilityActionFields/loaders";
import styles from "./page.module.scss";
export default function Compatibility({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = use(params);
const t = useTranslations("Compatibility");
return (
<>
<div className={styles.header}>
<Typography
as="h1"
size="xl"
weight="semiBold"
className={styles.title}
>
{t("title")}
</Typography>
<Typography as="p" size="sm" className={styles.description}>
{t("description")}
</Typography>
</div>
<Suspense key={id} fallback={<CompatibilityActionFieldsFormSkeleton />}>
<CompatibilityActionFieldsForm
fields={loadCompatibilityActionFields(id)}
actionId={id}
/>
</Suspense>
</>
);
}

View File

@ -0,0 +1,13 @@
import { use } from "react";
import CompatibilityResultPage from "@/components/domains/compatibility/CompatibilityResultPage/CompatibilityResultPage";
export default function CompatibilityResult({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = use(params);
return <CompatibilityResultPage id={id} />;
}

View File

@ -1,10 +1,10 @@
'use client'; "use client";
import { useEffect } from 'react'; import { useEffect } from "react";
import { Button, Typography } from '@/components/ui'; import { Button, Typography } from "@/components/ui";
import styles from "./error.module.scss" import styles from "./error.module.scss";
export default function Error({ export default function Error({
error, error,
@ -14,19 +14,20 @@ export default function Error({
reset: () => void; reset: () => void;
}) { }) {
useEffect(() => { useEffect(() => {
// eslint-disable-next-line no-console
console.error(error); console.error(error);
}, [error]); }, [error]);
return ( return (
<div className={styles.coreError}> <div className={styles.coreError}>
<Typography as='h2' size='xl' weight='bold'>Something went wrong!</Typography> <Typography as="h2" size="xl" weight="bold">
<Typography as='p' align='center'>{error.message}</Typography> Something went wrong!
<Button </Typography>
onClick={ <Typography as="p" align="center">
() => reset() {error.message}
} </Typography>
> <Button onClick={() => reset()}>
<Typography color='white'>Try again</Typography> <Typography color="white">Try again</Typography>
</Button> </Button>
</div> </div>
); );

View File

@ -7,10 +7,10 @@ export default function CoreLayout({
}: Readonly<{ }: Readonly<{
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
return <DrawerProvider> return (
<DrawerProvider>
<NavigationBar className={styles.navBar} /> <NavigationBar className={styles.navBar} />
<main className={styles.main}> <main className={styles.main}>{children}</main>
{children}
</main>
</DrawerProvider> </DrawerProvider>
);
} }

View File

@ -8,15 +8,19 @@ import {
MeditationSection, MeditationSection,
MeditationSectionSkeleton, MeditationSectionSkeleton,
PalmSection, PalmSection,
PalmSectionSkeleton PalmSectionSkeleton,
} from "@/components/domains/dashboard"; } from "@/components/domains/dashboard";
import { Horoscope } from "@/components/widgets"; import { Horoscope } from "@/components/widgets";
import { loadAssistants, loadCompatibility, loadMeditations, loadPalms } from "@/entities/dashboard/loaders"; import {
loadAssistants,
loadCompatibility,
loadMeditations,
loadPalms,
} from "@/entities/dashboard/loaders";
import styles from "./page.module.scss"; import styles from "./page.module.scss";
export default function Home() { export default function Home() {
return ( return (
<section className={styles.page}> <section className={styles.page}>
<Horoscope /> <Horoscope />

View File

@ -0,0 +1,16 @@
import PalmistryResultPage from "@/components/domains/palmistry/PalmistryResultPage/PalmistryResultPage";
import { startGeneration } from "@/entities/generations/api";
export default async function PalmistryResult({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const result = await startGeneration({
actionType: "palm",
actionId: id,
});
return <PalmistryResultPage id={result?.id} />;
}

View File

@ -9,7 +9,9 @@ export default async function PaymentFailed() {
return ( return (
<AnimatedInfoScreen <AnimatedInfoScreen
lottieAnimation={<LottieAnimation loadKey={ELottieKeys.loaderCheckMark} />} lottieAnimation={
<LottieAnimation loadKey={ELottieKeys.loaderCheckMark} />
}
title={t("title")} title={t("title")}
animationTime={0} animationTime={0}
animationTexts={[]} animationTexts={[]}

View File

@ -15,22 +15,30 @@ interface MetricsProps {
currency: string; currency: string;
} }
export default function Metrics({ fbPixels, productPrice, currency }: MetricsProps) { export default function Metrics({
fbPixels,
productPrice,
currency,
}: MetricsProps) {
const t = useTranslations("Payment.Success"); const t = useTranslations("Payment.Success");
const [isButtonVisible, setIsButtonVisible] = useState(false); const [isButtonVisible, setIsButtonVisible] = useState(false);
const navigateToHome = () => { const navigateToHome = () => {
window.location.href = ROUTES.home() window.location.href = ROUTES.home();
} };
// Yandex Metrica // Yandex Metrica
useEffect(() => { useEffect(() => {
const interval = setInterval(() => { const interval = setInterval(() => {
if (typeof window.ym === 'function' && typeof window.klaviyo === 'object' && typeof window.gtag === 'function') { if (
typeof window.ym === "function" &&
typeof window.klaviyo === "object" &&
typeof window.gtag === "function"
) {
try { try {
window.gtag('event', 'PaymentSuccess') window.gtag("event", "PaymentSuccess");
window.klaviyo.push(['track', "PaymentSuccess"]); window.klaviyo.push(["track", "PaymentSuccess"]);
window.ym(95799066, "init", { window.ym(95799066, "init", {
clickmap: true, clickmap: true,
@ -39,25 +47,24 @@ export default function Metrics({ fbPixels, productPrice, currency }: MetricsPro
webvisor: true, webvisor: true,
}); });
window.ym(95799066, 'reachGoal', "PaymentSuccess", {}, () => { window.ym(95799066, "reachGoal", "PaymentSuccess", {}, () => {
console.log("Запрос отправлен");
// deleteYm() // deleteYm()
setIsButtonVisible(true); setIsButtonVisible(true);
}) });
} catch (e) { } catch (e) {
console.error('YM error:', e) // eslint-disable-next-line no-console
console.error("YM error:", e);
} finally { } finally {
clearInterval(interval); clearInterval(interval);
} }
} }
}, 200); }, 200);
return () => clearInterval(interval) return () => clearInterval(interval);
}, []); }, []);
return <> return (
<>
{/* Klaviyo */} {/* Klaviyo */}
{/* <Script src="https://static.klaviyo.com/onsite/js/klaviyo.js?company_id=RM7w5r" /> */} {/* <Script src="https://static.klaviyo.com/onsite/js/klaviyo.js?company_id=RM7w5r" /> */}
<Script id="klaviyo-script"> <Script id="klaviyo-script">
@ -148,7 +155,11 @@ export default function Metrics({ fbPixels, productPrice, currency }: MetricsPro
</Script> </Script>
{/* Google Analytics */} {/* Google Analytics */}
<Script id="google-analytics-script" async src="https://www.googletagmanager.com/gtag/js?id=G-4N17LL3BB5" /> <Script
id="google-analytics-script"
async
src="https://www.googletagmanager.com/gtag/js?id=G-4N17LL3BB5"
/>
<Script id="google-analytics-script-config"> <Script id="google-analytics-script-config">
{`window.dataLayer = window.dataLayer || []; {`window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);} function gtag(){dataLayer.push(arguments);}
@ -158,7 +169,7 @@ export default function Metrics({ fbPixels, productPrice, currency }: MetricsPro
</Script> </Script>
{/* Facebook Pixel */} {/* Facebook Pixel */}
{fbPixels.map((pixel) => ( {fbPixels.map(pixel => (
<Script id={`facebook-pixel-${pixel}`} key={pixel}> <Script id={`facebook-pixel-${pixel}`} key={pixel}>
{`!function(f,b,e,v,n,t,s) {`!function(f,b,e,v,n,t,s)
{if(f.fbq)return;n=f.fbq=function(){n.callMethod? {if(f.fbq)return;n=f.fbq=function(){n.callMethod?
@ -174,12 +185,11 @@ fbq('track', 'Purchase', { value: ${productPrice}, currency: "${currency}" });`}
</Script> </Script>
))} ))}
{isButtonVisible && {isButtonVisible && (
<Button onClick={navigateToHome} className={styles.button}> <Button onClick={navigateToHome} className={styles.button}>
<Typography color="white"> <Typography color="white">{t("button")}</Typography>
{t("button")}
</Typography>
</Button> </Button>
} )}
</>; </>
);
} }

View File

@ -5,9 +5,11 @@ import { ELottieKeys } from "@/shared/constants/lottie";
import Metrics from "./Metrics"; import Metrics from "./Metrics";
export default async function PaymentSuccess({ searchParams }: { export default async function PaymentSuccess({
searchParams,
}: {
searchParams: Promise<{ searchParams: Promise<{
[key: string]: string | undefined [key: string]: string | undefined;
}>; }>;
}) { }) {
const params = await searchParams; const params = await searchParams;
@ -21,7 +23,9 @@ export default async function PaymentSuccess({ searchParams }: {
return ( return (
<> <>
<AnimatedInfoScreen <AnimatedInfoScreen
lottieAnimation={<LottieAnimation loadKey={ELottieKeys.loaderCheckMark} />} lottieAnimation={
<LottieAnimation loadKey={ELottieKeys.loaderCheckMark} />
}
title={t("title")} title={t("title")}
/> />
<Metrics <Metrics

View File

@ -1,88 +0,0 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
import { Billing, LogOut, ProfileBlock, ProfileInformation } from "@/components/domains/profile"
import { Card, Modal, Typography } from "@/components/ui";
import { ROUTES } from "@/shared/constants/client-routes";
import styles from "./page.module.scss"
export default function ProfilePage() {
const t = useTranslations('Profile');
const router = useRouter();
const [logoutModal, setLogoutModal] = useState(false);
const handleLogout = () => {
router.replace(ROUTES.home());
// logout();
};
const handleLogoutModal = () => {
setLogoutModal(true);
};
const handleBilling = () => {
router.push(ROUTES.profileSubscriptions())
}
const profileBlocks = [
{
title: t("profile_information.title"),
description: t("profile_information.description"),
children: <ProfileInformation />
},
{
title: t("billing.title"),
description: t("billing.description"),
children: <Billing onBilling={handleBilling} />
},
{
title: t("log_out.title"),
children: <LogOut onLogout={handleLogoutModal} />
}
]
return (
<>
<Card className={styles.card}>
{profileBlocks.map((block, index) => (
<div key={block.title}>
<ProfileBlock {...block}>
{block.children}
</ProfileBlock>
{index !== profileBlocks.length - 1 && <hr className={styles.line} />}
</div>
))}
</Card>
{logoutModal && <Modal
isCloseButtonVisible={false}
open={!!logoutModal}
onClose={() => setLogoutModal(false)}
className={styles.modal}
modalClassName={styles["modal-container"]}
>
<Typography as="h4" className={styles["modal-title"]}>
{t("log_out.modal.title")}
</Typography>
<p className={styles["modal-description"]}>
{t("log_out.modal.description")}
</p>
<div className={styles["modal-answers"]}>
<div className={styles["modal-answer"]} onClick={handleLogout}>
<p className={styles["modal-answer-text"]}>
{t("log_out.modal.log_out_button")}
</p>
</div>
<div className={styles["modal-answer"]} onClick={() => setLogoutModal(false)}>
<p className={styles["modal-answer-text"]}>
{t("log_out.modal.stay_button")}
</p>
</div>
</div>
</Modal>}
</>
)
}

View File

@ -8,44 +8,3 @@
background-color: #f0f0f0; background-color: #f0f0f0;
margin: 0; margin: 0;
} }
.modal-container {
max-width: 290px;
padding: 24px 0px 0px;
overflow: hidden;
}
.modal-title {
font-weight: 600;
margin-bottom: 16px;
padding-inline: 24px;
}
.modal-description {
padding-inline: 24px;
text-align: center;
}
.modal-answers {
display: flex;
flex-direction: row;
margin-top: 24px;
border-top: 1px solid #D9D9D9;
}
.modal-answer {
width: 50%;
height: 52px;
display: flex;
align-items: center;
justify-content: center;
color: #275DA7;
font-weight: 600;
cursor: pointer;
&:first-child {
border-right: 1px solid #D9D9D9;
}
}

View File

@ -1,8 +1,50 @@
import ProfilePage from "./Profile"; import { Suspense } from "react";
import { useTranslations } from "next-intl";
import {
Billing,
LogOut,
ProfileBlock,
ProfileInformation,
ProfileInformationSkeleton,
} from "@/components/domains/profile";
import { Card } from "@/components/ui";
import { loadUser } from "@/entities/user/loaders";
import styles from "./page.module.scss";
export default function Profile() { export default function Profile() {
const t = useTranslations("Profile");
const profileBlocks = [
{
title: t("profile_information.title"),
description: t("profile_information.description"),
children: (
<Suspense fallback={<ProfileInformationSkeleton />}>
<ProfileInformation user={loadUser()} />
</Suspense>
),
},
{
title: t("billing.title"),
description: t("billing.description"),
children: <Billing />,
},
{
title: t("log_out.title"),
children: <LogOut />,
},
];
return ( return (
<ProfilePage /> <Card className={styles.card}>
) {profileBlocks.map((block, index) => (
<div key={block.title}>
<ProfileBlock {...block}>{block.children}</ProfileBlock>
{index !== profileBlocks.length - 1 && <hr className={styles.line} />}
</div>
))}
</Card>
);
} }

View File

@ -1,28 +1,32 @@
'use client'; "use client";
import { useRouter } from 'next/navigation'; import { useRouter } from "next/navigation";
import { useTranslations } from 'next-intl'; import { useTranslations } from "next-intl";
import { Button, Typography } from '@/components/ui'; import { Button, Typography } from "@/components/ui";
import { ROUTES } from '@/shared/constants/client-routes'; import { ROUTES } from "@/shared/constants/client-routes";
import styles from "./page.module.scss" import styles from "./page.module.scss";
export default function Error() { export default function Error() {
const t = useTranslations("Subscriptions") const t = useTranslations("Subscriptions");
const router = useRouter() const router = useRouter();
return ( return (
<div className={styles.container}> <div className={styles.container}>
<Typography as="h1" className={styles.title}>{t("title")}</Typography> <Typography as="h1" className={styles.title}>
<Typography as='p' align='center'>{t("error")}</Typography> {t("title")}
</Typography>
<Typography as="p" align="center">
{t("error")}
</Typography>
<Button <Button
onClick={ onClick={
// () => reset() // () => reset()
() => router.push(ROUTES.retainingFunnelCancelSubscription()) () => router.push(ROUTES.retainingFunnelCancelSubscription())
} }
> >
<Typography color='white'>{t("try_again")}</Typography> <Typography color="white">{t("try_again")}</Typography>
</Button> </Button>
</div> </div>
); );

View File

@ -12,7 +12,7 @@
} }
.buttonCancel { .buttonCancel {
color: #ACB0BA; color: #acb0ba;
font-size: 16px; font-size: 16px;
line-height: 25px; line-height: 25px;
background: none; background: none;
@ -32,11 +32,11 @@
} }
.description { .description {
color: #ACB0BA; color: #acb0ba;
} }
.error { .error {
color: #FF0000; color: #ff0000;
} }
.modal-container { .modal-container {
@ -60,7 +60,7 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
margin-top: 24px; margin-top: 24px;
border-top: 1px solid #D9D9D9; border-top: 1px solid #d9d9d9;
} }
.modal-answer { .modal-answer {
@ -69,13 +69,13 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: #275DA7; color: #275da7;
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
text-align: center; text-align: center;
&:first-child { &:first-child {
border-right: 1px solid #D9D9D9; border-right: 1px solid #d9d9d9;
} }
} }
@ -88,9 +88,9 @@
} }
:global(.dark-theme) .modal-title { :global(.dark-theme) .modal-title {
color: #F7F7F7; color: #f7f7f7;
} }
:global(.dark-theme) .modal-description { :global(.dark-theme) .modal-description {
color: #F7F7F7; color: #f7f7f7;
} }

View File

@ -1,11 +1,15 @@
import { Suspense } from "react" import { Suspense } from "react";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { CancelSubscriptionModalProvider, SubscriptionsList, SubscriptionsListSkeleton } from "@/components/domains/profile/subscriptions"; import {
CancelSubscriptionModalProvider,
SubscriptionsList,
SubscriptionsListSkeleton,
} from "@/components/domains/profile/subscriptions";
import { Typography } from "@/components/ui"; import { Typography } from "@/components/ui";
import { loadSubscriptionsData } from "@/entities/subscriptions/loaders"; import { loadSubscriptionsData } from "@/entities/subscriptions/loaders";
import styles from "./page.module.scss" import styles from "./page.module.scss";
export default async function Subscriptions() { export default async function Subscriptions() {
const t = await getTranslations("Subscriptions"); const t = await getTranslations("Subscriptions");
@ -13,11 +17,13 @@ export default async function Subscriptions() {
return ( return (
<CancelSubscriptionModalProvider> <CancelSubscriptionModalProvider>
<div className={styles.container}> <div className={styles.container}>
<Typography as="h1" className={styles.title}>{t("title")}</Typography> <Typography as="h1" className={styles.title}>
{t("title")}
</Typography>
<Suspense fallback={<SubscriptionsListSkeleton />}> <Suspense fallback={<SubscriptionsListSkeleton />}>
<SubscriptionsList promise={loadSubscriptionsData()} /> <SubscriptionsList promise={loadSubscriptionsData()} />
</Suspense> </Suspense>
</div> </div>
</CancelSubscriptionModalProvider> </CancelSubscriptionModalProvider>
) );
} }

View File

@ -11,17 +11,18 @@ export default async function AppreciateChoice() {
t("descriptions.1"), t("descriptions.1"),
t("descriptions.2"), t("descriptions.2"),
t("descriptions.3"), t("descriptions.3"),
] ];
return ( return (
<AnimatedInfoScreen <AnimatedInfoScreen
lottieAnimation={<LottieAnimation loadKey={ELottieKeys.loaderCheckMark} />} lottieAnimation={
<LottieAnimation loadKey={ELottieKeys.loaderCheckMark} />
}
title={t("title")} title={t("title")}
animationTime={9000} animationTime={9000}
animationTexts={animationTexts} animationTexts={animationTexts}
buttonText={t("button")} buttonText={t("button")}
nextRoute={ROUTES.retainingFunnelWhatReason()} nextRoute={ROUTES.retainingFunnelWhatReason()}
/> />
);
)
} }

View File

@ -7,12 +7,12 @@
.title { .title {
font-size: 28px; font-size: 28px;
line-height: 125%; line-height: 125%;
color: #1A1A1A; color: #1a1a1a;
} }
.description { .description {
font-size: 20px; font-size: 20px;
line-height: 125%; line-height: 125%;
color: #2C2C2C; color: #2c2c2c;
padding-inline: 14px; padding-inline: 14px;
} }

View File

@ -15,10 +15,10 @@ export default function CanselSubscription() {
</Typography> </Typography>
<Typography as="p" className={styles.description}> <Typography as="p" className={styles.description}>
{t.rich("description", { {t.rich("description", {
br: () => <br /> br: () => <br />,
})} })}
</Typography> </Typography>
<Buttons /> <Buttons />
</div> </div>
) );
} }

View File

@ -15,7 +15,7 @@
line-height: 25px; line-height: 25px;
margin-top: 74px; margin-top: 74px;
padding-inline: 28px; padding-inline: 28px;
color: #ACB0BA; color: #acb0ba;
} }
.topSellingImage { .topSellingImage {
@ -43,6 +43,10 @@
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
background: linear-gradient(0deg, #FFFFFF 25.48%, rgba(255, 255, 255, 0) 100%); background: linear-gradient(
0deg,
#ffffff 25.48%,
rgba(255, 255, 255, 0) 100%
);
} }
} }

View File

@ -1,7 +1,10 @@
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { Offer } from "@/components/domains/retaining"; import { Offer } from "@/components/domains/retaining";
import { Buttons, LottieAnimations } from "@/components/domains/retaining/cancellation-of-subscription"; import {
Buttons,
LottieAnimations,
} from "@/components/domains/retaining/cancellation-of-subscription";
import { TopSellingSvg } from "@/components/domains/retaining/images"; import { TopSellingSvg } from "@/components/domains/retaining/images";
import { Typography } from "@/components/ui"; import { Typography } from "@/components/ui";
@ -30,5 +33,5 @@ export default async function CancellationOfSubscription() {
/> />
<Buttons /> <Buttons />
</div> </div>
) );
} }

View File

@ -1,10 +1,13 @@
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { ChangeMindAnswer, ChangeMindButtons } from "@/components/domains/retaining/change-mind"; import {
ChangeMindAnswer,
ChangeMindButtons,
} from "@/components/domains/retaining/change-mind";
import { Typography } from "@/components/ui"; import { Typography } from "@/components/ui";
export default function ChangeMind() { export default function ChangeMind() {
const t = useTranslations("ChangeMind") const t = useTranslations("ChangeMind");
const answers: ChangeMindAnswer[] = [ const answers: ChangeMindAnswer[] = [
{ {
@ -22,8 +25,8 @@ export default function ChangeMind() {
{ {
id: 4, id: 4,
title: t("answers.other"), title: t("answers.other"),
} },
] ];
return ( return (
<> <>
@ -32,5 +35,5 @@ export default function ChangeMind() {
</Typography> </Typography>
<ChangeMindButtons answers={answers} /> <ChangeMindButtons answers={answers} />
</> </>
) );
} }

View File

@ -20,7 +20,6 @@ const stepperRoutes: Record<ERetainingFunnel, string[]> = {
// ROUTES.retainingFunnelChangeMind(), // ROUTES.retainingFunnelChangeMind(),
// ROUTES.retainingFunnelSecondChance(), // ROUTES.retainingFunnelSecondChance(),
// ROUTES.retainingFunnelCancellationOfSubscription(), // ROUTES.retainingFunnelCancellationOfSubscription(),
], ],
[ERetainingFunnel.Purple]: [ [ERetainingFunnel.Purple]: [
ROUTES.retainingFunnelAppreciateChoice(), ROUTES.retainingFunnelAppreciateChoice(),
@ -30,12 +29,10 @@ const stepperRoutes: Record<ERetainingFunnel, string[]> = {
// ROUTES.retainingFunnelStopFor30Days(), // ROUTES.retainingFunnelStopFor30Days(),
// ROUTES.retainingFunnelCancellationOfSubscription(), // ROUTES.retainingFunnelCancellationOfSubscription(),
], ],
[ERetainingFunnel.Stay50]: [ [ERetainingFunnel.Stay50]: [ROUTES.retainingFunnelStay50Done()],
ROUTES.retainingFunnelStay50Done(), };
],
}
function StepperLayout({ children }: { children: React.ReactNode; }) { function StepperLayout({ children }: { children: React.ReactNode }) {
// const darkTheme = useSelector(selectors.selectDarkTheme); // const darkTheme = useSelector(selectors.selectDarkTheme);
// const mainRef = useRef<HTMLDivElement>(null); // const mainRef = useRef<HTMLDivElement>(null);
// useSchemeColorByElement(mainRef.current, "section.page, .page, section", [ // useSchemeColorByElement(mainRef.current, "section.page, .page, section", [

View File

@ -20,7 +20,7 @@
} }
.description { .description {
color: #ACB0BA; color: #acb0ba;
font-size: 17px; font-size: 17px;
line-height: 25px; line-height: 25px;
margin-top: 72px; margin-top: 72px;

View File

@ -13,13 +13,11 @@ export default async function PlanCancelled() {
<Typography as="h1" weight="semiBold" className={styles.title}> <Typography as="h1" weight="semiBold" className={styles.title}>
{t("title")} {t("title")}
</Typography> </Typography>
<span className={styles.icon}> <span className={styles.icon}>{t("icon")}</span>
{t("icon")}
</span>
<PlanCancelledButton /> <PlanCancelledButton />
<Typography as="p" className={styles.description}> <Typography as="p" className={styles.description}>
{t("description")} {t("description")}
</Typography> </Typography>
</div> </div>
) );
} }

View File

@ -15,5 +15,5 @@ export default async function SecondChance() {
</Typography> </Typography>
<SecondChancePage /> <SecondChancePage />
</div> </div>
) );
} }

View File

@ -7,19 +7,18 @@ import { ELottieKeys } from "@/shared/constants/lottie";
export default async function Stay50Done() { export default async function Stay50Done() {
const t = await getTranslations("Stay50Done"); const t = await getTranslations("Stay50Done");
const animationTexts = [ const animationTexts = [t("descriptions.1")];
t("descriptions.1"),
]
return ( return (
<AnimatedInfoScreen <AnimatedInfoScreen
lottieAnimation={<LottieAnimation loadKey={ELottieKeys.loaderCheckMark} />} lottieAnimation={
<LottieAnimation loadKey={ELottieKeys.loaderCheckMark} />
}
title={t("title")} title={t("title")}
animationTime={5000} animationTime={5000}
animationTexts={animationTexts} animationTexts={animationTexts}
buttonText={t("button")} buttonText={t("button")}
nextRoute={ROUTES.home()} nextRoute={ROUTES.home()}
/> />
);
)
} }

View File

@ -15,5 +15,5 @@ export default async function StopFor30Days() {
</Typography> </Typography>
<StopFor30DaysButtons /> <StopFor30DaysButtons />
</div> </div>
) );
} }

View File

@ -13,10 +13,8 @@ export default async function SubscriptionStopped() {
<Typography as="h1" weight="semiBold" className={styles.title}> <Typography as="h1" weight="semiBold" className={styles.title}>
{t("title")} {t("title")}
</Typography> </Typography>
<span className={styles.icon}> <span className={styles.icon}>{t("icon")}</span>
{t("icon")}
</span>
<SubscriptionStoppedButton /> <SubscriptionStoppedButton />
</div> </div>
) );
} }

View File

@ -1,64 +1,67 @@
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { WhatReasonAnswer, WhatReasonsButtons } from "@/components/domains/retaining/what-reason"; import {
WhatReasonAnswer,
WhatReasonsButtons,
} from "@/components/domains/retaining/what-reason";
import { Typography } from "@/components/ui"; import { Typography } from "@/components/ui";
import { ERetainingFunnel } from "@/types"; import { ERetainingFunnel } from "@/types";
export default function WhatReason() { export default function WhatReason() {
const t = useTranslations("WhatReason") const t = useTranslations("WhatReason");
const answers: WhatReasonAnswer[] = [ const answers: WhatReasonAnswer[] = [
{ {
id: 1, id: 1,
title: t("answers.no_promised_result"), title: t("answers.no_promised_result"),
funnel: ERetainingFunnel.Red funnel: ERetainingFunnel.Red,
}, },
{ {
id: 2, id: 2,
title: t("answers.too_expensive"), title: t("answers.too_expensive"),
funnel: ERetainingFunnel.Red funnel: ERetainingFunnel.Red,
}, },
{ {
id: 3, id: 3,
title: t("answers.high_auto_payment"), title: t("answers.high_auto_payment"),
funnel: ERetainingFunnel.Red funnel: ERetainingFunnel.Red,
}, },
{ {
id: 4, id: 4,
title: t("answers.unexpected_fee"), title: t("answers.unexpected_fee"),
funnel: ERetainingFunnel.Red funnel: ERetainingFunnel.Red,
}, },
{ {
id: 5, id: 5,
title: t("answers.want_pause"), title: t("answers.want_pause"),
funnel: ERetainingFunnel.Green funnel: ERetainingFunnel.Green,
}, },
{ {
id: 6, id: 6,
title: t("answers.service_not_as_expected"), title: t("answers.service_not_as_expected"),
funnel: ERetainingFunnel.Red funnel: ERetainingFunnel.Red,
}, },
{ {
id: 7, id: 7,
title: t("answers.found_alternative"), title: t("answers.found_alternative"),
funnel: ERetainingFunnel.Red funnel: ERetainingFunnel.Red,
}, },
{ {
id: 8, id: 8,
title: t("answers.dislike_app"), title: t("answers.dislike_app"),
funnel: ERetainingFunnel.Purple funnel: ERetainingFunnel.Purple,
}, },
{ {
id: 9, id: 9,
title: t("answers.hard_to_navigate"), title: t("answers.hard_to_navigate"),
funnel: ERetainingFunnel.Purple funnel: ERetainingFunnel.Purple,
}, },
{ {
id: 10, id: 10,
title: t("answers.other"), title: t("answers.other"),
funnel: ERetainingFunnel.Purple funnel: ERetainingFunnel.Purple,
} },
] ];
return ( return (
<> <>
@ -67,5 +70,5 @@ export default function WhatReason() {
</Typography> </Typography>
<WhatReasonsButtons answers={answers} /> <WhatReasonsButtons answers={answers} />
</> </>
) );
} }

View File

@ -34,8 +34,14 @@ import { ROUTES } from "@/shared/constants/client-routes";
function extractTrackingCookiesFromUrl(url: URL): Record<string, string> { function extractTrackingCookiesFromUrl(url: URL): Record<string, string> {
const trackingCookieKeys = [ const trackingCookieKeys = [
'_fbc', '_fbp', '_ym_uid', '_ym_d', '_ym_isad', '_ym_visorc', "_fbc",
'yandexuid', 'ymex' "_fbp",
"_ym_uid",
"_ym_d",
"_ym_isad",
"_ym_visorc",
"yandexuid",
"ymex",
]; ];
const cookies: Record<string, string> = {}; const cookies: Record<string, string> = {};
@ -43,8 +49,8 @@ function extractTrackingCookiesFromUrl(url: URL): Record<string, string> {
for (const [key, value] of url.searchParams.entries()) { for (const [key, value] of url.searchParams.entries()) {
if ( if (
trackingCookieKeys.includes(key) || trackingCookieKeys.includes(key) ||
key.startsWith('_ga') || key.startsWith("_ga") ||
key.startsWith('_gid') key.startsWith("_gid")
) { ) {
cookies[key] = value; cookies[key] = value;
} }
@ -63,7 +69,10 @@ export async function GET(req: NextRequest) {
const productPrice = searchParams.get("price"); const productPrice = searchParams.get("price");
const currency = searchParams.get("currency"); const currency = searchParams.get("currency");
const redirectUrl = new URL(`${ROUTES.payment()}`, process.env.NEXT_PUBLIC_APP_URL || ""); const redirectUrl = new URL(
`${ROUTES.payment()}`,
process.env.NEXT_PUBLIC_APP_URL || ""
);
if (productId) redirectUrl.searchParams.set("productId", productId); if (productId) redirectUrl.searchParams.set("productId", productId);
if (placementId) redirectUrl.searchParams.set("placementId", placementId); if (placementId) redirectUrl.searchParams.set("placementId", placementId);
if (paywallId) redirectUrl.searchParams.set("paywallId", paywallId); if (paywallId) redirectUrl.searchParams.set("paywallId", paywallId);

View File

@ -5,14 +5,16 @@ import type { Metadata } from "next";
import { Inter } from "next/font/google"; import { Inter } from "next/font/google";
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import { hasLocale, NextIntlClientProvider } from "next-intl"; import { hasLocale, NextIntlClientProvider } from "next-intl";
import { getMessages } from "next-intl/server";
import clsx from "clsx"; import clsx from "clsx";
import { routing } from "@/i18n/routing"; import { routing } from "@/i18n/routing";
import { StoreProvider } from "@/providers/StoreProvider";
import styles from "./layout.module.scss" import styles from "./layout.module.scss";
export function generateStaticParams() { export function generateStaticParams() {
return routing.locales.map((locale) => ({ locale })); return routing.locales.map(locale => ({ locale }));
} }
const inter = Inter({ const inter = Inter({
@ -23,7 +25,8 @@ const inter = Inter({
export const metadata: Metadata = { export const metadata: Metadata = {
title: "WIT", title: "WIT",
description: "More than 14M people have experienced the value of our products. Wit Apps, headquartered in Silicon Valley, California, is a tech company that constructs global enterprises specializing in mobile-first products. We believe in the transformative power of technology, capable of turning chaos into miracles, thus enhancing the lives of millions. To realize this vision, we integrate leading expertise in artificial intelligence with a deep understanding of consumer needs and lifestyle trends.", description:
"More than 14M people have experienced the value of our products. Wit Apps, headquartered in Silicon Valley, California, is a tech company that constructs global enterprises specializing in mobile-first products. We believe in the transformative power of technology, capable of turning chaos into miracles, thus enhancing the lives of millions. To realize this vision, we integrate leading expertise in artificial intelligence with a deep understanding of consumer needs and lifestyle trends.",
}; };
export default async function RootLayout({ export default async function RootLayout({
@ -38,10 +41,14 @@ export default async function RootLayout({
notFound(); notFound();
} }
const messages = await getMessages();
return ( return (
<html lang={locale}> <html lang={locale}>
<body className={clsx(inter.variable, styles.body)}> <body className={clsx(inter.variable, styles.body)}>
<NextIntlClientProvider>{children}</NextIntlClientProvider> <NextIntlClientProvider messages={messages}>
<StoreProvider>{children}</StoreProvider>
</NextIntlClientProvider>
</body> </body>
</html> </html>
); );

View File

@ -0,0 +1,9 @@
.errorToast {
position: fixed;
bottom: calc(0dvh + 32px);
left: 50%;
transform: translateX(-50%);
width: 100%;
max-width: 400px;
z-index: 1000;
}

View File

@ -0,0 +1,86 @@
"use client";
import { use, useState } from "react";
import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
import { Skeleton, Toast, Typography } from "@/components/ui";
import { ActionFieldsForm } from "@/components/widgets";
import { startGeneration } from "@/entities/generations/actions";
import { ROUTES } from "@/shared/constants/client-routes";
import { ActionField } from "@/types";
import styles from "./CompatibilityActionFieldsForm.module.scss";
interface CompatibilityActionFieldsFormProps {
fields: Promise<ActionField[]>;
actionId: string;
}
export default function CompatibilityActionFieldsForm({
fields,
actionId,
}: CompatibilityActionFieldsFormProps) {
const t = useTranslations("Compatibility");
const compatibilityActionFields = use(fields);
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const [formError, setFormError] = useState<string | null>(null);
const handleSubmit = async (
values: Record<string, string | number | null>
) => {
setIsLoading(true);
setFormError(null);
const response = await startGeneration({
actionType: "compatibility",
actionId,
variables: values,
});
setIsLoading(false);
if (response?.data?.id) {
router.push(ROUTES.compatibilityResult(response.data.id));
}
if (response.error) {
setFormError(response.error);
return;
}
};
// Обработка случая, когда поля не загрузились
if (!compatibilityActionFields || compatibilityActionFields.length === 0) {
return (
<Typography as="p" size="sm" color="danger">
{t("error")}
</Typography>
);
}
return (
<>
<ActionFieldsForm
fields={compatibilityActionFields}
onSubmit={handleSubmit}
buttonText={t("button")}
isLoading={isLoading}
/>
{formError && (
<Toast variant="error" classNameContainer={styles.errorToast}>
<Typography as="p" size="sm" color="black">
{t("error")}
</Typography>
</Toast>
)}
</>
);
}
export function CompatibilityActionFieldsFormSkeleton() {
return <Skeleton style={{ height: "250px" }} />;
}

View File

@ -0,0 +1,15 @@
.loadingContainer {
display: flex;
justify-content: center;
align-items: center;
height: calc(100dvh - 56px);
}
.title {
line-height: 30px;
}
.description {
line-height: 25px;
margin-top: 24px;
}

View File

@ -0,0 +1,42 @@
"use client";
import { useTranslations } from "next-intl";
import { Spinner, Toast, Typography } from "@/components/ui";
import { useGenerationPolling } from "@/hooks/generation/useGenerationPolling";
import styles from "./CompatibilityResultPage.module.scss";
interface CompatibilityResultPageProps {
id: string;
}
export default function CompatibilityResultPage({
id,
}: CompatibilityResultPageProps) {
const t = useTranslations("CompatibilityResult");
const { data, error, isLoading } = useGenerationPolling(id);
if (isLoading) {
return (
<div className={styles.loadingContainer}>
<Spinner />
</div>
);
}
if (error) {
return <Toast variant="error">{t("error")}</Toast>;
}
return (
<>
<Typography as="h1" size="xl" weight="semiBold" className={styles.title}>
{t("title")}
</Typography>
<Typography as="p" size="lg" align="left" className={styles.description}>
{data?.result}
</Typography>
</>
);
}

View File

@ -40,7 +40,7 @@
width: 8px; width: 8px;
height: 8px; height: 8px;
border-radius: 50%; border-radius: 50%;
background-color: #34D399; background-color: #34d399;
} }
} }

View File

@ -1,7 +1,7 @@
import { Button, Card, Stars, Typography } from "@/components/ui" import { Button, Card, Stars, Typography } from "@/components/ui";
import { Assistant } from "@/entities/dashboard/types" import { Assistant } from "@/entities/dashboard/types";
import styles from "./AdviserCard.module.scss" import styles from "./AdviserCard.module.scss";
type AdviserCardProps = Assistant; type AdviserCardProps = Assistant;
@ -10,10 +10,13 @@ export default function AdviserCard({
photoUrl, photoUrl,
rating, rating,
reviewCount, reviewCount,
description description,
}: AdviserCardProps) { }: AdviserCardProps) {
return ( return (
<Card className={styles.card} style={{ backgroundImage: `url(${photoUrl})` }}> <Card
className={styles.card}
style={{ backgroundImage: `url(${photoUrl})` }}
>
<div className={styles.content}> <div className={styles.content}>
<div className={styles.info}> <div className={styles.info}>
<div className={styles.name}> <div className={styles.name}>
@ -22,7 +25,12 @@ export default function AdviserCard({
</Typography> </Typography>
<div className={styles.indicator} /> <div className={styles.indicator} />
</div> </div>
<Typography className={styles.description} color="white" weight="medium" size="xs"> <Typography
className={styles.description}
color="white"
weight="medium"
size="xs"
>
{description} {description}
</Typography> </Typography>
<div className={styles.rating}> <div className={styles.rating}>
@ -43,5 +51,5 @@ export default function AdviserCard({
</div> </div>
<div className={styles.shadow} /> <div className={styles.shadow} />
</Card> </Card>
) );
} }

View File

@ -6,6 +6,7 @@
box-shadow: none; box-shadow: none;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
cursor: pointer;
} }
.content { .content {

View File

@ -12,7 +12,7 @@ export default function CompatibilityCard({
imageUrl, imageUrl,
title, title,
type, type,
minutes minutes,
}: CompatibilityCardProps) { }: CompatibilityCardProps) {
return ( return (
<Card className={styles.card}> <Card className={styles.card}>
@ -27,15 +27,17 @@ export default function CompatibilityCard({
<Typography size="lg" weight="medium" align="left"> <Typography size="lg" weight="medium" align="left">
{title} {title}
</Typography> </Typography>
<MetaLabel iconLabelProps={{ <MetaLabel
iconLabelProps={{
iconProps: { iconProps: {
name: IconName.Article, name: IconName.Article,
}, },
children: <Typography color="secondary">{type}</Typography> children: <Typography color="secondary">{type}</Typography>,
}}> }}
>
{minutes} min {minutes} min
</MetaLabel> </MetaLabel>
</div> </div>
</Card> </Card>
) );
} }

View File

@ -26,7 +26,7 @@
width: 40px; width: 40px;
height: 40px; height: 40px;
border-radius: 50%; border-radius: 50%;
background-color: #F5F5F7; background-color: #f5f5f7;
padding: 0; padding: 0;
& > .icon { & > .icon {

View File

@ -4,7 +4,7 @@ import { Button, Card, Icon, MetaLabel, Typography } from "@/components/ui";
import { IconName } from "@/components/ui/Icon/Icon"; import { IconName } from "@/components/ui/Icon/Icon";
import { Meditation } from "@/entities/dashboard/types"; import { Meditation } from "@/entities/dashboard/types";
import styles from "./MeditationCard.module.scss" import styles from "./MeditationCard.module.scss";
type MeditationCardProps = Meditation; type MeditationCardProps = Meditation;
@ -12,7 +12,7 @@ export default function MeditationCard({
imageUrl, imageUrl,
title, title,
type, type,
minutes minutes,
}: MeditationCardProps) { }: MeditationCardProps) {
return ( return (
<Card className={styles.card}> <Card className={styles.card}>
@ -28,17 +28,19 @@ export default function MeditationCard({
<Typography size="lg" weight="regular"> <Typography size="lg" weight="regular">
{title} {title}
</Typography> </Typography>
<MetaLabel iconLabelProps={{ <MetaLabel
iconLabelProps={{
iconProps: { iconProps: {
name: IconName.Video, name: IconName.Video,
color: "#6B7280", color: "#6B7280",
size: { size: {
width: 24, width: 24,
height: 25 height: 25,
}
}, },
children: <Typography color="secondary">{type}</Typography> },
}}> children: <Typography color="secondary">{type}</Typography>,
}}
>
{minutes} min {minutes} min
</MetaLabel> </MetaLabel>
</div> </div>
@ -48,12 +50,12 @@ export default function MeditationCard({
name={IconName.Chevron} name={IconName.Chevron}
size={{ size={{
width: 18, width: 18,
height: 18 height: 18,
}} }}
color="#A0A7B5" color="#A0A7B5"
/> />
</Button> </Button>
</div> </div>
</Card> </Card>
) );
} }

View File

@ -11,7 +11,11 @@
.image { .image {
width: 100%; width: 100%;
height: 123px; height: 123px;
background: linear-gradient(90deg, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0) 100%); background: linear-gradient(
90deg,
rgba(0, 0, 0, 0.5) 0%,
rgba(0, 0, 0, 0) 100%
);
display: flex; display: flex;
justify-content: center; justify-content: center;
} }

View File

@ -12,7 +12,7 @@ export default function PalmCard({
imageUrl, imageUrl,
title, title,
type, type,
minutes minutes,
}: PalmCardProps) { }: PalmCardProps) {
return ( return (
<Card className={styles.card}> <Card className={styles.card}>
@ -30,21 +30,23 @@ export default function PalmCard({
<Typography size="lg" align="left"> <Typography size="lg" align="left">
{title} {title}
</Typography> </Typography>
<MetaLabel iconLabelProps={{ <MetaLabel
iconLabelProps={{
iconProps: { iconProps: {
name: IconName.Video, name: IconName.Video,
color: "#6B7280", color: "#6B7280",
size: { size: {
width: 24, width: 24,
height: 25 height: 25,
}
}, },
children: <Typography color="secondary">{type}</Typography> },
}}> children: <Typography color="secondary">{type}</Typography>,
}}
>
{minutes} min {minutes} min
</MetaLabel> </MetaLabel>
</div> </div>
</div> </div>
</Card> </Card>
) );
} }

View File

@ -1,4 +1,4 @@
export { default as AdviserCard } from './AdviserCard/AdviserCard'; export { default as AdviserCard } from "./AdviserCard/AdviserCard";
export { default as CompatibilityCard } from './CompatibilityCard/CompatibilityCard'; export { default as CompatibilityCard } from "./CompatibilityCard/CompatibilityCard";
export { default as MeditationCard } from './MeditationCard/MeditationCard'; export { default as MeditationCard } from "./MeditationCard/MeditationCard";
export { default as PalmCard } from './PalmCard/PalmCard'; export { default as PalmCard } from "./PalmCard/PalmCard";

View File

@ -1,2 +1,2 @@
export * from './cards'; export * from "./cards";
export * from './sections'; export * from "./sections";

View File

@ -7,19 +7,23 @@ import { AdviserCard } from "../../cards";
import styles from "./AdvisersSection.module.scss"; import styles from "./AdvisersSection.module.scss";
export default function AdvisersSection({ promise }: { promise: Promise<Assistant[]> }) { export default function AdvisersSection({
promise,
}: {
promise: Promise<Assistant[]>;
}) {
const assistants = use(promise); const assistants = use(promise);
const columns = Math.ceil(assistants?.length / 2); const columns = Math.ceil(assistants?.length / 2);
return ( return (
<Section title="Advisers" contentClassName={styles.sectionContent}> <Section title="Advisers" contentClassName={styles.sectionContent}>
<Grid columns={columns} className={styles.grid}> <Grid columns={columns} className={styles.grid}>
{assistants.map((adviser) => ( {assistants.map(adviser => (
<AdviserCard key={adviser._id} {...adviser} /> <AdviserCard key={adviser._id} {...adviser} />
))} ))}
</Grid> </Grid>
</Section> </Section>
) );
} }
export function AdvisersSectionSkeleton() { export function AdvisersSectionSkeleton() {
@ -27,5 +31,5 @@ export function AdvisersSectionSkeleton() {
<Section title="Advisers" contentClassName={styles.sectionContent}> <Section title="Advisers" contentClassName={styles.sectionContent}>
<Skeleton className={styles.skeleton} /> <Skeleton className={styles.skeleton} />
</Section> </Section>
) );
} }

View File

@ -1,25 +1,36 @@
import { use } from "react"; import { use } from "react";
import Link from "next/link";
import { Grid, Section, Skeleton } from "@/components/ui"; import { Grid, Section, Skeleton } from "@/components/ui";
import { CompatibilityAction } from "@/entities/dashboard/types"; import { CompatibilityAction } from "@/entities/dashboard/types";
import { ROUTES } from "@/shared/constants/client-routes";
import { CompatibilityCard } from "../../cards"; import { CompatibilityCard } from "../../cards";
import styles from "./CompatibilitySection.module.scss"; import styles from "./CompatibilitySection.module.scss";
export default function CompatibilitySection({ promise }: { promise: Promise<CompatibilityAction[]> }) { export default function CompatibilitySection({
promise,
}: {
promise: Promise<CompatibilityAction[]>;
}) {
const compatibilities = use(promise); const compatibilities = use(promise);
const columns = Math.ceil(compatibilities?.length / 2); const columns = Math.ceil(compatibilities?.length / 2);
return ( return (
<Section title="Compatibility" contentClassName={styles.sectionContent}> <Section title="Compatibility" contentClassName={styles.sectionContent}>
<Grid columns={columns} className={styles.grid}> <Grid columns={columns} className={styles.grid}>
{compatibilities.map((compatibility) => ( {compatibilities.map(compatibility => (
<Link
href={ROUTES.compatibility(compatibility._id)}
key={compatibility._id}
>
<CompatibilityCard key={compatibility._id} {...compatibility} /> <CompatibilityCard key={compatibility._id} {...compatibility} />
</Link>
))} ))}
</Grid> </Grid>
</Section> </Section>
) );
} }
export function CompatibilitySectionSkeleton() { export function CompatibilitySectionSkeleton() {
@ -27,5 +38,5 @@ export function CompatibilitySectionSkeleton() {
<Section title="Compatibility" contentClassName={styles.sectionContent}> <Section title="Compatibility" contentClassName={styles.sectionContent}>
<Skeleton className={styles.skeleton} /> <Skeleton className={styles.skeleton} />
</Section> </Section>
) );
} }

View File

@ -7,19 +7,23 @@ import { MeditationCard } from "../../cards";
import styles from "./MeditationSection.module.scss"; import styles from "./MeditationSection.module.scss";
export default function MeditationSection({ promise }: { promise: Promise<Meditation[]> }) { export default function MeditationSection({
promise,
}: {
promise: Promise<Meditation[]>;
}) {
const meditations = use(promise); const meditations = use(promise);
const columns = meditations?.length; const columns = meditations?.length;
return ( return (
<Section title="Meditations" contentClassName={styles.sectionContent}> <Section title="Meditations" contentClassName={styles.sectionContent}>
<Grid columns={columns} className={styles.grid}> <Grid columns={columns} className={styles.grid}>
{meditations.map((meditation) => ( {meditations.map(meditation => (
<MeditationCard key={meditation._id} {...meditation} /> <MeditationCard key={meditation._id} {...meditation} />
))} ))}
</Grid> </Grid>
</Section> </Section>
) );
} }
export function MeditationSectionSkeleton() { export function MeditationSectionSkeleton() {
@ -27,5 +31,5 @@ export function MeditationSectionSkeleton() {
<Section title="Meditations" contentClassName={styles.sectionContent}> <Section title="Meditations" contentClassName={styles.sectionContent}>
<Skeleton className={styles.skeleton} /> <Skeleton className={styles.skeleton} />
</Section> </Section>
) );
} }

View File

@ -1,25 +1,33 @@
import { use } from "react"; import { use } from "react";
import Link from "next/link";
import { Grid, Section, Skeleton } from "@/components/ui"; import { Grid, Section, Skeleton } from "@/components/ui";
import { PalmAction } from "@/entities/dashboard/types"; import { PalmAction } from "@/entities/dashboard/types";
import { ROUTES } from "@/shared/constants/client-routes";
import { PalmCard } from "../../cards"; import { PalmCard } from "../../cards";
import styles from "./PalmSection.module.scss"; import styles from "./PalmSection.module.scss";
export default function PalmSection({ promise }: { promise: Promise<PalmAction[]> }) { export default function PalmSection({
promise,
}: {
promise: Promise<PalmAction[]>;
}) {
const palms = use(promise); const palms = use(promise);
const columns = palms?.length; const columns = palms?.length;
return ( return (
<Section title="Palm" contentClassName={styles.sectionContent}> <Section title="Palm" contentClassName={styles.sectionContent}>
<Grid columns={columns} className={styles.grid}> <Grid columns={columns} className={styles.grid}>
{palms.map((palm) => ( {palms.map(palm => (
<Link href={ROUTES.palmistryResult(palm._id)} key={palm._id}>
<PalmCard key={palm._id} {...palm} /> <PalmCard key={palm._id} {...palm} />
</Link>
))} ))}
</Grid> </Grid>
</Section> </Section>
) );
} }
export function PalmSectionSkeleton() { export function PalmSectionSkeleton() {
@ -27,5 +35,5 @@ export function PalmSectionSkeleton() {
<Section title="Palm" contentClassName={styles.sectionContent}> <Section title="Palm" contentClassName={styles.sectionContent}>
<Skeleton className={styles.skeleton} /> <Skeleton className={styles.skeleton} />
</Section> </Section>
) );
} }

View File

@ -1,4 +1,16 @@
export { default as AdvisersSection, AdvisersSectionSkeleton } from './AdvisersSection/AdvisersSection'; export {
export { default as CompatibilitySection, CompatibilitySectionSkeleton } from './CompatibilitySection/CompatibilitySection'; default as AdvisersSection,
export { default as MeditationSection, MeditationSectionSkeleton } from './MeditationSection/MeditationSection'; AdvisersSectionSkeleton,
export { default as PalmSection, PalmSectionSkeleton } from './PalmSection/PalmSection'; } from "./AdvisersSection/AdvisersSection";
export {
default as CompatibilitySection,
CompatibilitySectionSkeleton,
} from "./CompatibilitySection/CompatibilitySection";
export {
default as MeditationSection,
MeditationSectionSkeleton,
} from "./MeditationSection/MeditationSection";
export {
default as PalmSection,
PalmSectionSkeleton,
} from "./PalmSection/PalmSection";

View File

@ -0,0 +1,15 @@
.loadingContainer {
display: flex;
justify-content: center;
align-items: center;
height: calc(100dvh - 56px);
}
.title {
line-height: 30px;
}
.description {
line-height: 25px;
margin-top: 24px;
}

View File

@ -0,0 +1,40 @@
"use client";
import { useTranslations } from "next-intl";
import { Spinner, Toast, Typography } from "@/components/ui";
import { useGenerationPolling } from "@/hooks/generation/useGenerationPolling";
import styles from "./PalmistryResultPage.module.scss";
interface PalmistryResultPageProps {
id: string;
}
export default function PalmistryResultPage({ id }: PalmistryResultPageProps) {
const t = useTranslations("PalmistryResult");
const { data, error, isLoading } = useGenerationPolling(id);
if (isLoading) {
return (
<div className={styles.loadingContainer}>
<Spinner />
</div>
);
}
if (error) {
return <Toast variant="error">{t("error")}</Toast>;
}
return (
<>
<Typography as="h1" size="xl" weight="semiBold" className={styles.title}>
{t("title")}
</Typography>
<Typography as="p" size="lg" align="left" className={styles.description}>
{data?.result}
</Typography>
</>
);
}

View File

@ -1,25 +1,25 @@
"use client;" "use client";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { Button, Typography } from "@/components/ui"; import { Button, Typography } from "@/components/ui";
import { ROUTES } from "@/shared/constants/client-routes";
import styles from "./Billing.module.scss" import styles from "./Billing.module.scss";
interface IBillingProps { function Billing() {
onBilling: () => void; const t = useTranslations("Profile.billing");
} const router = useRouter();
function Billing({ onBilling }: IBillingProps) { const onBilling = () => {
const t = useTranslations('Profile.billing'); router.push(ROUTES.profileSubscriptions());
};
return ( return (
<div className={styles.container}> <div className={styles.container}>
<Button <Button className={styles.button} onClick={onBilling}>
className={styles.button}
onClick={onBilling}
>
<Typography size="xl" color="white"> <Typography size="xl" color="white">
{t("billing_button")} {t("billing_button")}
</Typography> </Typography>
@ -27,16 +27,27 @@ function Billing({ onBilling }: IBillingProps) {
<div className={styles.credits}> <div className={styles.credits}>
<Typography as="p" weight="bold" color="white" align="left"> <Typography as="p" weight="bold" color="white" align="left">
{t("credits.title", { {t("credits.title", {
credits: String(0) credits: String(0),
})} })}
</Typography> </Typography>
<Typography className={styles.creditsDescription} as="p" size="sm" color="white" align="left"> <Typography
className={styles.creditsDescription}
as="p"
size="sm"
color="white"
align="left"
>
{t("credits.description")} {t("credits.description")}
</Typography> </Typography>
</div> </div>
<Typography as="p" weight="bold" align="left" className={styles.anyQuestions}> <Typography
as="p"
weight="bold"
align="left"
className={styles.anyQuestions}
>
{t.rich("any_questions", { {t.rich("any_questions", {
link: (chunks) => ( link: chunks => (
<Link <Link
href="https://witapps.us/en#contact-us" href="https://witapps.us/en#contact-us"
target="_blank" target="_blank"
@ -45,20 +56,27 @@ function Billing({ onBilling }: IBillingProps) {
{chunks} {chunks}
</Link> </Link>
), ),
linkText: t("any_questions_link") linkText: t("any_questions_link"),
})} })}
</Typography> </Typography>
<Typography as="p" align="left" color="secondary" className={styles.subscriptionUpdate}> <Typography
as="p"
align="left"
color="secondary"
className={styles.subscriptionUpdate}
>
{t.rich("subscription_update", { {t.rich("subscription_update", {
bold: (chunks) => ( bold: chunks => (
<Typography weight="bold" color="secondary">{chunks}</Typography> <Typography weight="bold" color="secondary">
{chunks}
</Typography>
), ),
subscriptionUpdateBold: t("subscription_update_bold"), subscriptionUpdateBold: t("subscription_update_bold"),
br: () => <br /> br: () => <br />,
})} })}
</Typography> </Typography>
</div> </div>
) );
} }
export default Billing export default Billing;

View File

@ -1,3 +1,42 @@
.button { .button {
min-height: 60px; min-height: 60px;
} }
.modal-container {
max-width: 290px;
padding: 24px 0px 0px;
overflow: hidden;
}
.modal-title {
font-weight: 600;
margin-bottom: 16px;
padding-inline: 24px;
}
.modal-description {
padding-inline: 24px;
text-align: center;
}
.modal-answers {
display: flex;
flex-direction: row;
margin-top: 24px;
border-top: 1px solid #d9d9d9;
}
.modal-answer {
width: 50%;
height: 52px;
display: flex;
align-items: center;
justify-content: center;
color: #275da7;
font-weight: 600;
cursor: pointer;
&:first-child {
border-right: 1px solid #d9d9d9;
}
}

View File

@ -1,24 +1,70 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { Button, Typography } from "@/components/ui"; import { Button, Modal, Typography } from "@/components/ui";
import { ROUTES } from "@/shared/constants/client-routes";
import styles from "./LogOut.module.scss" import styles from "./LogOut.module.scss";
interface ILogOutProps { function LogOut() {
onLogout: () => void; const t = useTranslations("Profile.log_out");
} const router = useRouter();
function LogOut({ onLogout }: ILogOutProps) { const [logoutModal, setLogoutModal] = useState(false);
const t = useTranslations('Profile.log_out');
const handleLogout = () => {
router.replace(ROUTES.home());
// logout();
};
const onLogoutButton = () => {
setLogoutModal(true);
};
return ( return (
<Button <>
className={styles.button} <Button className={styles.button} onClick={onLogoutButton}>
onClick={onLogout} <Typography size="xl" color="white">
> {t("log_out_button")}
<Typography size="xl" color="white">{t("log_out_button")}</Typography> </Typography>
</Button> </Button>
)
{logoutModal && (
<Modal
isCloseButtonVisible={false}
open={!!logoutModal}
onClose={() => setLogoutModal(false)}
className={styles.modal}
modalClassName={styles["modal-container"]}
>
<Typography as="h4" className={styles["modal-title"]}>
{t("modal.title")}
</Typography>
<p className={styles["modal-description"]}>
{t("modal.description")}
</p>
<div className={styles["modal-answers"]}>
<div className={styles["modal-answer"]} onClick={handleLogout}>
<p className={styles["modal-answer-text"]}>
{t("modal.log_out_button")}
</p>
</div>
<div
className={styles["modal-answer"]}
onClick={() => setLogoutModal(false)}
>
<p className={styles["modal-answer-text"]}>
{t("modal.stay_button")}
</p>
</div>
</div>
</Modal>
)}
</>
);
} }
export default LogOut export default LogOut;

View File

@ -1,31 +1,35 @@
import { Typography } from "@/components/ui" import { Typography } from "@/components/ui";
import styles from "./ProfileBlock.module.scss" import styles from "./ProfileBlock.module.scss";
interface ProfileBlockProps { interface ProfileBlockProps {
title: string title: string;
description?: string description?: string;
children?: React.ReactNode children?: React.ReactNode;
} }
function ProfileBlock({ title, description, children }: ProfileBlockProps) { function ProfileBlock({ title, description, children }: ProfileBlockProps) {
return ( return (
<section className={styles.container}> <section className={styles.container}>
<header className={styles.header}> <header className={styles.header}>
<Typography className={styles.title} as="h2" size="xl" weight="semiBold" align="left"> <Typography
className={styles.title}
as="h2"
size="xl"
weight="semiBold"
align="left"
>
{title} {title}
</Typography> </Typography>
{description && {description && (
<Typography className={styles.description} size="sm" align="left"> <Typography className={styles.description} size="sm" align="left">
{description} {description}
</Typography> </Typography>
} )}
</header> </header>
{!!children && <div className={styles.content}> {!!children && <div className={styles.content}>{children}</div>}
{children}
</div>}
</section> </section>
) );
} }
export default ProfileBlock export default ProfileBlock;

View File

@ -1,17 +1,24 @@
/* eslint-disable @typescript-eslint/no-empty-function */
"use client"; "use client";
import { useTranslations } from "next-intl" import { use } from "react";
import { useTranslations } from "next-intl";
import { EmailInput, NameInput } from "@/components/ui"; import { EmailInput, NameInput, Skeleton } from "@/components/ui";
import { IUser } from "@/entities/user/types";
import styles from "./ProfileInformation.module.scss" import styles from "./ProfileInformation.module.scss";
function ProfileInformation() { interface IProfileInformationProps {
const t = useTranslations('Profile'); user: Promise<IUser>;
// const email = useSelector(selectors.selectEmail) || "" }
// const name = useSelector(selectors.selectUser)?.username || ""
const email = "Test email" export default function ProfileInformation({ user }: IProfileInformationProps) {
const name = "Test name" const userData = use(user);
const t = useTranslations("Profile");
const email = userData?.email || "";
const name = userData?.profile?.name || "";
return ( return (
<div className={styles.container}> <div className={styles.container}>
@ -35,7 +42,13 @@ function ProfileInformation() {
readonly readonly
/> />
</div> </div>
) );
} }
export default ProfileInformation export function ProfileInformationSkeleton() {
return (
<div className={styles.container}>
<Skeleton style={{ height: "136px" }} />
</div>
);
}

View File

@ -1,4 +1,7 @@
export { default as Billing } from "./Billing/Billing" export { default as Billing } from "./Billing/Billing";
export { default as LogOut } from "./LogOut/LogOut" export { default as LogOut } from "./LogOut/LogOut";
export { default as ProfileBlock } from "./ProfileBlock/ProfileBlock" export { default as ProfileBlock } from "./ProfileBlock/ProfileBlock";
export { default as ProfileInformation } from "./ProfileInformation/ProfileInformation" export {
default as ProfileInformation,
ProfileInformationSkeleton,
} from "./ProfileInformation/ProfileInformation";

View File

@ -0,0 +1,41 @@
.modal {
max-width: 290px;
padding: 24px 0px 0px;
overflow: hidden;
}
.title {
font-weight: 600;
margin-bottom: 16px;
padding-inline: 24px;
}
.description {
padding-inline: 24px;
text-align: center;
}
.actions {
display: flex;
flex-direction: row;
margin-top: 24px;
border-top: 1px solid #d9d9d9;
}
.action.action {
width: 50%;
height: 52px;
display: flex;
align-items: center;
justify-content: center;
color: #275da7;
font-weight: 600;
cursor: pointer;
text-align: center;
background-color: transparent;
border-radius: 0px;
&:first-child {
border-right: 1px solid #d9d9d9;
}
}

View File

@ -1,6 +1,12 @@
"use client"; "use client";
import { createContext, ReactNode,useContext, useState } from "react"; import {
createContext,
ReactNode,
useCallback,
useContext,
useState,
} from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
@ -8,6 +14,7 @@ import { Button, Typography } from "@/components/ui";
import Modal from "@/components/ui/Modal/Modal"; import Modal from "@/components/ui/Modal/Modal";
import { UserSubscription } from "@/entities/subscriptions/types"; import { UserSubscription } from "@/entities/subscriptions/types";
import { ROUTES } from "@/shared/constants/client-routes"; import { ROUTES } from "@/shared/constants/client-routes";
import { useRetainingActions } from "@/stores/retainingStore";
import styles from "./CancelSubscriptionModalProvider.module.scss"; import styles from "./CancelSubscriptionModalProvider.module.scss";
@ -16,7 +23,8 @@ type Ctx = { open: (sub: UserSubscription) => void };
const Context = createContext<Ctx | null>(null); const Context = createContext<Ctx | null>(null);
export const useCancelSubscriptionModal = () => { export const useCancelSubscriptionModal = () => {
const ctx = useContext(Context); const ctx = useContext(Context);
if (!ctx) throw new Error("useCancelSubscriptionModal must be inside provider"); if (!ctx)
throw new Error("useCancelSubscriptionModal must be inside provider");
return ctx; return ctx;
}; };
@ -25,21 +33,28 @@ export default function CancelSubscriptionModalProvider({
}: { }: {
children: ReactNode; children: ReactNode;
}) { }) {
const router = useRouter() const router = useRouter();
const t = useTranslations("Subscriptions"); const t = useTranslations("Subscriptions");
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const { setCancellingSubscription } = useRetainingActions();
const close = () => setIsOpen(false); const close = useCallback(() => setIsOpen(false), []);
const open = ( const open = useCallback(
// _sub: UserSubscription (subscription: UserSubscription) => {
) => { setCancellingSubscription(subscription);
setIsOpen(true) setIsOpen(true);
}; },
[setCancellingSubscription]
);
const handleCancel = () => { const handleCancel = useCallback(() => {
router.push(ROUTES.retainingFunnelCancelSubscription()) router.push(ROUTES.retainingFunnelCancelSubscription());
close(); close();
}; }, [router, close]);
const handleStay = useCallback(() => {
close();
}, [close]);
return ( return (
<Context.Provider value={{ open }}> <Context.Provider value={{ open }}>
@ -60,12 +75,10 @@ export default function CancelSubscriptionModalProvider({
</Typography> </Typography>
<div className={styles.actions}> <div className={styles.actions}>
<Button onClick={handleCancel}>{t("modal.cancel_button")}</Button> <Button className={styles.action} onClick={handleCancel}>
<Button {t("modal.cancel_button")}
variant="secondary" </Button>
onClick={close} <Button onClick={handleStay} className={styles.action}>
className={styles.stayButton}
>
{t("modal.stay_button")} {t("modal.stay_button")}
</Button> </Button>
</div> </div>

View File

@ -1,9 +1,9 @@
"use client"; "use client";
import { ReactNode } from "react"; import { ReactNode, useMemo } from "react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { Button } from "@/components/ui"; import { Button, Typography } from "@/components/ui";
import { Table } from "@/components/widgets"; import { Table } from "@/components/widgets";
import { UserSubscription } from "@/entities/subscriptions/types"; import { UserSubscription } from "@/entities/subscriptions/types";
import { formatDate } from "@/shared/utils/date"; import { formatDate } from "@/shared/utils/date";
@ -12,7 +12,7 @@ import { Currency } from "@/types";
import { useCancelSubscriptionModal } from "../CancelSubscriptionModalProvider/CancelSubscriptionModalProvider"; import { useCancelSubscriptionModal } from "../CancelSubscriptionModalProvider/CancelSubscriptionModalProvider";
import styles from "./SubscriptionTable.module.scss" import styles from "./SubscriptionTable.module.scss";
interface ITableProps { interface ITableProps {
subscription: UserSubscription; subscription: UserSubscription;
@ -22,26 +22,58 @@ export default function SubscriptionTable({ subscription }: ITableProps) {
const t = useTranslations("Subscriptions"); const t = useTranslations("Subscriptions");
const { open } = useCancelSubscriptionModal(); const { open } = useCancelSubscriptionModal();
const tableData: ReactNode[][] = [ const tableData: ReactNode[][] = useMemo(() => {
[t("table.subscription_type"), t(`table.subscription_type_value.${subscription.subscriptionType}`)], const data: ReactNode[][] = [
[t("table.subscription_status"), t(`table.subscription_status_value.${subscription.subscriptionStatus}`, { [
date: formatDate(subscription.cancellationDate) || "" t("table.subscription_type"),
})], t(`table.subscription_type_value.${subscription.subscriptionType}`),
[t("table.billing_period"), t(`table.billing_period_value.${subscription.billingPeriod}`)], ],
[
t("table.subscription_status"),
t(
`table.subscription_status_value.${subscription.subscriptionStatus}`,
{
date: formatDate(subscription.cancellationDate) || "",
}
),
],
[
t("table.billing_period"),
t(`table.billing_period_value.${subscription.billingPeriod}`),
],
[t("table.last_payment_on"), formatDate(subscription.lastPaymentOn)], [t("table.last_payment_on"), formatDate(subscription.lastPaymentOn)],
[t("table.renewal_date"), formatDate(subscription.renewalDate)], [t("table.renewal_date"), formatDate(subscription.renewalDate)],
[t("table.renewal_amount"), getFormattedPrice(subscription.renewalAmount, Currency[subscription.currency])], [
] t("table.renewal_amount"),
getFormattedPrice(
subscription.renewalAmount,
Currency[subscription.currency]
),
],
];
if (subscription.subscriptionStatus === "ACTIVE") { if (subscription.subscriptionStatus === "ACTIVE") {
tableData.push([ data.push([
<Button key={"cancel-subscription"} className={styles.buttonCancel} onClick={() => open(subscription)}> <Button
key={"cancel-subscription"}
className={styles.buttonCancel}
onClick={() => open(subscription)}
>
<Typography color="white">
{t("table.cancel_subscription")} {t("table.cancel_subscription")}
</Button> </Typography>
]) </Button>,
]);
} }
return ( return data;
<Table data={tableData} /> }, [subscription, t, open]);
)
// const tableData: ReactNode[][] = [
// [t("table.subscription_status"), t(`table.subscription_status_value.${subscription.subscriptionStatus}`, {
// date: formatDate(subscription.cancellationDate) || ""
// })],
// ]
return <Table data={tableData} />;
} }

View File

@ -1,37 +1,50 @@
import { use } from "react"; import { use } from "react";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { Typography } from "@/components/ui"; import { Skeleton, Typography } from "@/components/ui";
import { Skeleton } from "@/components/ui";
import { UserSubscription } from "@/entities/subscriptions/types"; import { UserSubscription } from "@/entities/subscriptions/types";
import SubscriptionTable from "../SubscriptionTable/SubscriptionTable"; import SubscriptionTable from "../SubscriptionTable/SubscriptionTable";
import styles from "./SubscriptionsList.module.scss" import styles from "./SubscriptionsList.module.scss";
export default function SubscriptionsList( export default function SubscriptionsList({
{ promise }: { promise: Promise<UserSubscription[]> } promise,
) { }: {
promise: Promise<UserSubscription[]>;
}) {
const t = use(getTranslations("Subscriptions")); const t = use(getTranslations("Subscriptions"));
const subscriptions = use(promise); const subscriptions = use(promise);
if (subscriptions?.length === 0) { if (subscriptions?.length === 0) {
return <div className={styles.container}> return (
<Typography as="h1" className={styles.title}>{t("title")}</Typography> <div className={styles.container}>
<Typography as="p" className={styles.description}>{t("no_subscriptions")}</Typography> <Typography as="h1" className={styles.title}>
{t("title")}
</Typography>
<Typography as="p" className={styles.description}>
{t("no_subscriptions")}
</Typography>
</div> </div>
);
} }
return <> return (
{subscriptions.map((subscription) => { <>
return <SubscriptionTable subscription={subscription} key={subscription.id} /> {subscriptions.map(subscription => {
return (
<SubscriptionTable
subscription={subscription}
key={subscription.id}
/>
);
})} })}
{/* <SubscriptionTable subscription={subscriptions[0]} key={subscriptions[0].id} /> */}
</> </>
);
} }
export function SubscriptionsListSkeleton() { export function SubscriptionsListSkeleton() {
return ( return <Skeleton style={{ height: "300px" }} className={styles.skeleton} />;
<Skeleton style={{ height: "300px" }} className={styles.skeleton} />
)
} }

View File

@ -1,3 +1,5 @@
export { default as CancelSubscriptionModalProvider } from "./CancelSubscriptionModalProvider/CancelSubscriptionModalProvider";
export { default as CancelSubscriptionModalProvider } from "./CancelSubscriptionModalProvider/CancelSubscriptionModalProvider" export {
export { default as SubscriptionsList, SubscriptionsListSkeleton } from "./SubscriptionsList/SubscriptionsList" default as SubscriptionsList,
SubscriptionsListSkeleton,
} from "./SubscriptionsList/SubscriptionsList";

View File

@ -3,11 +3,13 @@
border-radius: 8px; border-radius: 8px;
font-size: 28px; font-size: 28px;
font-weight: normal; font-weight: normal;
background: #F1F1F1; background: #f1f1f1;
background-blend-mode: color; background-blend-mode: color;
box-shadow: 2px 5px 2.5px #00000025; box-shadow: 2px 5px 2.5px #00000025;
color: #121620; color: #121620;
transition: background 0.3s ease, color 0.3s ease; transition:
background 0.3s ease,
color 0.3s ease;
will-change: background, color; will-change: background, color;
padding: 25px; padding: 25px;
line-height: 1; line-height: 1;
@ -15,7 +17,8 @@
min-width: none; min-width: none;
&.active { &.active {
background: linear-gradient(to right, #057dd4 23%, #224e90 74%, #0c6bc3 94%), background:
linear-gradient(to right, #057dd4 23%, #224e90 74%, #0c6bc3 94%),
linear-gradient(-45deg, #3a617120 9%, #21212120 72%, #21895120 96%); linear-gradient(-45deg, #3a617120 9%, #21212120 72%, #21895120 96%);
color: #fff; color: #fff;
} }

View File

@ -1,4 +1,6 @@
import MainButton, { ButtonProps as MainButtonProps } from "@/components/ui/Button/Button"; import MainButton, {
ButtonProps as MainButtonProps,
} from "@/components/ui/Button/Button";
import styles from "./Button.module.scss"; import styles from "./Button.module.scss";
@ -9,7 +11,10 @@ interface ButtonProps extends MainButtonProps {
function Button(props: ButtonProps) { function Button(props: ButtonProps) {
const { active, ...buttonProps } = props; const { active, ...buttonProps } = props;
return ( return (
<MainButton {...buttonProps} className={`${styles.button} ${props.className} ${active ? styles.active : ""}`}> <MainButton
{...buttonProps}
className={`${styles.button} ${props.className} ${active ? styles.active : ""}`}
>
{props.children} {props.children}
</MainButton> </MainButton>
); );

View File

@ -1,5 +1,3 @@
interface CheckMarkProps { interface CheckMarkProps {
active: boolean; active: boolean;
className?: string; className?: string;
@ -8,14 +6,32 @@ interface CheckMarkProps {
function CheckMark({ active, className = "" }: CheckMarkProps) { function CheckMark({ active, className = "" }: CheckMarkProps) {
return ( return (
<> <>
{active && {active && (
<svg className={className} width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg
className={className}
width="30"
height="30"
viewBox="0 0 30 30"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clipPath="url(#clip0_49_4129)"> <g clipPath="url(#clip0_49_4129)">
<g clipPath="url(#clip1_49_4129)"> <g clipPath="url(#clip1_49_4129)">
<path d="M25 0H5C3.67392 0 2.40215 0.526784 1.46447 1.46447C0.526784 2.40215 0 3.67392 0 5V25C0 26.3261 0.526784 27.5979 1.46447 28.5355C2.40215 29.4732 3.67392 30 5 30H25C26.3261 30 27.5979 29.4732 28.5355 28.5355C29.4732 27.5979 30 26.3261 30 25V5C30 3.67392 29.4732 2.40215 28.5355 1.46447C27.5979 0.526784 26.3261 0 25 0ZM22.1667 11.0167L14.55 21.0167C14.3947 21.2184 14.1953 21.3818 13.9671 21.4945C13.7389 21.6072 13.4879 21.6661 13.2333 21.6667C12.9802 21.668 12.73 21.6117 12.5019 21.502C12.2738 21.3922 12.0736 21.232 11.9167 21.0333L7.85 15.85C7.71539 15.6771 7.61616 15.4794 7.55797 15.2681C7.49978 15.0569 7.48377 14.8362 7.51086 14.6188C7.53794 14.4013 7.60759 14.1913 7.71582 14.0008C7.82406 13.8103 7.96876 13.6429 8.14167 13.5083C8.49087 13.2365 8.93376 13.1145 9.37291 13.1692C9.59035 13.1963 9.80033 13.2659 9.99086 13.3742C10.1814 13.4824 10.3487 13.6271 10.4833 13.8L13.2 17.2667L19.5 8.93333C19.6335 8.75824 19.8002 8.61115 19.9906 8.50048C20.1809 8.3898 20.3912 8.3177 20.6094 8.2883C20.8276 8.25889 21.0495 8.27276 21.2624 8.3291C21.4752 8.38544 21.6749 8.48316 21.85 8.61667C22.0251 8.75018 22.1722 8.91687 22.2829 9.10722C22.3935 9.29758 22.4656 9.50786 22.495 9.72608C22.5244 9.9443 22.5106 10.1662 22.4542 10.379C22.3979 10.5919 22.3002 10.7916 22.1667 10.9667V11.0167Z" fill="#1172AC" /> <path
d="M25 0H5C3.67392 0 2.40215 0.526784 1.46447 1.46447C0.526784 2.40215 0 3.67392 0 5V25C0 26.3261 0.526784 27.5979 1.46447 28.5355C2.40215 29.4732 3.67392 30 5 30H25C26.3261 30 27.5979 29.4732 28.5355 28.5355C29.4732 27.5979 30 26.3261 30 25V5C30 3.67392 29.4732 2.40215 28.5355 1.46447C27.5979 0.526784 26.3261 0 25 0ZM22.1667 11.0167L14.55 21.0167C14.3947 21.2184 14.1953 21.3818 13.9671 21.4945C13.7389 21.6072 13.4879 21.6661 13.2333 21.6667C12.9802 21.668 12.73 21.6117 12.5019 21.502C12.2738 21.3922 12.0736 21.232 11.9167 21.0333L7.85 15.85C7.71539 15.6771 7.61616 15.4794 7.55797 15.2681C7.49978 15.0569 7.48377 14.8362 7.51086 14.6188C7.53794 14.4013 7.60759 14.1913 7.71582 14.0008C7.82406 13.8103 7.96876 13.6429 8.14167 13.5083C8.49087 13.2365 8.93376 13.1145 9.37291 13.1692C9.59035 13.1963 9.80033 13.2659 9.99086 13.3742C10.1814 13.4824 10.3487 13.6271 10.4833 13.8L13.2 17.2667L19.5 8.93333C19.6335 8.75824 19.8002 8.61115 19.9906 8.50048C20.1809 8.3898 20.3912 8.3177 20.6094 8.2883C20.8276 8.25889 21.0495 8.27276 21.2624 8.3291C21.4752 8.38544 21.6749 8.48316 21.85 8.61667C22.0251 8.75018 22.1722 8.91687 22.2829 9.10722C22.3935 9.29758 22.4656 9.50786 22.495 9.72608C22.5244 9.9443 22.5106 10.1662 22.4542 10.379C22.3979 10.5919 22.3002 10.7916 22.1667 10.9667V11.0167Z"
fill="#1172AC"
/>
</g> </g>
</g> </g>
<rect x="1" y="1" width="28" height="28" rx="14" stroke="#1172AC" strokeWidth="2" /> <rect
x="1"
y="1"
width="28"
height="28"
rx="14"
stroke="#1172AC"
strokeWidth="2"
/>
<defs> <defs>
<clipPath id="clip0_49_4129"> <clipPath id="clip0_49_4129">
<rect width="30" height="30" rx="15" fill="white" /> <rect width="30" height="30" rx="15" fill="white" />
@ -25,21 +41,35 @@ function CheckMark({ active, className = "" }: CheckMarkProps) {
</clipPath> </clipPath>
</defs> </defs>
</svg> </svg>
} )}
{!active && {!active && (
<svg className={className} width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg
<g clipPath="url(#clip0_49_4525)"> className={className}
</g> width="30"
<rect x="1" y="1" width="28" height="28" rx="14" stroke="#1172AC" strokeWidth="2" /> height="30"
viewBox="0 0 30 30"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clipPath="url(#clip0_49_4525)" />
<rect
x="1"
y="1"
width="28"
height="28"
rx="14"
stroke="#1172AC"
strokeWidth="2"
/>
<defs> <defs>
<clipPath id="clip0_49_4525"> <clipPath id="clip0_49_4525">
<rect width="30" height="30" rx="15" fill="white" /> <rect width="30" height="30" rx="15" fill="white" />
</clipPath> </clipPath>
</defs> </defs>
</svg> </svg>
} )}
</> </>
) );
} }
export default CheckMark export default CheckMark;

View File

@ -11,7 +11,7 @@
cursor: pointer; cursor: pointer;
&.active { &.active {
border: 4px solid rgba(17, 114, 172, 1) border: 4px solid rgba(17, 114, 172, 1);
} }
& > .checkMark { & > .checkMark {
@ -27,7 +27,6 @@
font-size: 28px; font-size: 28px;
} }
& > .description { & > .description {
margin-top: 14px; margin-top: 14px;
color: #323232; color: #323232;
@ -45,7 +44,7 @@
margin-top: 33px; margin-top: 33px;
& > .oldPrice { & > .oldPrice {
color: #C4C4C4; color: #c4c4c4;
font-size: 28px; font-size: 28px;
line-height: 1; line-height: 1;
text-align: center; text-align: center;

View File

@ -31,18 +31,25 @@ function Offer({
onClick, onClick,
image, image,
className = "", className = "",
classNameTitle = "" classNameTitle = "",
}: OfferProps) { }: OfferProps) {
// const currency = useSelector(selectors.selectCurrency); // const currency = useSelector(selectors.selectCurrency);
const currency = Currency.USD; const currency = Currency.USD;
return ( return (
<div className={clsx(styles.container, active && styles.active, className)} onClick={onClick}> <div
className={clsx(styles.container, active && styles.active, className)}
onClick={onClick}
>
<CheckMark active={active} className={styles.checkMark} /> <CheckMark active={active} className={styles.checkMark} />
{image} {image}
<Typography as="h3" weight="bold" className={clsx(styles.title, classNameTitle)}> <Typography
as="h3"
weight="bold"
className={clsx(styles.title, classNameTitle)}
>
{title} {title}
</Typography> </Typography>
<Typography as="p" className={styles.description}> <Typography as="p" className={styles.description}>
@ -57,7 +64,7 @@ function Offer({
</Typography> </Typography>
</div> </div>
</div> </div>
) );
} }
export default Offer export default Offer;

View File

@ -4,8 +4,7 @@ import { usePathname } from "next/navigation";
import { StepperBar } from "@/components/layout"; import { StepperBar } from "@/components/layout";
import styles from "./RetainingStepper.module.scss" import styles from "./RetainingStepper.module.scss";
export default function RetainingStepper({ export default function RetainingStepper({
stepperRoutes, stepperRoutes,

View File

@ -4,14 +4,14 @@ import { useState } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { Spinner, Typography } from "@/components/ui" import { Spinner, Typography } from "@/components/ui";
import { useLottie } from "@/hooks/lottie/useLottie"; import { useLottie } from "@/hooks/lottie/useLottie";
import { ROUTES } from "@/shared/constants/client-routes"; import { ROUTES } from "@/shared/constants/client-routes";
import { ELottieKeys } from "@/shared/constants/lottie"; import { ELottieKeys } from "@/shared/constants/lottie";
import { RetainingButton } from "../.." import { RetainingButton } from "../..";
import styles from "./Buttons.module.scss" import styles from "./Buttons.module.scss";
export default function Buttons() { export default function Buttons() {
const t = useTranslations("CancelSubscription"); const t = useTranslations("CancelSubscription");
@ -30,7 +30,7 @@ export default function Buttons() {
router.push(ROUTES.retainingFunnelAppreciateChoice()); router.push(ROUTES.retainingFunnelAppreciateChoice());
}, 1000); }, 1000);
return () => clearTimeout(timer); return () => clearTimeout(timer);
} };
const handleStayButtonClick = async () => { const handleStayButtonClick = async () => {
if (isLoadingButton) return; if (isLoadingButton) return;
@ -46,21 +46,31 @@ export default function Buttons() {
// dispatch(actions.retainingFunnel.setFunnel(ERetainingFunnel.Stay50)); // dispatch(actions.retainingFunnel.setFunnel(ERetainingFunnel.Stay50));
// } // }
router.push(ROUTES.retainingFunnelStay50Done()); router.push(ROUTES.retainingFunnelStay50Done());
} };
return ( return (
<div className={styles.buttons}> <div className={styles.buttons}>
<RetainingButton className={styles.button} active={activeButton === "stay"} onClick={handleStayButtonClick}> <RetainingButton
{isLoadingButton === "stay" && <div className={styles.loaderContainer}> className={styles.button}
active={activeButton === "stay"}
onClick={handleStayButtonClick}
>
{isLoadingButton === "stay" && (
<div className={styles.loaderContainer}>
<Spinner /> <Spinner />
</div>} </div>
)}
<Typography className={styles.buttonIcon}>🙋</Typography> <Typography className={styles.buttonIcon}>🙋</Typography>
{t("stay_button")} {t("stay_button")}
</RetainingButton> </RetainingButton>
<RetainingButton className={styles.button} active={activeButton === "cancel"} onClick={handleCancelButtonClick}> <RetainingButton
className={styles.button}
active={activeButton === "cancel"}
onClick={handleCancelButtonClick}
>
<Typography className={styles.buttonIcon}>🙅</Typography> <Typography className={styles.buttonIcon}>🙅</Typography>
{t("cancel_button")} {t("cancel_button")}
</RetainingButton> </RetainingButton>
</div> </div>
) );
} }

View File

@ -1 +1 @@
export { default as Buttons } from "./Buttons/Buttons" export { default as Buttons } from "./Buttons/Buttons";

View File

@ -21,7 +21,7 @@
} }
.buttonCancel { .buttonCancel {
color: #ACB0BA; color: #acb0ba;
font-size: 16px; font-size: 16px;
line-height: 25px; line-height: 25px;
background: none; background: none;

View File

@ -5,9 +5,9 @@ import { useTranslations } from "next-intl";
import { Spinner, Toast } from "@/components/ui"; import { Spinner, Toast } from "@/components/ui";
import { RetainingButton } from "../.." import { RetainingButton } from "../..";
import styles from "./Buttons.module.scss" import styles from "./Buttons.module.scss";
// import { useRouter } from "next/navigation"; // import { useRouter } from "next/navigation";
// import { ROUTES } from "@/shared/constants/client-routes"; // import { ROUTES } from "@/shared/constants/client-routes";
@ -16,8 +16,10 @@ export default function Buttons() {
// const router = useRouter(); // const router = useRouter();
const [isToastVisible, setIsToastVisible] = useState(false); const [isToastVisible, setIsToastVisible] = useState(false);
const [isLoadingOfferButton, setIsLoadingOfferButton] = useState<boolean>(false); const [isLoadingOfferButton, setIsLoadingOfferButton] =
const [isLoadingCancelButton, setIsLoadingCancelButton] = useState<boolean>(false); useState<boolean>(false);
const [isLoadingCancelButton, setIsLoadingCancelButton] =
useState<boolean>(false);
const handleOfferButtonClick = async () => { const handleOfferButtonClick = async () => {
if (isLoadingOfferButton || isLoadingCancelButton) return; if (isLoadingOfferButton || isLoadingCancelButton) return;
@ -30,7 +32,7 @@ export default function Buttons() {
// if (response.status === "success") { // if (response.status === "success") {
// navigate(routes.client.retainingFunnelSubscriptionStopped()); // navigate(routes.client.retainingFunnelSubscriptionStopped());
// } // }
} };
const handleCancelClick = async () => { const handleCancelClick = async () => {
if (isToastVisible || isLoadingOfferButton || isLoadingCancelButton) return; if (isToastVisible || isLoadingOfferButton || isLoadingCancelButton) return;
@ -48,7 +50,7 @@ export default function Buttons() {
// }, 7000); // }, 7000);
// return () => clearTimeout(timer); // return () => clearTimeout(timer);
// } // }
} };
return ( return (
<> <>
@ -57,26 +59,29 @@ export default function Buttons() {
active={true} active={true}
onClick={handleOfferButtonClick} onClick={handleOfferButtonClick}
> >
{isLoadingOfferButton && <div className={styles.loaderContainer}> {isLoadingOfferButton && (
<div className={styles.loaderContainer}>
<Spinner /> <Spinner />
</div>} </div>
)}
{t("offer_button")} {t("offer_button")}
</RetainingButton> </RetainingButton>
<RetainingButton <RetainingButton
className={styles.buttonCancel} className={styles.buttonCancel}
onClick={handleCancelClick} onClick={handleCancelClick}
> >
{isLoadingCancelButton && <div className={styles.loaderContainer}> {isLoadingCancelButton && (
<div className={styles.loaderContainer}>
<Spinner /> <Spinner />
</div>} </div>
)}
{t("cancel_button")} {t("cancel_button")}
</RetainingButton> </RetainingButton>
{isToastVisible && <Toast {isToastVisible && (
classNameContainer={styles.toast} <Toast classNameContainer={styles.toast} variant="success">
variant="success"
>
Ваша подписка будет аннулирована! Ваша подписка будет аннулирована!
</Toast>} </Toast>
)}
</> </>
) );
} }

View File

@ -25,21 +25,21 @@ export default function LottieAnimations() {
height={100} height={100}
animationProps={{ animationProps={{
style: { style: {
backgroundColor: "transparent" backgroundColor: "transparent",
} },
}} }}
/> />
{isConfettiVisible && {isConfettiVisible && (
<LottieAnimation <LottieAnimation
loadKey={ELottieKeys.confetti} loadKey={ELottieKeys.confetti}
width={"100dvw"} width={"100dvw"}
height={"100dvh"} height={"100dvh"}
className={styles["lottie-animation-container-confetti"]} className={styles["lottie-animation-container-confetti"]}
animationProps={{ animationProps={{
className: styles["lottie-animation-confetti"] className: styles["lottie-animation-confetti"],
}} }}
/> />
} )}
</> </>
) );
} }

View File

@ -4,11 +4,11 @@ import { useState } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { ROUTES } from "@/shared/constants/client-routes"; import { ROUTES } from "@/shared/constants/client-routes";
import { ERetainingFunnel } from "@/types" import { ERetainingFunnel } from "@/types";
import { RetainingButton } from "../.."; import { RetainingButton } from "../..";
import styles from './Buttons.module.scss' import styles from "./Buttons.module.scss";
export interface ChangeMindAnswer { export interface ChangeMindAnswer {
id: number; id: number;
@ -16,18 +16,18 @@ export interface ChangeMindAnswer {
} }
interface ButtonsProps { interface ButtonsProps {
answers: ChangeMindAnswer[] answers: ChangeMindAnswer[];
} }
export default function Buttons({ export default function Buttons({ answers }: ButtonsProps) {
answers const router = useRouter();
}: ButtonsProps) {
const router = useRouter()
// usePreloadImages([ // usePreloadImages([
// images("vip_member.png") // images("vip_member.png")
// ]) // ])
const [activeAnswer, setActiveAnswer] = useState<ChangeMindAnswer | null>(null); const [activeAnswer, setActiveAnswer] = useState<ChangeMindAnswer | null>(
null
);
// const retainingFunnel = useSelector(selectors.selectRetainingFunnel); // const retainingFunnel = useSelector(selectors.selectRetainingFunnel);
const retainingFunnel = ERetainingFunnel.Red; const retainingFunnel = ERetainingFunnel.Red;
@ -45,11 +45,11 @@ export default function Buttons({
// } // }
}, 1000); }, 1000);
return () => clearTimeout(timer); return () => clearTimeout(timer);
} };
return ( return (
<div className={styles.answers}> <div className={styles.answers}>
{answers.map((answer) => ( {answers.map(answer => (
<RetainingButton <RetainingButton
key={answer.id} key={answer.id}
className={styles.answer} className={styles.answer}
@ -60,5 +60,5 @@ export default function Buttons({
</RetainingButton> </RetainingButton>
))} ))}
</div> </div>
) );
} }

View File

@ -1 +1,4 @@
export { type ChangeMindAnswer,default as ChangeMindButtons } from "./Buttons/Buttons" export {
type ChangeMindAnswer,
default as ChangeMindButtons,
} from "./Buttons/Buttons";

File diff suppressed because one or more lines are too long

View File

@ -1,33 +1,100 @@
import { SVGProps } from "react" import { SVGProps } from "react";
type TopSellingSvgProps = SVGProps<SVGSVGElement>;
type TopSellingSvgProps = SVGProps<SVGSVGElement>
function TopSellingSVG(props: TopSellingSvgProps) { function TopSellingSVG(props: TopSellingSvgProps) {
return ( return (
<svg width="227" height="232" viewBox="0 0 227 232" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}> <svg
<path d="M106.726 9.28711C110.069 5.74435 115.632 5.63391 119.114 8.95508L119.445 9.28711L127.9 18.249C131.185 21.7297 136.185 22.9673 140.695 21.4541L141.13 21.2988L152.467 17.0156C156.994 15.3055 162.015 17.5996 163.722 22.0654L163.876 22.5049L167.828 34.7246C169.28 39.2125 173.174 42.4539 177.821 43.0869L178.273 43.1406L190.601 44.3564C195.345 44.8246 198.81 48.9952 198.463 53.6943L198.417 54.1514L196.757 67.334C196.178 71.9304 198.243 76.4461 202.065 79.0186L202.44 79.2607L213.414 86.0811C217.415 88.5672 218.707 93.7524 216.414 97.8057L216.181 98.1943L209.041 109.438C206.578 113.318 206.5 118.238 208.81 122.183L209.041 122.562L216.181 133.806C218.705 137.782 217.618 143.014 213.793 145.67L213.414 145.919L202.44 152.739C198.506 155.185 196.3 159.633 196.709 164.222L196.757 164.666L198.417 177.849C199.013 182.579 195.715 186.882 191.056 187.586L190.601 187.644L178.273 188.859C173.579 189.323 169.584 192.439 167.977 196.845L167.828 197.275L163.876 209.495C162.387 214.099 157.461 216.591 152.906 215.138L152.467 214.984L141.13 210.701C136.653 209.01 131.616 210.086 128.224 213.422L127.9 213.751L119.445 222.713C116.102 226.256 110.539 226.366 107.057 223.045L106.726 222.713L98.2695 213.751C94.9851 210.271 89.986 209.033 85.4756 210.546L85.041 210.701L73.7041 214.984C69.1774 216.694 64.1562 214.4 62.4492 209.935L62.2949 209.495L58.3428 197.275C56.8914 192.787 52.9967 189.546 48.3496 188.913L47.8975 188.859L35.5703 187.644C30.8261 187.175 27.361 183.005 27.708 178.306L27.7539 177.849L29.4141 164.666C29.993 160.07 27.9279 155.554 24.1055 152.981L23.7305 152.739L12.7568 145.919C8.75633 143.433 7.46427 138.248 9.75684 134.194L9.99023 133.806L17.1299 122.562C19.5932 118.682 19.6697 113.762 17.3604 109.817L17.1299 109.438L9.99023 98.1943C7.46557 94.2181 8.55326 88.9864 12.3779 86.3301L12.7568 86.0811L23.7305 79.2607C27.6651 76.8154 29.8713 72.3675 29.4619 67.7783L29.4141 67.334L27.7539 54.1514C27.1581 49.4213 30.456 45.1178 35.1152 44.4141L35.5703 44.3564L47.8975 43.1406C52.5915 42.6774 56.5871 39.5615 58.1943 35.1553L58.3428 34.7246L62.2949 22.5049C63.784 17.9006 68.7099 15.4086 73.2646 16.8623L73.7041 17.0156L85.041 21.2988C89.5179 22.99 94.5546 21.9138 97.9473 18.5781L98.2695 18.249L106.726 9.28711Z" fill="#1849A8" stroke="#6B8CD1" strokeWidth="3.49749" /> width="227"
<circle cx="112.503" cy="116" r="83.3568" stroke="white" strokeWidth="2.33166" /> height="232"
<circle cx="112.502" cy="116" r="51.2965" stroke="white" strokeWidth="1.16583" /> viewBox="0 0 227 232"
<path d="M112.876 39.6382L116.206 45.5468L122.855 46.8883L118.264 51.8815L119.043 58.6192L112.876 55.7966L106.708 58.6192L107.487 51.8815L102.897 46.8883L109.545 45.5468L112.876 39.6382Z" fill="white" /> fill="none"
<path d="M113.732 194.434L110.402 188.525L103.753 187.184L108.344 182.191L107.565 175.453L113.732 178.275L119.899 175.453L119.121 182.191L123.711 187.184L117.062 188.525L113.732 194.434Z" fill="white" /> xmlns="http://www.w3.org/2000/svg"
<path d="M71.8148 53.8492L76.7585 56.7721L82.2018 54.9409L80.9497 60.5459L84.3733 65.157L78.6558 65.6982L75.3283 70.3791L73.0468 65.1087L67.5667 63.3905L71.8741 59.592L71.8148 53.8492Z" fill="white" /> {...props}
<path d="M154.793 180.223L149.849 177.3L144.406 179.131L145.658 173.526L142.235 168.915L147.952 168.374L151.28 163.693L153.561 168.963L159.041 170.682L154.734 174.48L154.793 180.223Z" fill="white" /> >
<path d="M143.64 55.5067L149.172 57.0504L153.956 53.8729L154.197 59.6109L158.697 63.1788L153.315 65.1814L151.312 70.564L147.744 66.0636L142.006 65.8224L145.184 61.0384L143.64 55.5067Z" fill="white" /> <path
<path d="M82.9673 178.565L77.4355 177.022L72.6516 180.199L72.4103 174.461L67.91 170.893L73.2926 168.891L75.2952 163.508L78.8631 168.008L84.6011 168.25L81.4236 173.034L82.9673 178.565Z" fill="white" /> d="M106.726 9.28711C110.069 5.74435 115.632 5.63391 119.114 8.95508L119.445 9.28711L127.9 18.249C131.185 21.7297 136.185 22.9673 140.695 21.4541L141.13 21.2988L152.467 17.0156C156.994 15.3055 162.015 17.5996 163.722 22.0654L163.876 22.5049L167.828 34.7246C169.28 39.2125 173.174 42.4539 177.821 43.0869L178.273 43.1406L190.601 44.3564C195.345 44.8246 198.81 48.9952 198.463 53.6943L198.417 54.1514L196.757 67.334C196.178 71.9304 198.243 76.4461 202.065 79.0186L202.44 79.2607L213.414 86.0811C217.415 88.5672 218.707 93.7524 216.414 97.8057L216.181 98.1943L209.041 109.438C206.578 113.318 206.5 118.238 208.81 122.183L209.041 122.562L216.181 133.806C218.705 137.782 217.618 143.014 213.793 145.67L213.414 145.919L202.44 152.739C198.506 155.185 196.3 159.633 196.709 164.222L196.757 164.666L198.417 177.849C199.013 182.579 195.715 186.882 191.056 187.586L190.601 187.644L178.273 188.859C173.579 189.323 169.584 192.439 167.977 196.845L167.828 197.275L163.876 209.495C162.387 214.099 157.461 216.591 152.906 215.138L152.467 214.984L141.13 210.701C136.653 209.01 131.616 210.086 128.224 213.422L127.9 213.751L119.445 222.713C116.102 226.256 110.539 226.366 107.057 223.045L106.726 222.713L98.2695 213.751C94.9851 210.271 89.986 209.033 85.4756 210.546L85.041 210.701L73.7041 214.984C69.1774 216.694 64.1562 214.4 62.4492 209.935L62.2949 209.495L58.3428 197.275C56.8914 192.787 52.9967 189.546 48.3496 188.913L47.8975 188.859L35.5703 187.644C30.8261 187.175 27.361 183.005 27.708 178.306L27.7539 177.849L29.4141 164.666C29.993 160.07 27.9279 155.554 24.1055 152.981L23.7305 152.739L12.7568 145.919C8.75633 143.433 7.46427 138.248 9.75684 134.194L9.99023 133.806L17.1299 122.562C19.5932 118.682 19.6697 113.762 17.3604 109.817L17.1299 109.438L9.99023 98.1943C7.46557 94.2181 8.55326 88.9864 12.3779 86.3301L12.7568 86.0811L23.7305 79.2607C27.6651 76.8154 29.8713 72.3675 29.4619 67.7783L29.4141 67.334L27.7539 54.1514C27.1581 49.4213 30.456 45.1178 35.1152 44.4141L35.5703 44.3564L47.8975 43.1406C52.5915 42.6774 56.5871 39.5615 58.1943 35.1553L58.3428 34.7246L62.2949 22.5049C63.784 17.9006 68.7099 15.4086 73.2646 16.8623L73.7041 17.0156L85.041 21.2988C89.5179 22.99 94.5546 21.9138 97.9473 18.5781L98.2695 18.249L106.726 9.28711Z"
<path d="M34.368 128.77H30.7349V107.177H24.4824V103.23H40.6346V107.177H34.368V128.77Z" fill="white" /> fill="#1849A8"
<path d="M51.5842 102.593C54.532 102.593 56.865 103.796 58.583 106.204C60.3104 108.611 61.1741 111.873 61.1741 115.991C61.1741 120.109 60.3151 123.378 58.5971 125.796C56.8791 128.203 54.5414 129.407 51.5842 129.407C48.6082 129.407 46.2565 128.203 44.5291 125.796C42.8111 123.389 41.9521 120.121 41.9521 115.991C41.9521 111.873 42.8158 108.611 44.5432 106.204C46.28 103.796 48.627 102.593 51.5842 102.593ZM51.5842 106.646C49.7723 106.646 48.3312 107.49 47.261 109.177C46.2002 110.864 45.6697 113.136 45.6697 115.991C45.6697 118.858 46.1955 121.136 47.2469 122.823C48.3078 124.51 49.7535 125.354 51.5842 125.354C53.3867 125.354 54.8137 124.51 55.8651 122.823C56.926 121.124 57.4564 118.847 57.4564 115.991C57.4564 113.136 56.926 110.864 55.8651 109.177C54.8137 107.49 53.3867 106.646 51.5842 106.646Z" fill="white" /> stroke="#6B8CD1"
<path d="M64.4772 103.23H72.6729C74.7008 103.23 76.3437 104.027 77.6017 105.619C78.869 107.212 79.5027 109.295 79.5027 111.867C79.5027 114.392 78.8503 116.445 77.5453 118.027C76.2498 119.608 74.574 120.398 72.518 120.398H68.1103V128.77H64.4772V103.23ZM68.1103 107.053V116.611H71.7013C73.0062 116.611 74.0154 116.198 74.7289 115.372C75.4518 114.546 75.8132 113.378 75.8132 111.867C75.8132 110.31 75.4565 109.118 74.743 108.292C74.0389 107.466 73.0297 107.053 71.7154 107.053H68.1103Z" fill="white" /> strokeWidth="3.49749"
<path d="M88.5451 121.832H92.0938C92.2064 122.976 92.6805 123.891 93.5161 124.575C94.3516 125.248 95.4125 125.584 96.6986 125.584C97.9003 125.584 98.886 125.242 99.6559 124.557C100.435 123.861 100.825 122.976 100.825 121.903C100.825 120.982 100.529 120.233 99.9375 119.655C99.3554 119.077 98.4119 118.599 97.107 118.221L94.4737 117.46C92.6336 116.941 91.2676 116.097 90.3758 114.929C89.4933 113.749 89.0521 112.209 89.0521 110.31C89.0521 107.985 89.7421 106.121 91.1221 104.717C92.5116 103.301 94.3422 102.593 96.6141 102.593C98.7452 102.593 100.51 103.289 101.909 104.681C103.308 106.074 104.049 107.867 104.134 110.062H100.656C100.534 108.941 100.106 108.05 99.3742 107.389C98.6419 106.729 97.7172 106.398 96.6 106.398C95.4265 106.398 94.483 106.729 93.7696 107.389C93.0654 108.038 92.7134 108.906 92.7134 109.991C92.7134 110.853 92.9903 111.555 93.5442 112.097C94.0981 112.628 95.0041 113.077 96.2621 113.442L98.5997 114.115C100.656 114.693 102.144 115.555 103.064 116.699C103.993 117.844 104.458 119.389 104.458 121.336C104.458 123.826 103.74 125.796 102.303 127.248C100.876 128.687 98.933 129.407 96.4733 129.407C94.1451 129.407 92.2628 128.723 90.8264 127.354C89.3994 125.985 88.639 124.145 88.5451 121.832Z" fill="white" /> />
<path d="M120.829 124.823V128.77H107.676V103.23H120.829V107.177H111.31V113.956H120.308V117.673H111.31V124.823H120.829Z" fill="white" /> <circle
<path d="M137.426 124.77V128.77H124.47V103.23H128.103V124.77H137.426Z" fill="white" /> cx="112.503"
<path d="M153.431 124.77V128.77H140.475V103.23H144.108V124.77H153.431Z" fill="white" /> cy="116"
r="83.3568"
stroke="white"
strokeWidth="2.33166"
/>
<circle
cx="112.502"
cy="116"
r="51.2965"
stroke="white"
strokeWidth="1.16583"
/>
<path
d="M112.876 39.6382L116.206 45.5468L122.855 46.8883L118.264 51.8815L119.043 58.6192L112.876 55.7966L106.708 58.6192L107.487 51.8815L102.897 46.8883L109.545 45.5468L112.876 39.6382Z"
fill="white"
/>
<path
d="M113.732 194.434L110.402 188.525L103.753 187.184L108.344 182.191L107.565 175.453L113.732 178.275L119.899 175.453L119.121 182.191L123.711 187.184L117.062 188.525L113.732 194.434Z"
fill="white"
/>
<path
d="M71.8148 53.8492L76.7585 56.7721L82.2018 54.9409L80.9497 60.5459L84.3733 65.157L78.6558 65.6982L75.3283 70.3791L73.0468 65.1087L67.5667 63.3905L71.8741 59.592L71.8148 53.8492Z"
fill="white"
/>
<path
d="M154.793 180.223L149.849 177.3L144.406 179.131L145.658 173.526L142.235 168.915L147.952 168.374L151.28 163.693L153.561 168.963L159.041 170.682L154.734 174.48L154.793 180.223Z"
fill="white"
/>
<path
d="M143.64 55.5067L149.172 57.0504L153.956 53.8729L154.197 59.6109L158.697 63.1788L153.315 65.1814L151.312 70.564L147.744 66.0636L142.006 65.8224L145.184 61.0384L143.64 55.5067Z"
fill="white"
/>
<path
d="M82.9673 178.565L77.4355 177.022L72.6516 180.199L72.4103 174.461L67.91 170.893L73.2926 168.891L75.2952 163.508L78.8631 168.008L84.6011 168.25L81.4236 173.034L82.9673 178.565Z"
fill="white"
/>
<path
d="M34.368 128.77H30.7349V107.177H24.4824V103.23H40.6346V107.177H34.368V128.77Z"
fill="white"
/>
<path
d="M51.5842 102.593C54.532 102.593 56.865 103.796 58.583 106.204C60.3104 108.611 61.1741 111.873 61.1741 115.991C61.1741 120.109 60.3151 123.378 58.5971 125.796C56.8791 128.203 54.5414 129.407 51.5842 129.407C48.6082 129.407 46.2565 128.203 44.5291 125.796C42.8111 123.389 41.9521 120.121 41.9521 115.991C41.9521 111.873 42.8158 108.611 44.5432 106.204C46.28 103.796 48.627 102.593 51.5842 102.593ZM51.5842 106.646C49.7723 106.646 48.3312 107.49 47.261 109.177C46.2002 110.864 45.6697 113.136 45.6697 115.991C45.6697 118.858 46.1955 121.136 47.2469 122.823C48.3078 124.51 49.7535 125.354 51.5842 125.354C53.3867 125.354 54.8137 124.51 55.8651 122.823C56.926 121.124 57.4564 118.847 57.4564 115.991C57.4564 113.136 56.926 110.864 55.8651 109.177C54.8137 107.49 53.3867 106.646 51.5842 106.646Z"
fill="white"
/>
<path
d="M64.4772 103.23H72.6729C74.7008 103.23 76.3437 104.027 77.6017 105.619C78.869 107.212 79.5027 109.295 79.5027 111.867C79.5027 114.392 78.8503 116.445 77.5453 118.027C76.2498 119.608 74.574 120.398 72.518 120.398H68.1103V128.77H64.4772V103.23ZM68.1103 107.053V116.611H71.7013C73.0062 116.611 74.0154 116.198 74.7289 115.372C75.4518 114.546 75.8132 113.378 75.8132 111.867C75.8132 110.31 75.4565 109.118 74.743 108.292C74.0389 107.466 73.0297 107.053 71.7154 107.053H68.1103Z"
fill="white"
/>
<path
d="M88.5451 121.832H92.0938C92.2064 122.976 92.6805 123.891 93.5161 124.575C94.3516 125.248 95.4125 125.584 96.6986 125.584C97.9003 125.584 98.886 125.242 99.6559 124.557C100.435 123.861 100.825 122.976 100.825 121.903C100.825 120.982 100.529 120.233 99.9375 119.655C99.3554 119.077 98.4119 118.599 97.107 118.221L94.4737 117.46C92.6336 116.941 91.2676 116.097 90.3758 114.929C89.4933 113.749 89.0521 112.209 89.0521 110.31C89.0521 107.985 89.7421 106.121 91.1221 104.717C92.5116 103.301 94.3422 102.593 96.6141 102.593C98.7452 102.593 100.51 103.289 101.909 104.681C103.308 106.074 104.049 107.867 104.134 110.062H100.656C100.534 108.941 100.106 108.05 99.3742 107.389C98.6419 106.729 97.7172 106.398 96.6 106.398C95.4265 106.398 94.483 106.729 93.7696 107.389C93.0654 108.038 92.7134 108.906 92.7134 109.991C92.7134 110.853 92.9903 111.555 93.5442 112.097C94.0981 112.628 95.0041 113.077 96.2621 113.442L98.5997 114.115C100.656 114.693 102.144 115.555 103.064 116.699C103.993 117.844 104.458 119.389 104.458 121.336C104.458 123.826 103.74 125.796 102.303 127.248C100.876 128.687 98.933 129.407 96.4733 129.407C94.1451 129.407 92.2628 128.723 90.8264 127.354C89.3994 125.985 88.639 124.145 88.5451 121.832Z"
fill="white"
/>
<path
d="M120.829 124.823V128.77H107.676V103.23H120.829V107.177H111.31V113.956H120.308V117.673H111.31V124.823H120.829Z"
fill="white"
/>
<path
d="M137.426 124.77V128.77H124.47V103.23H128.103V124.77H137.426Z"
fill="white"
/>
<path
d="M153.431 124.77V128.77H140.475V103.23H144.108V124.77H153.431Z"
fill="white"
/>
<path d="M160.114 128.77H156.48V103.23H160.114V128.77Z" fill="white" /> <path d="M160.114 128.77H156.48V103.23H160.114V128.77Z" fill="white" />
<path d="M167.768 128.77H164.262V103.23H167.416L177.386 120.593H177.625V103.23H181.118V128.77H177.977L168.007 111.389H167.768V128.77Z" fill="white" /> <path
<path d="M202.854 118.54C202.854 121.855 202.052 124.498 200.446 126.469C198.841 128.428 196.682 129.407 193.969 129.407C191.021 129.407 188.692 128.209 186.984 125.814C185.275 123.407 184.421 120.133 184.421 115.991C184.421 111.909 185.28 108.658 186.998 106.239C188.716 103.808 191.021 102.593 193.912 102.593C196.259 102.593 198.226 103.372 199.813 104.929C201.399 106.487 202.357 108.569 202.685 111.177H199.066C198.7 109.726 198.071 108.611 197.179 107.832C196.297 107.053 195.208 106.664 193.912 106.664C192.138 106.664 190.73 107.496 189.688 109.159C188.655 110.823 188.139 113.088 188.139 115.956C188.139 118.858 188.664 121.147 189.716 122.823C190.777 124.498 192.213 125.336 194.025 125.336C195.574 125.336 196.832 124.793 197.799 123.708C198.766 122.622 199.263 121.201 199.292 119.442L199.306 118.911H194.419V115.336H202.854V118.54Z" fill="white" /> d="M167.768 128.77H164.262V103.23H167.416L177.386 120.593H177.625V103.23H181.118V128.77H177.977L168.007 111.389H167.768V128.77Z"
fill="white"
/>
<path
d="M202.854 118.54C202.854 121.855 202.052 124.498 200.446 126.469C198.841 128.428 196.682 129.407 193.969 129.407C191.021 129.407 188.692 128.209 186.984 125.814C185.275 123.407 184.421 120.133 184.421 115.991C184.421 111.909 185.28 108.658 186.998 106.239C188.716 103.808 191.021 102.593 193.912 102.593C196.259 102.593 198.226 103.372 199.813 104.929C201.399 106.487 202.357 108.569 202.685 111.177H199.066C198.7 109.726 198.071 108.611 197.179 107.832C196.297 107.053 195.208 106.664 193.912 106.664C192.138 106.664 190.73 107.496 189.688 109.159C188.655 110.823 188.139 113.088 188.139 115.956C188.139 118.858 188.664 121.147 189.716 122.823C190.777 124.498 192.213 125.336 194.025 125.336C195.574 125.336 196.832 124.793 197.799 123.708C198.766 122.622 199.263 121.201 199.292 119.442L199.306 118.911H194.419V115.336H202.854V118.54Z"
fill="white"
/>
</svg> </svg>
);
)
} }
export default TopSellingSVG export default TopSellingSVG;

View File

@ -1,4 +1,4 @@
export { default as RetainingButton } from "./Button/Button" export { default as RetainingButton } from "./Button/Button";
export { default as CheckMark } from "./CheckMark/CheckMark" export { default as CheckMark } from "./CheckMark/CheckMark";
export { default as Offer } from "./Offer/Offer" export { default as Offer } from "./Offer/Offer";
export { default as RetainingStepper } from "./RetainingStepper/RetainingStepper" export { default as RetainingStepper } from "./RetainingStepper/RetainingStepper";

View File

@ -15,11 +15,15 @@ export default function Button() {
const handleButtonClick = () => { const handleButtonClick = () => {
router.push(ROUTES.home()); router.push(ROUTES.home());
} };
return ( return (
<RetainingButton className={styles.button} active={true} onClick={handleButtonClick}> <RetainingButton
className={styles.button}
active={true}
onClick={handleButtonClick}
>
{t("button")} {t("button")}
</RetainingButton> </RetainingButton>
) );
} }

View File

@ -55,9 +55,9 @@
.buttonCancel { .buttonCancel {
line-height: 20px; line-height: 20px;
color: #5D5D5D; color: #5d5d5d;
background: none; background: none;
outline: 2px solid #9A9797; outline: 2px solid #9a9797;
font-size: 28px; font-size: 28px;
margin-top: 22px; margin-top: 22px;
} }

View File

@ -27,7 +27,9 @@ export default function SecondChancePage() {
preloadKey: ELottieKeys.confetti, preloadKey: ELottieKeys.confetti,
}); });
const [activeOffer, setActiveOffer] = useState<"pause_30" | "free_chat_30">("pause_30"); const [activeOffer, setActiveOffer] = useState<"pause_30" | "free_chat_30">(
"pause_30"
);
const [isLoadingButton, setIsLoadingButton] = useState<boolean>(false); const [isLoadingButton, setIsLoadingButton] = useState<boolean>(false);
// const retainingFunnel = useSelector(selectors.selectRetainingFunnel); // const retainingFunnel = useSelector(selectors.selectRetainingFunnel);
// const token = useSelector(selectors.selectToken); // const token = useSelector(selectors.selectToken);
@ -38,7 +40,7 @@ export default function SecondChancePage() {
const handleOfferClick = (offer: "pause_30" | "free_chat_30") => { const handleOfferClick = (offer: "pause_30" | "free_chat_30") => {
if (isLoadingButton) return; if (isLoadingButton) return;
setActiveOffer(offer); setActiveOffer(offer);
} };
const handleGetOfferClick = async () => { const handleGetOfferClick = async () => {
if (isLoadingButton) return; if (isLoadingButton) return;
@ -52,7 +54,7 @@ export default function SecondChancePage() {
// if (response.status === "success") { // if (response.status === "success") {
// navigate(routes.client.retainingFunnelPlanCancelled()); // navigate(routes.client.retainingFunnelPlanCancelled());
// } // }
} };
const handleCancelClick = () => { const handleCancelClick = () => {
if (isLoadingButton) return; if (isLoadingButton) return;
@ -64,8 +66,7 @@ export default function SecondChancePage() {
// } // }
// if (retainingFunnel === ERetainingFunnel.Purple) { // if (retainingFunnel === ERetainingFunnel.Purple) {
// return navigate(routes.client.retainingFunnelStopFor30Days()); // return navigate(routes.client.retainingFunnelStopFor30Days());
};
}
return ( return (
<> <>
@ -106,9 +107,11 @@ export default function SecondChancePage() {
active={true} active={true}
onClick={handleGetOfferClick} onClick={handleGetOfferClick}
> >
{isLoadingButton && <div className={styles.loaderContainer}> {isLoadingButton && (
<div className={styles.loaderContainer}>
<Spinner /> <Spinner />
</div>} </div>
)}
{t("get_offer")} {t("get_offer")}
</RetainingButton> </RetainingButton>
</BlurComponent> </BlurComponent>
@ -120,5 +123,5 @@ export default function SecondChancePage() {
{t("cancel")} {t("cancel")}
</RetainingButton> </RetainingButton>
</> </>
) );
} }

View File

@ -26,7 +26,7 @@
& > .buttonCancel { & > .buttonCancel {
background: none; background: none;
color: #5D5D5D; color: #5d5d5d;
outline: 2px solid #9A9797; outline: 2px solid #9a9797;
} }
} }

View File

@ -39,7 +39,7 @@ export default function Buttons() {
// if (response.status === "success") { // if (response.status === "success") {
// navigate(routes.client.retainingFunnelSubscriptionStopped()); // navigate(routes.client.retainingFunnelSubscriptionStopped());
// } // }
} };
const handleCancelClick = () => { const handleCancelClick = () => {
if (isLoadingButton) return; if (isLoadingButton) return;
@ -47,7 +47,7 @@ export default function Buttons() {
// return navigate(routes.client.retainingFunnelChangeMind()); // return navigate(routes.client.retainingFunnelChangeMind());
// } // }
router.push(ROUTES.retainingFunnelCancellationOfSubscription()); router.push(ROUTES.retainingFunnelCancellationOfSubscription());
} };
return ( return (
<div className={styles.buttonsContainer}> <div className={styles.buttonsContainer}>
@ -56,9 +56,11 @@ export default function Buttons() {
active={true} active={true}
onClick={handleStopClick} onClick={handleStopClick}
> >
{isLoadingButton && <div className={styles.loaderContainer}> {isLoadingButton && (
<div className={styles.loaderContainer}>
<Spinner /> <Spinner />
</div>} </div>
)}
{t("stop")} {t("stop")}
</RetainingButton> </RetainingButton>
<RetainingButton <RetainingButton
@ -68,5 +70,5 @@ export default function Buttons() {
{t("cancel")} {t("cancel")}
</RetainingButton> </RetainingButton>
</div> </div>
) );
} }

View File

@ -15,11 +15,15 @@ export default function Button() {
const handleButtonClick = () => { const handleButtonClick = () => {
router.push(ROUTES.home()); router.push(ROUTES.home());
} };
return ( return (
<RetainingButton className={styles.button} active={true} onClick={handleButtonClick}> <RetainingButton
className={styles.button}
active={true}
onClick={handleButtonClick}
>
{t("button")} {t("button")}
</RetainingButton> </RetainingButton>
) );
} }

View File

@ -4,11 +4,11 @@ import { useState } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { ROUTES } from "@/shared/constants/client-routes"; import { ROUTES } from "@/shared/constants/client-routes";
import { ERetainingFunnel } from "@/types" import { ERetainingFunnel } from "@/types";
import { RetainingButton } from "../.."; import { RetainingButton } from "../..";
import styles from './Buttons.module.scss' import styles from "./Buttons.module.scss";
export interface WhatReasonAnswer { export interface WhatReasonAnswer {
id: number; id: number;
@ -17,15 +17,15 @@ export interface WhatReasonAnswer {
} }
interface ButtonsProps { interface ButtonsProps {
answers: WhatReasonAnswer[] answers: WhatReasonAnswer[];
} }
export default function Buttons({ export default function Buttons({ answers }: ButtonsProps) {
answers const router = useRouter();
}: ButtonsProps) {
const router = useRouter()
const [activeAnswer, setActiveAnswer] = useState<WhatReasonAnswer | null>(null); const [activeAnswer, setActiveAnswer] = useState<WhatReasonAnswer | null>(
null
);
const handleNext = (answer: WhatReasonAnswer) => { const handleNext = (answer: WhatReasonAnswer) => {
setActiveAnswer(answer); setActiveAnswer(answer);
@ -42,11 +42,11 @@ export default function Buttons({
} }
}, 1000); }, 1000);
return () => clearTimeout(timer); return () => clearTimeout(timer);
} };
return ( return (
<div className={styles.answers}> <div className={styles.answers}>
{answers.map((answer) => ( {answers.map(answer => (
<RetainingButton <RetainingButton
key={answer.id} key={answer.id}
className={styles.answer} className={styles.answer}
@ -57,5 +57,5 @@ export default function Buttons({
</RetainingButton> </RetainingButton>
))} ))}
</div> </div>
) );
} }

View File

@ -1 +1,4 @@
export { type WhatReasonAnswer,default as WhatReasonsButtons } from "./Buttons/Buttons" export {
type WhatReasonAnswer,
default as WhatReasonsButtons,
} from "./Buttons/Buttons";

View File

@ -34,7 +34,7 @@
} }
} }
.close { .close.close {
position: absolute; position: absolute;
top: 24px; top: 24px;
right: 24px; right: 24px;

View File

@ -1,13 +1,13 @@
'use client'; "use client";
import Link from 'next/link'; import Link from "next/link";
import clsx from 'clsx'; import clsx from "clsx";
import { Button, Icon, Typography } from '@/components/ui'; import { Button, Icon, Typography } from "@/components/ui";
import { IconName } from '@/components/ui/Icon/Icon'; import { IconName } from "@/components/ui/Icon/Icon";
import { ROUTES } from '@/shared/constants/client-routes'; import { ROUTES } from "@/shared/constants/client-routes";
import styles from './Drawer.module.scss'; import styles from "./Drawer.module.scss";
interface DrawerProps { interface DrawerProps {
isOpen: boolean; isOpen: boolean;
@ -23,14 +23,14 @@ export default function Drawer({ isOpen, onClose }: DrawerProps) {
<Icon <Icon
size={{ size={{
height: 18, height: 18,
width: 18 width: 18,
}} }}
name={IconName.Cross} name={IconName.Cross}
/> />
</Button> </Button>
<nav className={styles.content}> <nav className={styles.content}>
<Link href={ROUTES.profile()}> <Link href={ROUTES.profile()}>
<Typography weight='medium'>Profile</Typography> <Typography weight="medium">Profile</Typography>
</Link> </Link>
</nav> </nav>
</aside> </aside>

View File

@ -1,9 +1,16 @@
'use client'; "use client";
import { createContext, ReactNode, useCallback, useContext, useEffect,useState } from 'react'; import {
import { usePathname } from 'next/navigation'; createContext,
ReactNode,
useCallback,
useContext,
useEffect,
useState,
} from "react";
import { usePathname } from "next/navigation";
import Drawer from './Drawer'; import Drawer from "./Drawer";
type DrawerCtx = { type DrawerCtx = {
isOpen: boolean; isOpen: boolean;
@ -15,7 +22,7 @@ const DrawerContext = createContext<DrawerCtx | null>(null);
export function useDrawer() { export function useDrawer() {
const ctx = useContext(DrawerContext); const ctx = useContext(DrawerContext);
if (!ctx) throw new Error('useDrawer must be used within DrawerProvider'); if (!ctx) throw new Error("useDrawer must be used within DrawerProvider");
return ctx; return ctx;
} }

Some files were not shown because too many files have changed in this diff Show More