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/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]/(payment)/layout.tsx b/src/app/[locale]/(payment)/layout.tsx
index f8c99ba..ba4f04d 100644
--- a/src/app/[locale]/(payment)/layout.tsx
+++ b/src/app/[locale]/(payment)/layout.tsx
@@ -2,7 +2,7 @@ import { DrawerProvider, Header } from "@/components/layout";
import styles from "./layout.module.scss";
-export default function CoreLayout({
+export default function PaymentLayout({
children,
}: Readonly<{
children: React.ReactNode;
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 (
+
+
+
+ );
+}
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 (
+ <>
+
+
+
+
+
+
+ {t("title", {
+ discount: discountNew,
+ })}
+
+
+ {t.rich("instead", {
+ price: () => (
+
+ {getFormattedPrice(trialPrice, currency, 0)}
+
+ ),
+ oldPrice: () => (
+
+ {t("instead-old-price", {
+ oldPrice: getFormattedPrice(price, currency, 0),
+ })}
+
+ ),
+ })}
+
+
+
+
+
+ {t.rich("trial-duration", {
+ trialPeriod: await getPeriodTextServer(trialPeriod, trialInterval),
+ oldTrialPeriod: async () => (
+
+ {await getPeriodTextServer(oldTrialPeriod, oldTrialInterval)}
+
+ ),
+ })}
+
+
+
+
+ {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/components/domains/secret-discount/index.ts b/src/components/domains/secret-discount/index.ts
new file mode 100644
index 0000000..d6f218a
--- /dev/null
+++ b/src/components/domains/secret-discount/index.ts
@@ -0,0 +1,2 @@
+export * from "./save-off";
+export * from "./secret-discount";
diff --git a/src/components/domains/secret-discount/save-off/Blob1/Blob1.tsx b/src/components/domains/secret-discount/save-off/Blob1/Blob1.tsx
new file mode 100644
index 0000000..5670687
--- /dev/null
+++ b/src/components/domains/secret-discount/save-off/Blob1/Blob1.tsx
@@ -0,0 +1,34 @@
+import { SVGProps } from "react";
+
+function Blob1(props: SVGProps) {
+ return (
+
+ );
+}
+
+export default Blob1;
diff --git a/src/components/domains/secret-discount/save-off/Blob2/Blob2.tsx b/src/components/domains/secret-discount/save-off/Blob2/Blob2.tsx
new file mode 100644
index 0000000..84ab0fb
--- /dev/null
+++ b/src/components/domains/secret-discount/save-off/Blob2/Blob2.tsx
@@ -0,0 +1,34 @@
+import { SVGProps } from "react";
+
+function Blob2(props: SVGProps) {
+ return (
+
+ );
+}
+
+export default Blob2;
diff --git a/src/components/domains/secret-discount/save-off/Button/Button.module.scss b/src/components/domains/secret-discount/save-off/Button/Button.module.scss
new file mode 100644
index 0000000..01da169
--- /dev/null
+++ b/src/components/domains/secret-discount/save-off/Button/Button.module.scss
@@ -0,0 +1,5 @@
+.button {
+ max-width: 400px;
+ margin-top: 16px;
+ min-height: 60px;
+}
diff --git a/src/components/domains/secret-discount/save-off/Button/Button.tsx b/src/components/domains/secret-discount/save-off/Button/Button.tsx
new file mode 100644
index 0000000..2332476
--- /dev/null
+++ b/src/components/domains/secret-discount/save-off/Button/Button.tsx
@@ -0,0 +1,39 @@
+"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 ISaveOffButtonProps {
+ trialPeriod: PeriodType;
+ trialInterval: number;
+}
+
+export default function SaveOffButton({
+ trialPeriod,
+ trialInterval,
+}: ISaveOffButtonProps) {
+ const t = useTranslations("SaveOff");
+ const { getPeriodText } = usePeriod();
+ const router = useRouter();
+
+ const handleNext = () => {
+ router.push(ROUTES.secretDiscount());
+ };
+
+ return (
+
+ );
+}
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")}
+
+
+
+
+ {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 5a10f1a..4a9df87 100644
--- a/src/components/layout/Header/Header.tsx
+++ b/src/components/layout/Header/Header.tsx
@@ -1,6 +1,7 @@
"use client";
import Link from "next/link";
+import { useRouter } from "next/navigation";
import clsx from "clsx";
import { Badge, Button, Icon, IconName, Typography } from "@/components/ui";
@@ -14,39 +15,69 @@ import styles from "./Header.module.scss";
interface HeaderProps {
className?: string;
+ isVisibleMenuButton?: boolean;
+ isVisibleNotificationIcon?: boolean;
+ isVisibleSearchIcon?: boolean;
+ isVisibleBackButton?: boolean;
}
-export default function Header({ className }: HeaderProps) {
+export default function Header({
+ className,
+ isVisibleMenuButton = true,
+ isVisibleNotificationIcon = true,
+ isVisibleSearchIcon = true,
+ isVisibleBackButton = false,
+}: HeaderProps) {
const { open } = useDrawer();
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/ui/Typography/Typography.tsx b/src/components/ui/Typography/Typography.tsx
index 4d92e15..f7eaf0e 100644
--- a/src/components/ui/Typography/Typography.tsx
+++ b/src/components/ui/Typography/Typography.tsx
@@ -18,6 +18,7 @@ export type TypographyProps = {
| "success"
| "muted";
align?: "center" | "left" | "right";
+ style?: React.CSSProperties;
};
const sizeMap = {
@@ -54,6 +55,7 @@ export default function Typography({
size = "md",
color = "default",
align = "center",
+ style,
}: TypographyProps) {
return (
{children}
diff --git a/src/entities/user/api.ts b/src/entities/user/api.ts
index 0dcacef..11171c0 100644
--- a/src/entities/user/api.ts
+++ b/src/entities/user/api.ts
@@ -8,5 +8,6 @@ export const getMe = async (): Promise => {
tags: ["user", "me"],
schema: MeResponseSchema,
revalidate: 0,
+ skipAuthRedirect: true,
});
};
diff --git a/src/hooks/translations/usePeriod.ts b/src/hooks/translations/usePeriod.ts
new file mode 100644
index 0000000..7163a43
--- /dev/null
+++ b/src/hooks/translations/usePeriod.ts
@@ -0,0 +1,23 @@
+"use client";
+
+import { useCallback, useMemo } from "react";
+import { useTranslations } from "next-intl";
+
+import { PeriodKeyVariant, PeriodType } from "@/types/period";
+
+export const usePeriod = () => {
+ const t = useTranslations();
+
+ const getPeriodText = useCallback(
+ (
+ periodType: PeriodType,
+ count: number,
+ periodKeyVariant: PeriodKeyVariant = "period"
+ ) => {
+ return t(`${periodKeyVariant}.${periodType.toLowerCase()}`, { count });
+ },
+ [t]
+ );
+
+ return useMemo(() => ({ getPeriodText }), [getPeriodText]);
+};
diff --git a/src/shared/constants/client-routes.ts b/src/shared/constants/client-routes.ts
index e15256e..a969761 100644
--- a/src/shared/constants/client-routes.ts
+++ b/src/shared/constants/client-routes.ts
@@ -70,6 +70,10 @@ export const ROUTES = {
paymentSuccess: () => createRoute(["payment", "success"]),
paymentFailed: () => createRoute(["payment", "failed"]),
+ // Secret Discount
+ saveOff: () => createRoute(["save-off"]),
+ secretDiscount: () => createRoute(["secret-discount"]),
+
// Chat
chat: (id?: string) => createRoute(["chat", id]),
diff --git a/src/shared/constants/images/index.ts b/src/shared/constants/images/index.ts
index 102a360..8955d60 100644
--- a/src/shared/constants/images/index.ts
+++ b/src/shared/constants/images/index.ts
@@ -1,2 +1,3 @@
export * from "./email-marketing";
export * from "./retaining";
+export * from "./secret-discount";
diff --git a/src/shared/constants/images/secret-discount.ts b/src/shared/constants/images/secret-discount.ts
new file mode 100644
index 0000000..bf585d9
--- /dev/null
+++ b/src/shared/constants/images/secret-discount.ts
@@ -0,0 +1,2 @@
+export const secretDiscountImages = (path: string) =>
+ `/secret-discount/${path}`;
diff --git a/src/shared/utils/funnel.ts b/src/shared/utils/funnel.ts
new file mode 100644
index 0000000..ffd08f3
--- /dev/null
+++ b/src/shared/utils/funnel.ts
@@ -0,0 +1,19 @@
+import { IFunnelPaymentPlacement } from "@/entities/session/funnel/types";
+
+const getProperty = (
+ paymentData: IFunnelPaymentPlacement | IFunnelPaymentPlacement[] | null,
+ key: string
+) => {
+ const properties = Array.isArray(paymentData)
+ ? []
+ : (paymentData?.properties ?? []);
+
+ return (
+ properties.find(p => p.key === key) ?? {
+ key,
+ value: `property: "${key}" not found`,
+ }
+ );
+};
+
+export { getProperty };
diff --git a/src/shared/utils/period-server.ts b/src/shared/utils/period-server.ts
new file mode 100644
index 0000000..494df63
--- /dev/null
+++ b/src/shared/utils/period-server.ts
@@ -0,0 +1,13 @@
+import { getTranslations } from "next-intl/server";
+
+import { PeriodKeyVariant, PeriodType } from "@/types/period";
+
+export const getPeriodTextServer = async (
+ periodType: PeriodType,
+ count: number,
+ periodKeyVariant: PeriodKeyVariant = "period"
+): Promise => {
+ const t = await getTranslations(periodKeyVariant);
+
+ return t(periodType.toLowerCase(), { count });
+};
diff --git a/src/types/index.ts b/src/types/index.ts
index cf0cbe3..d6731b3 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -81,5 +81,3 @@ export enum ELocalesPlacement {
Profile = "profile",
RetainingFunnel = "retaining-funnel",
}
-
-export type PeriodType = "DAY" | "WEEK" | "MONTH" | "YEAR";
diff --git a/src/types/period.ts b/src/types/period.ts
new file mode 100644
index 0000000..a0bdade
--- /dev/null
+++ b/src/types/period.ts
@@ -0,0 +1,6 @@
+export type PeriodType = "DAY" | "WEEK" | "MONTH" | "YEAR";
+export type GrammaticalCase = "nominative" | "dat";
+export type PeriodKeyVariant =
+ | "period"
+ | "period_adjective"
+ | "period_without_count";