diff --git a/eslint.config.mjs b/eslint.config.mjs index c439e77..c053932 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -51,7 +51,7 @@ const eslintConfig = [ ["^\\u0000"], // side-effects ["^react", "^next", "^@?\\w"], // пакеты ["^@/"], // алиасы проекта - ["^\\.\\.(?!/?$)", "^\\./(?=.*/)", "^\\./?$"], // относительные + ["^\\.\\.?(?:/|$)"], // относительные импорты (включая "..") ["^.+\\.module\\.(css|scss)$"], // модули стилей ], }, diff --git a/messages/de.json b/messages/de.json index 053cc8d..112ccd3 100644 --- a/messages/de.json +++ b/messages/de.json @@ -37,6 +37,7 @@ }, "Subscriptions": { "title": "Manage my subscriptions", + "success_cancel_message": "Your subscription has been cancelled successfully", "modal": { "title": "Are you sure you want to cancel your subscription?", "description": "Are you sure you want to cancel your subscription?", @@ -54,7 +55,8 @@ "subscription_status": "Subscription Status", "subscription_status_value": { "ACTIVE": "Active", - "CANCELLED": "Cancels on {date}" + "CANCELLED": "Cancels on {date}", + "PAST_DUE": "Past due" }, "billing_period": "Billing Period", "billing_period_value": { @@ -73,40 +75,41 @@ "try_again": "Try again" }, "CancelSubscription": { - "title": "Жаль, что вы уходите…", - "description": "Многие уходят именно в тот момент, когда астролог начинает видеть поворотную точку в их истории.



Позвольте задать пару вопросов, чтобы сделать наш сервис лучше - и, возможно, предложить решение, которое больше подходит именно вам.", - "stay_button": "Остаться и уменьшить мой план на 50%", - "cancel_button": "Отменить" + "title": "Sad to see you go…", + "description": "Many leave exactly at the moment when the astrologist begins to see a turning point in their history.



Allow us to ask a few questions to make our service better - and, possibly, offer a solution that is more suitable for you.", + "stay_button": "Stay and reduce my plan by 50%", + "cancel_button": "Cancel", + "error_message": "Something went wrong. Please try again later." }, "Stay50Done": { - "title": "Мы ценим твой выбор!", + "title": "We value your choice!", "descriptions": { - "1": "План успешно изменен" + "1": "Plan successfully changed" }, - "button": "Готово" + "button": "Done" }, "AppreciateChoice": { - "title": "Мы ценим твой выбор!", + "title": "We value your choice!", "descriptions": { - "1": "Подбираем оптимальное решение...", - "2": "Составляем персонализированный опрос...", - "3": "Формируем выгодное предложение..." + "1": "Selecting the optimal solution...", + "2": "Creating a personalized survey...", + "3": "Forming a profitable offer..." }, "button": "Next" }, "WhatReason": { - "title": "Что стало причиной?", + "title": "What became the reason?", "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": "Другое" + "no_promised_result": "Did not receive the promised result", + "too_expensive": "Too expensive", + "high_auto_payment": "The cost of automatic payment is too high", + "unexpected_fee": "I did not expect an additional charge", + "want_pause": "I want to pause", + "service_not_as_expected": "The service was not as expected", + "found_alternative": "Found an alternative", + "dislike_app": "I did not like the app", + "hard_to_navigate": "It is difficult to navigate in the app", + "other": "Other" } }, "Payment": { @@ -120,57 +123,275 @@ } }, "SecondChance": { - "title": "Дайте нам второй шанс и получи самый лучший план БЕСПЛАТНО", + "title": "Give us a second chance and get the best plan for free", "offers": { "1": { - "title": "Бесплатный план на

1 месяц", - "description": "Используй весь потенциал AURA и даже больше.", + "title": "Free plan for 1 month", + "description": "Use the full potential of WITLAB and even more.", "old-price": "1900", "new-price": "0" }, "2": { - "title": "Бесплатный премиальный план", - "description": "Бесплатная 30 мин консультация с премиальным Эдвайзером", + "title": "Free premium plan", + "description": "Free 30 min consultation with a premium advisor", "old-price": "4900", "new-price": "0" } }, - "get_offer": "Получить бесплатный план", - "cancel": "Отменить" + "get_offer": "Get free plan", + "cancel": "Cancel", + "error_message": "Something went wrong. Please try again later." }, "ChangeMind": { - "title": "Что может изменить твое мнение?", + "title": "What could change your mind?", "answers": { - "more_chat_time": "Больше времени в чатах", - "more_personal_reports": "Больше персонализированных отчетов", - "individual_plan": "Индивидуальный план", - "other": "Другое" + "more_chat_time": "More time in chats", + "more_personal_reports": "More personalized reports", + "individual_plan": "Individual plan", + "other": "Other" } }, "StopFor30Days": { - "title": "Остановите подписку на тридцать дней. Никаких списаний.", - "stop": "Остановить", - "cancel": "Отменить" + "title": "Pause the subscription for 30 days. No charges.", + "stop": "Pause", + "cancel": "Cancel", + "error_message": "Something went wrong. Please try again later." }, "CancellationOfSubscription": { - "title": "Подписка аннулируется!", - "description": "Чтобы отменить подписку, нажмите “Подтвердить мои действия”", + "title": "Subscription cancelled!", + "description": "To cancel the subscription, click “Confirm my actions”", "offer": { - "title": "Бесплатный 2-месячный план", + "title": "Free 2-month plan", "old-price": "9900", "new-price": "0" }, - "offer_button": "Применить", - "cancel_button": "Я подтверждаю свои действия" + "offer_button": "Apply", + "cancel_button": "Confirm my actions", + "error_message": "Something went wrong. Please try again later.", + "toast_message": "Your subscription will be cancelled!" }, "PlanCancelled": { - "title": "Стандартный план Отменен!", + "title": "Standard plan cancelled!", "icon": "🥳", - "description": "Выполнен переход на бесплатный тридцатидневный план ", - "button": "Готово" + "description": "Completed transition to a free 30-day plan", + "button": "Done" }, "SubscriptionStopped": { - "title": "Подписка остановлена успешно!", + "title": "Subscription stopped successfully!", "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." + }, + "Meditation": { + "title": "Stop and breathe to help you relax and focus on what really matters.", + "subtitle": "Breathing practice will help improve your aura. Breath in the positive energy, breathe out the negative...", + "button": "BEGIN" + }, + "MeditationResult": { + "breath_relax": "Breath & Relax", + "breath_in": "Breath in", + "breath_out": "Breath out" + }, + "ActionFieldsForm": { + "required_field": "This field is required" + }, + "AdditionalPurchases": { + "caution": { + "title": "Caution!", + "description": "To prevent double charges please don`t close the page and don`t go back." + }, + "add-consultant": { + "title": "More for you", + "exclusive_offer": "Exclusive offer recommended for you to achieve your goals faster", + "your_unique_consultation": "Your unique individual consultation", + "30-minute": "30-minute private consultation with an expert", + "unlock_profound": "Unlock profound insights into your personality, relationships, career trajectory, and life's pivotal moments through astrology, empowering you to make informed decisions and achieve greater fulfillment.", + "one_time_price_offer": "One time price offer: ", + "choose_from": "Choose from 80+ experts astrologers.", + "original_price": "Original price: {oldPrice} ", + "save": "Save {discount}%", + "get_my_consultation": "Get my consultation", + "skip_this_offer": "Skip this offer", + "payment_error": "Something went wrong. Please try again later." + }, + "add-guides": { + "title": "Choose your sign-up offer 🔥", + "subtitle": "Available only now", + "description": "*You will be charged for the add-on services or offers selected at the time of purchase. This is a non-recuring payment.", + "button": "Get my copy", + "payment_error": "Something went wrong. Please try again later.", + "select_product_error": "Please select a product", + "skip_offer": "Skip offer", + + "products": { + "main_ultra_pack": { + "title": "ULTRA PACK", + "discount": "{discount}% OFF", + "subtitle": "(3 in 1 + 2 secret bonus reading)", + "price": " ( regular price )", + "emoji": "star_struck.webp" + }, + "main_numerology_analysis": { + "title": "NUMEROLOGY ANALYSIS", + "discount": "{discount}% OFF", + "price": " ( was )", + "emoji": "input_numbers.webp" + }, + "main_tarot_reading": { + "title": "TAROT READING", + "discount": "{discount}% OFF", + "price": " ( was )", + "emoji": "sunset.webp" + }, + "main_palmistry_guide": { + "title": "PALMISTRY GUIDE", + "discount": "{discount}% OFF", + "price": " ( was )", + "emoji": "rised_hand.webp" + }, + "main_money_reading": { + "title": "MONEY READING", + "discount": "{discount}% OFF", + "price": " ( was )", + "emoji": "money.png" + }, + "main_skip_offer": { + "title": "SKIP OFFER", + "discount": "{discount}% OFF", + "price": " ( was )", + "subtitle": "You are missing out on both readings", + "emoji": "rised_hand.webp" + } + } + } + }, + "Chat": { + "header": { + "title": "Chat", + "search_placeholder": "Type a Chat..." + }, + "new_messages": "New messages", + "view_all": "View All ({count})", + "hide_all": "Hide", + "typing": "is typing...", + "voice_message": "Voice message", + "photo": "Photo", + "correspondence_started": { + "title": "Correspondence started", + "pinned_chats": "Pinned Chats" + }, + "message_input_placeholder": "Type a message...", + "message_image_fallback": "Failed to load image", + "payment_error": "Something went wrong. Please try again later." + }, + "RefillTimerModal": { + "title": "Refill credits in 1 click", + "subtitle": " {newCredits} credits for {price}", + "button": "Get Credits", + "dont_want_to_continue": "I don't want to continue chatting", + "auto_refill_description": "Auto-refill keeps your readings uninterrupted. After using {afterCredits} credits, we'll automatically add {addCredits} more credits ({minutes} minutes of consultation) for a one-time payment. No recurring charges.", + "seconds": "seconds" + }, + "RefillOptionsModal": { + "header": { + "title": "{name} is waiting!", + "subtitle": "{name} is waiting for you in our chat..." + }, + "button": "Continue", + "refill_option": { + "popular": "POPULAR", + "credits": "{credits} credits", + "bonus": "+

credits", + "price": "{price}" + }, + "benefits": { + "1": { + "title": "Instant Access", + "description": "Continue chatting immediately" + }, + "2": { + "title": "Secure Payment", + "description": "256-bit SSL encryption" + }, + "3": { + "title": "Cancel Anytime", + "description": "No long-term commitment" + }, + "4": { + "title": "Best Value", + "description": "Most credits per dollar" + } + } + }, + "EmailMarketing": { + "Compatibility": { + "v1": { + "Landing": { + "title": "Special Offer!", + "description": "Everything for free. Trial include!", + "title-hey": "Hey, Sun 👋", + "description-hey": "Your wellness and happiness are key for us!", + "personalized-plan-title": "So we decided to give you your personalized plan and access to the trial of our app", + "personalized-plan-free": "FOR FREE!", + "title-highlights": "Highlights of your plan:", + "personal-astrologer-advice": "1:1 Advice from your personal astrologer", + "finding-compatible-partner": "Finding the most compatible partner", + "relationship-patterns-insights": "Insights into your relationship patterns, and emotional and sexual needs", + "better-understanding-yourself": "Better understanding of yourself and your needs", + "old-price-label": "OLD PRICE", + "new-price-label": "NEW PRICE", + "plan-includes-title": "Your plan also includes:", + "plan-includes-unlimited-horoscopes": "Unlimited daily / weekly / monthly / yearly horoscopes", + "plan-includes-astrology-lessons": "Astrology lessons and articles inside the app", + "plan-includes-lifestyle-calendars": "Beauty / health / travel and more calendars", + "plan-includes-compatibility-check": "Compatibility check with zodiac signs inside the app", + "review-1": "Horoscope tells realistic facts about day to day life, which can be easily relatable. It shows direction.", + "review-2": "It makes me feel safe, seeing, warm and smart.", + "review-3": "I love that we have the \"my profile\" option. I love learning about myself.", + "circular-text-image": "circular-text-en.png", + "statistics-banner-text": "Last week alone got this relationship guide", + "statistics-banner-count": "{count}+ people", + "money-back-guarantee-title": "Money-back guarantee", + "money-back-guarantee-text": "We are convinced that we will help you get a deeper understanding of your partner and how you can improve your relationship. After all of our stellar customer reviews, we are ready to return your money if you feel that this report doesn't provide any value. Find more about applicable limitations in our Money-back policy.", + "guaranteed-security-payments": "Guaranteed security payments", + "button-continue": "Continue", + "old-price": "up to {oldPrice}" + }, + "SpecialOffer": { + "title": "Special Offer!", + "start-trial": "Start your {days}-day trial", + "cancel-anytime": "No pressure. Cancel anytime", + "policy": "By continuing you agree that if you don't cancel prior to the end of the {days}-days trial, you will automatically be charged {price} every 2 weeks until you cancel in settings. Learn more about cancellation and refund policy in Subscription terms", + "button-continue": "Continue", + "pricing-summary-total-today": "Total today:", + "pricing-summary-code-applied": "Code applied!", + "pricing-summary-cost-after-trial": "Your cost per 2 weeks after trial", + "pricing-summary-trial-description": "You will be charged only {totalToday} for your {trialDuration}-day trial. Subscription renews automatically until cancelled. You can cancel at any time before the end of the trial.", + "reserved-for": "Reserved for {time}" + } + } + } } } diff --git a/messages/en.json b/messages/en.json index 4f039d7..112ccd3 100644 --- a/messages/en.json +++ b/messages/en.json @@ -75,41 +75,41 @@ "try_again": "Try again" }, "CancelSubscription": { - "title": "Жаль, что вы уходите…", - "description": "Многие уходят именно в тот момент, когда астролог начинает видеть поворотную точку в их истории.



Позвольте задать пару вопросов, чтобы сделать наш сервис лучше - и, возможно, предложить решение, которое больше подходит именно вам.", - "stay_button": "Остаться и уменьшить мой план на 50%", - "cancel_button": "Отменить", + "title": "Sad to see you go…", + "description": "Many leave exactly at the moment when the astrologist begins to see a turning point in their history.



Allow us to ask a few questions to make our service better - and, possibly, offer a solution that is more suitable for you.", + "stay_button": "Stay and reduce my plan by 50%", + "cancel_button": "Cancel", "error_message": "Something went wrong. Please try again later." }, "Stay50Done": { - "title": "Мы ценим твой выбор!", + "title": "We value your choice!", "descriptions": { - "1": "План успешно изменен" + "1": "Plan successfully changed" }, - "button": "Готово" + "button": "Done" }, "AppreciateChoice": { - "title": "Мы ценим твой выбор!", + "title": "We value your choice!", "descriptions": { - "1": "Подбираем оптимальное решение...", - "2": "Составляем персонализированный опрос...", - "3": "Формируем выгодное предложение..." + "1": "Selecting the optimal solution...", + "2": "Creating a personalized survey...", + "3": "Forming a profitable offer..." }, "button": "Next" }, "WhatReason": { - "title": "Что стало причиной?", + "title": "What became the reason?", "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": "Другое" + "no_promised_result": "Did not receive the promised result", + "too_expensive": "Too expensive", + "high_auto_payment": "The cost of automatic payment is too high", + "unexpected_fee": "I did not expect an additional charge", + "want_pause": "I want to pause", + "service_not_as_expected": "The service was not as expected", + "found_alternative": "Found an alternative", + "dislike_app": "I did not like the app", + "hard_to_navigate": "It is difficult to navigate in the app", + "other": "Other" } }, "Payment": { @@ -123,61 +123,61 @@ } }, "SecondChance": { - "title": "Дайте нам второй шанс и получи самый лучший план БЕСПЛАТНО", + "title": "Give us a second chance and get the best plan for free", "offers": { "1": { - "title": "Бесплатный план на

1 месяц", - "description": "Используй весь потенциал AURA и даже больше.", + "title": "Free plan for 1 month", + "description": "Use the full potential of WITLAB and even more.", "old-price": "1900", "new-price": "0" }, "2": { - "title": "Бесплатный премиальный план", - "description": "Бесплатная 30 мин консультация с премиальным Эдвайзером", + "title": "Free premium plan", + "description": "Free 30 min consultation with a premium advisor", "old-price": "4900", "new-price": "0" } }, - "get_offer": "Получить бесплатный план", - "cancel": "Отменить", + "get_offer": "Get free plan", + "cancel": "Cancel", "error_message": "Something went wrong. Please try again later." }, "ChangeMind": { - "title": "Что может изменить твое мнение?", + "title": "What could change your mind?", "answers": { - "more_chat_time": "Больше времени в чатах", - "more_personal_reports": "Больше персонализированных отчетов", - "individual_plan": "Индивидуальный план", - "other": "Другое" + "more_chat_time": "More time in chats", + "more_personal_reports": "More personalized reports", + "individual_plan": "Individual plan", + "other": "Other" } }, "StopFor30Days": { - "title": "Остановите подписку на тридцать дней. Никаких списаний.", - "stop": "Остановить", - "cancel": "Отменить", + "title": "Pause the subscription for 30 days. No charges.", + "stop": "Pause", + "cancel": "Cancel", "error_message": "Something went wrong. Please try again later." }, "CancellationOfSubscription": { - "title": "Подписка аннулируется!", - "description": "Чтобы отменить подписку, нажмите “Подтвердить мои действия”", + "title": "Subscription cancelled!", + "description": "To cancel the subscription, click “Confirm my actions”", "offer": { - "title": "Бесплатный 2-месячный план", + "title": "Free 2-month plan", "old-price": "9900", "new-price": "0" }, - "offer_button": "Применить", - "cancel_button": "Я подтверждаю свои действия", + "offer_button": "Apply", + "cancel_button": "Confirm my actions", "error_message": "Something went wrong. Please try again later.", "toast_message": "Your subscription will be cancelled!" }, "PlanCancelled": { - "title": "Стандартный план Отменен!", + "title": "Standard plan cancelled!", "icon": "🥳", - "description": "Выполнен переход на бесплатный тридцатидневный план ", - "button": "Готово" + "description": "Completed transition to a free 30-day plan", + "button": "Done" }, "SubscriptionStopped": { - "title": "Подписка остановлена успешно!", + "title": "Subscription stopped successfully!", "icon": "🎉" }, "DatePicker": { @@ -231,7 +231,7 @@ "one_time_price_offer": "One time price offer: ", "choose_from": "Choose from 80+ experts astrologers.", "original_price": "Original price: {oldPrice} ", - "save": "Economisez {discount}%", + "save": "Save {discount}%", "get_my_consultation": "Get my consultation", "skip_this_offer": "Skip this offer", "payment_error": "Something went wrong. Please try again later." @@ -286,5 +286,112 @@ } } } + }, + "Chat": { + "header": { + "title": "Chat", + "search_placeholder": "Type a Chat..." + }, + "new_messages": "New messages", + "view_all": "View All ({count})", + "hide_all": "Hide", + "typing": "is typing...", + "voice_message": "Voice message", + "photo": "Photo", + "correspondence_started": { + "title": "Correspondence started", + "pinned_chats": "Pinned Chats" + }, + "message_input_placeholder": "Type a message...", + "message_image_fallback": "Failed to load image", + "payment_error": "Something went wrong. Please try again later." + }, + "RefillTimerModal": { + "title": "Refill credits in 1 click", + "subtitle": " {newCredits} credits for {price}", + "button": "Get Credits", + "dont_want_to_continue": "I don't want to continue chatting", + "auto_refill_description": "Auto-refill keeps your readings uninterrupted. After using {afterCredits} credits, we'll automatically add {addCredits} more credits ({minutes} minutes of consultation) for a one-time payment. No recurring charges.", + "seconds": "seconds" + }, + "RefillOptionsModal": { + "header": { + "title": "{name} is waiting!", + "subtitle": "{name} is waiting for you in our chat..." + }, + "button": "Continue", + "refill_option": { + "popular": "POPULAR", + "credits": "{credits} credits", + "bonus": "+

credits", + "price": "{price}" + }, + "benefits": { + "1": { + "title": "Instant Access", + "description": "Continue chatting immediately" + }, + "2": { + "title": "Secure Payment", + "description": "256-bit SSL encryption" + }, + "3": { + "title": "Cancel Anytime", + "description": "No long-term commitment" + }, + "4": { + "title": "Best Value", + "description": "Most credits per dollar" + } + } + }, + "EmailMarketing": { + "Compatibility": { + "v1": { + "Landing": { + "title": "Special Offer!", + "description": "Everything for free. Trial include!", + "title-hey": "Hey, Sun 👋", + "description-hey": "Your wellness and happiness are key for us!", + "personalized-plan-title": "So we decided to give you your personalized plan and access to the trial of our app", + "personalized-plan-free": "FOR FREE!", + "title-highlights": "Highlights of your plan:", + "personal-astrologer-advice": "1:1 Advice from your personal astrologer", + "finding-compatible-partner": "Finding the most compatible partner", + "relationship-patterns-insights": "Insights into your relationship patterns, and emotional and sexual needs", + "better-understanding-yourself": "Better understanding of yourself and your needs", + "old-price-label": "OLD PRICE", + "new-price-label": "NEW PRICE", + "plan-includes-title": "Your plan also includes:", + "plan-includes-unlimited-horoscopes": "Unlimited daily / weekly / monthly / yearly horoscopes", + "plan-includes-astrology-lessons": "Astrology lessons and articles inside the app", + "plan-includes-lifestyle-calendars": "Beauty / health / travel and more calendars", + "plan-includes-compatibility-check": "Compatibility check with zodiac signs inside the app", + "review-1": "Horoscope tells realistic facts about day to day life, which can be easily relatable. It shows direction.", + "review-2": "It makes me feel safe, seeing, warm and smart.", + "review-3": "I love that we have the \"my profile\" option. I love learning about myself.", + "circular-text-image": "circular-text-en.png", + "statistics-banner-text": "Last week alone got this relationship guide", + "statistics-banner-count": "{count}+ people", + "money-back-guarantee-title": "Money-back guarantee", + "money-back-guarantee-text": "We are convinced that we will help you get a deeper understanding of your partner and how you can improve your relationship. After all of our stellar customer reviews, we are ready to return your money if you feel that this report doesn't provide any value. Find more about applicable limitations in our Money-back policy.", + "guaranteed-security-payments": "Guaranteed security payments", + "button-continue": "Continue", + "old-price": "up to {oldPrice}" + }, + "SpecialOffer": { + "title": "Special Offer!", + "start-trial": "Start your {days}-day trial", + "cancel-anytime": "No pressure. Cancel anytime", + "policy": "By continuing you agree that if you don't cancel prior to the end of the {days}-days trial, you will automatically be charged {price} every 2 weeks until you cancel in settings. Learn more about cancellation and refund policy in Subscription terms", + "button-continue": "Continue", + "pricing-summary-total-today": "Total today:", + "pricing-summary-code-applied": "Code applied!", + "pricing-summary-cost-after-trial": "Your cost per 2 weeks after trial", + "pricing-summary-trial-description": "You will be charged only {totalToday} for your {trialDuration}-day trial. Subscription renews automatically until cancelled. You can cancel at any time before the end of the trial.", + "reserved-for": "Reserved for {time}" + } + } + } } } diff --git a/messages/es.json b/messages/es.json index 45ed41e..112ccd3 100644 --- a/messages/es.json +++ b/messages/es.json @@ -1,184 +1,187 @@ { "HomePage": { - "title": "¡Hola mundo!", - "about": "Ir a la página acerca de" + "title": "Hello world!", + "about": "Go to the about page" }, "Profile": { "profile_information": { - "title": "Información del Perfil", - "description": "Para actualizar tu dirección de correo electrónico, por favor contacta al soporte.", - "email_placeholder": "Correo electrónico", - "name_placeholder": "Nombre" + "title": "Profile Information", + "description": "To update your email address please contact support.", + "email_placeholder": "Email", + "name_placeholder": "Name" }, "billing": { - "title": "Facturación", - "description": "Para acceder a la información de tu suscripción, por favor inicia sesión en tu cuenta de facturación.", - "subscription_type": "Tipo de Suscripción:", - "billing_button": "Facturación", + "title": "Billing", + "description": "To access your subscription information, please log into your billing account.", + "subscription_type": "Subscription Type:", + "billing_button": "Billing", "credits": { - "title": "Te quedan {credits} créditos", - "description": "Puedes usarlos para chatear con cualquier Experto en la plataforma." + "title": "You have {credits} credits left", + "description": "You can use them to chat with any Expert on the platform." }, - "any_questions": "¿Alguna pregunta? {linkText}", - "any_questions_link": "Contáctanos", - "subscription_update": "{subscriptionUpdateBold}

Si acabas de comprar o cambiar de plan, el estado de tu suscripción cambiará en unas horas.", - "subscription_update_bold": "La información de la suscripción se actualiza cada pocas horas." + "any_questions": "Any questions? {linkText}", + "any_questions_link": "Contact us", + "subscription_update": "{subscriptionUpdateBold}

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": "Cerrar sesión", - "log_out_button": "Cerrar sesión", + "title": "Log out", + "log_out_button": "Log out", "modal": { - "title": "¿Estás seguro de que quieres cerrar sesión?", - "description": "¿Estás seguro de que quieres cerrar sesión?", - "stay_button": "Quedarse", - "log_out_button": "Cerrar sesión" + "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": "Gestionar mis suscripciones", - "success_cancel_message": "Tu suscripción ha sido cancelada exitosamente", + "title": "Manage my subscriptions", + "success_cancel_message": "Your subscription has been cancelled successfully", "modal": { - "title": "¿Estás seguro de que quieres cancelar tu suscripción?", - "description": "¿Estás seguro de que quieres cancelar tu suscripción?", - "cancel_button": "Cancelar suscripción", - "stay_button": "Quedarse" + "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": "Tipo de Suscripción", + "subscription_type": "Subscription Type", "subscription_type_value": { - "DAY": "Suscripción Diaria", - "WEEK": "Suscripción Semanal", - "MONTH": "Suscripción Mensual", - "YEAR": "Suscripción Anual" + "DAY": "Daily Subscription", + "WEEK": "Weekly Subscription", + "MONTH": "Monthly Subscription", + "YEAR": "Yearly Subscription" }, - "subscription_status": "Estado de la Suscripción", + "subscription_status": "Subscription Status", "subscription_status_value": { - "ACTIVE": "Activa", - "CANCELLED": "Se cancela el {date}", - "PAST_DUE": "Vencida" + "ACTIVE": "Active", + "CANCELLED": "Cancels on {date}", + "PAST_DUE": "Past due" }, - "billing_period": "Período de Facturación", + "billing_period": "Billing Period", "billing_period_value": { - "DAY": "Día", - "WEEK": "Semana", - "MONTH": "Mes", - "YEAR": "Año" + "DAY": "Day", + "WEEK": "Week", + "MONTH": "Month", + "YEAR": "Year" }, - "last_payment_on": "Último Pago el", - "renewal_date": "Fecha de Renovación", - "renewal_amount": "Monto de Renovación", - "cancel_subscription": "Cancelar Suscripción" + "last_payment_on": "Last Payment On", + "renewal_date": "Renewal Date", + "renewal_amount": "Renewal Amount", + "cancel_subscription": "Cancel Subscription" }, - "no_subscriptions": "No tienes ninguna suscripción", - "error": "Algo salió mal. Por favor, inténtalo de nuevo más tarde.", - "try_again": "Intentar de nuevo" + "no_subscriptions": "You don't have any subscriptions", + "error": "Something went wrong. Please try again later.", + "try_again": "Try again" }, "CancelSubscription": { - "title": "Lamentamos que te vayas…", - "description": "Muchos se van justo en el momento en que el astrólogo comienza a ver el punto de inflexión en su historia.



Permítenos hacer un par de preguntas para mejorar nuestro servicio - y tal vez ofrecer una solución que se adapte mejor a ti.", - "stay_button": "Quedarse y reducir mi plan en 50%", - "cancel_button": "Cancelar", - "error_message": "Algo salió mal. Por favor, inténtalo de nuevo más tarde." + "title": "Sad to see you go…", + "description": "Many leave exactly at the moment when the astrologist begins to see a turning point in their history.



Allow us to ask a few questions to make our service better - and, possibly, offer a solution that is more suitable for you.", + "stay_button": "Stay and reduce my plan by 50%", + "cancel_button": "Cancel", + "error_message": "Something went wrong. Please try again later." }, "Stay50Done": { - "title": "¡Valoramos tu elección!", + "title": "We value your choice!", "descriptions": { - "1": "Plan cambiado exitosamente" + "1": "Plan successfully changed" }, - "button": "Listo" + "button": "Done" }, "AppreciateChoice": { - "title": "¡Valoramos tu elección!", + "title": "We value your choice!", "descriptions": { - "1": "Seleccionando la solución óptima...", - "2": "Creando encuesta personalizada...", - "3": "Formando oferta ventajosa..." + "1": "Selecting the optimal solution...", + "2": "Creating a personalized survey...", + "3": "Forming a profitable offer..." }, - "button": "Siguiente" + "button": "Next" }, "WhatReason": { - "title": "¿Cuál fue la razón?", + "title": "What became the reason?", "answers": { - "no_promised_result": "No obtuve el resultado prometido", - "too_expensive": "Muy caro", - "no_time": "No tengo tiempo", - "technical_issues": "Problemas técnicos", - "dislike_app": "No me gustó la aplicación", - "hard_to_navigate": "Es difícil navegar en la aplicación", - "other": "Otro" + "no_promised_result": "Did not receive the promised result", + "too_expensive": "Too expensive", + "high_auto_payment": "The cost of automatic payment is too high", + "unexpected_fee": "I did not expect an additional charge", + "want_pause": "I want to pause", + "service_not_as_expected": "The service was not as expected", + "found_alternative": "Found an alternative", + "dislike_app": "I did not like the app", + "hard_to_navigate": "It is difficult to navigate in the app", + "other": "Other" } }, "Payment": { "Success": { - "title": "Pago exitoso", - "button": "Listo" + "title": "Payment successful", + "button": "Done" }, "Error": { - "title": "Pago fallido", - "button": "Intentar de nuevo" + "title": "Payment failed", + "button": "Try again" } }, "SecondChance": { - "title": "Danos una segunda oportunidad y obtén el mejor plan GRATIS", + "title": "Give us a second chance and get the best plan for free", "offers": { "1": { - "title": "Plan gratuito por

1 mes", - "description": "Usa todo el potencial de AURA y aún más.", + "title": "Free plan for 1 month", + "description": "Use the full potential of WITLAB and even more.", "old-price": "1900", "new-price": "0" }, "2": { - "title": "Plan premium gratuito", - "description": "Consulta gratuita de 30 min con un Asesor premium", + "title": "Free premium plan", + "description": "Free 30 min consultation with a premium advisor", "old-price": "4900", "new-price": "0" } }, - "get_offer": "Obtener plan gratuito", - "cancel": "Cancelar", - "error_message": "Algo salió mal. Por favor, inténtalo de nuevo más tarde." + "get_offer": "Get free plan", + "cancel": "Cancel", + "error_message": "Something went wrong. Please try again later." }, "ChangeMind": { - "title": "¿Qué podría cambiar tu opinión?", + "title": "What could change your mind?", "answers": { - "more_chat_time": "Más tiempo de chat", - "more_personal_reports": "Más reportes personalizados", - "individual_plan": "Plan individual", - "other": "Otro" + "more_chat_time": "More time in chats", + "more_personal_reports": "More personalized reports", + "individual_plan": "Individual plan", + "other": "Other" } }, "StopFor30Days": { - "title": "Pausa la suscripción por treinta días. Sin cargos.", - "stop": "Pausar", - "cancel": "Cancelar", - "error_message": "Algo salió mal. Por favor, inténtalo de nuevo más tarde." + "title": "Pause the subscription for 30 days. No charges.", + "stop": "Pause", + "cancel": "Cancel", + "error_message": "Something went wrong. Please try again later." }, "CancellationOfSubscription": { - "title": "¡La suscripción será cancelada!", - "description": "Para cancelar la suscripción, presiona \"Confirmo mis acciones\"", + "title": "Subscription cancelled!", + "description": "To cancel the subscription, click “Confirm my actions”", "offer": { - "title": "Plan gratuito de 2 meses", + "title": "Free 2-month plan", "old-price": "9900", "new-price": "0" }, - "offer_button": "Aplicar", - "cancel_button": "Confirmo mis acciones", - "error_message": "Algo salió mal. Por favor, inténtalo de nuevo más tarde.", - "toast_message": "¡Tu suscripción será cancelada!" + "offer_button": "Apply", + "cancel_button": "Confirm my actions", + "error_message": "Something went wrong. Please try again later.", + "toast_message": "Your subscription will be cancelled!" }, "PlanCancelled": { - "title": "¡Plan Estándar Cancelado!", + "title": "Standard plan cancelled!", "icon": "🥳", - "description": "Transición completada al plan gratuito de treinta días", - "button": "Listo" + "description": "Completed transition to a free 30-day plan", + "button": "Done" }, "SubscriptionStopped": { - "title": "¡Suscripción pausada exitosamente!", + "title": "Subscription stopped successfully!", "icon": "🎉" }, "DatePicker": { - "year": "AAAA", + "year": "YYYY", "month": "MM", "day": "DD" }, @@ -188,30 +191,207 @@ "period": "AM/PM" }, "Compatibility": { - "title": "Tu Tipo de Personalidad", - "description": "Por favor ingresa tus datos para crear el reporte.", - "button": "Continuar", - "error": "Algo salió mal. Por favor, inténtalo de nuevo más tarde." + "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": "Tu Tipo de Personalidad", - "error": "Algo salió mal. Por favor, inténtalo de nuevo más tarde." + "title": "Your Personality Type", + "error": "Something went wrong. Please try again later." }, "PalmistryResult": { - "title": "Tu Tipo de Personalidad", - "error": "Algo salió mal. Por favor, inténtalo de nuevo más tarde." + "title": "Your Personality Type", + "error": "Something went wrong. Please try again later." }, "Meditation": { - "title": "Para y respira para ayudarte a relajar y enfocarte en lo que realmente importa.", - "subtitle": "La práctica de respiración ayudará a mejorar tu aura. Inhala la energía positiva, exhala la negativa...", - "button": "COMENZAR" + "title": "Stop and breathe to help you relax and focus on what really matters.", + "subtitle": "Breathing practice will help improve your aura. Breath in the positive energy, breathe out the negative...", + "button": "BEGIN" }, "MeditationResult": { - "breath_relax": "Respira y Relájate", - "breath_in": "Inhalar", - "breath_out": "Exhalar" + "breath_relax": "Breath & Relax", + "breath_in": "Breath in", + "breath_out": "Breath out" }, "ActionFieldsForm": { - "required_field": "Este campo es obligatorio" + "required_field": "This field is required" + }, + "AdditionalPurchases": { + "caution": { + "title": "Caution!", + "description": "To prevent double charges please don`t close the page and don`t go back." + }, + "add-consultant": { + "title": "More for you", + "exclusive_offer": "Exclusive offer recommended for you to achieve your goals faster", + "your_unique_consultation": "Your unique individual consultation", + "30-minute": "30-minute private consultation with an expert", + "unlock_profound": "Unlock profound insights into your personality, relationships, career trajectory, and life's pivotal moments through astrology, empowering you to make informed decisions and achieve greater fulfillment.", + "one_time_price_offer": "One time price offer: ", + "choose_from": "Choose from 80+ experts astrologers.", + "original_price": "Original price: {oldPrice} ", + "save": "Save {discount}%", + "get_my_consultation": "Get my consultation", + "skip_this_offer": "Skip this offer", + "payment_error": "Something went wrong. Please try again later." + }, + "add-guides": { + "title": "Choose your sign-up offer 🔥", + "subtitle": "Available only now", + "description": "*You will be charged for the add-on services or offers selected at the time of purchase. This is a non-recuring payment.", + "button": "Get my copy", + "payment_error": "Something went wrong. Please try again later.", + "select_product_error": "Please select a product", + "skip_offer": "Skip offer", + + "products": { + "main_ultra_pack": { + "title": "ULTRA PACK", + "discount": "{discount}% OFF", + "subtitle": "(3 in 1 + 2 secret bonus reading)", + "price": " ( regular price )", + "emoji": "star_struck.webp" + }, + "main_numerology_analysis": { + "title": "NUMEROLOGY ANALYSIS", + "discount": "{discount}% OFF", + "price": " ( was )", + "emoji": "input_numbers.webp" + }, + "main_tarot_reading": { + "title": "TAROT READING", + "discount": "{discount}% OFF", + "price": " ( was )", + "emoji": "sunset.webp" + }, + "main_palmistry_guide": { + "title": "PALMISTRY GUIDE", + "discount": "{discount}% OFF", + "price": " ( was )", + "emoji": "rised_hand.webp" + }, + "main_money_reading": { + "title": "MONEY READING", + "discount": "{discount}% OFF", + "price": " ( was )", + "emoji": "money.png" + }, + "main_skip_offer": { + "title": "SKIP OFFER", + "discount": "{discount}% OFF", + "price": " ( was )", + "subtitle": "You are missing out on both readings", + "emoji": "rised_hand.webp" + } + } + } + }, + "Chat": { + "header": { + "title": "Chat", + "search_placeholder": "Type a Chat..." + }, + "new_messages": "New messages", + "view_all": "View All ({count})", + "hide_all": "Hide", + "typing": "is typing...", + "voice_message": "Voice message", + "photo": "Photo", + "correspondence_started": { + "title": "Correspondence started", + "pinned_chats": "Pinned Chats" + }, + "message_input_placeholder": "Type a message...", + "message_image_fallback": "Failed to load image", + "payment_error": "Something went wrong. Please try again later." + }, + "RefillTimerModal": { + "title": "Refill credits in 1 click", + "subtitle": " {newCredits} credits for {price}", + "button": "Get Credits", + "dont_want_to_continue": "I don't want to continue chatting", + "auto_refill_description": "Auto-refill keeps your readings uninterrupted. After using {afterCredits} credits, we'll automatically add {addCredits} more credits ({minutes} minutes of consultation) for a one-time payment. No recurring charges.", + "seconds": "seconds" + }, + "RefillOptionsModal": { + "header": { + "title": "{name} is waiting!", + "subtitle": "{name} is waiting for you in our chat..." + }, + "button": "Continue", + "refill_option": { + "popular": "POPULAR", + "credits": "{credits} credits", + "bonus": "+

credits", + "price": "{price}" + }, + "benefits": { + "1": { + "title": "Instant Access", + "description": "Continue chatting immediately" + }, + "2": { + "title": "Secure Payment", + "description": "256-bit SSL encryption" + }, + "3": { + "title": "Cancel Anytime", + "description": "No long-term commitment" + }, + "4": { + "title": "Best Value", + "description": "Most credits per dollar" + } + } + }, + "EmailMarketing": { + "Compatibility": { + "v1": { + "Landing": { + "title": "Special Offer!", + "description": "Everything for free. Trial include!", + "title-hey": "Hey, Sun 👋", + "description-hey": "Your wellness and happiness are key for us!", + "personalized-plan-title": "So we decided to give you your personalized plan and access to the trial of our app", + "personalized-plan-free": "FOR FREE!", + "title-highlights": "Highlights of your plan:", + "personal-astrologer-advice": "1:1 Advice from your personal astrologer", + "finding-compatible-partner": "Finding the most compatible partner", + "relationship-patterns-insights": "Insights into your relationship patterns, and emotional and sexual needs", + "better-understanding-yourself": "Better understanding of yourself and your needs", + "old-price-label": "OLD PRICE", + "new-price-label": "NEW PRICE", + "plan-includes-title": "Your plan also includes:", + "plan-includes-unlimited-horoscopes": "Unlimited daily / weekly / monthly / yearly horoscopes", + "plan-includes-astrology-lessons": "Astrology lessons and articles inside the app", + "plan-includes-lifestyle-calendars": "Beauty / health / travel and more calendars", + "plan-includes-compatibility-check": "Compatibility check with zodiac signs inside the app", + "review-1": "Horoscope tells realistic facts about day to day life, which can be easily relatable. It shows direction.", + "review-2": "It makes me feel safe, seeing, warm and smart.", + "review-3": "I love that we have the \"my profile\" option. I love learning about myself.", + "circular-text-image": "circular-text-en.png", + "statistics-banner-text": "Last week alone got this relationship guide", + "statistics-banner-count": "{count}+ people", + "money-back-guarantee-title": "Money-back guarantee", + "money-back-guarantee-text": "We are convinced that we will help you get a deeper understanding of your partner and how you can improve your relationship. After all of our stellar customer reviews, we are ready to return your money if you feel that this report doesn't provide any value. Find more about applicable limitations in our Money-back policy.", + "guaranteed-security-payments": "Guaranteed security payments", + "button-continue": "Continue", + "old-price": "up to {oldPrice}" + }, + "SpecialOffer": { + "title": "Special Offer!", + "start-trial": "Start your {days}-day trial", + "cancel-anytime": "No pressure. Cancel anytime", + "policy": "By continuing you agree that if you don't cancel prior to the end of the {days}-days trial, you will automatically be charged {price} every 2 weeks until you cancel in settings. Learn more about cancellation and refund policy in Subscription terms", + "button-continue": "Continue", + "pricing-summary-total-today": "Total today:", + "pricing-summary-code-applied": "Code applied!", + "pricing-summary-cost-after-trial": "Your cost per 2 weeks after trial", + "pricing-summary-trial-description": "You will be charged only {totalToday} for your {trialDuration}-day trial. Subscription renews automatically until cancelled. You can cancel at any time before the end of the trial.", + "reserved-for": "Reserved for {time}" + } + } + } } } diff --git a/next.config.ts b/next.config.ts index 8a53b6f..dd5ad28 100644 --- a/next.config.ts +++ b/next.config.ts @@ -15,9 +15,9 @@ const nextConfig: NextConfig = { pathname: "/**", }, { - protocol: 'https', - hostname: 'assets.witlab.us', - pathname: '/**', + protocol: "https", + hostname: "assets.witlab.us", + pathname: "/**", }, ], }, diff --git a/package-lock.json b/package-lock.json index d679a69..2b8b666 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,9 +15,11 @@ "next": "15.3.3", "next-intl": "^4.1.0", "react": "^19.0.0", + "react-circular-progressbar": "^2.2.0", "react-dom": "^19.0.0", "sass": "^1.89.2", "server-only": "^0.0.1", + "socket.io-client": "^4.8.1", "zod": "^3.25.64", "zustand": "^5.0.5" }, @@ -1285,6 +1287,12 @@ "integrity": "sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==", "license": "MIT" }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, "node_modules/@swc/counter": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", @@ -2623,6 +2631,45 @@ "dev": true, "license": "MIT" }, + "node_modules/engine.io-client": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", + "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/es-abstract": { "version": "1.24.0", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", @@ -4401,7 +4448,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -4908,6 +4954,15 @@ "node": ">=0.10.0" } }, + "node_modules/react-circular-progressbar": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/react-circular-progressbar/-/react-circular-progressbar-2.2.0.tgz", + "integrity": "sha512-cgyqEHOzB0nWMZjKfWN3MfSa1LV3OatcDjPz68lchXQUEiBD5O1WsAtoVK4/DSL0B4USR//cTdok4zCBkq8X5g==", + "license": "MIT", + "peerDependencies": { + "react": ">=0.14.0" + } + }, "node_modules/react-dom": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", @@ -5370,6 +5425,68 @@ "is-arrayish": "^0.3.1" } }, + "node_modules/socket.io-client": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", + "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -5988,6 +6105,35 @@ "node": ">=0.10.0" } }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 6ace31c..4e635de 100644 --- a/package.json +++ b/package.json @@ -20,9 +20,11 @@ "next": "15.3.3", "next-intl": "^4.1.0", "react": "^19.0.0", + "react-circular-progressbar": "^2.2.0", "react-dom": "^19.0.0", "sass": "^1.89.2", "server-only": "^0.0.1", + "socket.io-client": "^4.8.1", "zod": "^3.25.64", "zustand": "^5.0.5" }, diff --git a/public/email-marketing/comp/v1/andi36_11.png b/public/email-marketing/comp/v1/andi36_11.png new file mode 100644 index 0000000..d6b6b08 Binary files /dev/null and b/public/email-marketing/comp/v1/andi36_11.png differ diff --git a/public/email-marketing/comp/v1/aramaska.png b/public/email-marketing/comp/v1/aramaska.png new file mode 100644 index 0000000..833d526 Binary files /dev/null and b/public/email-marketing/comp/v1/aramaska.png differ diff --git a/public/email-marketing/comp/v1/circular-text-en.png b/public/email-marketing/comp/v1/circular-text-en.png new file mode 100644 index 0000000..8605ac0 Binary files /dev/null and b/public/email-marketing/comp/v1/circular-text-en.png differ diff --git a/public/email-marketing/comp/v1/gift-big.png b/public/email-marketing/comp/v1/gift-big.png new file mode 100644 index 0000000..74b1121 Binary files /dev/null and b/public/email-marketing/comp/v1/gift-big.png differ diff --git a/public/email-marketing/comp/v1/gift-small.png b/public/email-marketing/comp/v1/gift-small.png new file mode 100644 index 0000000..761163d Binary files /dev/null and b/public/email-marketing/comp/v1/gift-small.png differ diff --git a/public/email-marketing/comp/v1/guarantee.svg b/public/email-marketing/comp/v1/guarantee.svg new file mode 100644 index 0000000..4a6bd33 --- /dev/null +++ b/public/email-marketing/comp/v1/guarantee.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/email-marketing/comp/v1/guaranteed.svg b/public/email-marketing/comp/v1/guaranteed.svg new file mode 100644 index 0000000..e1f047e --- /dev/null +++ b/public/email-marketing/comp/v1/guaranteed.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/public/email-marketing/comp/v1/hand-with-eye.svg b/public/email-marketing/comp/v1/hand-with-eye.svg new file mode 100644 index 0000000..c3dbd64 --- /dev/null +++ b/public/email-marketing/comp/v1/hand-with-eye.svg @@ -0,0 +1,279 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/email-marketing/comp/v1/messages.svg b/public/email-marketing/comp/v1/messages.svg new file mode 100644 index 0000000..10e1bc1 --- /dev/null +++ b/public/email-marketing/comp/v1/messages.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/email-marketing/comp/v1/patterso.png b/public/email-marketing/comp/v1/patterso.png new file mode 100644 index 0000000..bd3e9a9 Binary files /dev/null and b/public/email-marketing/comp/v1/patterso.png differ diff --git a/public/email-marketing/comp/v1/payments.png b/public/email-marketing/comp/v1/payments.png new file mode 100644 index 0000000..6c60729 Binary files /dev/null and b/public/email-marketing/comp/v1/payments.png differ diff --git a/public/email-marketing/comp/v1/relationships.svg b/public/email-marketing/comp/v1/relationships.svg new file mode 100644 index 0000000..9bf0d09 --- /dev/null +++ b/public/email-marketing/comp/v1/relationships.svg @@ -0,0 +1,390 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/email-marketing/comp/v1/relieved-face.svg b/public/email-marketing/comp/v1/relieved-face.svg new file mode 100644 index 0000000..7d8bebd --- /dev/null +++ b/public/email-marketing/comp/v1/relieved-face.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/public/email-marketing/comp/v1/revolving-hearts.svg b/public/email-marketing/comp/v1/revolving-hearts.svg new file mode 100644 index 0000000..42dd5db --- /dev/null +++ b/public/email-marketing/comp/v1/revolving-hearts.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/email-marketing/comp/v1/smartphone.svg b/public/email-marketing/comp/v1/smartphone.svg new file mode 100644 index 0000000..5c24c44 --- /dev/null +++ b/public/email-marketing/comp/v1/smartphone.svg @@ -0,0 +1,706 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/email-marketing/comp/v1/sparkling-heart.svg b/public/email-marketing/comp/v1/sparkling-heart.svg new file mode 100644 index 0000000..ef63348 --- /dev/null +++ b/public/email-marketing/comp/v1/sparkling-heart.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/public/email-marketing/comp/v1/star.png b/public/email-marketing/comp/v1/star.png new file mode 100644 index 0000000..eb4910b Binary files /dev/null and b/public/email-marketing/comp/v1/star.png differ diff --git a/public/email-marketing/comp/v1/stars.svg b/public/email-marketing/comp/v1/stars.svg new file mode 100644 index 0000000..b64dc79 --- /dev/null +++ b/public/email-marketing/comp/v1/stars.svg @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/email-marketing/comp/v1/yellow-heart.svg b/public/email-marketing/comp/v1/yellow-heart.svg new file mode 100644 index 0000000..19cd17a --- /dev/null +++ b/public/email-marketing/comp/v1/yellow-heart.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/email-marketing/comp/v1/zodiac-in-space.svg b/public/email-marketing/comp/v1/zodiac-in-space.svg new file mode 100644 index 0000000..6d0a76c --- /dev/null +++ b/public/email-marketing/comp/v1/zodiac-in-space.svg @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/test-user-avatar.png b/public/test-user-avatar.png new file mode 100644 index 0000000..216265d Binary files /dev/null and b/public/test-user-avatar.png differ diff --git a/src/app/[locale]/(additional-purchases)/layout.module.scss b/src/app/[locale]/(additional-purchases)/layout.module.scss index 9f967e9..d44184e 100644 --- a/src/app/[locale]/(additional-purchases)/layout.module.scss +++ b/src/app/[locale]/(additional-purchases)/layout.module.scss @@ -1,7 +1,7 @@ .layout { position: relative; padding: 24px; - padding-bottom: 120px; + padding-bottom: 220px; min-height: 100dvh; height: fit-content; } diff --git a/src/app/[locale]/(chat)/chat/[assistantId]/layout.tsx b/src/app/[locale]/(chat)/chat/[assistantId]/layout.tsx new file mode 100644 index 0000000..b7cdd0e --- /dev/null +++ b/src/app/[locale]/(chat)/chat/[assistantId]/layout.tsx @@ -0,0 +1,30 @@ +import { createChat, getChatMessages } from "@/entities/chats/api"; +import type { IChatMessage } from "@/entities/chats/types"; +import { ChatProvider } from "@/providers/chat-provider"; + +export default async function ChatLayout({ + children, + params, +}: Readonly<{ + children: React.ReactNode; + params: Promise<{ assistantId: string }>; +}>) { + const { assistantId } = await params; + + const { chatId } = await createChat(assistantId); + + const { messages: initialMessages, totalCount } = await getChatMessages( + chatId, + { limit: 50, page: 1 } + ); + + return ( + + {children} + + ); +} diff --git a/src/app/[locale]/(chat)/chat/[assistantId]/page.module.scss b/src/app/[locale]/(chat)/chat/[assistantId]/page.module.scss new file mode 100644 index 0000000..d7b23e8 --- /dev/null +++ b/src/app/[locale]/(chat)/chat/[assistantId]/page.module.scss @@ -0,0 +1,5 @@ +.container { + display: flex; + flex-direction: column; + height: 100dvh; +} diff --git a/src/app/[locale]/(chat)/chat/[assistantId]/page.tsx b/src/app/[locale]/(chat)/chat/[assistantId]/page.tsx new file mode 100644 index 0000000..1313da7 --- /dev/null +++ b/src/app/[locale]/(chat)/chat/[assistantId]/page.tsx @@ -0,0 +1,20 @@ +import { + ChatHeader, + ChatMessagesWrapper, + ChatModalsWrapper, + MessageInputWrapper, +} from "@/components/domains/chat"; +import { loadChatsList } from "@/entities/chats/loaders"; + +import styles from "./page.module.scss"; + +export default function Chat() { + return ( +
+ + + + +
+ ); +} diff --git a/src/app/[locale]/(chat)/chat/page.module.scss b/src/app/[locale]/(chat)/chat/page.module.scss new file mode 100644 index 0000000..d85fd4b --- /dev/null +++ b/src/app/[locale]/(chat)/chat/page.module.scss @@ -0,0 +1,10 @@ +.container { + padding: 38px 16px 220px; +} + +.categories { + padding: 32px 0; + display: flex; + flex-direction: column; + gap: 45px; +} diff --git a/src/app/[locale]/(chat)/chat/page.tsx b/src/app/[locale]/(chat)/chat/page.tsx new file mode 100644 index 0000000..1c19638 --- /dev/null +++ b/src/app/[locale]/(chat)/chat/page.tsx @@ -0,0 +1,41 @@ +import { Suspense } from "react"; + +import { + ChatCategories, + ChatCategoriesSkeleton, + ChatListHeader, + CorrespondenceStartedSkeleton, + CorrespondenceStartedWrapper, + NewMessagesWrapper, + NewMessagesWrapperSkeleton, +} from "@/components/domains/chat"; +import { NavigationBar } from "@/components/layout"; +import { loadChatsList } from "@/entities/chats/loaders"; + +import styles from "./page.module.scss"; + +export const dynamic = "force-dynamic"; +export const revalidate = 0; +export const fetchCache = "force-no-store"; + +export default function Chats() { + const chatsPromise = loadChatsList(); + + return ( +
+ +
+ }> + + + }> + + + }> + + +
+ +
+ ); +} diff --git a/src/app/[locale]/(chat)/layout.module.scss b/src/app/[locale]/(chat)/layout.module.scss new file mode 100644 index 0000000..9aadd1c --- /dev/null +++ b/src/app/[locale]/(chat)/layout.module.scss @@ -0,0 +1,3 @@ +.main { + min-height: 100dvh; +} diff --git a/src/app/[locale]/(chat)/layout.tsx b/src/app/[locale]/(chat)/layout.tsx new file mode 100644 index 0000000..8b5232e --- /dev/null +++ b/src/app/[locale]/(chat)/layout.tsx @@ -0,0 +1,15 @@ +import { ChatStoreProvider } from "@/providers/chat-store-provider"; + +import styles from "./layout.module.scss"; + +export default function ChatLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + +
{children}
+
+ ); +} diff --git a/src/app/[locale]/(core)/advisers/page.tsx b/src/app/[locale]/(core)/advisers/page.tsx index 1da4cfc..809a079 100644 --- a/src/app/[locale]/(core)/advisers/page.tsx +++ b/src/app/[locale]/(core)/advisers/page.tsx @@ -4,12 +4,17 @@ import { AdvisersSection, AdvisersSectionSkeleton, } from "@/components/domains/dashboard"; +import { loadChatsList } from "@/entities/chats/loaders"; import { loadAssistants } from "@/entities/dashboard/loaders"; export default function Advisers() { return ( }> - + ); } diff --git a/src/app/[locale]/(core)/layout.module.scss b/src/app/[locale]/(core)/layout.module.scss index 3577e50..06faeb5 100644 --- a/src/app/[locale]/(core)/layout.module.scss +++ b/src/app/[locale]/(core)/layout.module.scss @@ -1,6 +1,6 @@ .main { padding: 16px; - padding-bottom: 120px; + padding-bottom: 220px; } .navBar { diff --git a/src/app/[locale]/(core)/layout.tsx b/src/app/[locale]/(core)/layout.tsx index 1a066e9..654e78a 100644 --- a/src/app/[locale]/(core)/layout.tsx +++ b/src/app/[locale]/(core)/layout.tsx @@ -1,4 +1,6 @@ import { DrawerProvider, Header, NavigationBar } from "@/components/layout"; +import { loadChatsList } from "@/entities/chats/loaders"; +import { ChatStoreProvider } from "@/providers/chat-store-provider"; import styles from "./layout.module.scss"; @@ -7,11 +9,14 @@ export default function CoreLayout({ }: Readonly<{ children: React.ReactNode; }>) { + const chatsPromise = loadChatsList(); return ( -
-
{children}
- + +
+
{children}
+ + ); } diff --git a/src/app/[locale]/(core)/page.tsx b/src/app/[locale]/(core)/page.tsx index 98b6bd9..11cb9ab 100644 --- a/src/app/[locale]/(core)/page.tsx +++ b/src/app/[locale]/(core)/page.tsx @@ -7,10 +7,13 @@ import { CompatibilitySectionSkeleton, MeditationSection, MeditationSectionSkeleton, + NewMessagesSection, + NewMessagesSectionSkeleton, PalmSection, PalmSectionSkeleton, } from "@/components/domains/dashboard"; import { Horoscope } from "@/components/widgets"; +import { loadChatsList } from "@/entities/chats/loaders"; import { loadAssistants, loadCompatibility, @@ -21,12 +24,20 @@ import { import styles from "./page.module.scss"; export default function Home() { + const chatsPromise = loadChatsList(); return (
+ }> + + + }> - + }> diff --git a/src/app/[locale]/(email-marketing)/em/(compatibility)/c/v1/landing/page.module.scss b/src/app/[locale]/(email-marketing)/em/(compatibility)/c/v1/landing/page.module.scss new file mode 100644 index 0000000..f773a2e --- /dev/null +++ b/src/app/[locale]/(email-marketing)/em/(compatibility)/c/v1/landing/page.module.scss @@ -0,0 +1,150 @@ +.container { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + max-width: 460px; + margin: 0 auto; + padding-bottom: 140px; + overflow-x: hidden; + height: fit-content; + min-height: 100dvh; +} + +.backgroundElement { + position: absolute; + z-index: -1; +} + +.backgroundElement1 { + top: 25px; + right: -99px; +} + +.backgroundElement2 { + width: 37px; + height: auto; + top: 116px; + right: 16px; +} + +.backgroundElement3 { + top: 8px; + left: 258px; +} + +.backgroundElement4 { + top: 13px; + left: 4px; +} + +.backgroundElement5 { + top: -40px; + left: 39px; +} + +.backgroundElement6 { + top: -40px; + right: -85px; +} + +.backgroundElement7 { + top: -35px; + left: -60px; +} + +.backgroundElement8 { + top: -50px; + left: 0; +} + +.backgroundElement9 { + top: 100px; + right: 8px; +} + +.backgroundElement10 { + top: 205px; + left: 40px; +} + +.backgroundElement11 { + bottom: 30px; + left: 30px; +} + +.backgroundElement12 { + bottom: -70px; + left: -130px; +} + +.backgroundElement13 { + top: -10px; + right: -30px; +} + +.backgroundElement14 { + bottom: -50px; + right: -30px; +} + +.title { + position: relative; + margin-bottom: 0; + line-height: 106%; + font-weight: 600; + padding: 0; +} + +.titleSpecialOffer { + font-size: 65px; + margin-top: 25px; + padding-left: 18px; + padding-right: 18px; +} + +.titleHighlights { + font-size: 50px; + padding-left: 17px; + font-weight: 500; + width: 100%; + margin-top: 52px; +} + +.description { + position: relative; + width: 100%; + line-height: 125%; + padding: 0; + font-weight: 500; +} + +.descriptionSpecialOffer { + font-size: 17px; + margin-top: 2px; + padding-left: 18px; +} + +.descriptionHey { + font-size: 14px; + margin-top: 3px; + padding-left: 34px; +} + +.titleHey { + font-size: 28px; + font-weight: 700; + padding-left: 34px; + width: 100%; + margin-top: 57px; + + & > span { + font-size: 22px; + } +} + +.relative-container { + position: relative; + width: 100%; +} diff --git a/src/app/[locale]/(email-marketing)/em/(compatibility)/c/v1/landing/page.tsx b/src/app/[locale]/(email-marketing)/em/(compatibility)/c/v1/landing/page.tsx new file mode 100644 index 0000000..107248a --- /dev/null +++ b/src/app/[locale]/(email-marketing)/em/(compatibility)/c/v1/landing/page.tsx @@ -0,0 +1,417 @@ +import Image from "next/image"; +import { getTranslations } from "next-intl/server"; +import clsx from "clsx"; + +import { + AdviceFromAstrologer, + CustomerCounter, + FindingPartner, + GivePersonalizedPlan, + GuaranteedSecurityPayments, + InsightsRelationship, + LandingButtonWrapper, + MoneyBackGuarantee, + Payments, + PlanIncludes, + PriceComparison, + Reviews, + StatisticsBanner, + UnderstandingYourself, +} from "@/components/domains/email-marketing/compatibility/v1"; +import { Typography } from "@/components/ui"; +import { loadFunnelPaymentById } from "@/entities/session/funnel/loaders"; +import { IFunnelPaymentPlacement } from "@/entities/session/funnel/types"; +import { emailMarketingCompV1Images } from "@/shared/constants/images"; +import { translatePathEmailMarketingCompatibilityV1 } from "@/shared/constants/translate"; +import { getFormattedPrice } from "@/shared/utils/price"; +import { Currency, ELocalesPlacement } from "@/types"; + +import styles from "./page.module.scss"; + +const payload = { + funnel: ELocalesPlacement.EmailMarketingCompatibilityV2, +}; + +export default async function EmailMarketingCompatibilityV1Landing() { + const t = await getTranslations( + translatePathEmailMarketingCompatibilityV1("Landing") + ); + + const payment = (await loadFunnelPaymentById( + payload, + "main" + )) as IFunnelPaymentPlacement | null; + + const variant = payment?.variants?.[0]; + const currency = payment?.currency || Currency.USD; + + const features = [ + { + text: t("plan-includes-unlimited-horoscopes"), + }, + { + text: t("plan-includes-astrology-lessons"), + }, + { + text: t("plan-includes-lifestyle-calendars"), + }, + { + text: t("plan-includes-compatibility-check"), + }, + ]; + + return ( +
+ + + + Gift + + + <> + Gift + {t("title")} + + + + + + + + {t("description")} + + + + {t("title-hey")} + + + {t("description-hey")} + + + + + + + + + {t("title-highlights")} + + +
+ + + + + + + + + + + + + + + + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + +
+ ); +} diff --git a/src/app/[locale]/(email-marketing)/em/(compatibility)/c/v1/special-offer/page.module.scss b/src/app/[locale]/(email-marketing)/em/(compatibility)/c/v1/special-offer/page.module.scss new file mode 100644 index 0000000..c378f35 --- /dev/null +++ b/src/app/[locale]/(email-marketing)/em/(compatibility)/c/v1/special-offer/page.module.scss @@ -0,0 +1,61 @@ +.container { + width: 100%; + max-width: 560px; + height: fit-content; + min-height: 100dvh; + // overflow-x: hidden; + display: flex; + flex-direction: column; + align-items: center; + background-color: #5956e9; + color: #fff; + padding-top: 39px; + margin: 0 auto; +} + +.title { + line-height: 125%; +} + +.content { + width: 100%; + height: fit-content; + background-color: #fff; + border-radius: 30px 30px 0 0; + min-height: calc(100dvh - 39px - 26px * 1.25 - 29px); + margin-top: 22px; + padding: 53px 18px 160px; + color: #000; + display: flex; + flex-direction: column; + align-items: center; + + & > .contentTitle { + font-size: 23px; + line-height: 125%; + } + + & > .contentDescription { + display: flex; + align-items: center; + gap: 8px; + font-size: 18px; + line-height: 125%; + margin-top: 21px; + } + + & > .contentPolicy { + font-size: 12px; + line-height: 125%; + font-weight: 300; + margin-bottom: 0; + margin-top: 39px; + text-align: center; + color: #6f6d6d; + } + + & > .button { + margin-top: 59px; + max-width: 307px; + } +} diff --git a/src/app/[locale]/(email-marketing)/em/(compatibility)/c/v1/special-offer/page.tsx b/src/app/[locale]/(email-marketing)/em/(compatibility)/c/v1/special-offer/page.tsx new file mode 100644 index 0000000..d866b8b --- /dev/null +++ b/src/app/[locale]/(email-marketing)/em/(compatibility)/c/v1/special-offer/page.tsx @@ -0,0 +1,116 @@ +import { getTranslations } from "next-intl/server"; + +import { + PricingSummary, + SpecialOfferButtonWrapper, +} from "@/components/domains/email-marketing/compatibility/v1"; +import { Typography } from "@/components/ui"; +import { loadFunnelPaymentById } from "@/entities/session/funnel/loaders"; +import { IFunnelPaymentPlacement } from "@/entities/session/funnel/types"; +import { translatePathEmailMarketingCompatibilityV1 } from "@/shared/constants/translate"; +import { getFormattedPrice } from "@/shared/utils/price"; +import { Currency, ELocalesPlacement } from "@/types"; + +import styles from "./page.module.scss"; + +const payload = { + funnel: ELocalesPlacement.EmailMarketingCompatibilityV2, +}; + +export default async function SpecialOfferPage() { + const t = await getTranslations( + translatePathEmailMarketingCompatibilityV1("SpecialOffer") + ); + + const payment = (await loadFunnelPaymentById( + payload, + "main" + )) as IFunnelPaymentPlacement | null; + + const trialInterval = payment?.trialInterval || 7; + const variant = payment?.variants?.[0]; + const productId = variant?.id || ""; + const placementId = payment?.placementId || ""; + const paywallId = payment?.paywallId || ""; + const trialPrice = variant?.trialPrice || 0; + const price = variant?.price || 0; + const currency = payment?.currency || Currency.USD; + + return ( +
+ + {t("title")} + +
+ + {t("start-trial", { days: trialInterval })} + + + + + + + + + + + + + + {t("cancel-anytime")} + + + + + + +

+ {t("policy", { + days: trialInterval, + price: getFormattedPrice(price, currency), + })} +

+
+
+ ); +} diff --git a/src/app/[locale]/(payment)/layout.module.scss b/src/app/[locale]/(payment)/layout.module.scss index 3577e50..06faeb5 100644 --- a/src/app/[locale]/(payment)/layout.module.scss +++ b/src/app/[locale]/(payment)/layout.module.scss @@ -1,6 +1,6 @@ .main { padding: 16px; - padding-bottom: 120px; + padding-bottom: 220px; } .navBar { diff --git a/src/app/[locale]/(payment)/layout.tsx b/src/app/[locale]/(payment)/layout.tsx index f8c99ba..a717e7d 100644 --- a/src/app/[locale]/(payment)/layout.tsx +++ b/src/app/[locale]/(payment)/layout.tsx @@ -1,4 +1,5 @@ import { DrawerProvider, Header } from "@/components/layout"; +import { loadChatsList } from "@/entities/chats/loaders"; import styles from "./layout.module.scss"; @@ -9,7 +10,7 @@ export default function CoreLayout({ }>) { return ( -
+
{children}
); diff --git a/src/app/[locale]/auth/callback/route.ts b/src/app/[locale]/auth/callback/route.ts index 39e4049..f9ed64b 100644 --- a/src/app/[locale]/auth/callback/route.ts +++ b/src/app/[locale]/auth/callback/route.ts @@ -74,6 +74,7 @@ export async function GET(req: NextRequest) { `${nextUrl || 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); diff --git a/src/app/[locale]/layout.module.scss b/src/app/[locale]/layout.module.scss index 9061ae0..9c8bb1b 100644 --- a/src/app/[locale]/layout.module.scss +++ b/src/app/[locale]/layout.module.scss @@ -2,4 +2,5 @@ max-width: 560px; margin: 0 auto; position: relative; + min-height: 100dvh; } diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx index a6fe15e..acf3d6f 100644 --- a/src/app/[locale]/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -1,16 +1,22 @@ import "@/styles/reset.css"; import "@/styles/globals.css"; +import "react-circular-progressbar/dist/styles.css"; -import type { Metadata } from "next"; +import type { Metadata, Viewport } 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 { loadUser, loadUserId } from "@/entities/user/loaders"; import { routing } from "@/i18n/routing"; +import { AppUiStoreProvider } from "@/providers/app-ui-store-provider"; +import { ChatsInitializationProvider } from "@/providers/chats-initialization-provider"; import { RetainingStoreProvider } from "@/providers/retaining-store-provider"; +import SocketProvider from "@/providers/socket-provider"; import { ToastProvider } from "@/providers/toast-provider"; +import { UserProvider } from "@/providers/user-provider"; import styles from "./layout.module.scss"; @@ -24,6 +30,13 @@ const inter = Inter({ variable: "--font-inter", }); +export const viewport: Viewport = { + width: "device-width", + initialScale: 1, + maximumScale: 1, + userScalable: false, +}; + export const metadata: Metadata = { title: "WIT", description: @@ -44,13 +57,24 @@ export default async function RootLayout({ const messages = await getMessages(); + const user = await loadUser(); + const userId = await loadUserId(); + return ( - - {children} - + + + + + + {children} + + + + + diff --git a/src/components/domains/additional-purchases/AddConsultantButton/AddConsultantButton.tsx b/src/components/domains/additional-purchases/AddConsultantButton/AddConsultantButton.tsx index b33607a..531983e 100644 --- a/src/components/domains/additional-purchases/AddConsultantButton/AddConsultantButton.tsx +++ b/src/components/domains/additional-purchases/AddConsultantButton/AddConsultantButton.tsx @@ -7,10 +7,10 @@ import { BlurComponent } from "@/components/widgets"; import { useSingleCheckout } from "@/hooks/payment/useSingleCheckout"; import { useToast } from "@/providers/toast-provider"; -import styles from "./AddConsultantButton.module.scss"; - import { useMultiPageNavigationContext } from ".."; +import styles from "./AddConsultantButton.module.scss"; + export default function AddConsultantButton() { const t = useTranslations("AdditionalPurchases.add-consultant"); const { addToast } = useToast(); diff --git a/src/components/domains/additional-purchases/AddGuidesButton/AddGuidesButton.tsx b/src/components/domains/additional-purchases/AddGuidesButton/AddGuidesButton.tsx index 7f8fdb3..018942d 100644 --- a/src/components/domains/additional-purchases/AddGuidesButton/AddGuidesButton.tsx +++ b/src/components/domains/additional-purchases/AddGuidesButton/AddGuidesButton.tsx @@ -7,12 +7,11 @@ import { BlurComponent } from "@/components/widgets"; import { useSingleCheckout } from "@/hooks/payment/useSingleCheckout"; import { useToast } from "@/providers/toast-provider"; +import { useMultiPageNavigationContext } from ".."; import { useProductSelection } from "../ProductSelectionProvider"; import styles from "./AddGuidesButton.module.scss"; -import { useMultiPageNavigationContext } from ".."; - export default function AddGuidesButton() { const t = useTranslations("AdditionalPurchases.add-guides"); const { addToast } = useToast(); diff --git a/src/components/domains/additional-purchases/ConsultationTable/ConsultationTable.tsx b/src/components/domains/additional-purchases/ConsultationTable/ConsultationTable.tsx index 258942d..2ccc139 100644 --- a/src/components/domains/additional-purchases/ConsultationTable/ConsultationTable.tsx +++ b/src/components/domains/additional-purchases/ConsultationTable/ConsultationTable.tsx @@ -7,10 +7,10 @@ import { Typography } from "@/components/ui"; import { getFormattedPrice } from "@/shared/utils/price"; import { Currency } from "@/types"; -import styles from "./ConsultationTable.module.scss"; - import { useMultiPageNavigationContext } from ".."; +import styles from "./ConsultationTable.module.scss"; + export default function ConsultationTable() { const t = useTranslations("AdditionalPurchases.add-consultant"); const { navigation } = useMultiPageNavigationContext(); diff --git a/src/components/domains/additional-purchases/Offers/Offers.tsx b/src/components/domains/additional-purchases/Offers/Offers.tsx index 9be439f..64d18c1 100644 --- a/src/components/domains/additional-purchases/Offers/Offers.tsx +++ b/src/components/domains/additional-purchases/Offers/Offers.tsx @@ -5,12 +5,11 @@ import { useEffect, useMemo, useState } from "react"; import { Skeleton } from "@/components/ui"; import { IFunnelPaymentVariant } from "@/entities/session/funnel/types"; +import { Offer, useMultiPageNavigationContext } from ".."; import { useProductSelection } from "../ProductSelectionProvider"; import styles from "./Offers.module.scss"; -import { Offer, useMultiPageNavigationContext } from ".."; - export default function Offers() { const { navigation } = useMultiPageNavigationContext(); const data = navigation.currentItem; diff --git a/src/components/domains/chat/CategoryChats/CategoryChats.module.scss b/src/components/domains/chat/CategoryChats/CategoryChats.module.scss new file mode 100644 index 0000000..985ff63 --- /dev/null +++ b/src/components/domains/chat/CategoryChats/CategoryChats.module.scss @@ -0,0 +1,11 @@ +.chats { + width: 100%; + display: flex; + flex-direction: column; + + & > .chat { + background-color: transparent; + border: none; + box-shadow: none; + } +} diff --git a/src/components/domains/chat/CategoryChats/CategoryChats.tsx b/src/components/domains/chat/CategoryChats/CategoryChats.tsx new file mode 100644 index 0000000..5215927 --- /dev/null +++ b/src/components/domains/chat/CategoryChats/CategoryChats.tsx @@ -0,0 +1,57 @@ +"use client"; + +import { useRouter } from "next/navigation"; + +import { ChatItem } from "@/components/widgets"; +import { IChat } from "@/entities/chats/types"; +import { useChatStore } from "@/providers/chat-store-provider"; +import { ROUTES } from "@/shared/constants/client-routes"; +import { formatTime } from "@/shared/utils/date"; + +import styles from "./CategoryChats.module.scss"; + +interface CategoryChatsProps { + chats: IChat[]; + maxVisibleChats?: number; +} + +export default function CategoryChats({ + chats, + maxVisibleChats = 3, +}: CategoryChatsProps) { + const router = useRouter(); + const setCurrentChat = useChatStore(state => state.setCurrentChat); + + return ( +
+ {chats.slice(0, maxVisibleChats).map(chat => ( + { + setCurrentChat(chat); + router.push(ROUTES.chat(chat.assistantId)); + }} + /> + ))} +
+ ); +} diff --git a/src/components/domains/chat/ChatCategories/ChatCategories.tsx b/src/components/domains/chat/ChatCategories/ChatCategories.tsx new file mode 100644 index 0000000..33a3b13 --- /dev/null +++ b/src/components/domains/chat/ChatCategories/ChatCategories.tsx @@ -0,0 +1,77 @@ +"use client"; + +import { use, useState } from "react"; + +import { Skeleton } from "@/components/ui"; +import { Chips } from "@/components/widgets"; +import { IGetChatsListResponse } from "@/entities/chats/types"; +import { useChatsSocket } from "@/hooks/chats/useChatsSocket"; + +import { CategoryChats, ChatItemsList } from ".."; + +const MAX_HIDE_VISIBLE_COUNT = 3; + +interface ChatCategoriesProps { + chatsPromise: Promise; +} + +export default function ChatCategories({ chatsPromise }: ChatCategoriesProps) { + const chats = use(chatsPromise); + const { categorizedChats } = useChatsSocket({ initialChats: chats }); + + const [activeChip, setActiveChip] = useState("All"); + const [maxVisibleChats, setMaxVisibleChats] = useState< + Partial> + >({}); + + const chips = Object.keys(categorizedChats).map(key => ({ + text: key, + })); + chips.unshift({ + text: "All", + }); + + const filteredChats = Object.keys(categorizedChats).filter(key => { + if (activeChip === "All") return true; + return categorizedChats[key].some(chat => chat.category === activeChip); + }); + + return ( + <> + setActiveChip(chip.text)} + /> + + {filteredChats.map(key => ( + { + setMaxVisibleChats(prev => ({ + ...prev, + [key]: !!prev[key] ? null : categorizedChats[key].length, + })); + }, + }} + isVisibleViewAll={ + categorizedChats[key].length > MAX_HIDE_VISIBLE_COUNT + } + > + + + ))} + + ); +} + +export const ChatCategoriesSkeleton = () => { + return ; +}; diff --git a/src/components/domains/chat/ChatHeader/ChatHeader.module.scss b/src/components/domains/chat/ChatHeader/ChatHeader.module.scss new file mode 100644 index 0000000..e32de2b --- /dev/null +++ b/src/components/domains/chat/ChatHeader/ChatHeader.module.scss @@ -0,0 +1,75 @@ +.header { + position: sticky; + top: 0; + left: 0; + z-index: 100; + padding: 12px; + width: 100%; + max-width: 560px; + min-height: 71px; + background-color: #fff; + border-bottom: 1px solid #e5e7eb; + display: grid; + grid-template-columns: 1fr auto 1fr; + align-items: center; +} + +.back { + display: flex; + align-items: center; + gap: 4px; + cursor: pointer; + + & > .badge { + background-color: #fbbf24; + width: 24px; + } +} + +.time { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 4px; + + & > .timeText { + opacity: 0.8; + } +} + +.chatInfo { + display: flex; + align-items: center; + gap: 10px; + + & > .avatar { + border-radius: 50%; + width: 48px; + height: 48px; + // background-color: #f3f4f6; + background: linear-gradient(90deg, #3b82f6 0%, #4f46e5 100%); + } + + & > .chatInfoContent { + display: grid; + grid-template-rows: 1fr 14px; + justify-items: start; + gap: 2px; + + & > .name { + font-size: 18px; + // line-height: 28px; + color: #111827; + position: relative; + + & > .onlineIndicator { + position: absolute; + top: 0; + right: -12px; + width: 8px; + height: 8px; + border: none; + } + } + } +} diff --git a/src/components/domains/chat/ChatHeader/ChatHeader.tsx b/src/components/domains/chat/ChatHeader/ChatHeader.tsx new file mode 100644 index 0000000..5a65eac --- /dev/null +++ b/src/components/domains/chat/ChatHeader/ChatHeader.tsx @@ -0,0 +1,110 @@ +"use client"; + +import { use, useEffect, useState } from "react"; +import Image from "next/image"; +import { useRouter } from "next/navigation"; +import { useTranslations } from "next-intl"; + +import { + Badge, + Icon, + IconName, + OnlineIndicator, + Typography, +} from "@/components/ui"; +import { revalidateChatsPage } from "@/entities/chats/actions"; +import { IGetChatsListResponse } from "@/entities/chats/types"; +import { useChatsSocket } from "@/hooks/chats/useChatsSocket"; +import { useChat } from "@/providers/chat-provider"; +import { useChatStore } from "@/providers/chat-store-provider"; +import { formatSecondsToHHMMSS } from "@/shared/utils/date"; +import { delay } from "@/shared/utils/delay"; + +import styles from "./ChatHeader.module.scss"; + +interface ChatHeaderProps { + chatsPromise: Promise; +} + +export default function ChatHeader({ chatsPromise }: ChatHeaderProps) { + const t = useTranslations("Chat"); + const router = useRouter(); + const currentChat = useChatStore(state => state.currentChat); + const { isLoadingAdvisorMessage, isAvailableChatting } = useChat(); + const chats = use(chatsPromise); + const { totalUnreadCount } = useChatsSocket({ initialChats: chats }); + const [timer, setTimer] = useState(0); + + useEffect(() => { + (async () => { + await delay(1000); + if (isAvailableChatting) { + setTimer(timer + 1); + } + })(); + }, [isAvailableChatting, timer]); + + const handleBack = async () => { + await revalidateChatsPage(); + router.back(); + }; + + return ( +
+
+ + {!!totalUnreadCount && ( + + + {totalUnreadCount} + + + )} +
+
+ {!!currentChat?.assistantAvatar ? ( + Aaron (Taro) avatar + ) : ( +
+ )} +
+ + {currentChat?.assistantName} + + + + {isLoadingAdvisorMessage ? t("typing") : ""} + +
+
+
+ + {formatSecondsToHHMMSS(timer, { isHours: false })} + + +
+
+ ); +} diff --git a/src/components/domains/chat/ChatItemsList/ChatItemsList.module.scss b/src/components/domains/chat/ChatItemsList/ChatItemsList.module.scss new file mode 100644 index 0000000..613fe29 --- /dev/null +++ b/src/components/domains/chat/ChatItemsList/ChatItemsList.module.scss @@ -0,0 +1,6 @@ +.chatItemsList { + width: 100%; + display: flex; + flex-direction: column; + gap: 16px; +} diff --git a/src/components/domains/chat/ChatItemsList/ChatItemsList.tsx b/src/components/domains/chat/ChatItemsList/ChatItemsList.tsx new file mode 100644 index 0000000..2397bdf --- /dev/null +++ b/src/components/domains/chat/ChatItemsList/ChatItemsList.tsx @@ -0,0 +1,34 @@ +"use client"; + +import clsx from "clsx"; + +import { ChatItemsListHeader, ViewAllProps } from ".."; + +import styles from "./ChatItemsList.module.scss"; + +interface ChatItemsListProps { + className?: string; + children: React.ReactNode; + title: string; + viewAllProps: ViewAllProps; + isVisibleViewAll?: boolean; +} + +export default function ChatItemsList({ + className, + children, + title, + viewAllProps, + isVisibleViewAll = true, +}: ChatItemsListProps) { + return ( +
+ + {children} +
+ ); +} diff --git a/src/components/domains/chat/ChatItemsListHeader/ChatItemsListHeader.module.scss b/src/components/domains/chat/ChatItemsListHeader/ChatItemsListHeader.module.scss new file mode 100644 index 0000000..bcbf5d9 --- /dev/null +++ b/src/components/domains/chat/ChatItemsListHeader/ChatItemsListHeader.module.scss @@ -0,0 +1,11 @@ +.chatItemsListHeader { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + padding: 0 8px 0 16px; +} + +.title { + font-size: 18px; +} diff --git a/src/components/domains/chat/ChatItemsListHeader/ChatItemsListHeader.tsx b/src/components/domains/chat/ChatItemsListHeader/ChatItemsListHeader.tsx new file mode 100644 index 0000000..d48c447 --- /dev/null +++ b/src/components/domains/chat/ChatItemsListHeader/ChatItemsListHeader.tsx @@ -0,0 +1,26 @@ +import { Typography } from "@/components/ui"; + +import { ViewAll, ViewAllProps } from ".."; + +import styles from "./ChatItemsListHeader.module.scss"; + +interface ChatItemsListHeaderProps { + title: string; + viewAllProps: ViewAllProps; + isVisibleViewAll?: boolean; +} + +export default function ChatItemsListHeader({ + title, + viewAllProps, + isVisibleViewAll = true, +}: ChatItemsListHeaderProps) { + return ( +
+ + {title} + + {isVisibleViewAll && } +
+ ); +} diff --git a/src/components/domains/chat/ChatListHeader/ChatListHeader.module.scss b/src/components/domains/chat/ChatListHeader/ChatListHeader.module.scss new file mode 100644 index 0000000..b8461b9 --- /dev/null +++ b/src/components/domains/chat/ChatListHeader/ChatListHeader.module.scss @@ -0,0 +1,6 @@ +.header { + display: flex; + justify-content: space-between; + padding-inline: 12px 4px; + gap: 24px; +} diff --git a/src/components/domains/chat/ChatListHeader/ChatListHeader.tsx b/src/components/domains/chat/ChatListHeader/ChatListHeader.tsx new file mode 100644 index 0000000..fd5c748 --- /dev/null +++ b/src/components/domains/chat/ChatListHeader/ChatListHeader.tsx @@ -0,0 +1,18 @@ +import { useTranslations } from "next-intl"; + +import { SearchInput, Typography } from "@/components/ui"; + +import styles from "./ChatListHeader.module.scss"; + +export default function ChatListHeader() { + const t = useTranslations("Chat"); + return ( +
+ + {t("header.title")} + + {/* +
+ ); +} diff --git a/src/components/domains/chat/ChatMessage/ChatMessage.module.scss b/src/components/domains/chat/ChatMessage/ChatMessage.module.scss new file mode 100644 index 0000000..74ec592 --- /dev/null +++ b/src/components/domains/chat/ChatMessage/ChatMessage.module.scss @@ -0,0 +1,12 @@ +.message { + width: fit-content; + display: flex; + flex-direction: column; + max-width: calc(100% - 35px); + gap: 8px; + + &.own { + align-items: flex-end; + align-self: flex-end; + } +} diff --git a/src/components/domains/chat/ChatMessage/ChatMessage.tsx b/src/components/domains/chat/ChatMessage/ChatMessage.tsx new file mode 100644 index 0000000..fdac54c --- /dev/null +++ b/src/components/domains/chat/ChatMessage/ChatMessage.tsx @@ -0,0 +1,82 @@ +"use client"; + +import { useEffect } from "react"; +import clsx from "clsx"; + +import { useChat } from "@/providers/chat-provider"; + +import MessageAudio from "./MessageAudio/MessageAudio"; +import MessageBubble from "./MessageBubble/MessageBubble"; +import MessageImage from "./MessageImage/MessageImage"; +import MessageMeta from "./MessageMeta/MessageMeta"; +import MessageStatus from "./MessageStatus/MessageStatus"; +import MessageText from "./MessageText/MessageText"; +import MessageTyping from "./MessageTyping/MessageTyping"; + +import styles from "./ChatMessage.module.scss"; + +export interface ChatMessageProps { + message: { + id: string; + type: "text" | "image" | "audio" | "typing"; + content?: string; + imageUrl?: string; + audioUrl?: string; + duration?: number; + time: string | null; + isOwn: boolean; + isRead?: boolean; + }; +} + +export default function ChatMessage({ message }: ChatMessageProps) { + const { isConnected, read } = useChat(); + + useEffect(() => { + if ( + !!message.id && + !message.isRead && + message.id !== "typing" && + !message.id.startsWith("sending-message") && + isConnected + ) { + read([message.id]); + } + }, [message.id, message.isRead, read, isConnected]); + + return ( +
+ + {message.type === "text" && ( + + )} + + {message.type === "typing" && } + + {message.type === "image" && ( + <> + + {message.content && ( + + )} + + )} + + {message.type === "audio" && ( + <> + + {message.content && ( + + )} + + )} + + + {message.isOwn && } + +
+ ); +} diff --git a/src/components/domains/chat/ChatMessage/MessageAudio/MessageAudio.module.scss b/src/components/domains/chat/ChatMessage/MessageAudio/MessageAudio.module.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/components/domains/chat/ChatMessage/MessageAudio/MessageAudio.tsx b/src/components/domains/chat/ChatMessage/MessageAudio/MessageAudio.tsx new file mode 100644 index 0000000..4257e5e --- /dev/null +++ b/src/components/domains/chat/ChatMessage/MessageAudio/MessageAudio.tsx @@ -0,0 +1,23 @@ +import styles from "./MessageAudio.module.scss"; + +interface MessageAudioProps { + src: string; + duration?: number; +} + +export default function MessageAudio({ src, duration }: MessageAudioProps) { + return ( +
+
+ ); +} +function formatDuration(sec?: number) { + if (!sec) return null; + const m = Math.floor(sec / 60); + const s = sec % 60; + return `${m}:${s.toString().padStart(2, "0")}`; +} diff --git a/src/components/domains/chat/ChatMessage/MessageBubble/MessageBubble.module.scss b/src/components/domains/chat/ChatMessage/MessageBubble/MessageBubble.module.scss new file mode 100644 index 0000000..230bd13 --- /dev/null +++ b/src/components/domains/chat/ChatMessage/MessageBubble/MessageBubble.module.scss @@ -0,0 +1,13 @@ +.bubble { + background-color: #ffffff; + box-shadow: 0px 4px 6px 0px #00000017; + border-radius: 8px 24px 24px 24px; + // max-width: calc(100% - 35px); + width: fit-content; + overflow: hidden; + + &.own { + background: linear-gradient(90deg, #3b82f6 0%, #4f46e5 100%); + border-radius: 24px 8px 24px 24px; + } +} diff --git a/src/components/domains/chat/ChatMessage/MessageBubble/MessageBubble.tsx b/src/components/domains/chat/ChatMessage/MessageBubble/MessageBubble.tsx new file mode 100644 index 0000000..3446299 --- /dev/null +++ b/src/components/domains/chat/ChatMessage/MessageBubble/MessageBubble.tsx @@ -0,0 +1,16 @@ +import clsx from "clsx"; + +import styles from "./MessageBubble.module.scss"; + +interface MessageBubbleProps { + isOwn: boolean; + children: React.ReactNode; +} + +export default function MessageBubble({ isOwn, children }: MessageBubbleProps) { + return ( +
+ {children} +
+ ); +} diff --git a/src/components/domains/chat/ChatMessage/MessageImage/MessageImage.module.scss b/src/components/domains/chat/ChatMessage/MessageImage/MessageImage.module.scss new file mode 100644 index 0000000..01bd3f8 --- /dev/null +++ b/src/components/domains/chat/ChatMessage/MessageImage/MessageImage.module.scss @@ -0,0 +1,31 @@ +.imageWrapper { + position: relative; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + background: #e9e9e9; + overflow: hidden; +} + +.image { + position: relative; + height: 100%; + display: block; + object-fit: contain; + background: #e9e9e9; + z-index: 5; +} + +.bgImage { + object-fit: cover; + filter: blur(16px); +} + +.fallback { + padding: 16px; +} + +.spinner { + margin: 16px; +} diff --git a/src/components/domains/chat/ChatMessage/MessageImage/MessageImage.tsx b/src/components/domains/chat/ChatMessage/MessageImage/MessageImage.tsx new file mode 100644 index 0000000..b879b76 --- /dev/null +++ b/src/components/domains/chat/ChatMessage/MessageImage/MessageImage.tsx @@ -0,0 +1,60 @@ +"use client"; + +import { useState } from "react"; +import Image from "next/image"; +import { useTranslations } from "next-intl"; +import clsx from "clsx"; + +import { Typography } from "@/components/ui"; + +import styles from "./MessageImage.module.scss"; + +interface MessageImageProps { + src: string; + alt?: string; +} + +export default function MessageImage({ src, alt = "" }: MessageImageProps) { + const t = useTranslations("Chat"); + + // const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); + + return ( +
+ {!error ? ( + <> + {alt} + {alt} setLoading(false)} + onError={() => setError(true)} + loading="lazy" + /> + + ) : ( + + {t("message_image_fallback")} + + )} + {/* {loading && } */} +
+ ); +} diff --git a/src/components/domains/chat/ChatMessage/MessageMeta/MessageMeta.module.scss b/src/components/domains/chat/ChatMessage/MessageMeta/MessageMeta.module.scss new file mode 100644 index 0000000..ab7d34f --- /dev/null +++ b/src/components/domains/chat/ChatMessage/MessageMeta/MessageMeta.module.scss @@ -0,0 +1,6 @@ +.meta { + display: flex; + align-items: center; + padding-inline: 8px; + gap: 4px; +} diff --git a/src/components/domains/chat/ChatMessage/MessageMeta/MessageMeta.tsx b/src/components/domains/chat/ChatMessage/MessageMeta/MessageMeta.tsx new file mode 100644 index 0000000..8358043 --- /dev/null +++ b/src/components/domains/chat/ChatMessage/MessageMeta/MessageMeta.tsx @@ -0,0 +1,21 @@ +import { Typography } from "@/components/ui"; + +import styles from "./MessageMeta.module.scss"; + +interface MessageMetaProps { + time: string | null; + children?: React.ReactNode; +} + +export default function MessageMeta({ time, children }: MessageMetaProps) { + return ( +
+ {time && ( + + {time} + + )} + {children} +
+ ); +} diff --git a/src/components/domains/chat/ChatMessage/MessageStatus/MessageStatus.module.scss b/src/components/domains/chat/ChatMessage/MessageStatus/MessageStatus.module.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/components/domains/chat/ChatMessage/MessageStatus/MessageStatus.tsx b/src/components/domains/chat/ChatMessage/MessageStatus/MessageStatus.tsx new file mode 100644 index 0000000..572d994 --- /dev/null +++ b/src/components/domains/chat/ChatMessage/MessageStatus/MessageStatus.tsx @@ -0,0 +1,18 @@ +import { Icon, IconName } from "@/components/ui"; + +interface MessageStatusProps { + isRead?: boolean; +} + +export default function MessageStatus({ isRead }: MessageStatusProps) { + return ( + + ); +} diff --git a/src/components/domains/chat/ChatMessage/MessageText/MessageText.module.scss b/src/components/domains/chat/ChatMessage/MessageText/MessageText.module.scss new file mode 100644 index 0000000..92c4a0f --- /dev/null +++ b/src/components/domains/chat/ChatMessage/MessageText/MessageText.module.scss @@ -0,0 +1,9 @@ +.text { + line-height: 23px; + padding: 12px 16px; + overflow-wrap: anywhere; + + &.own { + color: #ffffff; + } +} diff --git a/src/components/domains/chat/ChatMessage/MessageText/MessageText.tsx b/src/components/domains/chat/ChatMessage/MessageText/MessageText.tsx new file mode 100644 index 0000000..e917056 --- /dev/null +++ b/src/components/domains/chat/ChatMessage/MessageText/MessageText.tsx @@ -0,0 +1,31 @@ +import clsx from "clsx"; + +import { Typography } from "@/components/ui"; + +import styles from "./MessageText.module.scss"; + +interface MessageTextProps { + text?: string; + isOwn: boolean; + className?: string; +} + +export default function MessageText({ + text, + isOwn, + className, +}: MessageTextProps) { + return ( + + {text} + + ); +} diff --git a/src/components/domains/chat/ChatMessage/MessageTyping/MessageTyping.module.scss b/src/components/domains/chat/ChatMessage/MessageTyping/MessageTyping.module.scss new file mode 100644 index 0000000..a53982f --- /dev/null +++ b/src/components/domains/chat/ChatMessage/MessageTyping/MessageTyping.module.scss @@ -0,0 +1,35 @@ +.loadingMessage { + display: flex; + align-items: flex-end; + gap: 4px; + padding: 16px; + + & > .dot { + display: block; + width: 4px; + height: 4px; + border-radius: 50%; + background-color: #333; + animation: bounceDot 1.2s infinite ease-in-out; + + &:nth-child(2) { + animation-delay: 0.15s; + } + + &:nth-child(3) { + animation-delay: 0.3s; + } + } +} + +@keyframes bounceDot { + 0%, + 60%, + 100% { + transform: translateY(0); + } + + 30% { + transform: translateY(-6px); + } +} diff --git a/src/components/domains/chat/ChatMessage/MessageTyping/MessageTyping.tsx b/src/components/domains/chat/ChatMessage/MessageTyping/MessageTyping.tsx new file mode 100644 index 0000000..7780eb7 --- /dev/null +++ b/src/components/domains/chat/ChatMessage/MessageTyping/MessageTyping.tsx @@ -0,0 +1,11 @@ +import styles from "./MessageTyping.module.scss"; + +export default function MessageTyping() { + return ( +
+ + + +
+ ); +} diff --git a/src/components/domains/chat/ChatMessages/ChatMessages.module.scss b/src/components/domains/chat/ChatMessages/ChatMessages.module.scss new file mode 100644 index 0000000..49f9b38 --- /dev/null +++ b/src/components/domains/chat/ChatMessages/ChatMessages.module.scss @@ -0,0 +1,11 @@ +.container { + width: 100%; + // height: 100%; + min-height: 100%; + padding: 36px 16px; + display: flex; + flex-direction: column; + justify-content: flex-start; + flex-direction: column-reverse; + gap: 8px; +} diff --git a/src/components/domains/chat/ChatMessages/ChatMessages.tsx b/src/components/domains/chat/ChatMessages/ChatMessages.tsx new file mode 100644 index 0000000..3dc786f --- /dev/null +++ b/src/components/domains/chat/ChatMessages/ChatMessages.tsx @@ -0,0 +1,33 @@ +import { ChatMessage, ChatMessageProps } from ".."; + +import styles from "./ChatMessages.module.scss"; + +interface ChatMessagesProps { + messages: ChatMessageProps["message"][]; + isLoadingAdvisorMessage?: boolean; +} + +export default function ChatMessages({ + messages, + isLoadingAdvisorMessage, +}: ChatMessagesProps) { + return ( +
+ {isLoadingAdvisorMessage && ( + + )} + {messages.map(message => ( + + ))} +
+ ); +} diff --git a/src/components/domains/chat/ChatMessagesWrapper/ChatMessagesWrapper.module.scss b/src/components/domains/chat/ChatMessagesWrapper/ChatMessagesWrapper.module.scss new file mode 100644 index 0000000..af59278 --- /dev/null +++ b/src/components/domains/chat/ChatMessagesWrapper/ChatMessagesWrapper.module.scss @@ -0,0 +1,18 @@ +.messagesWrapper { + flex: 1 1 0%; + overflow-y: auto; + scroll-behavior: smooth; + transition: padding-bottom 0.3s ease-in-out; +} + +.loaderTop { + display: flex; + justify-content: center; + padding-top: 16px; +} + +.suggestions.suggestions { + // position: sticky; + // bottom: 0; + padding: 0 16px 36px; +} diff --git a/src/components/domains/chat/ChatMessagesWrapper/ChatMessagesWrapper.tsx b/src/components/domains/chat/ChatMessagesWrapper/ChatMessagesWrapper.tsx new file mode 100644 index 0000000..0de2f99 --- /dev/null +++ b/src/components/domains/chat/ChatMessagesWrapper/ChatMessagesWrapper.tsx @@ -0,0 +1,100 @@ +"use client"; + +import { useCallback, useEffect, useMemo, useState } from "react"; + +import { Spinner } from "@/components/ui"; +import { useChat } from "@/providers/chat-provider"; +import { useChatStore } from "@/providers/chat-store-provider"; +import { formatTime } from "@/shared/utils/date"; + +import { ChatMessages, Suggestions } from ".."; + +import styles from "./ChatMessagesWrapper.module.scss"; + +export default function ChatMessagesWrapper() { + const { + messages: socketMessages, + isLoadingAdvisorMessage, + hasMoreOlderMessages, + isLoadingOlder, + messagesWrapperRef, + loadOlder, + scrollToBottom, + send, + } = useChat(); + + const { _hasHydrated } = useChatStore(state => state); + + const [isLoadOlder, setIsLoadOlder] = useState(false); + + const handleScroll = useCallback(() => { + const el = messagesWrapperRef.current; + if (!el) return; + + if (el.scrollTop < 100) { + setIsLoadOlder(true); + loadOlder(); + } + }, [loadOlder, messagesWrapperRef]); + + const mappedMessages = useMemo(() => { + const msgs = socketMessages.map(m => ({ + id: m.id, + type: "text" as const, + content: m.text, + isOwn: m.role === "user", + isRead: m.isRead, + time: formatTime(m.createdDate), + })); + return msgs; + }, [socketMessages]); + + useEffect(() => { + if (isLoadOlder) { + setIsLoadOlder(false); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [socketMessages]); + + useEffect(() => { + if (socketMessages.length > 0 && _hasHydrated && !isLoadOlder) { + const timeout = setTimeout(() => { + scrollToBottom(); + }); + return () => clearTimeout(timeout); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [socketMessages.length, scrollToBottom, _hasHydrated]); + + return ( +
+ {isLoadingOlder && hasMoreOlderMessages && ( +
+ +
+ )} + + { + send(suggestion); + }} + /> +
+ ); +} + +export const ChatMessagesWrapperLoader = () => { + return ( +
+ +
+ ); +}; diff --git a/src/components/domains/chat/ChatModalsWrapper/ChatModalsWrapper.module.scss b/src/components/domains/chat/ChatModalsWrapper/ChatModalsWrapper.module.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/components/domains/chat/ChatModalsWrapper/ChatModalsWrapper.tsx b/src/components/domains/chat/ChatModalsWrapper/ChatModalsWrapper.tsx new file mode 100644 index 0000000..0d2852e --- /dev/null +++ b/src/components/domains/chat/ChatModalsWrapper/ChatModalsWrapper.tsx @@ -0,0 +1,78 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useTranslations } from "next-intl"; + +import { ModalSheet } from "@/components/ui"; +import { useChat } from "@/providers/chat-provider"; +import { useToast } from "@/providers/toast-provider"; + +import { RefillOptionsModal, RefillTimerModal } from ".."; + +export default function ChatModalsWrapper() { + const t = useTranslations("Chat"); + const { refillModals } = useChat(); + const { addToast } = useToast(); + + const [isModalOpen, setIsModalOpen] = useState(false); + const [modalChild, setModalChild] = useState< + "refill-timer" | "refill-options" | null + >(null); + + const handleModalClose = () => { + setIsModalOpen(false); + const timeout = setTimeout(() => { + setModalChild(null); + }, 300); + + return () => clearTimeout(timeout); + }; + + useEffect(() => { + if (!!refillModals?.oneClick) { + setIsModalOpen(true); + return setModalChild("refill-timer"); + } + if (!!refillModals?.products) { + setIsModalOpen(true); + return setModalChild("refill-options"); + } + }, [refillModals]); + + const handlePaymentSuccess = () => { + handleModalClose(); + }; + + const handlePaymentError = () => { + addToast({ + variant: "error", + message: t("payment_error"), + duration: 5000, + }); + }; + + return ( + + {modalChild === "refill-timer" && !!refillModals?.oneClick && ( + setModalChild("refill-options")} + onDontWantToContinue={() => setModalChild("refill-options")} + onPaymentSuccess={handlePaymentSuccess} + onPaymentError={handlePaymentError} + /> + )} + {modalChild === "refill-options" && !!refillModals?.products && ( + + )} + + ); +} diff --git a/src/components/domains/chat/CorrespondenceStarted/CorrespondenceStarted.module.scss b/src/components/domains/chat/CorrespondenceStarted/CorrespondenceStarted.module.scss new file mode 100644 index 0000000..cafa006 --- /dev/null +++ b/src/components/domains/chat/CorrespondenceStarted/CorrespondenceStarted.module.scss @@ -0,0 +1,32 @@ +.card.card { + padding: 13px 0; +} + +.header { + margin-left: 21px; + display: flex; + align-items: center; + gap: 8px; +} + +.chats { + & > .chat { + background-color: transparent; + border: none; + box-shadow: none; + border-bottom: 1px solid #f3f4f6; + + &:last-child { + border-bottom: none; + } + } +} + +.container.container { + position: relative; + width: 100%; + height: fit-content; + display: flex; + flex-direction: column; + gap: 8px; +} diff --git a/src/components/domains/chat/CorrespondenceStarted/CorrespondenceStarted.tsx b/src/components/domains/chat/CorrespondenceStarted/CorrespondenceStarted.tsx new file mode 100644 index 0000000..7053a1f --- /dev/null +++ b/src/components/domains/chat/CorrespondenceStarted/CorrespondenceStarted.tsx @@ -0,0 +1,115 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { useTranslations } from "next-intl"; + +import { Card, Icon, IconName, Typography } from "@/components/ui"; +import { ChatItem } from "@/components/widgets"; +import { IChat } from "@/entities/chats/types"; +import { useChatStore } from "@/providers/chat-store-provider"; +import { ROUTES } from "@/shared/constants/client-routes"; +import { formatTime } from "@/shared/utils/date"; + +import styles from "./CorrespondenceStarted.module.scss"; + +const getTopPositionItem = (index: number) => { + return Array.from({ length: index }, (_, i) => i).reduce((acc, current) => { + return acc + 11 / 1.5 ** current; + }, 0); +}; + +interface CorrespondenceStartedProps { + chats: IChat[]; + isVisibleAll?: boolean; + maxHideVisibleCount?: number; +} + +export default function CorrespondenceStarted({ + chats, + isVisibleAll = false, + maxHideVisibleCount = 3, +}: CorrespondenceStartedProps) { + const router = useRouter(); + const setCurrentChat = useChatStore(state => state.setCurrentChat); + + return ( + + {chats + .slice(0, isVisibleAll ? chats.length : maxHideVisibleCount) + .map((chat, index) => ( + { + setCurrentChat(chat); + router.push(ROUTES.chat(chat.assistantId)); + }} + /> + ))} + + ); +} + +function Container({ + children, + isVisibleAll, + chats, +}: { + children: React.ReactNode; + isVisibleAll: boolean; + chats: IChat[]; +}) { + const t = useTranslations("Chat"); + + if (isVisibleAll) { + return ( + +
+ + + {t("correspondence_started.pinned_chats")} + +
+ +
{children}
+
+ ); + } + return ( +
+ {children} +
+ ); +} diff --git a/src/components/domains/chat/CorrespondenceStartedWrapper/CorrespondenceStartedWrapper.tsx b/src/components/domains/chat/CorrespondenceStartedWrapper/CorrespondenceStartedWrapper.tsx new file mode 100644 index 0000000..658bfea --- /dev/null +++ b/src/components/domains/chat/CorrespondenceStartedWrapper/CorrespondenceStartedWrapper.tsx @@ -0,0 +1,62 @@ +"use client"; + +import { use } from "react"; +import { useTranslations } from "next-intl"; + +import { Skeleton } from "@/components/ui"; +import { IGetChatsListResponse } from "@/entities/chats/types"; +import { useChatsSocket } from "@/hooks/chats/useChatsSocket"; +import { useAppUiStore } from "@/providers/app-ui-store-provider"; + +import { ChatItemsList, CorrespondenceStarted } from ".."; + +interface CorrespondenceStartedWrapperProps { + chatsPromise: Promise; +} + +export default function CorrespondenceStartedWrapper({ + chatsPromise, +}: CorrespondenceStartedWrapperProps) { + const t = useTranslations("Chat"); + const chats = use(chatsPromise); + const { startedChats } = useChatsSocket({ initialChats: chats }); + + const { isVisibleAll } = useAppUiStore( + state => state.chats.correspondenceStarted + ); + const hasHydrated = useAppUiStore(state => state._hasHydrated); + const setChatsCorrespondenceStarted = useAppUiStore( + state => state.setChatsCorrespondenceStarted + ); + + if (!hasHydrated) return ; + + return ( + <> + {!!startedChats.length && ( + { + setChatsCorrespondenceStarted({ + isVisibleAll: !isVisibleAll, + }); + }, + }} + isVisibleViewAll={startedChats.length > 1} + > + + + )} + + ); +} + +export const CorrespondenceStartedSkeleton = () => { + return ; +}; diff --git a/src/components/domains/chat/CreditsModals/BenefitsList/BenefitsList.module.scss b/src/components/domains/chat/CreditsModals/BenefitsList/BenefitsList.module.scss new file mode 100644 index 0000000..54c7ff9 --- /dev/null +++ b/src/components/domains/chat/CreditsModals/BenefitsList/BenefitsList.module.scss @@ -0,0 +1,19 @@ +.benefits { + margin-top: 22px; +} + +.benefit { + display: flex; + flex-direction: column; + gap: 4px; +} + +.header { + display: flex; + align-items: center; + gap: 8px; + + & > .title { + color: #111827; + } +} diff --git a/src/components/domains/chat/CreditsModals/BenefitsList/BenefitsList.tsx b/src/components/domains/chat/CreditsModals/BenefitsList/BenefitsList.tsx new file mode 100644 index 0000000..3090787 --- /dev/null +++ b/src/components/domains/chat/CreditsModals/BenefitsList/BenefitsList.tsx @@ -0,0 +1,48 @@ +import { useMessages, useTranslations } from "next-intl"; + +import { Grid, Icon, IconName, Typography } from "@/components/ui"; + +import styles from "./BenefitsList.module.scss"; + +export default function BenefitsList() { + const t = useTranslations("RefillOptionsModal.benefits"); + + const messages = useMessages(); + const keys = Object.keys(messages.RefillOptionsModal.benefits); + + const icons = [ + IconName.Thunderbolt, + IconName.Shield, + IconName.Check, + IconName.Star, + ]; + + return ( + + {keys.map((key, idx) => ( +
+
+ {icons[idx] && ( + + )} + + {t(`${key}.title`)} + +
+ + {t(`${key}.description`)} + +
+ ))} +
+ ); +} diff --git a/src/components/domains/chat/CreditsModals/RefillOption/RefillOption.module.scss b/src/components/domains/chat/CreditsModals/RefillOption/RefillOption.module.scss new file mode 100644 index 0000000..0a0eeb0 --- /dev/null +++ b/src/components/domains/chat/CreditsModals/RefillOption/RefillOption.module.scss @@ -0,0 +1,58 @@ +.option.option { + position: relative; + background-color: #ffffff; + box-shadow: + 0px 4px 6px 0px #0000001a, + 0px 2px 4px 0px #0000001a; + border-radius: 16px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + border: 3px solid transparent; + padding: 24px 14px 8px; + min-width: 100px; + width: fit-content; + + &.selected.selected { + border-color: #3b82f6; + } +} + +.popularBadge.popularBadge { + padding: 4px 12px; + background-color: #3b82f6; + border-radius: 16px; + position: absolute; + top: -14px; + left: 50%; + transform: translate(-50%, 0); +} + +.checkIcon.checkIcon { + border: 2px solid #b8babf; + border-radius: 50%; + position: relative; + + &.selected.selected { + border-color: #3b82f6; + background-color: #3b82f6; + } +} + +.credits.credits { + font-size: 20px; + margin-top: 12px; +} + +.bonus.bonus { + background-color: #22c55e; + padding: 2px 17px; + border-radius: 999px; + margin-top: 8px; +} + +.price.price { + color: #111827; + margin-top: 16px; +} diff --git a/src/components/domains/chat/CreditsModals/RefillOption/RefillOption.tsx b/src/components/domains/chat/CreditsModals/RefillOption/RefillOption.tsx new file mode 100644 index 0000000..fdc977e --- /dev/null +++ b/src/components/domains/chat/CreditsModals/RefillOption/RefillOption.tsx @@ -0,0 +1,68 @@ +import { useTranslations } from "next-intl"; +import clsx from "clsx"; + +import { Button, Icon, IconName, Typography } from "@/components/ui"; +import { IRefillModalsProduct } from "@/services/socket/events"; +import { getFormattedPrice } from "@/shared/utils/price"; + +import styles from "./RefillOption.module.scss"; + +interface RefillOptionProps { + product: IRefillModalsProduct; + selected?: boolean; + popular?: boolean; + onClick?: () => void; + className?: string; +} + +export default function RefillOption({ + product, + selected = false, + popular = false, + onClick, + className, +}: RefillOptionProps) { + const t = useTranslations("RefillOptionsModal.refill_option"); + const { credits, price, bonus, currency } = product; + + return ( + + ); +} diff --git a/src/components/domains/chat/CreditsModals/RefillOptions/RefillOptions.module.scss b/src/components/domains/chat/CreditsModals/RefillOptions/RefillOptions.module.scss new file mode 100644 index 0000000..a8190d2 --- /dev/null +++ b/src/components/domains/chat/CreditsModals/RefillOptions/RefillOptions.module.scss @@ -0,0 +1,4 @@ +.options { + display: flex; + gap: 12px; +} diff --git a/src/components/domains/chat/CreditsModals/RefillOptions/RefillOptions.tsx b/src/components/domains/chat/CreditsModals/RefillOptions/RefillOptions.tsx new file mode 100644 index 0000000..5a3bf77 --- /dev/null +++ b/src/components/domains/chat/CreditsModals/RefillOptions/RefillOptions.tsx @@ -0,0 +1,41 @@ +"use client"; + +import clsx from "clsx"; + +import { IRefillModalsProduct } from "@/services/socket/events"; + +import RefillOption from "../RefillOption/RefillOption"; + +import styles from "./RefillOptions.module.scss"; + +interface RefillOptionsProps { + options: IRefillModalsProduct[]; + selectedOption?: IRefillModalsProduct; + onChange?: (selected: IRefillModalsProduct) => void; + className?: string; +} + +export default function RefillOptions({ + options, + selectedOption, + onChange, + className, +}: RefillOptionsProps) { + const handleSelect = (option: IRefillModalsProduct) => { + onChange?.(option); + }; + + return ( +
+ {options.map((option, idx) => ( + handleSelect(option)} + /> + ))} +
+ ); +} diff --git a/src/components/domains/chat/CreditsModals/RefillOptionsHeader/RefillOptionsHeader.module.scss b/src/components/domains/chat/CreditsModals/RefillOptionsHeader/RefillOptionsHeader.module.scss new file mode 100644 index 0000000..c2e4ed2 --- /dev/null +++ b/src/components/domains/chat/CreditsModals/RefillOptionsHeader/RefillOptionsHeader.module.scss @@ -0,0 +1,41 @@ +.container { + display: grid; + grid-template-columns: 48px 1fr 92px; + justify-items: start; + gap: 16px; + margin-top: 16px; + + & > .avatar { + border-radius: 50%; + object-fit: cover; + } + + & > .info { + display: flex; + flex-direction: column; + gap: 4px; + + & > .title { + font-size: 18px; + color: #111827; + } + + & > .subtitle { + color: #585e69; + } + } + + & > .timer { + display: flex; + align-items: center; + justify-content: center; + background: #f97316; + border-radius: 100px; + width: 92px; + height: 36px; + + & > .timerValue { + font-size: 18px; + } + } +} diff --git a/src/components/domains/chat/CreditsModals/RefillOptionsHeader/RefillOptionsHeader.tsx b/src/components/domains/chat/CreditsModals/RefillOptionsHeader/RefillOptionsHeader.tsx new file mode 100644 index 0000000..e1a6499 --- /dev/null +++ b/src/components/domains/chat/CreditsModals/RefillOptionsHeader/RefillOptionsHeader.tsx @@ -0,0 +1,64 @@ +"use client"; + +import { useEffect } from "react"; +import Image from "next/image"; +import { useTranslations } from "next-intl"; + +import { Typography } from "@/components/ui"; +import { useTimer } from "@/hooks/timer/useTimer"; +import { useChatStore } from "@/providers/chat-store-provider"; +import { useUser } from "@/providers/user-provider"; + +import styles from "./RefillOptionsHeader.module.scss"; + +interface RefillOptionsHeaderProps { + onTimerLeft?: () => void; +} + +export default function RefillOptionsHeader({ + onTimerLeft, +}: RefillOptionsHeaderProps) { + const t = useTranslations("RefillOptionsModal.header"); + const { time, isFinished } = useTimer({ + initialSeconds: 60, + }); + + const currentChat = useChatStore(state => state.currentChat); + const { user } = useUser(); + + useEffect(() => { + if (isFinished) { + onTimerLeft?.(); + } + }, [isFinished, onTimerLeft]); + + return ( +
+ User avatar +
+ + {t("title", { name: currentChat?.assistantName || "" })} + + + {t("subtitle", { name: user?.profile.name || "" })} + +
+
+ + {time} + +
+
+ ); +} diff --git a/src/components/domains/chat/CreditsModals/RefillOptionsModal/RefillOptionsModal.module.scss b/src/components/domains/chat/CreditsModals/RefillOptionsModal/RefillOptionsModal.module.scss new file mode 100644 index 0000000..a504d51 --- /dev/null +++ b/src/components/domains/chat/CreditsModals/RefillOptionsModal/RefillOptionsModal.module.scss @@ -0,0 +1,22 @@ +.container { + display: flex; + flex-direction: column; + align-items: center; +} + +.options { + margin-top: 36px; +} + +.button { + max-width: 310px; + background-color: #2563eb; + padding: 16px 32px; + min-height: 60px; + margin-top: 43px; + border-radius: 16px; + + & > .buttonText { + font-size: 22px; + } +} diff --git a/src/components/domains/chat/CreditsModals/RefillOptionsModal/RefillOptionsModal.tsx b/src/components/domains/chat/CreditsModals/RefillOptionsModal/RefillOptionsModal.tsx new file mode 100644 index 0000000..9063c4a --- /dev/null +++ b/src/components/domains/chat/CreditsModals/RefillOptionsModal/RefillOptionsModal.tsx @@ -0,0 +1,79 @@ +"use client"; + +import { useState } from "react"; +import { useTranslations } from "next-intl"; + +import { Button, Spinner, Typography } from "@/components/ui"; +import { useSingleCheckout } from "@/hooks/payment/useSingleCheckout"; +import { useChatStore } from "@/providers/chat-store-provider"; +import { IRefillModals, IRefillModalsProduct } from "@/services/socket/events"; + +import BenefitsList from "../BenefitsList/BenefitsList"; +import RefillOptions from "../RefillOptions/RefillOptions"; +import RefillOptionsHeader from "../RefillOptionsHeader/RefillOptionsHeader"; + +import styles from "./RefillOptionsModal.module.scss"; + +interface RefillOptionsModalProps { + data: NonNullable; + onTimerLeft?: () => void; + onPaymentSuccess?: () => void; + onPaymentError?: (error?: string) => void; +} + +export default function RefillOptionsModal({ + data, + onTimerLeft, + onPaymentSuccess, + onPaymentError, +}: RefillOptionsModalProps) { + const t = useTranslations("RefillOptionsModal"); + + const isAutoTopUp = useChatStore(state => state.isAutoTopUp); + + const { handleSingleCheckout, isLoading } = useSingleCheckout({ + onSuccess: onPaymentSuccess, + onError: onPaymentError, + }); + + const [selectedOption, setSelectedOption] = useState( + data[1] ?? data[0] + ); + + const handlePayment = () => { + if (isLoading) return; + handleSingleCheckout({ + productId: selectedOption.id, + key: selectedOption.key, + isAutoTopUp, + }); + }; + + return ( +
+ + + setSelectedOption(option)} + /> + + + + +
+ ); +} diff --git a/src/components/domains/chat/CreditsModals/RefillTimerModal/RefillTimerModal.module.scss b/src/components/domains/chat/CreditsModals/RefillTimerModal/RefillTimerModal.module.scss new file mode 100644 index 0000000..abd0da8 --- /dev/null +++ b/src/components/domains/chat/CreditsModals/RefillTimerModal/RefillTimerModal.module.scss @@ -0,0 +1,92 @@ +.container { + display: flex; + flex-direction: column; + align-items: center; +} + +.title { + line-height: 24px; + margin-top: 12px; +} + +.subtitle { + line-height: 24px; + margin-top: 10px; + + & > .oldCredits { + text-decoration: line-through; + } +} + +.button { + max-width: 310px; + background-color: #2563eb; + padding: 16px 32px; + min-height: 60px; + margin-top: 26px; + + & > .buttonText { + font-size: 22px; + } +} + +.autoRefillContainer { + display: grid; + grid-template-columns: 24px 1fr; + gap: 16px; + margin-top: 16px; + + & > .autoRefillIconButton { + padding: 0; + min-height: 0; + height: fit-content; + width: fit-content; + background: none; + + & > .autoRefillIcon { + background: #ffffff33; + border-radius: 50%; + padding: 4px; + height: 20px; + width: 20px; + } + } + + & > .autoRefillDescription { + line-height: 23px; + } +} + +.progressContainer { + position: relative; + width: 180px; + height: 180px; + margin-top: 16px; + + & > .progressTextContainer { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + display: flex; + flex-direction: column; + gap: 6px; + + & > .progressTextValue { + font-size: 44px; + } + } +} + +.dontWantToContinue.dontWantToContinue { + padding: 0; + min-height: 0; + width: fit-content; + margin-top: 22px; + background: none; + + & > .dontWantToContinueText { + border-bottom: 1px solid currentColor; + padding-bottom: 2px; + } +} diff --git a/src/components/domains/chat/CreditsModals/RefillTimerModal/RefillTimerModal.tsx b/src/components/domains/chat/CreditsModals/RefillTimerModal/RefillTimerModal.tsx new file mode 100644 index 0000000..f19f0da --- /dev/null +++ b/src/components/domains/chat/CreditsModals/RefillTimerModal/RefillTimerModal.tsx @@ -0,0 +1,160 @@ +"use client"; + +import { useEffect } from "react"; +import { CircularProgressbar } from "react-circular-progressbar"; +import { useTranslations } from "next-intl"; + +import { Button, Icon, IconName, Spinner, Typography } from "@/components/ui"; +import { useSingleCheckout } from "@/hooks/payment/useSingleCheckout"; +import { useTimer } from "@/hooks/timer/useTimer"; +import { useChatStore } from "@/providers/chat-store-provider"; +import { IRefillModals } from "@/services/socket/events"; +import { getFormattedPrice } from "@/shared/utils/price"; + +import styles from "./RefillTimerModal.module.scss"; + +interface RefillTimerModalProps { + data: NonNullable; + onTimerLeft?: () => void; + onDontWantToContinue?: () => void; + onPaymentSuccess?: () => void; + onPaymentError?: (error?: string) => void; +} + +export default function RefillTimerModal({ + data, + onTimerLeft, + onDontWantToContinue, + onPaymentSuccess, + onPaymentError, +}: RefillTimerModalProps) { + const { timer, product, autoTopUp } = data; + const TIMER_SECONDS = (timer ?? 30_000) / 1000; + const { isAutoTopUp, setIsAutoTopUp } = useChatStore(state => state); + + const t = useTranslations("RefillTimerModal"); + + const { handleSingleCheckout, isLoading } = useSingleCheckout({ + onSuccess: onPaymentSuccess, + onError: onPaymentError, + }); + + const { seconds, isFinished } = useTimer({ + initialSeconds: TIMER_SECONDS, + }); + + useEffect(() => { + if (isFinished) { + onTimerLeft?.(); + } + }, [isFinished, onTimerLeft]); + + useEffect(() => { + setIsAutoTopUp(autoTopUp.value); + }, [autoTopUp, setIsAutoTopUp]); + + const handleGetCredits = () => { + if (isLoading) return; + handleSingleCheckout({ + productId: product.id, + key: product.key, + isAutoTopUp, + }); + }; + + return ( +
+ + {t("title")} + + + {t.rich("subtitle", { + oldCredits: () => 100, + newCredits: product.credits, + price: getFormattedPrice(product.price, product.currency), + })} + +
+
+ + {seconds} + + + {t("seconds")} + +
+ +
+ + +
+ + + {t("auto_refill_description", { + afterCredits: autoTopUp.after, + addCredits: autoTopUp.credits, + minutes: autoTopUp.minutes, + })} + +
+
+ ); +} diff --git a/src/components/domains/chat/LastMessagePreview/LastMessagePreview.module.scss b/src/components/domains/chat/LastMessagePreview/LastMessagePreview.module.scss new file mode 100644 index 0000000..42ddb38 --- /dev/null +++ b/src/components/domains/chat/LastMessagePreview/LastMessagePreview.module.scss @@ -0,0 +1,20 @@ +.messagePreview { + display: flex; + align-items: center; + gap: 2px; +} + +.text { + width: fit-content; + line-height: 20px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + display: -webkit-box; + -webkit-line-clamp: 2; + line-clamp: 2; + -webkit-box-orient: vertical; + white-space: normal; + word-break: break-word; +} diff --git a/src/components/domains/chat/LastMessagePreview/LastMessagePreview.tsx b/src/components/domains/chat/LastMessagePreview/LastMessagePreview.tsx new file mode 100644 index 0000000..d3fd91c --- /dev/null +++ b/src/components/domains/chat/LastMessagePreview/LastMessagePreview.tsx @@ -0,0 +1,81 @@ +import { useTranslations } from "next-intl"; + +import { Icon, IconName, Typography } from "@/components/ui"; + +import styles from "./LastMessagePreview.module.scss"; + +export interface LastMessagePreviewProps { + message: { + type: "text" | "voice" | "image"; + content?: string; + }; + isTyping?: boolean; + isRead?: boolean; +} + +export default function LastMessagePreview({ + message, + isTyping, + isRead, +}: LastMessagePreviewProps) { + const t = useTranslations("Chat"); + + const getMessageIcon = () => { + switch (message.type) { + case "voice": + return IconName.Microphone; + case "image": + return IconName.Image; + default: + return null; + } + }; + + const getMessageText = () => { + switch (message.type) { + case "voice": + return t("voice_message"); + case "image": + return t("photo"); + default: + return message.content; + } + }; + + if (isTyping) { + return ( + + {t("typing")} + + ); + } + + const messageIcon = getMessageIcon(); + + return ( +
+ {isRead && ( + + )} + {messageIcon && ( + + )} + + {getMessageText()} + +
+ ); +} diff --git a/src/components/domains/chat/MessageInput/MessageInput.module.scss b/src/components/domains/chat/MessageInput/MessageInput.module.scss new file mode 100644 index 0000000..a703a61 --- /dev/null +++ b/src/components/domains/chat/MessageInput/MessageInput.module.scss @@ -0,0 +1,22 @@ +.container { + padding: 16px; + width: 100%; + background-color: #fff; + border-top: 1px solid #e5e7eb; + border-radius: 16px 16px 0 0; + display: grid; + grid-template-columns: 1fr 40px; + align-items: end; + gap: 12px; +} + +.sendButton.sendButton { + width: 40px; + height: 40px; + border-radius: 50%; + background: linear-gradient(90deg, #3b82f6 0%, #4f46e5 100%); + padding: 0; + display: flex; + align-items: center; + justify-content: center; +} diff --git a/src/components/domains/chat/MessageInput/MessageInput.tsx b/src/components/domains/chat/MessageInput/MessageInput.tsx new file mode 100644 index 0000000..4f38d5f --- /dev/null +++ b/src/components/domains/chat/MessageInput/MessageInput.tsx @@ -0,0 +1,43 @@ +"use client"; + +import { useState } from "react"; +import { useTranslations } from "next-intl"; + +import { Button, Icon, IconName, TextareaAutoResize } from "@/components/ui"; + +import styles from "./MessageInput.module.scss"; + +interface MessageInputProps { + onSend: (message: string) => void; +} + +export default function MessageInput({ onSend }: MessageInputProps) { + const t = useTranslations("Chat"); + const [message, setMessage] = useState(""); + + const handleSend = () => { + if (message.trim()) { + onSend(message.trim()); + setMessage(""); + } + }; + + return ( +
+ setMessage(e.target.value)} + placeholder={t("message_input_placeholder")} + maxRows={5} + /> + +
+ ); +} diff --git a/src/components/domains/chat/MessageInputWrapper/MessageInputWrapper.module.scss b/src/components/domains/chat/MessageInputWrapper/MessageInputWrapper.module.scss new file mode 100644 index 0000000..abb98de --- /dev/null +++ b/src/components/domains/chat/MessageInputWrapper/MessageInputWrapper.module.scss @@ -0,0 +1,4 @@ +.container { + flex-shrink: 0; + position: relative; +} diff --git a/src/components/domains/chat/MessageInputWrapper/MessageInputWrapper.tsx b/src/components/domains/chat/MessageInputWrapper/MessageInputWrapper.tsx new file mode 100644 index 0000000..20e32a1 --- /dev/null +++ b/src/components/domains/chat/MessageInputWrapper/MessageInputWrapper.tsx @@ -0,0 +1,19 @@ +"use client"; + +import { useChat } from "@/providers/chat-provider"; + +import { MessageInput } from ".."; + +import styles from "./MessageInputWrapper.module.scss"; + +export default function MessageInputWrapper() { + const { send } = useChat(); + + return ( +
+
+ +
+
+ ); +} diff --git a/src/components/domains/chat/NewMessages/NewMessages.module.scss b/src/components/domains/chat/NewMessages/NewMessages.module.scss new file mode 100644 index 0000000..86995f0 --- /dev/null +++ b/src/components/domains/chat/NewMessages/NewMessages.module.scss @@ -0,0 +1,28 @@ +.container.container { + position: relative; + width: 100%; + height: fit-content; + display: flex; + flex-direction: column; + gap: 8px; +} + +.newMessage { + left: 0; + width: 100%; +} + +.card.card { + padding: 13px 0; + + & > .newMessage { + background-color: transparent; + border: none; + box-shadow: none; + border-bottom: 1px solid #f3f4f6; + + &:last-child { + border-bottom: none; + } + } +} diff --git a/src/components/domains/chat/NewMessages/NewMessages.tsx b/src/components/domains/chat/NewMessages/NewMessages.tsx new file mode 100644 index 0000000..7997c2f --- /dev/null +++ b/src/components/domains/chat/NewMessages/NewMessages.tsx @@ -0,0 +1,101 @@ +"use client"; + +import { useRouter } from "next/navigation"; + +import { Card } from "@/components/ui"; +import { ChatItem } from "@/components/widgets"; +import { IChat } from "@/entities/chats/types"; +import { useChatStore } from "@/providers/chat-store-provider"; +import { ROUTES } from "@/shared/constants/client-routes"; +import { formatTime } from "@/shared/utils/date"; + +import styles from "./NewMessages.module.scss"; + +const getTopPositionItem = (index: number) => { + return Array.from({ length: index }, (_, i) => i).reduce((acc, current) => { + return acc + 11 / 1.5 ** current; + }, 0); +}; + +interface NewMessagesProps { + chats: IChat[]; + isVisibleAll: boolean; + maxHideVisibleCount?: number; +} + +export default function NewMessages({ + chats, + isVisibleAll = false, + maxHideVisibleCount = 3, +}: NewMessagesProps) { + const router = useRouter(); + const setCurrentChat = useChatStore(state => state.setCurrentChat); + + return ( + + {chats + .slice(0, isVisibleAll ? chats.length : maxHideVisibleCount) + .map((chat, index) => ( + { + setCurrentChat(chat); + router.push(ROUTES.chat(chat.assistantId)); + }} + /> + ))} + + ); +} + +function Container({ + children, + isVisibleAll, + chats, +}: { + children: React.ReactNode; + isVisibleAll: boolean; + chats: IChat[]; +}) { + if (isVisibleAll) { + return {children}; + } + return ( +
+ {children} +
+ ); +} diff --git a/src/components/domains/chat/NewMessagesWrapper/NewMessagesWrapper.tsx b/src/components/domains/chat/NewMessagesWrapper/NewMessagesWrapper.tsx new file mode 100644 index 0000000..18ab9b4 --- /dev/null +++ b/src/components/domains/chat/NewMessagesWrapper/NewMessagesWrapper.tsx @@ -0,0 +1,53 @@ +"use client"; + +import { use } from "react"; +import { useTranslations } from "next-intl"; + +import { Skeleton } from "@/components/ui"; +import { IGetChatsListResponse } from "@/entities/chats/types"; +import { useChatsSocket } from "@/hooks/chats/useChatsSocket"; +import { useAppUiStore } from "@/providers/app-ui-store-provider"; + +import { ChatItemsList, NewMessages } from ".."; + +interface NewMessagesWrapperProps { + chatsPromise: Promise; +} + +export default function NewMessagesWrapper({ + chatsPromise, +}: NewMessagesWrapperProps) { + const t = useTranslations("Chat"); + const chats = use(chatsPromise); + const { unreadChats } = useChatsSocket({ initialChats: chats }); + + const { isVisibleAll } = useAppUiStore(state => state.chats.newMessages); + const hasHydrated = useAppUiStore(state => state._hasHydrated); + const setChatsNewMessages = useAppUiStore(state => state.setChatsNewMessages); + + if (!hasHydrated) return ; + + return ( + <> + {!!unreadChats.length && ( + { + setChatsNewMessages({ isVisibleAll: !isVisibleAll }); + }, + }} + isVisibleViewAll={unreadChats.length > 1} + > + + + )} + + ); +} + +export const NewMessagesWrapperSkeleton = () => { + return ; +}; diff --git a/src/components/domains/chat/Suggestions/Suggestions.module.scss b/src/components/domains/chat/Suggestions/Suggestions.module.scss new file mode 100644 index 0000000..66391ec --- /dev/null +++ b/src/components/domains/chat/Suggestions/Suggestions.module.scss @@ -0,0 +1,20 @@ +.container { + display: flex; + flex-wrap: wrap; + gap: 12px; + padding: 12px 16px; + + & > .suggestion { + width: fit-content; + padding: 8px 16px; + border-radius: 9999px; + border: 2px solid rgba(229, 231, 235, 1); + background: var(--background); + cursor: pointer; + + & > .suggestionText { + color: #1f2937; + font-size: 15px; + } + } +} diff --git a/src/components/domains/chat/Suggestions/Suggestions.tsx b/src/components/domains/chat/Suggestions/Suggestions.tsx new file mode 100644 index 0000000..b9cfc84 --- /dev/null +++ b/src/components/domains/chat/Suggestions/Suggestions.tsx @@ -0,0 +1,49 @@ +"use client"; + +import { useRef } from "react"; +import clsx from "clsx"; + +import { Typography } from "@/components/ui"; +import { useChatStore } from "@/providers/chat-store-provider"; + +import styles from "./Suggestions.module.scss"; + +interface SuggestionsProps { + className?: string; + onSuggestionClick: (suggestion: string) => void; +} + +export default function Suggestions({ + className, + onSuggestionClick, +}: SuggestionsProps) { + const { suggestions } = useChatStore(state => state); + const suggestionsRef = useRef(null); + + return ( + <> + {!!suggestions?.length && ( +
+ {suggestions?.map((suggestion, index) => ( +
{ + onSuggestionClick(suggestion); + }} + > + + {suggestion} + +
+ ))} +
+ )} + + ); +} diff --git a/src/components/domains/chat/ViewAll/ViewAll.module.scss b/src/components/domains/chat/ViewAll/ViewAll.module.scss new file mode 100644 index 0000000..50e2561 --- /dev/null +++ b/src/components/domains/chat/ViewAll/ViewAll.module.scss @@ -0,0 +1,8 @@ +.viewAllButton.viewAllButton { + padding: 0; + min-width: 0; + min-height: 0; + width: fit-content; + height: fit-content; + background-color: transparent; +} diff --git a/src/components/domains/chat/ViewAll/ViewAll.tsx b/src/components/domains/chat/ViewAll/ViewAll.tsx new file mode 100644 index 0000000..e844bbc --- /dev/null +++ b/src/components/domains/chat/ViewAll/ViewAll.tsx @@ -0,0 +1,25 @@ +"use client"; + +import { useTranslations } from "next-intl"; + +import { Button, Typography } from "@/components/ui"; + +import styles from "./ViewAll.module.scss"; + +export interface ViewAllProps { + isAll: boolean; + count: number; + onClick?: () => void; +} + +export default function ViewAll({ count, isAll, onClick }: ViewAllProps) { + const t = useTranslations("Chat"); + + return ( + + ); +} diff --git a/src/components/domains/chat/index.ts b/src/components/domains/chat/index.ts new file mode 100644 index 0000000..81bece5 --- /dev/null +++ b/src/components/domains/chat/index.ts @@ -0,0 +1,39 @@ +export { default as CategoryChats } from "./CategoryChats/CategoryChats"; +export { + default as ChatCategories, + ChatCategoriesSkeleton, +} from "./ChatCategories/ChatCategories"; +export { default as ChatHeader } from "./ChatHeader/ChatHeader"; +export { default as ChatItemsList } from "./ChatItemsList/ChatItemsList"; +export { default as ChatItemsListHeader } from "./ChatItemsListHeader/ChatItemsListHeader"; +export { default as ChatListHeader } from "./ChatListHeader/ChatListHeader"; +export { + default as ChatMessage, + type ChatMessageProps, +} from "./ChatMessage/ChatMessage"; +export { default as ChatMessages } from "./ChatMessages/ChatMessages"; +export { + default as ChatMessagesWrapper, + ChatMessagesWrapperLoader, +} from "./ChatMessagesWrapper/ChatMessagesWrapper"; +export { default as ChatModalsWrapper } from "./ChatModalsWrapper/ChatModalsWrapper"; +export { default as CorrespondenceStarted } from "./CorrespondenceStarted/CorrespondenceStarted"; +export { + CorrespondenceStartedSkeleton, + default as CorrespondenceStartedWrapper, +} from "./CorrespondenceStartedWrapper/CorrespondenceStartedWrapper"; +export { default as RefillOptionsModal } from "./CreditsModals/RefillOptionsModal/RefillOptionsModal"; +export { default as RefillTimerModal } from "./CreditsModals/RefillTimerModal/RefillTimerModal"; +export { + default as LastMessagePreview, + type LastMessagePreviewProps, +} from "./LastMessagePreview/LastMessagePreview"; +export { default as MessageInput } from "./MessageInput/MessageInput"; +export { default as MessageInputWrapper } from "./MessageInputWrapper/MessageInputWrapper"; +export { default as NewMessages } from "./NewMessages/NewMessages"; +export { + default as NewMessagesWrapper, + NewMessagesWrapperSkeleton, +} from "./NewMessagesWrapper/NewMessagesWrapper"; +export { default as Suggestions } from "./Suggestions/Suggestions"; +export { default as ViewAll, type ViewAllProps } from "./ViewAll/ViewAll"; diff --git a/src/components/domains/dashboard/cards/AdviserCard/AdviserCard.tsx b/src/components/domains/dashboard/cards/AdviserCard/AdviserCard.tsx index b65da6d..45a0eb3 100644 --- a/src/components/domains/dashboard/cards/AdviserCard/AdviserCard.tsx +++ b/src/components/domains/dashboard/cards/AdviserCard/AdviserCard.tsx @@ -1,21 +1,35 @@ +"use client"; + +import { useRouter } from "next/navigation"; + import { Button, Card, Stars, Typography } from "@/components/ui"; +import { IChat } from "@/entities/chats/types"; import { Assistant } from "@/entities/dashboard/types"; +import { useChatStore } from "@/providers/chat-store-provider"; +import { ROUTES } from "@/shared/constants/client-routes"; import styles from "./AdviserCard.module.scss"; -type AdviserCardProps = Assistant; +type AdviserCardProps = { + assistant: Assistant; + chat: IChat | null; +}; + +export default function AdviserCard({ assistant, chat }: AdviserCardProps) { + const router = useRouter(); + const { _id, name, photoUrl, rating, reviewCount, description } = assistant; + const setCurrentChat = useChatStore(state => state.setCurrentChat); -export default function AdviserCard({ - name, - photoUrl, - rating, - reviewCount, - description, -}: AdviserCardProps) { return ( { + if (chat) { + setCurrentChat(chat); + } + router.push(ROUTES.chat(_id)); + }} >
diff --git a/src/components/domains/dashboard/sections/AdvisersSection/AdvisersSection.tsx b/src/components/domains/dashboard/sections/AdvisersSection/AdvisersSection.tsx index 75999e8..1f95f79 100644 --- a/src/components/domains/dashboard/sections/AdvisersSection/AdvisersSection.tsx +++ b/src/components/domains/dashboard/sections/AdvisersSection/AdvisersSection.tsx @@ -2,6 +2,7 @@ import { use } from "react"; import clsx from "clsx"; import { Grid, Section, Skeleton } from "@/components/ui"; +import { IChat, IGetChatsListResponse } from "@/entities/chats/types"; import { Assistant } from "@/entities/dashboard/types"; import { AdviserCard } from "../../cards"; @@ -9,15 +10,22 @@ import { AdviserCard } from "../../cards"; import styles from "./AdvisersSection.module.scss"; interface AdvisersSectionProps { - promise: Promise; + promiseAssistants: Promise; + promiseChats: Promise; gridDisplayMode?: "vertical" | "horizontal"; } +const getChatByAssistantId = (assistantId: string, chats: IChat[]) => { + return chats.find(chat => chat.assistantId === assistantId) || null; +}; + export default function AdvisersSection({ - promise, + promiseAssistants, + promiseChats, gridDisplayMode = "horizontal", }: AdvisersSectionProps) { - const assistants = use(promise); + const assistants = use(promiseAssistants); + const chats = use(promiseChats); const columns = Math.ceil(assistants?.length / 2); return ( @@ -27,7 +35,14 @@ export default function AdvisersSection({ className={clsx(styles.grid, styles[gridDisplayMode])} > {assistants.map(adviser => ( - + ))}
diff --git a/src/components/domains/dashboard/sections/NewMessagesSection/NewMessagesSection.module.scss b/src/components/domains/dashboard/sections/NewMessagesSection/NewMessagesSection.module.scss new file mode 100644 index 0000000..1d09984 --- /dev/null +++ b/src/components/domains/dashboard/sections/NewMessagesSection/NewMessagesSection.module.scss @@ -0,0 +1,6 @@ +.container { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 16px; +} diff --git a/src/components/domains/dashboard/sections/NewMessagesSection/NewMessagesSection.tsx b/src/components/domains/dashboard/sections/NewMessagesSection/NewMessagesSection.tsx new file mode 100644 index 0000000..c5a31ca --- /dev/null +++ b/src/components/domains/dashboard/sections/NewMessagesSection/NewMessagesSection.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { use } from "react"; + +import { NewMessages, ViewAll } from "@/components/domains/chat"; +import { Skeleton } from "@/components/ui"; +import { IGetChatsListResponse } from "@/entities/chats/types"; +import { useChatsSocket } from "@/hooks/chats/useChatsSocket"; +import { useAppUiStore } from "@/providers/app-ui-store-provider"; + +import styles from "./NewMessagesSection.module.scss"; + +interface NewMessagesSectionProps { + chatsPromise: Promise; +} + +export default function NewMessagesSection({ + chatsPromise, +}: NewMessagesSectionProps) { + const chats = use(chatsPromise); + const { unreadChats } = useChatsSocket({ initialChats: chats }); + + const { isVisibleAll } = useAppUiStore(state => state.home.newMessages); + const hasHydrated = useAppUiStore(state => state._hasHydrated); + const setHomeNewMessages = useAppUiStore(state => state.setHomeNewMessages); + + if (!hasHydrated) return ; + + return ( + <> + {!!unreadChats.length && ( +
+ {unreadChats.length > 1 && ( + { + setHomeNewMessages({ isVisibleAll: !isVisibleAll }); + }} + /> + )} + +
+ )} + + ); +} + +export const NewMessagesSectionSkeleton = () => { + return ; +}; diff --git a/src/components/domains/dashboard/sections/index.ts b/src/components/domains/dashboard/sections/index.ts index 6fa52de..f373901 100644 --- a/src/components/domains/dashboard/sections/index.ts +++ b/src/components/domains/dashboard/sections/index.ts @@ -10,6 +10,10 @@ export { default as MeditationSection, MeditationSectionSkeleton, } from "./MeditationSection/MeditationSection"; +export { + default as NewMessagesSection, + NewMessagesSectionSkeleton, +} from "./NewMessagesSection/NewMessagesSection"; export { default as PalmSection, PalmSectionSkeleton, diff --git a/src/components/domains/email-marketing/compatibility/v1/AdviceFromAstrologer/AdviceFromAstrologer.module.scss b/src/components/domains/email-marketing/compatibility/v1/AdviceFromAstrologer/AdviceFromAstrologer.module.scss new file mode 100644 index 0000000..7245001 --- /dev/null +++ b/src/components/domains/email-marketing/compatibility/v1/AdviceFromAstrologer/AdviceFromAstrologer.module.scss @@ -0,0 +1,16 @@ +.container { + width: 100%; + height: fit-content; + border-radius: 30px; + backdrop-filter: blur(5px); + display: flex; + flex-direction: column; + align-items: center; + margin-top: 32px; + padding: 25px 18px 22px; + background: rgba(225, 225, 225, 0.44); +} + +.messages { + margin-top: 22px; +} diff --git a/src/components/domains/email-marketing/compatibility/v1/AdviceFromAstrologer/AdviceFromAstrologer.tsx b/src/components/domains/email-marketing/compatibility/v1/AdviceFromAstrologer/AdviceFromAstrologer.tsx new file mode 100644 index 0000000..074a44c --- /dev/null +++ b/src/components/domains/email-marketing/compatibility/v1/AdviceFromAstrologer/AdviceFromAstrologer.tsx @@ -0,0 +1,31 @@ +import Image from "next/image"; +import { useTranslations } from "next-intl"; + +import { emailMarketingCompV1Images } from "@/shared/constants/images"; +import { translatePathEmailMarketingCompatibilityV1 } from "@/shared/constants/translate"; + +import { TextWithEmoji } from ".."; + +import styles from "./AdviceFromAstrologer.module.scss"; + +export default function AdviceFromAstrologer() { + const t = useTranslations( + translatePathEmailMarketingCompatibilityV1("Landing") + ); + + return ( +
+ + messages +
+ ); +} diff --git a/src/components/domains/email-marketing/compatibility/v1/CountdownTimer/CountdownTimer.module.scss b/src/components/domains/email-marketing/compatibility/v1/CountdownTimer/CountdownTimer.module.scss new file mode 100644 index 0000000..ef8cf2e --- /dev/null +++ b/src/components/domains/email-marketing/compatibility/v1/CountdownTimer/CountdownTimer.module.scss @@ -0,0 +1,9 @@ +.timer { + background-color: #f95d53; + color: white; + padding: 3px 7px; + border-radius: 23px; + font-size: 13px; + font-weight: 500; + line-height: 125%; +} diff --git a/src/components/domains/email-marketing/compatibility/v1/CountdownTimer/CountdownTimer.tsx b/src/components/domains/email-marketing/compatibility/v1/CountdownTimer/CountdownTimer.tsx new file mode 100644 index 0000000..4644997 --- /dev/null +++ b/src/components/domains/email-marketing/compatibility/v1/CountdownTimer/CountdownTimer.tsx @@ -0,0 +1,20 @@ +"use client"; + +import { useTranslations } from "next-intl"; + +import { useTimer } from "@/hooks/timer/useTimer"; +import { translatePathEmailMarketingCompatibilityV1 } from "@/shared/constants/translate"; + +import styles from "./CountdownTimer.module.scss"; + +export default function CountdownTimer() { + const t = useTranslations( + translatePathEmailMarketingCompatibilityV1("SpecialOffer") + ); + + const { time } = useTimer({ + initialSeconds: 600, + }); + + return {t("reserved-for", { time })}; +} diff --git a/src/components/domains/email-marketing/compatibility/v1/CustomerCounter/CustomerCounter.module.scss b/src/components/domains/email-marketing/compatibility/v1/CustomerCounter/CustomerCounter.module.scss new file mode 100644 index 0000000..75ba73b --- /dev/null +++ b/src/components/domains/email-marketing/compatibility/v1/CustomerCounter/CustomerCounter.module.scss @@ -0,0 +1,88 @@ +.container { + position: relative; + width: 263px; + height: 263px; + // background: #7A6BE2; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin-top: 26px; + box-shadow: + 0px 0px 12px 0px rgba(0, 0, 0, 0.46), + inset 6px 6px 82px 0px rgba(0, 0, 0, 0.25); + -webkit-backface-visibility: hidden; + backface-visibility: hidden; +} + +.circularText { + position: absolute; + width: calc(100% + 24px); + max-width: none; + height: calc(100% + 24px); + animation: rotate 20s linear infinite; + -webkit-transform: translateZ(0); + transform: translateZ(0); + -webkit-perspective: 1000; + perspective: 1000; + -webkit-backface-visibility: hidden; + backface-visibility: hidden; + + svg { + width: 100%; + height: 100%; + -webkit-transform: translateZ(0); + transform: translateZ(0); + } + + text { + fill: white; + font-size: 8px; + font-weight: 600; + -webkit-font-smoothing: antialiased; + + &:nth-of-type(2) { + -webkit-transform: rotate(180deg) translateZ(0); + -moz-transform: rotate(180deg) translateZ(0); + -ms-transform: rotate(180deg) translateZ(0); + -o-transform: rotate(180deg) translateZ(0); + transform: rotate(180deg) translateZ(0); + + -webkit-transform-origin: center center; + -moz-transform-origin: center center; + -ms-transform-origin: center center; + -o-transform-origin: center center; + transform-origin: center center; + } + } +} + +.count { + color: white; + font-size: 37px; + line-height: 41px; + font-weight: bold; + z-index: 1; +} + +@keyframes rotate { + from { + -webkit-transform: rotate(0deg) translateZ(0); + transform: rotate(0deg) translateZ(0); + } + + to { + -webkit-transform: rotate(360deg) translateZ(0); + transform: rotate(360deg) translateZ(0); + } +} + +@-webkit-keyframes rotate { + from { + -webkit-transform: rotate(0deg) translateZ(0); + } + + to { + -webkit-transform: rotate(360deg) translateZ(0); + } +} diff --git a/src/components/domains/email-marketing/compatibility/v1/CustomerCounter/CustomerCounter.tsx b/src/components/domains/email-marketing/compatibility/v1/CustomerCounter/CustomerCounter.tsx new file mode 100644 index 0000000..ee23d68 --- /dev/null +++ b/src/components/domains/email-marketing/compatibility/v1/CustomerCounter/CustomerCounter.tsx @@ -0,0 +1,30 @@ +import Image from "next/image"; +import { useTranslations } from "next-intl"; + +import { emailMarketingCompV1Images } from "@/shared/constants/images"; +import { translatePathEmailMarketingCompatibilityV1 } from "@/shared/constants/translate"; + +import styles from "./CustomerCounter.module.scss"; + +interface CustomerCounterProps { + count: number; +} + +export default function CustomerCounter({ count }: CustomerCounterProps) { + const t = useTranslations( + translatePathEmailMarketingCompatibilityV1("Landing") + ); + + return ( +
+ circular-text-image +
{count}
+
+ ); +} diff --git a/src/components/domains/email-marketing/compatibility/v1/FindingPartner/FindingPartner.module.scss b/src/components/domains/email-marketing/compatibility/v1/FindingPartner/FindingPartner.module.scss new file mode 100644 index 0000000..75cf09f --- /dev/null +++ b/src/components/domains/email-marketing/compatibility/v1/FindingPartner/FindingPartner.module.scss @@ -0,0 +1,16 @@ +.container { + width: 100%; + height: fit-content; + border-radius: 30px; + display: flex; + flex-direction: column; + align-items: center; + margin-top: 32px; + padding: 25px 20px 0; + background: linear-gradient(180deg, #13082b 0%, #401b91 100%); + color: #fff; +} + +.smartphone { + margin-top: 29px; +} diff --git a/src/components/domains/email-marketing/compatibility/v1/FindingPartner/FindingPartner.tsx b/src/components/domains/email-marketing/compatibility/v1/FindingPartner/FindingPartner.tsx new file mode 100644 index 0000000..f3afe93 --- /dev/null +++ b/src/components/domains/email-marketing/compatibility/v1/FindingPartner/FindingPartner.tsx @@ -0,0 +1,32 @@ +import Image from "next/image"; +import { useTranslations } from "next-intl"; + +import { emailMarketingCompV1Images } from "@/shared/constants/images"; +import { translatePathEmailMarketingCompatibilityV1 } from "@/shared/constants/translate"; + +import { TextWithEmoji } from ".."; + +import styles from "./FindingPartner.module.scss"; + +export default function FindingPartner() { + const t = useTranslations( + translatePathEmailMarketingCompatibilityV1("Landing") + ); + + return ( +
+ + smartphone +
+ ); +} diff --git a/src/components/domains/email-marketing/compatibility/v1/GivePersonalizedPlan/GivePersonalizedPlan.module.scss b/src/components/domains/email-marketing/compatibility/v1/GivePersonalizedPlan/GivePersonalizedPlan.module.scss new file mode 100644 index 0000000..d7251de --- /dev/null +++ b/src/components/domains/email-marketing/compatibility/v1/GivePersonalizedPlan/GivePersonalizedPlan.module.scss @@ -0,0 +1,34 @@ +.container { + width: 100%; + height: fit-content; + background-color: #27272e; + color: #fff; + border-radius: 41px; + margin-top: 27px; +} + +.title { + font-size: 21px; + font-weight: 600; + padding: 25px 30px 0; + margin: 0; + line-height: 125%; + + & > span { + color: #7da1ff; + font-weight: 700; + font-size: 24px; + } +} + +.stars { + width: 100%; + object-fit: cover; + margin-top: -46px; +} + +.zodiacs { + width: 100%; + object-fit: cover; + margin-top: -10px; +} diff --git a/src/components/domains/email-marketing/compatibility/v1/GivePersonalizedPlan/GivePersonalizedPlan.tsx b/src/components/domains/email-marketing/compatibility/v1/GivePersonalizedPlan/GivePersonalizedPlan.tsx new file mode 100644 index 0000000..e869604 --- /dev/null +++ b/src/components/domains/email-marketing/compatibility/v1/GivePersonalizedPlan/GivePersonalizedPlan.tsx @@ -0,0 +1,37 @@ +import Image from "next/image"; +import { useTranslations } from "next-intl"; + +import { Typography } from "@/components/ui"; +import { emailMarketingCompV1Images } from "@/shared/constants/images"; +import { translatePathEmailMarketingCompatibilityV1 } from "@/shared/constants/translate"; + +import styles from "./GivePersonalizedPlan.module.scss"; + +export default function GivePersonalizedPlan() { + const t = useTranslations( + translatePathEmailMarketingCompatibilityV1("Landing") + ); + + return ( +
+ + {t("personalized-plan-title")} + {t("personalized-plan-free")} + + stars + stars +
+ ); +} diff --git a/src/components/domains/email-marketing/compatibility/v1/GuaranteedSecurityPayments/GuaranteedSecurityPayments.module.scss b/src/components/domains/email-marketing/compatibility/v1/GuaranteedSecurityPayments/GuaranteedSecurityPayments.module.scss new file mode 100644 index 0000000..d7172a2 --- /dev/null +++ b/src/components/domains/email-marketing/compatibility/v1/GuaranteedSecurityPayments/GuaranteedSecurityPayments.module.scss @@ -0,0 +1,10 @@ +.container { + width: 100%; + max-width: 255px; + display: flex; + align-items: center; + gap: 10px; + font-size: 15px; + line-height: 25px; + margin-top: 30px; +} diff --git a/src/components/domains/email-marketing/compatibility/v1/GuaranteedSecurityPayments/GuaranteedSecurityPayments.tsx b/src/components/domains/email-marketing/compatibility/v1/GuaranteedSecurityPayments/GuaranteedSecurityPayments.tsx new file mode 100644 index 0000000..6b6cee9 --- /dev/null +++ b/src/components/domains/email-marketing/compatibility/v1/GuaranteedSecurityPayments/GuaranteedSecurityPayments.tsx @@ -0,0 +1,27 @@ +import Image from "next/image"; +import { useTranslations } from "next-intl"; + +import { Typography } from "@/components/ui"; +import { emailMarketingCompV1Images } from "@/shared/constants/images"; +import { translatePathEmailMarketingCompatibilityV1 } from "@/shared/constants/translate"; + +import styles from "./GuaranteedSecurityPayments.module.scss"; + +export default function GuaranteedSecurityPayments() { + const t = useTranslations( + translatePathEmailMarketingCompatibilityV1("Landing") + ); + return ( +
+ guaranteed + + {t("guaranteed-security-payments")} + +
+ ); +} diff --git a/src/components/domains/email-marketing/compatibility/v1/InsightsRelationship/InsightsRelationship.module.scss b/src/components/domains/email-marketing/compatibility/v1/InsightsRelationship/InsightsRelationship.module.scss new file mode 100644 index 0000000..3836ae5 --- /dev/null +++ b/src/components/domains/email-marketing/compatibility/v1/InsightsRelationship/InsightsRelationship.module.scss @@ -0,0 +1,16 @@ +.container { + width: 100%; + height: fit-content; + border-radius: 30px; + display: flex; + flex-direction: column; + align-items: center; + margin-top: 32px; + padding: 25px 20px 30px; + background: #fca3fd; + color: #fff; +} + +.image.image { + margin-top: 29px; +} diff --git a/src/components/domains/email-marketing/compatibility/v1/InsightsRelationship/InsightsRelationship.tsx b/src/components/domains/email-marketing/compatibility/v1/InsightsRelationship/InsightsRelationship.tsx new file mode 100644 index 0000000..f402904 --- /dev/null +++ b/src/components/domains/email-marketing/compatibility/v1/InsightsRelationship/InsightsRelationship.tsx @@ -0,0 +1,32 @@ +import Image from "next/image"; +import { useTranslations } from "next-intl"; + +import { emailMarketingCompV1Images } from "@/shared/constants/images"; +import { translatePathEmailMarketingCompatibilityV1 } from "@/shared/constants/translate"; + +import { TextWithEmoji } from ".."; + +import styles from "./InsightsRelationship.module.scss"; + +export default function InsightsRelationship() { + const t = useTranslations( + translatePathEmailMarketingCompatibilityV1("Landing") + ); + + return ( +
+ + relationships +
+ ); +} diff --git a/src/components/domains/email-marketing/compatibility/v1/LandingButtonWrapper/LandingButtonWrapper.module.scss b/src/components/domains/email-marketing/compatibility/v1/LandingButtonWrapper/LandingButtonWrapper.module.scss new file mode 100644 index 0000000..5953a53 --- /dev/null +++ b/src/components/domains/email-marketing/compatibility/v1/LandingButtonWrapper/LandingButtonWrapper.module.scss @@ -0,0 +1,23 @@ +.buttonContainer { + width: 100%; + display: flex; + justify-content: center; + position: fixed; + bottom: 0; + pointer-events: none; + z-index: 9999; + + .buttonContinue { + position: relative; + z-index: 1000; + max-width: 307px; + min-height: 59px; + margin-top: 48px; + margin-bottom: 64px; + pointer-events: all; + + & > .text { + font-size: 25px; + } + } +} diff --git a/src/components/domains/email-marketing/compatibility/v1/LandingButtonWrapper/LandingButtonWrapper.tsx b/src/components/domains/email-marketing/compatibility/v1/LandingButtonWrapper/LandingButtonWrapper.tsx new file mode 100644 index 0000000..1fa66fd --- /dev/null +++ b/src/components/domains/email-marketing/compatibility/v1/LandingButtonWrapper/LandingButtonWrapper.tsx @@ -0,0 +1,34 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { useTranslations } from "next-intl"; + +import { Button, Typography } from "@/components/ui"; +import { BlurComponent } from "@/components/widgets"; +import { ROUTES } from "@/shared/constants/client-routes"; +import { translatePathEmailMarketingCompatibilityV1 } from "@/shared/constants/translate"; + +import styles from "./LandingButtonWrapper.module.scss"; + +export default function LandingButtonWrapper() { + const router = useRouter(); + const t = useTranslations( + translatePathEmailMarketingCompatibilityV1("Landing") + ); + + const handleContinue = () => { + router.push(ROUTES.emailMarketingCompatibilityV1SpecialOffer()); + }; + + return ( +
+ + + +
+ ); +} diff --git a/src/components/domains/email-marketing/compatibility/v1/MoneyBackGuarantee/MoneyBackGuarantee.module.scss b/src/components/domains/email-marketing/compatibility/v1/MoneyBackGuarantee/MoneyBackGuarantee.module.scss new file mode 100644 index 0000000..ff7153b --- /dev/null +++ b/src/components/domains/email-marketing/compatibility/v1/MoneyBackGuarantee/MoneyBackGuarantee.module.scss @@ -0,0 +1,19 @@ +.container { + width: 100%; + height: fit-content; + border-radius: 30px; + backdrop-filter: blur(5px); + display: flex; + flex-direction: column; + align-items: center; + margin-top: 21px; + padding: 30px 30px 30px; + background-color: rgba(212, 212, 212, 0.35); +} + +.text { + font-size: 19px; + margin-top: 22px; + line-height: 25px; + color: #2c2c2c; +} diff --git a/src/components/domains/email-marketing/compatibility/v1/MoneyBackGuarantee/MoneyBackGuarantee.tsx b/src/components/domains/email-marketing/compatibility/v1/MoneyBackGuarantee/MoneyBackGuarantee.tsx new file mode 100644 index 0000000..3a93dff --- /dev/null +++ b/src/components/domains/email-marketing/compatibility/v1/MoneyBackGuarantee/MoneyBackGuarantee.tsx @@ -0,0 +1,23 @@ +import { useTranslations } from "next-intl"; + +import { translatePathEmailMarketingCompatibilityV1 } from "@/shared/constants/translate"; + +import { TextWithEmoji } from ".."; + +import styles from "./MoneyBackGuarantee.module.scss"; + +export default function MoneyBackGuarantee() { + const t = useTranslations( + translatePathEmailMarketingCompatibilityV1("Landing") + ); + + return ( +
+ +

{t("money-back-guarantee-text")}

+
+ ); +} diff --git a/src/components/domains/email-marketing/compatibility/v1/Payments/Payments.module.scss b/src/components/domains/email-marketing/compatibility/v1/Payments/Payments.module.scss new file mode 100644 index 0000000..44830a7 --- /dev/null +++ b/src/components/domains/email-marketing/compatibility/v1/Payments/Payments.module.scss @@ -0,0 +1,8 @@ +.container { + width: 100%; + margin: 30px auto 0; + + & > img { + width: 100%; + } +} diff --git a/src/components/domains/email-marketing/compatibility/v1/Payments/Payments.tsx b/src/components/domains/email-marketing/compatibility/v1/Payments/Payments.tsx new file mode 100644 index 0000000..a998720 --- /dev/null +++ b/src/components/domains/email-marketing/compatibility/v1/Payments/Payments.tsx @@ -0,0 +1,18 @@ +import Image from "next/image"; + +import { emailMarketingCompV1Images } from "@/shared/constants/images"; + +import styles from "./Payments.module.scss"; + +export default function Payments() { + return ( +
+ payments +
+ ); +} diff --git a/src/components/domains/email-marketing/compatibility/v1/PlanIncludes/PlanIncludes.module.scss b/src/components/domains/email-marketing/compatibility/v1/PlanIncludes/PlanIncludes.module.scss new file mode 100644 index 0000000..39ecdd3 --- /dev/null +++ b/src/components/domains/email-marketing/compatibility/v1/PlanIncludes/PlanIncludes.module.scss @@ -0,0 +1,39 @@ +.container { + margin-top: 48px; + max-width: 345px; +} + +.title { + font-size: 22px; + font-weight: 600; + text-align: center; +} + +.featuresList { + list-style: none; + padding: 0; + margin: 0; + margin-top: 30px; + display: flex; + flex-direction: column; + gap: 19px; +} + +.featureItem { + position: relative; + padding-left: 20px; + font-weight: 500; + font-size: 17px; + line-height: 21px; + + &:before { + content: "•"; + position: absolute; + left: 0; + color: black; + } + + &:last-child { + margin-bottom: 0; + } +} diff --git a/src/components/domains/email-marketing/compatibility/v1/PlanIncludes/PlanIncludes.tsx b/src/components/domains/email-marketing/compatibility/v1/PlanIncludes/PlanIncludes.tsx new file mode 100644 index 0000000..e064873 --- /dev/null +++ b/src/components/domains/email-marketing/compatibility/v1/PlanIncludes/PlanIncludes.tsx @@ -0,0 +1,29 @@ +import { Typography } from "@/components/ui"; + +import styles from "./PlanIncludes.module.scss"; + +interface PlanFeature { + text: string; +} + +interface PlanIncludesProps { + features: PlanFeature[]; + title: string; +} + +export default function PlanIncludes({ title, features }: PlanIncludesProps) { + return ( +
+ + {title} + +
    + {features.map((feature, index) => ( +
  • + {feature.text} +
  • + ))} +
+
+ ); +} diff --git a/src/components/domains/email-marketing/compatibility/v1/PriceComparison/PriceComparison.module.scss b/src/components/domains/email-marketing/compatibility/v1/PriceComparison/PriceComparison.module.scss new file mode 100644 index 0000000..243042d --- /dev/null +++ b/src/components/domains/email-marketing/compatibility/v1/PriceComparison/PriceComparison.module.scss @@ -0,0 +1,75 @@ +// styles.module.scss +.container { + display: flex; + gap: 37px; + align-items: center; + margin-top: 42px; + color: #090909; +} + +.priceCard { + background: white; + border-radius: 8px; + overflow: hidden; + width: 200px; + box-shadow: 2px 2px 10px 0 rgba(0, 0, 0, 0.29); + + &.oldPriceCard { + width: 130px; + + & > .header { + padding: 20px 0; + } + + & > .priceContent { + padding: 8px 0 50px; + color: #838080; + font-weight: 600; + + & > .price { + font-size: 30px; + line-height: 40px; + } + } + } + + &.newPriceCard { + width: fit-content; + min-width: 152px; + max-width: 172px; + + & > .header { + padding: 24px 0; + background-color: #993cfe; + color: #fff; + } + + & > .priceContent { + padding: 55px 0 78px; + + & > .price { + font-size: 59px; + line-height: 25px; + font-weight: 600; + } + } + } +} + +.header { + background: #adaaab; + text-align: center; + font-weight: 600; + font-size: 18px; +} + +.priceContent { + text-align: center; +} + +.upTo { + color: #838080; + font-size: 21px; + line-height: 30px; + margin-bottom: 13px; +} diff --git a/src/components/domains/email-marketing/compatibility/v1/PriceComparison/PriceComparison.tsx b/src/components/domains/email-marketing/compatibility/v1/PriceComparison/PriceComparison.tsx new file mode 100644 index 0000000..cc131e9 --- /dev/null +++ b/src/components/domains/email-marketing/compatibility/v1/PriceComparison/PriceComparison.tsx @@ -0,0 +1,38 @@ +import { useTranslations } from "next-intl"; +import clsx from "clsx"; + +import { translatePathEmailMarketingCompatibilityV1 } from "@/shared/constants/translate"; + +import styles from "./PriceComparison.module.scss"; + +interface PriceComparisonProps { + oldPrice: number | string | React.ReactNode; + newPrice: number | string; +} + +export default function PriceComparison({ + oldPrice, + newPrice, +}: PriceComparisonProps) { + const t = useTranslations( + translatePathEmailMarketingCompatibilityV1("Landing") + ); + + return ( +
+
+
{t("old-price-label")}
+
+
{oldPrice}
+
+
+ +
+
{t("new-price-label")}
+
+
{newPrice}
+
+
+
+ ); +} diff --git a/src/components/domains/email-marketing/compatibility/v1/PricingSummary/PricingSummary.module.scss b/src/components/domains/email-marketing/compatibility/v1/PricingSummary/PricingSummary.module.scss new file mode 100644 index 0000000..40d8d9e --- /dev/null +++ b/src/components/domains/email-marketing/compatibility/v1/PricingSummary/PricingSummary.module.scss @@ -0,0 +1,102 @@ +.container { + display: flex; + align-items: center; + flex-direction: column; + width: 100%; +} + +.table { + background-color: white; + border-radius: 25px; + padding: 26px 13px 22px; + box-shadow: 0 2px 7px rgba(0, 0, 0, 0.25); + width: 100%; + margin-top: 39px; +} + +.row { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + padding: 10px 11px; +} + +.totalRow { + background-color: #ededed; + border-radius: 32px; + font-size: 17px; + line-height: 125%; + + .label { + font-weight: 500; + } + + .price { + font-weight: 500; + } +} + +.codeRow { + margin-top: 17px; + padding: 0 8px 0 11px; + + .codeApplied { + font-weight: 500; + font-size: 15px; + line-height: 125%; + } +} + +.costRow { + background-color: #ededed; + border-radius: 32px; + font-size: 17px; + line-height: 125%; + margin-top: 17px; + + .label { + font-size: 14px; + font-weight: 500; + max-width: 60%; + line-height: 125%; + } + + .prices { + display: flex; + gap: 14px; + align-items: flex-end; + } + + .originalPrice { + font-size: 13px; + text-decoration: line-through; + color: #5a5a5a; + line-height: 125%; + font-weight: 500; + } + + .discountedPrice { + font-weight: 700; + font-size: 17px; + line-height: 125%; + } +} + +.savingsRow { + width: 100%; + text-align: center; + color: #555; + font-size: 13px; + font-weight: 500; + margin-top: 15px; +} + +.description { + max-width: 295px; + font-size: 12px; + line-height: 125%; + margin-top: 20px; + text-align: center; + font-weight: 300; +} diff --git a/src/components/domains/email-marketing/compatibility/v1/PricingSummary/PricingSummary.tsx b/src/components/domains/email-marketing/compatibility/v1/PricingSummary/PricingSummary.tsx new file mode 100644 index 0000000..6e3dc5a --- /dev/null +++ b/src/components/domains/email-marketing/compatibility/v1/PricingSummary/PricingSummary.tsx @@ -0,0 +1,73 @@ +import { useTranslations } from "next-intl"; +import clsx from "clsx"; + +import { translatePathEmailMarketingCompatibilityV1 } from "@/shared/constants/translate"; +import { getFormattedPrice } from "@/shared/utils/price"; +import { Currency } from "@/types"; + +import { CountdownTimer } from ".."; + +import styles from "./PricingSummary.module.scss"; + +interface PricingSummaryProps { + totalToday: number | string; + originalPrice: number | string; + discountedPrice: number | string; + trialDuration: number | string; + saveText: string; + currency: Currency; +} + +export default function PricingSummary({ + totalToday, + originalPrice, + discountedPrice, + trialDuration, + saveText, + currency, +}: PricingSummaryProps) { + const t = useTranslations( + translatePathEmailMarketingCompatibilityV1("SpecialOffer") + ); + + return ( +
+
+
+ + {t("pricing-summary-total-today")} + + + {getFormattedPrice(Number(totalToday), currency)} + +
+
+ + {t("pricing-summary-code-applied")} + + +
+
+ + {t("pricing-summary-cost-after-trial")} + +
+ + {getFormattedPrice(Number(originalPrice), currency)} + + + {getFormattedPrice(Number(discountedPrice), currency)} + +
+
+
{saveText}
+
+

+ {t("pricing-summary-trial-description", { + totalToday: getFormattedPrice(Number(totalToday), currency), + trialDuration: trialDuration, + })} +

+
+ ); +} diff --git a/src/components/domains/email-marketing/compatibility/v1/Review/Review.module.scss b/src/components/domains/email-marketing/compatibility/v1/Review/Review.module.scss new file mode 100644 index 0000000..91d2d6e --- /dev/null +++ b/src/components/domains/email-marketing/compatibility/v1/Review/Review.module.scss @@ -0,0 +1,74 @@ +.container { + width: 100%; + border-radius: 10px; + padding: 6px 20px 14px 7px; + background-color: #fff; + box-shadow: 1px 4px 10px 0px rgba(0, 0, 0, 0.21); +} + +.header { + display: grid; + grid-template-columns: 35px 1fr; + align-items: center; + + gap: 4px; + + .avatar { + width: 35px; + height: 35px; + } + + & > .info { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: 2px; + color: #000; + + & .name { + line-height: 125%; + } + + & .date { + font-size: 12px; + line-height: 125%; + } + + & > .stars { + display: flex; + flex-direction: row; + align-items: center; + gap: 3px; + + & > img { + width: 14px; + } + } + } +} + +.tagline { + color: #224e90; + line-height: 125%; +} + +.text { + margin-top: 6px; + margin-left: 40px; + line-height: 125%; + font-weight: 300; +} + +.avatar-chars { + width: 26px; + height: 26px; + border-radius: 50%; + background-color: #e37fd9; + color: #0f1323; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + text-transform: uppercase; +} diff --git a/src/components/domains/email-marketing/compatibility/v1/Review/Review.tsx b/src/components/domains/email-marketing/compatibility/v1/Review/Review.tsx new file mode 100644 index 0000000..56e2ec9 --- /dev/null +++ b/src/components/domains/email-marketing/compatibility/v1/Review/Review.tsx @@ -0,0 +1,79 @@ +import Image from "next/image"; + +import { Typography } from "@/components/ui"; +import { emailMarketingCompV1Images } from "@/shared/constants/images"; + +import styles from "./Review.module.scss"; + +export interface IReviewProps { + username: string; + tagline?: string; + stars?: number; + avatar: string; + text: string; + date: string; + gender: "male" | "female"; +} + +export default function Review({ + username, + tagline, + stars = 5, + avatar, + text, + date, + gender, +}: IReviewProps) { + return ( +
+
+ {!!avatar?.length && ( + Avatar + )} + {!avatar?.length && ( +
{username.slice(0, 2)}
+ )} +
+
+ + {username} + + + {date} + +
+
+ {Array.from({ length: stars }).map((_, index) => ( + star + ))} +
+
+
+ {tagline && ( + + {tagline} + + )} + + {text} + +
+ ); +} diff --git a/src/components/domains/email-marketing/compatibility/v1/Reviews/Reviews.module.scss b/src/components/domains/email-marketing/compatibility/v1/Reviews/Reviews.module.scss new file mode 100644 index 0000000..27f83bc --- /dev/null +++ b/src/components/domains/email-marketing/compatibility/v1/Reviews/Reviews.module.scss @@ -0,0 +1,8 @@ +.container { + display: flex; + flex-direction: column; + align-items: center; + margin-top: 40px; + max-width: 345px; + gap: 20px; +} diff --git a/src/components/domains/email-marketing/compatibility/v1/Reviews/Reviews.tsx b/src/components/domains/email-marketing/compatibility/v1/Reviews/Reviews.tsx new file mode 100644 index 0000000..6966b50 --- /dev/null +++ b/src/components/domains/email-marketing/compatibility/v1/Reviews/Reviews.tsx @@ -0,0 +1,46 @@ +import { useTranslations } from "next-intl"; + +import { emailMarketingCompV1Images } from "@/shared/constants/images"; +import { translatePathEmailMarketingCompatibilityV1 } from "@/shared/constants/translate"; + +import { IReviewProps, Review } from ".."; + +import styles from "./Reviews.module.scss"; + +export default function Reviews() { + const t = useTranslations( + translatePathEmailMarketingCompatibilityV1("Landing") + ); + + const reviews: IReviewProps[] = [ + { + avatar: emailMarketingCompV1Images("andi36_11.png"), + username: "@andi36_11", + date: "03/05/2025", + gender: "male", + text: t("review-1"), + }, + { + avatar: emailMarketingCompV1Images("aramaska.png"), + username: "@aramaska", + date: "02/17/2025", + gender: "female", + text: t("review-2"), + }, + { + avatar: emailMarketingCompV1Images("patterso.png"), + username: "@patterso", + date: "01/01/2025", + gender: "female", + text: t("review-3"), + }, + ]; + + return ( +
+ {reviews.map((review, index) => ( + + ))} +
+ ); +} diff --git a/src/components/domains/email-marketing/compatibility/v1/SpecialOfferButtonWrapper/SpecialOfferButtonWrapper.module.scss b/src/components/domains/email-marketing/compatibility/v1/SpecialOfferButtonWrapper/SpecialOfferButtonWrapper.module.scss new file mode 100644 index 0000000..7d2ca98 --- /dev/null +++ b/src/components/domains/email-marketing/compatibility/v1/SpecialOfferButtonWrapper/SpecialOfferButtonWrapper.module.scss @@ -0,0 +1,24 @@ +.buttonContainer { + width: 100%; + display: flex; + justify-content: center; + position: sticky; + bottom: calc(0dvh + 25px); + margin-top: 46px; + pointer-events: none; + z-index: 1000; + + .button { + position: relative; + z-index: 1000; + max-width: 300px; + margin-top: 24px; + margin-bottom: 12px; + pointer-events: all; + } +} + +.gradientBlur { + left: -18px !important; + right: -18px !important; +} diff --git a/src/components/domains/email-marketing/compatibility/v1/SpecialOfferButtonWrapper/SpecialOfferButtonWrapper.tsx b/src/components/domains/email-marketing/compatibility/v1/SpecialOfferButtonWrapper/SpecialOfferButtonWrapper.tsx new file mode 100644 index 0000000..50ee989 --- /dev/null +++ b/src/components/domains/email-marketing/compatibility/v1/SpecialOfferButtonWrapper/SpecialOfferButtonWrapper.tsx @@ -0,0 +1,53 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { useTranslations } from "next-intl"; + +import { Button, Typography } from "@/components/ui"; +import { BlurComponent } from "@/components/widgets"; +import { ROUTES } from "@/shared/constants/client-routes"; +import { translatePathEmailMarketingCompatibilityV1 } from "@/shared/constants/translate"; + +import styles from "./SpecialOfferButtonWrapper.module.scss"; + +interface SpecialOfferButtonWrapperProps { + productId: string; + placementId: string; + paywallId: string; +} + +export default function SpecialOfferButtonWrapper({ + productId, + placementId, + paywallId, +}: SpecialOfferButtonWrapperProps) { + const router = useRouter(); + const t = useTranslations( + translatePathEmailMarketingCompatibilityV1("SpecialOffer") + ); + + const openPaymentModal = () => { + router.push( + ROUTES.payment({ + productId, + placementId, + paywallId, + }) + ); + }; + + return ( +
+ + + +
+ ); +} diff --git a/src/components/domains/email-marketing/compatibility/v1/StatisticsBanner/StatisticsBanner.module.scss b/src/components/domains/email-marketing/compatibility/v1/StatisticsBanner/StatisticsBanner.module.scss new file mode 100644 index 0000000..e6a0501 --- /dev/null +++ b/src/components/domains/email-marketing/compatibility/v1/StatisticsBanner/StatisticsBanner.module.scss @@ -0,0 +1,28 @@ +.container { + background-color: #ff9c71; + margin-top: 27px; + padding: 22px 16px; + width: 100%; +} + +.content { + display: flex; + justify-content: space-between; + align-items: center; +} + +.text { + font-size: 19px; + line-height: 25px; +} + +.infoIcon { + color: #ffffff; + display: flex; + align-items: center; + + svg { + width: 24px; + height: 24px; + } +} diff --git a/src/components/domains/email-marketing/compatibility/v1/StatisticsBanner/StatisticsBanner.tsx b/src/components/domains/email-marketing/compatibility/v1/StatisticsBanner/StatisticsBanner.tsx new file mode 100644 index 0000000..53f7c8f --- /dev/null +++ b/src/components/domains/email-marketing/compatibility/v1/StatisticsBanner/StatisticsBanner.tsx @@ -0,0 +1,48 @@ +import { useTranslations } from "next-intl"; + +import { Typography } from "@/components/ui"; +import { translatePathEmailMarketingCompatibilityV1 } from "@/shared/constants/translate"; + +import styles from "./StatisticsBanner.module.scss"; + +interface StatisticsBannerProps { + count: number; +} + +export default function StatisticsBanner({ count }: StatisticsBannerProps) { + const t = useTranslations( + translatePathEmailMarketingCompatibilityV1("Landing") + ); + + return ( +
+
+ + {t.rich("statistics-banner-text", { + count: () => ( + + {t("statistics-banner-count", { + count, + })} + + ), + })} + +
+ + + +
+
+
+ ); +} diff --git a/src/components/domains/email-marketing/compatibility/v1/TextWithEmoji/TextWithEmoji.module.scss b/src/components/domains/email-marketing/compatibility/v1/TextWithEmoji/TextWithEmoji.module.scss new file mode 100644 index 0000000..d08b57f --- /dev/null +++ b/src/components/domains/email-marketing/compatibility/v1/TextWithEmoji/TextWithEmoji.module.scss @@ -0,0 +1,12 @@ +.container { + width: 100%; + display: flex; + align-items: flex-start; + gap: 4px; +} + +.text { + font-size: 22px; + font-weight: 600; + line-height: 25px; +} diff --git a/src/components/domains/email-marketing/compatibility/v1/TextWithEmoji/TextWithEmoji.tsx b/src/components/domains/email-marketing/compatibility/v1/TextWithEmoji/TextWithEmoji.tsx new file mode 100644 index 0000000..c53e9d8 --- /dev/null +++ b/src/components/domains/email-marketing/compatibility/v1/TextWithEmoji/TextWithEmoji.tsx @@ -0,0 +1,36 @@ +import Image from "next/image"; + +import { Typography, TypographyProps } from "@/components/ui"; +import { emailMarketingCompV1Images } from "@/shared/constants/images"; + +import styles from "./TextWithEmoji.module.scss"; + +interface TextWithEmojiProps { + text: string; + emoji: string; + emojiWidth?: number; + emojiHeight?: number; + color?: TypographyProps["color"]; +} + +export default function TextWithEmoji({ + text, + emoji, + emojiWidth = 23, + emojiHeight = 23, + color = "default", +}: TextWithEmojiProps) { + return ( +
+ {emoji} + + {text} + +
+ ); +} diff --git a/src/components/domains/email-marketing/compatibility/v1/UnderstandingYourself/UnderstandingYourself.module.scss b/src/components/domains/email-marketing/compatibility/v1/UnderstandingYourself/UnderstandingYourself.module.scss new file mode 100644 index 0000000..52af69c --- /dev/null +++ b/src/components/domains/email-marketing/compatibility/v1/UnderstandingYourself/UnderstandingYourself.module.scss @@ -0,0 +1,16 @@ +.container { + width: 100%; + height: fit-content; + border-radius: 30px; + backdrop-filter: blur(5px); + display: flex; + flex-direction: column; + align-items: center; + margin-top: 50px; + padding: 25px 18px 0; + background-color: rgba(225, 225, 225, 0.44); +} + +.hand { + margin-top: -13px; +} diff --git a/src/components/domains/email-marketing/compatibility/v1/UnderstandingYourself/UnderstandingYourself.tsx b/src/components/domains/email-marketing/compatibility/v1/UnderstandingYourself/UnderstandingYourself.tsx new file mode 100644 index 0000000..adaf41e --- /dev/null +++ b/src/components/domains/email-marketing/compatibility/v1/UnderstandingYourself/UnderstandingYourself.tsx @@ -0,0 +1,31 @@ +import Image from "next/image"; +import { useTranslations } from "next-intl"; + +import { emailMarketingCompV1Images } from "@/shared/constants/images"; +import { translatePathEmailMarketingCompatibilityV1 } from "@/shared/constants/translate"; + +import { TextWithEmoji } from ".."; + +import styles from "./UnderstandingYourself.module.scss"; + +export default function UnderstandingYourself() { + const t = useTranslations( + translatePathEmailMarketingCompatibilityV1("Landing") + ); + + return ( +
+ + hand with eye +
+ ); +} diff --git a/src/components/domains/email-marketing/compatibility/v1/index.ts b/src/components/domains/email-marketing/compatibility/v1/index.ts new file mode 100644 index 0000000..264008c --- /dev/null +++ b/src/components/domains/email-marketing/compatibility/v1/index.ts @@ -0,0 +1,19 @@ +export { default as AdviceFromAstrologer } from "./AdviceFromAstrologer/AdviceFromAstrologer"; +export { default as CountdownTimer } from "./CountdownTimer/CountdownTimer"; +export { default as CustomerCounter } from "./CustomerCounter/CustomerCounter"; +export { default as FindingPartner } from "./FindingPartner/FindingPartner"; +export { default as GivePersonalizedPlan } from "./GivePersonalizedPlan/GivePersonalizedPlan"; +export { default as GuaranteedSecurityPayments } from "./GuaranteedSecurityPayments/GuaranteedSecurityPayments"; +export { default as InsightsRelationship } from "./InsightsRelationship/InsightsRelationship"; +export { default as LandingButtonWrapper } from "./LandingButtonWrapper/LandingButtonWrapper"; +export { default as MoneyBackGuarantee } from "./MoneyBackGuarantee/MoneyBackGuarantee"; +export { default as Payments } from "./Payments/Payments"; +export { default as PlanIncludes } from "./PlanIncludes/PlanIncludes"; +export { default as PriceComparison } from "./PriceComparison/PriceComparison"; +export { default as PricingSummary } from "./PricingSummary/PricingSummary"; +export { type IReviewProps, default as Review } from "./Review/Review"; +export { default as Reviews } from "./Reviews/Reviews"; +export { default as SpecialOfferButtonWrapper } from "./SpecialOfferButtonWrapper/SpecialOfferButtonWrapper"; +export { default as StatisticsBanner } from "./StatisticsBanner/StatisticsBanner"; +export { default as TextWithEmoji } from "./TextWithEmoji/TextWithEmoji"; +export { default as UnderstandingYourself } from "./UnderstandingYourself/UnderstandingYourself"; diff --git a/src/components/domains/profile/Billing/Billing.tsx b/src/components/domains/profile/Billing/Billing.tsx index 213f135..2d19e8e 100644 --- a/src/components/domains/profile/Billing/Billing.tsx +++ b/src/components/domains/profile/Billing/Billing.tsx @@ -5,6 +5,7 @@ import { useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; import { Button, Typography } from "@/components/ui"; +import { useUserBalance } from "@/hooks/balance/useUserBalance"; import { ROUTES } from "@/shared/constants/client-routes"; import styles from "./Billing.module.scss"; @@ -12,6 +13,10 @@ import styles from "./Billing.module.scss"; function Billing() { const t = useTranslations("Profile.billing"); const router = useRouter(); + const { balance, isLoading } = useUserBalance(); + + // Round balance to nearest integer when available + const roundedBalance = balance !== null ? Math.round(balance) : null; const onBilling = () => { router.push(ROUTES.profileSubscriptions()); @@ -27,7 +32,7 @@ function Billing() {
{t("credits.title", { - credits: String(0), + credits: isLoading || roundedBalance === null ? "..." : String(roundedBalance), })} { close(); diff --git a/src/components/domains/profile/subscriptions/SubscriptionTable/SubscriptionTable.tsx b/src/components/domains/profile/subscriptions/SubscriptionTable/SubscriptionTable.tsx index bf55aaa..81e7fb0 100644 --- a/src/components/domains/profile/subscriptions/SubscriptionTable/SubscriptionTable.tsx +++ b/src/components/domains/profile/subscriptions/SubscriptionTable/SubscriptionTable.tsx @@ -37,7 +37,9 @@ export default function SubscriptionTable({ subscription }: ITableProps) { } return "Cancelled"; } - return t(`table.subscription_status_value.${subscription.subscriptionStatus}`); + return t( + `table.subscription_status_value.${subscription.subscriptionStatus}` + ); }, [subscription.subscriptionStatus, subscription.cancellationDate, t]); const tableData: ReactNode[][] = useMemo(() => { @@ -46,10 +48,7 @@ export default function SubscriptionTable({ subscription }: ITableProps) { t("table.subscription_type"), t(`table.subscription_type_value.${subscription.subscriptionType}`), ], - [ - t("table.subscription_status"), - getSubscriptionStatusText(), - ], + [t("table.subscription_status"), getSubscriptionStatusText()], [ t("table.billing_period"), t(`table.billing_period_value.${subscription.billingPeriod}`), diff --git a/src/components/domains/retaining/Offer/Offer.tsx b/src/components/domains/retaining/Offer/Offer.tsx index 6ff0965..4a378fb 100644 --- a/src/components/domains/retaining/Offer/Offer.tsx +++ b/src/components/domains/retaining/Offer/Offer.tsx @@ -4,10 +4,10 @@ import { Typography } from "@/components/ui"; import { getFormattedPrice } from "@/shared/utils/price"; import { Currency } from "@/types"; -import styles from "./Offer.module.scss"; - import { CheckMark } from ".."; +import styles from "./Offer.module.scss"; + interface OfferProps { title?: string | React.ReactNode; description?: string; diff --git a/src/components/domains/retaining/second-chance/SecondChancePage/SecondChancePage.tsx b/src/components/domains/retaining/second-chance/SecondChancePage/SecondChancePage.tsx index a9bd934..8cd4350 100644 --- a/src/components/domains/retaining/second-chance/SecondChancePage/SecondChancePage.tsx +++ b/src/components/domains/retaining/second-chance/SecondChancePage/SecondChancePage.tsx @@ -14,7 +14,7 @@ import { useLottie } from "@/hooks/lottie/useLottie"; import { useRetainingStore } from "@/providers/retaining-store-provider"; import { useToast } from "@/providers/toast-provider"; import { ROUTES } from "@/shared/constants/client-routes"; -import { retainingImages } from "@/shared/constants/images/retaining"; +import { retainingImages } from "@/shared/constants/images"; import { ELottieKeys } from "@/shared/constants/lottie"; import { ERetainingFunnel } from "@/types"; diff --git a/src/components/layout/Header/Header.tsx b/src/components/layout/Header/Header.tsx index 132c9d7..25940c8 100644 --- a/src/components/layout/Header/Header.tsx +++ b/src/components/layout/Header/Header.tsx @@ -1,23 +1,28 @@ "use client"; +import { use } from "react"; import Link from "next/link"; import clsx from "clsx"; import { Button, Icon, IconName } from "@/components/ui"; +import { IGetChatsListResponse } from "@/entities/chats/types"; +import { useChatsSocket } from "@/hooks/chats/useChatsSocket"; import { ROUTES } from "@/shared/constants/client-routes"; +import { useDrawer } from ".."; import Logo from "../Logo/Logo"; import styles from "./Header.module.scss"; -import { useDrawer } from ".."; - interface HeaderProps { className?: string; + chatsPromise: Promise; } -export default function Header({ className }: HeaderProps) { +export default function Header({ className, chatsPromise }: HeaderProps) { const { open } = useDrawer(); + const chats = use(chatsPromise); + const { totalUnreadCount } = useChatsSocket({ initialChats: chats }); return (
@@ -28,7 +33,9 @@ export default function Header({ className }: HeaderProps) {
- + + +
diff --git a/src/components/layout/NavigationBar/NavigationBar.tsx b/src/components/layout/NavigationBar/NavigationBar.tsx index 1c7b986..f13a166 100644 --- a/src/components/layout/NavigationBar/NavigationBar.tsx +++ b/src/components/layout/NavigationBar/NavigationBar.tsx @@ -1,21 +1,35 @@ "use client"; +import { use } from "react"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { useLocale } from "next-intl"; import clsx from "clsx"; import { Badge, Icon, Typography } from "@/components/ui"; +import { IGetChatsListResponse } from "@/entities/chats/types"; +import { useChatsSocket } from "@/hooks/chats/useChatsSocket"; import { ROUTES } from "@/shared/constants/client-routes"; -import { navItems } from "@/shared/constants/navigation"; +import { NavItem, navItems } from "@/shared/constants/navigation"; import { stripLocale } from "@/shared/utils/path"; import styles from "./NavigationBar.module.scss"; -export default function NavigationBar() { +const getBadge = (item: NavItem, totalUnreadCount: number) => { + if (item.badgeId === "unreadCount") return totalUnreadCount; + return null; +}; + +interface NavigationBarProps { + chatsPromise: Promise; +} + +export default function NavigationBar({ chatsPromise }: NavigationBarProps) { const pathname = usePathname(); const locale = useLocale(); const pathnameWithoutLocale = stripLocale(pathname, locale); + const chats = use(chatsPromise); + const { totalUnreadCount } = useChatsSocket({ initialChats: chats }); return (