main
compatibility & palm generations & add zustand & config prettier eslint
This commit is contained in:
parent
665b7bad90
commit
67f4dfdf3d
@ -1,7 +1,12 @@
|
|||||||
{
|
{
|
||||||
"semi": true,
|
"semi": true,
|
||||||
"tabWidth": 2,
|
"trailingComma": "es5",
|
||||||
"printWidth": 100,
|
"singleQuote": false,
|
||||||
"singleQuote": true,
|
"printWidth": 80,
|
||||||
"trailingComma": "es5"
|
"tabWidth": 2,
|
||||||
}
|
"useTabs": false,
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"bracketSameLine": false,
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"endOfLine": "lf"
|
||||||
|
}
|
||||||
|
|||||||
34
.vscode/settings.json
vendored
Normal file
34
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"editor.formatOnSave": false,
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": "explicit"
|
||||||
|
// "source.organizeImports": "explicit"
|
||||||
|
},
|
||||||
|
"eslint.validate": [
|
||||||
|
"javascript",
|
||||||
|
"javascriptreact",
|
||||||
|
"typescript",
|
||||||
|
"typescriptreact"
|
||||||
|
],
|
||||||
|
// "typescript.preferences.importModuleSpecifier": "relative",
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"files.associations": {
|
||||||
|
"*.module.scss": "scss"
|
||||||
|
},
|
||||||
|
"[javascript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[typescriptreact]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[javascriptreact]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"eslint.workingDirectories": ["."],
|
||||||
|
"eslint.format.enable": true,
|
||||||
|
"editor.tabSize": 2,
|
||||||
|
"editor.insertSpaces": true
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
version: '3.8'
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
@ -10,4 +10,4 @@ services:
|
|||||||
- /app/node_modules
|
- /app/node_modules
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=development
|
- NODE_ENV=development
|
||||||
command: npm run dev
|
command: npm run dev
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
import { dirname } from "path";
|
|
||||||
import { fileURLToPath } from "url";
|
|
||||||
import { FlatCompat } from "@eslint/eslintrc";
|
import { FlatCompat } from "@eslint/eslintrc";
|
||||||
import eslintPluginImport from "eslint-plugin-import";
|
import eslintPluginImport from "eslint-plugin-import";
|
||||||
import eslintPluginUnused from "eslint-plugin-unused-imports";
|
import eslintPluginReact from "eslint-plugin-react";
|
||||||
|
import eslintPluginReactHooks from "eslint-plugin-react-hooks";
|
||||||
import eslintPluginSort from "eslint-plugin-simple-import-sort";
|
import eslintPluginSort from "eslint-plugin-simple-import-sort";
|
||||||
|
import eslintPluginUnused from "eslint-plugin-unused-imports";
|
||||||
|
import { dirname } from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
@ -20,12 +22,14 @@ const eslintConfig = [
|
|||||||
import: eslintPluginImport,
|
import: eslintPluginImport,
|
||||||
"unused-imports": eslintPluginUnused,
|
"unused-imports": eslintPluginUnused,
|
||||||
"simple-import-sort": eslintPluginSort,
|
"simple-import-sort": eslintPluginSort,
|
||||||
|
react: eslintPluginReact,
|
||||||
|
"react-hooks": eslintPluginReactHooks,
|
||||||
},
|
},
|
||||||
|
|
||||||
rules: {
|
rules: {
|
||||||
/* неиспользуемые переменные и импорты */
|
/* неиспользуемые переменные и импорты */
|
||||||
"@typescript-eslint/no-unused-vars": [
|
"@typescript-eslint/no-unused-vars": [
|
||||||
"error",
|
"warn",
|
||||||
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
|
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
|
||||||
],
|
],
|
||||||
"unused-imports/no-unused-imports": "error",
|
"unused-imports/no-unused-imports": "error",
|
||||||
@ -44,15 +48,61 @@ const eslintConfig = [
|
|||||||
"error",
|
"error",
|
||||||
{
|
{
|
||||||
groups: [
|
groups: [
|
||||||
["^\\u0000"], // side-effects
|
["^\\u0000"], // side-effects
|
||||||
["^react", "^next", "^@?\\w"],// пакеты
|
["^react", "^next", "^@?\\w"], // пакеты
|
||||||
["^@/"], // алиасы проекта
|
["^@/"], // алиасы проекта
|
||||||
["^\\.\\.(?!/?$)", "^\\./(?=.*/)", "^\\./?$"], // относительные
|
["^\\.\\.(?!/?$)", "^\\./(?=.*/)", "^\\./?$"], // относительные
|
||||||
["^.+\\.module\\.(css|scss)$"], // модули стилей
|
["^.+\\.module\\.(css|scss)$"], // модули стилей
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"simple-import-sort/exports": "error",
|
"simple-import-sort/exports": "error",
|
||||||
|
|
||||||
|
/* React правила */
|
||||||
|
"react/jsx-uses-react": "off", // не нужно в React 17+
|
||||||
|
"react/react-in-jsx-scope": "off", // не нужно в React 17+
|
||||||
|
"react/prop-types": "off", // используем TypeScript
|
||||||
|
"react/display-name": "warn",
|
||||||
|
"react/jsx-key": "error",
|
||||||
|
"react/jsx-no-duplicate-props": "error",
|
||||||
|
"react/jsx-no-undef": "error",
|
||||||
|
// "react/no-array-index-key": "warn",
|
||||||
|
"react/no-danger": "warn",
|
||||||
|
"react/no-deprecated": "error",
|
||||||
|
"react/no-direct-mutation-state": "error",
|
||||||
|
"react/no-find-dom-node": "error",
|
||||||
|
"react/no-is-mounted": "error",
|
||||||
|
"react/no-render-return-value": "error",
|
||||||
|
"react/no-string-refs": "error",
|
||||||
|
"react/no-unescaped-entities": "warn",
|
||||||
|
"react/no-unknown-property": "error",
|
||||||
|
"react/no-unsafe": "warn",
|
||||||
|
"react/self-closing-comp": "error",
|
||||||
|
|
||||||
|
/* React Hooks правила */
|
||||||
|
"react-hooks/rules-of-hooks": "error",
|
||||||
|
"react-hooks/exhaustive-deps": "warn",
|
||||||
|
|
||||||
|
/* TypeScript правила */
|
||||||
|
"@typescript-eslint/no-explicit-any": "warn",
|
||||||
|
"@typescript-eslint/no-non-null-assertion": "warn",
|
||||||
|
"@typescript-eslint/no-var-requires": "error",
|
||||||
|
"@typescript-eslint/explicit-function-return-type": "off",
|
||||||
|
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||||
|
"@typescript-eslint/no-empty-function": "warn",
|
||||||
|
"@typescript-eslint/no-inferrable-types": "error",
|
||||||
|
|
||||||
|
/* Общие правила */
|
||||||
|
"no-console": "warn",
|
||||||
|
"no-debugger": "error",
|
||||||
|
"no-alert": "warn",
|
||||||
|
"no-var": "error",
|
||||||
|
"prefer-const": "error",
|
||||||
|
"no-unused-expressions": "error",
|
||||||
|
"no-duplicate-imports": "error",
|
||||||
|
"no-multiple-empty-lines": ["error", { max: 2 }],
|
||||||
|
"eol-last": "error",
|
||||||
|
"no-trailing-spaces": "error",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
344
messages/de.json
344
messages/de.json
@ -1,176 +1,176 @@
|
|||||||
{
|
{
|
||||||
"HomePage": {
|
"HomePage": {
|
||||||
"title": "Hello world!",
|
"title": "Hello world!",
|
||||||
"about": "Go to the about page"
|
"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"
|
||||||
},
|
},
|
||||||
"Profile": {
|
"billing": {
|
||||||
"profile_information": {
|
"title": "Billing",
|
||||||
"title": "Profile Information",
|
"description": "To access your subscription information, please log into your billing account.",
|
||||||
"description": "To update your email address please contact support.",
|
"subscription_type": "Subscription Type:",
|
||||||
"email_placeholder": "Email",
|
"billing_button": "Billing",
|
||||||
"name_placeholder": "Name"
|
"credits": {
|
||||||
},
|
"title": "You have {credits} credits left",
|
||||||
"billing": {
|
"description": "You can use them to chat with any Expert on the platform."
|
||||||
"title": "Billing",
|
},
|
||||||
"description": "To access your subscription information, please log into your billing account.",
|
"any_questions": "Any questions? <link>{linkText}</link>",
|
||||||
"subscription_type": "Subscription Type:",
|
"any_questions_link": "Contact us",
|
||||||
"billing_button": "Billing",
|
"subscription_update": "<bold>{subscriptionUpdateBold}</bold><br></br>If you've just purchased or changed plan, your subscription status will change in a few hours.",
|
||||||
"credits": {
|
"subscription_update_bold": "Subscription information is updated every few hours."
|
||||||
"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": {
|
"log_out": {
|
||||||
"title": "Manage my subscriptions",
|
"title": "Log out",
|
||||||
"modal": {
|
"log_out_button": "Log out",
|
||||||
"title": "Are you sure you want to cancel your subscription?",
|
"modal": {
|
||||||
"description": "Are you sure you want to cancel your subscription?",
|
"title": "Are you sure you want to log out?",
|
||||||
"cancel_button": "Cancel subscription",
|
"description": "Are you sure you want to log out?",
|
||||||
"stay_button": "Stay"
|
"stay_button": "Stay",
|
||||||
},
|
"log_out_button": "Log out"
|
||||||
"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": "🎉"
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"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": "🎉"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
369
messages/en.json
369
messages/en.json
@ -1,176 +1,201 @@
|
|||||||
{
|
{
|
||||||
"HomePage": {
|
"HomePage": {
|
||||||
"title": "Hello world!",
|
"title": "Hello world!",
|
||||||
"about": "Go to the about page"
|
"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"
|
||||||
},
|
},
|
||||||
"Profile": {
|
"billing": {
|
||||||
"profile_information": {
|
"title": "Billing",
|
||||||
"title": "Profile Information",
|
"description": "To access your subscription information, please log into your billing account.",
|
||||||
"description": "To update your email address please contact support.",
|
"subscription_type": "Subscription Type:",
|
||||||
"email_placeholder": "Email",
|
"billing_button": "Billing",
|
||||||
"name_placeholder": "Name"
|
"credits": {
|
||||||
},
|
"title": "You have {credits} credits left",
|
||||||
"billing": {
|
"description": "You can use them to chat with any Expert on the platform."
|
||||||
"title": "Billing",
|
},
|
||||||
"description": "To access your subscription information, please log into your billing account.",
|
"any_questions": "Any questions? <link>{linkText}</link>",
|
||||||
"subscription_type": "Subscription Type:",
|
"any_questions_link": "Contact us",
|
||||||
"billing_button": "Billing",
|
"subscription_update": "<bold>{subscriptionUpdateBold}</bold><br></br>If you've just purchased or changed plan, your subscription status will change in a few hours.",
|
||||||
"credits": {
|
"subscription_update_bold": "Subscription information is updated every few hours."
|
||||||
"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": {
|
"log_out": {
|
||||||
"title": "Manage my subscriptions",
|
"title": "Log out",
|
||||||
"modal": {
|
"log_out_button": "Log out",
|
||||||
"title": "Are you sure you want to cancel your subscription?",
|
"modal": {
|
||||||
"description": "Are you sure you want to cancel your subscription?",
|
"title": "Are you sure you want to log out?",
|
||||||
"cancel_button": "Cancel subscription",
|
"description": "Are you sure you want to log out?",
|
||||||
"stay_button": "Stay"
|
"stay_button": "Stay",
|
||||||
},
|
"log_out_button": "Log out"
|
||||||
"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": "🎉"
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"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>",
|
||||||
|
"PAST_DUE": "Past due"
|
||||||
|
},
|
||||||
|
"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": "🎉"
|
||||||
|
},
|
||||||
|
"DatePicker": {
|
||||||
|
"year": "YYYY",
|
||||||
|
"month": "MM",
|
||||||
|
"day": "DD"
|
||||||
|
},
|
||||||
|
"TimePicker": {
|
||||||
|
"hour": "HH",
|
||||||
|
"minute": "MM",
|
||||||
|
"period": "AM/PM"
|
||||||
|
},
|
||||||
|
"Compatibility": {
|
||||||
|
"title": "Your Personality Type",
|
||||||
|
"description": "Please input your data to create the report.",
|
||||||
|
"button": "Continue",
|
||||||
|
"error": "Something went wrong. Please try again later."
|
||||||
|
},
|
||||||
|
"CompatibilityResult": {
|
||||||
|
"title": "Your Personality Type",
|
||||||
|
"error": "Something went wrong. Please try again later."
|
||||||
|
},
|
||||||
|
"PalmistryResult": {
|
||||||
|
"title": "Your Personality Type",
|
||||||
|
"error": "Something went wrong. Please try again later."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
38
package-lock.json
generated
38
package-lock.json
generated
@ -18,7 +18,8 @@
|
|||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"sass": "^1.89.2",
|
"sass": "^1.89.2",
|
||||||
"server-only": "^0.0.1",
|
"server-only": "^0.0.1",
|
||||||
"zod": "^3.25.64"
|
"zod": "^3.25.64",
|
||||||
|
"zustand": "^5.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
@ -28,6 +29,8 @@
|
|||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "15.3.3",
|
"eslint-config-next": "15.3.3",
|
||||||
"eslint-plugin-import": "^2.31.0",
|
"eslint-plugin-import": "^2.31.0",
|
||||||
|
"eslint-plugin-react": "^7.37.5",
|
||||||
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||||
"eslint-plugin-unused-imports": "^4.1.4",
|
"eslint-plugin-unused-imports": "^4.1.4",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
@ -1343,7 +1346,7 @@
|
|||||||
"version": "19.1.8",
|
"version": "19.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz",
|
||||||
"integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==",
|
"integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
@ -2441,7 +2444,7 @@
|
|||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/damerau-levenshtein": {
|
"node_modules/damerau-levenshtein": {
|
||||||
@ -6006,6 +6009,35 @@
|
|||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zustand": {
|
||||||
|
"version": "5.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.5.tgz",
|
||||||
|
"integrity": "sha512-mILtRfKW9xM47hqxGIxCv12gXusoY/xTSHBYApXozR0HmQv299whhBeeAcRy+KrPPybzosvJBCOmVjq6x12fCg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.20.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": ">=18.0.0",
|
||||||
|
"immer": ">=9.0.6",
|
||||||
|
"react": ">=18.0.0",
|
||||||
|
"use-sync-external-store": ">=1.2.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"immer": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"use-sync-external-store": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
12
package.json
12
package.json
@ -7,7 +7,10 @@
|
|||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start -p 3001",
|
"start": "next start -p 3001",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"lint:fix": "next lint --fix"
|
"lint:fix": "next lint --fix",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"format:check": "prettier --check .",
|
||||||
|
"type-check": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lottiefiles/dotlottie-react": "^0.14.1",
|
"@lottiefiles/dotlottie-react": "^0.14.1",
|
||||||
@ -20,7 +23,8 @@
|
|||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"sass": "^1.89.2",
|
"sass": "^1.89.2",
|
||||||
"server-only": "^0.0.1",
|
"server-only": "^0.0.1",
|
||||||
"zod": "^3.25.64"
|
"zod": "^3.25.64",
|
||||||
|
"zustand": "^5.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
@ -30,9 +34,11 @@
|
|||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "15.3.3",
|
"eslint-config-next": "15.3.3",
|
||||||
"eslint-plugin-import": "^2.31.0",
|
"eslint-plugin-import": "^2.31.0",
|
||||||
|
"eslint-plugin-react": "^7.37.5",
|
||||||
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||||
"eslint-plugin-unused-imports": "^4.1.4",
|
"eslint-plugin-unused-imports": "^4.1.4",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,24 +1,24 @@
|
|||||||
(function (m, e, t, r, i, k, a) {
|
(function (m, e, t, r, i, k, a) {
|
||||||
m[i] =
|
m[i] =
|
||||||
m[i] ||
|
m[i] ||
|
||||||
function () {
|
function () {
|
||||||
(m[i].a = m[i].a || []).push(arguments);
|
(m[i].a = m[i].a || []).push(arguments);
|
||||||
};
|
};
|
||||||
m[i].l = 1 * new Date();
|
m[i].l = 1 * new Date();
|
||||||
for (var j = 0; j < document.scripts.length; j++) {
|
for (var j = 0; j < document.scripts.length; j++) {
|
||||||
if (document.scripts[j].src === r) {
|
if (document.scripts[j].src === r) {
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
(k = e.createElement(t)),
|
}
|
||||||
(a = e.getElementsByTagName(t)[0]),
|
(k = e.createElement(t)),
|
||||||
(k.async = 1),
|
(a = e.getElementsByTagName(t)[0]),
|
||||||
(k.src = r),
|
(k.async = 1),
|
||||||
a.parentNode.insertBefore(k, a);
|
(k.src = r),
|
||||||
|
a.parentNode.insertBefore(k, a);
|
||||||
})(
|
})(
|
||||||
window,
|
window,
|
||||||
document,
|
document,
|
||||||
"script",
|
"script",
|
||||||
"https://cdn.jsdelivr.net/npm/yandex-metrica-watch/tag.js",
|
"https://cdn.jsdelivr.net/npm/yandex-metrica-watch/tag.js",
|
||||||
"ym"
|
"ym"
|
||||||
);
|
);
|
||||||
|
|||||||
@ -0,0 +1,6 @@
|
|||||||
|
.coreError {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
34
src/app/[locale]/(core)/compatibility/[id]/error.tsx
Normal file
34
src/app/[locale]/(core)/compatibility/[id]/error.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
import { Button, Typography } from "@/components/ui";
|
||||||
|
|
||||||
|
import styles from "./error.module.scss";
|
||||||
|
|
||||||
|
export default function Error({
|
||||||
|
error,
|
||||||
|
reset,
|
||||||
|
}: {
|
||||||
|
error: Error & { digest?: string };
|
||||||
|
reset: () => void;
|
||||||
|
}) {
|
||||||
|
useEffect(() => {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(error);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.coreError}>
|
||||||
|
<Typography as="h2" size="xl" weight="bold">
|
||||||
|
Something went wrong!
|
||||||
|
</Typography>
|
||||||
|
<Typography as="p" align="center">
|
||||||
|
{error.message}
|
||||||
|
</Typography>
|
||||||
|
<Button onClick={() => reset()}>
|
||||||
|
<Typography color="white">Try again</Typography>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
11
src/app/[locale]/(core)/compatibility/[id]/page.module.scss
Normal file
11
src/app/[locale]/(core)/compatibility/[id]/page.module.scss
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
.header {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
|
||||||
|
& > .title {
|
||||||
|
line-height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .description {
|
||||||
|
line-height: 25px;
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/app/[locale]/(core)/compatibility/[id]/page.tsx
Normal file
43
src/app/[locale]/(core)/compatibility/[id]/page.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { Suspense, use } from "react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
import CompatibilityActionFieldsForm, {
|
||||||
|
CompatibilityActionFieldsFormSkeleton,
|
||||||
|
} from "@/components/domains/compatibility/CompatibilityActionFieldsForm/CompatibilityActionFieldsForm";
|
||||||
|
import { Typography } from "@/components/ui";
|
||||||
|
import { loadCompatibilityActionFields } from "@/entities/compatibilityActionFields/loaders";
|
||||||
|
|
||||||
|
import styles from "./page.module.scss";
|
||||||
|
|
||||||
|
export default function Compatibility({
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
params: Promise<{ id: string }>;
|
||||||
|
}) {
|
||||||
|
const { id } = use(params);
|
||||||
|
const t = useTranslations("Compatibility");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={styles.header}>
|
||||||
|
<Typography
|
||||||
|
as="h1"
|
||||||
|
size="xl"
|
||||||
|
weight="semiBold"
|
||||||
|
className={styles.title}
|
||||||
|
>
|
||||||
|
{t("title")}
|
||||||
|
</Typography>
|
||||||
|
<Typography as="p" size="sm" className={styles.description}>
|
||||||
|
{t("description")}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<Suspense key={id} fallback={<CompatibilityActionFieldsFormSkeleton />}>
|
||||||
|
<CompatibilityActionFieldsForm
|
||||||
|
fields={loadCompatibilityActionFields(id)}
|
||||||
|
actionId={id}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
13
src/app/[locale]/(core)/compatibility/result/[id]/page.tsx
Normal file
13
src/app/[locale]/(core)/compatibility/result/[id]/page.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { use } from "react";
|
||||||
|
|
||||||
|
import CompatibilityResultPage from "@/components/domains/compatibility/CompatibilityResultPage/CompatibilityResultPage";
|
||||||
|
|
||||||
|
export default function CompatibilityResult({
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
params: Promise<{ id: string }>;
|
||||||
|
}) {
|
||||||
|
const { id } = use(params);
|
||||||
|
|
||||||
|
return <CompatibilityResultPage id={id} />;
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
.coreError {
|
.coreError {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from "react";
|
||||||
|
|
||||||
import { Button, Typography } from '@/components/ui';
|
import { Button, Typography } from "@/components/ui";
|
||||||
|
|
||||||
import styles from "./error.module.scss"
|
import styles from "./error.module.scss";
|
||||||
|
|
||||||
export default function Error({
|
export default function Error({
|
||||||
error,
|
error,
|
||||||
@ -14,20 +14,21 @@ export default function Error({
|
|||||||
reset: () => void;
|
reset: () => void;
|
||||||
}) {
|
}) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}, [error]);
|
}, [error]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.coreError}>
|
<div className={styles.coreError}>
|
||||||
<Typography as='h2' size='xl' weight='bold'>Something went wrong!</Typography>
|
<Typography as="h2" size="xl" weight="bold">
|
||||||
<Typography as='p' align='center'>{error.message}</Typography>
|
Something went wrong!
|
||||||
<Button
|
</Typography>
|
||||||
onClick={
|
<Typography as="p" align="center">
|
||||||
() => reset()
|
{error.message}
|
||||||
}
|
</Typography>
|
||||||
>
|
<Button onClick={() => reset()}>
|
||||||
<Typography color='white'>Try again</Typography>
|
<Typography color="white">Try again</Typography>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
.main {
|
.main {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
padding-bottom: 64px;
|
padding-bottom: 64px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navBar {
|
.navBar {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 7777;
|
z-index: 7777;
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,14 +3,14 @@ import { DrawerProvider, NavigationBar } from "@/components/layout";
|
|||||||
import styles from "./layout.module.scss";
|
import styles from "./layout.module.scss";
|
||||||
|
|
||||||
export default function CoreLayout({
|
export default function CoreLayout({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return <DrawerProvider>
|
return (
|
||||||
<NavigationBar className={styles.navBar} />
|
<DrawerProvider>
|
||||||
<main className={styles.main}>
|
<NavigationBar className={styles.navBar} />
|
||||||
{children}
|
<main className={styles.main}>{children}</main>
|
||||||
</main>
|
|
||||||
</DrawerProvider>
|
</DrawerProvider>
|
||||||
}
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
.coreSpinnerContainer {
|
.coreSpinnerContainer {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
.page {
|
.page {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 24px;
|
gap: 24px;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,41 +1,45 @@
|
|||||||
import { Suspense } from "react";
|
import { Suspense } from "react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AdvisersSection,
|
AdvisersSection,
|
||||||
AdvisersSectionSkeleton,
|
AdvisersSectionSkeleton,
|
||||||
CompatibilitySection,
|
CompatibilitySection,
|
||||||
CompatibilitySectionSkeleton,
|
CompatibilitySectionSkeleton,
|
||||||
MeditationSection,
|
MeditationSection,
|
||||||
MeditationSectionSkeleton,
|
MeditationSectionSkeleton,
|
||||||
PalmSection,
|
PalmSection,
|
||||||
PalmSectionSkeleton
|
PalmSectionSkeleton,
|
||||||
} from "@/components/domains/dashboard";
|
} from "@/components/domains/dashboard";
|
||||||
import { Horoscope } from "@/components/widgets";
|
import { Horoscope } from "@/components/widgets";
|
||||||
import { loadAssistants, loadCompatibility, loadMeditations, loadPalms } from "@/entities/dashboard/loaders";
|
import {
|
||||||
|
loadAssistants,
|
||||||
|
loadCompatibility,
|
||||||
|
loadMeditations,
|
||||||
|
loadPalms,
|
||||||
|
} from "@/entities/dashboard/loaders";
|
||||||
|
|
||||||
import styles from "./page.module.scss";
|
import styles from "./page.module.scss";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<section className={styles.page}>
|
||||||
|
<Horoscope />
|
||||||
|
|
||||||
return (
|
<Suspense fallback={<AdvisersSectionSkeleton />}>
|
||||||
<section className={styles.page}>
|
<AdvisersSection promise={loadAssistants()} />
|
||||||
<Horoscope />
|
</Suspense>
|
||||||
|
|
||||||
<Suspense fallback={<AdvisersSectionSkeleton />}>
|
<Suspense fallback={<CompatibilitySectionSkeleton />}>
|
||||||
<AdvisersSection promise={loadAssistants()} />
|
<CompatibilitySection promise={loadCompatibility()} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|
||||||
<Suspense fallback={<CompatibilitySectionSkeleton />}>
|
<Suspense fallback={<MeditationSectionSkeleton />}>
|
||||||
<CompatibilitySection promise={loadCompatibility()} />
|
<MeditationSection promise={loadMeditations()} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|
||||||
<Suspense fallback={<MeditationSectionSkeleton />}>
|
<Suspense fallback={<PalmSectionSkeleton />}>
|
||||||
<MeditationSection promise={loadMeditations()} />
|
<PalmSection promise={loadPalms()} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
</section>
|
||||||
<Suspense fallback={<PalmSectionSkeleton />}>
|
);
|
||||||
<PalmSection promise={loadPalms()} />
|
}
|
||||||
</Suspense>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
16
src/app/[locale]/(core)/palmistry/result/[id]/page.tsx
Normal file
16
src/app/[locale]/(core)/palmistry/result/[id]/page.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import PalmistryResultPage from "@/components/domains/palmistry/PalmistryResultPage/PalmistryResultPage";
|
||||||
|
import { startGeneration } from "@/entities/generations/api";
|
||||||
|
|
||||||
|
export default async function PalmistryResult({
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
params: Promise<{ id: string }>;
|
||||||
|
}) {
|
||||||
|
const { id } = await params;
|
||||||
|
const result = await startGeneration({
|
||||||
|
actionType: "palm",
|
||||||
|
actionId: id,
|
||||||
|
});
|
||||||
|
|
||||||
|
return <PalmistryResultPage id={result?.id} />;
|
||||||
|
}
|
||||||
@ -5,16 +5,18 @@ import { ROUTES } from "@/shared/constants/client-routes";
|
|||||||
import { ELottieKeys } from "@/shared/constants/lottie";
|
import { ELottieKeys } from "@/shared/constants/lottie";
|
||||||
|
|
||||||
export default async function PaymentFailed() {
|
export default async function PaymentFailed() {
|
||||||
const t = await getTranslations("Payment.Error");
|
const t = await getTranslations("Payment.Error");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatedInfoScreen
|
<AnimatedInfoScreen
|
||||||
lottieAnimation={<LottieAnimation loadKey={ELottieKeys.loaderCheckMark} />}
|
lottieAnimation={
|
||||||
title={t("title")}
|
<LottieAnimation loadKey={ELottieKeys.loaderCheckMark} />
|
||||||
animationTime={0}
|
}
|
||||||
animationTexts={[]}
|
title={t("title")}
|
||||||
buttonText={t("button")}
|
animationTime={0}
|
||||||
nextRoute={ROUTES.home()}
|
animationTexts={[]}
|
||||||
/>
|
buttonText={t("button")}
|
||||||
);
|
nextRoute={ROUTES.home()}
|
||||||
}
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -4,26 +4,26 @@ import { createPaymentCheckout } from "@/entities/payment/api";
|
|||||||
import { ROUTES } from "@/shared/constants/client-routes";
|
import { ROUTES } from "@/shared/constants/client-routes";
|
||||||
|
|
||||||
export async function GET(req: NextRequest) {
|
export async function GET(req: NextRequest) {
|
||||||
const productId = req.nextUrl.searchParams.get("productId");
|
const productId = req.nextUrl.searchParams.get("productId");
|
||||||
const placementId = req.nextUrl.searchParams.get("placementId");
|
const placementId = req.nextUrl.searchParams.get("placementId");
|
||||||
const paywallId = req.nextUrl.searchParams.get("paywallId");
|
const paywallId = req.nextUrl.searchParams.get("paywallId");
|
||||||
const fbPixels = req.nextUrl.searchParams.get("fb_pixels");
|
const fbPixels = req.nextUrl.searchParams.get("fb_pixels");
|
||||||
const productPrice = req.nextUrl.searchParams.get("price");
|
const productPrice = req.nextUrl.searchParams.get("price");
|
||||||
const currency = req.nextUrl.searchParams.get("currency");
|
const currency = req.nextUrl.searchParams.get("currency");
|
||||||
|
|
||||||
const data = await createPaymentCheckout({
|
const data = await createPaymentCheckout({
|
||||||
productId: productId || "",
|
productId: productId || "",
|
||||||
placementId: placementId || "",
|
placementId: placementId || "",
|
||||||
paywallId: paywallId || "",
|
paywallId: paywallId || "",
|
||||||
});
|
});
|
||||||
|
|
||||||
let redirectUrl: URL = new URL(data?.paymentUrl || "", req.nextUrl.origin);
|
let redirectUrl: URL = new URL(data?.paymentUrl || "", req.nextUrl.origin);
|
||||||
if (!redirectUrl) {
|
if (!redirectUrl) {
|
||||||
redirectUrl = new URL(`${ROUTES.paymentFailed()}`, origin);
|
redirectUrl = new URL(`${ROUTES.paymentFailed()}`, origin);
|
||||||
}
|
}
|
||||||
if (fbPixels) redirectUrl.searchParams.set("fb_pixels", fbPixels);
|
if (fbPixels) redirectUrl.searchParams.set("fb_pixels", fbPixels);
|
||||||
if (productPrice) redirectUrl.searchParams.set("price", productPrice);
|
if (productPrice) redirectUrl.searchParams.set("price", productPrice);
|
||||||
if (currency) redirectUrl.searchParams.set("currency", currency);
|
if (currency) redirectUrl.searchParams.set("currency", currency);
|
||||||
|
|
||||||
return NextResponse.redirect(redirectUrl, { status: 307 });
|
return NextResponse.redirect(redirectUrl, { status: 307 });
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,23 +1,23 @@
|
|||||||
.button {
|
.button {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: calc(0dvh + 64px);
|
bottom: calc(0dvh + 64px);
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(-50%, 0);
|
transform: translate(-50%, 0);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
width: calc(100dvw - 32px);
|
width: calc(100dvw - 32px);
|
||||||
animation: fadeIn 0.5s ease-in-out 2s forwards;
|
animation: fadeIn 0.5s ease-in-out 2s forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadeIn {
|
@keyframes fadeIn {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,65 +10,72 @@ import { ROUTES } from "@/shared/constants/client-routes";
|
|||||||
import styles from "./Metrics.module.scss";
|
import styles from "./Metrics.module.scss";
|
||||||
|
|
||||||
interface MetricsProps {
|
interface MetricsProps {
|
||||||
fbPixels: string[];
|
fbPixels: string[];
|
||||||
productPrice: string;
|
productPrice: string;
|
||||||
currency: string;
|
currency: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Metrics({ fbPixels, productPrice, currency }: MetricsProps) {
|
export default function Metrics({
|
||||||
const t = useTranslations("Payment.Success");
|
fbPixels,
|
||||||
|
productPrice,
|
||||||
|
currency,
|
||||||
|
}: MetricsProps) {
|
||||||
|
const t = useTranslations("Payment.Success");
|
||||||
|
|
||||||
const [isButtonVisible, setIsButtonVisible] = useState(false);
|
const [isButtonVisible, setIsButtonVisible] = useState(false);
|
||||||
|
|
||||||
const navigateToHome = () => {
|
const navigateToHome = () => {
|
||||||
window.location.href = ROUTES.home()
|
window.location.href = ROUTES.home();
|
||||||
}
|
};
|
||||||
|
|
||||||
// Yandex Metrica
|
// Yandex Metrica
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
if (typeof window.ym === 'function' && typeof window.klaviyo === 'object' && typeof window.gtag === 'function') {
|
if (
|
||||||
try {
|
typeof window.ym === "function" &&
|
||||||
window.gtag('event', 'PaymentSuccess')
|
typeof window.klaviyo === "object" &&
|
||||||
window.klaviyo.push(['track', "PaymentSuccess"]);
|
typeof window.gtag === "function"
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
window.gtag("event", "PaymentSuccess");
|
||||||
|
window.klaviyo.push(["track", "PaymentSuccess"]);
|
||||||
|
|
||||||
window.ym(95799066, "init", {
|
window.ym(95799066, "init", {
|
||||||
clickmap: true,
|
clickmap: true,
|
||||||
trackLinks: true,
|
trackLinks: true,
|
||||||
accurateTrackBounce: true,
|
accurateTrackBounce: true,
|
||||||
webvisor: true,
|
webvisor: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
window.ym(95799066, 'reachGoal', "PaymentSuccess", {}, () => {
|
window.ym(95799066, "reachGoal", "PaymentSuccess", {}, () => {
|
||||||
console.log("Запрос отправлен");
|
// deleteYm()
|
||||||
// deleteYm()
|
setIsButtonVisible(true);
|
||||||
setIsButtonVisible(true);
|
});
|
||||||
})
|
} catch (e) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error("YM error:", e);
|
||||||
|
} finally {
|
||||||
|
clearInterval(interval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
|
||||||
} catch (e) {
|
return () => clearInterval(interval);
|
||||||
console.error('YM error:', e)
|
}, []);
|
||||||
} finally {
|
|
||||||
clearInterval(interval);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 200);
|
|
||||||
|
|
||||||
return () => clearInterval(interval)
|
return (
|
||||||
}, []);
|
<>
|
||||||
|
{/* Klaviyo */}
|
||||||
return <>
|
{/* <Script src="https://static.klaviyo.com/onsite/js/klaviyo.js?company_id=RM7w5r" /> */}
|
||||||
|
<Script id="klaviyo-script">
|
||||||
{/* Klaviyo */}
|
{`const script = document.createElement("script");
|
||||||
{/* <Script src="https://static.klaviyo.com/onsite/js/klaviyo.js?company_id=RM7w5r" /> */}
|
|
||||||
<Script id="klaviyo-script">
|
|
||||||
{`const script = document.createElement("script");
|
|
||||||
script.src = "https://static.klaviyo.com/onsite/js/klaviyo.js?company_id=RM7w5r";
|
script.src = "https://static.klaviyo.com/onsite/js/klaviyo.js?company_id=RM7w5r";
|
||||||
script.type = "text/javascript";
|
script.type = "text/javascript";
|
||||||
script.async = "";
|
script.async = "";
|
||||||
document.head.appendChild(script);`}
|
document.head.appendChild(script);`}
|
||||||
</Script>
|
</Script>
|
||||||
<Script id="klaviyo-script-proxy">
|
<Script id="klaviyo-script-proxy">
|
||||||
{`
|
{`
|
||||||
!(function () {
|
!(function () {
|
||||||
if (!window.klaviyo) {
|
if (!window.klaviyo) {
|
||||||
window._klOnsite = window._klOnsite || [];
|
window._klOnsite = window._klOnsite || [];
|
||||||
@ -117,11 +124,11 @@ export default function Metrics({ fbPixels, productPrice, currency }: MetricsPro
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
`}
|
`}
|
||||||
</Script>
|
</Script>
|
||||||
|
|
||||||
{/* Yandex Metrica */}
|
{/* Yandex Metrica */}
|
||||||
<Script id="yandex-metrica-script">
|
<Script id="yandex-metrica-script">
|
||||||
{`(function (m, e, t, r, i, k, a) {
|
{`(function (m, e, t, r, i, k, a) {
|
||||||
m[i] =
|
m[i] =
|
||||||
m[i] ||
|
m[i] ||
|
||||||
function () {
|
function () {
|
||||||
@ -145,22 +152,26 @@ export default function Metrics({ fbPixels, productPrice, currency }: MetricsPro
|
|||||||
"https://cdn.jsdelivr.net/npm/yandex-metrica-watch/tag.js",
|
"https://cdn.jsdelivr.net/npm/yandex-metrica-watch/tag.js",
|
||||||
"ym"
|
"ym"
|
||||||
);`}
|
);`}
|
||||||
</Script>
|
</Script>
|
||||||
|
|
||||||
{/* Google Analytics */}
|
{/* Google Analytics */}
|
||||||
<Script id="google-analytics-script" async src="https://www.googletagmanager.com/gtag/js?id=G-4N17LL3BB5" />
|
<Script
|
||||||
<Script id="google-analytics-script-config">
|
id="google-analytics-script"
|
||||||
{`window.dataLayer = window.dataLayer || [];
|
async
|
||||||
|
src="https://www.googletagmanager.com/gtag/js?id=G-4N17LL3BB5"
|
||||||
|
/>
|
||||||
|
<Script id="google-analytics-script-config">
|
||||||
|
{`window.dataLayer = window.dataLayer || [];
|
||||||
function gtag(){dataLayer.push(arguments);}
|
function gtag(){dataLayer.push(arguments);}
|
||||||
gtag('js', new Date());
|
gtag('js', new Date());
|
||||||
|
|
||||||
gtag('config','G-4N17LL3BB5');`}
|
gtag('config','G-4N17LL3BB5');`}
|
||||||
</Script>
|
</Script>
|
||||||
|
|
||||||
{/* Facebook Pixel */}
|
{/* Facebook Pixel */}
|
||||||
{fbPixels.map((pixel) => (
|
{fbPixels.map(pixel => (
|
||||||
<Script id={`facebook-pixel-${pixel}`} key={pixel}>
|
<Script id={`facebook-pixel-${pixel}`} key={pixel}>
|
||||||
{`!function(f,b,e,v,n,t,s)
|
{`!function(f,b,e,v,n,t,s)
|
||||||
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
|
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
|
||||||
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
|
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
|
||||||
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
|
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
|
||||||
@ -171,15 +182,14 @@ s.parentNode.insertBefore(t,s)}(window, document,'script',
|
|||||||
fbq('init', '${pixel}');
|
fbq('init', '${pixel}');
|
||||||
fbq('track', 'PageView');
|
fbq('track', 'PageView');
|
||||||
fbq('track', 'Purchase', { value: ${productPrice}, currency: "${currency}" });`}
|
fbq('track', 'Purchase', { value: ${productPrice}, currency: "${currency}" });`}
|
||||||
</Script>
|
</Script>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{isButtonVisible &&
|
{isButtonVisible && (
|
||||||
<Button onClick={navigateToHome} className={styles.button}>
|
<Button onClick={navigateToHome} className={styles.button}>
|
||||||
<Typography color="white">
|
<Typography color="white">{t("button")}</Typography>
|
||||||
{t("button")}
|
</Button>
|
||||||
</Typography>
|
)}
|
||||||
</Button>
|
</>
|
||||||
}
|
);
|
||||||
</>;
|
}
|
||||||
}
|
|
||||||
|
|||||||
@ -5,30 +5,34 @@ import { ELottieKeys } from "@/shared/constants/lottie";
|
|||||||
|
|
||||||
import Metrics from "./Metrics";
|
import Metrics from "./Metrics";
|
||||||
|
|
||||||
export default async function PaymentSuccess({ searchParams }: {
|
export default async function PaymentSuccess({
|
||||||
searchParams: Promise<{
|
searchParams,
|
||||||
[key: string]: string | undefined
|
}: {
|
||||||
}>;
|
searchParams: Promise<{
|
||||||
|
[key: string]: string | undefined;
|
||||||
|
}>;
|
||||||
}) {
|
}) {
|
||||||
const params = await searchParams;
|
const params = await searchParams;
|
||||||
|
|
||||||
const fbPixels = params?.fb_pixels?.split(",") || [];
|
const fbPixels = params?.fb_pixels?.split(",") || [];
|
||||||
const productPrice = params?.price || "0";
|
const productPrice = params?.price || "0";
|
||||||
const currency = params?.currency || "USD";
|
const currency = params?.currency || "USD";
|
||||||
|
|
||||||
const t = await getTranslations("Payment.Success");
|
const t = await getTranslations("Payment.Success");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AnimatedInfoScreen
|
<AnimatedInfoScreen
|
||||||
lottieAnimation={<LottieAnimation loadKey={ELottieKeys.loaderCheckMark} />}
|
lottieAnimation={
|
||||||
title={t("title")}
|
<LottieAnimation loadKey={ELottieKeys.loaderCheckMark} />
|
||||||
/>
|
}
|
||||||
<Metrics
|
title={t("title")}
|
||||||
fbPixels={fbPixels}
|
/>
|
||||||
productPrice={productPrice}
|
<Metrics
|
||||||
currency={currency}
|
fbPixels={fbPixels}
|
||||||
/>
|
productPrice={productPrice}
|
||||||
</>
|
currency={currency}
|
||||||
);
|
/>
|
||||||
}
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -1,88 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { useTranslations } from "next-intl";
|
|
||||||
|
|
||||||
import { Billing, LogOut, ProfileBlock, ProfileInformation } from "@/components/domains/profile"
|
|
||||||
import { Card, Modal, Typography } from "@/components/ui";
|
|
||||||
import { ROUTES } from "@/shared/constants/client-routes";
|
|
||||||
|
|
||||||
import styles from "./page.module.scss"
|
|
||||||
|
|
||||||
export default function ProfilePage() {
|
|
||||||
const t = useTranslations('Profile');
|
|
||||||
const router = useRouter();
|
|
||||||
const [logoutModal, setLogoutModal] = useState(false);
|
|
||||||
|
|
||||||
const handleLogout = () => {
|
|
||||||
router.replace(ROUTES.home());
|
|
||||||
// logout();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLogoutModal = () => {
|
|
||||||
setLogoutModal(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleBilling = () => {
|
|
||||||
router.push(ROUTES.profileSubscriptions())
|
|
||||||
}
|
|
||||||
|
|
||||||
const profileBlocks = [
|
|
||||||
{
|
|
||||||
title: t("profile_information.title"),
|
|
||||||
description: t("profile_information.description"),
|
|
||||||
children: <ProfileInformation />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("billing.title"),
|
|
||||||
description: t("billing.description"),
|
|
||||||
children: <Billing onBilling={handleBilling} />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("log_out.title"),
|
|
||||||
children: <LogOut onLogout={handleLogoutModal} />
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Card className={styles.card}>
|
|
||||||
{profileBlocks.map((block, index) => (
|
|
||||||
<div key={block.title}>
|
|
||||||
<ProfileBlock {...block}>
|
|
||||||
{block.children}
|
|
||||||
</ProfileBlock>
|
|
||||||
{index !== profileBlocks.length - 1 && <hr className={styles.line} />}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</Card>
|
|
||||||
{logoutModal && <Modal
|
|
||||||
isCloseButtonVisible={false}
|
|
||||||
open={!!logoutModal}
|
|
||||||
onClose={() => setLogoutModal(false)}
|
|
||||||
className={styles.modal}
|
|
||||||
modalClassName={styles["modal-container"]}
|
|
||||||
>
|
|
||||||
<Typography as="h4" className={styles["modal-title"]}>
|
|
||||||
{t("log_out.modal.title")}
|
|
||||||
</Typography>
|
|
||||||
<p className={styles["modal-description"]}>
|
|
||||||
{t("log_out.modal.description")}
|
|
||||||
</p>
|
|
||||||
<div className={styles["modal-answers"]}>
|
|
||||||
<div className={styles["modal-answer"]} onClick={handleLogout}>
|
|
||||||
<p className={styles["modal-answer-text"]}>
|
|
||||||
{t("log_out.modal.log_out_button")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className={styles["modal-answer"]} onClick={() => setLogoutModal(false)}>
|
|
||||||
<p className={styles["modal-answer-text"]}>
|
|
||||||
{t("log_out.modal.stay_button")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,51 +1,10 @@
|
|||||||
.card {
|
.card {
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.line {
|
.line {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 1px;
|
height: 1px;
|
||||||
background-color: #f0f0f0;
|
background-color: #f0f0f0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.modal-container {
|
|
||||||
max-width: 290px;
|
|
||||||
padding: 24px 0px 0px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-title {
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
padding-inline: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-description {
|
|
||||||
padding-inline: 24px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-answers {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
margin-top: 24px;
|
|
||||||
border-top: 1px solid #D9D9D9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-answer {
|
|
||||||
width: 50%;
|
|
||||||
height: 52px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: #275DA7;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
border-right: 1px solid #D9D9D9;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,8 +1,50 @@
|
|||||||
import ProfilePage from "./Profile";
|
import { Suspense } from "react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Billing,
|
||||||
|
LogOut,
|
||||||
|
ProfileBlock,
|
||||||
|
ProfileInformation,
|
||||||
|
ProfileInformationSkeleton,
|
||||||
|
} from "@/components/domains/profile";
|
||||||
|
import { Card } from "@/components/ui";
|
||||||
|
import { loadUser } from "@/entities/user/loaders";
|
||||||
|
|
||||||
|
import styles from "./page.module.scss";
|
||||||
|
|
||||||
export default function Profile() {
|
export default function Profile() {
|
||||||
|
const t = useTranslations("Profile");
|
||||||
|
|
||||||
return (
|
const profileBlocks = [
|
||||||
<ProfilePage />
|
{
|
||||||
)
|
title: t("profile_information.title"),
|
||||||
}
|
description: t("profile_information.description"),
|
||||||
|
children: (
|
||||||
|
<Suspense fallback={<ProfileInformationSkeleton />}>
|
||||||
|
<ProfileInformation user={loadUser()} />
|
||||||
|
</Suspense>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("billing.title"),
|
||||||
|
description: t("billing.description"),
|
||||||
|
children: <Billing />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("log_out.title"),
|
||||||
|
children: <LogOut />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -1,29 +1,33 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from "next/navigation";
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
import { Button, Typography } from '@/components/ui';
|
import { Button, Typography } from "@/components/ui";
|
||||||
import { ROUTES } from '@/shared/constants/client-routes';
|
import { ROUTES } from "@/shared/constants/client-routes";
|
||||||
|
|
||||||
import styles from "./page.module.scss"
|
import styles from "./page.module.scss";
|
||||||
|
|
||||||
export default function Error() {
|
export default function Error() {
|
||||||
const t = useTranslations("Subscriptions")
|
const t = useTranslations("Subscriptions");
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<Typography as="h1" className={styles.title}>{t("title")}</Typography>
|
<Typography as="h1" className={styles.title}>
|
||||||
<Typography as='p' align='center'>{t("error")}</Typography>
|
{t("title")}
|
||||||
|
</Typography>
|
||||||
|
<Typography as="p" align="center">
|
||||||
|
{t("error")}
|
||||||
|
</Typography>
|
||||||
<Button
|
<Button
|
||||||
onClick={
|
onClick={
|
||||||
// () => reset()
|
// () => reset()
|
||||||
() => router.push(ROUTES.retainingFunnelCancelSubscription())
|
() => router.push(ROUTES.retainingFunnelCancelSubscription())
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Typography color='white'>{t("try_again")}</Typography>
|
<Typography color="white">{t("try_again")}</Typography>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,96 +1,96 @@
|
|||||||
.container {
|
.container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 24px;
|
gap: 24px;
|
||||||
padding-inline: 8px;
|
padding-inline: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttonCancel {
|
.buttonCancel {
|
||||||
color: #ACB0BA;
|
color: #acb0ba;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
line-height: 25px;
|
line-height: 25px;
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
max-width: none;
|
max-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.description,
|
.description,
|
||||||
.error {
|
.error {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
line-height: 25px;
|
line-height: 25px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.description {
|
.description {
|
||||||
color: #ACB0BA;
|
color: #acb0ba;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
color: #FF0000;
|
color: #ff0000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-container {
|
.modal-container {
|
||||||
max-width: 290px;
|
max-width: 290px;
|
||||||
padding: 24px 0px 0px;
|
padding: 24px 0px 0px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-title {
|
.modal-title {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
padding-inline: 24px;
|
padding-inline: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-description {
|
.modal-description {
|
||||||
padding-inline: 24px;
|
padding-inline: 24px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-answers {
|
.modal-answers {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
margin-top: 24px;
|
margin-top: 24px;
|
||||||
border-top: 1px solid #D9D9D9;
|
border-top: 1px solid #d9d9d9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-answer {
|
.modal-answer {
|
||||||
width: 50%;
|
width: 50%;
|
||||||
height: 52px;
|
height: 52px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: #275DA7;
|
color: #275da7;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
&:first-child {
|
&:first-child {
|
||||||
border-right: 1px solid #D9D9D9;
|
border-right: 1px solid #d9d9d9;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.dark-theme) .modal-container {
|
:global(.dark-theme) .modal-container {
|
||||||
background-color: #343639;
|
background-color: #343639;
|
||||||
|
|
||||||
&>.modal-answers>.modal-answer {
|
& > .modal-answers > .modal-answer {
|
||||||
color: #1e7dff;
|
color: #1e7dff;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.dark-theme) .modal-title {
|
:global(.dark-theme) .modal-title {
|
||||||
color: #F7F7F7;
|
color: #f7f7f7;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.dark-theme) .modal-description {
|
:global(.dark-theme) .modal-description {
|
||||||
color: #F7F7F7;
|
color: #f7f7f7;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,23 +1,29 @@
|
|||||||
import { Suspense } from "react"
|
import { Suspense } from "react";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
|
|
||||||
import { CancelSubscriptionModalProvider, SubscriptionsList, SubscriptionsListSkeleton } from "@/components/domains/profile/subscriptions";
|
import {
|
||||||
|
CancelSubscriptionModalProvider,
|
||||||
|
SubscriptionsList,
|
||||||
|
SubscriptionsListSkeleton,
|
||||||
|
} from "@/components/domains/profile/subscriptions";
|
||||||
import { Typography } from "@/components/ui";
|
import { Typography } from "@/components/ui";
|
||||||
import { loadSubscriptionsData } from "@/entities/subscriptions/loaders";
|
import { loadSubscriptionsData } from "@/entities/subscriptions/loaders";
|
||||||
|
|
||||||
import styles from "./page.module.scss"
|
import styles from "./page.module.scss";
|
||||||
|
|
||||||
export default async function Subscriptions() {
|
export default async function Subscriptions() {
|
||||||
const t = await getTranslations("Subscriptions");
|
const t = await getTranslations("Subscriptions");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CancelSubscriptionModalProvider>
|
<CancelSubscriptionModalProvider>
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<Typography as="h1" className={styles.title}>{t("title")}</Typography>
|
<Typography as="h1" className={styles.title}>
|
||||||
<Suspense fallback={<SubscriptionsListSkeleton />}>
|
{t("title")}
|
||||||
<SubscriptionsList promise={loadSubscriptionsData()} />
|
</Typography>
|
||||||
</Suspense>
|
<Suspense fallback={<SubscriptionsListSkeleton />}>
|
||||||
</div>
|
<SubscriptionsList promise={loadSubscriptionsData()} />
|
||||||
</CancelSubscriptionModalProvider>
|
</Suspense>
|
||||||
)
|
</div>
|
||||||
}
|
</CancelSubscriptionModalProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -5,23 +5,24 @@ import { ROUTES } from "@/shared/constants/client-routes";
|
|||||||
import { ELottieKeys } from "@/shared/constants/lottie";
|
import { ELottieKeys } from "@/shared/constants/lottie";
|
||||||
|
|
||||||
export default async function AppreciateChoice() {
|
export default async function AppreciateChoice() {
|
||||||
const t = await getTranslations("AppreciateChoice");
|
const t = await getTranslations("AppreciateChoice");
|
||||||
|
|
||||||
const animationTexts = [
|
const animationTexts = [
|
||||||
t("descriptions.1"),
|
t("descriptions.1"),
|
||||||
t("descriptions.2"),
|
t("descriptions.2"),
|
||||||
t("descriptions.3"),
|
t("descriptions.3"),
|
||||||
]
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatedInfoScreen
|
<AnimatedInfoScreen
|
||||||
lottieAnimation={<LottieAnimation loadKey={ELottieKeys.loaderCheckMark} />}
|
lottieAnimation={
|
||||||
title={t("title")}
|
<LottieAnimation loadKey={ELottieKeys.loaderCheckMark} />
|
||||||
animationTime={9000}
|
}
|
||||||
animationTexts={animationTexts}
|
title={t("title")}
|
||||||
buttonText={t("button")}
|
animationTime={9000}
|
||||||
nextRoute={ROUTES.retainingFunnelWhatReason()}
|
animationTexts={animationTexts}
|
||||||
/>
|
buttonText={t("button")}
|
||||||
|
nextRoute={ROUTES.retainingFunnelWhatReason()}
|
||||||
)
|
/>
|
||||||
}
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -1,18 +1,18 @@
|
|||||||
.container {
|
.container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 24px;
|
gap: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
line-height: 125%;
|
line-height: 125%;
|
||||||
color: #1A1A1A;
|
color: #1a1a1a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.description {
|
.description {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
line-height: 125%;
|
line-height: 125%;
|
||||||
color: #2C2C2C;
|
color: #2c2c2c;
|
||||||
padding-inline: 14px;
|
padding-inline: 14px;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,19 +6,19 @@ import { Typography } from "@/components/ui";
|
|||||||
import styles from "./page.module.scss";
|
import styles from "./page.module.scss";
|
||||||
|
|
||||||
export default function CanselSubscription() {
|
export default function CanselSubscription() {
|
||||||
const t = useTranslations("CancelSubscription");
|
const t = useTranslations("CancelSubscription");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<Typography as="h1" size="xl" weight="bold" className={styles.title}>
|
<Typography as="h1" size="xl" weight="bold" className={styles.title}>
|
||||||
{t("title")}
|
{t("title")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography as="p" className={styles.description}>
|
<Typography as="p" className={styles.description}>
|
||||||
{t.rich("description", {
|
{t.rich("description", {
|
||||||
br: () => <br />
|
br: () => <br />,
|
||||||
})}
|
})}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Buttons />
|
<Buttons />
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,48 +1,52 @@
|
|||||||
.container {
|
.container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding-top: 28px;
|
padding-top: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
font-size: 27px;
|
font-size: 27px;
|
||||||
line-height: 40px;
|
line-height: 40px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.description {
|
.description {
|
||||||
line-height: 25px;
|
line-height: 25px;
|
||||||
margin-top: 74px;
|
margin-top: 74px;
|
||||||
padding-inline: 28px;
|
padding-inline: 28px;
|
||||||
color: #ACB0BA;
|
color: #acb0ba;
|
||||||
}
|
}
|
||||||
|
|
||||||
.topSellingImage {
|
.topSellingImage {
|
||||||
margin-top: -50px;
|
margin-top: -50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.offer {
|
.offer {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
background-image: url("/retaining/zodiac_circle.png");
|
background-image: url("/retaining/zodiac_circle.png");
|
||||||
background-size: 125%;
|
background-size: 125%;
|
||||||
background-position: center -45px;
|
background-position: center -45px;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
|
|
||||||
& * {
|
& * {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
background: linear-gradient(0deg, #FFFFFF 25.48%, rgba(255, 255, 255, 0) 100%);
|
background: linear-gradient(
|
||||||
}
|
0deg,
|
||||||
}
|
#ffffff 25.48%,
|
||||||
|
rgba(255, 255, 255, 0) 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,34 +1,37 @@
|
|||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
|
|
||||||
import { Offer } from "@/components/domains/retaining";
|
import { Offer } from "@/components/domains/retaining";
|
||||||
import { Buttons, LottieAnimations } from "@/components/domains/retaining/cancellation-of-subscription";
|
import {
|
||||||
|
Buttons,
|
||||||
|
LottieAnimations,
|
||||||
|
} from "@/components/domains/retaining/cancellation-of-subscription";
|
||||||
import { TopSellingSvg } from "@/components/domains/retaining/images";
|
import { TopSellingSvg } from "@/components/domains/retaining/images";
|
||||||
import { Typography } from "@/components/ui";
|
import { Typography } from "@/components/ui";
|
||||||
|
|
||||||
import styles from "./page.module.scss";
|
import styles from "./page.module.scss";
|
||||||
|
|
||||||
export default async function CancellationOfSubscription() {
|
export default async function CancellationOfSubscription() {
|
||||||
const t = await getTranslations("CancellationOfSubscription");
|
const t = await getTranslations("CancellationOfSubscription");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<LottieAnimations />
|
<LottieAnimations />
|
||||||
<Typography as="h1" weight="bold" className={styles.title}>
|
<Typography as="h1" weight="bold" className={styles.title}>
|
||||||
{t("title")}
|
{t("title")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography as="p" align="left" className={styles.description}>
|
<Typography as="p" align="left" className={styles.description}>
|
||||||
{t("description")}
|
{t("description")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Offer
|
<Offer
|
||||||
className={styles.offer}
|
className={styles.offer}
|
||||||
classNameTitle={styles.titleOffer}
|
classNameTitle={styles.titleOffer}
|
||||||
title={t("offer.title")}
|
title={t("offer.title")}
|
||||||
oldPrice={t("offer.old-price")}
|
oldPrice={t("offer.old-price")}
|
||||||
newPrice={t("offer.new-price")}
|
newPrice={t("offer.new-price")}
|
||||||
image={<TopSellingSvg className={styles.topSellingImage} />}
|
image={<TopSellingSvg className={styles.topSellingImage} />}
|
||||||
active={true}
|
active={true}
|
||||||
/>
|
/>
|
||||||
<Buttons />
|
<Buttons />
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,36 +1,39 @@
|
|||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
import { ChangeMindAnswer, ChangeMindButtons } from "@/components/domains/retaining/change-mind";
|
import {
|
||||||
|
ChangeMindAnswer,
|
||||||
|
ChangeMindButtons,
|
||||||
|
} from "@/components/domains/retaining/change-mind";
|
||||||
import { Typography } from "@/components/ui";
|
import { Typography } from "@/components/ui";
|
||||||
|
|
||||||
export default function ChangeMind() {
|
export default function ChangeMind() {
|
||||||
const t = useTranslations("ChangeMind")
|
const t = useTranslations("ChangeMind");
|
||||||
|
|
||||||
const answers: ChangeMindAnswer[] = [
|
const answers: ChangeMindAnswer[] = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
title: t("answers.more_chat_time"),
|
title: t("answers.more_chat_time"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
title: t("answers.more_personal_reports"),
|
title: t("answers.more_personal_reports"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
title: t("answers.individual_plan"),
|
title: t("answers.individual_plan"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 4,
|
id: 4,
|
||||||
title: t("answers.other"),
|
title: t("answers.other"),
|
||||||
}
|
},
|
||||||
]
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Typography size="xl" weight="bold" as="h1">
|
<Typography size="xl" weight="bold" as="h1">
|
||||||
{t("title")}
|
{t("title")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<ChangeMindButtons answers={answers} />
|
<ChangeMindButtons answers={answers} />
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,51 +5,48 @@ import { ROUTES } from "@/shared/constants/client-routes";
|
|||||||
import { ERetainingFunnel } from "@/types";
|
import { ERetainingFunnel } from "@/types";
|
||||||
|
|
||||||
const stepperRoutes: Record<ERetainingFunnel, string[]> = {
|
const stepperRoutes: Record<ERetainingFunnel, string[]> = {
|
||||||
[ERetainingFunnel.Red]: [
|
[ERetainingFunnel.Red]: [
|
||||||
ROUTES.retainingFunnelAppreciateChoice(),
|
ROUTES.retainingFunnelAppreciateChoice(),
|
||||||
// ROUTES.retainingFunnelWhatReason(),
|
// ROUTES.retainingFunnelWhatReason(),
|
||||||
// ROUTES.retainingFunnelSecondChance(),
|
// ROUTES.retainingFunnelSecondChance(),
|
||||||
// ROUTES.retainingFunnelChangeMind(),
|
// ROUTES.retainingFunnelChangeMind(),
|
||||||
// ROUTES.retainingFunnelStopFor30Days(),
|
// ROUTES.retainingFunnelStopFor30Days(),
|
||||||
// ROUTES.retainingFunnelCancellationOfSubscription(),
|
// ROUTES.retainingFunnelCancellationOfSubscription(),
|
||||||
],
|
],
|
||||||
[ERetainingFunnel.Green]: [
|
[ERetainingFunnel.Green]: [
|
||||||
ROUTES.retainingFunnelAppreciateChoice(),
|
ROUTES.retainingFunnelAppreciateChoice(),
|
||||||
// ROUTES.retainingFunnelWhatReason(),
|
// ROUTES.retainingFunnelWhatReason(),
|
||||||
// ROUTES.retainingFunnelStopFor30Days(),
|
// ROUTES.retainingFunnelStopFor30Days(),
|
||||||
// ROUTES.retainingFunnelChangeMind(),
|
// ROUTES.retainingFunnelChangeMind(),
|
||||||
// ROUTES.retainingFunnelSecondChance(),
|
// ROUTES.retainingFunnelSecondChance(),
|
||||||
// ROUTES.retainingFunnelCancellationOfSubscription(),
|
// 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 }) {
|
||||||
[ERetainingFunnel.Purple]: [
|
// const darkTheme = useSelector(selectors.selectDarkTheme);
|
||||||
ROUTES.retainingFunnelAppreciateChoice(),
|
// const mainRef = useRef<HTMLDivElement>(null);
|
||||||
// ROUTES.retainingFunnelWhatReason(),
|
// useSchemeColorByElement(mainRef.current, "section.page, .page, section", [
|
||||||
// ROUTES.retainingFunnelChangeMind(),
|
// location,
|
||||||
// ROUTES.retainingFunnelSecondChance(),
|
// ]);
|
||||||
// ROUTES.retainingFunnelStopFor30Days(),
|
// const retainingFunnel = useSelector(selectors.selectRetainingFunnel);
|
||||||
// ROUTES.retainingFunnelCancellationOfSubscription(),
|
const retainingFunnel = ERetainingFunnel.Red;
|
||||||
],
|
|
||||||
[ERetainingFunnel.Stay50]: [
|
|
||||||
ROUTES.retainingFunnelStay50Done(),
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
function StepperLayout({ children }: { children: React.ReactNode; }) {
|
return (
|
||||||
// const darkTheme = useSelector(selectors.selectDarkTheme);
|
<>
|
||||||
// const mainRef = useRef<HTMLDivElement>(null);
|
<RetainingStepper stepperRoutes={stepperRoutes[retainingFunnel]} />
|
||||||
// useSchemeColorByElement(mainRef.current, "section.page, .page, section", [
|
{children}
|
||||||
// location,
|
</>
|
||||||
// ]);
|
);
|
||||||
// const retainingFunnel = useSelector(selectors.selectRetainingFunnel);
|
|
||||||
const retainingFunnel = ERetainingFunnel.Red;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<RetainingStepper stepperRoutes={stepperRoutes[retainingFunnel]} />
|
|
||||||
{children}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default StepperLayout;
|
export default StepperLayout;
|
||||||
|
|||||||
@ -1,27 +1,27 @@
|
|||||||
.container {
|
.container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
padding: 80px 28px 0;
|
padding: 80px 28px 0;
|
||||||
min-height: calc(100dvh - 124px);
|
min-height: calc(100dvh - 124px);
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
font-size: 27px;
|
font-size: 27px;
|
||||||
line-height: 40px;
|
line-height: 40px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
font-size: 80px;
|
font-size: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.description {
|
.description {
|
||||||
color: #ACB0BA;
|
color: #acb0ba;
|
||||||
font-size: 17px;
|
font-size: 17px;
|
||||||
line-height: 25px;
|
line-height: 25px;
|
||||||
margin-top: 72px;
|
margin-top: 72px;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,20 +6,18 @@ import { Typography } from "@/components/ui";
|
|||||||
import styles from "./page.module.scss";
|
import styles from "./page.module.scss";
|
||||||
|
|
||||||
export default async function PlanCancelled() {
|
export default async function PlanCancelled() {
|
||||||
const t = await getTranslations("PlanCancelled");
|
const t = await getTranslations("PlanCancelled");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<Typography as="h1" weight="semiBold" className={styles.title}>
|
<Typography as="h1" weight="semiBold" className={styles.title}>
|
||||||
{t("title")}
|
{t("title")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<span className={styles.icon}>
|
<span className={styles.icon}>{t("icon")}</span>
|
||||||
{t("icon")}
|
<PlanCancelledButton />
|
||||||
</span>
|
<Typography as="p" className={styles.description}>
|
||||||
<PlanCancelledButton />
|
{t("description")}
|
||||||
<Typography as="p" className={styles.description}>
|
</Typography>
|
||||||
{t("description")}
|
</div>
|
||||||
</Typography>
|
);
|
||||||
</div>
|
}
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
.container {
|
.container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
// overflow-x: clip;
|
// overflow-x: clip;
|
||||||
// padding-inline: 2px;
|
// padding-inline: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
line-height: 150%;
|
line-height: 150%;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,14 +6,14 @@ import { Typography } from "@/components/ui";
|
|||||||
import styles from "./page.module.scss";
|
import styles from "./page.module.scss";
|
||||||
|
|
||||||
export default async function SecondChance() {
|
export default async function SecondChance() {
|
||||||
const t = await getTranslations("SecondChance");
|
const t = await getTranslations("SecondChance");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<Typography as="h1" weight="bold" size="xl" className={styles.title}>
|
<Typography as="h1" weight="bold" size="xl" className={styles.title}>
|
||||||
{t("title")}
|
{t("title")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<SecondChancePage />
|
<SecondChancePage />
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,21 +5,20 @@ import { ROUTES } from "@/shared/constants/client-routes";
|
|||||||
import { ELottieKeys } from "@/shared/constants/lottie";
|
import { ELottieKeys } from "@/shared/constants/lottie";
|
||||||
|
|
||||||
export default async function Stay50Done() {
|
export default async function Stay50Done() {
|
||||||
const t = await getTranslations("Stay50Done");
|
const t = await getTranslations("Stay50Done");
|
||||||
|
|
||||||
const animationTexts = [
|
const animationTexts = [t("descriptions.1")];
|
||||||
t("descriptions.1"),
|
|
||||||
]
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatedInfoScreen
|
<AnimatedInfoScreen
|
||||||
lottieAnimation={<LottieAnimation loadKey={ELottieKeys.loaderCheckMark} />}
|
lottieAnimation={
|
||||||
title={t("title")}
|
<LottieAnimation loadKey={ELottieKeys.loaderCheckMark} />
|
||||||
animationTime={5000}
|
}
|
||||||
animationTexts={animationTexts}
|
title={t("title")}
|
||||||
buttonText={t("button")}
|
animationTime={5000}
|
||||||
nextRoute={ROUTES.home()}
|
animationTexts={animationTexts}
|
||||||
/>
|
buttonText={t("button")}
|
||||||
|
nextRoute={ROUTES.home()}
|
||||||
)
|
/>
|
||||||
}
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
.title {
|
.title {
|
||||||
font-size: 27px;
|
font-size: 27px;
|
||||||
line-height: 40px;
|
line-height: 40px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,14 +6,14 @@ import { Typography } from "@/components/ui";
|
|||||||
import styles from "./page.module.scss";
|
import styles from "./page.module.scss";
|
||||||
|
|
||||||
export default async function StopFor30Days() {
|
export default async function StopFor30Days() {
|
||||||
const t = await getTranslations("StopFor30Days");
|
const t = await getTranslations("StopFor30Days");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Typography as="h1" weight="bold" className={styles.title}>
|
<Typography as="h1" weight="bold" className={styles.title}>
|
||||||
{t("title")}
|
{t("title")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<StopFor30DaysButtons />
|
<StopFor30DaysButtons />
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,20 +1,20 @@
|
|||||||
.container {
|
.container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
padding: 80px 28px 0;
|
padding: 80px 28px 0;
|
||||||
min-height: calc(100dvh - 124px);
|
min-height: calc(100dvh - 124px);
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
font-size: 27px;
|
font-size: 27px;
|
||||||
line-height: 40px;
|
line-height: 40px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
font-size: 80px;
|
font-size: 80px;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,17 +6,15 @@ import { Typography } from "@/components/ui";
|
|||||||
import styles from "./page.module.scss";
|
import styles from "./page.module.scss";
|
||||||
|
|
||||||
export default async function SubscriptionStopped() {
|
export default async function SubscriptionStopped() {
|
||||||
const t = await getTranslations("SubscriptionStopped");
|
const t = await getTranslations("SubscriptionStopped");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<Typography as="h1" weight="semiBold" className={styles.title}>
|
<Typography as="h1" weight="semiBold" className={styles.title}>
|
||||||
{t("title")}
|
{t("title")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<span className={styles.icon}>
|
<span className={styles.icon}>{t("icon")}</span>
|
||||||
{t("icon")}
|
<SubscriptionStoppedButton />
|
||||||
</span>
|
</div>
|
||||||
<SubscriptionStoppedButton />
|
);
|
||||||
</div>
|
}
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,71 +1,74 @@
|
|||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
import { WhatReasonAnswer, WhatReasonsButtons } from "@/components/domains/retaining/what-reason";
|
import {
|
||||||
|
WhatReasonAnswer,
|
||||||
|
WhatReasonsButtons,
|
||||||
|
} from "@/components/domains/retaining/what-reason";
|
||||||
import { Typography } from "@/components/ui";
|
import { Typography } from "@/components/ui";
|
||||||
import { ERetainingFunnel } from "@/types";
|
import { ERetainingFunnel } from "@/types";
|
||||||
|
|
||||||
export default function WhatReason() {
|
export default function WhatReason() {
|
||||||
const t = useTranslations("WhatReason")
|
const t = useTranslations("WhatReason");
|
||||||
|
|
||||||
const answers: WhatReasonAnswer[] = [
|
const answers: WhatReasonAnswer[] = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
title: t("answers.no_promised_result"),
|
title: t("answers.no_promised_result"),
|
||||||
funnel: ERetainingFunnel.Red
|
funnel: ERetainingFunnel.Red,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
title: t("answers.too_expensive"),
|
title: t("answers.too_expensive"),
|
||||||
funnel: ERetainingFunnel.Red
|
funnel: ERetainingFunnel.Red,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
title: t("answers.high_auto_payment"),
|
title: t("answers.high_auto_payment"),
|
||||||
funnel: ERetainingFunnel.Red
|
funnel: ERetainingFunnel.Red,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 4,
|
id: 4,
|
||||||
title: t("answers.unexpected_fee"),
|
title: t("answers.unexpected_fee"),
|
||||||
funnel: ERetainingFunnel.Red
|
funnel: ERetainingFunnel.Red,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 5,
|
id: 5,
|
||||||
title: t("answers.want_pause"),
|
title: t("answers.want_pause"),
|
||||||
funnel: ERetainingFunnel.Green
|
funnel: ERetainingFunnel.Green,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 6,
|
id: 6,
|
||||||
title: t("answers.service_not_as_expected"),
|
title: t("answers.service_not_as_expected"),
|
||||||
funnel: ERetainingFunnel.Red
|
funnel: ERetainingFunnel.Red,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 7,
|
id: 7,
|
||||||
title: t("answers.found_alternative"),
|
title: t("answers.found_alternative"),
|
||||||
funnel: ERetainingFunnel.Red
|
funnel: ERetainingFunnel.Red,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 8,
|
id: 8,
|
||||||
title: t("answers.dislike_app"),
|
title: t("answers.dislike_app"),
|
||||||
funnel: ERetainingFunnel.Purple
|
funnel: ERetainingFunnel.Purple,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 9,
|
id: 9,
|
||||||
title: t("answers.hard_to_navigate"),
|
title: t("answers.hard_to_navigate"),
|
||||||
funnel: ERetainingFunnel.Purple
|
funnel: ERetainingFunnel.Purple,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 10,
|
id: 10,
|
||||||
title: t("answers.other"),
|
title: t("answers.other"),
|
||||||
funnel: ERetainingFunnel.Purple
|
funnel: ERetainingFunnel.Purple,
|
||||||
}
|
},
|
||||||
]
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Typography size="xl" weight="bold" as="h1">
|
<Typography size="xl" weight="bold" as="h1">
|
||||||
{t("title")}
|
{t("title")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<WhatReasonsButtons answers={answers} />
|
<WhatReasonsButtons answers={answers} />
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,8 +34,14 @@ import { ROUTES } from "@/shared/constants/client-routes";
|
|||||||
|
|
||||||
function extractTrackingCookiesFromUrl(url: URL): Record<string, string> {
|
function extractTrackingCookiesFromUrl(url: URL): Record<string, string> {
|
||||||
const trackingCookieKeys = [
|
const trackingCookieKeys = [
|
||||||
'_fbc', '_fbp', '_ym_uid', '_ym_d', '_ym_isad', '_ym_visorc',
|
"_fbc",
|
||||||
'yandexuid', 'ymex'
|
"_fbp",
|
||||||
|
"_ym_uid",
|
||||||
|
"_ym_d",
|
||||||
|
"_ym_isad",
|
||||||
|
"_ym_visorc",
|
||||||
|
"yandexuid",
|
||||||
|
"ymex",
|
||||||
];
|
];
|
||||||
|
|
||||||
const cookies: Record<string, string> = {};
|
const cookies: Record<string, string> = {};
|
||||||
@ -43,8 +49,8 @@ function extractTrackingCookiesFromUrl(url: URL): Record<string, string> {
|
|||||||
for (const [key, value] of url.searchParams.entries()) {
|
for (const [key, value] of url.searchParams.entries()) {
|
||||||
if (
|
if (
|
||||||
trackingCookieKeys.includes(key) ||
|
trackingCookieKeys.includes(key) ||
|
||||||
key.startsWith('_ga') ||
|
key.startsWith("_ga") ||
|
||||||
key.startsWith('_gid')
|
key.startsWith("_gid")
|
||||||
) {
|
) {
|
||||||
cookies[key] = value;
|
cookies[key] = value;
|
||||||
}
|
}
|
||||||
@ -63,7 +69,10 @@ export async function GET(req: NextRequest) {
|
|||||||
const productPrice = searchParams.get("price");
|
const productPrice = searchParams.get("price");
|
||||||
const currency = searchParams.get("currency");
|
const currency = searchParams.get("currency");
|
||||||
|
|
||||||
const redirectUrl = new URL(`${ROUTES.payment()}`, process.env.NEXT_PUBLIC_APP_URL || "");
|
const redirectUrl = new URL(
|
||||||
|
`${ROUTES.payment()}`,
|
||||||
|
process.env.NEXT_PUBLIC_APP_URL || ""
|
||||||
|
);
|
||||||
if (productId) redirectUrl.searchParams.set("productId", productId);
|
if (productId) redirectUrl.searchParams.set("productId", productId);
|
||||||
if (placementId) redirectUrl.searchParams.set("placementId", placementId);
|
if (placementId) redirectUrl.searchParams.set("placementId", placementId);
|
||||||
if (paywallId) redirectUrl.searchParams.set("paywallId", paywallId);
|
if (paywallId) redirectUrl.searchParams.set("paywallId", paywallId);
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
.body {
|
.body {
|
||||||
max-width: 560px;
|
max-width: 560px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,14 +5,16 @@ import type { Metadata } from "next";
|
|||||||
import { Inter } from "next/font/google";
|
import { Inter } from "next/font/google";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { hasLocale, NextIntlClientProvider } from "next-intl";
|
import { hasLocale, NextIntlClientProvider } from "next-intl";
|
||||||
|
import { getMessages } from "next-intl/server";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
||||||
import { routing } from "@/i18n/routing";
|
import { routing } from "@/i18n/routing";
|
||||||
|
import { StoreProvider } from "@/providers/StoreProvider";
|
||||||
|
|
||||||
import styles from "./layout.module.scss"
|
import styles from "./layout.module.scss";
|
||||||
|
|
||||||
export function generateStaticParams() {
|
export function generateStaticParams() {
|
||||||
return routing.locales.map((locale) => ({ locale }));
|
return routing.locales.map(locale => ({ locale }));
|
||||||
}
|
}
|
||||||
|
|
||||||
const inter = Inter({
|
const inter = Inter({
|
||||||
@ -23,7 +25,8 @@ const inter = Inter({
|
|||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "WIT",
|
title: "WIT",
|
||||||
description: "More than 14M people have experienced the value of our products. Wit Apps, headquartered in Silicon Valley, California, is a tech company that constructs global enterprises specializing in mobile-first products. We believe in the transformative power of technology, capable of turning chaos into miracles, thus enhancing the lives of millions. To realize this vision, we integrate leading expertise in artificial intelligence with a deep understanding of consumer needs and lifestyle trends.",
|
description:
|
||||||
|
"More than 14M people have experienced the value of our products. Wit Apps, headquartered in Silicon Valley, California, is a tech company that constructs global enterprises specializing in mobile-first products. We believe in the transformative power of technology, capable of turning chaos into miracles, thus enhancing the lives of millions. To realize this vision, we integrate leading expertise in artificial intelligence with a deep understanding of consumer needs and lifestyle trends.",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function RootLayout({
|
export default async function RootLayout({
|
||||||
@ -38,10 +41,14 @@ export default async function RootLayout({
|
|||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const messages = await getMessages();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang={locale}>
|
<html lang={locale}>
|
||||||
<body className={clsx(inter.variable, styles.body)}>
|
<body className={clsx(inter.variable, styles.body)}>
|
||||||
<NextIntlClientProvider>{children}</NextIntlClientProvider>
|
<NextIntlClientProvider messages={messages}>
|
||||||
|
<StoreProvider>{children}</StoreProvider>
|
||||||
|
</NextIntlClientProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -0,0 +1,9 @@
|
|||||||
|
.errorToast {
|
||||||
|
position: fixed;
|
||||||
|
bottom: calc(0dvh + 32px);
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
@ -0,0 +1,86 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { use, useState } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
import { Skeleton, Toast, Typography } from "@/components/ui";
|
||||||
|
import { ActionFieldsForm } from "@/components/widgets";
|
||||||
|
import { startGeneration } from "@/entities/generations/actions";
|
||||||
|
import { ROUTES } from "@/shared/constants/client-routes";
|
||||||
|
import { ActionField } from "@/types";
|
||||||
|
|
||||||
|
import styles from "./CompatibilityActionFieldsForm.module.scss";
|
||||||
|
|
||||||
|
interface CompatibilityActionFieldsFormProps {
|
||||||
|
fields: Promise<ActionField[]>;
|
||||||
|
actionId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CompatibilityActionFieldsForm({
|
||||||
|
fields,
|
||||||
|
actionId,
|
||||||
|
}: CompatibilityActionFieldsFormProps) {
|
||||||
|
const t = useTranslations("Compatibility");
|
||||||
|
const compatibilityActionFields = use(fields);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [formError, setFormError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const handleSubmit = async (
|
||||||
|
values: Record<string, string | number | null>
|
||||||
|
) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
setFormError(null);
|
||||||
|
|
||||||
|
const response = await startGeneration({
|
||||||
|
actionType: "compatibility",
|
||||||
|
actionId,
|
||||||
|
variables: values,
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
if (response?.data?.id) {
|
||||||
|
router.push(ROUTES.compatibilityResult(response.data.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.error) {
|
||||||
|
setFormError(response.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Обработка случая, когда поля не загрузились
|
||||||
|
if (!compatibilityActionFields || compatibilityActionFields.length === 0) {
|
||||||
|
return (
|
||||||
|
<Typography as="p" size="sm" color="danger">
|
||||||
|
{t("error")}
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ActionFieldsForm
|
||||||
|
fields={compatibilityActionFields}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
buttonText={t("button")}
|
||||||
|
isLoading={isLoading}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{formError && (
|
||||||
|
<Toast variant="error" classNameContainer={styles.errorToast}>
|
||||||
|
<Typography as="p" size="sm" color="black">
|
||||||
|
{t("error")}
|
||||||
|
</Typography>
|
||||||
|
</Toast>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CompatibilityActionFieldsFormSkeleton() {
|
||||||
|
return <Skeleton style={{ height: "250px" }} />;
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
.loadingContainer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: calc(100dvh - 56px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
line-height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
line-height: 25px;
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
import { Spinner, Toast, Typography } from "@/components/ui";
|
||||||
|
import { useGenerationPolling } from "@/hooks/generation/useGenerationPolling";
|
||||||
|
|
||||||
|
import styles from "./CompatibilityResultPage.module.scss";
|
||||||
|
|
||||||
|
interface CompatibilityResultPageProps {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CompatibilityResultPage({
|
||||||
|
id,
|
||||||
|
}: CompatibilityResultPageProps) {
|
||||||
|
const t = useTranslations("CompatibilityResult");
|
||||||
|
const { data, error, isLoading } = useGenerationPolling(id);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className={styles.loadingContainer}>
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <Toast variant="error">{t("error")}</Toast>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Typography as="h1" size="xl" weight="semiBold" className={styles.title}>
|
||||||
|
{t("title")}
|
||||||
|
</Typography>
|
||||||
|
<Typography as="p" size="lg" align="left" className={styles.description}>
|
||||||
|
{data?.result}
|
||||||
|
</Typography>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,63 +1,63 @@
|
|||||||
.card.card {
|
.card.card {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
min-width: 160px;
|
min-width: 160px;
|
||||||
height: 235px;
|
height: 235px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .info {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 10px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
align-items: flex-start;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
&>* {
|
& > .name {
|
||||||
z-index: 1;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
& > .indicator {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #34d399;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&>.info {
|
& > .rating {
|
||||||
width: 100%;
|
display: flex;
|
||||||
display: flex;
|
align-items: center;
|
||||||
flex-direction: column;
|
gap: 4px;
|
||||||
align-items: flex-start;
|
|
||||||
gap: 6px;
|
|
||||||
|
|
||||||
&>.name {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-start;
|
|
||||||
gap: 6px;
|
|
||||||
|
|
||||||
&>.indicator {
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: #34D399;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&>.rating {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.shadow {
|
.shadow {
|
||||||
height: 160px;
|
height: 160px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
background: linear-gradient(0deg, #174280 0%, rgba(0, 0, 0, 0) 70.95%);
|
background: linear-gradient(0deg, #174280 0%, rgba(0, 0, 0, 0) 70.95%);
|
||||||
// border: 1px solid rgba(229, 231, 235, 1);
|
// border: 1px solid rgba(229, 231, 235, 1);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,47 +1,55 @@
|
|||||||
import { Button, Card, Stars, Typography } from "@/components/ui"
|
import { Button, Card, Stars, Typography } from "@/components/ui";
|
||||||
import { Assistant } from "@/entities/dashboard/types"
|
import { Assistant } from "@/entities/dashboard/types";
|
||||||
|
|
||||||
import styles from "./AdviserCard.module.scss"
|
import styles from "./AdviserCard.module.scss";
|
||||||
|
|
||||||
type AdviserCardProps = Assistant;
|
type AdviserCardProps = Assistant;
|
||||||
|
|
||||||
export default function AdviserCard({
|
export default function AdviserCard({
|
||||||
name,
|
name,
|
||||||
photoUrl,
|
photoUrl,
|
||||||
rating,
|
rating,
|
||||||
reviewCount,
|
reviewCount,
|
||||||
description
|
description,
|
||||||
}: AdviserCardProps) {
|
}: AdviserCardProps) {
|
||||||
return (
|
return (
|
||||||
<Card className={styles.card} style={{ backgroundImage: `url(${photoUrl})` }}>
|
<Card
|
||||||
<div className={styles.content}>
|
className={styles.card}
|
||||||
<div className={styles.info}>
|
style={{ backgroundImage: `url(${photoUrl})` }}
|
||||||
<div className={styles.name}>
|
>
|
||||||
<Typography color="white" weight="bold">
|
<div className={styles.content}>
|
||||||
{name}
|
<div className={styles.info}>
|
||||||
</Typography>
|
<div className={styles.name}>
|
||||||
<div className={styles.indicator} />
|
<Typography color="white" weight="bold">
|
||||||
</div>
|
{name}
|
||||||
<Typography className={styles.description} color="white" weight="medium" size="xs">
|
</Typography>
|
||||||
{description}
|
<div className={styles.indicator} />
|
||||||
</Typography>
|
</div>
|
||||||
<div className={styles.rating}>
|
<Typography
|
||||||
<Typography color="white" weight="medium" size="xs">
|
className={styles.description}
|
||||||
{rating}
|
color="white"
|
||||||
</Typography>
|
weight="medium"
|
||||||
<Stars rating={rating} />
|
size="xs"
|
||||||
<Typography color="white" weight="medium" size="xs">
|
>
|
||||||
({reviewCount})
|
{description}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
<div className={styles.rating}>
|
||||||
</div>
|
<Typography color="white" weight="medium" size="xs">
|
||||||
<Button size="sm">
|
{rating}
|
||||||
<Typography color="white" weight="bold" size="sm">
|
</Typography>
|
||||||
CHAT | FREE
|
<Stars rating={rating} />
|
||||||
</Typography>
|
<Typography color="white" weight="medium" size="xs">
|
||||||
</Button>
|
({reviewCount})
|
||||||
</div>
|
</Typography>
|
||||||
<div className={styles.shadow} />
|
</div>
|
||||||
</Card>
|
</div>
|
||||||
)
|
<Button size="sm">
|
||||||
}
|
<Typography color="white" weight="bold" size="sm">
|
||||||
|
CHAT | FREE
|
||||||
|
</Typography>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className={styles.shadow} />
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -1,21 +1,22 @@
|
|||||||
.card.card {
|
.card.card {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
min-width: 320px;
|
min-width: 320px;
|
||||||
height: 110px;
|
height: 110px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
padding: 22px 16px 16px;
|
padding: 22px 16px 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.compatibilityImage {
|
.compatibilityImage {
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
object-position: center;
|
object-position: center;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,33 +9,35 @@ import styles from "./CompatibilityCard.module.scss";
|
|||||||
type CompatibilityCardProps = CompatibilityAction;
|
type CompatibilityCardProps = CompatibilityAction;
|
||||||
|
|
||||||
export default function CompatibilityCard({
|
export default function CompatibilityCard({
|
||||||
imageUrl,
|
imageUrl,
|
||||||
title,
|
title,
|
||||||
type,
|
type,
|
||||||
minutes
|
minutes,
|
||||||
}: CompatibilityCardProps) {
|
}: CompatibilityCardProps) {
|
||||||
return (
|
return (
|
||||||
<Card className={styles.card}>
|
<Card className={styles.card}>
|
||||||
<Image
|
<Image
|
||||||
className={styles.compatibilityImage}
|
className={styles.compatibilityImage}
|
||||||
src={imageUrl}
|
src={imageUrl}
|
||||||
alt="Compatibility image"
|
alt="Compatibility image"
|
||||||
width={120}
|
width={120}
|
||||||
height={110}
|
height={110}
|
||||||
/>
|
/>
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<Typography size="lg" weight="medium" align="left">
|
<Typography size="lg" weight="medium" align="left">
|
||||||
{title}
|
{title}
|
||||||
</Typography>
|
</Typography>
|
||||||
<MetaLabel iconLabelProps={{
|
<MetaLabel
|
||||||
iconProps: {
|
iconLabelProps={{
|
||||||
name: IconName.Article,
|
iconProps: {
|
||||||
},
|
name: IconName.Article,
|
||||||
children: <Typography color="secondary">{type}</Typography>
|
},
|
||||||
}}>
|
children: <Typography color="secondary">{type}</Typography>,
|
||||||
{minutes} min
|
}}
|
||||||
</MetaLabel>
|
>
|
||||||
</div>
|
{minutes} min
|
||||||
</Card>
|
</MetaLabel>
|
||||||
)
|
</div>
|
||||||
}
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -1,41 +1,41 @@
|
|||||||
.card.card {
|
.card.card {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
min-width: 342px;
|
min-width: 342px;
|
||||||
height: 308px;
|
height: 308px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
& > .info {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
align-items: flex-start;
|
||||||
align-items: center;
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
&>.info {
|
& > .button {
|
||||||
display: flex;
|
width: 40px;
|
||||||
flex-direction: column;
|
height: 40px;
|
||||||
align-items: flex-start;
|
border-radius: 50%;
|
||||||
gap: 4px;
|
background-color: #f5f5f7;
|
||||||
}
|
padding: 0;
|
||||||
|
|
||||||
&>.button {
|
& > .icon {
|
||||||
width: 40px;
|
transform: rotate(180deg);
|
||||||
height: 40px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: #F5F5F7;
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
&>.icon {
|
|
||||||
transform: rotate(180deg);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.meditationImage {
|
.meditationImage {
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
object-position: center;
|
object-position: center;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,56 +4,58 @@ import { Button, Card, Icon, MetaLabel, Typography } from "@/components/ui";
|
|||||||
import { IconName } from "@/components/ui/Icon/Icon";
|
import { IconName } from "@/components/ui/Icon/Icon";
|
||||||
import { Meditation } from "@/entities/dashboard/types";
|
import { Meditation } from "@/entities/dashboard/types";
|
||||||
|
|
||||||
import styles from "./MeditationCard.module.scss"
|
import styles from "./MeditationCard.module.scss";
|
||||||
|
|
||||||
type MeditationCardProps = Meditation;
|
type MeditationCardProps = Meditation;
|
||||||
|
|
||||||
export default function MeditationCard({
|
export default function MeditationCard({
|
||||||
imageUrl,
|
imageUrl,
|
||||||
title,
|
title,
|
||||||
type,
|
type,
|
||||||
minutes
|
minutes,
|
||||||
}: MeditationCardProps) {
|
}: MeditationCardProps) {
|
||||||
return (
|
return (
|
||||||
<Card className={styles.card}>
|
<Card className={styles.card}>
|
||||||
<Image
|
<Image
|
||||||
className={styles.meditationImage}
|
className={styles.meditationImage}
|
||||||
src={imageUrl}
|
src={imageUrl}
|
||||||
alt="Meditation image"
|
alt="Meditation image"
|
||||||
width={342}
|
width={342}
|
||||||
height={216}
|
height={216}
|
||||||
/>
|
/>
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<div className={styles.info}>
|
<div className={styles.info}>
|
||||||
<Typography size="lg" weight="regular">
|
<Typography size="lg" weight="regular">
|
||||||
{title}
|
{title}
|
||||||
</Typography>
|
</Typography>
|
||||||
<MetaLabel iconLabelProps={{
|
<MetaLabel
|
||||||
iconProps: {
|
iconLabelProps={{
|
||||||
name: IconName.Video,
|
iconProps: {
|
||||||
color: "#6B7280",
|
name: IconName.Video,
|
||||||
size: {
|
color: "#6B7280",
|
||||||
width: 24,
|
size: {
|
||||||
height: 25
|
width: 24,
|
||||||
}
|
height: 25,
|
||||||
},
|
},
|
||||||
children: <Typography color="secondary">{type}</Typography>
|
},
|
||||||
}}>
|
children: <Typography color="secondary">{type}</Typography>,
|
||||||
{minutes} min
|
}}
|
||||||
</MetaLabel>
|
>
|
||||||
</div>
|
{minutes} min
|
||||||
<Button className={styles.button}>
|
</MetaLabel>
|
||||||
<Icon
|
</div>
|
||||||
className={styles.icon}
|
<Button className={styles.button}>
|
||||||
name={IconName.Chevron}
|
<Icon
|
||||||
size={{
|
className={styles.icon}
|
||||||
width: 18,
|
name={IconName.Chevron}
|
||||||
height: 18
|
size={{
|
||||||
}}
|
width: 18,
|
||||||
color="#A0A7B5"
|
height: 18,
|
||||||
/>
|
}}
|
||||||
</Button>
|
color="#A0A7B5"
|
||||||
</div>
|
/>
|
||||||
</Card>
|
</Button>
|
||||||
)
|
</div>
|
||||||
}
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -1,33 +1,37 @@
|
|||||||
.card.card {
|
.card.card {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
height: 227px;
|
height: 227px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image {
|
.image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 123px;
|
height: 123px;
|
||||||
background: linear-gradient(90deg, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0) 100%);
|
background: linear-gradient(
|
||||||
display: flex;
|
90deg,
|
||||||
justify-content: center;
|
rgba(0, 0, 0, 0.5) 0%,
|
||||||
|
rgba(0, 0, 0, 0) 100%
|
||||||
|
);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
padding: 14px 12px 12px;
|
padding: 14px 12px 12px;
|
||||||
|
|
||||||
&>.info {
|
& > .info {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 15px;
|
gap: 15px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.palmImage {
|
.palmImage {
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
object-position: center;
|
object-position: center;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,42 +9,44 @@ import styles from "./PalmCard.module.scss";
|
|||||||
type PalmCardProps = PalmAction;
|
type PalmCardProps = PalmAction;
|
||||||
|
|
||||||
export default function PalmCard({
|
export default function PalmCard({
|
||||||
imageUrl,
|
imageUrl,
|
||||||
title,
|
title,
|
||||||
type,
|
type,
|
||||||
minutes
|
minutes,
|
||||||
}: PalmCardProps) {
|
}: PalmCardProps) {
|
||||||
return (
|
return (
|
||||||
<Card className={styles.card}>
|
<Card className={styles.card}>
|
||||||
<div className={styles.image}>
|
<div className={styles.image}>
|
||||||
<Image
|
<Image
|
||||||
className={styles.palmImage}
|
className={styles.palmImage}
|
||||||
src={imageUrl}
|
src={imageUrl}
|
||||||
alt="Palm image"
|
alt="Palm image"
|
||||||
width={99}
|
width={99}
|
||||||
height={123}
|
height={123}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<div className={styles.info}>
|
<div className={styles.info}>
|
||||||
<Typography size="lg" align="left">
|
<Typography size="lg" align="left">
|
||||||
{title}
|
{title}
|
||||||
</Typography>
|
</Typography>
|
||||||
<MetaLabel iconLabelProps={{
|
<MetaLabel
|
||||||
iconProps: {
|
iconLabelProps={{
|
||||||
name: IconName.Video,
|
iconProps: {
|
||||||
color: "#6B7280",
|
name: IconName.Video,
|
||||||
size: {
|
color: "#6B7280",
|
||||||
width: 24,
|
size: {
|
||||||
height: 25
|
width: 24,
|
||||||
}
|
height: 25,
|
||||||
},
|
},
|
||||||
children: <Typography color="secondary">{type}</Typography>
|
},
|
||||||
}}>
|
children: <Typography color="secondary">{type}</Typography>,
|
||||||
{minutes} min
|
}}
|
||||||
</MetaLabel>
|
>
|
||||||
</div>
|
{minutes} min
|
||||||
</div>
|
</MetaLabel>
|
||||||
</Card>
|
</div>
|
||||||
)
|
</div>
|
||||||
}
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
export { default as AdviserCard } from './AdviserCard/AdviserCard';
|
export { default as AdviserCard } from "./AdviserCard/AdviserCard";
|
||||||
export { default as CompatibilityCard } from './CompatibilityCard/CompatibilityCard';
|
export { default as CompatibilityCard } from "./CompatibilityCard/CompatibilityCard";
|
||||||
export { default as MeditationCard } from './MeditationCard/MeditationCard';
|
export { default as MeditationCard } from "./MeditationCard/MeditationCard";
|
||||||
export { default as PalmCard } from './PalmCard/PalmCard';
|
export { default as PalmCard } from "./PalmCard/PalmCard";
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
export * from './cards';
|
export * from "./cards";
|
||||||
export * from './sections';
|
export * from "./sections";
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
.sectionContent.sectionContent {
|
.sectionContent.sectionContent {
|
||||||
overflow-x: scroll;
|
overflow-x: scroll;
|
||||||
width: calc(100% + 32px);
|
width: calc(100% + 32px);
|
||||||
padding: 32px 16px;
|
padding: 32px 16px;
|
||||||
margin: -32px -16px;
|
margin: -32px -16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.skeleton.skeleton {
|
.skeleton.skeleton {
|
||||||
height: 486px;
|
height: 486px;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,25 +7,29 @@ import { AdviserCard } from "../../cards";
|
|||||||
|
|
||||||
import styles from "./AdvisersSection.module.scss";
|
import styles from "./AdvisersSection.module.scss";
|
||||||
|
|
||||||
export default function AdvisersSection({ promise }: { promise: Promise<Assistant[]> }) {
|
export default function AdvisersSection({
|
||||||
const assistants = use(promise);
|
promise,
|
||||||
const columns = Math.ceil(assistants?.length / 2);
|
}: {
|
||||||
|
promise: Promise<Assistant[]>;
|
||||||
|
}) {
|
||||||
|
const assistants = use(promise);
|
||||||
|
const columns = Math.ceil(assistants?.length / 2);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Section title="Advisers" contentClassName={styles.sectionContent}>
|
<Section title="Advisers" contentClassName={styles.sectionContent}>
|
||||||
<Grid columns={columns} className={styles.grid}>
|
<Grid columns={columns} className={styles.grid}>
|
||||||
{assistants.map((adviser) => (
|
{assistants.map(adviser => (
|
||||||
<AdviserCard key={adviser._id} {...adviser} />
|
<AdviserCard key={adviser._id} {...adviser} />
|
||||||
))}
|
))}
|
||||||
</Grid>
|
</Grid>
|
||||||
</Section>
|
</Section>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AdvisersSectionSkeleton() {
|
export function AdvisersSectionSkeleton() {
|
||||||
return (
|
return (
|
||||||
<Section title="Advisers" contentClassName={styles.sectionContent}>
|
<Section title="Advisers" contentClassName={styles.sectionContent}>
|
||||||
<Skeleton className={styles.skeleton} />
|
<Skeleton className={styles.skeleton} />
|
||||||
</Section>
|
</Section>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
.sectionContent.sectionContent {
|
.sectionContent.sectionContent {
|
||||||
overflow-x: scroll;
|
overflow-x: scroll;
|
||||||
width: calc(100% + 32px);
|
width: calc(100% + 32px);
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
margin: -16px;
|
margin: -16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.skeleton.skeleton {
|
.skeleton.skeleton {
|
||||||
height: 236px;
|
height: 236px;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,31 +1,42 @@
|
|||||||
import { use } from "react";
|
import { use } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
import { Grid, Section, Skeleton } from "@/components/ui";
|
import { Grid, Section, Skeleton } from "@/components/ui";
|
||||||
import { CompatibilityAction } from "@/entities/dashboard/types";
|
import { CompatibilityAction } from "@/entities/dashboard/types";
|
||||||
|
import { ROUTES } from "@/shared/constants/client-routes";
|
||||||
|
|
||||||
import { CompatibilityCard } from "../../cards";
|
import { CompatibilityCard } from "../../cards";
|
||||||
|
|
||||||
import styles from "./CompatibilitySection.module.scss";
|
import styles from "./CompatibilitySection.module.scss";
|
||||||
|
|
||||||
export default function CompatibilitySection({ promise }: { promise: Promise<CompatibilityAction[]> }) {
|
export default function CompatibilitySection({
|
||||||
const compatibilities = use(promise);
|
promise,
|
||||||
const columns = Math.ceil(compatibilities?.length / 2);
|
}: {
|
||||||
|
promise: Promise<CompatibilityAction[]>;
|
||||||
|
}) {
|
||||||
|
const compatibilities = use(promise);
|
||||||
|
const columns = Math.ceil(compatibilities?.length / 2);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Section title="Compatibility" contentClassName={styles.sectionContent}>
|
<Section title="Compatibility" contentClassName={styles.sectionContent}>
|
||||||
<Grid columns={columns} className={styles.grid}>
|
<Grid columns={columns} className={styles.grid}>
|
||||||
{compatibilities.map((compatibility) => (
|
{compatibilities.map(compatibility => (
|
||||||
<CompatibilityCard key={compatibility._id} {...compatibility} />
|
<Link
|
||||||
))}
|
href={ROUTES.compatibility(compatibility._id)}
|
||||||
</Grid>
|
key={compatibility._id}
|
||||||
</Section>
|
>
|
||||||
)
|
<CompatibilityCard key={compatibility._id} {...compatibility} />
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CompatibilitySectionSkeleton() {
|
export function CompatibilitySectionSkeleton() {
|
||||||
return (
|
return (
|
||||||
<Section title="Compatibility" contentClassName={styles.sectionContent}>
|
<Section title="Compatibility" contentClassName={styles.sectionContent}>
|
||||||
<Skeleton className={styles.skeleton} />
|
<Skeleton className={styles.skeleton} />
|
||||||
</Section>
|
</Section>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
.sectionContent.sectionContent {
|
.sectionContent.sectionContent {
|
||||||
overflow-x: scroll;
|
overflow-x: scroll;
|
||||||
width: calc(100% + 32px);
|
width: calc(100% + 32px);
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
margin: -16px;
|
margin: -16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.skeleton.skeleton {
|
.skeleton.skeleton {
|
||||||
height: 308px;
|
height: 308px;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,25 +7,29 @@ import { MeditationCard } from "../../cards";
|
|||||||
|
|
||||||
import styles from "./MeditationSection.module.scss";
|
import styles from "./MeditationSection.module.scss";
|
||||||
|
|
||||||
export default function MeditationSection({ promise }: { promise: Promise<Meditation[]> }) {
|
export default function MeditationSection({
|
||||||
const meditations = use(promise);
|
promise,
|
||||||
const columns = meditations?.length;
|
}: {
|
||||||
|
promise: Promise<Meditation[]>;
|
||||||
|
}) {
|
||||||
|
const meditations = use(promise);
|
||||||
|
const columns = meditations?.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Section title="Meditations" contentClassName={styles.sectionContent}>
|
<Section title="Meditations" contentClassName={styles.sectionContent}>
|
||||||
<Grid columns={columns} className={styles.grid}>
|
<Grid columns={columns} className={styles.grid}>
|
||||||
{meditations.map((meditation) => (
|
{meditations.map(meditation => (
|
||||||
<MeditationCard key={meditation._id} {...meditation} />
|
<MeditationCard key={meditation._id} {...meditation} />
|
||||||
))}
|
))}
|
||||||
</Grid>
|
</Grid>
|
||||||
</Section>
|
</Section>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MeditationSectionSkeleton() {
|
export function MeditationSectionSkeleton() {
|
||||||
return (
|
return (
|
||||||
<Section title="Meditations" contentClassName={styles.sectionContent}>
|
<Section title="Meditations" contentClassName={styles.sectionContent}>
|
||||||
<Skeleton className={styles.skeleton} />
|
<Skeleton className={styles.skeleton} />
|
||||||
</Section>
|
</Section>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
.sectionContent.sectionContent {
|
.sectionContent.sectionContent {
|
||||||
overflow-x: scroll;
|
overflow-x: scroll;
|
||||||
width: calc(100% + 32px);
|
width: calc(100% + 32px);
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
margin: -16px;
|
margin: -16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.skeleton.skeleton {
|
.skeleton.skeleton {
|
||||||
height: 227px;
|
height: 227px;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,31 +1,39 @@
|
|||||||
import { use } from "react";
|
import { use } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
import { Grid, Section, Skeleton } from "@/components/ui";
|
import { Grid, Section, Skeleton } from "@/components/ui";
|
||||||
import { PalmAction } from "@/entities/dashboard/types";
|
import { PalmAction } from "@/entities/dashboard/types";
|
||||||
|
import { ROUTES } from "@/shared/constants/client-routes";
|
||||||
|
|
||||||
import { PalmCard } from "../../cards";
|
import { PalmCard } from "../../cards";
|
||||||
|
|
||||||
import styles from "./PalmSection.module.scss";
|
import styles from "./PalmSection.module.scss";
|
||||||
|
|
||||||
export default function PalmSection({ promise }: { promise: Promise<PalmAction[]> }) {
|
export default function PalmSection({
|
||||||
const palms = use(promise);
|
promise,
|
||||||
const columns = palms?.length;
|
}: {
|
||||||
|
promise: Promise<PalmAction[]>;
|
||||||
|
}) {
|
||||||
|
const palms = use(promise);
|
||||||
|
const columns = palms?.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Section title="Palm" contentClassName={styles.sectionContent}>
|
<Section title="Palm" contentClassName={styles.sectionContent}>
|
||||||
<Grid columns={columns} className={styles.grid}>
|
<Grid columns={columns} className={styles.grid}>
|
||||||
{palms.map((palm) => (
|
{palms.map(palm => (
|
||||||
<PalmCard key={palm._id} {...palm} />
|
<Link href={ROUTES.palmistryResult(palm._id)} key={palm._id}>
|
||||||
))}
|
<PalmCard key={palm._id} {...palm} />
|
||||||
</Grid>
|
</Link>
|
||||||
</Section>
|
))}
|
||||||
)
|
</Grid>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PalmSectionSkeleton() {
|
export function PalmSectionSkeleton() {
|
||||||
return (
|
return (
|
||||||
<Section title="Palm" contentClassName={styles.sectionContent}>
|
<Section title="Palm" contentClassName={styles.sectionContent}>
|
||||||
<Skeleton className={styles.skeleton} />
|
<Skeleton className={styles.skeleton} />
|
||||||
</Section>
|
</Section>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,16 @@
|
|||||||
export { default as AdvisersSection, AdvisersSectionSkeleton } from './AdvisersSection/AdvisersSection';
|
export {
|
||||||
export { default as CompatibilitySection, CompatibilitySectionSkeleton } from './CompatibilitySection/CompatibilitySection';
|
default as AdvisersSection,
|
||||||
export { default as MeditationSection, MeditationSectionSkeleton } from './MeditationSection/MeditationSection';
|
AdvisersSectionSkeleton,
|
||||||
export { default as PalmSection, PalmSectionSkeleton } from './PalmSection/PalmSection';
|
} from "./AdvisersSection/AdvisersSection";
|
||||||
|
export {
|
||||||
|
default as CompatibilitySection,
|
||||||
|
CompatibilitySectionSkeleton,
|
||||||
|
} from "./CompatibilitySection/CompatibilitySection";
|
||||||
|
export {
|
||||||
|
default as MeditationSection,
|
||||||
|
MeditationSectionSkeleton,
|
||||||
|
} from "./MeditationSection/MeditationSection";
|
||||||
|
export {
|
||||||
|
default as PalmSection,
|
||||||
|
PalmSectionSkeleton,
|
||||||
|
} from "./PalmSection/PalmSection";
|
||||||
|
|||||||
@ -0,0 +1,15 @@
|
|||||||
|
.loadingContainer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: calc(100dvh - 56px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
line-height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
line-height: 25px;
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
import { Spinner, Toast, Typography } from "@/components/ui";
|
||||||
|
import { useGenerationPolling } from "@/hooks/generation/useGenerationPolling";
|
||||||
|
|
||||||
|
import styles from "./PalmistryResultPage.module.scss";
|
||||||
|
|
||||||
|
interface PalmistryResultPageProps {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PalmistryResultPage({ id }: PalmistryResultPageProps) {
|
||||||
|
const t = useTranslations("PalmistryResult");
|
||||||
|
const { data, error, isLoading } = useGenerationPolling(id);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className={styles.loadingContainer}>
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <Toast variant="error">{t("error")}</Toast>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Typography as="h1" size="xl" weight="semiBold" className={styles.title}>
|
||||||
|
{t("title")}
|
||||||
|
</Typography>
|
||||||
|
<Typography as="p" size="lg" align="left" className={styles.description}>
|
||||||
|
{data?.result}
|
||||||
|
</Typography>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,35 +1,35 @@
|
|||||||
.container {
|
.container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
min-height: 60px;
|
min-height: 60px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.credits {
|
.credits {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
background-color: #275ca7;
|
background-color: #275ca7;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
.creditsDescription {
|
.creditsDescription {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.anyQuestions {
|
.anyQuestions {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
&>a {
|
& > a {
|
||||||
color: #275ca7;
|
color: #275ca7;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.subscriptionUpdate {
|
.subscriptionUpdate {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
line-height: 1.25;
|
line-height: 1.25;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,64 +1,82 @@
|
|||||||
"use client;"
|
"use client";
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
import { Button, Typography } from "@/components/ui";
|
import { Button, Typography } from "@/components/ui";
|
||||||
|
import { ROUTES } from "@/shared/constants/client-routes";
|
||||||
|
|
||||||
import styles from "./Billing.module.scss"
|
import styles from "./Billing.module.scss";
|
||||||
|
|
||||||
interface IBillingProps {
|
function Billing() {
|
||||||
onBilling: () => void;
|
const t = useTranslations("Profile.billing");
|
||||||
}
|
const router = useRouter();
|
||||||
|
|
||||||
function Billing({ onBilling }: IBillingProps) {
|
const onBilling = () => {
|
||||||
const t = useTranslations('Profile.billing');
|
router.push(ROUTES.profileSubscriptions());
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<Button
|
<Button className={styles.button} onClick={onBilling}>
|
||||||
className={styles.button}
|
<Typography size="xl" color="white">
|
||||||
onClick={onBilling}
|
{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"
|
||||||
>
|
>
|
||||||
<Typography size="xl" color="white">
|
{chunks}
|
||||||
{t("billing_button")}
|
</Link>
|
||||||
</Typography>
|
),
|
||||||
</Button>
|
linkText: t("any_questions_link"),
|
||||||
<div className={styles.credits}>
|
})}
|
||||||
<Typography as="p" weight="bold" color="white" align="left">
|
</Typography>
|
||||||
{t("credits.title", {
|
<Typography
|
||||||
credits: String(0)
|
as="p"
|
||||||
})}
|
align="left"
|
||||||
</Typography>
|
color="secondary"
|
||||||
<Typography className={styles.creditsDescription} as="p" size="sm" color="white" align="left">
|
className={styles.subscriptionUpdate}
|
||||||
{t("credits.description")}
|
>
|
||||||
</Typography>
|
{t.rich("subscription_update", {
|
||||||
</div>
|
bold: chunks => (
|
||||||
<Typography as="p" weight="bold" align="left" className={styles.anyQuestions}>
|
<Typography weight="bold" color="secondary">
|
||||||
{t.rich("any_questions", {
|
{chunks}
|
||||||
link: (chunks) => (
|
|
||||||
<Link
|
|
||||||
href="https://witapps.us/en#contact-us"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
{chunks}
|
|
||||||
</Link>
|
|
||||||
),
|
|
||||||
linkText: t("any_questions_link")
|
|
||||||
})}
|
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography as="p" align="left" color="secondary" className={styles.subscriptionUpdate}>
|
),
|
||||||
{t.rich("subscription_update", {
|
subscriptionUpdateBold: t("subscription_update_bold"),
|
||||||
bold: (chunks) => (
|
br: () => <br />,
|
||||||
<Typography weight="bold" color="secondary">{chunks}</Typography>
|
})}
|
||||||
),
|
</Typography>
|
||||||
subscriptionUpdateBold: t("subscription_update_bold"),
|
</div>
|
||||||
br: () => <br />
|
);
|
||||||
})}
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Billing
|
export default Billing;
|
||||||
|
|||||||
@ -1,3 +1,42 @@
|
|||||||
.button {
|
.button {
|
||||||
min-height: 60px;
|
min-height: 60px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-container {
|
||||||
|
max-width: 290px;
|
||||||
|
padding: 24px 0px 0px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-title {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-inline: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-description {
|
||||||
|
padding-inline: 24px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-answers {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin-top: 24px;
|
||||||
|
border-top: 1px solid #d9d9d9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-answer {
|
||||||
|
width: 50%;
|
||||||
|
height: 52px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #275da7;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
border-right: 1px solid #d9d9d9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,24 +1,70 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
import { Button, Typography } from "@/components/ui";
|
import { Button, Modal, Typography } from "@/components/ui";
|
||||||
|
import { ROUTES } from "@/shared/constants/client-routes";
|
||||||
|
|
||||||
import styles from "./LogOut.module.scss"
|
import styles from "./LogOut.module.scss";
|
||||||
|
|
||||||
interface ILogOutProps {
|
function LogOut() {
|
||||||
onLogout: () => void;
|
const t = useTranslations("Profile.log_out");
|
||||||
}
|
const router = useRouter();
|
||||||
|
|
||||||
function LogOut({ onLogout }: ILogOutProps) {
|
const [logoutModal, setLogoutModal] = useState(false);
|
||||||
const t = useTranslations('Profile.log_out');
|
|
||||||
|
|
||||||
return (
|
const handleLogout = () => {
|
||||||
<Button
|
router.replace(ROUTES.home());
|
||||||
className={styles.button}
|
// logout();
|
||||||
onClick={onLogout}
|
};
|
||||||
|
|
||||||
|
const onLogoutButton = () => {
|
||||||
|
setLogoutModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button className={styles.button} onClick={onLogoutButton}>
|
||||||
|
<Typography size="xl" color="white">
|
||||||
|
{t("log_out_button")}
|
||||||
|
</Typography>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{logoutModal && (
|
||||||
|
<Modal
|
||||||
|
isCloseButtonVisible={false}
|
||||||
|
open={!!logoutModal}
|
||||||
|
onClose={() => setLogoutModal(false)}
|
||||||
|
className={styles.modal}
|
||||||
|
modalClassName={styles["modal-container"]}
|
||||||
>
|
>
|
||||||
<Typography size="xl" color="white">{t("log_out_button")}</Typography>
|
<Typography as="h4" className={styles["modal-title"]}>
|
||||||
</Button>
|
{t("modal.title")}
|
||||||
)
|
</Typography>
|
||||||
|
<p className={styles["modal-description"]}>
|
||||||
|
{t("modal.description")}
|
||||||
|
</p>
|
||||||
|
<div className={styles["modal-answers"]}>
|
||||||
|
<div className={styles["modal-answer"]} onClick={handleLogout}>
|
||||||
|
<p className={styles["modal-answer-text"]}>
|
||||||
|
{t("modal.log_out_button")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={styles["modal-answer"]}
|
||||||
|
onClick={() => setLogoutModal(false)}
|
||||||
|
>
|
||||||
|
<p className={styles["modal-answer-text"]}>
|
||||||
|
{t("modal.stay_button")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default LogOut
|
export default LogOut;
|
||||||
|
|||||||
@ -1,23 +1,23 @@
|
|||||||
.container {
|
.container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
|
|
||||||
&>.title {
|
& > .title {
|
||||||
line-height: 32px;
|
line-height: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&>.description {
|
& > .description {
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,31 +1,35 @@
|
|||||||
import { Typography } from "@/components/ui"
|
import { Typography } from "@/components/ui";
|
||||||
|
|
||||||
import styles from "./ProfileBlock.module.scss"
|
import styles from "./ProfileBlock.module.scss";
|
||||||
|
|
||||||
interface ProfileBlockProps {
|
interface ProfileBlockProps {
|
||||||
title: string
|
title: string;
|
||||||
description?: string
|
description?: string;
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ProfileBlock({ title, description, children }: ProfileBlockProps) {
|
function ProfileBlock({ title, description, children }: ProfileBlockProps) {
|
||||||
return (
|
return (
|
||||||
<section className={styles.container}>
|
<section className={styles.container}>
|
||||||
<header className={styles.header}>
|
<header className={styles.header}>
|
||||||
<Typography className={styles.title} as="h2" size="xl" weight="semiBold" align="left">
|
<Typography
|
||||||
{title}
|
className={styles.title}
|
||||||
</Typography>
|
as="h2"
|
||||||
{description &&
|
size="xl"
|
||||||
<Typography className={styles.description} size="sm" align="left">
|
weight="semiBold"
|
||||||
{description}
|
align="left"
|
||||||
</Typography>
|
>
|
||||||
}
|
{title}
|
||||||
</header>
|
</Typography>
|
||||||
{!!children && <div className={styles.content}>
|
{description && (
|
||||||
{children}
|
<Typography className={styles.description} size="sm" align="left">
|
||||||
</div>}
|
{description}
|
||||||
</section>
|
</Typography>
|
||||||
)
|
)}
|
||||||
|
</header>
|
||||||
|
{!!children && <div className={styles.content}>{children}</div>}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ProfileBlock
|
export default ProfileBlock;
|
||||||
|
|||||||
@ -1,16 +1,16 @@
|
|||||||
.container {
|
.container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input {
|
.input {
|
||||||
background-color: #f3f3f3;
|
background-color: #f3f3f3;
|
||||||
min-height: 60px;
|
min-height: 60px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputContainer {
|
.inputContainer {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,41 +1,54 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useTranslations } from "next-intl"
|
import { use } from "react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
import { EmailInput, NameInput } from "@/components/ui";
|
import { EmailInput, NameInput, Skeleton } from "@/components/ui";
|
||||||
|
import { IUser } from "@/entities/user/types";
|
||||||
|
|
||||||
import styles from "./ProfileInformation.module.scss"
|
import styles from "./ProfileInformation.module.scss";
|
||||||
|
|
||||||
function ProfileInformation() {
|
interface IProfileInformationProps {
|
||||||
const t = useTranslations('Profile');
|
user: Promise<IUser>;
|
||||||
// const email = useSelector(selectors.selectEmail) || ""
|
|
||||||
// const name = useSelector(selectors.selectUser)?.username || ""
|
|
||||||
const email = "Test email"
|
|
||||||
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
|
export default function ProfileInformation({ user }: IProfileInformationProps) {
|
||||||
|
const userData = use(user);
|
||||||
|
|
||||||
|
const t = useTranslations("Profile");
|
||||||
|
const email = userData?.email || "";
|
||||||
|
const name = userData?.profile?.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 function ProfileInformationSkeleton() {
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<Skeleton style={{ height: "136px" }} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -1,4 +1,7 @@
|
|||||||
export { default as Billing } from "./Billing/Billing"
|
export { default as Billing } from "./Billing/Billing";
|
||||||
export { default as LogOut } from "./LogOut/LogOut"
|
export { default as LogOut } from "./LogOut/LogOut";
|
||||||
export { default as ProfileBlock } from "./ProfileBlock/ProfileBlock"
|
export { default as ProfileBlock } from "./ProfileBlock/ProfileBlock";
|
||||||
export { default as ProfileInformation } from "./ProfileInformation/ProfileInformation"
|
export {
|
||||||
|
default as ProfileInformation,
|
||||||
|
ProfileInformationSkeleton,
|
||||||
|
} from "./ProfileInformation/ProfileInformation";
|
||||||
|
|||||||
@ -0,0 +1,41 @@
|
|||||||
|
.modal {
|
||||||
|
max-width: 290px;
|
||||||
|
padding: 24px 0px 0px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-inline: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
padding-inline: 24px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin-top: 24px;
|
||||||
|
border-top: 1px solid #d9d9d9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action.action {
|
||||||
|
width: 50%;
|
||||||
|
height: 52px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #275da7;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: center;
|
||||||
|
background-color: transparent;
|
||||||
|
border-radius: 0px;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
border-right: 1px solid #d9d9d9;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { createContext, ReactNode,useContext, useState } from "react";
|
import {
|
||||||
|
createContext,
|
||||||
|
ReactNode,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
@ -8,6 +14,7 @@ import { Button, Typography } from "@/components/ui";
|
|||||||
import Modal from "@/components/ui/Modal/Modal";
|
import Modal from "@/components/ui/Modal/Modal";
|
||||||
import { UserSubscription } from "@/entities/subscriptions/types";
|
import { UserSubscription } from "@/entities/subscriptions/types";
|
||||||
import { ROUTES } from "@/shared/constants/client-routes";
|
import { ROUTES } from "@/shared/constants/client-routes";
|
||||||
|
import { useRetainingActions } from "@/stores/retainingStore";
|
||||||
|
|
||||||
import styles from "./CancelSubscriptionModalProvider.module.scss";
|
import styles from "./CancelSubscriptionModalProvider.module.scss";
|
||||||
|
|
||||||
@ -15,61 +22,67 @@ type Ctx = { open: (sub: UserSubscription) => void };
|
|||||||
|
|
||||||
const Context = createContext<Ctx | null>(null);
|
const Context = createContext<Ctx | null>(null);
|
||||||
export const useCancelSubscriptionModal = () => {
|
export const useCancelSubscriptionModal = () => {
|
||||||
const ctx = useContext(Context);
|
const ctx = useContext(Context);
|
||||||
if (!ctx) throw new Error("useCancelSubscriptionModal must be inside provider");
|
if (!ctx)
|
||||||
return ctx;
|
throw new Error("useCancelSubscriptionModal must be inside provider");
|
||||||
|
return ctx;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function CancelSubscriptionModalProvider({
|
export default function CancelSubscriptionModalProvider({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
const t = useTranslations("Subscriptions");
|
const t = useTranslations("Subscriptions");
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const { setCancellingSubscription } = useRetainingActions();
|
||||||
|
|
||||||
const close = () => setIsOpen(false);
|
const close = useCallback(() => setIsOpen(false), []);
|
||||||
const open = (
|
const open = useCallback(
|
||||||
// _sub: UserSubscription
|
(subscription: UserSubscription) => {
|
||||||
) => {
|
setCancellingSubscription(subscription);
|
||||||
setIsOpen(true)
|
setIsOpen(true);
|
||||||
};
|
},
|
||||||
|
[setCancellingSubscription]
|
||||||
|
);
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = useCallback(() => {
|
||||||
router.push(ROUTES.retainingFunnelCancelSubscription())
|
router.push(ROUTES.retainingFunnelCancelSubscription());
|
||||||
close();
|
close();
|
||||||
};
|
}, [router, close]);
|
||||||
|
|
||||||
return (
|
const handleStay = useCallback(() => {
|
||||||
<Context.Provider value={{ open }}>
|
close();
|
||||||
{children}
|
}, [close]);
|
||||||
|
|
||||||
<Modal
|
return (
|
||||||
open={!!isOpen}
|
<Context.Provider value={{ open }}>
|
||||||
onClose={close}
|
{children}
|
||||||
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}>
|
<Modal
|
||||||
<Button onClick={handleCancel}>{t("modal.cancel_button")}</Button>
|
open={!!isOpen}
|
||||||
<Button
|
onClose={close}
|
||||||
variant="secondary"
|
isCloseButtonVisible={false}
|
||||||
onClick={close}
|
className={styles.overlay}
|
||||||
className={styles.stayButton}
|
modalClassName={styles.modal}
|
||||||
>
|
>
|
||||||
{t("modal.stay_button")}
|
<Typography as="h4" className={styles.title}>
|
||||||
</Button>
|
{t("modal.title")}
|
||||||
</div>
|
</Typography>
|
||||||
</Modal>
|
<Typography as="p" className={styles.description}>
|
||||||
</Context.Provider>
|
{t("modal.description")}
|
||||||
);
|
</Typography>
|
||||||
}
|
|
||||||
|
<div className={styles.actions}>
|
||||||
|
<Button className={styles.action} onClick={handleCancel}>
|
||||||
|
{t("modal.cancel_button")}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleStay} className={styles.action}>
|
||||||
|
{t("modal.stay_button")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</Context.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -1,33 +1,33 @@
|
|||||||
.container {
|
.container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
background-color: #f0f0f4;
|
background-color: #f0f0f4;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 16px 8px;
|
padding: 16px 8px;
|
||||||
border-bottom: 1px solid #e5e7eb;
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.cell {
|
.cell {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: #7d8785;
|
color: #7d8785;
|
||||||
|
|
||||||
&:nth-child(2) {
|
&:nth-child(2) {
|
||||||
color: #090909;
|
color: #090909;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { ReactNode } from "react";
|
import { ReactNode, useMemo } from "react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
import { Button } from "@/components/ui";
|
import { Button, Typography } from "@/components/ui";
|
||||||
import { Table } from "@/components/widgets";
|
import { Table } from "@/components/widgets";
|
||||||
import { UserSubscription } from "@/entities/subscriptions/types";
|
import { UserSubscription } from "@/entities/subscriptions/types";
|
||||||
import { formatDate } from "@/shared/utils/date";
|
import { formatDate } from "@/shared/utils/date";
|
||||||
@ -12,36 +12,68 @@ import { Currency } from "@/types";
|
|||||||
|
|
||||||
import { useCancelSubscriptionModal } from "../CancelSubscriptionModalProvider/CancelSubscriptionModalProvider";
|
import { useCancelSubscriptionModal } from "../CancelSubscriptionModalProvider/CancelSubscriptionModalProvider";
|
||||||
|
|
||||||
import styles from "./SubscriptionTable.module.scss"
|
import styles from "./SubscriptionTable.module.scss";
|
||||||
|
|
||||||
interface ITableProps {
|
interface ITableProps {
|
||||||
subscription: UserSubscription;
|
subscription: UserSubscription;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SubscriptionTable({ subscription }: ITableProps) {
|
export default function SubscriptionTable({ subscription }: ITableProps) {
|
||||||
const t = useTranslations("Subscriptions");
|
const t = useTranslations("Subscriptions");
|
||||||
const { open } = useCancelSubscriptionModal();
|
const { open } = useCancelSubscriptionModal();
|
||||||
|
|
||||||
const tableData: ReactNode[][] = [
|
const tableData: ReactNode[][] = useMemo(() => {
|
||||||
[t("table.subscription_type"), t(`table.subscription_type_value.${subscription.subscriptionType}`)],
|
const data: ReactNode[][] = [
|
||||||
[t("table.subscription_status"), t(`table.subscription_status_value.${subscription.subscriptionStatus}`, {
|
[
|
||||||
date: formatDate(subscription.cancellationDate) || ""
|
t("table.subscription_type"),
|
||||||
})],
|
t(`table.subscription_type_value.${subscription.subscriptionType}`),
|
||||||
[t("table.billing_period"), t(`table.billing_period_value.${subscription.billingPeriod}`)],
|
],
|
||||||
[t("table.last_payment_on"), formatDate(subscription.lastPaymentOn)],
|
[
|
||||||
[t("table.renewal_date"), formatDate(subscription.renewalDate)],
|
t("table.subscription_status"),
|
||||||
[t("table.renewal_amount"), getFormattedPrice(subscription.renewalAmount, Currency[subscription.currency])],
|
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") {
|
if (subscription.subscriptionStatus === "ACTIVE") {
|
||||||
tableData.push([
|
data.push([
|
||||||
<Button key={"cancel-subscription"} className={styles.buttonCancel} onClick={() => open(subscription)}>
|
<Button
|
||||||
{t("table.cancel_subscription")}
|
key={"cancel-subscription"}
|
||||||
</Button>
|
className={styles.buttonCancel}
|
||||||
])
|
onClick={() => open(subscription)}
|
||||||
|
>
|
||||||
|
<Typography color="white">
|
||||||
|
{t("table.cancel_subscription")}
|
||||||
|
</Typography>
|
||||||
|
</Button>,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return data;
|
||||||
<Table data={tableData} />
|
}, [subscription, t, open]);
|
||||||
)
|
|
||||||
}
|
// const tableData: ReactNode[][] = [
|
||||||
|
// [t("table.subscription_status"), t(`table.subscription_status_value.${subscription.subscriptionStatus}`, {
|
||||||
|
// date: formatDate(subscription.cancellationDate) || ""
|
||||||
|
// })],
|
||||||
|
// ]
|
||||||
|
|
||||||
|
return <Table data={tableData} />;
|
||||||
|
}
|
||||||
|
|||||||
@ -1,37 +1,50 @@
|
|||||||
import { use } from "react";
|
import { use } from "react";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
|
|
||||||
import { Typography } from "@/components/ui";
|
import { Skeleton, Typography } from "@/components/ui";
|
||||||
import { Skeleton } from "@/components/ui";
|
|
||||||
import { UserSubscription } from "@/entities/subscriptions/types";
|
import { UserSubscription } from "@/entities/subscriptions/types";
|
||||||
|
|
||||||
import SubscriptionTable from "../SubscriptionTable/SubscriptionTable";
|
import SubscriptionTable from "../SubscriptionTable/SubscriptionTable";
|
||||||
|
|
||||||
import styles from "./SubscriptionsList.module.scss"
|
import styles from "./SubscriptionsList.module.scss";
|
||||||
|
|
||||||
export default function SubscriptionsList(
|
export default function SubscriptionsList({
|
||||||
{ promise }: { promise: Promise<UserSubscription[]> }
|
promise,
|
||||||
) {
|
}: {
|
||||||
const t = use(getTranslations("Subscriptions"));
|
promise: Promise<UserSubscription[]>;
|
||||||
|
}) {
|
||||||
|
const t = use(getTranslations("Subscriptions"));
|
||||||
|
|
||||||
const subscriptions = use(promise);
|
const subscriptions = use(promise);
|
||||||
|
|
||||||
if (subscriptions?.length === 0) {
|
if (subscriptions?.length === 0) {
|
||||||
return <div className={styles.container}>
|
return (
|
||||||
<Typography as="h1" className={styles.title}>{t("title")}</Typography>
|
<div className={styles.container}>
|
||||||
<Typography as="p" className={styles.description}>{t("no_subscriptions")}</Typography>
|
<Typography as="h1" className={styles.title}>
|
||||||
</div>
|
{t("title")}
|
||||||
}
|
</Typography>
|
||||||
|
<Typography as="p" className={styles.description}>
|
||||||
|
{t("no_subscriptions")}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return <>
|
return (
|
||||||
{subscriptions.map((subscription) => {
|
<>
|
||||||
return <SubscriptionTable subscription={subscription} key={subscription.id} />
|
{subscriptions.map(subscription => {
|
||||||
})}
|
return (
|
||||||
|
<SubscriptionTable
|
||||||
|
subscription={subscription}
|
||||||
|
key={subscription.id}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{/* <SubscriptionTable subscription={subscriptions[0]} key={subscriptions[0].id} /> */}
|
||||||
</>
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SubscriptionsListSkeleton() {
|
export function SubscriptionsListSkeleton() {
|
||||||
return (
|
return <Skeleton style={{ height: "300px" }} className={styles.skeleton} />;
|
||||||
<Skeleton style={{ height: "300px" }} className={styles.skeleton} />
|
}
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
export { default as CancelSubscriptionModalProvider } from "./CancelSubscriptionModalProvider/CancelSubscriptionModalProvider";
|
||||||
export { default as CancelSubscriptionModalProvider } from "./CancelSubscriptionModalProvider/CancelSubscriptionModalProvider"
|
export {
|
||||||
export { default as SubscriptionsList, SubscriptionsListSkeleton } from "./SubscriptionsList/SubscriptionsList"
|
default as SubscriptionsList,
|
||||||
|
SubscriptionsListSkeleton,
|
||||||
|
} from "./SubscriptionsList/SubscriptionsList";
|
||||||
|
|||||||
@ -1,22 +1,25 @@
|
|||||||
.button {
|
.button {
|
||||||
min-height: 71px;
|
min-height: 71px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
background: #F1F1F1;
|
background: #f1f1f1;
|
||||||
background-blend-mode: color;
|
background-blend-mode: color;
|
||||||
box-shadow: 2px 5px 2.5px #00000025;
|
box-shadow: 2px 5px 2.5px #00000025;
|
||||||
color: #121620;
|
color: #121620;
|
||||||
transition: background 0.3s ease, color 0.3s ease;
|
transition:
|
||||||
will-change: background, color;
|
background 0.3s ease,
|
||||||
padding: 25px;
|
color 0.3s ease;
|
||||||
line-height: 1;
|
will-change: background, color;
|
||||||
word-break: break-word;
|
padding: 25px;
|
||||||
min-width: none;
|
line-height: 1;
|
||||||
|
word-break: break-word;
|
||||||
|
min-width: none;
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
background: linear-gradient(to right, #057dd4 23%, #224e90 74%, #0c6bc3 94%),
|
background:
|
||||||
linear-gradient(-45deg, #3a617120 9%, #21212120 72%, #21895120 96%);
|
linear-gradient(to right, #057dd4 23%, #224e90 74%, #0c6bc3 94%),
|
||||||
color: #fff;
|
linear-gradient(-45deg, #3a617120 9%, #21212120 72%, #21895120 96%);
|
||||||
}
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,18 +1,23 @@
|
|||||||
import MainButton, { ButtonProps as MainButtonProps } from "@/components/ui/Button/Button";
|
import MainButton, {
|
||||||
|
ButtonProps as MainButtonProps,
|
||||||
|
} from "@/components/ui/Button/Button";
|
||||||
|
|
||||||
import styles from "./Button.module.scss";
|
import styles from "./Button.module.scss";
|
||||||
|
|
||||||
interface ButtonProps extends MainButtonProps {
|
interface ButtonProps extends MainButtonProps {
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Button(props: ButtonProps) {
|
function Button(props: ButtonProps) {
|
||||||
const { active, ...buttonProps } = props;
|
const { active, ...buttonProps } = props;
|
||||||
return (
|
return (
|
||||||
<MainButton {...buttonProps} className={`${styles.button} ${props.className} ${active ? styles.active : ""}`}>
|
<MainButton
|
||||||
{props.children}
|
{...buttonProps}
|
||||||
</MainButton>
|
className={`${styles.button} ${props.className} ${active ? styles.active : ""}`}
|
||||||
);
|
>
|
||||||
|
{props.children}
|
||||||
|
</MainButton>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Button;
|
export default Button;
|
||||||
|
|||||||
@ -1,45 +1,75 @@
|
|||||||
|
|
||||||
|
|
||||||
interface CheckMarkProps {
|
interface CheckMarkProps {
|
||||||
active: boolean;
|
active: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function CheckMark({ active, className = "" }: CheckMarkProps) {
|
function CheckMark({ active, className = "" }: CheckMarkProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{active &&
|
{active && (
|
||||||
<svg className={className} width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg
|
||||||
<g clipPath="url(#clip0_49_4129)">
|
className={className}
|
||||||
<g clipPath="url(#clip1_49_4129)">
|
width="30"
|
||||||
<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" />
|
height="30"
|
||||||
</g>
|
viewBox="0 0 30 30"
|
||||||
</g>
|
fill="none"
|
||||||
<rect x="1" y="1" width="28" height="28" rx="14" stroke="#1172AC" strokeWidth="2" />
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<defs>
|
>
|
||||||
<clipPath id="clip0_49_4129">
|
<g clipPath="url(#clip0_49_4129)">
|
||||||
<rect width="30" height="30" rx="15" fill="white" />
|
<g clipPath="url(#clip1_49_4129)">
|
||||||
</clipPath>
|
<path
|
||||||
<clipPath id="clip1_49_4129">
|
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"
|
||||||
<rect width="30" height="30" rx="15" fill="white" />
|
fill="#1172AC"
|
||||||
</clipPath>
|
/>
|
||||||
</defs>
|
</g>
|
||||||
</svg>
|
</g>
|
||||||
}
|
<rect
|
||||||
{!active &&
|
x="1"
|
||||||
<svg className={className} width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
y="1"
|
||||||
<g clipPath="url(#clip0_49_4525)">
|
width="28"
|
||||||
</g>
|
height="28"
|
||||||
<rect x="1" y="1" width="28" height="28" rx="14" stroke="#1172AC" strokeWidth="2" />
|
rx="14"
|
||||||
<defs>
|
stroke="#1172AC"
|
||||||
<clipPath id="clip0_49_4525">
|
strokeWidth="2"
|
||||||
<rect width="30" height="30" rx="15" fill="white" />
|
/>
|
||||||
</clipPath>
|
<defs>
|
||||||
</defs>
|
<clipPath id="clip0_49_4129">
|
||||||
</svg>
|
<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)" />
|
||||||
|
<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
|
export default CheckMark;
|
||||||
|
|||||||
@ -1,62 +1,61 @@
|
|||||||
.container {
|
.container {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
padding: 72px 20px 65px;
|
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;
|
display: flex;
|
||||||
flex-direction: column;
|
align-items: flex-end;
|
||||||
align-items: center;
|
justify-content: center;
|
||||||
box-shadow: 0px 0px 10.2px 0px rgba(0, 0, 0, 0.25);
|
gap: 10px;
|
||||||
border: 4px solid transparent;
|
margin-top: 33px;
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&.active {
|
& > .oldPrice {
|
||||||
border: 4px solid rgba(17, 114, 172, 1)
|
color: #c4c4c4;
|
||||||
|
font-size: 28px;
|
||||||
|
line-height: 1;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: line-through;
|
||||||
}
|
}
|
||||||
|
|
||||||
&>.checkMark {
|
& > .newPrice {
|
||||||
position: absolute;
|
color: #000;
|
||||||
top: 21px;
|
font-size: 36px;
|
||||||
right: 14px;
|
line-height: 1;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
&>.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -11,53 +11,60 @@ import styles from "./Offer.module.scss";
|
|||||||
import { CheckMark } from "..";
|
import { CheckMark } from "..";
|
||||||
|
|
||||||
interface OfferProps {
|
interface OfferProps {
|
||||||
title?: string | React.ReactNode;
|
title?: string | React.ReactNode;
|
||||||
description?: string;
|
description?: string;
|
||||||
oldPrice?: number | string;
|
oldPrice?: number | string;
|
||||||
newPrice?: number | string;
|
newPrice?: number | string;
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
image?: React.ReactNode;
|
image?: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
classNameTitle?: string;
|
classNameTitle?: string;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Offer({
|
function Offer({
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
oldPrice,
|
oldPrice,
|
||||||
newPrice,
|
newPrice,
|
||||||
active = false,
|
active = false,
|
||||||
onClick,
|
onClick,
|
||||||
image,
|
image,
|
||||||
className = "",
|
className = "",
|
||||||
classNameTitle = ""
|
classNameTitle = "",
|
||||||
}: OfferProps) {
|
}: OfferProps) {
|
||||||
// const currency = useSelector(selectors.selectCurrency);
|
// const currency = useSelector(selectors.selectCurrency);
|
||||||
const currency = Currency.USD;
|
const currency = Currency.USD;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx(styles.container, active && styles.active, className)} onClick={onClick}>
|
<div
|
||||||
<CheckMark active={active} className={styles.checkMark} />
|
className={clsx(styles.container, active && styles.active, className)}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<CheckMark active={active} className={styles.checkMark} />
|
||||||
|
|
||||||
{image}
|
{image}
|
||||||
|
|
||||||
<Typography as="h3" weight="bold" className={clsx(styles.title, classNameTitle)}>
|
<Typography
|
||||||
{title}
|
as="h3"
|
||||||
</Typography>
|
weight="bold"
|
||||||
<Typography as="p" className={styles.description}>
|
className={clsx(styles.title, classNameTitle)}
|
||||||
{description}
|
>
|
||||||
</Typography>
|
{title}
|
||||||
<div className={styles.priceContainer}>
|
</Typography>
|
||||||
<Typography weight="bold" className={styles.oldPrice}>
|
<Typography as="p" className={styles.description}>
|
||||||
{getFormattedPrice(Number(oldPrice), currency)}
|
{description}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography weight="bold" className={styles.newPrice}>
|
<div className={styles.priceContainer}>
|
||||||
{getFormattedPrice(Number(newPrice), currency)}
|
<Typography weight="bold" className={styles.oldPrice}>
|
||||||
</Typography>
|
{getFormattedPrice(Number(oldPrice), currency)}
|
||||||
</div>
|
</Typography>
|
||||||
</div>
|
<Typography weight="bold" className={styles.newPrice}>
|
||||||
)
|
{getFormattedPrice(Number(newPrice), currency)}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Offer
|
export default Offer;
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
.stepper-bar {
|
.stepper-bar {
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,41 +4,40 @@ import { usePathname } from "next/navigation";
|
|||||||
|
|
||||||
import { StepperBar } from "@/components/layout";
|
import { StepperBar } from "@/components/layout";
|
||||||
|
|
||||||
import styles from "./RetainingStepper.module.scss"
|
import styles from "./RetainingStepper.module.scss";
|
||||||
|
|
||||||
|
|
||||||
export default function RetainingStepper({
|
export default function RetainingStepper({
|
||||||
stepperRoutes,
|
stepperRoutes,
|
||||||
}: {
|
}: {
|
||||||
stepperRoutes: string[];
|
stepperRoutes: string[];
|
||||||
}) {
|
}) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
const getCurrentStep = () => {
|
const getCurrentStep = () => {
|
||||||
// if ([
|
// if ([
|
||||||
// ROUTES.retainingFunnelPlanCancelled(),
|
// ROUTES.retainingFunnelPlanCancelled(),
|
||||||
// ROUTES.retainingFunnelSubscriptionStopped(),
|
// ROUTES.retainingFunnelSubscriptionStopped(),
|
||||||
// ].some(route => location.pathname.includes(route))) {
|
// ].some(route => location.pathname.includes(route))) {
|
||||||
// return stepperRoutes[retainingFunnel].length;
|
// return stepperRoutes[retainingFunnel].length;
|
||||||
// }
|
// }
|
||||||
let index = 0;
|
let index = 0;
|
||||||
for (const route of stepperRoutes) {
|
for (const route of stepperRoutes) {
|
||||||
if (pathname.includes(route)) {
|
if (pathname.includes(route)) {
|
||||||
return index + 1;
|
return index + 1;
|
||||||
}
|
}
|
||||||
index++;
|
index++;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// логика выбора шага по pathname
|
// логика выбора шага по pathname
|
||||||
return (
|
return (
|
||||||
<StepperBar
|
<StepperBar
|
||||||
length={stepperRoutes.length}
|
length={stepperRoutes.length}
|
||||||
currentStep={getCurrentStep()}
|
currentStep={getCurrentStep()}
|
||||||
// color={darkTheme ? "#B2BCFF" : "#353E75"}
|
// color={darkTheme ? "#B2BCFF" : "#353E75"}
|
||||||
color={"#353E75"}
|
color={"#353E75"}
|
||||||
className={styles["stepper-bar"]}
|
className={styles["stepper-bar"]}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user