diff --git a/global.d.ts b/global.d.ts new file mode 100644 index 0000000..98af3d3 --- /dev/null +++ b/global.d.ts @@ -0,0 +1,7 @@ +declare global { + interface Window { + webkitAudioContext: typeof AudioContext; + } +} + +export {}; diff --git a/messages/en.json b/messages/en.json index 112ccd3..4802ca1 100644 --- a/messages/en.json +++ b/messages/en.json @@ -393,5 +393,36 @@ } } } + }, + "SaveOff": { + "title": "SAVE {discount}% OFF!", + "instead": " instead ", + "instead-old-price": "of {oldPrice}", + "trial-duration": "{trialPeriod} trial instead of ", + "discount-offer": "{discount}% off on your Personalized Plan", + "button-trial": "GET {trialPeriod} trial" + }, + "SecretDiscount": { + "title": "You get a secret discount!", + "button-trial": "GET {trialPeriod} TRIAL", + "secret-discount-table_cost-after-trial": "Your cost per {trialPeriod} after trial:", + "secret-discount-table_discount-applied": "Secret discount applied!", + "secret-discount-table_subtitle": "No pressure. Cancel anytime.", + "secret-discount-table_title": "You get a secret discount!", + "secret-discount-table_total-today": "Total today", + "secret-discount-table_you-save": "You save {amount}", + "policy": "By continuing you agree that if you don't cancel prior to the end of the {trialPeriod} trial, you will automatically be charged the standard rate of {price} every {billingPeriod} until you cancel in settings. Learn more about cancellation and refund policy in Subscription terms." + }, + "period": { + "day": "{count, plural, zero {# days} one {# day} two {# days} few {# days} many {# days} other {# days}}", + "week": "{count, plural, zero {# weeks} one {# week} two {# weeks} few {# weeks} many {# weeks} other {# weeks}}", + "month": "{count, plural, zero {# months} one {# month} two {# months} few {# months} many {# months} other {# months}}", + "year": "{count, plural, zero {# years} one {# year} two {# years} few {# years} many {# years} other {# years}}" + }, + "period_adjective": { + "day": "{count, plural, zero {#-days} one {#-day} two {#-days} few {#-days} many {#-days} other {#-days}}", + "week": "{count, plural, zero {#-weeks} one {#-week} two {#-weeks} few {#-weeks} many {#-weeks} other {#-weeks}}", + "month": "{count, plural, zero {#-months} one {#-month} two {#-months} few {#-months} many {#-months} other {#-months}}", + "year": "{count, plural, zero {#-years} one {#-year} two {#-years} few {#-years} many {#-years} other {#-years}}" } } diff --git a/package-lock.json b/package-lock.json index 2b8b666..4994245 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@lottiefiles/dotlottie-react": "^0.14.1", + "@tanstack/react-virtual": "^3.13.12", "client-only": "^0.0.1", "clsx": "^2.1.1", "idb": "^8.0.3", @@ -200,13 +201,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.2.tgz", - "integrity": "sha512-4SaFZCNfJqvk/kenHpI8xvN42DMaoycy4PzKc5otHxRswww1kAt82OlBuwRVLofCACCTZEcla2Ydxv8scMXaTg==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", + "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.15.0", + "@eslint/core": "^0.15.1", "levn": "^0.4.1" }, "engines": { @@ -214,9 +215,9 @@ } }, "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.0.tgz", - "integrity": "sha512-b7ePw78tEWWkpgZCDYkbqDOP8dmM6qe+AOC6iuJqlq1R/0ahMAeH3qynpnqKFGkMltrp44ohV4ubGyvLX28tzw==", + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1308,6 +1309,33 @@ "tslib": "^2.8.0" } }, + "node_modules/@tanstack/react-virtual": { + "version": "3.13.12", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz", + "integrity": "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.13.12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.13.12", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz", + "integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz", diff --git a/package.json b/package.json index 4e635de..bef9539 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ }, "dependencies": { "@lottiefiles/dotlottie-react": "^0.14.1", + "@tanstack/react-virtual": "^3.13.12", "client-only": "^0.0.1", "clsx": "^2.1.1", "idb": "^8.0.3", diff --git a/public/audio/notification-new-message-1.mp3 b/public/audio/notification-new-message-1.mp3 new file mode 100644 index 0000000..3860589 Binary files /dev/null and b/public/audio/notification-new-message-1.mp3 differ diff --git a/public/audio/notification-new-message-1.wav b/public/audio/notification-new-message-1.wav new file mode 100644 index 0000000..c7017ad Binary files /dev/null and b/public/audio/notification-new-message-1.wav differ diff --git a/public/secret-discount/fire.png b/public/secret-discount/fire.png new file mode 100644 index 0000000..1966e9a Binary files /dev/null and b/public/secret-discount/fire.png differ diff --git a/public/secret-discount/gift.png b/public/secret-discount/gift.png new file mode 100644 index 0000000..bf58560 Binary files /dev/null and b/public/secret-discount/gift.png differ diff --git a/public/secret-discount/gift.svg b/public/secret-discount/gift.svg new file mode 100644 index 0000000..1bfccec --- /dev/null +++ b/public/secret-discount/gift.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/app/[locale]/(chat)/chat/[assistantId]/page.module.scss b/src/app/[locale]/(chat)/chat/[assistantId]/page.module.scss index d7b23e8..428e6e8 100644 --- a/src/app/[locale]/(chat)/chat/[assistantId]/page.module.scss +++ b/src/app/[locale]/(chat)/chat/[assistantId]/page.module.scss @@ -2,4 +2,5 @@ display: flex; flex-direction: column; height: 100dvh; + position: relative; } diff --git a/src/app/[locale]/(chat)/chat/[assistantId]/page.tsx b/src/app/[locale]/(chat)/chat/[assistantId]/page.tsx index 1313da7..eef4ad4 100644 --- a/src/app/[locale]/(chat)/chat/[assistantId]/page.tsx +++ b/src/app/[locale]/(chat)/chat/[assistantId]/page.tsx @@ -4,14 +4,13 @@ import { 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.tsx b/src/app/[locale]/(chat)/chat/page.tsx index 1c19638..6199b68 100644 --- a/src/app/[locale]/(chat)/chat/page.tsx +++ b/src/app/[locale]/(chat)/chat/page.tsx @@ -10,7 +10,6 @@ import { NewMessagesWrapperSkeleton, } from "@/components/domains/chat"; import { NavigationBar } from "@/components/layout"; -import { loadChatsList } from "@/entities/chats/loaders"; import styles from "./page.module.scss"; @@ -19,23 +18,21 @@ export const revalidate = 0; export const fetchCache = "force-no-store"; export default function Chats() { - const chatsPromise = loadChatsList(); - return (
}> - + }> - + }> - +
- +
); } diff --git a/src/app/[locale]/(core)/layout.tsx b/src/app/[locale]/(core)/layout.tsx index 654e78a..a2730b8 100644 --- a/src/app/[locale]/(core)/layout.tsx +++ b/src/app/[locale]/(core)/layout.tsx @@ -1,5 +1,4 @@ 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"; @@ -9,13 +8,12 @@ export default function CoreLayout({ }: Readonly<{ children: React.ReactNode; }>) { - const chatsPromise = loadChatsList(); return ( -
+
{children}
- + ); diff --git a/src/app/[locale]/(core)/page.module.scss b/src/app/[locale]/(core)/page.module.scss index 6465f91..7f581db 100644 --- a/src/app/[locale]/(core)/page.module.scss +++ b/src/app/[locale]/(core)/page.module.scss @@ -2,4 +2,5 @@ display: flex; flex-direction: column; gap: 24px; + position: relative; } diff --git a/src/app/[locale]/(core)/page.tsx b/src/app/[locale]/(core)/page.tsx index 11cb9ab..54a0512 100644 --- a/src/app/[locale]/(core)/page.tsx +++ b/src/app/[locale]/(core)/page.tsx @@ -28,7 +28,7 @@ export default function Home() { return (
}> - + diff --git a/src/app/[locale]/(payment)/layout.tsx b/src/app/[locale]/(payment)/layout.tsx index a717e7d..ba4f04d 100644 --- a/src/app/[locale]/(payment)/layout.tsx +++ b/src/app/[locale]/(payment)/layout.tsx @@ -1,16 +1,15 @@ import { DrawerProvider, Header } from "@/components/layout"; -import { loadChatsList } from "@/entities/chats/loaders"; import styles from "./layout.module.scss"; -export default function CoreLayout({ +export default function PaymentLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { return ( -
+
{children}
); diff --git a/src/app/[locale]/(payment)/payment/success/Metrics.module.scss b/src/app/[locale]/(payment)/payment/success/Button.module.scss similarity index 100% rename from src/app/[locale]/(payment)/payment/success/Metrics.module.scss rename to src/app/[locale]/(payment)/payment/success/Button.module.scss diff --git a/src/app/[locale]/(payment)/payment/success/Button.tsx b/src/app/[locale]/(payment)/payment/success/Button.tsx new file mode 100644 index 0000000..65b0541 --- /dev/null +++ b/src/app/[locale]/(payment)/payment/success/Button.tsx @@ -0,0 +1,24 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { useTranslations } from "next-intl"; + +import { Button, Typography } from "@/components/ui"; +import { ROUTES } from "@/shared/constants/client-routes"; + +import styles from "./Button.module.scss"; + +export default function PaymentSuccessButton() { + const t = useTranslations("Payment.Success"); + const router = useRouter(); + + const handleNext = () => { + router.push(ROUTES.additionalPurchases()); + }; + + return ( + + ); +} diff --git a/src/app/[locale]/(payment)/payment/success/Metrics.tsx b/src/app/[locale]/(payment)/payment/success/Metrics.tsx deleted file mode 100644 index eb12a63..0000000 --- a/src/app/[locale]/(payment)/payment/success/Metrics.tsx +++ /dev/null @@ -1,195 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; -import Script from "next/script"; -import { useTranslations } from "next-intl"; - -import { Button, Typography } from "@/components/ui"; -import { ROUTES } from "@/shared/constants/client-routes"; - -import styles from "./Metrics.module.scss"; - -interface MetricsProps { - fbPixels: string[]; - productPrice: string; - currency: string; -} - -export default function Metrics({ - fbPixels, - productPrice, - currency, -}: MetricsProps) { - const t = useTranslations("Payment.Success"); - - const [isButtonVisible, setIsButtonVisible] = useState(false); - - const handleNext = () => { - window.location.href = ROUTES.additionalPurchases(); - }; - - // Yandex Metrica - useEffect(() => { - const interval = setInterval(() => { - if ( - typeof window.ym === "function" && - typeof window.klaviyo === "object" && - typeof window.gtag === "function" - ) { - try { - window.gtag("event", "PaymentSuccess"); - window.klaviyo.push(["track", "PaymentSuccess"]); - - window.ym(95799066, "init", { - clickmap: true, - trackLinks: true, - accurateTrackBounce: true, - webvisor: true, - }); - - window.ym(95799066, "reachGoal", "PaymentSuccess", {}, () => { - // deleteYm() - setIsButtonVisible(true); - }); - } catch (e) { - // eslint-disable-next-line no-console - console.error("YM error:", e); - } finally { - clearInterval(interval); - } - } - }, 200); - - return () => clearInterval(interval); - }, []); - - return ( - <> - {/* Klaviyo */} - {/* - - - {/* Yandex Metrica */} - - - {/* Google Analytics */} - - - {/* Facebook Pixel */} - {fbPixels.map(pixel => ( - - ))} - - {isButtonVisible && ( - - )} - - ); -} diff --git a/src/app/[locale]/(payment)/payment/success/page.tsx b/src/app/[locale]/(payment)/payment/success/page.tsx index 138c657..6614735 100644 --- a/src/app/[locale]/(payment)/payment/success/page.tsx +++ b/src/app/[locale]/(payment)/payment/success/page.tsx @@ -3,21 +3,9 @@ import { getTranslations } from "next-intl/server"; import { AnimatedInfoScreen, LottieAnimation } from "@/components/widgets"; import { ELottieKeys } from "@/shared/constants/lottie"; -import Metrics from "./Metrics"; - -export default async function PaymentSuccess({ - searchParams, -}: { - searchParams: Promise<{ - [key: string]: string | undefined; - }>; -}) { - const params = await searchParams; - - const fbPixels = params?.fb_pixels?.split(",") || []; - const productPrice = params?.price || "0"; - const currency = params?.currency || "USD"; +import PaymentSuccessButton from "./Button"; +export default async function PaymentSuccess() { const t = await getTranslations("Payment.Success"); return ( @@ -28,11 +16,7 @@ export default async function PaymentSuccess({ } title={t("title")} /> - + ); } diff --git a/src/app/[locale]/(secret-discount)/layout.module.scss b/src/app/[locale]/(secret-discount)/layout.module.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/[locale]/(secret-discount)/layout.tsx b/src/app/[locale]/(secret-discount)/layout.tsx new file mode 100644 index 0000000..e899a12 --- /dev/null +++ b/src/app/[locale]/(secret-discount)/layout.tsx @@ -0,0 +1,13 @@ +import { DrawerProvider } from "@/components/layout"; + +export default function SecretDiscountLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + +
{children}
+
+ ); +} diff --git a/src/app/[locale]/(secret-discount)/save-off/page.module.scss b/src/app/[locale]/(secret-discount)/save-off/page.module.scss new file mode 100644 index 0000000..3124a2d --- /dev/null +++ b/src/app/[locale]/(secret-discount)/save-off/page.module.scss @@ -0,0 +1,60 @@ +.container { + width: 100%; + height: fit-content; + display: flex; + flex-direction: column; + align-items: center; + padding: 58px 26px 120px; + + & > .title { + margin-top: 32px; + margin-bottom: 14px; + line-height: 26px; + color: #275ca7; + } + + & > .description { + font-size: 18px; + line-height: 24px; + font-weight: 400; + color: #363636; + + & > .price { + font-weight: 600; + background: linear-gradient(90deg, #ffa1ba 0%, #9a55ff 100%); + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + } + + & > .discount { + text-decoration: line-through; + } + } + + & > .point { + width: 100%; + display: flex; + align-items: center; + justify-content: flex-start; + gap: 8px; + font-size: 14px; + line-height: 21px; + color: #2c2c2c; + margin-top: 6px; + } + + & > .blob { + position: absolute; + top: 0; + right: 0; + z-index: -1; + } + + & > .blob2 { + position: absolute; + bottom: 0; + right: 0; + z-index: -1; + } +} diff --git a/src/app/[locale]/(secret-discount)/save-off/page.tsx b/src/app/[locale]/(secret-discount)/save-off/page.tsx new file mode 100644 index 0000000..40b71b8 --- /dev/null +++ b/src/app/[locale]/(secret-discount)/save-off/page.tsx @@ -0,0 +1,134 @@ +import Image from "next/image"; +import { getTranslations } from "next-intl/server"; + +import { + Blob1, + Blob2, + SaveOffButton, +} from "@/components/domains/secret-discount"; +import { Header } from "@/components/layout"; +import { Typography } from "@/components/ui"; +import { loadFunnelPaymentById } from "@/entities/session/funnel/loaders"; +import { IFunnelPaymentPlacement } from "@/entities/session/funnel/types"; +import { secretDiscountImages } from "@/shared/constants/images"; +import { getProperty } from "@/shared/utils/funnel"; +import { getPeriodTextServer } from "@/shared/utils/period-server"; +import { getFormattedPrice } from "@/shared/utils/price"; +import { Currency, ELocalesPlacement } from "@/types"; + +import styles from "./page.module.scss"; + +const payload = { + funnel: ELocalesPlacement.CompatibilityV2, +}; + +export default async function SaveOffPage() { + const t = await getTranslations("SaveOff"); + + const paymentData = (await loadFunnelPaymentById( + payload, + "main_secret_discount" + )) as IFunnelPaymentPlacement; + + const paymentDataMain = (await loadFunnelPaymentById( + payload, + "main" + )) as IFunnelPaymentPlacement; + + const currency = paymentData?.currency ?? Currency.USD; + const discountNew = getProperty(paymentData, "discount.new")?.value; + const product = paymentData?.variants?.[0]; + const trialPrice = product?.trialPrice ?? 0; + const trialPeriod = paymentData?.trialPeriod ?? "DAY"; + const trialInterval = paymentData?.trialInterval ?? 0; + + const price = paymentDataMain?.price ?? 0; + const oldTrialPeriod = paymentDataMain?.trialPeriod ?? "DAY"; + const oldTrialInterval = paymentDataMain?.trialInterval ?? 0; + + return ( + <> +
+
+ + + Gift + + {t("title", { + discount: discountNew, + })} + + + {t.rich("instead", { + price: () => ( + + {getFormattedPrice(trialPrice, currency, 0)} + + ), + oldPrice: () => ( + + {t("instead-old-price", { + oldPrice: getFormattedPrice(price, currency, 0), + })} + + ), + })} + + + + fire + + {t.rich("trial-duration", { + trialPeriod: await getPeriodTextServer(trialPeriod, trialInterval), + oldTrialPeriod: async () => ( + + {await getPeriodTextServer(oldTrialPeriod, oldTrialInterval)} + + ), + })} + + + + gift + {t("discount-offer", { + discount: discountNew, + })} + + + +
+ + ); +} diff --git a/src/app/[locale]/(secret-discount)/secret-discount/page.module.scss b/src/app/[locale]/(secret-discount)/secret-discount/page.module.scss new file mode 100644 index 0000000..54ca023 --- /dev/null +++ b/src/app/[locale]/(secret-discount)/secret-discount/page.module.scss @@ -0,0 +1,27 @@ +.container { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + // position: relative; + min-height: calc(100dvh - 60px); + padding: 32px 26px 0; + + & > .title { + padding: 15px 0; + background-color: #f096c4; + margin: 32px 0 0; + line-height: 100%; + width: calc(100% + 52px); + font-size: 20px; + text-transform: uppercase; + white-space: nowrap; + } +} + +.blob3 { + position: absolute; + top: 0; + left: 0; + z-index: -1; +} diff --git a/src/app/[locale]/(secret-discount)/secret-discount/page.tsx b/src/app/[locale]/(secret-discount)/secret-discount/page.tsx new file mode 100644 index 0000000..235c675 --- /dev/null +++ b/src/app/[locale]/(secret-discount)/secret-discount/page.tsx @@ -0,0 +1,101 @@ +import { getTranslations } from "next-intl/server"; + +import { + Blob3, + SecretDiscountButton, + SecretDiscountTable, +} from "@/components/domains/secret-discount"; +import SecretDiscountPolicy from "@/components/domains/secret-discount/secret-discount/Policy/Policy"; +import { Header } from "@/components/layout"; +import { Typography } from "@/components/ui"; +import { loadFunnelPaymentById } from "@/entities/session/funnel/loaders"; +import { IFunnelPaymentPlacement } from "@/entities/session/funnel/types"; +import { getProperty } from "@/shared/utils/funnel"; +import { Currency, ELocalesPlacement } from "@/types"; + +import styles from "./page.module.scss"; + +const payload = { + funnel: ELocalesPlacement.CompatibilityV2, +}; + +export default async function SecretDiscountPage() { + const t = await getTranslations("SecretDiscount"); + + const paymentData = (await loadFunnelPaymentById( + payload, + "main_secret_discount" + )) as IFunnelPaymentPlacement; + + const paymentDataMain = (await loadFunnelPaymentById( + payload, + "main" + )) as IFunnelPaymentPlacement; + + const currency = paymentData?.currency ?? Currency.USD; + const product = paymentData?.variants?.[0]; + const trialPrice = product?.trialPrice ?? 0; + const trialPeriod = paymentData?.trialPeriod ?? "DAY"; + const trialInterval = paymentData?.trialInterval ?? 0; + const billingPeriod = paymentData?.billingPeriod ?? "DAY"; + const billingInterval = paymentData?.billingInterval ?? 0; + + const productId = product?.id ?? ""; + const placementId = paymentData?.placementId ?? ""; + const paywallId = paymentData?.paywallId ?? ""; + + const oldPrice = paymentDataMain?.price ?? 0; + const price = paymentData?.price ?? 0; + + const discountNew = getProperty(paymentData, "discount.new")?.value; + const discountOld = getProperty(paymentData, "discount.old")?.value; + + return ( + <> +
+ +
+ + {t("title")} + + + + + + + +
+ + ); +} diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx index 10550cf..f9b67b4 100644 --- a/src/app/[locale]/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -10,10 +10,13 @@ import { getMessages } from "next-intl/server"; import clsx from "clsx"; import YandexMetrika from "@/components/analytics/YandexMetrika"; +import { loadChatsList } from "@/entities/chats/loaders"; import { loadUser, loadUserId } from "@/entities/user/loaders"; import { routing } from "@/i18n/routing"; import { AppUiStoreProvider } from "@/providers/app-ui-store-provider"; +import { AudioProvider } from "@/providers/audio-provider"; import { ChatsInitializationProvider } from "@/providers/chats-initialization-provider"; +import { ChatsProvider } from "@/providers/chats-provider"; import { RetainingStoreProvider } from "@/providers/retaining-store-provider"; import SocketProvider from "@/providers/socket-provider"; import { ToastProvider } from "@/providers/toast-provider"; @@ -60,6 +63,7 @@ export default async function RootLayout({ const user = await loadUser(); const userId = await loadUserId(); + const chats = await loadChatsList(); return ( @@ -69,11 +73,15 @@ export default async function RootLayout({ - - - {children} - - + + + + + {children} + + + + diff --git a/src/components/domains/chat/ChatCategories/ChatCategories.tsx b/src/components/domains/chat/ChatCategories/ChatCategories.tsx index 33a3b13..8dadc63 100644 --- a/src/components/domains/chat/ChatCategories/ChatCategories.tsx +++ b/src/components/domains/chat/ChatCategories/ChatCategories.tsx @@ -1,23 +1,17 @@ "use client"; -import { use, useState } from "react"; +import { 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 { useChats } from "@/providers/chats-provider"; 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 }); +export default function ChatCategories() { + const { categorizedChats } = useChats(); const [activeChip, setActiveChip] = useState("All"); const [maxVisibleChats, setMaxVisibleChats] = useState< diff --git a/src/components/domains/chat/ChatHeader/ChatHeader.tsx b/src/components/domains/chat/ChatHeader/ChatHeader.tsx index 4cb8074..f54befe 100644 --- a/src/components/domains/chat/ChatHeader/ChatHeader.tsx +++ b/src/components/domains/chat/ChatHeader/ChatHeader.tsx @@ -1,6 +1,6 @@ "use client"; -import { use, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; @@ -13,26 +13,20 @@ import { UserAvatar, } 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 { useChats } from "@/providers/chats-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) { +export default function ChatHeader() { 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 { totalUnreadCount } = useChats(); const [timer, setTimer] = useState(0); useEffect(() => { diff --git a/src/components/domains/chat/ChatMessage/ChatMessage.module.scss b/src/components/domains/chat/ChatMessage/ChatMessage.module.scss index 74ec592..303170d 100644 --- a/src/components/domains/chat/ChatMessage/ChatMessage.module.scss +++ b/src/components/domains/chat/ChatMessage/ChatMessage.module.scss @@ -8,5 +8,6 @@ &.own { align-items: flex-end; align-self: flex-end; + margin-left: auto; } } diff --git a/src/components/domains/chat/ChatMessage/ChatMessage.tsx b/src/components/domains/chat/ChatMessage/ChatMessage.tsx index fdac54c..c6b0b51 100644 --- a/src/components/domains/chat/ChatMessage/ChatMessage.tsx +++ b/src/components/domains/chat/ChatMessage/ChatMessage.tsx @@ -3,11 +3,11 @@ import { useEffect } from "react"; import clsx from "clsx"; +import { IChatMessage } from "@/entities/chats/types"; import { useChat } from "@/providers/chat-provider"; +import { formatTime } from "@/shared/utils/date"; -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"; @@ -16,21 +16,23 @@ 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; - }; + // message: { + // id: string; + // type: "text" | "image" | "voice" | "typing"; + // text?: string; + // imageUrl?: string; + // audioUrl?: string; + // duration?: number; + // time: string | null; + // isOwn: boolean; + // isRead?: boolean; + // }; + message: IChatMessage; } export default function ChatMessage({ message }: ChatMessageProps) { const { isConnected, read } = useChat(); + const isOwn = message.role === "user"; useEffect(() => { if ( @@ -45,37 +47,33 @@ export default function ChatMessage({ message }: ChatMessageProps) { }, [message.id, message.isRead, read, isConnected]); return ( -
- - {message.type === "text" && ( - +
+ + {message.type === "text" && message.id !== "typing" && ( + )} - {message.type === "typing" && } + {message.id === "typing" && } - {message.type === "image" && ( + {/* {message.type === "image" && ( <> - {message.content && ( - - )} + {message.text && } )} - {message.type === "audio" && ( + {message.type === "voice" && ( <> - {message.content && ( - - )} + {message.text && } - )} + )} */} - - {message.isOwn && } + + {isOwn && }
); diff --git a/src/components/domains/chat/ChatMessages/ChatMessages.tsx b/src/components/domains/chat/ChatMessages/ChatMessages.tsx index 3dc786f..5123ed7 100644 --- a/src/components/domains/chat/ChatMessages/ChatMessages.tsx +++ b/src/components/domains/chat/ChatMessages/ChatMessages.tsx @@ -17,11 +17,11 @@ export default function ChatMessages({ )} diff --git a/src/components/domains/chat/ChatMessagesWrapper/ChatMessagesWrapper.module.scss b/src/components/domains/chat/ChatMessagesWrapper/ChatMessagesWrapper.module.scss index af59278..7c44795 100644 --- a/src/components/domains/chat/ChatMessagesWrapper/ChatMessagesWrapper.module.scss +++ b/src/components/domains/chat/ChatMessagesWrapper/ChatMessagesWrapper.module.scss @@ -1,18 +1,45 @@ .messagesWrapper { flex: 1 1 0%; overflow-y: auto; - scroll-behavior: smooth; transition: padding-bottom 0.3s ease-in-out; + position: relative; + transform: scaleY(-1); } .loaderTop { display: flex; justify-content: center; - padding-top: 16px; + padding-bottom: 16px; } .suggestions.suggestions { // position: sticky; // bottom: 0; padding: 0 16px 36px; + margin-bottom: 0; + transform: scaleY(-1); +} + +.scrollToBottomButton.scrollToBottomButton { + position: absolute; + right: 16px; + display: flex; + justify-content: center; + align-items: center; + z-index: 444; + padding: 8px; + width: fit-content; + background-color: #fff; + box-shadow: 0 4px 6px #00000017; + + & > .badge { + position: absolute; + top: -8px; + right: -8px; + background-color: #fbbf24; + min-width: 24px; + min-height: 24px; + max-width: 28px; + max-height: 28px; + } } diff --git a/src/components/domains/chat/ChatMessagesWrapper/ChatMessagesWrapper.tsx b/src/components/domains/chat/ChatMessagesWrapper/ChatMessagesWrapper.tsx index 0de2f99..b5650c7 100644 --- a/src/components/domains/chat/ChatMessagesWrapper/ChatMessagesWrapper.tsx +++ b/src/components/domains/chat/ChatMessagesWrapper/ChatMessagesWrapper.tsx @@ -1,93 +1,174 @@ "use client"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useVirtualizer } from "@tanstack/react-virtual"; -import { Spinner } from "@/components/ui"; +import { Button, Icon, IconName, 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 { ChatMessage, Suggestions } from ".."; import styles from "./ChatMessagesWrapper.module.scss"; export default function ChatMessagesWrapper() { + const messagesWrapperRef = useRef(null); + + const [isScrolledUp, setIsScrolledUp] = useState(false); + const { messages: socketMessages, isLoadingAdvisorMessage, hasMoreOlderMessages, isLoadingOlder, - messagesWrapperRef, + // unreadMessagesCount, loadOlder, - scrollToBottom, send, } = useChat(); - const { _hasHydrated } = useChatStore(state => state); + const messages = useMemo(() => { + const msgs = [...socketMessages]; + if (isLoadingAdvisorMessage) { + msgs.unshift({ + id: "typing", + type: "text", + text: "…", + role: "assistant", + isRead: false, + createdDate: new Date().toISOString(), + }); + } + return msgs; + }, [isLoadingAdvisorMessage, socketMessages]); - const [isLoadOlder, setIsLoadOlder] = useState(false); + const virtualizer = useVirtualizer({ + enabled: messages.length > 0, + count: hasMoreOlderMessages ? messages.length + 1 : messages.length, + getScrollElement: () => messagesWrapperRef.current, + measureElement: el => el.getBoundingClientRect().height, + getItemKey: idx => messages[idx]?.id ?? idx, + estimateSize: _i => 100, + overscan: 5, + paddingStart: 36, + paddingEnd: 36, + gap: 8, + }); - const handleScroll = useCallback(() => { - const el = messagesWrapperRef.current; - if (!el) return; + const items = virtualizer.getVirtualItems(); - if (el.scrollTop < 100) { - setIsLoadOlder(true); + const scrollToBottom = useCallback(() => { + virtualizer.scrollToOffset(0); + }, [virtualizer]); + + useEffect(() => { + const handleScroll = (e: WheelEvent) => { + e.preventDefault(); + const currentTarget = e.currentTarget as HTMLElement; + + if (currentTarget) { + currentTarget.scrollTop -= e.deltaY; + } + }; + messagesWrapperRef.current?.addEventListener("wheel", handleScroll, { + passive: false, + }); + return () => { + // eslint-disable-next-line react-hooks/exhaustive-deps + messagesWrapperRef.current?.removeEventListener("wheel", handleScroll); + }; + }, []); + + useEffect(() => { + const [lastItem] = [...items].reverse(); + + if (!lastItem) { + return; + } + + if ( + lastItem.index >= messages.length - 1 && + hasMoreOlderMessages && + !isLoadingOlder + ) { 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]); + }, [hasMoreOlderMessages, loadOlder, messages.length, isLoadingOlder, items]); useEffect(() => { - if (isLoadOlder) { - setIsLoadOlder(false); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [socketMessages]); + if (!messagesWrapperRef.current || messages.length === 0) return; - 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]); + setIsScrolledUp((virtualizer.scrollOffset || 0) > 600); + }, [virtualizer.scrollOffset, messages.length, messagesWrapperRef]); return ( -
- {isLoadingOlder && hasMoreOlderMessages && ( -
- -
+ <> + {isScrolledUp && ( + )} - - { - send(suggestion); - }} - /> -
+
+ { + send(suggestion); + }} + /> +
+ {items.map(virtualRow => { + const message = messages[virtualRow.index]; + const isLoaderRow = virtualRow.index > messages.length - 1; + + return ( +
+ {!isLoaderRow && ( + + )} + {isLoaderRow && ( +
+ +
+ )} +
+ ); + })} +
+
+ ); } diff --git a/src/components/domains/chat/CorrespondenceStartedWrapper/CorrespondenceStartedWrapper.tsx b/src/components/domains/chat/CorrespondenceStartedWrapper/CorrespondenceStartedWrapper.tsx index 658bfea..b488bab 100644 --- a/src/components/domains/chat/CorrespondenceStartedWrapper/CorrespondenceStartedWrapper.tsx +++ b/src/components/domains/chat/CorrespondenceStartedWrapper/CorrespondenceStartedWrapper.tsx @@ -1,25 +1,16 @@ "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 { useChats } from "@/providers/chats-provider"; import { ChatItemsList, CorrespondenceStarted } from ".."; -interface CorrespondenceStartedWrapperProps { - chatsPromise: Promise; -} - -export default function CorrespondenceStartedWrapper({ - chatsPromise, -}: CorrespondenceStartedWrapperProps) { +export default function CorrespondenceStartedWrapper() { const t = useTranslations("Chat"); - const chats = use(chatsPromise); - const { startedChats } = useChatsSocket({ initialChats: chats }); + const { startedChats } = useChats(); const { isVisibleAll } = useAppUiStore( state => state.chats.correspondenceStarted diff --git a/src/components/domains/chat/MessageInput/MessageInput.tsx b/src/components/domains/chat/MessageInput/MessageInput.tsx index 4f38d5f..eac5336 100644 --- a/src/components/domains/chat/MessageInput/MessageInput.tsx +++ b/src/components/domains/chat/MessageInput/MessageInput.tsx @@ -22,12 +22,37 @@ export default function MessageInput({ onSend }: MessageInputProps) { } }; + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + if (e.ctrlKey) { + // Ctrl + Enter - добавляем перенос строки + e.preventDefault(); + const target = e.target as HTMLTextAreaElement; + const start = target.selectionStart; + const end = target.selectionEnd; + const newValue = + message.substring(0, start) + "\n" + message.substring(end); + setMessage(newValue); + + // Устанавливаем курсор после переноса строки + setTimeout(() => { + target.selectionStart = target.selectionEnd = start + 1; + }, 0); + } else { + // Enter без Ctrl - отправляем сообщение + e.preventDefault(); + handleSend(); + } + } + }; + return (
setMessage(e.target.value)} placeholder={t("message_input_placeholder")} + onKeyDown={handleKeyDown} maxRows={5} /> + ); +} diff --git a/src/components/domains/secret-discount/save-off/index.ts b/src/components/domains/secret-discount/save-off/index.ts new file mode 100644 index 0000000..b4e9c47 --- /dev/null +++ b/src/components/domains/secret-discount/save-off/index.ts @@ -0,0 +1,3 @@ +export { default as Blob1 } from "./Blob1/Blob1"; +export { default as Blob2 } from "./Blob2/Blob2"; +export { default as SaveOffButton } from "./Button/Button"; diff --git a/src/components/domains/secret-discount/secret-discount/Blob3/Blob3.tsx b/src/components/domains/secret-discount/secret-discount/Blob3/Blob3.tsx new file mode 100644 index 0000000..49a6e9c --- /dev/null +++ b/src/components/domains/secret-discount/secret-discount/Blob3/Blob3.tsx @@ -0,0 +1,34 @@ +import { SVGProps } from "react"; + +function Blob3(props: SVGProps) { + return ( + + + + + + + + + + ); +} + +export default Blob3; diff --git a/src/components/domains/secret-discount/secret-discount/Blob4/Blob4.tsx b/src/components/domains/secret-discount/secret-discount/Blob4/Blob4.tsx new file mode 100644 index 0000000..ba8ee95 --- /dev/null +++ b/src/components/domains/secret-discount/secret-discount/Blob4/Blob4.tsx @@ -0,0 +1,36 @@ +import { SVGProps } from "react"; + +function Blob4(props: SVGProps) { + const width = props.width ? Number(props.width) : 419; + const height = props.height ? Number(props.height) : 193; + return ( + + + + + + + + + + ); +} + +export default Blob4; diff --git a/src/components/domains/secret-discount/secret-discount/Button/Button.module.scss b/src/components/domains/secret-discount/secret-discount/Button/Button.module.scss new file mode 100644 index 0000000..cde1e54 --- /dev/null +++ b/src/components/domains/secret-discount/secret-discount/Button/Button.module.scss @@ -0,0 +1,6 @@ +.button { + max-width: 400px; + margin-top: 30px; + min-height: 60px; + text-transform: uppercase; +} diff --git a/src/components/domains/secret-discount/secret-discount/Button/Button.tsx b/src/components/domains/secret-discount/secret-discount/Button/Button.tsx new file mode 100644 index 0000000..b896d48 --- /dev/null +++ b/src/components/domains/secret-discount/secret-discount/Button/Button.tsx @@ -0,0 +1,55 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { useTranslations } from "next-intl"; + +import { Button, Typography } from "@/components/ui"; +import { usePeriod } from "@/hooks/translations/usePeriod"; +import { ROUTES } from "@/shared/constants/client-routes"; +import { PeriodType } from "@/types/period"; + +import styles from "./Button.module.scss"; + +interface ISecretDiscountButtonProps { + trialPeriod: PeriodType; + trialInterval: number; + productId: string; + placementId: string; + paywallId: string; +} + +export default function SecretDiscountButton({ + trialPeriod, + trialInterval, + productId, + placementId, + paywallId, +}: ISecretDiscountButtonProps) { + const t = useTranslations("SecretDiscount"); + const { getPeriodText } = usePeriod(); + const router = useRouter(); + + const handleNext = () => { + router.push( + ROUTES.payment({ + productId, + placementId, + paywallId, + }) + ); + }; + + return ( + + ); +} diff --git a/src/components/domains/secret-discount/secret-discount/Policy/Policy.module.scss b/src/components/domains/secret-discount/secret-discount/Policy/Policy.module.scss new file mode 100644 index 0000000..a6e7277 --- /dev/null +++ b/src/components/domains/secret-discount/secret-discount/Policy/Policy.module.scss @@ -0,0 +1,36 @@ +// .policy { +// position: sticky; +// top: 0; +// margin-top: auto; +// padding: 34px 14px; +// font-size: 13px; +// font-weight: 400; +// line-height: 130%; +// } + +.policy-container { + position: absolute; + width: calc(100%); + max-width: 560px; + height: fit-content; + bottom: 0; + left: 50%; + transform: translateX(-50%); + + & > .policy { + width: 100%; + margin: 34px 0; + padding: 0 14px; + font-size: 13px; + font-weight: 400; + line-height: 130%; + color: #fff; + } + + .blob4 { + position: absolute; + bottom: 0; + left: 0; + z-index: -1; + } +} diff --git a/src/components/domains/secret-discount/secret-discount/Policy/Policy.tsx b/src/components/domains/secret-discount/secret-discount/Policy/Policy.tsx new file mode 100644 index 0000000..c8ed12f --- /dev/null +++ b/src/components/domains/secret-discount/secret-discount/Policy/Policy.tsx @@ -0,0 +1,60 @@ +"use client"; + +import { useTranslations } from "next-intl"; + +import { Typography } from "@/components/ui"; +import { useDynamicSize } from "@/hooks/DOM/useDynamicSize"; +import { usePeriod } from "@/hooks/translations/usePeriod"; +import { getFormattedPrice } from "@/shared/utils/price"; +import { Currency } from "@/types"; +import { PeriodType } from "@/types/period"; + +import { Blob4 } from ".."; + +import styles from "./Policy.module.scss"; + +interface ISecretDiscountPolicy { + trialPeriod: PeriodType; + trialInterval: number; + billingPeriod: PeriodType; + billingInterval: number; + price: number; + currency: Currency; +} + +export default function SecretDiscountPolicy({ + trialPeriod, + trialInterval, + billingPeriod, + billingInterval, + price, + currency, +}: ISecretDiscountPolicy) { + const t = useTranslations("SecretDiscount"); + const { getPeriodText } = usePeriod(); + const { + height, + width, + elementRef: policyContainerRef, + } = useDynamicSize({ defaultWidth: 560, defaultHeight: 193 }); + + return ( + <> +
+ + {t("policy", { + trialPeriod: getPeriodText( + trialPeriod, + trialInterval, + "period_adjective" + ), + price: getFormattedPrice(price, currency, 0), + billingPeriod: getPeriodText(billingPeriod, billingInterval), + })} + + +
+
+ + ); +} diff --git a/src/components/domains/secret-discount/secret-discount/SecretDiscountTable/SecretDiscountTable.module.scss b/src/components/domains/secret-discount/secret-discount/SecretDiscountTable/SecretDiscountTable.module.scss new file mode 100644 index 0000000..82fec77 --- /dev/null +++ b/src/components/domains/secret-discount/secret-discount/SecretDiscountTable/SecretDiscountTable.module.scss @@ -0,0 +1,95 @@ +.container { + background-color: #fff; + border-radius: 13px; + width: calc(100% + 24px); + padding: 16px 0 22px; + box-shadow: 2px 11px 17px -1px rgba(0, 0, 0, 0.13); + margin-top: 42px; + color: #363636; +} + +.title { + line-height: 24px; +} + +.subtitle { + font-size: 13px; + line-height: 16px; + margin-top: 5px; +} + +.applied { + width: 100%; + background-color: #293d68; + padding: 7px 10px; + margin-top: 12px; + display: flex; + align-items: center; + justify-content: flex-start; + gap: 10px; + + // & > img { + // width: 17px; + // } + + & > .title { + font-size: 15px; + line-height: 19px; + } + + & > .oldDiscount { + font-size: 15px; + color: #b2b2b2; + text-decoration: line-through; + margin-left: 4px; + } + + & > .newDiscount { + line-height: 19px; + margin-left: 4px; + } +} + +.gridLine { + display: grid; + grid-template-columns: 1fr 22px 22px; + align-items: center; + gap: 28px; + margin-top: 8px; + padding: 0 24px 0 10px; + + // & > p { + // font-size: 12px; + // font-weight: 400; + // line-height: 130%; + // margin-bottom: 0; + // } + + &.afterTrial > .lineText { + line-height: 130%; + + &.oldPrice { + text-decoration: line-through; + } + } + + &.save { + margin-top: 2px; + } + + &.totalToday { + & > .totalTodayText { + line-height: 130%; + margin-top: 8px; + } + } +} + +.hr { + display: block; + width: 100%; + margin: 0; + background-color: #363636; + margin-top: 6px; + height: 1px; +} diff --git a/src/components/domains/secret-discount/secret-discount/SecretDiscountTable/SecretDiscountTable.tsx b/src/components/domains/secret-discount/secret-discount/SecretDiscountTable/SecretDiscountTable.tsx new file mode 100644 index 0000000..cf502f4 --- /dev/null +++ b/src/components/domains/secret-discount/secret-discount/SecretDiscountTable/SecretDiscountTable.tsx @@ -0,0 +1,114 @@ +import Image from "next/image"; +import { getTranslations } from "next-intl/server"; +import clsx from "clsx"; + +import { Typography } from "@/components/ui"; +import { secretDiscountImages } from "@/shared/constants/images"; +import { getPeriodTextServer } from "@/shared/utils/period-server"; +import { getFormattedPrice } from "@/shared/utils/price"; +import { Currency } from "@/types"; +import { PeriodType } from "@/types/period"; + +import styles from "./SecretDiscountTable.module.scss"; + +const formatDiscount = (discount: number) => `-${discount}%`; + +interface ISecretDiscountTableProps { + trialPrice: number; + trialInterval: number; + trialPeriod: PeriodType; + oldPrice: number; + oldDiscount: number; + newDiscount: number; + currency: Currency; +} + +export default async function SecretDiscountTable({ + trialPrice, + trialInterval, + trialPeriod, + oldPrice, + oldDiscount, + newDiscount, + currency, +}: ISecretDiscountTableProps) { + const t = await getTranslations("SecretDiscount"); + + return ( +
+ + {t("secret-discount-table_title")} + + + {t("secret-discount-table_subtitle")} + +
+ Gift + + {t("secret-discount-table_discount-applied")} + + + {formatDiscount(oldDiscount)} + + + {formatDiscount(newDiscount)} + +
+
+ + {t("secret-discount-table_cost-after-trial", { + trialPeriod: await getPeriodTextServer(trialPeriod, trialInterval), + })} + + + {getFormattedPrice(oldPrice, currency, 0)} + + + {getFormattedPrice(trialPrice, currency, 0)} + +
+
+ + {t("secret-discount-table_you-save", { + amount: getFormattedPrice(oldPrice - trialPrice, currency, 0), + })} + +
+
+
+ + {t("secret-discount-table_total-today")} + + + {getFormattedPrice(trialPrice, currency, 0)} + +
+
+ ); +} diff --git a/src/components/domains/secret-discount/secret-discount/index.ts b/src/components/domains/secret-discount/secret-discount/index.ts new file mode 100644 index 0000000..8bf3336 --- /dev/null +++ b/src/components/domains/secret-discount/secret-discount/index.ts @@ -0,0 +1,5 @@ +export { default as Blob3 } from "./Blob3/Blob3"; +export { default as Blob4 } from "./Blob4/Blob4"; +export { default as SecretDiscountButton } from "./Button/Button"; +export { default as SecretDiscountTable } from "./SecretDiscountTable/SecretDiscountTable"; +export { default as SecretDiscountPolicy } from "./SecretDiscountTable/SecretDiscountTable"; diff --git a/src/components/layout/Header/Header.module.scss b/src/components/layout/Header/Header.module.scss index bae08d4..ba66aac 100644 --- a/src/components/layout/Header/Header.module.scss +++ b/src/components/layout/Header/Header.module.scss @@ -47,3 +47,10 @@ } } } + +.backButton.backButton { + width: fit-content; + padding: 0; + padding-right: 16px; + background: none; +} diff --git a/src/components/layout/Header/Header.tsx b/src/components/layout/Header/Header.tsx index 1aa5253..4a9df87 100644 --- a/src/components/layout/Header/Header.tsx +++ b/src/components/layout/Header/Header.tsx @@ -1,12 +1,11 @@ "use client"; -import { use } from "react"; import Link from "next/link"; +import { useRouter } from "next/navigation"; import clsx from "clsx"; import { Badge, Button, Icon, IconName, Typography } from "@/components/ui"; -import { IGetChatsListResponse } from "@/entities/chats/types"; -import { useChatsSocket } from "@/hooks/chats/useChatsSocket"; +import { useChats } from "@/providers/chats-provider"; import { ROUTES } from "@/shared/constants/client-routes"; import { useDrawer } from ".."; @@ -16,41 +15,69 @@ import styles from "./Header.module.scss"; interface HeaderProps { className?: string; - chatsPromise: Promise; + isVisibleMenuButton?: boolean; + isVisibleNotificationIcon?: boolean; + isVisibleSearchIcon?: boolean; + isVisibleBackButton?: boolean; } -export default function Header({ className, chatsPromise }: HeaderProps) { +export default function Header({ + className, + isVisibleMenuButton = true, + isVisibleNotificationIcon = true, + isVisibleSearchIcon = true, + isVisibleBackButton = false, +}: HeaderProps) { const { open } = useDrawer(); - const chats = use(chatsPromise); - const { totalUnreadCount } = useChatsSocket({ initialChats: chats }); + const { totalUnreadCount } = useChats(); + const router = useRouter(); + + const handleBack = () => { + router.back(); + }; return (
- +
+ {isVisibleBackButton && ( + + )} + {isVisibleMenuButton && ( + + )} +
- - - - {totalUnreadCount > 99 ? "99+" : totalUnreadCount} - - - + {isVisibleNotificationIcon && ( + + + + {totalUnreadCount > 99 ? "99+" : totalUnreadCount} + + + + )} - + {isVisibleSearchIcon && }
); diff --git a/src/components/layout/NavigationBar/NavigationBar.tsx b/src/components/layout/NavigationBar/NavigationBar.tsx index eb3639d..45a0aee 100644 --- a/src/components/layout/NavigationBar/NavigationBar.tsx +++ b/src/components/layout/NavigationBar/NavigationBar.tsx @@ -1,14 +1,12 @@ "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 { useChats } from "@/providers/chats-provider"; import { ROUTES } from "@/shared/constants/client-routes"; import { NavItem, navItems } from "@/shared/constants/navigation"; import { stripLocale } from "@/shared/utils/path"; @@ -20,16 +18,11 @@ const getBadge = (item: NavItem, totalUnreadCount: number) => { return null; }; -interface NavigationBarProps { - chatsPromise: Promise; -} - -export default function NavigationBar({ chatsPromise }: NavigationBarProps) { +export default function NavigationBar() { const pathname = usePathname(); const locale = useLocale(); const pathnameWithoutLocale = stripLocale(pathname, locale); - const chats = use(chatsPromise); - const { totalUnreadCount } = useChatsSocket({ initialChats: chats }); + const { totalUnreadCount } = useChats(); return (