main
add profile path & auth
This commit is contained in:
parent
851bcc1d4f
commit
fc01784acb
@ -1,6 +1,9 @@
|
||||
import { dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
import eslintPluginImport from "eslint-plugin-import";
|
||||
import eslintPluginUnused from "eslint-plugin-unused-imports";
|
||||
import eslintPluginSort from "eslint-plugin-simple-import-sort";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
@ -11,6 +14,47 @@ const compat = new FlatCompat({
|
||||
|
||||
const eslintConfig = [
|
||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||
|
||||
{
|
||||
plugins: {
|
||||
import: eslintPluginImport,
|
||||
"unused-imports": eslintPluginUnused,
|
||||
"simple-import-sort": eslintPluginSort,
|
||||
},
|
||||
|
||||
rules: {
|
||||
/* неиспользуемые переменные и импорты */
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
|
||||
],
|
||||
"unused-imports/no-unused-imports": "error",
|
||||
"unused-imports/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
vars: "all",
|
||||
varsIgnorePattern: "^_",
|
||||
args: "after-used",
|
||||
argsIgnorePattern: "^_",
|
||||
},
|
||||
],
|
||||
|
||||
/* порядок импортов: стили .module.(s)css внизу */
|
||||
"simple-import-sort/imports": [
|
||||
"error",
|
||||
{
|
||||
groups: [
|
||||
["^\\u0000"], // side-effects
|
||||
["^react", "^next", "^@?\\w"],// пакеты
|
||||
["^@/"], // алиасы проекта
|
||||
["^\\.\\.(?!/?$)", "^\\./(?=.*/)", "^\\./?$"], // относительные
|
||||
["^.+\\.module\\.(css|scss)$"], // модули стилей
|
||||
],
|
||||
},
|
||||
],
|
||||
"simple-import-sort/exports": "error",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default eslintConfig;
|
||||
|
||||
176
messages/de.json
Normal file
176
messages/de.json
Normal file
@ -0,0 +1,176 @@
|
||||
{
|
||||
"HomePage": {
|
||||
"title": "Hello world!",
|
||||
"about": "Go to the about page"
|
||||
},
|
||||
"Profile": {
|
||||
"profile_information": {
|
||||
"title": "Profile Information",
|
||||
"description": "To update your email address please contact support.",
|
||||
"email_placeholder": "Email",
|
||||
"name_placeholder": "Name"
|
||||
},
|
||||
"billing": {
|
||||
"title": "Billing",
|
||||
"description": "To access your subscription information, please log into your billing account.",
|
||||
"subscription_type": "Subscription Type:",
|
||||
"billing_button": "Billing",
|
||||
"credits": {
|
||||
"title": "You have {credits} credits left",
|
||||
"description": "You can use them to chat with any Expert on the platform."
|
||||
},
|
||||
"any_questions": "Any questions? <link>{linkText}</link>",
|
||||
"any_questions_link": "Contact us",
|
||||
"subscription_update": "<bold>{subscriptionUpdateBold}</bold><br></br>If you've just purchased or changed plan, your subscription status will change in a few hours.",
|
||||
"subscription_update_bold": "Subscription information is updated every few hours."
|
||||
},
|
||||
"log_out": {
|
||||
"title": "Log out",
|
||||
"log_out_button": "Log out",
|
||||
"modal": {
|
||||
"title": "Are you sure you want to log out?",
|
||||
"description": "Are you sure you want to log out?",
|
||||
"stay_button": "Stay",
|
||||
"log_out_button": "Log out"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Subscriptions": {
|
||||
"title": "Manage my subscriptions",
|
||||
"modal": {
|
||||
"title": "Are you sure you want to cancel your subscription?",
|
||||
"description": "Are you sure you want to cancel your subscription?",
|
||||
"cancel_button": "Cancel subscription",
|
||||
"stay_button": "Stay"
|
||||
},
|
||||
"table": {
|
||||
"subscription_type": "Subscription Type",
|
||||
"subscription_type_value": {
|
||||
"DAY": "Daily Subscription",
|
||||
"WEEK": "Weekly Subscription",
|
||||
"MONTH": "Monthly Subscription",
|
||||
"YEAR": "Yearly Subscription"
|
||||
},
|
||||
"subscription_status": "Subscription Status",
|
||||
"subscription_status_value": {
|
||||
"ACTIVE": "Active",
|
||||
"CANCELLED": "Cancels on <date>"
|
||||
},
|
||||
"billing_period": "Billing Period",
|
||||
"billing_period_value": {
|
||||
"DAY": "Day",
|
||||
"WEEK": "Week",
|
||||
"MONTH": "Month",
|
||||
"YEAR": "Year"
|
||||
},
|
||||
"last_payment_on": "Last Payment On",
|
||||
"renewal_date": "Renewal Date",
|
||||
"renewal_amount": "Renewal Amount",
|
||||
"cancel_subscription": "Cancel Subscription"
|
||||
},
|
||||
"no_subscriptions": "You don't have any subscriptions",
|
||||
"error": "Something went wrong. Please try again later.",
|
||||
"try_again": "Try again"
|
||||
},
|
||||
"CancelSubscription": {
|
||||
"title": "Жаль, что вы уходите…",
|
||||
"description": "Многие уходят именно в тот момент, когда астролог начинает видеть поворотную точку в их истории.<br></br><br></br>Позвольте задать пару вопросов, чтобы сделать наш сервис лучше - и, возможно, предложить решение, которое больше подходит именно вам.",
|
||||
"stay_button": "Остаться и уменьшить мой план на 50%",
|
||||
"cancel_button": "Отменить"
|
||||
},
|
||||
"Stay50Done": {
|
||||
"title": "Мы ценим твой выбор!",
|
||||
"descriptions": {
|
||||
"1": "План успешно изменен"
|
||||
},
|
||||
"button": "Готово"
|
||||
},
|
||||
"AppreciateChoice": {
|
||||
"title": "Мы ценим твой выбор!",
|
||||
"descriptions": {
|
||||
"1": "Подбираем оптимальное решение...",
|
||||
"2": "Составляем персонализированный опрос...",
|
||||
"3": "Формируем выгодное предложение..."
|
||||
},
|
||||
"button": "Next"
|
||||
},
|
||||
"WhatReason": {
|
||||
"title": "Что стало причиной?",
|
||||
"answers": {
|
||||
"no_promised_result": "Не получил(а) обещанного результата",
|
||||
"too_expensive": "Слишком дорого",
|
||||
"high_auto_payment": "Стоимость автоматической оплаты слишком высока",
|
||||
"unexpected_fee": "Я не ожидал дополнительной платы",
|
||||
"want_pause": "Хочу сделать паузу",
|
||||
"service_not_as_expected": "Сервис оказался не таким, как ожидал(а)",
|
||||
"found_alternative": "Нашёл(а) альтернативу",
|
||||
"dislike_app": "Мне не понравилось приложение",
|
||||
"hard_to_navigate": "В приложении сложно ориентироваться",
|
||||
"other": "Другое"
|
||||
}
|
||||
},
|
||||
"Payment": {
|
||||
"Success": {
|
||||
"title": "Payment successful",
|
||||
"button": "Done"
|
||||
},
|
||||
"Error": {
|
||||
"title": "Payment failed",
|
||||
"button": "Try again"
|
||||
}
|
||||
},
|
||||
"SecondChance": {
|
||||
"title": "Дайте нам второй шанс и получи самый лучший план БЕСПЛАТНО",
|
||||
"offers": {
|
||||
"1": {
|
||||
"title": "Бесплатный план на<br></br>1 месяц",
|
||||
"description": "Используй весь потенциал AURA и даже больше.",
|
||||
"old-price": "1900",
|
||||
"new-price": "0"
|
||||
},
|
||||
"2": {
|
||||
"title": "Бесплатный премиальный план",
|
||||
"description": "Бесплатная 30 мин консультация с премиальным Эдвайзером",
|
||||
"old-price": "4900",
|
||||
"new-price": "0"
|
||||
}
|
||||
},
|
||||
"get_offer": "Получить бесплатный план",
|
||||
"cancel": "Отменить"
|
||||
},
|
||||
"ChangeMind": {
|
||||
"title": "Что может изменить твое мнение?",
|
||||
"answers": {
|
||||
"more_chat_time": "Больше времени в чатах",
|
||||
"more_personal_reports": "Больше персонализированных отчетов",
|
||||
"individual_plan": "Индивидуальный план",
|
||||
"other": "Другое"
|
||||
}
|
||||
},
|
||||
"StopFor30Days": {
|
||||
"title": "Остановите подписку на тридцать дней. Никаких списаний.",
|
||||
"stop": "Остановить",
|
||||
"cancel": "Отменить"
|
||||
},
|
||||
"CancellationOfSubscription": {
|
||||
"title": "Подписка аннулируется!",
|
||||
"description": "Чтобы отменить подписку, нажмите “Подтвердить мои действия”",
|
||||
"offer": {
|
||||
"title": "Бесплатный 2-месячный план",
|
||||
"old-price": "9900",
|
||||
"new-price": "0"
|
||||
},
|
||||
"offer_button": "Применить",
|
||||
"cancel_button": "Я подтверждаю свои действия"
|
||||
},
|
||||
"PlanCancelled": {
|
||||
"title": "Стандартный план Отменен!",
|
||||
"icon": "🥳",
|
||||
"description": "Выполнен переход на бесплатный тридцатидневный план ",
|
||||
"button": "Готово"
|
||||
},
|
||||
"SubscriptionStopped": {
|
||||
"title": "Подписка остановлена успешно!",
|
||||
"icon": "🎉"
|
||||
}
|
||||
}
|
||||
176
messages/en.json
Normal file
176
messages/en.json
Normal file
@ -0,0 +1,176 @@
|
||||
{
|
||||
"HomePage": {
|
||||
"title": "Hello world!",
|
||||
"about": "Go to the about page"
|
||||
},
|
||||
"Profile": {
|
||||
"profile_information": {
|
||||
"title": "Profile Information",
|
||||
"description": "To update your email address please contact support.",
|
||||
"email_placeholder": "Email",
|
||||
"name_placeholder": "Name"
|
||||
},
|
||||
"billing": {
|
||||
"title": "Billing",
|
||||
"description": "To access your subscription information, please log into your billing account.",
|
||||
"subscription_type": "Subscription Type:",
|
||||
"billing_button": "Billing",
|
||||
"credits": {
|
||||
"title": "You have {credits} credits left",
|
||||
"description": "You can use them to chat with any Expert on the platform."
|
||||
},
|
||||
"any_questions": "Any questions? <link>{linkText}</link>",
|
||||
"any_questions_link": "Contact us",
|
||||
"subscription_update": "<bold>{subscriptionUpdateBold}</bold><br></br>If you've just purchased or changed plan, your subscription status will change in a few hours.",
|
||||
"subscription_update_bold": "Subscription information is updated every few hours."
|
||||
},
|
||||
"log_out": {
|
||||
"title": "Log out",
|
||||
"log_out_button": "Log out",
|
||||
"modal": {
|
||||
"title": "Are you sure you want to log out?",
|
||||
"description": "Are you sure you want to log out?",
|
||||
"stay_button": "Stay",
|
||||
"log_out_button": "Log out"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Subscriptions": {
|
||||
"title": "Manage my subscriptions",
|
||||
"modal": {
|
||||
"title": "Are you sure you want to cancel your subscription?",
|
||||
"description": "Are you sure you want to cancel your subscription?",
|
||||
"cancel_button": "Cancel subscription",
|
||||
"stay_button": "Stay"
|
||||
},
|
||||
"table": {
|
||||
"subscription_type": "Subscription Type",
|
||||
"subscription_type_value": {
|
||||
"DAY": "Daily Subscription",
|
||||
"WEEK": "Weekly Subscription",
|
||||
"MONTH": "Monthly Subscription",
|
||||
"YEAR": "Yearly Subscription"
|
||||
},
|
||||
"subscription_status": "Subscription Status",
|
||||
"subscription_status_value": {
|
||||
"ACTIVE": "Active",
|
||||
"CANCELLED": "Cancels on <date>"
|
||||
},
|
||||
"billing_period": "Billing Period",
|
||||
"billing_period_value": {
|
||||
"DAY": "Day",
|
||||
"WEEK": "Week",
|
||||
"MONTH": "Month",
|
||||
"YEAR": "Year"
|
||||
},
|
||||
"last_payment_on": "Last Payment On",
|
||||
"renewal_date": "Renewal Date",
|
||||
"renewal_amount": "Renewal Amount",
|
||||
"cancel_subscription": "Cancel Subscription"
|
||||
},
|
||||
"no_subscriptions": "You don't have any subscriptions",
|
||||
"error": "Something went wrong. Please try again later.",
|
||||
"try_again": "Try again"
|
||||
},
|
||||
"CancelSubscription": {
|
||||
"title": "Жаль, что вы уходите…",
|
||||
"description": "Многие уходят именно в тот момент, когда астролог начинает видеть поворотную точку в их истории.<br></br><br></br>Позвольте задать пару вопросов, чтобы сделать наш сервис лучше - и, возможно, предложить решение, которое больше подходит именно вам.",
|
||||
"stay_button": "Остаться и уменьшить мой план на 50%",
|
||||
"cancel_button": "Отменить"
|
||||
},
|
||||
"Stay50Done": {
|
||||
"title": "Мы ценим твой выбор!",
|
||||
"descriptions": {
|
||||
"1": "План успешно изменен"
|
||||
},
|
||||
"button": "Готово"
|
||||
},
|
||||
"AppreciateChoice": {
|
||||
"title": "Мы ценим твой выбор!",
|
||||
"descriptions": {
|
||||
"1": "Подбираем оптимальное решение...",
|
||||
"2": "Составляем персонализированный опрос...",
|
||||
"3": "Формируем выгодное предложение..."
|
||||
},
|
||||
"button": "Next"
|
||||
},
|
||||
"WhatReason": {
|
||||
"title": "Что стало причиной?",
|
||||
"answers": {
|
||||
"no_promised_result": "Не получил(а) обещанного результата",
|
||||
"too_expensive": "Слишком дорого",
|
||||
"high_auto_payment": "Стоимость автоматической оплаты слишком высока",
|
||||
"unexpected_fee": "Я не ожидал дополнительной платы",
|
||||
"want_pause": "Хочу сделать паузу",
|
||||
"service_not_as_expected": "Сервис оказался не таким, как ожидал(а)",
|
||||
"found_alternative": "Нашёл(а) альтернативу",
|
||||
"dislike_app": "Мне не понравилось приложение",
|
||||
"hard_to_navigate": "В приложении сложно ориентироваться",
|
||||
"other": "Другое"
|
||||
}
|
||||
},
|
||||
"Payment": {
|
||||
"Success": {
|
||||
"title": "Payment successful",
|
||||
"button": "Done"
|
||||
},
|
||||
"Error": {
|
||||
"title": "Payment failed",
|
||||
"button": "Try again"
|
||||
}
|
||||
},
|
||||
"SecondChance": {
|
||||
"title": "Дайте нам второй шанс и получи самый лучший план БЕСПЛАТНО",
|
||||
"offers": {
|
||||
"1": {
|
||||
"title": "Бесплатный план на<br></br>1 месяц",
|
||||
"description": "Используй весь потенциал AURA и даже больше.",
|
||||
"old-price": "1900",
|
||||
"new-price": "0"
|
||||
},
|
||||
"2": {
|
||||
"title": "Бесплатный премиальный план",
|
||||
"description": "Бесплатная 30 мин консультация с премиальным Эдвайзером",
|
||||
"old-price": "4900",
|
||||
"new-price": "0"
|
||||
}
|
||||
},
|
||||
"get_offer": "Получить бесплатный план",
|
||||
"cancel": "Отменить"
|
||||
},
|
||||
"ChangeMind": {
|
||||
"title": "Что может изменить твое мнение?",
|
||||
"answers": {
|
||||
"more_chat_time": "Больше времени в чатах",
|
||||
"more_personal_reports": "Больше персонализированных отчетов",
|
||||
"individual_plan": "Индивидуальный план",
|
||||
"other": "Другое"
|
||||
}
|
||||
},
|
||||
"StopFor30Days": {
|
||||
"title": "Остановите подписку на тридцать дней. Никаких списаний.",
|
||||
"stop": "Остановить",
|
||||
"cancel": "Отменить"
|
||||
},
|
||||
"CancellationOfSubscription": {
|
||||
"title": "Подписка аннулируется!",
|
||||
"description": "Чтобы отменить подписку, нажмите “Подтвердить мои действия”",
|
||||
"offer": {
|
||||
"title": "Бесплатный 2-месячный план",
|
||||
"old-price": "9900",
|
||||
"new-price": "0"
|
||||
},
|
||||
"offer_button": "Применить",
|
||||
"cancel_button": "Я подтверждаю свои действия"
|
||||
},
|
||||
"PlanCancelled": {
|
||||
"title": "Стандартный план Отменен!",
|
||||
"icon": "🥳",
|
||||
"description": "Выполнен переход на бесплатный тридцатидневный план ",
|
||||
"button": "Готово"
|
||||
},
|
||||
"SubscriptionStopped": {
|
||||
"title": "Подписка остановлена успешно!",
|
||||
"icon": "🎉"
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
import type { NextConfig } from "next";
|
||||
import createNextIntlPlugin from "next-intl/plugin";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
env: {
|
||||
@ -15,4 +16,5 @@ const nextConfig: NextConfig = {
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
const withNextIntl = createNextIntlPlugin();
|
||||
export default withNextIntl(nextConfig);
|
||||
|
||||
168
package-lock.json
generated
168
package-lock.json
generated
@ -8,11 +8,14 @@
|
||||
"name": "app-core",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@lottiefiles/dotlottie-react": "^0.14.1",
|
||||
"clsx": "^2.1.1",
|
||||
"next": "15.3.3",
|
||||
"next-intl": "^4.1.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"sass": "^1.89.2"
|
||||
"sass": "^1.89.2",
|
||||
"zod": "^3.25.64"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
@ -199,6 +202,66 @@
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/ecma402-abstract": {
|
||||
"version": "2.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.4.tgz",
|
||||
"integrity": "sha512-qrycXDeaORzIqNhBOx0btnhpD1c+/qFIHAN9znofuMJX6QBwtbrmlpWfD4oiUUD2vJUOIYFA/gYtg2KAMGG7sA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@formatjs/fast-memoize": "2.2.7",
|
||||
"@formatjs/intl-localematcher": "0.6.1",
|
||||
"decimal.js": "^10.4.3",
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/ecma402-abstract/node_modules/@formatjs/intl-localematcher": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.1.tgz",
|
||||
"integrity": "sha512-ePEgLgVCqi2BBFnTMWPfIghu6FkbZnnBVhO2sSxvLfrdFw7wCHAHiDoM2h4NRgjbaY7+B7HgOLZGkK187pZTZg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/fast-memoize": {
|
||||
"version": "2.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz",
|
||||
"integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/icu-messageformat-parser": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.2.tgz",
|
||||
"integrity": "sha512-AfiMi5NOSo2TQImsYAg8UYddsNJ/vUEv/HaNqiFjnI3ZFfWihUtD5QtuX6kHl8+H+d3qvnE/3HZrfzgdWpsLNA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@formatjs/ecma402-abstract": "2.3.4",
|
||||
"@formatjs/icu-skeleton-parser": "1.8.14",
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/icu-skeleton-parser": {
|
||||
"version": "1.8.14",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.14.tgz",
|
||||
"integrity": "sha512-i4q4V4qslThK4Ig8SxyD76cp3+QJ3sAqr7f6q9VVfeGtxG9OhiAk3y9XF6Q41OymsKzsGQ6OQQoJNY4/lI8TcQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@formatjs/ecma402-abstract": "2.3.4",
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/intl-localematcher": {
|
||||
"version": "0.5.10",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.10.tgz",
|
||||
"integrity": "sha512-af3qATX+m4Rnd9+wHcjJ4w2ijq+rAVP3CCinJQvFv1kgSu1W6jypUmvleJxcewdxmutM8dmIRZFxO/IQBZmP2Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "2"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanfs/core": {
|
||||
"version": "0.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||
@ -661,6 +724,24 @@
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@lottiefiles/dotlottie-react": {
|
||||
"version": "0.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@lottiefiles/dotlottie-react/-/dotlottie-react-0.14.1.tgz",
|
||||
"integrity": "sha512-bLZ85h9LzsV6kLrPm9vNhw+OdPemo0AY5PH4bUSnJxRt6ksCUZyHn4PZdTUV7tdCLOyX8fkwqICD+t2/yXkQeA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lottiefiles/dotlottie-web": "0.46.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^17 || ^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/@lottiefiles/dotlottie-web": {
|
||||
"version": "0.46.0",
|
||||
"resolved": "https://registry.npmjs.org/@lottiefiles/dotlottie-web/-/dotlottie-web-0.46.0.tgz",
|
||||
"integrity": "sha512-wirTuVcR/cu9HTckgrXjQggeO4sap4QPBLWDUyBDEQJugY3CLHm4lECrDtLBtIzUJZXTeyidvTOY9lZIQ5qdTw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "0.2.11",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.11.tgz",
|
||||
@ -1189,6 +1270,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@schummar/icu-type-parser": {
|
||||
"version": "1.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@schummar/icu-type-parser/-/icu-type-parser-1.21.5.tgz",
|
||||
"integrity": "sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@swc/counter": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
|
||||
@ -2402,6 +2489,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decimal.js": {
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz",
|
||||
"integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/deep-is": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
||||
@ -3573,6 +3666,18 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/intl-messageformat": {
|
||||
"version": "10.7.16",
|
||||
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.16.tgz",
|
||||
"integrity": "sha512-UmdmHUmp5CIKKjSoE10la5yfU+AYJAaiYLsodbjL4lji83JNvgOQUjGaGhGrpFCb0Uh7sl7qfP1IyILa8Z40ug==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@formatjs/ecma402-abstract": "2.3.4",
|
||||
"@formatjs/fast-memoize": "2.2.7",
|
||||
"@formatjs/icu-messageformat-parser": "2.11.2",
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-array-buffer": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
||||
@ -4268,6 +4373,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/negotiator": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
|
||||
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/next": {
|
||||
"version": "15.3.3",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-15.3.3.tgz",
|
||||
@ -4322,6 +4436,33 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/next-intl": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.1.0.tgz",
|
||||
"integrity": "sha512-JNJRjc7sdnfUxhZmGcvzDszZ60tQKrygV/VLsgzXhnJDxQPn1cN2rVpc53adA1SvBJwPK2O6Sc6b4gYSILjCzw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/amannn"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@formatjs/intl-localematcher": "^0.5.4",
|
||||
"negotiator": "^1.0.0",
|
||||
"use-intl": "^4.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0",
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/node-addon-api": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
|
||||
@ -5549,7 +5690,7 @@
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
@ -5628,6 +5769,20 @@
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/use-intl": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.1.0.tgz",
|
||||
"integrity": "sha512-mQvDYFvoGn+bm/PWvlQOtluKCknsQ5a9F1Cj0hMfBjMBVTwnOqLPd6srhjvVdEQEQFVyHM1PfyifKqKYb11M9Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@formatjs/fast-memoize": "^2.2.0",
|
||||
"@schummar/icu-type-parser": "1.21.5",
|
||||
"intl-messageformat": "^10.5.14"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
@ -5755,6 +5910,15 @@
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.25.67",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.67.tgz",
|
||||
"integrity": "sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,11 +9,16 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@lottiefiles/dotlottie-react": "^0.14.1",
|
||||
"client-only": "^0.0.1",
|
||||
"clsx": "^2.1.1",
|
||||
"idb": "^8.0.3",
|
||||
"next": "15.3.3",
|
||||
"next-intl": "^4.1.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"sass": "^1.89.2",
|
||||
"server-only": "^0.0.1",
|
||||
"zod": "^3.25.64"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -23,6 +28,9 @@
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.3.3",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"prettier": "^3.5.3",
|
||||
"typescript": "^5"
|
||||
}
|
||||
|
||||
BIN
public/retaining/vip_member.png
Normal file
BIN
public/retaining/vip_member.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
BIN
public/retaining/zodiac_circle.png
Normal file
BIN
public/retaining/zodiac_circle.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 195 KiB |
@ -1,10 +0,0 @@
|
||||
import { Spinner } from "@/components/ui";
|
||||
import styles from "./loading.module.scss"
|
||||
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className={styles.coreSpinnerContainer}>
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,7 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { Button, Typography } from '@/components/ui';
|
||||
|
||||
import styles from "./error.module.scss"
|
||||
|
||||
export default function Error({
|
||||
@ -1,4 +1,5 @@
|
||||
import { NavigationBar } from "@/components/layout";
|
||||
import { DrawerProvider, NavigationBar } from "@/components/layout";
|
||||
|
||||
import styles from "./layout.module.scss";
|
||||
|
||||
export default function CoreLayout({
|
||||
@ -6,10 +7,10 @@ export default function CoreLayout({
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return <>
|
||||
return <DrawerProvider>
|
||||
<NavigationBar className={styles.navBar} />
|
||||
<main className={styles.main}>
|
||||
{children}
|
||||
</main>
|
||||
</>
|
||||
</DrawerProvider>
|
||||
}
|
||||
@ -1,6 +1,5 @@
|
||||
import { Suspense } from "react";
|
||||
import { loadAssistants, loadCompatibility, loadMeditations, loadPalms } from "@/entities/dashboard/loaders";
|
||||
import { Horoscope } from "@/components/widgets";
|
||||
|
||||
import {
|
||||
AdvisersSection,
|
||||
AdvisersSectionSkeleton,
|
||||
@ -11,9 +10,13 @@ import {
|
||||
PalmSection,
|
||||
PalmSectionSkeleton
|
||||
} from "@/components/domains/dashboard";
|
||||
import { Horoscope } from "@/components/widgets";
|
||||
import { loadAssistants, loadCompatibility, loadMeditations, loadPalms } from "@/entities/dashboard/loaders";
|
||||
|
||||
import styles from "./page.module.scss";
|
||||
|
||||
export default function Home() {
|
||||
|
||||
return (
|
||||
<section className={styles.page}>
|
||||
<Horoscope />
|
||||
20
src/app/[locale]/(core)/payment/failed/page.tsx
Normal file
20
src/app/[locale]/(core)/payment/failed/page.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
import { AnimatedInfoScreen, LottieAnimation } from "@/components/widgets";
|
||||
import { ROUTES } from "@/shared/constants/client-routes";
|
||||
import { ELottieKeys } from "@/shared/constants/lottie";
|
||||
|
||||
export default async function PaymentFailed() {
|
||||
const t = await getTranslations("Payment.Error");
|
||||
|
||||
return (
|
||||
<AnimatedInfoScreen
|
||||
lottieAnimation={<LottieAnimation loadKey={ELottieKeys.loaderCheckMark} />}
|
||||
title={t("title")}
|
||||
animationTime={0}
|
||||
animationTexts={[]}
|
||||
buttonText={t("button")}
|
||||
nextRoute={ROUTES.home()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
23
src/app/[locale]/(core)/payment/route.ts
Normal file
23
src/app/[locale]/(core)/payment/route.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
import { createPaymentCheckout } from "@/entities/payment/api";
|
||||
import { ROUTES } from "@/shared/constants/client-routes";
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const productId = req.nextUrl.searchParams.get("productId");
|
||||
const placementId = req.nextUrl.searchParams.get("placementId");
|
||||
const paywallId = req.nextUrl.searchParams.get("paywallId");
|
||||
|
||||
const data = await createPaymentCheckout({
|
||||
productId: productId || "",
|
||||
placementId: placementId || "",
|
||||
paywallId: paywallId || "",
|
||||
});
|
||||
|
||||
let redirectUrl: string | URL = data?.paymentUrl;
|
||||
if (!redirectUrl) {
|
||||
redirectUrl = new URL(`${ROUTES.paymentFailed()}`, origin);
|
||||
}
|
||||
|
||||
return NextResponse.redirect(redirectUrl, { status: 307 });
|
||||
}
|
||||
20
src/app/[locale]/(core)/payment/success/page.tsx
Normal file
20
src/app/[locale]/(core)/payment/success/page.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
import { AnimatedInfoScreen, LottieAnimation } from "@/components/widgets";
|
||||
import { ROUTES } from "@/shared/constants/client-routes";
|
||||
import { ELottieKeys } from "@/shared/constants/lottie";
|
||||
|
||||
export default async function PaymentSuccess() {
|
||||
const t = await getTranslations("Payment.Success");
|
||||
|
||||
return (
|
||||
<AnimatedInfoScreen
|
||||
lottieAnimation={<LottieAnimation loadKey={ELottieKeys.loaderCheckMark} />}
|
||||
title={t("title")}
|
||||
animationTime={0}
|
||||
animationTexts={[]}
|
||||
buttonText={t("button")}
|
||||
nextRoute={ROUTES.home()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
88
src/app/[locale]/(core)/profile/Profile.tsx
Normal file
88
src/app/[locale]/(core)/profile/Profile.tsx
Normal file
@ -0,0 +1,88 @@
|
||||
"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>}
|
||||
</>
|
||||
)
|
||||
}
|
||||
51
src/app/[locale]/(core)/profile/page.module.scss
Normal file
51
src/app/[locale]/(core)/profile/page.module.scss
Normal file
@ -0,0 +1,51 @@
|
||||
.card {
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.line {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: #f0f0f0;
|
||||
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;
|
||||
}
|
||||
}
|
||||
8
src/app/[locale]/(core)/profile/page.tsx
Normal file
8
src/app/[locale]/(core)/profile/page.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
import ProfilePage from "./Profile";
|
||||
|
||||
export default function Profile() {
|
||||
|
||||
return (
|
||||
<ProfilePage />
|
||||
)
|
||||
}
|
||||
29
src/app/[locale]/(core)/profile/subscriptions/error.tsx
Normal file
29
src/app/[locale]/(core)/profile/subscriptions/error.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
import { Button, Typography } from '@/components/ui';
|
||||
import { ROUTES } from '@/shared/constants/client-routes';
|
||||
|
||||
import styles from "./page.module.scss"
|
||||
|
||||
export default function Error() {
|
||||
const t = useTranslations("Subscriptions")
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Typography as="h1" className={styles.title}>{t("title")}</Typography>
|
||||
<Typography as='p' align='center'>{t("error")}</Typography>
|
||||
<Button
|
||||
onClick={
|
||||
// () => reset()
|
||||
() => router.push(ROUTES.retainingFunnelCancelSubscription())
|
||||
}
|
||||
>
|
||||
<Typography color='white'>{t("try_again")}</Typography>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,96 @@
|
||||
.container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
padding-inline: 8px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.buttonCancel {
|
||||
color: #ACB0BA;
|
||||
font-size: 16px;
|
||||
line-height: 25px;
|
||||
background: none;
|
||||
border: none;
|
||||
text-decoration: underline;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
min-height: 0;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.description,
|
||||
.error {
|
||||
font-size: 16px;
|
||||
line-height: 25px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: #ACB0BA;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #FF0000;
|
||||
}
|
||||
|
||||
.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;
|
||||
text-align: center;
|
||||
|
||||
&:first-child {
|
||||
border-right: 1px solid #D9D9D9;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.dark-theme) .modal-container {
|
||||
background-color: #343639;
|
||||
|
||||
&>.modal-answers>.modal-answer {
|
||||
color: #1e7dff;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.dark-theme) .modal-title {
|
||||
color: #F7F7F7;
|
||||
}
|
||||
|
||||
:global(.dark-theme) .modal-description {
|
||||
color: #F7F7F7;
|
||||
}
|
||||
23
src/app/[locale]/(core)/profile/subscriptions/page.tsx
Normal file
23
src/app/[locale]/(core)/profile/subscriptions/page.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { Suspense } from "react"
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
import { CancelSubscriptionModalProvider, SubscriptionsList, SubscriptionsListSkeleton } from "@/components/domains/profile/subscriptions";
|
||||
import { Typography } from "@/components/ui";
|
||||
import { loadSubscriptionsData } from "@/entities/subscriptions/loaders";
|
||||
|
||||
import styles from "./page.module.scss"
|
||||
|
||||
export default async function Subscriptions() {
|
||||
const t = await getTranslations("Subscriptions");
|
||||
|
||||
return (
|
||||
<CancelSubscriptionModalProvider>
|
||||
<div className={styles.container}>
|
||||
<Typography as="h1" className={styles.title}>{t("title")}</Typography>
|
||||
<Suspense fallback={<SubscriptionsListSkeleton />}>
|
||||
<SubscriptionsList promise={loadSubscriptionsData()} />
|
||||
</Suspense>
|
||||
</div>
|
||||
</CancelSubscriptionModalProvider>
|
||||
)
|
||||
}
|
||||
27
src/app/[locale]/(core)/retaining/appreciate-choice/page.tsx
Normal file
27
src/app/[locale]/(core)/retaining/appreciate-choice/page.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
import { AnimatedInfoScreen, LottieAnimation } from "@/components/widgets";
|
||||
import { ROUTES } from "@/shared/constants/client-routes";
|
||||
import { ELottieKeys } from "@/shared/constants/lottie";
|
||||
|
||||
export default async function AppreciateChoice() {
|
||||
const t = await getTranslations("AppreciateChoice");
|
||||
|
||||
const animationTexts = [
|
||||
t("descriptions.1"),
|
||||
t("descriptions.2"),
|
||||
t("descriptions.3"),
|
||||
]
|
||||
|
||||
return (
|
||||
<AnimatedInfoScreen
|
||||
lottieAnimation={<LottieAnimation loadKey={ELottieKeys.loaderCheckMark} />}
|
||||
title={t("title")}
|
||||
animationTime={9000}
|
||||
animationTexts={animationTexts}
|
||||
buttonText={t("button")}
|
||||
nextRoute={ROUTES.retainingFunnelWhatReason()}
|
||||
/>
|
||||
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 28px;
|
||||
line-height: 125%;
|
||||
color: #1A1A1A;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 20px;
|
||||
line-height: 125%;
|
||||
color: #2C2C2C;
|
||||
padding-inline: 14px;
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { Buttons } from "@/components/domains/retaining/cancel-subscription";
|
||||
import { Typography } from "@/components/ui";
|
||||
|
||||
import styles from "./page.module.scss";
|
||||
|
||||
export default function CanselSubscription() {
|
||||
const t = useTranslations("CancelSubscription");
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Typography as="h1" size="xl" weight="bold" className={styles.title}>
|
||||
{t("title")}
|
||||
</Typography>
|
||||
<Typography as="p" className={styles.description}>
|
||||
{t.rich("description", {
|
||||
br: () => <br />
|
||||
})}
|
||||
</Typography>
|
||||
<Buttons />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-top: 28px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 27px;
|
||||
line-height: 40px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.description {
|
||||
line-height: 25px;
|
||||
margin-top: 74px;
|
||||
padding-inline: 28px;
|
||||
color: #ACB0BA;
|
||||
}
|
||||
|
||||
.topSellingImage {
|
||||
margin-top: -50px;
|
||||
}
|
||||
|
||||
.offer {
|
||||
margin-top: 12px;
|
||||
background-image: url("/retaining/zodiac_circle.png");
|
||||
background-size: 125%;
|
||||
background-position: center -45px;
|
||||
background-repeat: no-repeat;
|
||||
|
||||
& * {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 14px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: linear-gradient(0deg, #FFFFFF 25.48%, rgba(255, 255, 255, 0) 100%);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
import { Offer } from "@/components/domains/retaining";
|
||||
import { Buttons, LottieAnimations } from "@/components/domains/retaining/cancellation-of-subscription";
|
||||
import { TopSellingSvg } from "@/components/domains/retaining/images";
|
||||
import { Typography } from "@/components/ui";
|
||||
|
||||
import styles from "./page.module.scss";
|
||||
|
||||
export default async function CancellationOfSubscription() {
|
||||
const t = await getTranslations("CancellationOfSubscription");
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<LottieAnimations />
|
||||
<Typography as="h1" weight="bold" className={styles.title}>
|
||||
{t("title")}
|
||||
</Typography>
|
||||
<Typography as="p" align="left" className={styles.description}>
|
||||
{t("description")}
|
||||
</Typography>
|
||||
<Offer
|
||||
className={styles.offer}
|
||||
classNameTitle={styles.titleOffer}
|
||||
title={t("offer.title")}
|
||||
oldPrice={t("offer.old-price")}
|
||||
newPrice={t("offer.new-price")}
|
||||
image={<TopSellingSvg className={styles.topSellingImage} />}
|
||||
active={true}
|
||||
/>
|
||||
<Buttons />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
36
src/app/[locale]/(core)/retaining/change-mind/page.tsx
Normal file
36
src/app/[locale]/(core)/retaining/change-mind/page.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { ChangeMindAnswer, ChangeMindButtons } from "@/components/domains/retaining/change-mind";
|
||||
import { Typography } from "@/components/ui";
|
||||
|
||||
export default function ChangeMind() {
|
||||
const t = useTranslations("ChangeMind")
|
||||
|
||||
const answers: ChangeMindAnswer[] = [
|
||||
{
|
||||
id: 1,
|
||||
title: t("answers.more_chat_time"),
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: t("answers.more_personal_reports"),
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: t("answers.individual_plan"),
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: t("answers.other"),
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography size="xl" weight="bold" as="h1">
|
||||
{t("title")}
|
||||
</Typography>
|
||||
<ChangeMindButtons answers={answers} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
55
src/app/[locale]/(core)/retaining/layout.tsx
Normal file
55
src/app/[locale]/(core)/retaining/layout.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
// import { useSchemeColorByElement } from "@/hooks/useSchemeColorByElement";
|
||||
// import { useRef } from "react";
|
||||
import { RetainingStepper } from "@/components/domains/retaining";
|
||||
import { ROUTES } from "@/shared/constants/client-routes";
|
||||
import { ERetainingFunnel } from "@/types";
|
||||
|
||||
const stepperRoutes: Record<ERetainingFunnel, string[]> = {
|
||||
[ERetainingFunnel.Red]: [
|
||||
ROUTES.retainingFunnelAppreciateChoice(),
|
||||
// ROUTES.retainingFunnelWhatReason(),
|
||||
// ROUTES.retainingFunnelSecondChance(),
|
||||
// ROUTES.retainingFunnelChangeMind(),
|
||||
// ROUTES.retainingFunnelStopFor30Days(),
|
||||
// ROUTES.retainingFunnelCancellationOfSubscription(),
|
||||
],
|
||||
[ERetainingFunnel.Green]: [
|
||||
ROUTES.retainingFunnelAppreciateChoice(),
|
||||
// ROUTES.retainingFunnelWhatReason(),
|
||||
// ROUTES.retainingFunnelStopFor30Days(),
|
||||
// ROUTES.retainingFunnelChangeMind(),
|
||||
// ROUTES.retainingFunnelSecondChance(),
|
||||
// ROUTES.retainingFunnelCancellationOfSubscription(),
|
||||
|
||||
],
|
||||
[ERetainingFunnel.Purple]: [
|
||||
ROUTES.retainingFunnelAppreciateChoice(),
|
||||
// ROUTES.retainingFunnelWhatReason(),
|
||||
// ROUTES.retainingFunnelChangeMind(),
|
||||
// ROUTES.retainingFunnelSecondChance(),
|
||||
// ROUTES.retainingFunnelStopFor30Days(),
|
||||
// ROUTES.retainingFunnelCancellationOfSubscription(),
|
||||
],
|
||||
[ERetainingFunnel.Stay50]: [
|
||||
ROUTES.retainingFunnelStay50Done(),
|
||||
],
|
||||
}
|
||||
|
||||
function StepperLayout({ children }: { children: React.ReactNode; }) {
|
||||
// const darkTheme = useSelector(selectors.selectDarkTheme);
|
||||
// const mainRef = useRef<HTMLDivElement>(null);
|
||||
// useSchemeColorByElement(mainRef.current, "section.page, .page, section", [
|
||||
// location,
|
||||
// ]);
|
||||
// const retainingFunnel = useSelector(selectors.selectRetainingFunnel);
|
||||
const retainingFunnel = ERetainingFunnel.Red;
|
||||
|
||||
return (
|
||||
<>
|
||||
<RetainingStepper stepperRoutes={stepperRoutes[retainingFunnel]} />
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default StepperLayout;
|
||||
@ -0,0 +1,27 @@
|
||||
.container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding: 80px 28px 0;
|
||||
min-height: calc(100dvh - 124px);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 27px;
|
||||
line-height: 40px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 80px;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: #ACB0BA;
|
||||
font-size: 17px;
|
||||
line-height: 25px;
|
||||
margin-top: 72px;
|
||||
}
|
||||
25
src/app/[locale]/(core)/retaining/plan-cancelled/page.tsx
Normal file
25
src/app/[locale]/(core)/retaining/plan-cancelled/page.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
import { PlanCancelledButton } from "@/components/domains/retaining/plan-cancelled";
|
||||
import { Typography } from "@/components/ui";
|
||||
|
||||
import styles from "./page.module.scss";
|
||||
|
||||
export default async function PlanCancelled() {
|
||||
const t = await getTranslations("PlanCancelled");
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Typography as="h1" weight="semiBold" className={styles.title}>
|
||||
{t("title")}
|
||||
</Typography>
|
||||
<span className={styles.icon}>
|
||||
{t("icon")}
|
||||
</span>
|
||||
<PlanCancelledButton />
|
||||
<Typography as="p" className={styles.description}>
|
||||
{t("description")}
|
||||
</Typography>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
.container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
// overflow-x: clip;
|
||||
// padding-inline: 2px;
|
||||
}
|
||||
|
||||
.title {
|
||||
line-height: 150%;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
19
src/app/[locale]/(core)/retaining/second-chance/page.tsx
Normal file
19
src/app/[locale]/(core)/retaining/second-chance/page.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
import { SecondChancePage } from "@/components/domains/retaining/second-chance";
|
||||
import { Typography } from "@/components/ui";
|
||||
|
||||
import styles from "./page.module.scss";
|
||||
|
||||
export default async function SecondChance() {
|
||||
const t = await getTranslations("SecondChance");
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Typography as="h1" weight="bold" size="xl" className={styles.title}>
|
||||
{t("title")}
|
||||
</Typography>
|
||||
<SecondChancePage />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
25
src/app/[locale]/(core)/retaining/stay-50-done/page.tsx
Normal file
25
src/app/[locale]/(core)/retaining/stay-50-done/page.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
import { AnimatedInfoScreen, LottieAnimation } from "@/components/widgets";
|
||||
import { ROUTES } from "@/shared/constants/client-routes";
|
||||
import { ELottieKeys } from "@/shared/constants/lottie";
|
||||
|
||||
export default async function Stay50Done() {
|
||||
const t = await getTranslations("Stay50Done");
|
||||
|
||||
const animationTexts = [
|
||||
t("descriptions.1"),
|
||||
]
|
||||
|
||||
return (
|
||||
<AnimatedInfoScreen
|
||||
lottieAnimation={<LottieAnimation loadKey={ELottieKeys.loaderCheckMark} />}
|
||||
title={t("title")}
|
||||
animationTime={5000}
|
||||
animationTexts={animationTexts}
|
||||
buttonText={t("button")}
|
||||
nextRoute={ROUTES.home()}
|
||||
/>
|
||||
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
.title {
|
||||
font-size: 27px;
|
||||
line-height: 40px;
|
||||
margin: 0;
|
||||
}
|
||||
19
src/app/[locale]/(core)/retaining/stop-for-30-days/page.tsx
Normal file
19
src/app/[locale]/(core)/retaining/stop-for-30-days/page.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
import { StopFor30DaysButtons } from "@/components/domains/retaining/stop-for-30-days";
|
||||
import { Typography } from "@/components/ui";
|
||||
|
||||
import styles from "./page.module.scss";
|
||||
|
||||
export default async function StopFor30Days() {
|
||||
const t = await getTranslations("StopFor30Days");
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Typography as="h1" weight="bold" className={styles.title}>
|
||||
{t("title")}
|
||||
</Typography>
|
||||
<StopFor30DaysButtons />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
.container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding: 80px 28px 0;
|
||||
min-height: calc(100dvh - 124px);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 27px;
|
||||
line-height: 40px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 80px;
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
import { SubscriptionStoppedButton } from "@/components/domains/retaining/subscription-stopped";
|
||||
import { Typography } from "@/components/ui";
|
||||
|
||||
import styles from "./page.module.scss";
|
||||
|
||||
export default async function SubscriptionStopped() {
|
||||
const t = await getTranslations("SubscriptionStopped");
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Typography as="h1" weight="semiBold" className={styles.title}>
|
||||
{t("title")}
|
||||
</Typography>
|
||||
<span className={styles.icon}>
|
||||
{t("icon")}
|
||||
</span>
|
||||
<SubscriptionStoppedButton />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
71
src/app/[locale]/(core)/retaining/what-reason/page.tsx
Normal file
71
src/app/[locale]/(core)/retaining/what-reason/page.tsx
Normal file
@ -0,0 +1,71 @@
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { WhatReasonAnswer, WhatReasonsButtons } from "@/components/domains/retaining/what-reason";
|
||||
import { Typography } from "@/components/ui";
|
||||
import { ERetainingFunnel } from "@/types";
|
||||
|
||||
export default function WhatReason() {
|
||||
const t = useTranslations("WhatReason")
|
||||
|
||||
const answers: WhatReasonAnswer[] = [
|
||||
{
|
||||
id: 1,
|
||||
title: t("answers.no_promised_result"),
|
||||
funnel: ERetainingFunnel.Red
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: t("answers.too_expensive"),
|
||||
funnel: ERetainingFunnel.Red
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: t("answers.high_auto_payment"),
|
||||
funnel: ERetainingFunnel.Red
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: t("answers.unexpected_fee"),
|
||||
funnel: ERetainingFunnel.Red
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: t("answers.want_pause"),
|
||||
funnel: ERetainingFunnel.Green
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
title: t("answers.service_not_as_expected"),
|
||||
funnel: ERetainingFunnel.Red
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
title: t("answers.found_alternative"),
|
||||
funnel: ERetainingFunnel.Red
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
title: t("answers.dislike_app"),
|
||||
funnel: ERetainingFunnel.Purple
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
title: t("answers.hard_to_navigate"),
|
||||
funnel: ERetainingFunnel.Purple
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
title: t("answers.other"),
|
||||
funnel: ERetainingFunnel.Purple
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography size="xl" weight="bold" as="h1">
|
||||
{t("title")}
|
||||
</Typography>
|
||||
<WhatReasonsButtons answers={answers} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
29
src/app/[locale]/auth/callback/route.ts
Normal file
29
src/app/[locale]/auth/callback/route.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
import { ROUTES } from "@/shared/constants/client-routes";
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const { searchParams, origin } = req.nextUrl;
|
||||
const token = searchParams.get("jwtToken");
|
||||
const productId = searchParams.get("productId");
|
||||
const placementId = searchParams.get("placementId");
|
||||
const paywallId = searchParams.get("paywallId");
|
||||
|
||||
const redirectUrl = new URL(`${ROUTES.payment()}`, origin);
|
||||
if (productId) redirectUrl.searchParams.set("productId", productId);
|
||||
if (placementId) redirectUrl.searchParams.set("placementId", placementId);
|
||||
if (paywallId) redirectUrl.searchParams.set("paywallId", paywallId);
|
||||
|
||||
const res = NextResponse.redirect(redirectUrl);
|
||||
|
||||
res.cookies.set({
|
||||
name: "accessToken",
|
||||
value: token || "",
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
maxAge: 60 * 60 * 24 * 365,
|
||||
});
|
||||
return res;
|
||||
}
|
||||
@ -1,9 +1,20 @@
|
||||
import "@/styles/reset.css";
|
||||
import "@/styles/globals.css";
|
||||
|
||||
import type { Metadata } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
import { notFound } from "next/navigation";
|
||||
import { hasLocale, NextIntlClientProvider } from "next-intl";
|
||||
import clsx from "clsx";
|
||||
import "@/styles/globals.css";
|
||||
|
||||
import { routing } from "@/i18n/routing";
|
||||
|
||||
import styles from "./layout.module.scss"
|
||||
|
||||
export function generateStaticParams() {
|
||||
return routing.locales.map((locale) => ({ locale }));
|
||||
}
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ["latin", "cyrillic"],
|
||||
display: "swap",
|
||||
@ -15,14 +26,23 @@ export const metadata: Metadata = {
|
||||
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 function RootLayout({
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
params,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
params: Promise<{ locale: string }>;
|
||||
}>) {
|
||||
const { locale } = await params;
|
||||
if (!hasLocale(routing.locales, locale)) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={clsx(inter.variable, styles.body)}>{children}</body>
|
||||
<html lang={locale}>
|
||||
<body className={clsx(inter.variable, styles.body)}>
|
||||
<NextIntlClientProvider>{children}</NextIntlClientProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
import { Assistant } from "@/entities/dashboard/types"
|
||||
import { Button, Card, Stars, Typography } from "@/components/ui"
|
||||
import { Assistant } from "@/entities/dashboard/types"
|
||||
|
||||
import styles from "./AdviserCard.module.scss"
|
||||
|
||||
type AdviserCardProps = Assistant;
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import Image from "next/image";
|
||||
import { CompatibilityAction } from "@/entities/dashboard/types";
|
||||
|
||||
import { Card, MetaLabel, Typography } from "@/components/ui";
|
||||
import { IconName } from "@/components/ui/Icon/Icon";
|
||||
import { CompatibilityAction } from "@/entities/dashboard/types";
|
||||
|
||||
import styles from "./CompatibilityCard.module.scss";
|
||||
|
||||
type CompatibilityCardProps = CompatibilityAction;
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import Image from "next/image";
|
||||
import { Meditation } from "@/entities/dashboard/types";
|
||||
|
||||
import { Button, Card, Icon, MetaLabel, Typography } from "@/components/ui";
|
||||
import { IconName } from "@/components/ui/Icon/Icon";
|
||||
import { Meditation } from "@/entities/dashboard/types";
|
||||
|
||||
import styles from "./MeditationCard.module.scss"
|
||||
|
||||
type MeditationCardProps = Meditation;
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import Image from "next/image";
|
||||
import { PalmAction } from "@/entities/dashboard/types";
|
||||
|
||||
import { Card, MetaLabel, Typography } from "@/components/ui";
|
||||
import { IconName } from "@/components/ui/Icon/Icon";
|
||||
import { PalmAction } from "@/entities/dashboard/types";
|
||||
|
||||
import styles from "./PalmCard.module.scss";
|
||||
|
||||
type PalmCardProps = PalmAction;
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
import { Assistant } from "@/entities/dashboard/types";
|
||||
import { use } from "react";
|
||||
import { AdviserCard } from "../../cards";
|
||||
|
||||
import { Grid, Section, Skeleton } from "@/components/ui";
|
||||
import { Assistant } from "@/entities/dashboard/types";
|
||||
|
||||
import { AdviserCard } from "../../cards";
|
||||
|
||||
import styles from "./AdvisersSection.module.scss";
|
||||
|
||||
export default function AdvisersSection({ promise }: { promise: Promise<Assistant[]> }) {
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
import { CompatibilityAction } from "@/entities/dashboard/types";
|
||||
import { use } from "react";
|
||||
|
||||
import { Grid, Section, Skeleton } from "@/components/ui";
|
||||
import { CompatibilityAction } from "@/entities/dashboard/types";
|
||||
|
||||
import { CompatibilityCard } from "../../cards";
|
||||
|
||||
import styles from "./CompatibilitySection.module.scss";
|
||||
|
||||
export default function CompatibilitySection({ promise }: { promise: Promise<CompatibilityAction[]> }) {
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
import { Meditation } from "@/entities/dashboard/types";
|
||||
import { use } from "react";
|
||||
|
||||
import { Grid, Section, Skeleton } from "@/components/ui";
|
||||
import { Meditation } from "@/entities/dashboard/types";
|
||||
|
||||
import { MeditationCard } from "../../cards";
|
||||
|
||||
import styles from "./MeditationSection.module.scss";
|
||||
|
||||
export default function MeditationSection({ promise }: { promise: Promise<Meditation[]> }) {
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
import { PalmAction } from "@/entities/dashboard/types";
|
||||
import { use } from "react";
|
||||
|
||||
import { Grid, Section, Skeleton } from "@/components/ui";
|
||||
import { PalmAction } from "@/entities/dashboard/types";
|
||||
|
||||
import { PalmCard } from "../../cards";
|
||||
|
||||
import styles from "./PalmSection.module.scss";
|
||||
|
||||
export default function PalmSection({ promise }: { promise: Promise<PalmAction[]> }) {
|
||||
|
||||
35
src/components/domains/profile/Billing/Billing.module.scss
Normal file
35
src/components/domains/profile/Billing/Billing.module.scss
Normal file
@ -0,0 +1,35 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.button {
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.credits {
|
||||
padding: 16px;
|
||||
background-color: #275ca7;
|
||||
border-radius: 12px;
|
||||
width: 100%;
|
||||
|
||||
.creditsDescription {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.anyQuestions {
|
||||
width: 100%;
|
||||
|
||||
&>a {
|
||||
color: #275ca7;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.subscriptionUpdate {
|
||||
width: 100%;
|
||||
line-height: 1.25;
|
||||
}
|
||||
64
src/components/domains/profile/Billing/Billing.tsx
Normal file
64
src/components/domains/profile/Billing/Billing.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
"use client;"
|
||||
|
||||
import Link from "next/link";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { Button, Typography } from "@/components/ui";
|
||||
|
||||
import styles from "./Billing.module.scss"
|
||||
|
||||
interface IBillingProps {
|
||||
onBilling: () => void;
|
||||
}
|
||||
|
||||
function Billing({ onBilling }: IBillingProps) {
|
||||
const t = useTranslations('Profile.billing');
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Button
|
||||
className={styles.button}
|
||||
onClick={onBilling}
|
||||
>
|
||||
<Typography size="xl" color="white">
|
||||
{t("billing_button")}
|
||||
</Typography>
|
||||
</Button>
|
||||
<div className={styles.credits}>
|
||||
<Typography as="p" weight="bold" color="white" align="left">
|
||||
{t("credits.title", {
|
||||
credits: String(0)
|
||||
})}
|
||||
</Typography>
|
||||
<Typography className={styles.creditsDescription} as="p" size="sm" color="white" align="left">
|
||||
{t("credits.description")}
|
||||
</Typography>
|
||||
</div>
|
||||
<Typography as="p" weight="bold" align="left" className={styles.anyQuestions}>
|
||||
{t.rich("any_questions", {
|
||||
link: (chunks) => (
|
||||
<Link
|
||||
href="https://witapps.us/en#contact-us"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{chunks}
|
||||
</Link>
|
||||
),
|
||||
linkText: t("any_questions_link")
|
||||
})}
|
||||
</Typography>
|
||||
<Typography as="p" align="left" color="secondary" className={styles.subscriptionUpdate}>
|
||||
{t.rich("subscription_update", {
|
||||
bold: (chunks) => (
|
||||
<Typography weight="bold" color="secondary">{chunks}</Typography>
|
||||
),
|
||||
subscriptionUpdateBold: t("subscription_update_bold"),
|
||||
br: () => <br />
|
||||
})}
|
||||
</Typography>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Billing
|
||||
3
src/components/domains/profile/LogOut/LogOut.module.scss
Normal file
3
src/components/domains/profile/LogOut/LogOut.module.scss
Normal file
@ -0,0 +1,3 @@
|
||||
.button {
|
||||
min-height: 60px;
|
||||
}
|
||||
24
src/components/domains/profile/LogOut/LogOut.tsx
Normal file
24
src/components/domains/profile/LogOut/LogOut.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { Button, Typography } from "@/components/ui";
|
||||
|
||||
import styles from "./LogOut.module.scss"
|
||||
|
||||
interface ILogOutProps {
|
||||
onLogout: () => void;
|
||||
}
|
||||
|
||||
function LogOut({ onLogout }: ILogOutProps) {
|
||||
const t = useTranslations('Profile.log_out');
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={styles.button}
|
||||
onClick={onLogout}
|
||||
>
|
||||
<Typography size="xl" color="white">{t("log_out_button")}</Typography>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export default LogOut
|
||||
@ -0,0 +1,23 @@
|
||||
.container {
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
&>.title {
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
&>.description {
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 100%;
|
||||
margin-top: 16px;
|
||||
}
|
||||
31
src/components/domains/profile/ProfileBlock/ProfileBlock.tsx
Normal file
31
src/components/domains/profile/ProfileBlock/ProfileBlock.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import { Typography } from "@/components/ui"
|
||||
|
||||
import styles from "./ProfileBlock.module.scss"
|
||||
|
||||
interface ProfileBlockProps {
|
||||
title: string
|
||||
description?: string
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
function ProfileBlock({ title, description, children }: ProfileBlockProps) {
|
||||
return (
|
||||
<section className={styles.container}>
|
||||
<header className={styles.header}>
|
||||
<Typography className={styles.title} as="h2" size="xl" weight="semiBold" align="left">
|
||||
{title}
|
||||
</Typography>
|
||||
{description &&
|
||||
<Typography className={styles.description} size="sm" align="left">
|
||||
{description}
|
||||
</Typography>
|
||||
}
|
||||
</header>
|
||||
{!!children && <div className={styles.content}>
|
||||
{children}
|
||||
</div>}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProfileBlock
|
||||
@ -0,0 +1,16 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.input {
|
||||
background-color: #f3f3f3;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.inputContainer {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl"
|
||||
|
||||
import { EmailInput, NameInput } from "@/components/ui";
|
||||
|
||||
import styles from "./ProfileInformation.module.scss"
|
||||
|
||||
function ProfileInformation() {
|
||||
const t = useTranslations('Profile');
|
||||
// const email = useSelector(selectors.selectEmail) || ""
|
||||
// const name = useSelector(selectors.selectUser)?.username || ""
|
||||
const email = "Test email"
|
||||
const name = "Test name"
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<EmailInput
|
||||
name="email"
|
||||
value={email}
|
||||
placeholder={t("profile_information.email_placeholder")}
|
||||
inputContainerClassName={styles.inputContainer}
|
||||
inputClassName={styles.input}
|
||||
onValid={() => { }}
|
||||
onInvalid={() => { }}
|
||||
readonly
|
||||
/>
|
||||
<NameInput
|
||||
value={name}
|
||||
placeholder={t("profile_information.name_placeholder")}
|
||||
inputContainerClassName={styles.inputContainer}
|
||||
inputClassName={styles.input}
|
||||
onValid={() => { }}
|
||||
onInvalid={() => { }}
|
||||
readonly
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProfileInformation
|
||||
4
src/components/domains/profile/index.ts
Normal file
4
src/components/domains/profile/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export { default as Billing } from "./Billing/Billing"
|
||||
export { default as LogOut } from "./LogOut/LogOut"
|
||||
export { default as ProfileBlock } from "./ProfileBlock/ProfileBlock"
|
||||
export { default as ProfileInformation } from "./ProfileInformation/ProfileInformation"
|
||||
@ -0,0 +1,75 @@
|
||||
"use client";
|
||||
|
||||
import { createContext, ReactNode,useContext, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { Button, Typography } from "@/components/ui";
|
||||
import Modal from "@/components/ui/Modal/Modal";
|
||||
import { UserSubscription } from "@/entities/subscriptions/types";
|
||||
import { ROUTES } from "@/shared/constants/client-routes";
|
||||
|
||||
import styles from "./CancelSubscriptionModalProvider.module.scss";
|
||||
|
||||
type Ctx = { open: (sub: UserSubscription) => void };
|
||||
|
||||
const Context = createContext<Ctx | null>(null);
|
||||
export const useCancelSubscriptionModal = () => {
|
||||
const ctx = useContext(Context);
|
||||
if (!ctx) throw new Error("useCancelSubscriptionModal must be inside provider");
|
||||
return ctx;
|
||||
};
|
||||
|
||||
export default function CancelSubscriptionModalProvider({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const router = useRouter()
|
||||
const t = useTranslations("Subscriptions");
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const close = () => setIsOpen(false);
|
||||
const open = (
|
||||
// _sub: UserSubscription
|
||||
) => {
|
||||
setIsOpen(true)
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
router.push(ROUTES.retainingFunnelCancelSubscription())
|
||||
close();
|
||||
};
|
||||
|
||||
return (
|
||||
<Context.Provider value={{ open }}>
|
||||
{children}
|
||||
|
||||
<Modal
|
||||
open={!!isOpen}
|
||||
onClose={close}
|
||||
isCloseButtonVisible={false}
|
||||
className={styles.overlay}
|
||||
modalClassName={styles.modal}
|
||||
>
|
||||
<Typography as="h4" className={styles.title}>
|
||||
{t("modal.title")}
|
||||
</Typography>
|
||||
<Typography as="p" className={styles.description}>
|
||||
{t("modal.description")}
|
||||
</Typography>
|
||||
|
||||
<div className={styles.actions}>
|
||||
<Button onClick={handleCancel}>{t("modal.cancel_button")}</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={close}
|
||||
className={styles.stayButton}
|
||||
>
|
||||
{t("modal.stay_button")}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</Context.Provider>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
.container {
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
background-color: #f0f0f4;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding: 16px 8px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.cell {
|
||||
width: 100%;
|
||||
line-height: 1.5;
|
||||
font-size: 16px;
|
||||
color: #7d8785;
|
||||
|
||||
&:nth-child(2) {
|
||||
color: #090909;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
"use client";
|
||||
|
||||
import { ReactNode } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { Button } from "@/components/ui";
|
||||
import { Table } from "@/components/widgets";
|
||||
import { UserSubscription } from "@/entities/subscriptions/types";
|
||||
import { formatDate } from "@/shared/utils/date";
|
||||
import { getFormattedPrice } from "@/shared/utils/price";
|
||||
import { Currency } from "@/types";
|
||||
|
||||
import { useCancelSubscriptionModal } from "../CancelSubscriptionModalProvider/CancelSubscriptionModalProvider";
|
||||
|
||||
import styles from "./SubscriptionTable.module.scss"
|
||||
|
||||
interface ITableProps {
|
||||
subscription: UserSubscription;
|
||||
}
|
||||
|
||||
export default function SubscriptionTable({ subscription }: ITableProps) {
|
||||
const t = useTranslations("Subscriptions");
|
||||
const { open } = useCancelSubscriptionModal();
|
||||
|
||||
const tableData: ReactNode[][] = [
|
||||
[t("table.subscription_type"), t(`table.subscription_type_value.${subscription.subscriptionType}`)],
|
||||
[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.renewal_date"), formatDate(subscription.renewalDate)],
|
||||
[t("table.renewal_amount"), getFormattedPrice(subscription.renewalAmount, Currency[subscription.currency])],
|
||||
]
|
||||
|
||||
if (subscription.subscriptionStatus === "ACTIVE") {
|
||||
tableData.push([
|
||||
<Button key={"cancel-subscription"} className={styles.buttonCancel} onClick={() => open(subscription)}>
|
||||
{t("table.cancel_subscription")}
|
||||
</Button>
|
||||
])
|
||||
}
|
||||
|
||||
return (
|
||||
<Table data={tableData} />
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
import { use } from "react";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
import { Typography } from "@/components/ui";
|
||||
import { Skeleton } from "@/components/ui";
|
||||
import { UserSubscription } from "@/entities/subscriptions/types";
|
||||
|
||||
import SubscriptionTable from "../SubscriptionTable/SubscriptionTable";
|
||||
|
||||
import styles from "./SubscriptionsList.module.scss"
|
||||
|
||||
export default function SubscriptionsList(
|
||||
{ promise }: { promise: Promise<UserSubscription[]> }
|
||||
) {
|
||||
const t = use(getTranslations("Subscriptions"));
|
||||
|
||||
const subscriptions = use(promise);
|
||||
|
||||
if (subscriptions?.length === 0) {
|
||||
return <div className={styles.container}>
|
||||
<Typography as="h1" className={styles.title}>{t("title")}</Typography>
|
||||
<Typography as="p" className={styles.description}>{t("no_subscriptions")}</Typography>
|
||||
</div>
|
||||
}
|
||||
|
||||
return <>
|
||||
{subscriptions.map((subscription) => {
|
||||
return <SubscriptionTable subscription={subscription} key={subscription.id} />
|
||||
})}
|
||||
</>
|
||||
}
|
||||
|
||||
export function SubscriptionsListSkeleton() {
|
||||
return (
|
||||
<Skeleton style={{ height: "300px" }} className={styles.skeleton} />
|
||||
)
|
||||
}
|
||||
3
src/components/domains/profile/subscriptions/index.ts
Normal file
3
src/components/domains/profile/subscriptions/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
|
||||
export { default as CancelSubscriptionModalProvider } from "./CancelSubscriptionModalProvider/CancelSubscriptionModalProvider"
|
||||
export { default as SubscriptionsList, SubscriptionsListSkeleton } from "./SubscriptionsList/SubscriptionsList"
|
||||
22
src/components/domains/retaining/Button/Button.module.scss
Normal file
22
src/components/domains/retaining/Button/Button.module.scss
Normal file
@ -0,0 +1,22 @@
|
||||
.button {
|
||||
min-height: 71px;
|
||||
border-radius: 8px;
|
||||
font-size: 28px;
|
||||
font-weight: normal;
|
||||
background: #F1F1F1;
|
||||
background-blend-mode: color;
|
||||
box-shadow: 2px 5px 2.5px #00000025;
|
||||
color: #121620;
|
||||
transition: background 0.3s ease, color 0.3s ease;
|
||||
will-change: background, color;
|
||||
padding: 25px;
|
||||
line-height: 1;
|
||||
word-break: break-word;
|
||||
min-width: none;
|
||||
|
||||
&.active {
|
||||
background: linear-gradient(to right, #057dd4 23%, #224e90 74%, #0c6bc3 94%),
|
||||
linear-gradient(-45deg, #3a617120 9%, #21212120 72%, #21895120 96%);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
18
src/components/domains/retaining/Button/Button.tsx
Normal file
18
src/components/domains/retaining/Button/Button.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import MainButton, { ButtonProps as MainButtonProps } from "@/components/ui/Button/Button";
|
||||
|
||||
import styles from "./Button.module.scss";
|
||||
|
||||
interface ButtonProps extends MainButtonProps {
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
function Button(props: ButtonProps) {
|
||||
const { active, ...buttonProps } = props;
|
||||
return (
|
||||
<MainButton {...buttonProps} className={`${styles.button} ${props.className} ${active ? styles.active : ""}`}>
|
||||
{props.children}
|
||||
</MainButton>
|
||||
);
|
||||
}
|
||||
|
||||
export default Button;
|
||||
45
src/components/domains/retaining/CheckMark/CheckMark.tsx
Normal file
45
src/components/domains/retaining/CheckMark/CheckMark.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
|
||||
|
||||
interface CheckMarkProps {
|
||||
active: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function CheckMark({ active, className = "" }: CheckMarkProps) {
|
||||
return (
|
||||
<>
|
||||
{active &&
|
||||
<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(#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" />
|
||||
</g>
|
||||
</g>
|
||||
<rect x="1" y="1" width="28" height="28" rx="14" stroke="#1172AC" strokeWidth="2" />
|
||||
<defs>
|
||||
<clipPath id="clip0_49_4129">
|
||||
<rect width="30" height="30" rx="15" fill="white" />
|
||||
</clipPath>
|
||||
<clipPath id="clip1_49_4129">
|
||||
<rect width="30" height="30" rx="15" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
}
|
||||
{!active &&
|
||||
<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_4525)">
|
||||
</g>
|
||||
<rect x="1" y="1" width="28" height="28" rx="14" stroke="#1172AC" strokeWidth="2" />
|
||||
<defs>
|
||||
<clipPath id="clip0_49_4525">
|
||||
<rect width="30" height="30" rx="15" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default CheckMark
|
||||
62
src/components/domains/retaining/Offer/Offer.module.scss
Normal file
62
src/components/domains/retaining/Offer/Offer.module.scss
Normal file
@ -0,0 +1,62 @@
|
||||
.container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
border-radius: 14px;
|
||||
padding: 72px 20px 65px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
box-shadow: 0px 0px 10.2px 0px rgba(0, 0, 0, 0.25);
|
||||
border: 4px solid transparent;
|
||||
cursor: pointer;
|
||||
|
||||
&.active {
|
||||
border: 4px solid rgba(17, 114, 172, 1)
|
||||
}
|
||||
|
||||
&>.checkMark {
|
||||
position: absolute;
|
||||
top: 21px;
|
||||
right: 14px;
|
||||
}
|
||||
|
||||
&>.title {
|
||||
margin: 0;
|
||||
margin-top: 4px;
|
||||
color: #323232;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
|
||||
&>.description {
|
||||
margin-top: 14px;
|
||||
color: #323232;
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
text-align: left;
|
||||
padding-inline: 7px;
|
||||
}
|
||||
|
||||
&>.priceContainer {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin-top: 33px;
|
||||
|
||||
&>.oldPrice {
|
||||
color: #C4C4C4;
|
||||
font-size: 28px;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
&>.newPrice {
|
||||
color: #000;
|
||||
font-size: 36px;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
63
src/components/domains/retaining/Offer/Offer.tsx
Normal file
63
src/components/domains/retaining/Offer/Offer.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
// import { useSelector } from "react-redux";
|
||||
// import { selectors } from "@/store";
|
||||
import clsx from "clsx";
|
||||
|
||||
import { Typography } from "@/components/ui";
|
||||
import { getFormattedPrice } from "@/shared/utils/price";
|
||||
import { Currency } from "@/types";
|
||||
|
||||
import styles from "./Offer.module.scss";
|
||||
|
||||
import { CheckMark } from "..";
|
||||
|
||||
interface OfferProps {
|
||||
title?: string | React.ReactNode;
|
||||
description?: string;
|
||||
oldPrice?: number | string;
|
||||
newPrice?: number | string;
|
||||
active?: boolean;
|
||||
image?: React.ReactNode;
|
||||
className?: string;
|
||||
classNameTitle?: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
function Offer({
|
||||
title,
|
||||
description,
|
||||
oldPrice,
|
||||
newPrice,
|
||||
active = false,
|
||||
onClick,
|
||||
image,
|
||||
className = "",
|
||||
classNameTitle = ""
|
||||
}: OfferProps) {
|
||||
// const currency = useSelector(selectors.selectCurrency);
|
||||
const currency = Currency.USD;
|
||||
|
||||
return (
|
||||
<div className={clsx(styles.container, active && styles.active, className)} onClick={onClick}>
|
||||
<CheckMark active={active} className={styles.checkMark} />
|
||||
|
||||
{image}
|
||||
|
||||
<Typography as="h3" weight="bold" className={clsx(styles.title, classNameTitle)}>
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography as="p" className={styles.description}>
|
||||
{description}
|
||||
</Typography>
|
||||
<div className={styles.priceContainer}>
|
||||
<Typography weight="bold" className={styles.oldPrice}>
|
||||
{getFormattedPrice(Number(oldPrice), currency)}
|
||||
</Typography>
|
||||
<Typography weight="bold" className={styles.newPrice}>
|
||||
{getFormattedPrice(Number(newPrice), currency)}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Offer
|
||||
@ -0,0 +1,3 @@
|
||||
.stepper-bar {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
"use client";
|
||||
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
import { StepperBar } from "@/components/layout";
|
||||
|
||||
import styles from "./RetainingStepper.module.scss"
|
||||
|
||||
|
||||
export default function RetainingStepper({
|
||||
stepperRoutes,
|
||||
}: {
|
||||
stepperRoutes: string[];
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
|
||||
const getCurrentStep = () => {
|
||||
// if ([
|
||||
// ROUTES.retainingFunnelPlanCancelled(),
|
||||
// ROUTES.retainingFunnelSubscriptionStopped(),
|
||||
// ].some(route => location.pathname.includes(route))) {
|
||||
// return stepperRoutes[retainingFunnel].length;
|
||||
// }
|
||||
let index = 0;
|
||||
for (const route of stepperRoutes) {
|
||||
if (pathname.includes(route)) {
|
||||
return index + 1;
|
||||
}
|
||||
index++;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
// логика выбора шага по pathname
|
||||
return (
|
||||
<StepperBar
|
||||
length={stepperRoutes.length}
|
||||
currentStep={getCurrentStep()}
|
||||
// color={darkTheme ? "#B2BCFF" : "#353E75"}
|
||||
color={"#353E75"}
|
||||
className={styles["stepper-bar"]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
.buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
|
||||
&>.button {
|
||||
position: relative;
|
||||
min-height: 110px;
|
||||
display: grid;
|
||||
grid-template-columns: 75px 1fr;
|
||||
gap: 13px;
|
||||
text-align: left;
|
||||
border-radius: 21px;
|
||||
|
||||
&>.buttonIcon {
|
||||
font-size: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&>.loaderContainer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 21px;
|
||||
backdrop-filter: blur(3px);
|
||||
transition: background-color 0.3s ease-in-out;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,66 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { Spinner, Typography } from "@/components/ui"
|
||||
import { useLottie } from "@/hooks/lottie/useLottie";
|
||||
import { ROUTES } from "@/shared/constants/client-routes";
|
||||
import { ELottieKeys } from "@/shared/constants/lottie";
|
||||
|
||||
import { RetainingButton } from "../.."
|
||||
|
||||
import styles from "./Buttons.module.scss"
|
||||
|
||||
export default function Buttons() {
|
||||
const t = useTranslations("CancelSubscription");
|
||||
const router = useRouter();
|
||||
useLottie({
|
||||
preloadKey: ELottieKeys.loaderCheckMark,
|
||||
});
|
||||
|
||||
const [activeButton, setActiveButton] = useState<"stay" | "cancel">();
|
||||
const [isLoadingButton, setIsLoadingButton] = useState<"stay" | "cancel">();
|
||||
|
||||
const handleCancelButtonClick = () => {
|
||||
if (isLoadingButton) return;
|
||||
setActiveButton("cancel");
|
||||
const timer = setTimeout(() => {
|
||||
router.push(ROUTES.retainingFunnelAppreciateChoice());
|
||||
}, 1000);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
|
||||
const handleStayButtonClick = async () => {
|
||||
if (isLoadingButton) return;
|
||||
setActiveButton("stay");
|
||||
setIsLoadingButton("stay");
|
||||
|
||||
// const response = await api.userSubscriptionAction({
|
||||
// subscriptionId: cancellingSubscriptionId,
|
||||
// action: "discount_50",
|
||||
// token
|
||||
// });
|
||||
// if (response.status === "success") {
|
||||
// dispatch(actions.retainingFunnel.setFunnel(ERetainingFunnel.Stay50));
|
||||
// }
|
||||
router.push(ROUTES.retainingFunnelStay50Done());
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.buttons}>
|
||||
<RetainingButton className={styles.button} active={activeButton === "stay"} onClick={handleStayButtonClick}>
|
||||
{isLoadingButton === "stay" && <div className={styles.loaderContainer}>
|
||||
<Spinner />
|
||||
</div>}
|
||||
<Typography className={styles.buttonIcon}>🙋♀️</Typography>
|
||||
{t("stay_button")}
|
||||
</RetainingButton>
|
||||
<RetainingButton className={styles.button} active={activeButton === "cancel"} onClick={handleCancelButtonClick}>
|
||||
<Typography className={styles.buttonIcon}>🙅♀️</Typography>
|
||||
{t("cancel_button")}
|
||||
</RetainingButton>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export { default as Buttons } from "./Buttons/Buttons"
|
||||
@ -0,0 +1,60 @@
|
||||
.buttonOffer {
|
||||
margin-top: 22px;
|
||||
font-size: 28px;
|
||||
line-height: 20px;
|
||||
position: relative;
|
||||
|
||||
&>.loaderContainer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
border-radius: inherit;
|
||||
backdrop-filter: blur(3px);
|
||||
transition: background-color 0.3s ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
.buttonCancel {
|
||||
color: #ACB0BA;
|
||||
font-size: 16px;
|
||||
line-height: 25px;
|
||||
background: none;
|
||||
border: none;
|
||||
margin-top: 12px;
|
||||
text-decoration: underline;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
|
||||
&>.loaderContainer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: inherit;
|
||||
backdrop-filter: blur(3px);
|
||||
transition: background-color 0.3s ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
.toast {
|
||||
position: fixed;
|
||||
bottom: calc(0dvh + 16px);
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin-inline: auto;
|
||||
padding-inline: 16px;
|
||||
max-width: 560px;
|
||||
z-index: 1000;
|
||||
}
|
||||
@ -0,0 +1,82 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { Spinner, Toast } from "@/components/ui";
|
||||
|
||||
import { RetainingButton } from "../.."
|
||||
|
||||
import styles from "./Buttons.module.scss"
|
||||
// import { useRouter } from "next/navigation";
|
||||
// import { ROUTES } from "@/shared/constants/client-routes";
|
||||
|
||||
export default function Buttons() {
|
||||
const t = useTranslations("CancellationOfSubscription");
|
||||
// const router = useRouter();
|
||||
|
||||
const [isToastVisible, setIsToastVisible] = useState(false);
|
||||
const [isLoadingOfferButton, setIsLoadingOfferButton] = useState<boolean>(false);
|
||||
const [isLoadingCancelButton, setIsLoadingCancelButton] = useState<boolean>(false);
|
||||
|
||||
const handleOfferButtonClick = async () => {
|
||||
if (isLoadingOfferButton || isLoadingCancelButton) return;
|
||||
setIsLoadingOfferButton(true);
|
||||
// const response = await api.userSubscriptionAction({
|
||||
// subscriptionId: cancellingSubscriptionId,
|
||||
// action: "pause_60",
|
||||
// token
|
||||
// });
|
||||
// if (response.status === "success") {
|
||||
// navigate(routes.client.retainingFunnelSubscriptionStopped());
|
||||
// }
|
||||
}
|
||||
|
||||
const handleCancelClick = async () => {
|
||||
if (isToastVisible || isLoadingOfferButton || isLoadingCancelButton) return;
|
||||
setIsLoadingCancelButton(true);
|
||||
setIsToastVisible(true);
|
||||
// const response = await api.userSubscriptionAction({
|
||||
// subscriptionId: cancellingSubscriptionId,
|
||||
// action: "cancel",
|
||||
// token
|
||||
// });
|
||||
// if (response.status === "success") {
|
||||
// setIsToastVisible(true);
|
||||
// const timer = setTimeout(() => {
|
||||
// router.push(ROUTES.profile());
|
||||
// }, 7000);
|
||||
// return () => clearTimeout(timer);
|
||||
// }
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<RetainingButton
|
||||
className={styles.buttonOffer}
|
||||
active={true}
|
||||
onClick={handleOfferButtonClick}
|
||||
>
|
||||
{isLoadingOfferButton && <div className={styles.loaderContainer}>
|
||||
<Spinner />
|
||||
</div>}
|
||||
{t("offer_button")}
|
||||
</RetainingButton>
|
||||
<RetainingButton
|
||||
className={styles.buttonCancel}
|
||||
onClick={handleCancelClick}
|
||||
>
|
||||
{isLoadingCancelButton && <div className={styles.loaderContainer}>
|
||||
<Spinner />
|
||||
</div>}
|
||||
{t("cancel_button")}
|
||||
</RetainingButton>
|
||||
{isToastVisible && <Toast
|
||||
classNameContainer={styles.toast}
|
||||
variant="success"
|
||||
>
|
||||
Ваша подписка будет аннулирована!
|
||||
</Toast>}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
.lottie-animation-container-confetti {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100dvw;
|
||||
height: 100dvh;
|
||||
z-index: 9999;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.lottie-animation-confetti {
|
||||
width: 100dvw;
|
||||
height: 100dvh;
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { LottieAnimation } from "@/components/widgets";
|
||||
import { ELottieKeys } from "@/shared/constants/lottie";
|
||||
|
||||
import styles from "./LottieAnimations.module.scss";
|
||||
|
||||
export default function LottieAnimations() {
|
||||
const [isConfettiVisible, setIsConfettiVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setIsConfettiVisible(true);
|
||||
}, 2000);
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<LottieAnimation
|
||||
loadKey={ELottieKeys.loaderCheckMark2}
|
||||
width={100}
|
||||
height={100}
|
||||
animationProps={{
|
||||
style: {
|
||||
backgroundColor: "transparent"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{isConfettiVisible &&
|
||||
<LottieAnimation
|
||||
loadKey={ELottieKeys.confetti}
|
||||
width={"100dvw"}
|
||||
height={"100dvh"}
|
||||
className={styles["lottie-animation-container-confetti"]}
|
||||
animationProps={{
|
||||
className: styles["lottie-animation-confetti"]
|
||||
}}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
export { default as Buttons } from "./Buttons/Buttons";
|
||||
export { default as LottieAnimations } from "./LottieAnimations/LottieAnimations";
|
||||
@ -0,0 +1,10 @@
|
||||
.answers {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 24px;
|
||||
gap: 16px;
|
||||
|
||||
&>.answer {
|
||||
border-radius: 21px;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,64 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { ROUTES } from "@/shared/constants/client-routes";
|
||||
import { ERetainingFunnel } from "@/types"
|
||||
|
||||
import { RetainingButton } from "../..";
|
||||
|
||||
import styles from './Buttons.module.scss'
|
||||
|
||||
export interface ChangeMindAnswer {
|
||||
id: number;
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface ButtonsProps {
|
||||
answers: ChangeMindAnswer[]
|
||||
}
|
||||
|
||||
export default function Buttons({
|
||||
answers
|
||||
}: ButtonsProps) {
|
||||
const router = useRouter()
|
||||
// usePreloadImages([
|
||||
// images("vip_member.png")
|
||||
// ])
|
||||
|
||||
const [activeAnswer, setActiveAnswer] = useState<ChangeMindAnswer | null>(null);
|
||||
// const retainingFunnel = useSelector(selectors.selectRetainingFunnel);
|
||||
const retainingFunnel = ERetainingFunnel.Red;
|
||||
|
||||
const handleNext = (answer: ChangeMindAnswer) => {
|
||||
setActiveAnswer(answer);
|
||||
const timer = setTimeout(() => {
|
||||
if (retainingFunnel === ERetainingFunnel.Red) {
|
||||
router.push(ROUTES.retainingFunnelStopFor30Days());
|
||||
}
|
||||
// if (retainingFunnel === ERetainingFunnel.Green) {
|
||||
// router.push(ROUTES.retainingFunnelSecondChance());
|
||||
// }
|
||||
// if (retainingFunnel === ERetainingFunnel.Purple) {
|
||||
// router.push(ROUTES.retainingFunnelSecondChance());
|
||||
// }
|
||||
}, 1000);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.answers}>
|
||||
{answers.map((answer) => (
|
||||
<RetainingButton
|
||||
key={answer.id}
|
||||
className={styles.answer}
|
||||
active={activeAnswer?.id === answer.id}
|
||||
onClick={() => handleNext(answer)}
|
||||
>
|
||||
{answer.title}
|
||||
</RetainingButton>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
1
src/components/domains/retaining/change-mind/index.tsx
Normal file
1
src/components/domains/retaining/change-mind/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export { type ChangeMindAnswer,default as ChangeMindButtons } from "./Buttons/Buttons"
|
||||
34
src/components/domains/retaining/images/EyeSvg/EyeSvg.tsx
Normal file
34
src/components/domains/retaining/images/EyeSvg/EyeSvg.tsx
Normal file
File diff suppressed because one or more lines are too long
@ -0,0 +1,33 @@
|
||||
import { SVGProps } from "react"
|
||||
|
||||
|
||||
type TopSellingSvgProps = SVGProps<SVGSVGElement>
|
||||
|
||||
function TopSellingSVG(props: TopSellingSvgProps) {
|
||||
return (
|
||||
<svg width="227" height="232" viewBox="0 0 227 232" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<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" />
|
||||
<circle cx="112.503" 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="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>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default TopSellingSVG
|
||||
2
src/components/domains/retaining/images/index.ts
Normal file
2
src/components/domains/retaining/images/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { default as EyeSvg } from "./EyeSvg/EyeSvg";
|
||||
export { default as TopSellingSvg } from "./TopSellingSvg/TopSellingSvg";
|
||||
4
src/components/domains/retaining/index.ts
Normal file
4
src/components/domains/retaining/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export { default as RetainingButton } from "./Button/Button"
|
||||
export { default as CheckMark } from "./CheckMark/CheckMark"
|
||||
export { default as Offer } from "./Offer/Offer"
|
||||
export { default as RetainingStepper } from "./RetainingStepper/RetainingStepper"
|
||||
@ -0,0 +1,4 @@
|
||||
.button {
|
||||
width: 100%;
|
||||
margin-top: 36px;
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { ROUTES } from "@/shared/constants/client-routes";
|
||||
|
||||
import { RetainingButton } from "../..";
|
||||
|
||||
import styles from "./Button.module.scss";
|
||||
|
||||
export default function Button() {
|
||||
const t = useTranslations("PlanCancelled");
|
||||
const router = useRouter();
|
||||
|
||||
const handleButtonClick = () => {
|
||||
router.push(ROUTES.home());
|
||||
}
|
||||
|
||||
return (
|
||||
<RetainingButton className={styles.button} active={true} onClick={handleButtonClick}>
|
||||
{t("button")}
|
||||
</RetainingButton>
|
||||
)
|
||||
}
|
||||
1
src/components/domains/retaining/plan-cancelled/index.ts
Normal file
1
src/components/domains/retaining/plan-cancelled/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default as PlanCancelledButton } from "./Button/Button";
|
||||
@ -0,0 +1,63 @@
|
||||
.offers {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 29px;
|
||||
}
|
||||
|
||||
.buttonOfferContainer {
|
||||
width: 100%;
|
||||
position: sticky;
|
||||
bottom: calc(0dvh + 16px);
|
||||
pointer-events: none;
|
||||
margin-top: 211px;
|
||||
overflow-x: clip;
|
||||
z-index: 9999;
|
||||
|
||||
&>.blur {
|
||||
padding-top: 40px;
|
||||
}
|
||||
|
||||
& .buttonOffer {
|
||||
position: relative;
|
||||
line-height: 25px;
|
||||
padding: 15px 20px;
|
||||
font-size: 28px;
|
||||
position: relative;
|
||||
z-index: 1000;
|
||||
pointer-events: all;
|
||||
|
||||
&>.loaderContainer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
border-radius: inherit;
|
||||
backdrop-filter: blur(3px);
|
||||
transition: background-color 0.3s ease-in-out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.offerImageEYE {
|
||||
margin-top: -72px;
|
||||
}
|
||||
|
||||
.offerImageVIP {
|
||||
margin-top: -72px;
|
||||
width: calc(100% - 84px);
|
||||
max-width: 236px;
|
||||
}
|
||||
|
||||
.buttonCancel {
|
||||
line-height: 20px;
|
||||
color: #5D5D5D;
|
||||
background: none;
|
||||
outline: 2px solid #9A9797;
|
||||
font-size: 28px;
|
||||
margin-top: 22px;
|
||||
}
|
||||
@ -0,0 +1,124 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { Offer, RetainingButton } from "@/components/domains/retaining";
|
||||
import { EyeSvg } from "@/components/domains/retaining/images";
|
||||
import { Spinner } from "@/components/ui";
|
||||
import { BlurComponent } from "@/components/widgets";
|
||||
import { useLottie } from "@/hooks/lottie/useLottie";
|
||||
import { ROUTES } from "@/shared/constants/client-routes";
|
||||
import { retainingImages } from "@/shared/constants/images/retaining";
|
||||
import { ELottieKeys } from "@/shared/constants/lottie";
|
||||
import { ERetainingFunnel } from "@/types";
|
||||
|
||||
import styles from "./SecondChancePage.module.scss";
|
||||
|
||||
export default function SecondChancePage() {
|
||||
const router = useRouter();
|
||||
const t = useTranslations("SecondChance");
|
||||
useLottie({
|
||||
preloadKey: ELottieKeys.loaderCheckMark2,
|
||||
});
|
||||
useLottie({
|
||||
preloadKey: ELottieKeys.confetti,
|
||||
});
|
||||
|
||||
const [activeOffer, setActiveOffer] = useState<"pause_30" | "free_chat_30">("pause_30");
|
||||
const [isLoadingButton, setIsLoadingButton] = useState<boolean>(false);
|
||||
// const retainingFunnel = useSelector(selectors.selectRetainingFunnel);
|
||||
// const token = useSelector(selectors.selectToken);
|
||||
// const cancellingSubscriptionId = useSelector(selectors.selectCancellingSubscriptionId);
|
||||
|
||||
const retainingFunnel = ERetainingFunnel.Red;
|
||||
|
||||
const handleOfferClick = (offer: "pause_30" | "free_chat_30") => {
|
||||
if (isLoadingButton) return;
|
||||
setActiveOffer(offer);
|
||||
}
|
||||
|
||||
const handleGetOfferClick = async () => {
|
||||
if (isLoadingButton) return;
|
||||
setIsLoadingButton(true);
|
||||
|
||||
// const response = await api.userSubscriptionAction({
|
||||
// subscriptionId: cancellingSubscriptionId,
|
||||
// action: activeOffer,
|
||||
// token
|
||||
// });
|
||||
// if (response.status === "success") {
|
||||
// navigate(routes.client.retainingFunnelPlanCancelled());
|
||||
// }
|
||||
}
|
||||
|
||||
const handleCancelClick = () => {
|
||||
if (isLoadingButton) return;
|
||||
if (retainingFunnel === ERetainingFunnel.Red) {
|
||||
router.push(ROUTES.retainingFunnelChangeMind());
|
||||
}
|
||||
// if (retainingFunnel === ERetainingFunnel.Green) {
|
||||
// return navigate(routes.client.retainingFunnelCancellationOfSubscription());
|
||||
// }
|
||||
// if (retainingFunnel === ERetainingFunnel.Purple) {
|
||||
// return navigate(routes.client.retainingFunnelStopFor30Days());
|
||||
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.offers}>
|
||||
<Offer
|
||||
title={t.rich("offers.1.title", { br: () => <br /> })}
|
||||
description={t("offers.1.description")}
|
||||
oldPrice={t("offers.1.old-price")}
|
||||
newPrice={t("offers.1.new-price")}
|
||||
onClick={() => handleOfferClick("pause_30")}
|
||||
active={activeOffer === "pause_30"}
|
||||
image={<EyeSvg className={styles.offerImageEYE} />}
|
||||
/>
|
||||
<Offer
|
||||
title={t("offers.2.title")}
|
||||
description={t("offers.2.description")}
|
||||
oldPrice={t("offers.2.old-price")}
|
||||
newPrice={t("offers.2.new-price")}
|
||||
onClick={() => handleOfferClick("free_chat_30")}
|
||||
active={activeOffer === "free_chat_30"}
|
||||
image={
|
||||
// <img className={styles.offerImageVIP} src={images("vip_member.png")} alt="vip member" />
|
||||
<Image
|
||||
src={retainingImages("vip_member.png")}
|
||||
alt="vip member"
|
||||
className={styles.offerImageVIP}
|
||||
width={711}
|
||||
height={609}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.buttonOfferContainer}>
|
||||
<BlurComponent className={styles.blur} isActiveBlur={true}>
|
||||
<RetainingButton
|
||||
className={styles.buttonOffer}
|
||||
active={true}
|
||||
onClick={handleGetOfferClick}
|
||||
>
|
||||
{isLoadingButton && <div className={styles.loaderContainer}>
|
||||
<Spinner />
|
||||
</div>}
|
||||
{t("get_offer")}
|
||||
</RetainingButton>
|
||||
</BlurComponent>
|
||||
</div>
|
||||
<RetainingButton
|
||||
className={styles.buttonCancel}
|
||||
onClick={handleCancelClick}
|
||||
>
|
||||
{t("cancel")}
|
||||
</RetainingButton>
|
||||
</>
|
||||
)
|
||||
}
|
||||
1
src/components/domains/retaining/second-chance/index.ts
Normal file
1
src/components/domains/retaining/second-chance/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default as SecondChancePage } from "./SecondChancePage/SecondChancePage";
|
||||
@ -0,0 +1,32 @@
|
||||
.buttonsContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 32px;
|
||||
margin-top: 188px;
|
||||
|
||||
&>.button {
|
||||
position: relative;
|
||||
|
||||
&>.loaderContainer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
border-radius: inherit;
|
||||
backdrop-filter: blur(3px);
|
||||
transition: background-color 0.3s ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
&>.buttonCancel {
|
||||
background: none;
|
||||
color: #5D5D5D;
|
||||
outline: 2px solid #9A9797;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,72 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { RetainingButton } from "@/components/domains/retaining";
|
||||
import { Spinner } from "@/components/ui";
|
||||
import { useLottie } from "@/hooks/lottie/useLottie";
|
||||
import { ROUTES } from "@/shared/constants/client-routes";
|
||||
import { ELottieKeys } from "@/shared/constants/lottie";
|
||||
|
||||
import styles from "./Buttons.module.scss";
|
||||
|
||||
export default function Buttons() {
|
||||
const t = useTranslations("StopFor30Days");
|
||||
const router = useRouter();
|
||||
// const api = useApi();
|
||||
// const token = useSelector(selectors.selectToken);
|
||||
// const cancellingSubscriptionId = useSelector(selectors.selectCancellingSubscriptionId);
|
||||
useLottie({
|
||||
preloadKey: ELottieKeys.loaderCheckMark2,
|
||||
});
|
||||
useLottie({
|
||||
preloadKey: ELottieKeys.confetti,
|
||||
});
|
||||
// const retainingFunnel = useSelector(selectors.selectRetainingFunnel);
|
||||
// const retainingFunnel = ERetainingFunnel.Red;
|
||||
const [isLoadingButton, setIsLoadingButton] = useState<boolean>(false);
|
||||
|
||||
const handleStopClick = async () => {
|
||||
if (isLoadingButton) return;
|
||||
setIsLoadingButton(true);
|
||||
// const response = await api.userSubscriptionAction({
|
||||
// subscriptionId: cancellingSubscriptionId,
|
||||
// action: "pause_30",
|
||||
// token
|
||||
// });
|
||||
// if (response.status === "success") {
|
||||
// navigate(routes.client.retainingFunnelSubscriptionStopped());
|
||||
// }
|
||||
}
|
||||
|
||||
const handleCancelClick = () => {
|
||||
if (isLoadingButton) return;
|
||||
// if (retainingFunnel === ERetainingFunnel.Green) {
|
||||
// return navigate(routes.client.retainingFunnelChangeMind());
|
||||
// }
|
||||
router.push(ROUTES.retainingFunnelCancellationOfSubscription());
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.buttonsContainer}>
|
||||
<RetainingButton
|
||||
className={styles.button}
|
||||
active={true}
|
||||
onClick={handleStopClick}
|
||||
>
|
||||
{isLoadingButton && <div className={styles.loaderContainer}>
|
||||
<Spinner />
|
||||
</div>}
|
||||
{t("stop")}
|
||||
</RetainingButton>
|
||||
<RetainingButton
|
||||
className={styles.buttonCancel}
|
||||
onClick={handleCancelClick}
|
||||
>
|
||||
{t("cancel")}
|
||||
</RetainingButton>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export { default as StopFor30DaysButtons } from "./Buttons/Buttons";
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user