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 ( + +
{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/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")} + +
+ 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 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";