main
compatibility & palm generations & add zustand & config prettier eslint
This commit is contained in:
parent
665b7bad90
commit
67f4dfdf3d
@ -1,7 +1,12 @@
|
||||
{
|
||||
"semi": true,
|
||||
"tabWidth": 2,
|
||||
"printWidth": 100,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
"semi": true,
|
||||
"trailingComma": "es5",
|
||||
"singleQuote": false,
|
||||
"printWidth": 80,
|
||||
"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:
|
||||
app:
|
||||
@ -10,4 +10,4 @@ services:
|
||||
- /app/node_modules
|
||||
environment:
|
||||
- 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 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 eslintPluginUnused from "eslint-plugin-unused-imports";
|
||||
import { dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
@ -20,12 +22,14 @@ const eslintConfig = [
|
||||
import: eslintPluginImport,
|
||||
"unused-imports": eslintPluginUnused,
|
||||
"simple-import-sort": eslintPluginSort,
|
||||
react: eslintPluginReact,
|
||||
"react-hooks": eslintPluginReactHooks,
|
||||
},
|
||||
|
||||
rules: {
|
||||
/* неиспользуемые переменные и импорты */
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
"warn",
|
||||
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
|
||||
],
|
||||
"unused-imports/no-unused-imports": "error",
|
||||
@ -44,15 +48,61 @@ const eslintConfig = [
|
||||
"error",
|
||||
{
|
||||
groups: [
|
||||
["^\\u0000"], // side-effects
|
||||
["^react", "^next", "^@?\\w"],// пакеты
|
||||
["^@/"], // алиасы проекта
|
||||
["^\\u0000"], // side-effects
|
||||
["^react", "^next", "^@?\\w"], // пакеты
|
||||
["^@/"], // алиасы проекта
|
||||
["^\\.\\.(?!/?$)", "^\\./(?=.*/)", "^\\./?$"], // относительные
|
||||
["^.+\\.module\\.(css|scss)$"], // модули стилей
|
||||
["^.+\\.module\\.(css|scss)$"], // модули стилей
|
||||
],
|
||||
},
|
||||
],
|
||||
"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": {
|
||||
"title": "Hello world!",
|
||||
"about": "Go to the about page"
|
||||
"HomePage": {
|
||||
"title": "Hello world!",
|
||||
"about": "Go to the about page"
|
||||
},
|
||||
"Profile": {
|
||||
"profile_information": {
|
||||
"title": "Profile Information",
|
||||
"description": "To update your email address please contact support.",
|
||||
"email_placeholder": "Email",
|
||||
"name_placeholder": "Name"
|
||||
},
|
||||
"Profile": {
|
||||
"profile_information": {
|
||||
"title": "Profile Information",
|
||||
"description": "To update your email address please contact support.",
|
||||
"email_placeholder": "Email",
|
||||
"name_placeholder": "Name"
|
||||
},
|
||||
"billing": {
|
||||
"title": "Billing",
|
||||
"description": "To access your subscription information, please log into your billing account.",
|
||||
"subscription_type": "Subscription Type:",
|
||||
"billing_button": "Billing",
|
||||
"credits": {
|
||||
"title": "You have {credits} credits left",
|
||||
"description": "You can use them to chat with any Expert on the platform."
|
||||
},
|
||||
"any_questions": "Any questions? <link>{linkText}</link>",
|
||||
"any_questions_link": "Contact us",
|
||||
"subscription_update": "<bold>{subscriptionUpdateBold}</bold><br></br>If you've just purchased or changed plan, your subscription status will change in a few hours.",
|
||||
"subscription_update_bold": "Subscription information is updated every few hours."
|
||||
},
|
||||
"log_out": {
|
||||
"title": "Log out",
|
||||
"log_out_button": "Log out",
|
||||
"modal": {
|
||||
"title": "Are you sure you want to log out?",
|
||||
"description": "Are you sure you want to log out?",
|
||||
"stay_button": "Stay",
|
||||
"log_out_button": "Log out"
|
||||
}
|
||||
}
|
||||
"billing": {
|
||||
"title": "Billing",
|
||||
"description": "To access your subscription information, please log into your billing account.",
|
||||
"subscription_type": "Subscription Type:",
|
||||
"billing_button": "Billing",
|
||||
"credits": {
|
||||
"title": "You have {credits} credits left",
|
||||
"description": "You can use them to chat with any Expert on the platform."
|
||||
},
|
||||
"any_questions": "Any questions? <link>{linkText}</link>",
|
||||
"any_questions_link": "Contact us",
|
||||
"subscription_update": "<bold>{subscriptionUpdateBold}</bold><br></br>If you've just purchased or changed plan, your subscription status will change in a few hours.",
|
||||
"subscription_update_bold": "Subscription information is updated every few hours."
|
||||
},
|
||||
"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": "🎉"
|
||||
"log_out": {
|
||||
"title": "Log out",
|
||||
"log_out_button": "Log out",
|
||||
"modal": {
|
||||
"title": "Are you sure you want to log out?",
|
||||
"description": "Are you sure you want to log out?",
|
||||
"stay_button": "Stay",
|
||||
"log_out_button": "Log out"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Subscriptions": {
|
||||
"title": "Manage my subscriptions",
|
||||
"modal": {
|
||||
"title": "Are you sure you want to cancel your subscription?",
|
||||
"description": "Are you sure you want to cancel your subscription?",
|
||||
"cancel_button": "Cancel subscription",
|
||||
"stay_button": "Stay"
|
||||
},
|
||||
"table": {
|
||||
"subscription_type": "Subscription Type",
|
||||
"subscription_type_value": {
|
||||
"DAY": "Daily Subscription",
|
||||
"WEEK": "Weekly Subscription",
|
||||
"MONTH": "Monthly Subscription",
|
||||
"YEAR": "Yearly Subscription"
|
||||
},
|
||||
"subscription_status": "Subscription Status",
|
||||
"subscription_status_value": {
|
||||
"ACTIVE": "Active",
|
||||
"CANCELLED": "Cancels on <date>"
|
||||
},
|
||||
"billing_period": "Billing Period",
|
||||
"billing_period_value": {
|
||||
"DAY": "Day",
|
||||
"WEEK": "Week",
|
||||
"MONTH": "Month",
|
||||
"YEAR": "Year"
|
||||
},
|
||||
"last_payment_on": "Last Payment On",
|
||||
"renewal_date": "Renewal Date",
|
||||
"renewal_amount": "Renewal Amount",
|
||||
"cancel_subscription": "Cancel Subscription"
|
||||
},
|
||||
"no_subscriptions": "You don't have any subscriptions",
|
||||
"error": "Something went wrong. Please try again later.",
|
||||
"try_again": "Try again"
|
||||
},
|
||||
"CancelSubscription": {
|
||||
"title": "Жаль, что вы уходите…",
|
||||
"description": "Многие уходят именно в тот момент, когда астролог начинает видеть поворотную точку в их истории.<br></br><br></br>Позвольте задать пару вопросов, чтобы сделать наш сервис лучше - и, возможно, предложить решение, которое больше подходит именно вам.",
|
||||
"stay_button": "Остаться и уменьшить мой план на 50%",
|
||||
"cancel_button": "Отменить"
|
||||
},
|
||||
"Stay50Done": {
|
||||
"title": "Мы ценим твой выбор!",
|
||||
"descriptions": {
|
||||
"1": "План успешно изменен"
|
||||
},
|
||||
"button": "Готово"
|
||||
},
|
||||
"AppreciateChoice": {
|
||||
"title": "Мы ценим твой выбор!",
|
||||
"descriptions": {
|
||||
"1": "Подбираем оптимальное решение...",
|
||||
"2": "Составляем персонализированный опрос...",
|
||||
"3": "Формируем выгодное предложение..."
|
||||
},
|
||||
"button": "Next"
|
||||
},
|
||||
"WhatReason": {
|
||||
"title": "Что стало причиной?",
|
||||
"answers": {
|
||||
"no_promised_result": "Не получил(а) обещанного результата",
|
||||
"too_expensive": "Слишком дорого",
|
||||
"high_auto_payment": "Стоимость автоматической оплаты слишком высока",
|
||||
"unexpected_fee": "Я не ожидал дополнительной платы",
|
||||
"want_pause": "Хочу сделать паузу",
|
||||
"service_not_as_expected": "Сервис оказался не таким, как ожидал(а)",
|
||||
"found_alternative": "Нашёл(а) альтернативу",
|
||||
"dislike_app": "Мне не понравилось приложение",
|
||||
"hard_to_navigate": "В приложении сложно ориентироваться",
|
||||
"other": "Другое"
|
||||
}
|
||||
},
|
||||
"Payment": {
|
||||
"Success": {
|
||||
"title": "Payment successful",
|
||||
"button": "Done"
|
||||
},
|
||||
"Error": {
|
||||
"title": "Payment failed",
|
||||
"button": "Try again"
|
||||
}
|
||||
},
|
||||
"SecondChance": {
|
||||
"title": "Дайте нам второй шанс и получи самый лучший план БЕСПЛАТНО",
|
||||
"offers": {
|
||||
"1": {
|
||||
"title": "Бесплатный план на<br></br>1 месяц",
|
||||
"description": "Используй весь потенциал AURA и даже больше.",
|
||||
"old-price": "1900",
|
||||
"new-price": "0"
|
||||
},
|
||||
"2": {
|
||||
"title": "Бесплатный премиальный план",
|
||||
"description": "Бесплатная 30 мин консультация с премиальным Эдвайзером",
|
||||
"old-price": "4900",
|
||||
"new-price": "0"
|
||||
}
|
||||
},
|
||||
"get_offer": "Получить бесплатный план",
|
||||
"cancel": "Отменить"
|
||||
},
|
||||
"ChangeMind": {
|
||||
"title": "Что может изменить твое мнение?",
|
||||
"answers": {
|
||||
"more_chat_time": "Больше времени в чатах",
|
||||
"more_personal_reports": "Больше персонализированных отчетов",
|
||||
"individual_plan": "Индивидуальный план",
|
||||
"other": "Другое"
|
||||
}
|
||||
},
|
||||
"StopFor30Days": {
|
||||
"title": "Остановите подписку на тридцать дней. Никаких списаний.",
|
||||
"stop": "Остановить",
|
||||
"cancel": "Отменить"
|
||||
},
|
||||
"CancellationOfSubscription": {
|
||||
"title": "Подписка аннулируется!",
|
||||
"description": "Чтобы отменить подписку, нажмите “Подтвердить мои действия”",
|
||||
"offer": {
|
||||
"title": "Бесплатный 2-месячный план",
|
||||
"old-price": "9900",
|
||||
"new-price": "0"
|
||||
},
|
||||
"offer_button": "Применить",
|
||||
"cancel_button": "Я подтверждаю свои действия"
|
||||
},
|
||||
"PlanCancelled": {
|
||||
"title": "Стандартный план Отменен!",
|
||||
"icon": "🥳",
|
||||
"description": "Выполнен переход на бесплатный тридцатидневный план ",
|
||||
"button": "Готово"
|
||||
},
|
||||
"SubscriptionStopped": {
|
||||
"title": "Подписка остановлена успешно!",
|
||||
"icon": "🎉"
|
||||
}
|
||||
}
|
||||
|
||||
369
messages/en.json
369
messages/en.json
@ -1,176 +1,201 @@
|
||||
{
|
||||
"HomePage": {
|
||||
"title": "Hello world!",
|
||||
"about": "Go to the about page"
|
||||
"HomePage": {
|
||||
"title": "Hello world!",
|
||||
"about": "Go to the about page"
|
||||
},
|
||||
"Profile": {
|
||||
"profile_information": {
|
||||
"title": "Profile Information",
|
||||
"description": "To update your email address please contact support.",
|
||||
"email_placeholder": "Email",
|
||||
"name_placeholder": "Name"
|
||||
},
|
||||
"Profile": {
|
||||
"profile_information": {
|
||||
"title": "Profile Information",
|
||||
"description": "To update your email address please contact support.",
|
||||
"email_placeholder": "Email",
|
||||
"name_placeholder": "Name"
|
||||
},
|
||||
"billing": {
|
||||
"title": "Billing",
|
||||
"description": "To access your subscription information, please log into your billing account.",
|
||||
"subscription_type": "Subscription Type:",
|
||||
"billing_button": "Billing",
|
||||
"credits": {
|
||||
"title": "You have {credits} credits left",
|
||||
"description": "You can use them to chat with any Expert on the platform."
|
||||
},
|
||||
"any_questions": "Any questions? <link>{linkText}</link>",
|
||||
"any_questions_link": "Contact us",
|
||||
"subscription_update": "<bold>{subscriptionUpdateBold}</bold><br></br>If you've just purchased or changed plan, your subscription status will change in a few hours.",
|
||||
"subscription_update_bold": "Subscription information is updated every few hours."
|
||||
},
|
||||
"log_out": {
|
||||
"title": "Log out",
|
||||
"log_out_button": "Log out",
|
||||
"modal": {
|
||||
"title": "Are you sure you want to log out?",
|
||||
"description": "Are you sure you want to log out?",
|
||||
"stay_button": "Stay",
|
||||
"log_out_button": "Log out"
|
||||
}
|
||||
}
|
||||
"billing": {
|
||||
"title": "Billing",
|
||||
"description": "To access your subscription information, please log into your billing account.",
|
||||
"subscription_type": "Subscription Type:",
|
||||
"billing_button": "Billing",
|
||||
"credits": {
|
||||
"title": "You have {credits} credits left",
|
||||
"description": "You can use them to chat with any Expert on the platform."
|
||||
},
|
||||
"any_questions": "Any questions? <link>{linkText}</link>",
|
||||
"any_questions_link": "Contact us",
|
||||
"subscription_update": "<bold>{subscriptionUpdateBold}</bold><br></br>If you've just purchased or changed plan, your subscription status will change in a few hours.",
|
||||
"subscription_update_bold": "Subscription information is updated every few hours."
|
||||
},
|
||||
"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": "🎉"
|
||||
"log_out": {
|
||||
"title": "Log out",
|
||||
"log_out_button": "Log out",
|
||||
"modal": {
|
||||
"title": "Are you sure you want to log out?",
|
||||
"description": "Are you sure you want to log out?",
|
||||
"stay_button": "Stay",
|
||||
"log_out_button": "Log out"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Subscriptions": {
|
||||
"title": "Manage my subscriptions",
|
||||
"modal": {
|
||||
"title": "Are you sure you want to cancel your subscription?",
|
||||
"description": "Are you sure you want to cancel your subscription?",
|
||||
"cancel_button": "Cancel subscription",
|
||||
"stay_button": "Stay"
|
||||
},
|
||||
"table": {
|
||||
"subscription_type": "Subscription Type",
|
||||
"subscription_type_value": {
|
||||
"DAY": "Daily Subscription",
|
||||
"WEEK": "Weekly Subscription",
|
||||
"MONTH": "Monthly Subscription",
|
||||
"YEAR": "Yearly Subscription"
|
||||
},
|
||||
"subscription_status": "Subscription Status",
|
||||
"subscription_status_value": {
|
||||
"ACTIVE": "Active",
|
||||
"CANCELLED": "Cancels on <date>",
|
||||
"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",
|
||||
"sass": "^1.89.2",
|
||||
"server-only": "^0.0.1",
|
||||
"zod": "^3.25.64"
|
||||
"zod": "^3.25.64",
|
||||
"zustand": "^5.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
@ -28,6 +29,8 @@
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.3.3",
|
||||
"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-unused-imports": "^4.1.4",
|
||||
"prettier": "^3.5.3",
|
||||
@ -1343,7 +1346,7 @@
|
||||
"version": "19.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz",
|
||||
"integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
@ -2441,7 +2444,7 @@
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/damerau-levenshtein": {
|
||||
@ -6006,6 +6009,35 @@
|
||||
"funding": {
|
||||
"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",
|
||||
"start": "next start -p 3001",
|
||||
"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": {
|
||||
"@lottiefiles/dotlottie-react": "^0.14.1",
|
||||
@ -20,7 +23,8 @@
|
||||
"react-dom": "^19.0.0",
|
||||
"sass": "^1.89.2",
|
||||
"server-only": "^0.0.1",
|
||||
"zod": "^3.25.64"
|
||||
"zod": "^3.25.64",
|
||||
"zustand": "^5.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
@ -30,9 +34,11 @@
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.3.3",
|
||||
"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-unused-imports": "^4.1.4",
|
||||
"prettier": "^3.5.3",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,24 +1,24 @@
|
||||
(function (m, e, t, r, i, k, a) {
|
||||
m[i] =
|
||||
m[i] ||
|
||||
function () {
|
||||
(m[i].a = m[i].a || []).push(arguments);
|
||||
};
|
||||
m[i].l = 1 * new Date();
|
||||
for (var j = 0; j < document.scripts.length; j++) {
|
||||
if (document.scripts[j].src === r) {
|
||||
return;
|
||||
}
|
||||
m[i] =
|
||||
m[i] ||
|
||||
function () {
|
||||
(m[i].a = m[i].a || []).push(arguments);
|
||||
};
|
||||
m[i].l = 1 * new Date();
|
||||
for (var j = 0; j < document.scripts.length; j++) {
|
||||
if (document.scripts[j].src === r) {
|
||||
return;
|
||||
}
|
||||
(k = e.createElement(t)),
|
||||
(a = e.getElementsByTagName(t)[0]),
|
||||
(k.async = 1),
|
||||
(k.src = r),
|
||||
a.parentNode.insertBefore(k, a);
|
||||
}
|
||||
(k = e.createElement(t)),
|
||||
(a = e.getElementsByTagName(t)[0]),
|
||||
(k.async = 1),
|
||||
(k.src = r),
|
||||
a.parentNode.insertBefore(k, a);
|
||||
})(
|
||||
window,
|
||||
document,
|
||||
"script",
|
||||
"https://cdn.jsdelivr.net/npm/yandex-metrica-watch/tag.js",
|
||||
"ym"
|
||||
);
|
||||
window,
|
||||
document,
|
||||
"script",
|
||||
"https://cdn.jsdelivr.net/npm/yandex-metrica-watch/tag.js",
|
||||
"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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
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({
|
||||
error,
|
||||
@ -14,20 +14,21 @@ export default function Error({
|
||||
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>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
.main {
|
||||
padding: 16px;
|
||||
padding-bottom: 64px;
|
||||
padding: 16px;
|
||||
padding-bottom: 64px;
|
||||
}
|
||||
|
||||
.navBar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 7777;
|
||||
background: var(--background);
|
||||
}
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 7777;
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
@ -3,14 +3,14 @@ import { DrawerProvider, NavigationBar } from "@/components/layout";
|
||||
import styles from "./layout.module.scss";
|
||||
|
||||
export default function CoreLayout({
|
||||
children,
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return <DrawerProvider>
|
||||
<NavigationBar className={styles.navBar} />
|
||||
<main className={styles.main}>
|
||||
{children}
|
||||
</main>
|
||||
return (
|
||||
<DrawerProvider>
|
||||
<NavigationBar className={styles.navBar} />
|
||||
<main className={styles.main}>{children}</main>
|
||||
</DrawerProvider>
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
.coreSpinnerContainer {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
.page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
@ -1,41 +1,45 @@
|
||||
import { Suspense } from "react";
|
||||
|
||||
import {
|
||||
AdvisersSection,
|
||||
AdvisersSectionSkeleton,
|
||||
CompatibilitySection,
|
||||
CompatibilitySectionSkeleton,
|
||||
MeditationSection,
|
||||
MeditationSectionSkeleton,
|
||||
PalmSection,
|
||||
PalmSectionSkeleton
|
||||
AdvisersSection,
|
||||
AdvisersSectionSkeleton,
|
||||
CompatibilitySection,
|
||||
CompatibilitySectionSkeleton,
|
||||
MeditationSection,
|
||||
MeditationSectionSkeleton,
|
||||
PalmSection,
|
||||
PalmSectionSkeleton,
|
||||
} from "@/components/domains/dashboard";
|
||||
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";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<section className={styles.page}>
|
||||
<Horoscope />
|
||||
|
||||
return (
|
||||
<section className={styles.page}>
|
||||
<Horoscope />
|
||||
<Suspense fallback={<AdvisersSectionSkeleton />}>
|
||||
<AdvisersSection promise={loadAssistants()} />
|
||||
</Suspense>
|
||||
|
||||
<Suspense fallback={<AdvisersSectionSkeleton />}>
|
||||
<AdvisersSection promise={loadAssistants()} />
|
||||
</Suspense>
|
||||
<Suspense fallback={<CompatibilitySectionSkeleton />}>
|
||||
<CompatibilitySection promise={loadCompatibility()} />
|
||||
</Suspense>
|
||||
|
||||
<Suspense fallback={<CompatibilitySectionSkeleton />}>
|
||||
<CompatibilitySection promise={loadCompatibility()} />
|
||||
</Suspense>
|
||||
<Suspense fallback={<MeditationSectionSkeleton />}>
|
||||
<MeditationSection promise={loadMeditations()} />
|
||||
</Suspense>
|
||||
|
||||
<Suspense fallback={<MeditationSectionSkeleton />}>
|
||||
<MeditationSection promise={loadMeditations()} />
|
||||
</Suspense>
|
||||
|
||||
<Suspense fallback={<PalmSectionSkeleton />}>
|
||||
<PalmSection promise={loadPalms()} />
|
||||
</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";
|
||||
|
||||
export default async function PaymentFailed() {
|
||||
const t = await getTranslations("Payment.Error");
|
||||
const t = await getTranslations("Payment.Error");
|
||||
|
||||
return (
|
||||
<AnimatedInfoScreen
|
||||
lottieAnimation={<LottieAnimation loadKey={ELottieKeys.loaderCheckMark} />}
|
||||
title={t("title")}
|
||||
animationTime={0}
|
||||
animationTexts={[]}
|
||||
buttonText={t("button")}
|
||||
nextRoute={ROUTES.home()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<AnimatedInfoScreen
|
||||
lottieAnimation={
|
||||
<LottieAnimation loadKey={ELottieKeys.loaderCheckMark} />
|
||||
}
|
||||
title={t("title")}
|
||||
animationTime={0}
|
||||
animationTexts={[]}
|
||||
buttonText={t("button")}
|
||||
nextRoute={ROUTES.home()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -4,26 +4,26 @@ import { createPaymentCheckout } from "@/entities/payment/api";
|
||||
import { ROUTES } from "@/shared/constants/client-routes";
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const productId = req.nextUrl.searchParams.get("productId");
|
||||
const placementId = req.nextUrl.searchParams.get("placementId");
|
||||
const paywallId = req.nextUrl.searchParams.get("paywallId");
|
||||
const fbPixels = req.nextUrl.searchParams.get("fb_pixels");
|
||||
const productPrice = req.nextUrl.searchParams.get("price");
|
||||
const currency = req.nextUrl.searchParams.get("currency");
|
||||
const productId = req.nextUrl.searchParams.get("productId");
|
||||
const placementId = req.nextUrl.searchParams.get("placementId");
|
||||
const paywallId = req.nextUrl.searchParams.get("paywallId");
|
||||
const fbPixels = req.nextUrl.searchParams.get("fb_pixels");
|
||||
const productPrice = req.nextUrl.searchParams.get("price");
|
||||
const currency = req.nextUrl.searchParams.get("currency");
|
||||
|
||||
const data = await createPaymentCheckout({
|
||||
productId: productId || "",
|
||||
placementId: placementId || "",
|
||||
paywallId: paywallId || "",
|
||||
});
|
||||
const data = await createPaymentCheckout({
|
||||
productId: productId || "",
|
||||
placementId: placementId || "",
|
||||
paywallId: paywallId || "",
|
||||
});
|
||||
|
||||
let redirectUrl: URL = new URL(data?.paymentUrl || "", req.nextUrl.origin);
|
||||
if (!redirectUrl) {
|
||||
redirectUrl = new URL(`${ROUTES.paymentFailed()}`, origin);
|
||||
}
|
||||
if (fbPixels) redirectUrl.searchParams.set("fb_pixels", fbPixels);
|
||||
if (productPrice) redirectUrl.searchParams.set("price", productPrice);
|
||||
if (currency) redirectUrl.searchParams.set("currency", currency);
|
||||
let redirectUrl: URL = new URL(data?.paymentUrl || "", req.nextUrl.origin);
|
||||
if (!redirectUrl) {
|
||||
redirectUrl = new URL(`${ROUTES.paymentFailed()}`, origin);
|
||||
}
|
||||
if (fbPixels) redirectUrl.searchParams.set("fb_pixels", fbPixels);
|
||||
if (productPrice) redirectUrl.searchParams.set("price", productPrice);
|
||||
if (currency) redirectUrl.searchParams.set("currency", currency);
|
||||
|
||||
return NextResponse.redirect(redirectUrl, { status: 307 });
|
||||
}
|
||||
return NextResponse.redirect(redirectUrl, { status: 307 });
|
||||
}
|
||||
|
||||
@ -1,23 +1,23 @@
|
||||
.button {
|
||||
position: fixed;
|
||||
bottom: calc(0dvh + 64px);
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
max-width: 400px;
|
||||
width: calc(100dvw - 32px);
|
||||
animation: fadeIn 0.5s ease-in-out 2s forwards;
|
||||
position: fixed;
|
||||
bottom: calc(0dvh + 64px);
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
max-width: 400px;
|
||||
width: calc(100dvw - 32px);
|
||||
animation: fadeIn 0.5s ease-in-out 2s forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
from {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,65 +10,72 @@ import { ROUTES } from "@/shared/constants/client-routes";
|
||||
import styles from "./Metrics.module.scss";
|
||||
|
||||
interface MetricsProps {
|
||||
fbPixels: string[];
|
||||
productPrice: string;
|
||||
currency: string;
|
||||
fbPixels: string[];
|
||||
productPrice: string;
|
||||
currency: string;
|
||||
}
|
||||
|
||||
export default function Metrics({ fbPixels, productPrice, currency }: MetricsProps) {
|
||||
const t = useTranslations("Payment.Success");
|
||||
export default function Metrics({
|
||||
fbPixels,
|
||||
productPrice,
|
||||
currency,
|
||||
}: MetricsProps) {
|
||||
const t = useTranslations("Payment.Success");
|
||||
|
||||
const [isButtonVisible, setIsButtonVisible] = useState(false);
|
||||
const [isButtonVisible, setIsButtonVisible] = useState(false);
|
||||
|
||||
const navigateToHome = () => {
|
||||
window.location.href = ROUTES.home()
|
||||
}
|
||||
const navigateToHome = () => {
|
||||
window.location.href = ROUTES.home();
|
||||
};
|
||||
|
||||
// Yandex Metrica
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
if (typeof window.ym === 'function' && typeof window.klaviyo === 'object' && typeof window.gtag === 'function') {
|
||||
try {
|
||||
window.gtag('event', 'PaymentSuccess')
|
||||
window.klaviyo.push(['track', "PaymentSuccess"]);
|
||||
// Yandex Metrica
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
if (
|
||||
typeof window.ym === "function" &&
|
||||
typeof window.klaviyo === "object" &&
|
||||
typeof window.gtag === "function"
|
||||
) {
|
||||
try {
|
||||
window.gtag("event", "PaymentSuccess");
|
||||
window.klaviyo.push(["track", "PaymentSuccess"]);
|
||||
|
||||
window.ym(95799066, "init", {
|
||||
clickmap: true,
|
||||
trackLinks: true,
|
||||
accurateTrackBounce: true,
|
||||
webvisor: true,
|
||||
});
|
||||
window.ym(95799066, "init", {
|
||||
clickmap: true,
|
||||
trackLinks: true,
|
||||
accurateTrackBounce: true,
|
||||
webvisor: true,
|
||||
});
|
||||
|
||||
window.ym(95799066, 'reachGoal', "PaymentSuccess", {}, () => {
|
||||
console.log("Запрос отправлен");
|
||||
// deleteYm()
|
||||
setIsButtonVisible(true);
|
||||
})
|
||||
window.ym(95799066, "reachGoal", "PaymentSuccess", {}, () => {
|
||||
// deleteYm()
|
||||
setIsButtonVisible(true);
|
||||
});
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("YM error:", e);
|
||||
} finally {
|
||||
clearInterval(interval);
|
||||
}
|
||||
}
|
||||
}, 200);
|
||||
|
||||
} catch (e) {
|
||||
console.error('YM error:', e)
|
||||
} finally {
|
||||
clearInterval(interval);
|
||||
}
|
||||
}
|
||||
}, 200);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, []);
|
||||
|
||||
return <>
|
||||
|
||||
{/* Klaviyo */}
|
||||
{/* <Script src="https://static.klaviyo.com/onsite/js/klaviyo.js?company_id=RM7w5r" /> */}
|
||||
<Script id="klaviyo-script">
|
||||
{`const script = document.createElement("script");
|
||||
return (
|
||||
<>
|
||||
{/* Klaviyo */}
|
||||
{/* <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.type = "text/javascript";
|
||||
script.async = "";
|
||||
document.head.appendChild(script);`}
|
||||
</Script>
|
||||
<Script id="klaviyo-script-proxy">
|
||||
{`
|
||||
</Script>
|
||||
<Script id="klaviyo-script-proxy">
|
||||
{`
|
||||
!(function () {
|
||||
if (!window.klaviyo) {
|
||||
window._klOnsite = window._klOnsite || [];
|
||||
@ -117,11 +124,11 @@ export default function Metrics({ fbPixels, productPrice, currency }: MetricsPro
|
||||
}
|
||||
})();
|
||||
`}
|
||||
</Script>
|
||||
</Script>
|
||||
|
||||
{/* Yandex Metrica */}
|
||||
<Script id="yandex-metrica-script">
|
||||
{`(function (m, e, t, r, i, k, a) {
|
||||
{/* Yandex Metrica */}
|
||||
<Script id="yandex-metrica-script">
|
||||
{`(function (m, e, t, r, i, k, a) {
|
||||
m[i] =
|
||||
m[i] ||
|
||||
function () {
|
||||
@ -145,22 +152,26 @@ export default function Metrics({ fbPixels, productPrice, currency }: MetricsPro
|
||||
"https://cdn.jsdelivr.net/npm/yandex-metrica-watch/tag.js",
|
||||
"ym"
|
||||
);`}
|
||||
</Script>
|
||||
</Script>
|
||||
|
||||
{/* Google Analytics */}
|
||||
<Script id="google-analytics-script" async src="https://www.googletagmanager.com/gtag/js?id=G-4N17LL3BB5" />
|
||||
<Script id="google-analytics-script-config">
|
||||
{`window.dataLayer = window.dataLayer || [];
|
||||
{/* Google Analytics */}
|
||||
<Script
|
||||
id="google-analytics-script"
|
||||
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);}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config','G-4N17LL3BB5');`}
|
||||
</Script>
|
||||
</Script>
|
||||
|
||||
{/* Facebook Pixel */}
|
||||
{fbPixels.map((pixel) => (
|
||||
<Script id={`facebook-pixel-${pixel}`} key={pixel}>
|
||||
{`!function(f,b,e,v,n,t,s)
|
||||
{/* Facebook Pixel */}
|
||||
{fbPixels.map(pixel => (
|
||||
<Script id={`facebook-pixel-${pixel}`} key={pixel}>
|
||||
{`!function(f,b,e,v,n,t,s)
|
||||
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
|
||||
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';
|
||||
@ -171,15 +182,14 @@ s.parentNode.insertBefore(t,s)}(window, document,'script',
|
||||
fbq('init', '${pixel}');
|
||||
fbq('track', 'PageView');
|
||||
fbq('track', 'Purchase', { value: ${productPrice}, currency: "${currency}" });`}
|
||||
</Script>
|
||||
))}
|
||||
</Script>
|
||||
))}
|
||||
|
||||
{isButtonVisible &&
|
||||
<Button onClick={navigateToHome} className={styles.button}>
|
||||
<Typography color="white">
|
||||
{t("button")}
|
||||
</Typography>
|
||||
</Button>
|
||||
}
|
||||
</>;
|
||||
}
|
||||
{isButtonVisible && (
|
||||
<Button onClick={navigateToHome} className={styles.button}>
|
||||
<Typography color="white">{t("button")}</Typography>
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -5,30 +5,34 @@ import { ELottieKeys } from "@/shared/constants/lottie";
|
||||
|
||||
import Metrics from "./Metrics";
|
||||
|
||||
export default async function PaymentSuccess({ searchParams }: {
|
||||
searchParams: Promise<{
|
||||
[key: string]: string | undefined
|
||||
}>;
|
||||
export default async function PaymentSuccess({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{
|
||||
[key: string]: string | undefined;
|
||||
}>;
|
||||
}) {
|
||||
const params = await searchParams;
|
||||
const params = await searchParams;
|
||||
|
||||
const fbPixels = params?.fb_pixels?.split(",") || [];
|
||||
const productPrice = params?.price || "0";
|
||||
const currency = params?.currency || "USD";
|
||||
const fbPixels = params?.fb_pixels?.split(",") || [];
|
||||
const productPrice = params?.price || "0";
|
||||
const currency = params?.currency || "USD";
|
||||
|
||||
const t = await getTranslations("Payment.Success");
|
||||
const t = await getTranslations("Payment.Success");
|
||||
|
||||
return (
|
||||
<>
|
||||
<AnimatedInfoScreen
|
||||
lottieAnimation={<LottieAnimation loadKey={ELottieKeys.loaderCheckMark} />}
|
||||
title={t("title")}
|
||||
/>
|
||||
<Metrics
|
||||
fbPixels={fbPixels}
|
||||
productPrice={productPrice}
|
||||
currency={currency}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<AnimatedInfoScreen
|
||||
lottieAnimation={
|
||||
<LottieAnimation loadKey={ELottieKeys.loaderCheckMark} />
|
||||
}
|
||||
title={t("title")}
|
||||
/>
|
||||
<Metrics
|
||||
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 {
|
||||
padding: 6px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.line {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: #f0f0f0;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: #f0f0f0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.modal-container {
|
||||
max-width: 290px;
|
||||
padding: 24px 0px 0px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 16px;
|
||||
padding-inline: 24px;
|
||||
}
|
||||
|
||||
.modal-description {
|
||||
padding-inline: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal-answers {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-top: 24px;
|
||||
border-top: 1px solid #D9D9D9;
|
||||
}
|
||||
|
||||
.modal-answer {
|
||||
width: 50%;
|
||||
height: 52px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #275DA7;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
|
||||
&:first-child {
|
||||
border-right: 1px solid #D9D9D9;
|
||||
}
|
||||
}
|
||||
@ -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() {
|
||||
const t = useTranslations("Profile");
|
||||
|
||||
return (
|
||||
<ProfilePage />
|
||||
)
|
||||
}
|
||||
const profileBlocks = [
|
||||
{
|
||||
title: t("profile_information.title"),
|
||||
description: t("profile_information.description"),
|
||||
children: (
|
||||
<Suspense fallback={<ProfileInformationSkeleton />}>
|
||||
<ProfileInformation user={loadUser()} />
|
||||
</Suspense>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("billing.title"),
|
||||
description: t("billing.description"),
|
||||
children: <Billing />,
|
||||
},
|
||||
{
|
||||
title: t("log_out.title"),
|
||||
children: <LogOut />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<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 { useTranslations } from 'next-intl';
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { Button, Typography } from '@/components/ui';
|
||||
import { ROUTES } from '@/shared/constants/client-routes';
|
||||
import { Button, Typography } from "@/components/ui";
|
||||
import { ROUTES } from "@/shared/constants/client-routes";
|
||||
|
||||
import styles from "./page.module.scss"
|
||||
import styles from "./page.module.scss";
|
||||
|
||||
export default function Error() {
|
||||
const t = useTranslations("Subscriptions")
|
||||
const router = useRouter()
|
||||
const t = useTranslations("Subscriptions");
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Typography as="h1" className={styles.title}>{t("title")}</Typography>
|
||||
<Typography as='p' align='center'>{t("error")}</Typography>
|
||||
<Typography as="h1" className={styles.title}>
|
||||
{t("title")}
|
||||
</Typography>
|
||||
<Typography as="p" align="center">
|
||||
{t("error")}
|
||||
</Typography>
|
||||
<Button
|
||||
onClick={
|
||||
// () => reset()
|
||||
() => router.push(ROUTES.retainingFunnelCancelSubscription())
|
||||
}
|
||||
>
|
||||
<Typography color='white'>{t("try_again")}</Typography>
|
||||
<Typography color="white">{t("try_again")}</Typography>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,96 +1,96 @@
|
||||
.container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
padding-inline: 8px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
padding-inline: 8px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.buttonCancel {
|
||||
color: #ACB0BA;
|
||||
font-size: 16px;
|
||||
line-height: 25px;
|
||||
background: none;
|
||||
border: none;
|
||||
text-decoration: underline;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
min-height: 0;
|
||||
max-width: none;
|
||||
color: #acb0ba;
|
||||
font-size: 16px;
|
||||
line-height: 25px;
|
||||
background: none;
|
||||
border: none;
|
||||
text-decoration: underline;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
min-height: 0;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.description,
|
||||
.error {
|
||||
font-size: 16px;
|
||||
line-height: 25px;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
line-height: 25px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: #ACB0BA;
|
||||
color: #acb0ba;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #FF0000;
|
||||
color: #ff0000;
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
max-width: 290px;
|
||||
padding: 24px 0px 0px;
|
||||
overflow: hidden;
|
||||
max-width: 290px;
|
||||
padding: 24px 0px 0px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 16px;
|
||||
padding-inline: 24px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16px;
|
||||
padding-inline: 24px;
|
||||
}
|
||||
|
||||
.modal-description {
|
||||
padding-inline: 24px;
|
||||
text-align: center;
|
||||
padding-inline: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal-answers {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-top: 24px;
|
||||
border-top: 1px solid #D9D9D9;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-top: 24px;
|
||||
border-top: 1px solid #d9d9d9;
|
||||
}
|
||||
|
||||
.modal-answer {
|
||||
width: 50%;
|
||||
height: 52px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #275DA7;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
width: 50%;
|
||||
height: 52px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #275da7;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
|
||||
&:first-child {
|
||||
border-right: 1px solid #D9D9D9;
|
||||
}
|
||||
&:first-child {
|
||||
border-right: 1px solid #d9d9d9;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.dark-theme) .modal-container {
|
||||
background-color: #343639;
|
||||
background-color: #343639;
|
||||
|
||||
&>.modal-answers>.modal-answer {
|
||||
color: #1e7dff;
|
||||
}
|
||||
& > .modal-answers > .modal-answer {
|
||||
color: #1e7dff;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.dark-theme) .modal-title {
|
||||
color: #F7F7F7;
|
||||
color: #f7f7f7;
|
||||
}
|
||||
|
||||
: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 { CancelSubscriptionModalProvider, SubscriptionsList, SubscriptionsListSkeleton } from "@/components/domains/profile/subscriptions";
|
||||
import {
|
||||
CancelSubscriptionModalProvider,
|
||||
SubscriptionsList,
|
||||
SubscriptionsListSkeleton,
|
||||
} from "@/components/domains/profile/subscriptions";
|
||||
import { Typography } from "@/components/ui";
|
||||
import { loadSubscriptionsData } from "@/entities/subscriptions/loaders";
|
||||
|
||||
import styles from "./page.module.scss"
|
||||
import styles from "./page.module.scss";
|
||||
|
||||
export default async function Subscriptions() {
|
||||
const t = await getTranslations("Subscriptions");
|
||||
const t = await getTranslations("Subscriptions");
|
||||
|
||||
return (
|
||||
<CancelSubscriptionModalProvider>
|
||||
<div className={styles.container}>
|
||||
<Typography as="h1" className={styles.title}>{t("title")}</Typography>
|
||||
<Suspense fallback={<SubscriptionsListSkeleton />}>
|
||||
<SubscriptionsList promise={loadSubscriptionsData()} />
|
||||
</Suspense>
|
||||
</div>
|
||||
</CancelSubscriptionModalProvider>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<CancelSubscriptionModalProvider>
|
||||
<div className={styles.container}>
|
||||
<Typography as="h1" className={styles.title}>
|
||||
{t("title")}
|
||||
</Typography>
|
||||
<Suspense fallback={<SubscriptionsListSkeleton />}>
|
||||
<SubscriptionsList promise={loadSubscriptionsData()} />
|
||||
</Suspense>
|
||||
</div>
|
||||
</CancelSubscriptionModalProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@ -5,23 +5,24 @@ import { ROUTES } from "@/shared/constants/client-routes";
|
||||
import { ELottieKeys } from "@/shared/constants/lottie";
|
||||
|
||||
export default async function AppreciateChoice() {
|
||||
const t = await getTranslations("AppreciateChoice");
|
||||
const t = await getTranslations("AppreciateChoice");
|
||||
|
||||
const animationTexts = [
|
||||
t("descriptions.1"),
|
||||
t("descriptions.2"),
|
||||
t("descriptions.3"),
|
||||
]
|
||||
const animationTexts = [
|
||||
t("descriptions.1"),
|
||||
t("descriptions.2"),
|
||||
t("descriptions.3"),
|
||||
];
|
||||
|
||||
return (
|
||||
<AnimatedInfoScreen
|
||||
lottieAnimation={<LottieAnimation loadKey={ELottieKeys.loaderCheckMark} />}
|
||||
title={t("title")}
|
||||
animationTime={9000}
|
||||
animationTexts={animationTexts}
|
||||
buttonText={t("button")}
|
||||
nextRoute={ROUTES.retainingFunnelWhatReason()}
|
||||
/>
|
||||
|
||||
)
|
||||
}
|
||||
return (
|
||||
<AnimatedInfoScreen
|
||||
lottieAnimation={
|
||||
<LottieAnimation loadKey={ELottieKeys.loaderCheckMark} />
|
||||
}
|
||||
title={t("title")}
|
||||
animationTime={9000}
|
||||
animationTexts={animationTexts}
|
||||
buttonText={t("button")}
|
||||
nextRoute={ROUTES.retainingFunnelWhatReason()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,18 +1,18 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 28px;
|
||||
line-height: 125%;
|
||||
color: #1A1A1A;
|
||||
font-size: 28px;
|
||||
line-height: 125%;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 20px;
|
||||
line-height: 125%;
|
||||
color: #2C2C2C;
|
||||
padding-inline: 14px;
|
||||
}
|
||||
font-size: 20px;
|
||||
line-height: 125%;
|
||||
color: #2c2c2c;
|
||||
padding-inline: 14px;
|
||||
}
|
||||
|
||||
@ -6,19 +6,19 @@ import { Typography } from "@/components/ui";
|
||||
import styles from "./page.module.scss";
|
||||
|
||||
export default function CanselSubscription() {
|
||||
const t = useTranslations("CancelSubscription");
|
||||
const t = useTranslations("CancelSubscription");
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Typography as="h1" size="xl" weight="bold" className={styles.title}>
|
||||
{t("title")}
|
||||
</Typography>
|
||||
<Typography as="p" className={styles.description}>
|
||||
{t.rich("description", {
|
||||
br: () => <br />
|
||||
})}
|
||||
</Typography>
|
||||
<Buttons />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Typography as="h1" size="xl" weight="bold" className={styles.title}>
|
||||
{t("title")}
|
||||
</Typography>
|
||||
<Typography as="p" className={styles.description}>
|
||||
{t.rich("description", {
|
||||
br: () => <br />,
|
||||
})}
|
||||
</Typography>
|
||||
<Buttons />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,48 +1,52 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-top: 28px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-top: 28px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 27px;
|
||||
line-height: 40px;
|
||||
margin: 0;
|
||||
font-size: 27px;
|
||||
line-height: 40px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.description {
|
||||
line-height: 25px;
|
||||
margin-top: 74px;
|
||||
padding-inline: 28px;
|
||||
color: #ACB0BA;
|
||||
line-height: 25px;
|
||||
margin-top: 74px;
|
||||
padding-inline: 28px;
|
||||
color: #acb0ba;
|
||||
}
|
||||
|
||||
.topSellingImage {
|
||||
margin-top: -50px;
|
||||
margin-top: -50px;
|
||||
}
|
||||
|
||||
.offer {
|
||||
margin-top: 12px;
|
||||
background-image: url("/retaining/zodiac_circle.png");
|
||||
background-size: 125%;
|
||||
background-position: center -45px;
|
||||
background-repeat: no-repeat;
|
||||
margin-top: 12px;
|
||||
background-image: url("/retaining/zodiac_circle.png");
|
||||
background-size: 125%;
|
||||
background-position: center -45px;
|
||||
background-repeat: no-repeat;
|
||||
|
||||
& * {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
& * {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 14px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: linear-gradient(0deg, #FFFFFF 25.48%, rgba(255, 255, 255, 0) 100%);
|
||||
}
|
||||
}
|
||||
&::after {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 14px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: linear-gradient(
|
||||
0deg,
|
||||
#ffffff 25.48%,
|
||||
rgba(255, 255, 255, 0) 100%
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,34 +1,37 @@
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
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 { Typography } from "@/components/ui";
|
||||
|
||||
import styles from "./page.module.scss";
|
||||
|
||||
export default async function CancellationOfSubscription() {
|
||||
const t = await getTranslations("CancellationOfSubscription");
|
||||
const t = await getTranslations("CancellationOfSubscription");
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<LottieAnimations />
|
||||
<Typography as="h1" weight="bold" className={styles.title}>
|
||||
{t("title")}
|
||||
</Typography>
|
||||
<Typography as="p" align="left" className={styles.description}>
|
||||
{t("description")}
|
||||
</Typography>
|
||||
<Offer
|
||||
className={styles.offer}
|
||||
classNameTitle={styles.titleOffer}
|
||||
title={t("offer.title")}
|
||||
oldPrice={t("offer.old-price")}
|
||||
newPrice={t("offer.new-price")}
|
||||
image={<TopSellingSvg className={styles.topSellingImage} />}
|
||||
active={true}
|
||||
/>
|
||||
<Buttons />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<LottieAnimations />
|
||||
<Typography as="h1" weight="bold" className={styles.title}>
|
||||
{t("title")}
|
||||
</Typography>
|
||||
<Typography as="p" align="left" className={styles.description}>
|
||||
{t("description")}
|
||||
</Typography>
|
||||
<Offer
|
||||
className={styles.offer}
|
||||
classNameTitle={styles.titleOffer}
|
||||
title={t("offer.title")}
|
||||
oldPrice={t("offer.old-price")}
|
||||
newPrice={t("offer.new-price")}
|
||||
image={<TopSellingSvg className={styles.topSellingImage} />}
|
||||
active={true}
|
||||
/>
|
||||
<Buttons />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,36 +1,39 @@
|
||||
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";
|
||||
|
||||
export default function ChangeMind() {
|
||||
const t = useTranslations("ChangeMind")
|
||||
const t = useTranslations("ChangeMind");
|
||||
|
||||
const answers: ChangeMindAnswer[] = [
|
||||
{
|
||||
id: 1,
|
||||
title: t("answers.more_chat_time"),
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: t("answers.more_personal_reports"),
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: t("answers.individual_plan"),
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: t("answers.other"),
|
||||
}
|
||||
]
|
||||
const answers: ChangeMindAnswer[] = [
|
||||
{
|
||||
id: 1,
|
||||
title: t("answers.more_chat_time"),
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: t("answers.more_personal_reports"),
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: t("answers.individual_plan"),
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: t("answers.other"),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography size="xl" weight="bold" as="h1">
|
||||
{t("title")}
|
||||
</Typography>
|
||||
<ChangeMindButtons answers={answers} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Typography size="xl" weight="bold" as="h1">
|
||||
{t("title")}
|
||||
</Typography>
|
||||
<ChangeMindButtons answers={answers} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -5,51 +5,48 @@ import { ROUTES } from "@/shared/constants/client-routes";
|
||||
import { ERetainingFunnel } from "@/types";
|
||||
|
||||
const stepperRoutes: Record<ERetainingFunnel, string[]> = {
|
||||
[ERetainingFunnel.Red]: [
|
||||
ROUTES.retainingFunnelAppreciateChoice(),
|
||||
// ROUTES.retainingFunnelWhatReason(),
|
||||
// ROUTES.retainingFunnelSecondChance(),
|
||||
// ROUTES.retainingFunnelChangeMind(),
|
||||
// ROUTES.retainingFunnelStopFor30Days(),
|
||||
// ROUTES.retainingFunnelCancellationOfSubscription(),
|
||||
],
|
||||
[ERetainingFunnel.Green]: [
|
||||
ROUTES.retainingFunnelAppreciateChoice(),
|
||||
// ROUTES.retainingFunnelWhatReason(),
|
||||
// ROUTES.retainingFunnelStopFor30Days(),
|
||||
// ROUTES.retainingFunnelChangeMind(),
|
||||
// ROUTES.retainingFunnelSecondChance(),
|
||||
// ROUTES.retainingFunnelCancellationOfSubscription(),
|
||||
[ERetainingFunnel.Red]: [
|
||||
ROUTES.retainingFunnelAppreciateChoice(),
|
||||
// ROUTES.retainingFunnelWhatReason(),
|
||||
// ROUTES.retainingFunnelSecondChance(),
|
||||
// ROUTES.retainingFunnelChangeMind(),
|
||||
// ROUTES.retainingFunnelStopFor30Days(),
|
||||
// ROUTES.retainingFunnelCancellationOfSubscription(),
|
||||
],
|
||||
[ERetainingFunnel.Green]: [
|
||||
ROUTES.retainingFunnelAppreciateChoice(),
|
||||
// ROUTES.retainingFunnelWhatReason(),
|
||||
// ROUTES.retainingFunnelStopFor30Days(),
|
||||
// ROUTES.retainingFunnelChangeMind(),
|
||||
// ROUTES.retainingFunnelSecondChance(),
|
||||
// ROUTES.retainingFunnelCancellationOfSubscription(),
|
||||
],
|
||||
[ERetainingFunnel.Purple]: [
|
||||
ROUTES.retainingFunnelAppreciateChoice(),
|
||||
// ROUTES.retainingFunnelWhatReason(),
|
||||
// ROUTES.retainingFunnelChangeMind(),
|
||||
// ROUTES.retainingFunnelSecondChance(),
|
||||
// ROUTES.retainingFunnelStopFor30Days(),
|
||||
// ROUTES.retainingFunnelCancellationOfSubscription(),
|
||||
],
|
||||
[ERetainingFunnel.Stay50]: [ROUTES.retainingFunnelStay50Done()],
|
||||
};
|
||||
|
||||
],
|
||||
[ERetainingFunnel.Purple]: [
|
||||
ROUTES.retainingFunnelAppreciateChoice(),
|
||||
// ROUTES.retainingFunnelWhatReason(),
|
||||
// ROUTES.retainingFunnelChangeMind(),
|
||||
// ROUTES.retainingFunnelSecondChance(),
|
||||
// ROUTES.retainingFunnelStopFor30Days(),
|
||||
// ROUTES.retainingFunnelCancellationOfSubscription(),
|
||||
],
|
||||
[ERetainingFunnel.Stay50]: [
|
||||
ROUTES.retainingFunnelStay50Done(),
|
||||
],
|
||||
}
|
||||
function StepperLayout({ children }: { children: React.ReactNode }) {
|
||||
// const darkTheme = useSelector(selectors.selectDarkTheme);
|
||||
// const mainRef = useRef<HTMLDivElement>(null);
|
||||
// useSchemeColorByElement(mainRef.current, "section.page, .page, section", [
|
||||
// location,
|
||||
// ]);
|
||||
// const retainingFunnel = useSelector(selectors.selectRetainingFunnel);
|
||||
const retainingFunnel = ERetainingFunnel.Red;
|
||||
|
||||
function StepperLayout({ children }: { children: React.ReactNode; }) {
|
||||
// const darkTheme = useSelector(selectors.selectDarkTheme);
|
||||
// const mainRef = useRef<HTMLDivElement>(null);
|
||||
// useSchemeColorByElement(mainRef.current, "section.page, .page, section", [
|
||||
// location,
|
||||
// ]);
|
||||
// const retainingFunnel = useSelector(selectors.selectRetainingFunnel);
|
||||
const retainingFunnel = ERetainingFunnel.Red;
|
||||
|
||||
return (
|
||||
<>
|
||||
<RetainingStepper stepperRoutes={stepperRoutes[retainingFunnel]} />
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<RetainingStepper stepperRoutes={stepperRoutes[retainingFunnel]} />
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default StepperLayout;
|
||||
|
||||
@ -1,27 +1,27 @@
|
||||
.container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding: 80px 28px 0;
|
||||
min-height: calc(100dvh - 124px);
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding: 80px 28px 0;
|
||||
min-height: calc(100dvh - 124px);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 27px;
|
||||
line-height: 40px;
|
||||
margin: 0;
|
||||
font-size: 27px;
|
||||
line-height: 40px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 80px;
|
||||
font-size: 80px;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: #ACB0BA;
|
||||
font-size: 17px;
|
||||
line-height: 25px;
|
||||
margin-top: 72px;
|
||||
}
|
||||
color: #acb0ba;
|
||||
font-size: 17px;
|
||||
line-height: 25px;
|
||||
margin-top: 72px;
|
||||
}
|
||||
|
||||
@ -6,20 +6,18 @@ import { Typography } from "@/components/ui";
|
||||
import styles from "./page.module.scss";
|
||||
|
||||
export default async function PlanCancelled() {
|
||||
const t = await getTranslations("PlanCancelled");
|
||||
const t = await getTranslations("PlanCancelled");
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Typography as="h1" weight="semiBold" className={styles.title}>
|
||||
{t("title")}
|
||||
</Typography>
|
||||
<span className={styles.icon}>
|
||||
{t("icon")}
|
||||
</span>
|
||||
<PlanCancelledButton />
|
||||
<Typography as="p" className={styles.description}>
|
||||
{t("description")}
|
||||
</Typography>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Typography as="h1" weight="semiBold" className={styles.title}>
|
||||
{t("title")}
|
||||
</Typography>
|
||||
<span className={styles.icon}>{t("icon")}</span>
|
||||
<PlanCancelledButton />
|
||||
<Typography as="p" className={styles.description}>
|
||||
{t("description")}
|
||||
</Typography>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
.container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
// overflow-x: clip;
|
||||
// padding-inline: 2px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
// overflow-x: clip;
|
||||
// padding-inline: 2px;
|
||||
}
|
||||
|
||||
.title {
|
||||
line-height: 150%;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
line-height: 150%;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
@ -6,14 +6,14 @@ import { Typography } from "@/components/ui";
|
||||
import styles from "./page.module.scss";
|
||||
|
||||
export default async function SecondChance() {
|
||||
const t = await getTranslations("SecondChance");
|
||||
const t = await getTranslations("SecondChance");
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Typography as="h1" weight="bold" size="xl" className={styles.title}>
|
||||
{t("title")}
|
||||
</Typography>
|
||||
<SecondChancePage />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Typography as="h1" weight="bold" size="xl" className={styles.title}>
|
||||
{t("title")}
|
||||
</Typography>
|
||||
<SecondChancePage />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -5,21 +5,20 @@ import { ROUTES } from "@/shared/constants/client-routes";
|
||||
import { ELottieKeys } from "@/shared/constants/lottie";
|
||||
|
||||
export default async function Stay50Done() {
|
||||
const t = await getTranslations("Stay50Done");
|
||||
const t = await getTranslations("Stay50Done");
|
||||
|
||||
const animationTexts = [
|
||||
t("descriptions.1"),
|
||||
]
|
||||
const animationTexts = [t("descriptions.1")];
|
||||
|
||||
return (
|
||||
<AnimatedInfoScreen
|
||||
lottieAnimation={<LottieAnimation loadKey={ELottieKeys.loaderCheckMark} />}
|
||||
title={t("title")}
|
||||
animationTime={5000}
|
||||
animationTexts={animationTexts}
|
||||
buttonText={t("button")}
|
||||
nextRoute={ROUTES.home()}
|
||||
/>
|
||||
|
||||
)
|
||||
}
|
||||
return (
|
||||
<AnimatedInfoScreen
|
||||
lottieAnimation={
|
||||
<LottieAnimation loadKey={ELottieKeys.loaderCheckMark} />
|
||||
}
|
||||
title={t("title")}
|
||||
animationTime={5000}
|
||||
animationTexts={animationTexts}
|
||||
buttonText={t("button")}
|
||||
nextRoute={ROUTES.home()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
.title {
|
||||
font-size: 27px;
|
||||
line-height: 40px;
|
||||
margin: 0;
|
||||
}
|
||||
font-size: 27px;
|
||||
line-height: 40px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@ -6,14 +6,14 @@ import { Typography } from "@/components/ui";
|
||||
import styles from "./page.module.scss";
|
||||
|
||||
export default async function StopFor30Days() {
|
||||
const t = await getTranslations("StopFor30Days");
|
||||
const t = await getTranslations("StopFor30Days");
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Typography as="h1" weight="bold" className={styles.title}>
|
||||
{t("title")}
|
||||
</Typography>
|
||||
<StopFor30DaysButtons />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<Typography as="h1" weight="bold" className={styles.title}>
|
||||
{t("title")}
|
||||
</Typography>
|
||||
<StopFor30DaysButtons />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,20 +1,20 @@
|
||||
.container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding: 80px 28px 0;
|
||||
min-height: calc(100dvh - 124px);
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding: 80px 28px 0;
|
||||
min-height: calc(100dvh - 124px);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 27px;
|
||||
line-height: 40px;
|
||||
margin: 0;
|
||||
font-size: 27px;
|
||||
line-height: 40px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 80px;
|
||||
}
|
||||
font-size: 80px;
|
||||
}
|
||||
|
||||
@ -6,17 +6,15 @@ import { Typography } from "@/components/ui";
|
||||
import styles from "./page.module.scss";
|
||||
|
||||
export default async function SubscriptionStopped() {
|
||||
const t = await getTranslations("SubscriptionStopped");
|
||||
const t = await getTranslations("SubscriptionStopped");
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Typography as="h1" weight="semiBold" className={styles.title}>
|
||||
{t("title")}
|
||||
</Typography>
|
||||
<span className={styles.icon}>
|
||||
{t("icon")}
|
||||
</span>
|
||||
<SubscriptionStoppedButton />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Typography as="h1" weight="semiBold" className={styles.title}>
|
||||
{t("title")}
|
||||
</Typography>
|
||||
<span className={styles.icon}>{t("icon")}</span>
|
||||
<SubscriptionStoppedButton />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,71 +1,74 @@
|
||||
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 { ERetainingFunnel } from "@/types";
|
||||
|
||||
export default function WhatReason() {
|
||||
const t = useTranslations("WhatReason")
|
||||
const t = useTranslations("WhatReason");
|
||||
|
||||
const answers: WhatReasonAnswer[] = [
|
||||
{
|
||||
id: 1,
|
||||
title: t("answers.no_promised_result"),
|
||||
funnel: ERetainingFunnel.Red
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: t("answers.too_expensive"),
|
||||
funnel: ERetainingFunnel.Red
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: t("answers.high_auto_payment"),
|
||||
funnel: ERetainingFunnel.Red
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: t("answers.unexpected_fee"),
|
||||
funnel: ERetainingFunnel.Red
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: t("answers.want_pause"),
|
||||
funnel: ERetainingFunnel.Green
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
title: t("answers.service_not_as_expected"),
|
||||
funnel: ERetainingFunnel.Red
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
title: t("answers.found_alternative"),
|
||||
funnel: ERetainingFunnel.Red
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
title: t("answers.dislike_app"),
|
||||
funnel: ERetainingFunnel.Purple
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
title: t("answers.hard_to_navigate"),
|
||||
funnel: ERetainingFunnel.Purple
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
title: t("answers.other"),
|
||||
funnel: ERetainingFunnel.Purple
|
||||
}
|
||||
]
|
||||
const answers: WhatReasonAnswer[] = [
|
||||
{
|
||||
id: 1,
|
||||
title: t("answers.no_promised_result"),
|
||||
funnel: ERetainingFunnel.Red,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: t("answers.too_expensive"),
|
||||
funnel: ERetainingFunnel.Red,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: t("answers.high_auto_payment"),
|
||||
funnel: ERetainingFunnel.Red,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: t("answers.unexpected_fee"),
|
||||
funnel: ERetainingFunnel.Red,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: t("answers.want_pause"),
|
||||
funnel: ERetainingFunnel.Green,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
title: t("answers.service_not_as_expected"),
|
||||
funnel: ERetainingFunnel.Red,
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
title: t("answers.found_alternative"),
|
||||
funnel: ERetainingFunnel.Red,
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
title: t("answers.dislike_app"),
|
||||
funnel: ERetainingFunnel.Purple,
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
title: t("answers.hard_to_navigate"),
|
||||
funnel: ERetainingFunnel.Purple,
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
title: t("answers.other"),
|
||||
funnel: ERetainingFunnel.Purple,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography size="xl" weight="bold" as="h1">
|
||||
{t("title")}
|
||||
</Typography>
|
||||
<WhatReasonsButtons answers={answers} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Typography size="xl" weight="bold" as="h1">
|
||||
{t("title")}
|
||||
</Typography>
|
||||
<WhatReasonsButtons answers={answers} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -34,8 +34,14 @@ import { ROUTES } from "@/shared/constants/client-routes";
|
||||
|
||||
function extractTrackingCookiesFromUrl(url: URL): Record<string, string> {
|
||||
const trackingCookieKeys = [
|
||||
'_fbc', '_fbp', '_ym_uid', '_ym_d', '_ym_isad', '_ym_visorc',
|
||||
'yandexuid', 'ymex'
|
||||
"_fbc",
|
||||
"_fbp",
|
||||
"_ym_uid",
|
||||
"_ym_d",
|
||||
"_ym_isad",
|
||||
"_ym_visorc",
|
||||
"yandexuid",
|
||||
"ymex",
|
||||
];
|
||||
|
||||
const cookies: Record<string, string> = {};
|
||||
@ -43,8 +49,8 @@ function extractTrackingCookiesFromUrl(url: URL): Record<string, string> {
|
||||
for (const [key, value] of url.searchParams.entries()) {
|
||||
if (
|
||||
trackingCookieKeys.includes(key) ||
|
||||
key.startsWith('_ga') ||
|
||||
key.startsWith('_gid')
|
||||
key.startsWith("_ga") ||
|
||||
key.startsWith("_gid")
|
||||
) {
|
||||
cookies[key] = value;
|
||||
}
|
||||
@ -63,7 +69,10 @@ export async function GET(req: NextRequest) {
|
||||
const productPrice = searchParams.get("price");
|
||||
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 (placementId) redirectUrl.searchParams.set("placementId", placementId);
|
||||
if (paywallId) redirectUrl.searchParams.set("paywallId", paywallId);
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
.body {
|
||||
max-width: 560px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
}
|
||||
max-width: 560px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@ -5,14 +5,16 @@ import type { Metadata } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
import { notFound } from "next/navigation";
|
||||
import { hasLocale, NextIntlClientProvider } from "next-intl";
|
||||
import { getMessages } from "next-intl/server";
|
||||
import clsx from "clsx";
|
||||
|
||||
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() {
|
||||
return routing.locales.map((locale) => ({ locale }));
|
||||
return routing.locales.map(locale => ({ locale }));
|
||||
}
|
||||
|
||||
const inter = Inter({
|
||||
@ -23,7 +25,8 @@ const inter = Inter({
|
||||
|
||||
export const metadata: Metadata = {
|
||||
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({
|
||||
@ -38,10 +41,14 @@ export default async function RootLayout({
|
||||
notFound();
|
||||
}
|
||||
|
||||
const messages = await getMessages();
|
||||
|
||||
return (
|
||||
<html lang={locale}>
|
||||
<body className={clsx(inter.variable, styles.body)}>
|
||||
<NextIntlClientProvider>{children}</NextIntlClientProvider>
|
||||
<NextIntlClientProvider messages={messages}>
|
||||
<StoreProvider>{children}</StoreProvider>
|
||||
</NextIntlClientProvider>
|
||||
</body>
|
||||
</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 {
|
||||
padding: 0;
|
||||
min-width: 160px;
|
||||
height: 235px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
padding: 0;
|
||||
min-width: 160px;
|
||||
height: 235px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
||||
& > * {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
& > .info {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
|
||||
&>* {
|
||||
z-index: 1;
|
||||
& > .name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 6px;
|
||||
|
||||
& > .indicator {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: #34d399;
|
||||
}
|
||||
}
|
||||
|
||||
&>.info {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
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;
|
||||
}
|
||||
& > .rating {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.shadow {
|
||||
height: 160px;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background: linear-gradient(0deg, #174280 0%, rgba(0, 0, 0, 0) 70.95%);
|
||||
// border: 1px solid rgba(229, 231, 235, 1);
|
||||
}
|
||||
height: 160px;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background: linear-gradient(0deg, #174280 0%, rgba(0, 0, 0, 0) 70.95%);
|
||||
// border: 1px solid rgba(229, 231, 235, 1);
|
||||
}
|
||||
|
||||
@ -1,47 +1,55 @@
|
||||
import { Button, Card, Stars, Typography } from "@/components/ui"
|
||||
import { Assistant } from "@/entities/dashboard/types"
|
||||
import { Button, Card, Stars, Typography } from "@/components/ui";
|
||||
import { Assistant } from "@/entities/dashboard/types";
|
||||
|
||||
import styles from "./AdviserCard.module.scss"
|
||||
import styles from "./AdviserCard.module.scss";
|
||||
|
||||
type AdviserCardProps = Assistant;
|
||||
|
||||
export default function AdviserCard({
|
||||
name,
|
||||
photoUrl,
|
||||
rating,
|
||||
reviewCount,
|
||||
description
|
||||
name,
|
||||
photoUrl,
|
||||
rating,
|
||||
reviewCount,
|
||||
description,
|
||||
}: AdviserCardProps) {
|
||||
return (
|
||||
<Card className={styles.card} style={{ backgroundImage: `url(${photoUrl})` }}>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.info}>
|
||||
<div className={styles.name}>
|
||||
<Typography color="white" weight="bold">
|
||||
{name}
|
||||
</Typography>
|
||||
<div className={styles.indicator} />
|
||||
</div>
|
||||
<Typography className={styles.description} color="white" weight="medium" size="xs">
|
||||
{description}
|
||||
</Typography>
|
||||
<div className={styles.rating}>
|
||||
<Typography color="white" weight="medium" size="xs">
|
||||
{rating}
|
||||
</Typography>
|
||||
<Stars rating={rating} />
|
||||
<Typography color="white" weight="medium" size="xs">
|
||||
({reviewCount})
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<Button size="sm">
|
||||
<Typography color="white" weight="bold" size="sm">
|
||||
CHAT | FREE
|
||||
</Typography>
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.shadow} />
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Card
|
||||
className={styles.card}
|
||||
style={{ backgroundImage: `url(${photoUrl})` }}
|
||||
>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.info}>
|
||||
<div className={styles.name}>
|
||||
<Typography color="white" weight="bold">
|
||||
{name}
|
||||
</Typography>
|
||||
<div className={styles.indicator} />
|
||||
</div>
|
||||
<Typography
|
||||
className={styles.description}
|
||||
color="white"
|
||||
weight="medium"
|
||||
size="xs"
|
||||
>
|
||||
{description}
|
||||
</Typography>
|
||||
<div className={styles.rating}>
|
||||
<Typography color="white" weight="medium" size="xs">
|
||||
{rating}
|
||||
</Typography>
|
||||
<Stars rating={rating} />
|
||||
<Typography color="white" weight="medium" size="xs">
|
||||
({reviewCount})
|
||||
</Typography>
|
||||
</div>
|
||||
</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 {
|
||||
padding: 0;
|
||||
min-width: 320px;
|
||||
height: 110px;
|
||||
overflow: hidden;
|
||||
box-shadow: none;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 0;
|
||||
min-width: 320px;
|
||||
height: 110px;
|
||||
overflow: hidden;
|
||||
box-shadow: none;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 22px 16px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: 22px 16px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.compatibilityImage {
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
|
||||
@ -9,33 +9,35 @@ import styles from "./CompatibilityCard.module.scss";
|
||||
type CompatibilityCardProps = CompatibilityAction;
|
||||
|
||||
export default function CompatibilityCard({
|
||||
imageUrl,
|
||||
title,
|
||||
type,
|
||||
minutes
|
||||
imageUrl,
|
||||
title,
|
||||
type,
|
||||
minutes,
|
||||
}: CompatibilityCardProps) {
|
||||
return (
|
||||
<Card className={styles.card}>
|
||||
<Image
|
||||
className={styles.compatibilityImage}
|
||||
src={imageUrl}
|
||||
alt="Compatibility image"
|
||||
width={120}
|
||||
height={110}
|
||||
/>
|
||||
<div className={styles.content}>
|
||||
<Typography size="lg" weight="medium" align="left">
|
||||
{title}
|
||||
</Typography>
|
||||
<MetaLabel iconLabelProps={{
|
||||
iconProps: {
|
||||
name: IconName.Article,
|
||||
},
|
||||
children: <Typography color="secondary">{type}</Typography>
|
||||
}}>
|
||||
{minutes} min
|
||||
</MetaLabel>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Card className={styles.card}>
|
||||
<Image
|
||||
className={styles.compatibilityImage}
|
||||
src={imageUrl}
|
||||
alt="Compatibility image"
|
||||
width={120}
|
||||
height={110}
|
||||
/>
|
||||
<div className={styles.content}>
|
||||
<Typography size="lg" weight="medium" align="left">
|
||||
{title}
|
||||
</Typography>
|
||||
<MetaLabel
|
||||
iconLabelProps={{
|
||||
iconProps: {
|
||||
name: IconName.Article,
|
||||
},
|
||||
children: <Typography color="secondary">{type}</Typography>,
|
||||
}}
|
||||
>
|
||||
{minutes} min
|
||||
</MetaLabel>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,41 +1,41 @@
|
||||
.card.card {
|
||||
padding: 0;
|
||||
min-width: 342px;
|
||||
height: 308px;
|
||||
overflow: hidden;
|
||||
box-shadow: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
min-width: 342px;
|
||||
height: 308px;
|
||||
overflow: hidden;
|
||||
box-shadow: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 16px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
& > .info {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
&>.info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
&>.button {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background-color: #F5F5F7;
|
||||
padding: 0;
|
||||
|
||||
&>.icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
& > .button {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background-color: #f5f5f7;
|
||||
padding: 0;
|
||||
|
||||
& > .icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.meditationImage {
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
|
||||
@ -4,56 +4,58 @@ import { Button, Card, Icon, MetaLabel, Typography } from "@/components/ui";
|
||||
import { IconName } from "@/components/ui/Icon/Icon";
|
||||
import { Meditation } from "@/entities/dashboard/types";
|
||||
|
||||
import styles from "./MeditationCard.module.scss"
|
||||
import styles from "./MeditationCard.module.scss";
|
||||
|
||||
type MeditationCardProps = Meditation;
|
||||
|
||||
export default function MeditationCard({
|
||||
imageUrl,
|
||||
title,
|
||||
type,
|
||||
minutes
|
||||
imageUrl,
|
||||
title,
|
||||
type,
|
||||
minutes,
|
||||
}: MeditationCardProps) {
|
||||
return (
|
||||
<Card className={styles.card}>
|
||||
<Image
|
||||
className={styles.meditationImage}
|
||||
src={imageUrl}
|
||||
alt="Meditation image"
|
||||
width={342}
|
||||
height={216}
|
||||
/>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.info}>
|
||||
<Typography size="lg" weight="regular">
|
||||
{title}
|
||||
</Typography>
|
||||
<MetaLabel iconLabelProps={{
|
||||
iconProps: {
|
||||
name: IconName.Video,
|
||||
color: "#6B7280",
|
||||
size: {
|
||||
width: 24,
|
||||
height: 25
|
||||
}
|
||||
},
|
||||
children: <Typography color="secondary">{type}</Typography>
|
||||
}}>
|
||||
{minutes} min
|
||||
</MetaLabel>
|
||||
</div>
|
||||
<Button className={styles.button}>
|
||||
<Icon
|
||||
className={styles.icon}
|
||||
name={IconName.Chevron}
|
||||
size={{
|
||||
width: 18,
|
||||
height: 18
|
||||
}}
|
||||
color="#A0A7B5"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Card className={styles.card}>
|
||||
<Image
|
||||
className={styles.meditationImage}
|
||||
src={imageUrl}
|
||||
alt="Meditation image"
|
||||
width={342}
|
||||
height={216}
|
||||
/>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.info}>
|
||||
<Typography size="lg" weight="regular">
|
||||
{title}
|
||||
</Typography>
|
||||
<MetaLabel
|
||||
iconLabelProps={{
|
||||
iconProps: {
|
||||
name: IconName.Video,
|
||||
color: "#6B7280",
|
||||
size: {
|
||||
width: 24,
|
||||
height: 25,
|
||||
},
|
||||
},
|
||||
children: <Typography color="secondary">{type}</Typography>,
|
||||
}}
|
||||
>
|
||||
{minutes} min
|
||||
</MetaLabel>
|
||||
</div>
|
||||
<Button className={styles.button}>
|
||||
<Icon
|
||||
className={styles.icon}
|
||||
name={IconName.Chevron}
|
||||
size={{
|
||||
width: 18,
|
||||
height: 18,
|
||||
}}
|
||||
color="#A0A7B5"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,33 +1,37 @@
|
||||
.card.card {
|
||||
padding: 0;
|
||||
min-width: 200px;
|
||||
height: 227px;
|
||||
overflow: hidden;
|
||||
box-shadow: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
min-width: 200px;
|
||||
height: 227px;
|
||||
overflow: hidden;
|
||||
box-shadow: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.image {
|
||||
width: 100%;
|
||||
height: 123px;
|
||||
background: linear-gradient(90deg, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0) 100%);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 123px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(0, 0, 0, 0.5) 0%,
|
||||
rgba(0, 0, 0, 0) 100%
|
||||
);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 14px 12px 12px;
|
||||
padding: 14px 12px 12px;
|
||||
|
||||
&>.info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 15px;
|
||||
}
|
||||
& > .info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.palmImage {
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
|
||||
@ -9,42 +9,44 @@ import styles from "./PalmCard.module.scss";
|
||||
type PalmCardProps = PalmAction;
|
||||
|
||||
export default function PalmCard({
|
||||
imageUrl,
|
||||
title,
|
||||
type,
|
||||
minutes
|
||||
imageUrl,
|
||||
title,
|
||||
type,
|
||||
minutes,
|
||||
}: PalmCardProps) {
|
||||
return (
|
||||
<Card className={styles.card}>
|
||||
<div className={styles.image}>
|
||||
<Image
|
||||
className={styles.palmImage}
|
||||
src={imageUrl}
|
||||
alt="Palm image"
|
||||
width={99}
|
||||
height={123}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.info}>
|
||||
<Typography size="lg" align="left">
|
||||
{title}
|
||||
</Typography>
|
||||
<MetaLabel iconLabelProps={{
|
||||
iconProps: {
|
||||
name: IconName.Video,
|
||||
color: "#6B7280",
|
||||
size: {
|
||||
width: 24,
|
||||
height: 25
|
||||
}
|
||||
},
|
||||
children: <Typography color="secondary">{type}</Typography>
|
||||
}}>
|
||||
{minutes} min
|
||||
</MetaLabel>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Card className={styles.card}>
|
||||
<div className={styles.image}>
|
||||
<Image
|
||||
className={styles.palmImage}
|
||||
src={imageUrl}
|
||||
alt="Palm image"
|
||||
width={99}
|
||||
height={123}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.info}>
|
||||
<Typography size="lg" align="left">
|
||||
{title}
|
||||
</Typography>
|
||||
<MetaLabel
|
||||
iconLabelProps={{
|
||||
iconProps: {
|
||||
name: IconName.Video,
|
||||
color: "#6B7280",
|
||||
size: {
|
||||
width: 24,
|
||||
height: 25,
|
||||
},
|
||||
},
|
||||
children: <Typography color="secondary">{type}</Typography>,
|
||||
}}
|
||||
>
|
||||
{minutes} min
|
||||
</MetaLabel>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
export { default as AdviserCard } from './AdviserCard/AdviserCard';
|
||||
export { default as CompatibilityCard } from './CompatibilityCard/CompatibilityCard';
|
||||
export { default as MeditationCard } from './MeditationCard/MeditationCard';
|
||||
export { default as PalmCard } from './PalmCard/PalmCard';
|
||||
export { default as AdviserCard } from "./AdviserCard/AdviserCard";
|
||||
export { default as CompatibilityCard } from "./CompatibilityCard/CompatibilityCard";
|
||||
export { default as MeditationCard } from "./MeditationCard/MeditationCard";
|
||||
export { default as PalmCard } from "./PalmCard/PalmCard";
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
export * from './cards';
|
||||
export * from './sections';
|
||||
export * from "./cards";
|
||||
export * from "./sections";
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
.sectionContent.sectionContent {
|
||||
overflow-x: scroll;
|
||||
width: calc(100% + 32px);
|
||||
padding: 32px 16px;
|
||||
margin: -32px -16px;
|
||||
overflow-x: scroll;
|
||||
width: calc(100% + 32px);
|
||||
padding: 32px 16px;
|
||||
margin: -32px -16px;
|
||||
}
|
||||
|
||||
.skeleton.skeleton {
|
||||
height: 486px;
|
||||
}
|
||||
height: 486px;
|
||||
}
|
||||
|
||||
@ -7,25 +7,29 @@ import { AdviserCard } from "../../cards";
|
||||
|
||||
import styles from "./AdvisersSection.module.scss";
|
||||
|
||||
export default function AdvisersSection({ promise }: { promise: Promise<Assistant[]> }) {
|
||||
const assistants = use(promise);
|
||||
const columns = Math.ceil(assistants?.length / 2);
|
||||
export default function AdvisersSection({
|
||||
promise,
|
||||
}: {
|
||||
promise: Promise<Assistant[]>;
|
||||
}) {
|
||||
const assistants = use(promise);
|
||||
const columns = Math.ceil(assistants?.length / 2);
|
||||
|
||||
return (
|
||||
<Section title="Advisers" contentClassName={styles.sectionContent}>
|
||||
<Grid columns={columns} className={styles.grid}>
|
||||
{assistants.map((adviser) => (
|
||||
<AdviserCard key={adviser._id} {...adviser} />
|
||||
))}
|
||||
</Grid>
|
||||
</Section>
|
||||
)
|
||||
return (
|
||||
<Section title="Advisers" contentClassName={styles.sectionContent}>
|
||||
<Grid columns={columns} className={styles.grid}>
|
||||
{assistants.map(adviser => (
|
||||
<AdviserCard key={adviser._id} {...adviser} />
|
||||
))}
|
||||
</Grid>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
export function AdvisersSectionSkeleton() {
|
||||
return (
|
||||
<Section title="Advisers" contentClassName={styles.sectionContent}>
|
||||
<Skeleton className={styles.skeleton} />
|
||||
</Section>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Section title="Advisers" contentClassName={styles.sectionContent}>
|
||||
<Skeleton className={styles.skeleton} />
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
.sectionContent.sectionContent {
|
||||
overflow-x: scroll;
|
||||
width: calc(100% + 32px);
|
||||
padding: 16px;
|
||||
margin: -16px;
|
||||
overflow-x: scroll;
|
||||
width: calc(100% + 32px);
|
||||
padding: 16px;
|
||||
margin: -16px;
|
||||
}
|
||||
|
||||
.skeleton.skeleton {
|
||||
height: 236px;
|
||||
}
|
||||
height: 236px;
|
||||
}
|
||||
|
||||
@ -1,31 +1,42 @@
|
||||
import { use } from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
import { Grid, Section, Skeleton } from "@/components/ui";
|
||||
import { CompatibilityAction } from "@/entities/dashboard/types";
|
||||
import { ROUTES } from "@/shared/constants/client-routes";
|
||||
|
||||
import { CompatibilityCard } from "../../cards";
|
||||
|
||||
import styles from "./CompatibilitySection.module.scss";
|
||||
|
||||
export default function CompatibilitySection({ promise }: { promise: Promise<CompatibilityAction[]> }) {
|
||||
const compatibilities = use(promise);
|
||||
const columns = Math.ceil(compatibilities?.length / 2);
|
||||
export default function CompatibilitySection({
|
||||
promise,
|
||||
}: {
|
||||
promise: Promise<CompatibilityAction[]>;
|
||||
}) {
|
||||
const compatibilities = use(promise);
|
||||
const columns = Math.ceil(compatibilities?.length / 2);
|
||||
|
||||
return (
|
||||
<Section title="Compatibility" contentClassName={styles.sectionContent}>
|
||||
<Grid columns={columns} className={styles.grid}>
|
||||
{compatibilities.map((compatibility) => (
|
||||
<CompatibilityCard key={compatibility._id} {...compatibility} />
|
||||
))}
|
||||
</Grid>
|
||||
</Section>
|
||||
)
|
||||
return (
|
||||
<Section title="Compatibility" contentClassName={styles.sectionContent}>
|
||||
<Grid columns={columns} className={styles.grid}>
|
||||
{compatibilities.map(compatibility => (
|
||||
<Link
|
||||
href={ROUTES.compatibility(compatibility._id)}
|
||||
key={compatibility._id}
|
||||
>
|
||||
<CompatibilityCard key={compatibility._id} {...compatibility} />
|
||||
</Link>
|
||||
))}
|
||||
</Grid>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
export function CompatibilitySectionSkeleton() {
|
||||
return (
|
||||
<Section title="Compatibility" contentClassName={styles.sectionContent}>
|
||||
<Skeleton className={styles.skeleton} />
|
||||
</Section>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Section title="Compatibility" contentClassName={styles.sectionContent}>
|
||||
<Skeleton className={styles.skeleton} />
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
.sectionContent.sectionContent {
|
||||
overflow-x: scroll;
|
||||
width: calc(100% + 32px);
|
||||
padding: 16px;
|
||||
margin: -16px;
|
||||
overflow-x: scroll;
|
||||
width: calc(100% + 32px);
|
||||
padding: 16px;
|
||||
margin: -16px;
|
||||
}
|
||||
|
||||
.skeleton.skeleton {
|
||||
height: 308px;
|
||||
}
|
||||
height: 308px;
|
||||
}
|
||||
|
||||
@ -7,25 +7,29 @@ import { MeditationCard } from "../../cards";
|
||||
|
||||
import styles from "./MeditationSection.module.scss";
|
||||
|
||||
export default function MeditationSection({ promise }: { promise: Promise<Meditation[]> }) {
|
||||
const meditations = use(promise);
|
||||
const columns = meditations?.length;
|
||||
export default function MeditationSection({
|
||||
promise,
|
||||
}: {
|
||||
promise: Promise<Meditation[]>;
|
||||
}) {
|
||||
const meditations = use(promise);
|
||||
const columns = meditations?.length;
|
||||
|
||||
return (
|
||||
<Section title="Meditations" contentClassName={styles.sectionContent}>
|
||||
<Grid columns={columns} className={styles.grid}>
|
||||
{meditations.map((meditation) => (
|
||||
<MeditationCard key={meditation._id} {...meditation} />
|
||||
))}
|
||||
</Grid>
|
||||
</Section>
|
||||
)
|
||||
return (
|
||||
<Section title="Meditations" contentClassName={styles.sectionContent}>
|
||||
<Grid columns={columns} className={styles.grid}>
|
||||
{meditations.map(meditation => (
|
||||
<MeditationCard key={meditation._id} {...meditation} />
|
||||
))}
|
||||
</Grid>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
export function MeditationSectionSkeleton() {
|
||||
return (
|
||||
<Section title="Meditations" contentClassName={styles.sectionContent}>
|
||||
<Skeleton className={styles.skeleton} />
|
||||
</Section>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Section title="Meditations" contentClassName={styles.sectionContent}>
|
||||
<Skeleton className={styles.skeleton} />
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
.sectionContent.sectionContent {
|
||||
overflow-x: scroll;
|
||||
width: calc(100% + 32px);
|
||||
padding: 16px;
|
||||
margin: -16px;
|
||||
overflow-x: scroll;
|
||||
width: calc(100% + 32px);
|
||||
padding: 16px;
|
||||
margin: -16px;
|
||||
}
|
||||
|
||||
.skeleton.skeleton {
|
||||
height: 227px;
|
||||
}
|
||||
height: 227px;
|
||||
}
|
||||
|
||||
@ -1,31 +1,39 @@
|
||||
import { use } from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
import { Grid, Section, Skeleton } from "@/components/ui";
|
||||
import { PalmAction } from "@/entities/dashboard/types";
|
||||
import { ROUTES } from "@/shared/constants/client-routes";
|
||||
|
||||
import { PalmCard } from "../../cards";
|
||||
|
||||
import styles from "./PalmSection.module.scss";
|
||||
|
||||
export default function PalmSection({ promise }: { promise: Promise<PalmAction[]> }) {
|
||||
const palms = use(promise);
|
||||
const columns = palms?.length;
|
||||
export default function PalmSection({
|
||||
promise,
|
||||
}: {
|
||||
promise: Promise<PalmAction[]>;
|
||||
}) {
|
||||
const palms = use(promise);
|
||||
const columns = palms?.length;
|
||||
|
||||
return (
|
||||
<Section title="Palm" contentClassName={styles.sectionContent}>
|
||||
<Grid columns={columns} className={styles.grid}>
|
||||
{palms.map((palm) => (
|
||||
<PalmCard key={palm._id} {...palm} />
|
||||
))}
|
||||
</Grid>
|
||||
</Section>
|
||||
)
|
||||
return (
|
||||
<Section title="Palm" contentClassName={styles.sectionContent}>
|
||||
<Grid columns={columns} className={styles.grid}>
|
||||
{palms.map(palm => (
|
||||
<Link href={ROUTES.palmistryResult(palm._id)} key={palm._id}>
|
||||
<PalmCard key={palm._id} {...palm} />
|
||||
</Link>
|
||||
))}
|
||||
</Grid>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
export function PalmSectionSkeleton() {
|
||||
return (
|
||||
<Section title="Palm" contentClassName={styles.sectionContent}>
|
||||
<Skeleton className={styles.skeleton} />
|
||||
</Section>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Section title="Palm" contentClassName={styles.sectionContent}>
|
||||
<Skeleton className={styles.skeleton} />
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,4 +1,16 @@
|
||||
export { default as AdvisersSection, AdvisersSectionSkeleton } 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';
|
||||
export {
|
||||
default as AdvisersSection,
|
||||
AdvisersSectionSkeleton,
|
||||
} 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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.button {
|
||||
min-height: 60px;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.credits {
|
||||
padding: 16px;
|
||||
background-color: #275ca7;
|
||||
border-radius: 12px;
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
background-color: #275ca7;
|
||||
border-radius: 12px;
|
||||
width: 100%;
|
||||
|
||||
.creditsDescription {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.creditsDescription {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.anyQuestions {
|
||||
width: 100%;
|
||||
width: 100%;
|
||||
|
||||
&>a {
|
||||
color: #275ca7;
|
||||
text-decoration: underline;
|
||||
}
|
||||
& > a {
|
||||
color: #275ca7;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.subscriptionUpdate {
|
||||
width: 100%;
|
||||
line-height: 1.25;
|
||||
}
|
||||
width: 100%;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
@ -1,64 +1,82 @@
|
||||
"use client;"
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { Button, Typography } from "@/components/ui";
|
||||
import { ROUTES } from "@/shared/constants/client-routes";
|
||||
|
||||
import styles from "./Billing.module.scss"
|
||||
import styles from "./Billing.module.scss";
|
||||
|
||||
interface IBillingProps {
|
||||
onBilling: () => void;
|
||||
}
|
||||
function Billing() {
|
||||
const t = useTranslations("Profile.billing");
|
||||
const router = useRouter();
|
||||
|
||||
function Billing({ onBilling }: IBillingProps) {
|
||||
const t = useTranslations('Profile.billing');
|
||||
const onBilling = () => {
|
||||
router.push(ROUTES.profileSubscriptions());
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Button
|
||||
className={styles.button}
|
||||
onClick={onBilling}
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Button className={styles.button} onClick={onBilling}>
|
||||
<Typography size="xl" color="white">
|
||||
{t("billing_button")}
|
||||
</Typography>
|
||||
</Button>
|
||||
<div className={styles.credits}>
|
||||
<Typography as="p" weight="bold" color="white" align="left">
|
||||
{t("credits.title", {
|
||||
credits: String(0),
|
||||
})}
|
||||
</Typography>
|
||||
<Typography
|
||||
className={styles.creditsDescription}
|
||||
as="p"
|
||||
size="sm"
|
||||
color="white"
|
||||
align="left"
|
||||
>
|
||||
{t("credits.description")}
|
||||
</Typography>
|
||||
</div>
|
||||
<Typography
|
||||
as="p"
|
||||
weight="bold"
|
||||
align="left"
|
||||
className={styles.anyQuestions}
|
||||
>
|
||||
{t.rich("any_questions", {
|
||||
link: chunks => (
|
||||
<Link
|
||||
href="https://witapps.us/en#contact-us"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Typography size="xl" color="white">
|
||||
{t("billing_button")}
|
||||
</Typography>
|
||||
</Button>
|
||||
<div className={styles.credits}>
|
||||
<Typography as="p" weight="bold" color="white" align="left">
|
||||
{t("credits.title", {
|
||||
credits: String(0)
|
||||
})}
|
||||
</Typography>
|
||||
<Typography className={styles.creditsDescription} as="p" size="sm" color="white" align="left">
|
||||
{t("credits.description")}
|
||||
</Typography>
|
||||
</div>
|
||||
<Typography as="p" weight="bold" align="left" className={styles.anyQuestions}>
|
||||
{t.rich("any_questions", {
|
||||
link: (chunks) => (
|
||||
<Link
|
||||
href="https://witapps.us/en#contact-us"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{chunks}
|
||||
</Link>
|
||||
),
|
||||
linkText: t("any_questions_link")
|
||||
})}
|
||||
{chunks}
|
||||
</Link>
|
||||
),
|
||||
linkText: t("any_questions_link"),
|
||||
})}
|
||||
</Typography>
|
||||
<Typography
|
||||
as="p"
|
||||
align="left"
|
||||
color="secondary"
|
||||
className={styles.subscriptionUpdate}
|
||||
>
|
||||
{t.rich("subscription_update", {
|
||||
bold: chunks => (
|
||||
<Typography weight="bold" color="secondary">
|
||||
{chunks}
|
||||
</Typography>
|
||||
<Typography as="p" align="left" color="secondary" className={styles.subscriptionUpdate}>
|
||||
{t.rich("subscription_update", {
|
||||
bold: (chunks) => (
|
||||
<Typography weight="bold" color="secondary">{chunks}</Typography>
|
||||
),
|
||||
subscriptionUpdateBold: t("subscription_update_bold"),
|
||||
br: () => <br />
|
||||
})}
|
||||
</Typography>
|
||||
</div>
|
||||
)
|
||||
),
|
||||
subscriptionUpdateBold: t("subscription_update_bold"),
|
||||
br: () => <br />,
|
||||
})}
|
||||
</Typography>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Billing
|
||||
export default Billing;
|
||||
|
||||
@ -1,3 +1,42 @@
|
||||
.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 { 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 {
|
||||
onLogout: () => void;
|
||||
}
|
||||
function LogOut() {
|
||||
const t = useTranslations("Profile.log_out");
|
||||
const router = useRouter();
|
||||
|
||||
function LogOut({ onLogout }: ILogOutProps) {
|
||||
const t = useTranslations('Profile.log_out');
|
||||
const [logoutModal, setLogoutModal] = useState(false);
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={styles.button}
|
||||
onClick={onLogout}
|
||||
const handleLogout = () => {
|
||||
router.replace(ROUTES.home());
|
||||
// logout();
|
||||
};
|
||||
|
||||
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>
|
||||
</Button>
|
||||
)
|
||||
<Typography as="h4" className={styles["modal-title"]}>
|
||||
{t("modal.title")}
|
||||
</Typography>
|
||||
<p className={styles["modal-description"]}>
|
||||
{t("modal.description")}
|
||||
</p>
|
||||
<div className={styles["modal-answers"]}>
|
||||
<div className={styles["modal-answer"]} onClick={handleLogout}>
|
||||
<p className={styles["modal-answer-text"]}>
|
||||
{t("modal.log_out_button")}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className={styles["modal-answer"]}
|
||||
onClick={() => setLogoutModal(false)}
|
||||
>
|
||||
<p className={styles["modal-answer-text"]}>
|
||||
{t("modal.stay_button")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default LogOut
|
||||
export default LogOut;
|
||||
|
||||
@ -1,23 +1,23 @@
|
||||
.container {
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
&>.title {
|
||||
line-height: 32px;
|
||||
}
|
||||
& > .title {
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
&>.description {
|
||||
line-height: 20px;
|
||||
}
|
||||
& > .description {
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 100%;
|
||||
margin-top: 16px;
|
||||
}
|
||||
width: 100%;
|
||||
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 {
|
||||
title: string
|
||||
description?: string
|
||||
children?: React.ReactNode
|
||||
title: string;
|
||||
description?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
function ProfileBlock({ title, description, children }: ProfileBlockProps) {
|
||||
return (
|
||||
<section className={styles.container}>
|
||||
<header className={styles.header}>
|
||||
<Typography className={styles.title} as="h2" size="xl" weight="semiBold" align="left">
|
||||
{title}
|
||||
</Typography>
|
||||
{description &&
|
||||
<Typography className={styles.description} size="sm" align="left">
|
||||
{description}
|
||||
</Typography>
|
||||
}
|
||||
</header>
|
||||
{!!children && <div className={styles.content}>
|
||||
{children}
|
||||
</div>}
|
||||
</section>
|
||||
)
|
||||
return (
|
||||
<section className={styles.container}>
|
||||
<header className={styles.header}>
|
||||
<Typography
|
||||
className={styles.title}
|
||||
as="h2"
|
||||
size="xl"
|
||||
weight="semiBold"
|
||||
align="left"
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
{description && (
|
||||
<Typography className={styles.description} size="sm" align="left">
|
||||
{description}
|
||||
</Typography>
|
||||
)}
|
||||
</header>
|
||||
{!!children && <div className={styles.content}>{children}</div>}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default ProfileBlock
|
||||
export default ProfileBlock;
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.input {
|
||||
background-color: #f3f3f3;
|
||||
min-height: 60px;
|
||||
background-color: #f3f3f3;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.inputContainer {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
@ -1,41 +1,54 @@
|
||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||
"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() {
|
||||
const t = useTranslations('Profile');
|
||||
// const email = useSelector(selectors.selectEmail) || ""
|
||||
// const name = useSelector(selectors.selectUser)?.username || ""
|
||||
const email = "Test email"
|
||||
const name = "Test name"
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<EmailInput
|
||||
name="email"
|
||||
value={email}
|
||||
placeholder={t("profile_information.email_placeholder")}
|
||||
inputContainerClassName={styles.inputContainer}
|
||||
inputClassName={styles.input}
|
||||
onValid={() => { }}
|
||||
onInvalid={() => { }}
|
||||
readonly
|
||||
/>
|
||||
<NameInput
|
||||
value={name}
|
||||
placeholder={t("profile_information.name_placeholder")}
|
||||
inputContainerClassName={styles.inputContainer}
|
||||
inputClassName={styles.input}
|
||||
onValid={() => { }}
|
||||
onInvalid={() => { }}
|
||||
readonly
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
interface IProfileInformationProps {
|
||||
user: Promise<IUser>;
|
||||
}
|
||||
|
||||
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 LogOut } from "./LogOut/LogOut"
|
||||
export { default as ProfileBlock } from "./ProfileBlock/ProfileBlock"
|
||||
export { default as ProfileInformation } from "./ProfileInformation/ProfileInformation"
|
||||
export { default as Billing } from "./Billing/Billing";
|
||||
export { default as LogOut } from "./LogOut/LogOut";
|
||||
export { default as ProfileBlock } from "./ProfileBlock/ProfileBlock";
|
||||
export {
|
||||
default as ProfileInformation,
|
||||
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";
|
||||
|
||||
import { createContext, ReactNode,useContext, useState } from "react";
|
||||
import {
|
||||
createContext,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useContext,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
@ -8,6 +14,7 @@ import { Button, Typography } from "@/components/ui";
|
||||
import Modal from "@/components/ui/Modal/Modal";
|
||||
import { UserSubscription } from "@/entities/subscriptions/types";
|
||||
import { ROUTES } from "@/shared/constants/client-routes";
|
||||
import { useRetainingActions } from "@/stores/retainingStore";
|
||||
|
||||
import styles from "./CancelSubscriptionModalProvider.module.scss";
|
||||
|
||||
@ -15,61 +22,67 @@ type Ctx = { open: (sub: UserSubscription) => void };
|
||||
|
||||
const Context = createContext<Ctx | null>(null);
|
||||
export const useCancelSubscriptionModal = () => {
|
||||
const ctx = useContext(Context);
|
||||
if (!ctx) throw new Error("useCancelSubscriptionModal must be inside provider");
|
||||
return ctx;
|
||||
const ctx = useContext(Context);
|
||||
if (!ctx)
|
||||
throw new Error("useCancelSubscriptionModal must be inside provider");
|
||||
return ctx;
|
||||
};
|
||||
|
||||
export default function CancelSubscriptionModalProvider({
|
||||
children,
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const router = useRouter()
|
||||
const t = useTranslations("Subscriptions");
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const router = useRouter();
|
||||
const t = useTranslations("Subscriptions");
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { setCancellingSubscription } = useRetainingActions();
|
||||
|
||||
const close = () => setIsOpen(false);
|
||||
const open = (
|
||||
// _sub: UserSubscription
|
||||
) => {
|
||||
setIsOpen(true)
|
||||
};
|
||||
const close = useCallback(() => setIsOpen(false), []);
|
||||
const open = useCallback(
|
||||
(subscription: UserSubscription) => {
|
||||
setCancellingSubscription(subscription);
|
||||
setIsOpen(true);
|
||||
},
|
||||
[setCancellingSubscription]
|
||||
);
|
||||
|
||||
const handleCancel = () => {
|
||||
router.push(ROUTES.retainingFunnelCancelSubscription())
|
||||
close();
|
||||
};
|
||||
const handleCancel = useCallback(() => {
|
||||
router.push(ROUTES.retainingFunnelCancelSubscription());
|
||||
close();
|
||||
}, [router, close]);
|
||||
|
||||
return (
|
||||
<Context.Provider value={{ open }}>
|
||||
{children}
|
||||
const handleStay = useCallback(() => {
|
||||
close();
|
||||
}, [close]);
|
||||
|
||||
<Modal
|
||||
open={!!isOpen}
|
||||
onClose={close}
|
||||
isCloseButtonVisible={false}
|
||||
className={styles.overlay}
|
||||
modalClassName={styles.modal}
|
||||
>
|
||||
<Typography as="h4" className={styles.title}>
|
||||
{t("modal.title")}
|
||||
</Typography>
|
||||
<Typography as="p" className={styles.description}>
|
||||
{t("modal.description")}
|
||||
</Typography>
|
||||
return (
|
||||
<Context.Provider value={{ open }}>
|
||||
{children}
|
||||
|
||||
<div className={styles.actions}>
|
||||
<Button onClick={handleCancel}>{t("modal.cancel_button")}</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={close}
|
||||
className={styles.stayButton}
|
||||
>
|
||||
{t("modal.stay_button")}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</Context.Provider>
|
||||
);
|
||||
}
|
||||
<Modal
|
||||
open={!!isOpen}
|
||||
onClose={close}
|
||||
isCloseButtonVisible={false}
|
||||
className={styles.overlay}
|
||||
modalClassName={styles.modal}
|
||||
>
|
||||
<Typography as="h4" className={styles.title}>
|
||||
{t("modal.title")}
|
||||
</Typography>
|
||||
<Typography as="p" className={styles.description}>
|
||||
{t("modal.description")}
|
||||
</Typography>
|
||||
|
||||
<div className={styles.actions}>
|
||||
<Button 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 {
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
background-color: #f0f0f4;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
background-color: #f0f0f4;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding: 16px 8px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding: 16px 8px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.cell {
|
||||
width: 100%;
|
||||
line-height: 1.5;
|
||||
font-size: 16px;
|
||||
color: #7d8785;
|
||||
width: 100%;
|
||||
line-height: 1.5;
|
||||
font-size: 16px;
|
||||
color: #7d8785;
|
||||
|
||||
&:nth-child(2) {
|
||||
color: #090909;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
&:nth-child(2) {
|
||||
color: #090909;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { ReactNode } from "react";
|
||||
import { ReactNode, useMemo } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { Button } from "@/components/ui";
|
||||
import { Button, Typography } from "@/components/ui";
|
||||
import { Table } from "@/components/widgets";
|
||||
import { UserSubscription } from "@/entities/subscriptions/types";
|
||||
import { formatDate } from "@/shared/utils/date";
|
||||
@ -12,36 +12,68 @@ import { Currency } from "@/types";
|
||||
|
||||
import { useCancelSubscriptionModal } from "../CancelSubscriptionModalProvider/CancelSubscriptionModalProvider";
|
||||
|
||||
import styles from "./SubscriptionTable.module.scss"
|
||||
import styles from "./SubscriptionTable.module.scss";
|
||||
|
||||
interface ITableProps {
|
||||
subscription: UserSubscription;
|
||||
subscription: UserSubscription;
|
||||
}
|
||||
|
||||
export default function SubscriptionTable({ subscription }: ITableProps) {
|
||||
const t = useTranslations("Subscriptions");
|
||||
const { open } = useCancelSubscriptionModal();
|
||||
const t = useTranslations("Subscriptions");
|
||||
const { open } = useCancelSubscriptionModal();
|
||||
|
||||
const tableData: ReactNode[][] = [
|
||||
[t("table.subscription_type"), t(`table.subscription_type_value.${subscription.subscriptionType}`)],
|
||||
[t("table.subscription_status"), t(`table.subscription_status_value.${subscription.subscriptionStatus}`, {
|
||||
date: formatDate(subscription.cancellationDate) || ""
|
||||
})],
|
||||
[t("table.billing_period"), t(`table.billing_period_value.${subscription.billingPeriod}`)],
|
||||
[t("table.last_payment_on"), formatDate(subscription.lastPaymentOn)],
|
||||
[t("table.renewal_date"), formatDate(subscription.renewalDate)],
|
||||
[t("table.renewal_amount"), getFormattedPrice(subscription.renewalAmount, Currency[subscription.currency])],
|
||||
]
|
||||
const tableData: ReactNode[][] = useMemo(() => {
|
||||
const data: ReactNode[][] = [
|
||||
[
|
||||
t("table.subscription_type"),
|
||||
t(`table.subscription_type_value.${subscription.subscriptionType}`),
|
||||
],
|
||||
[
|
||||
t("table.subscription_status"),
|
||||
t(
|
||||
`table.subscription_status_value.${subscription.subscriptionStatus}`,
|
||||
{
|
||||
date: formatDate(subscription.cancellationDate) || "",
|
||||
}
|
||||
),
|
||||
],
|
||||
[
|
||||
t("table.billing_period"),
|
||||
t(`table.billing_period_value.${subscription.billingPeriod}`),
|
||||
],
|
||||
[t("table.last_payment_on"), formatDate(subscription.lastPaymentOn)],
|
||||
[t("table.renewal_date"), formatDate(subscription.renewalDate)],
|
||||
[
|
||||
t("table.renewal_amount"),
|
||||
getFormattedPrice(
|
||||
subscription.renewalAmount,
|
||||
Currency[subscription.currency]
|
||||
),
|
||||
],
|
||||
];
|
||||
|
||||
if (subscription.subscriptionStatus === "ACTIVE") {
|
||||
tableData.push([
|
||||
<Button key={"cancel-subscription"} className={styles.buttonCancel} onClick={() => open(subscription)}>
|
||||
{t("table.cancel_subscription")}
|
||||
</Button>
|
||||
])
|
||||
data.push([
|
||||
<Button
|
||||
key={"cancel-subscription"}
|
||||
className={styles.buttonCancel}
|
||||
onClick={() => open(subscription)}
|
||||
>
|
||||
<Typography color="white">
|
||||
{t("table.cancel_subscription")}
|
||||
</Typography>
|
||||
</Button>,
|
||||
]);
|
||||
}
|
||||
|
||||
return (
|
||||
<Table data={tableData} />
|
||||
)
|
||||
}
|
||||
return data;
|
||||
}, [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 { getTranslations } from "next-intl/server";
|
||||
|
||||
import { Typography } from "@/components/ui";
|
||||
import { Skeleton } from "@/components/ui";
|
||||
import { Skeleton, Typography } from "@/components/ui";
|
||||
import { UserSubscription } from "@/entities/subscriptions/types";
|
||||
|
||||
import SubscriptionTable from "../SubscriptionTable/SubscriptionTable";
|
||||
|
||||
import styles from "./SubscriptionsList.module.scss"
|
||||
import styles from "./SubscriptionsList.module.scss";
|
||||
|
||||
export default function SubscriptionsList(
|
||||
{ promise }: { promise: Promise<UserSubscription[]> }
|
||||
) {
|
||||
const t = use(getTranslations("Subscriptions"));
|
||||
export default function SubscriptionsList({
|
||||
promise,
|
||||
}: {
|
||||
promise: Promise<UserSubscription[]>;
|
||||
}) {
|
||||
const t = use(getTranslations("Subscriptions"));
|
||||
|
||||
const subscriptions = use(promise);
|
||||
const subscriptions = use(promise);
|
||||
|
||||
if (subscriptions?.length === 0) {
|
||||
return <div className={styles.container}>
|
||||
<Typography as="h1" className={styles.title}>{t("title")}</Typography>
|
||||
<Typography as="p" className={styles.description}>{t("no_subscriptions")}</Typography>
|
||||
</div>
|
||||
}
|
||||
if (subscriptions?.length === 0) {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Typography as="h1" className={styles.title}>
|
||||
{t("title")}
|
||||
</Typography>
|
||||
<Typography as="p" className={styles.description}>
|
||||
{t("no_subscriptions")}
|
||||
</Typography>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <>
|
||||
{subscriptions.map((subscription) => {
|
||||
return <SubscriptionTable subscription={subscription} key={subscription.id} />
|
||||
})}
|
||||
return (
|
||||
<>
|
||||
{subscriptions.map(subscription => {
|
||||
return (
|
||||
<SubscriptionTable
|
||||
subscription={subscription}
|
||||
key={subscription.id}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{/* <SubscriptionTable subscription={subscriptions[0]} key={subscriptions[0].id} /> */}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function SubscriptionsListSkeleton() {
|
||||
return (
|
||||
<Skeleton style={{ height: "300px" }} className={styles.skeleton} />
|
||||
)
|
||||
}
|
||||
return <Skeleton style={{ height: "300px" }} className={styles.skeleton} />;
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
|
||||
export { default as CancelSubscriptionModalProvider } from "./CancelSubscriptionModalProvider/CancelSubscriptionModalProvider"
|
||||
export { default as SubscriptionsList, SubscriptionsListSkeleton } from "./SubscriptionsList/SubscriptionsList"
|
||||
export { default as CancelSubscriptionModalProvider } from "./CancelSubscriptionModalProvider/CancelSubscriptionModalProvider";
|
||||
export {
|
||||
default as SubscriptionsList,
|
||||
SubscriptionsListSkeleton,
|
||||
} from "./SubscriptionsList/SubscriptionsList";
|
||||
|
||||
@ -1,22 +1,25 @@
|
||||
.button {
|
||||
min-height: 71px;
|
||||
border-radius: 8px;
|
||||
font-size: 28px;
|
||||
font-weight: normal;
|
||||
background: #F1F1F1;
|
||||
background-blend-mode: color;
|
||||
box-shadow: 2px 5px 2.5px #00000025;
|
||||
color: #121620;
|
||||
transition: background 0.3s ease, color 0.3s ease;
|
||||
will-change: background, color;
|
||||
padding: 25px;
|
||||
line-height: 1;
|
||||
word-break: break-word;
|
||||
min-width: none;
|
||||
min-height: 71px;
|
||||
border-radius: 8px;
|
||||
font-size: 28px;
|
||||
font-weight: normal;
|
||||
background: #f1f1f1;
|
||||
background-blend-mode: color;
|
||||
box-shadow: 2px 5px 2.5px #00000025;
|
||||
color: #121620;
|
||||
transition:
|
||||
background 0.3s ease,
|
||||
color 0.3s ease;
|
||||
will-change: background, color;
|
||||
padding: 25px;
|
||||
line-height: 1;
|
||||
word-break: break-word;
|
||||
min-width: none;
|
||||
|
||||
&.active {
|
||||
background: linear-gradient(to right, #057dd4 23%, #224e90 74%, #0c6bc3 94%),
|
||||
linear-gradient(-45deg, #3a617120 9%, #21212120 72%, #21895120 96%);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
&.active {
|
||||
background:
|
||||
linear-gradient(to right, #057dd4 23%, #224e90 74%, #0c6bc3 94%),
|
||||
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";
|
||||
|
||||
interface ButtonProps extends MainButtonProps {
|
||||
active?: boolean;
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
function Button(props: ButtonProps) {
|
||||
const { active, ...buttonProps } = props;
|
||||
return (
|
||||
<MainButton {...buttonProps} className={`${styles.button} ${props.className} ${active ? styles.active : ""}`}>
|
||||
{props.children}
|
||||
</MainButton>
|
||||
);
|
||||
const { active, ...buttonProps } = props;
|
||||
return (
|
||||
<MainButton
|
||||
{...buttonProps}
|
||||
className={`${styles.button} ${props.className} ${active ? styles.active : ""}`}
|
||||
>
|
||||
{props.children}
|
||||
</MainButton>
|
||||
);
|
||||
}
|
||||
|
||||
export default Button;
|
||||
|
||||
@ -1,45 +1,75 @@
|
||||
|
||||
|
||||
interface CheckMarkProps {
|
||||
active: boolean;
|
||||
className?: string;
|
||||
active: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function CheckMark({ active, className = "" }: CheckMarkProps) {
|
||||
return (
|
||||
<>
|
||||
{active &&
|
||||
<svg className={className} width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clipPath="url(#clip0_49_4129)">
|
||||
<g clipPath="url(#clip1_49_4129)">
|
||||
<path d="M25 0H5C3.67392 0 2.40215 0.526784 1.46447 1.46447C0.526784 2.40215 0 3.67392 0 5V25C0 26.3261 0.526784 27.5979 1.46447 28.5355C2.40215 29.4732 3.67392 30 5 30H25C26.3261 30 27.5979 29.4732 28.5355 28.5355C29.4732 27.5979 30 26.3261 30 25V5C30 3.67392 29.4732 2.40215 28.5355 1.46447C27.5979 0.526784 26.3261 0 25 0ZM22.1667 11.0167L14.55 21.0167C14.3947 21.2184 14.1953 21.3818 13.9671 21.4945C13.7389 21.6072 13.4879 21.6661 13.2333 21.6667C12.9802 21.668 12.73 21.6117 12.5019 21.502C12.2738 21.3922 12.0736 21.232 11.9167 21.0333L7.85 15.85C7.71539 15.6771 7.61616 15.4794 7.55797 15.2681C7.49978 15.0569 7.48377 14.8362 7.51086 14.6188C7.53794 14.4013 7.60759 14.1913 7.71582 14.0008C7.82406 13.8103 7.96876 13.6429 8.14167 13.5083C8.49087 13.2365 8.93376 13.1145 9.37291 13.1692C9.59035 13.1963 9.80033 13.2659 9.99086 13.3742C10.1814 13.4824 10.3487 13.6271 10.4833 13.8L13.2 17.2667L19.5 8.93333C19.6335 8.75824 19.8002 8.61115 19.9906 8.50048C20.1809 8.3898 20.3912 8.3177 20.6094 8.2883C20.8276 8.25889 21.0495 8.27276 21.2624 8.3291C21.4752 8.38544 21.6749 8.48316 21.85 8.61667C22.0251 8.75018 22.1722 8.91687 22.2829 9.10722C22.3935 9.29758 22.4656 9.50786 22.495 9.72608C22.5244 9.9443 22.5106 10.1662 22.4542 10.379C22.3979 10.5919 22.3002 10.7916 22.1667 10.9667V11.0167Z" fill="#1172AC" />
|
||||
</g>
|
||||
</g>
|
||||
<rect x="1" y="1" width="28" height="28" rx="14" stroke="#1172AC" strokeWidth="2" />
|
||||
<defs>
|
||||
<clipPath id="clip0_49_4129">
|
||||
<rect width="30" height="30" rx="15" fill="white" />
|
||||
</clipPath>
|
||||
<clipPath id="clip1_49_4129">
|
||||
<rect width="30" height="30" rx="15" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
}
|
||||
{!active &&
|
||||
<svg className={className} width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clipPath="url(#clip0_49_4525)">
|
||||
</g>
|
||||
<rect x="1" y="1" width="28" height="28" rx="14" stroke="#1172AC" strokeWidth="2" />
|
||||
<defs>
|
||||
<clipPath id="clip0_49_4525">
|
||||
<rect width="30" height="30" rx="15" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
}
|
||||
</>
|
||||
)
|
||||
return (
|
||||
<>
|
||||
{active && (
|
||||
<svg
|
||||
className={className}
|
||||
width="30"
|
||||
height="30"
|
||||
viewBox="0 0 30 30"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clipPath="url(#clip0_49_4129)">
|
||||
<g clipPath="url(#clip1_49_4129)">
|
||||
<path
|
||||
d="M25 0H5C3.67392 0 2.40215 0.526784 1.46447 1.46447C0.526784 2.40215 0 3.67392 0 5V25C0 26.3261 0.526784 27.5979 1.46447 28.5355C2.40215 29.4732 3.67392 30 5 30H25C26.3261 30 27.5979 29.4732 28.5355 28.5355C29.4732 27.5979 30 26.3261 30 25V5C30 3.67392 29.4732 2.40215 28.5355 1.46447C27.5979 0.526784 26.3261 0 25 0ZM22.1667 11.0167L14.55 21.0167C14.3947 21.2184 14.1953 21.3818 13.9671 21.4945C13.7389 21.6072 13.4879 21.6661 13.2333 21.6667C12.9802 21.668 12.73 21.6117 12.5019 21.502C12.2738 21.3922 12.0736 21.232 11.9167 21.0333L7.85 15.85C7.71539 15.6771 7.61616 15.4794 7.55797 15.2681C7.49978 15.0569 7.48377 14.8362 7.51086 14.6188C7.53794 14.4013 7.60759 14.1913 7.71582 14.0008C7.82406 13.8103 7.96876 13.6429 8.14167 13.5083C8.49087 13.2365 8.93376 13.1145 9.37291 13.1692C9.59035 13.1963 9.80033 13.2659 9.99086 13.3742C10.1814 13.4824 10.3487 13.6271 10.4833 13.8L13.2 17.2667L19.5 8.93333C19.6335 8.75824 19.8002 8.61115 19.9906 8.50048C20.1809 8.3898 20.3912 8.3177 20.6094 8.2883C20.8276 8.25889 21.0495 8.27276 21.2624 8.3291C21.4752 8.38544 21.6749 8.48316 21.85 8.61667C22.0251 8.75018 22.1722 8.91687 22.2829 9.10722C22.3935 9.29758 22.4656 9.50786 22.495 9.72608C22.5244 9.9443 22.5106 10.1662 22.4542 10.379C22.3979 10.5919 22.3002 10.7916 22.1667 10.9667V11.0167Z"
|
||||
fill="#1172AC"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<rect
|
||||
x="1"
|
||||
y="1"
|
||||
width="28"
|
||||
height="28"
|
||||
rx="14"
|
||||
stroke="#1172AC"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<defs>
|
||||
<clipPath id="clip0_49_4129">
|
||||
<rect width="30" height="30" rx="15" fill="white" />
|
||||
</clipPath>
|
||||
<clipPath id="clip1_49_4129">
|
||||
<rect width="30" height="30" rx="15" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
)}
|
||||
{!active && (
|
||||
<svg
|
||||
className={className}
|
||||
width="30"
|
||||
height="30"
|
||||
viewBox="0 0 30 30"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clipPath="url(#clip0_49_4525)" />
|
||||
<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 {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
border-radius: 14px;
|
||||
padding: 72px 20px 65px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
border-radius: 14px;
|
||||
padding: 72px 20px 65px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
box-shadow: 0px 0px 10.2px 0px rgba(0, 0, 0, 0.25);
|
||||
border: 4px solid transparent;
|
||||
cursor: pointer;
|
||||
|
||||
&.active {
|
||||
border: 4px solid rgba(17, 114, 172, 1);
|
||||
}
|
||||
|
||||
& > .checkMark {
|
||||
position: absolute;
|
||||
top: 21px;
|
||||
right: 14px;
|
||||
}
|
||||
|
||||
& > .title {
|
||||
margin: 0;
|
||||
margin-top: 4px;
|
||||
color: #323232;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
& > .description {
|
||||
margin-top: 14px;
|
||||
color: #323232;
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
text-align: left;
|
||||
padding-inline: 7px;
|
||||
}
|
||||
|
||||
& > .priceContainer {
|
||||
display: flex;
|
||||
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;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin-top: 33px;
|
||||
|
||||
&.active {
|
||||
border: 4px solid rgba(17, 114, 172, 1)
|
||||
& > .oldPrice {
|
||||
color: #c4c4c4;
|
||||
font-size: 28px;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
&>.checkMark {
|
||||
position: absolute;
|
||||
top: 21px;
|
||||
right: 14px;
|
||||
& > .newPrice {
|
||||
color: #000;
|
||||
font-size: 36px;
|
||||
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 "..";
|
||||
|
||||
interface OfferProps {
|
||||
title?: string | React.ReactNode;
|
||||
description?: string;
|
||||
oldPrice?: number | string;
|
||||
newPrice?: number | string;
|
||||
active?: boolean;
|
||||
image?: React.ReactNode;
|
||||
className?: string;
|
||||
classNameTitle?: string;
|
||||
onClick?: () => void;
|
||||
title?: string | React.ReactNode;
|
||||
description?: string;
|
||||
oldPrice?: number | string;
|
||||
newPrice?: number | string;
|
||||
active?: boolean;
|
||||
image?: React.ReactNode;
|
||||
className?: string;
|
||||
classNameTitle?: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
function Offer({
|
||||
title,
|
||||
description,
|
||||
oldPrice,
|
||||
newPrice,
|
||||
active = false,
|
||||
onClick,
|
||||
image,
|
||||
className = "",
|
||||
classNameTitle = ""
|
||||
title,
|
||||
description,
|
||||
oldPrice,
|
||||
newPrice,
|
||||
active = false,
|
||||
onClick,
|
||||
image,
|
||||
className = "",
|
||||
classNameTitle = "",
|
||||
}: OfferProps) {
|
||||
// const currency = useSelector(selectors.selectCurrency);
|
||||
const currency = Currency.USD;
|
||||
// const currency = useSelector(selectors.selectCurrency);
|
||||
const currency = Currency.USD;
|
||||
|
||||
return (
|
||||
<div className={clsx(styles.container, active && styles.active, className)} onClick={onClick}>
|
||||
<CheckMark active={active} className={styles.checkMark} />
|
||||
return (
|
||||
<div
|
||||
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)}>
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography as="p" className={styles.description}>
|
||||
{description}
|
||||
</Typography>
|
||||
<div className={styles.priceContainer}>
|
||||
<Typography weight="bold" className={styles.oldPrice}>
|
||||
{getFormattedPrice(Number(oldPrice), currency)}
|
||||
</Typography>
|
||||
<Typography weight="bold" className={styles.newPrice}>
|
||||
{getFormattedPrice(Number(newPrice), currency)}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
<Typography
|
||||
as="h3"
|
||||
weight="bold"
|
||||
className={clsx(styles.title, classNameTitle)}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography as="p" className={styles.description}>
|
||||
{description}
|
||||
</Typography>
|
||||
<div className={styles.priceContainer}>
|
||||
<Typography weight="bold" className={styles.oldPrice}>
|
||||
{getFormattedPrice(Number(oldPrice), currency)}
|
||||
</Typography>
|
||||
<Typography weight="bold" className={styles.newPrice}>
|
||||
{getFormattedPrice(Number(newPrice), currency)}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Offer
|
||||
export default Offer;
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
.stepper-bar {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
@ -4,41 +4,40 @@ import { usePathname } from "next/navigation";
|
||||
|
||||
import { StepperBar } from "@/components/layout";
|
||||
|
||||
import styles from "./RetainingStepper.module.scss"
|
||||
|
||||
import styles from "./RetainingStepper.module.scss";
|
||||
|
||||
export default function RetainingStepper({
|
||||
stepperRoutes,
|
||||
stepperRoutes,
|
||||
}: {
|
||||
stepperRoutes: string[];
|
||||
stepperRoutes: string[];
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
const pathname = usePathname();
|
||||
|
||||
const getCurrentStep = () => {
|
||||
// if ([
|
||||
// ROUTES.retainingFunnelPlanCancelled(),
|
||||
// ROUTES.retainingFunnelSubscriptionStopped(),
|
||||
// ].some(route => location.pathname.includes(route))) {
|
||||
// return stepperRoutes[retainingFunnel].length;
|
||||
// }
|
||||
let index = 0;
|
||||
for (const route of stepperRoutes) {
|
||||
if (pathname.includes(route)) {
|
||||
return index + 1;
|
||||
}
|
||||
index++;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
const getCurrentStep = () => {
|
||||
// if ([
|
||||
// ROUTES.retainingFunnelPlanCancelled(),
|
||||
// ROUTES.retainingFunnelSubscriptionStopped(),
|
||||
// ].some(route => location.pathname.includes(route))) {
|
||||
// return stepperRoutes[retainingFunnel].length;
|
||||
// }
|
||||
let index = 0;
|
||||
for (const route of stepperRoutes) {
|
||||
if (pathname.includes(route)) {
|
||||
return index + 1;
|
||||
}
|
||||
index++;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
// логика выбора шага по pathname
|
||||
return (
|
||||
<StepperBar
|
||||
length={stepperRoutes.length}
|
||||
currentStep={getCurrentStep()}
|
||||
// color={darkTheme ? "#B2BCFF" : "#353E75"}
|
||||
color={"#353E75"}
|
||||
className={styles["stepper-bar"]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
// логика выбора шага по pathname
|
||||
return (
|
||||
<StepperBar
|
||||
length={stepperRoutes.length}
|
||||
currentStep={getCurrentStep()}
|
||||
// color={darkTheme ? "#B2BCFF" : "#353E75"}
|
||||
color={"#353E75"}
|
||||
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