secret-discount
pages with secret discount
This commit is contained in:
parent
630ebaaf1b
commit
c7aa6f264c
@ -393,5 +393,36 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"SaveOff": {
|
||||
"title": "SAVE {discount}% OFF!",
|
||||
"instead": "<price></price> instead <oldPrice></oldPrice>",
|
||||
"instead-old-price": "of {oldPrice}",
|
||||
"trial-duration": "{trialPeriod} trial instead of <oldTrialPeriod></oldTrialPeriod>",
|
||||
"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}}"
|
||||
}
|
||||
}
|
||||
|
||||
BIN
public/secret-discount/fire.png
Normal file
BIN
public/secret-discount/fire.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
BIN
public/secret-discount/gift.png
Normal file
BIN
public/secret-discount/gift.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
23
public/secret-discount/gift.svg
Normal file
23
public/secret-discount/gift.svg
Normal file
@ -0,0 +1,23 @@
|
||||
<svg width="280" height="241" viewBox="0 0 280 241" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M132.73 66.5252C119.181 61.0832 91.4158 51.9763 90.3052 34.3178C89.861 28.8759 90.4163 29.7643 94.9697 31.2081C98.9679 32.5408 102.744 35.5395 105.854 38.316C116.404 47.645 122.402 60.639 126.067 73.9662C129.731 87.7376 151.166 81.8514 147.39 68.08C141.948 47.5339 131.175 28.8759 113.739 16.3261C100.412 6.77495 81.5315 3.33208 71.5361 18.7694C61.7628 33.8736 71.7582 53.6422 83.0863 64.1929C95.3029 75.521 111.629 81.8514 126.844 87.9597C140.171 93.1796 145.946 71.6339 132.73 66.5252Z" fill="#FFD86E"/>
|
||||
<path d="M155.275 87.9596C170.935 81.7402 186.039 75.1877 199.033 64.1927C210.805 54.1973 220.023 33.4291 210.583 18.7692C200.255 2.88766 182.041 7.33008 168.381 16.3259C150.722 27.9872 140.06 48.089 134.729 68.0798C130.953 81.8513 152.388 87.7374 156.053 73.966C159.829 60.1946 165.937 48.3112 176.266 38.3158C179.264 35.3172 183.152 32.8739 187.15 31.208C192.036 29.2089 191.703 29.3199 191.814 34.3176C192.481 51.4208 161.717 61.6384 149.389 66.525C136.173 71.6338 141.948 93.1794 155.275 87.9596Z" fill="#FFD86E"/>
|
||||
<path d="M174.378 69.9678H149.945H30.0001V103.286H52.7673H149.945H174.378H235.905H252.12V69.9678H174.378Z" fill="#ED4C4C"/>
|
||||
<path d="M132.397 103.286H149.723V86.1829V69.1907H132.397V86.1829V103.286Z" fill="#FFD86E"/>
|
||||
<path d="M236.572 132.161H45.5488V241H236.572V132.161Z" fill="#ED4C4C"/>
|
||||
<mask id="mask0_1_7976" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="45" y="132" width="192" height="109">
|
||||
<path d="M236.572 132.161H45.5488V241H236.572V132.161Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_1_7976)">
|
||||
<g opacity="0.4">
|
||||
<path d="M-20.5872 191.634V181.528H300.487V191.634H-20.5872Z" fill="#FAE5E5" stroke="#ED4C4C"/>
|
||||
<path d="M-20.5872 222.73V212.624H300.487V222.73H-20.5872Z" fill="#FAE5E5" stroke="#ED4C4C"/>
|
||||
</g>
|
||||
</g>
|
||||
<path d="M132.397 241H149.723V186.58V132.161H132.397V186.58V241Z" fill="#FFD86E"/>
|
||||
<path d="M279.325 7.77419C279.325 11.8294 276.106 15.0484 272.05 15.0484C267.995 15.0484 264.776 11.8294 264.776 7.77419C264.776 3.719 267.995 0.5 272.05 0.5C276.106 0.5 279.325 3.719 279.325 7.77419Z" fill="#FC6524" stroke="#ED4C4C"/>
|
||||
<path d="M238.288 56.6405C238.288 58.8617 236.511 60.6387 234.29 60.6387C232.069 60.6387 230.292 58.8617 230.292 56.6405C230.292 54.4193 232.069 52.6423 234.29 52.6423C236.511 52.6423 238.288 54.4193 238.288 56.6405Z" fill="#FFD86E"/>
|
||||
<path d="M118.344 118.834C118.344 121.055 116.567 122.832 114.346 122.832C112.125 122.832 110.348 121.055 110.348 118.834C110.348 116.613 112.125 114.836 114.346 114.836C116.567 114.836 118.344 116.613 118.344 118.834Z" fill="#C14040"/>
|
||||
<path d="M17.7232 56.6405C17.7232 60.9719 14.2803 64.4147 9.949 64.4147C5.61766 64.4147 2.1748 60.9719 2.1748 56.6405C2.1748 52.3092 5.61766 48.8663 9.949 48.8663C14.2803 48.8663 17.7232 52.3092 17.7232 56.6405Z" stroke="#ED4C4C" stroke-width="4" stroke-miterlimit="10"/>
|
||||
<path d="M266.164 138.825C266.164 141.712 263.832 144.045 260.944 144.045C258.057 144.045 255.725 141.712 255.725 138.825C255.725 135.937 258.057 133.605 260.944 133.605C263.832 133.605 266.164 135.937 266.164 138.825Z" stroke="#ED4C4C" stroke-width="4" stroke-miterlimit="10"/>
|
||||
<path d="M43.2666 124.276L34.1597 140.046L25.0527 124.276H43.2666Z" fill="#FFD86E"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
@ -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;
|
||||
|
||||
13
src/app/[locale]/(secret-discount)/layout.tsx
Normal file
13
src/app/[locale]/(secret-discount)/layout.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import { DrawerProvider } from "@/components/layout";
|
||||
|
||||
export default function SecretDiscountLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<DrawerProvider>
|
||||
<section>{children}</section>
|
||||
</DrawerProvider>
|
||||
);
|
||||
}
|
||||
60
src/app/[locale]/(secret-discount)/save-off/page.module.scss
Normal file
60
src/app/[locale]/(secret-discount)/save-off/page.module.scss
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
134
src/app/[locale]/(secret-discount)/save-off/page.tsx
Normal file
134
src/app/[locale]/(secret-discount)/save-off/page.tsx
Normal file
@ -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 (
|
||||
<>
|
||||
<Header
|
||||
isVisibleMenuButton={false}
|
||||
isVisibleNotificationIcon={false}
|
||||
isVisibleSearchIcon={false}
|
||||
/>
|
||||
<div className={styles.container}>
|
||||
<Blob1 className={styles.blob} />
|
||||
<Blob2 className={styles.blob2} />
|
||||
<Image
|
||||
src={secretDiscountImages("gift.svg")}
|
||||
alt="Gift"
|
||||
width={280}
|
||||
height={241}
|
||||
/>
|
||||
<Typography
|
||||
as="h1"
|
||||
size="2xl"
|
||||
weight="semiBold"
|
||||
className={styles.title}
|
||||
>
|
||||
{t("title", {
|
||||
discount: discountNew,
|
||||
})}
|
||||
</Typography>
|
||||
<Typography as="p" className={styles.description}>
|
||||
{t.rich("instead", {
|
||||
price: () => (
|
||||
<span className={styles.price}>
|
||||
{getFormattedPrice(trialPrice, currency, 0)}
|
||||
</span>
|
||||
),
|
||||
oldPrice: () => (
|
||||
<span className={styles.discount}>
|
||||
{t("instead-old-price", {
|
||||
oldPrice: getFormattedPrice(price, currency, 0),
|
||||
})}
|
||||
</span>
|
||||
),
|
||||
})}
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
as="p"
|
||||
weight="semiBold"
|
||||
className={styles.point}
|
||||
style={{ marginTop: 12 }}
|
||||
>
|
||||
<Image
|
||||
src={secretDiscountImages("fire.png")}
|
||||
alt="fire"
|
||||
width={31}
|
||||
height={31}
|
||||
/>
|
||||
|
||||
{t.rich("trial-duration", {
|
||||
trialPeriod: await getPeriodTextServer(trialPeriod, trialInterval),
|
||||
oldTrialPeriod: async () => (
|
||||
<s>
|
||||
{await getPeriodTextServer(oldTrialPeriod, oldTrialInterval)}
|
||||
</s>
|
||||
),
|
||||
})}
|
||||
</Typography>
|
||||
|
||||
<Typography as="p" weight="semiBold" className={styles.point}>
|
||||
<Image
|
||||
src={secretDiscountImages("gift.png")}
|
||||
alt="gift"
|
||||
width={31}
|
||||
height={31}
|
||||
/>
|
||||
{t("discount-offer", {
|
||||
discount: discountNew,
|
||||
})}
|
||||
</Typography>
|
||||
|
||||
<SaveOffButton
|
||||
trialPeriod={trialPeriod}
|
||||
trialInterval={trialInterval}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
101
src/app/[locale]/(secret-discount)/secret-discount/page.tsx
Normal file
101
src/app/[locale]/(secret-discount)/secret-discount/page.tsx
Normal file
@ -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 (
|
||||
<>
|
||||
<Header
|
||||
isVisibleMenuButton={false}
|
||||
isVisibleNotificationIcon={false}
|
||||
isVisibleSearchIcon={false}
|
||||
isVisibleBackButton={true}
|
||||
/>
|
||||
<Blob3 className={styles.blob3} />
|
||||
<div className={styles.container}>
|
||||
<Typography
|
||||
as="h1"
|
||||
weight="semiBold"
|
||||
color="white"
|
||||
className={styles.title}
|
||||
>
|
||||
{t("title")}
|
||||
</Typography>
|
||||
|
||||
<SecretDiscountTable
|
||||
trialPrice={trialPrice}
|
||||
trialInterval={trialInterval}
|
||||
trialPeriod={trialPeriod}
|
||||
oldPrice={oldPrice}
|
||||
oldDiscount={Number(discountOld)}
|
||||
newDiscount={Number(discountNew)}
|
||||
currency={currency}
|
||||
/>
|
||||
|
||||
<SecretDiscountButton
|
||||
trialPeriod={trialPeriod}
|
||||
trialInterval={trialInterval}
|
||||
productId={productId}
|
||||
placementId={placementId}
|
||||
paywallId={paywallId}
|
||||
/>
|
||||
|
||||
<SecretDiscountPolicy
|
||||
trialPeriod={trialPeriod}
|
||||
trialInterval={trialInterval}
|
||||
billingPeriod={billingPeriod}
|
||||
billingInterval={billingInterval}
|
||||
price={price}
|
||||
currency={currency}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
2
src/components/domains/secret-discount/index.ts
Normal file
2
src/components/domains/secret-discount/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./save-off";
|
||||
export * from "./secret-discount";
|
||||
@ -0,0 +1,34 @@
|
||||
import { SVGProps } from "react";
|
||||
|
||||
function Blob1(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
width="216"
|
||||
height="150"
|
||||
viewBox="0 0 216 150"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M303.878 -65.5499C282.778 -127.95 170.378 -59.0499 127.378 -68.8499C79.078 -79.7499 25.378 -76.5499 4.67803 -24.4499C-6.12197 2.95011 -0.321915 63.4501 44.4781 56.3501C67.8781 52.6501 89.578 27.2501 113.378 32.9501C140.878 39.4501 139.178 66.0501 145.778 86.2501C160.678 132.15 220.678 171.55 264.478 134.25C289.978 112.55 288.078 74.4501 290.278 45.1501C292.778 11.3501 311.978 -21.9499 306.178 -56.3499C305.645 -59.6832 304.878 -62.7499 303.878 -65.5499Z"
|
||||
fill="url(#paint0_linear_1_7947)"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_1_7947"
|
||||
x1="227.33"
|
||||
y1="-166.848"
|
||||
x2="364.777"
|
||||
y2="7.71622"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#E3FEF9" />
|
||||
<stop offset="1" stopColor="#BCF0FF" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default Blob1;
|
||||
@ -0,0 +1,34 @@
|
||||
import { SVGProps } from "react";
|
||||
|
||||
function Blob2(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
width="197"
|
||||
height="173"
|
||||
viewBox="0 0 197 173"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M376.745 8.31103C313.484 -9.05387 250.755 2.50459 205.527 52.2953C171.122 90.2852 159.804 138.706 100.572 145.714C42.2871 152.666 -19.4446 181.366 6.01194 257.137C34.4467 341.884 139.028 318.246 203.745 298.11C229.74 289.96 254.95 280.876 281.311 274.066C329.257 261.653 377.408 245.774 415.485 213.013C440.246 191.695 461.108 118.086 457.281 86.0026C452.562 46.4328 423.44 24.1359 387.627 11.7445C384.09 10.5207 380.463 9.37619 376.745 8.31103Z"
|
||||
fill="url(#paint0_linear_1_7945)"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_1_7945"
|
||||
x1="237.028"
|
||||
y1="-87.8634"
|
||||
x2="464.157"
|
||||
y2="55.102"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#FEF5E3" />
|
||||
<stop offset="1" stopColor="#BCFFBE" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default Blob2;
|
||||
@ -0,0 +1,5 @@
|
||||
.button {
|
||||
max-width: 400px;
|
||||
margin-top: 16px;
|
||||
min-height: 60px;
|
||||
}
|
||||
@ -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 (
|
||||
<Button className={styles.button} onClick={handleNext}>
|
||||
<Typography color="white" size="xl">
|
||||
{t("button-trial", {
|
||||
trialPeriod: getPeriodText(trialPeriod, trialInterval),
|
||||
})}
|
||||
</Typography>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
3
src/components/domains/secret-discount/save-off/index.ts
Normal file
3
src/components/domains/secret-discount/save-off/index.ts
Normal file
@ -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";
|
||||
@ -0,0 +1,34 @@
|
||||
import { SVGProps } from "react";
|
||||
|
||||
function Blob3(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
width="147"
|
||||
height="285"
|
||||
viewBox="0 0 147 285"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M-29.9172 282.953C56.8893 300.323 52.2376 123.186 92.5422 79.7173C137.689 30.8185 170.054 -33.8408 123.328 -92.9459C98.6956 -123.929 24.3829 -157.832 2.5382 -100.912C-8.88165 -71.187 6.0972 -28.8594 -16.5319 -4.98539C-42.5788 22.658 -72.3955 2.80487 -100.341 -3.08616C-163.777 -16.5852 -249.958 26.7819 -235.981 102.831C-227.863 147.096 -182.242 170.486 -149.619 192.736C-111.96 218.358 -86.1061 263.084 -42.1707 279.449C-37.9326 281.068 -33.8482 282.236 -29.9172 282.953Z"
|
||||
fill="url(#paint0_linear_1_8023)"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_1_8023"
|
||||
x1="139.426"
|
||||
y1="261.917"
|
||||
x2="-156.124"
|
||||
y2="304.57"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#E3FEF9" />
|
||||
<stop offset="1" stopColor="#BCF0FF" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default Blob3;
|
||||
@ -0,0 +1,36 @@
|
||||
import { SVGProps } from "react";
|
||||
|
||||
function Blob4(props: SVGProps<SVGSVGElement>) {
|
||||
const width = props.width ? Number(props.width) : 419;
|
||||
const height = props.height ? Number(props.height) : 193;
|
||||
return (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox={`0 0 ${width} ${height}`}
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d={`M${width + 78} 37.2239C${width * 0.58} 116.789 ${width * 0.35} -66.6269 ${width * -0.186} 27.6975V${height}H${width}V37.2239Z`}
|
||||
fill="url(#paint0_linear_1_8022)"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_1_8022"
|
||||
x1={1.186 * width}
|
||||
y1={height}
|
||||
x2={-0.186 * width}
|
||||
y2={height}
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#FFA1BA" />
|
||||
<stop offset="1" stopColor="#9A55FF" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default Blob4;
|
||||
@ -0,0 +1,6 @@
|
||||
.button {
|
||||
max-width: 400px;
|
||||
margin-top: 30px;
|
||||
min-height: 60px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
@ -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 (
|
||||
<Button className={styles.button} onClick={handleNext}>
|
||||
<Typography color="white" size="lg" weight="semiBold">
|
||||
{t("button-trial", {
|
||||
trialPeriod: getPeriodText(
|
||||
trialPeriod,
|
||||
trialInterval,
|
||||
"period_adjective"
|
||||
),
|
||||
})}
|
||||
</Typography>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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<HTMLDivElement>({ defaultWidth: 560, defaultHeight: 193 });
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles["policy-container"]} ref={policyContainerRef}>
|
||||
<Typography as="p" align="left" className={styles.policy}>
|
||||
{t("policy", {
|
||||
trialPeriod: getPeriodText(
|
||||
trialPeriod,
|
||||
trialInterval,
|
||||
"period_adjective"
|
||||
),
|
||||
price: getFormattedPrice(price, currency, 0),
|
||||
billingPeriod: getPeriodText(billingPeriod, billingInterval),
|
||||
})}
|
||||
</Typography>
|
||||
<Blob4 className={styles.blob4} width={width} height={height + 68} />
|
||||
</div>
|
||||
<div style={{ marginTop: `${height + 76}px` }} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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 (
|
||||
<div className={styles.container}>
|
||||
<Typography as="h3" size="lg" weight="semiBold" className={styles.title}>
|
||||
{t("secret-discount-table_title")}
|
||||
</Typography>
|
||||
<Typography as="p" className={styles.subtitle}>
|
||||
{t("secret-discount-table_subtitle")}
|
||||
</Typography>
|
||||
<div className={styles.applied}>
|
||||
<Image
|
||||
src={secretDiscountImages("gift.png")}
|
||||
alt="Gift"
|
||||
width={17}
|
||||
height={17}
|
||||
/>
|
||||
<Typography
|
||||
as="h4"
|
||||
weight="medium"
|
||||
color="white"
|
||||
className={styles.title}
|
||||
>
|
||||
{t("secret-discount-table_discount-applied")}
|
||||
</Typography>
|
||||
<Typography className={styles.oldDiscount}>
|
||||
{formatDiscount(oldDiscount)}
|
||||
</Typography>
|
||||
<Typography
|
||||
color="white"
|
||||
size="lg"
|
||||
weight="semiBold"
|
||||
className={styles.newDiscount}
|
||||
>
|
||||
{formatDiscount(newDiscount)}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={`${styles.gridLine} ${styles.afterTrial}`}>
|
||||
<Typography as="p" size="xs" align="left">
|
||||
{t("secret-discount-table_cost-after-trial", {
|
||||
trialPeriod: await getPeriodTextServer(trialPeriod, trialInterval),
|
||||
})}
|
||||
</Typography>
|
||||
<Typography
|
||||
className={clsx(styles.lineText, styles.oldPrice)}
|
||||
size="xs"
|
||||
>
|
||||
{getFormattedPrice(oldPrice, currency, 0)}
|
||||
</Typography>
|
||||
<Typography
|
||||
className={clsx(styles.lineText, styles.newPrice)}
|
||||
size="xs"
|
||||
>
|
||||
{getFormattedPrice(trialPrice, currency, 0)}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={`${styles.gridLine} ${styles["save"]}`}>
|
||||
<Typography as="p" size="xs" align="left">
|
||||
{t("secret-discount-table_you-save", {
|
||||
amount: getFormattedPrice(oldPrice - trialPrice, currency, 0),
|
||||
})}
|
||||
</Typography>
|
||||
</div>
|
||||
<hr className={styles.hr} />
|
||||
<div className={`${styles.gridLine} ${styles.totalToday}`}>
|
||||
<Typography
|
||||
as="p"
|
||||
weight="bold"
|
||||
align="left"
|
||||
className={styles.totalTodayText}
|
||||
>
|
||||
{t("secret-discount-table_total-today")}
|
||||
</Typography>
|
||||
<Typography weight="bold" className={styles.totalTodayText}>
|
||||
{getFormattedPrice(trialPrice, currency, 0)}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -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";
|
||||
@ -47,3 +47,10 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.backButton.backButton {
|
||||
width: fit-content;
|
||||
padding: 0;
|
||||
padding-right: 16px;
|
||||
background: none;
|
||||
}
|
||||
|
||||
@ -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 (
|
||||
<header className={clsx(styles.header, className)}>
|
||||
<Button className={styles.menuButton} onClick={open}>
|
||||
<Icon name={IconName.Menu} />
|
||||
</Button>
|
||||
<div>
|
||||
{isVisibleBackButton && (
|
||||
<Button className={styles.backButton} onClick={handleBack}>
|
||||
<Icon
|
||||
name={IconName.ChevronLeft}
|
||||
size={{ height: 22, width: 22 }}
|
||||
color="#374151"
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
{isVisibleMenuButton && (
|
||||
<Button className={styles.menuButton} onClick={open}>
|
||||
<Icon name={IconName.Menu} />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<Link href={ROUTES.home()}>
|
||||
<Logo />
|
||||
</Link>
|
||||
<div>
|
||||
<Link href={ROUTES.chat()}>
|
||||
<Icon
|
||||
name={IconName.Notification}
|
||||
className={styles.notificationIcon}
|
||||
>
|
||||
<Badge className={styles.badge}>
|
||||
<Typography
|
||||
weight="semiBold"
|
||||
size="xs"
|
||||
color="white"
|
||||
className={styles.badgeContent}
|
||||
>
|
||||
{totalUnreadCount > 99 ? "99+" : totalUnreadCount}
|
||||
</Typography>
|
||||
</Badge>
|
||||
</Icon>
|
||||
{isVisibleNotificationIcon && (
|
||||
<Icon
|
||||
name={IconName.Notification}
|
||||
className={styles.notificationIcon}
|
||||
>
|
||||
<Badge className={styles.badge}>
|
||||
<Typography
|
||||
weight="semiBold"
|
||||
size="xs"
|
||||
color="white"
|
||||
className={styles.badgeContent}
|
||||
>
|
||||
{totalUnreadCount > 99 ? "99+" : totalUnreadCount}
|
||||
</Typography>
|
||||
</Badge>
|
||||
</Icon>
|
||||
)}
|
||||
</Link>
|
||||
<Icon name={IconName.Search} />
|
||||
{isVisibleSearchIcon && <Icon name={IconName.Search} />}
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
|
||||
@ -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 (
|
||||
<Component
|
||||
@ -66,6 +68,7 @@ export default function Typography({
|
||||
)}
|
||||
style={{
|
||||
textAlign: align,
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@ -8,5 +8,6 @@ export const getMe = async (): Promise<IMeResponse> => {
|
||||
tags: ["user", "me"],
|
||||
schema: MeResponseSchema,
|
||||
revalidate: 0,
|
||||
skipAuthRedirect: true,
|
||||
});
|
||||
};
|
||||
|
||||
23
src/hooks/translations/usePeriod.ts
Normal file
23
src/hooks/translations/usePeriod.ts
Normal file
@ -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]);
|
||||
};
|
||||
@ -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]),
|
||||
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
export * from "./email-marketing";
|
||||
export * from "./retaining";
|
||||
export * from "./secret-discount";
|
||||
|
||||
2
src/shared/constants/images/secret-discount.ts
Normal file
2
src/shared/constants/images/secret-discount.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export const secretDiscountImages = (path: string) =>
|
||||
`/secret-discount/${path}`;
|
||||
19
src/shared/utils/funnel.ts
Normal file
19
src/shared/utils/funnel.ts
Normal file
@ -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 };
|
||||
13
src/shared/utils/period-server.ts
Normal file
13
src/shared/utils/period-server.ts
Normal file
@ -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<string> => {
|
||||
const t = await getTranslations(periodKeyVariant);
|
||||
|
||||
return t(periodType.toLowerCase(), { count });
|
||||
};
|
||||
@ -81,5 +81,3 @@ export enum ELocalesPlacement {
|
||||
Profile = "profile",
|
||||
RetainingFunnel = "retaining-funnel",
|
||||
}
|
||||
|
||||
export type PeriodType = "DAY" | "WEEK" | "MONTH" | "YEAR";
|
||||
|
||||
6
src/types/period.ts
Normal file
6
src/types/period.ts
Normal file
@ -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";
|
||||
Loading…
Reference in New Issue
Block a user