AW-493-additional-purchases
navigation
This commit is contained in:
parent
f8e5f52139
commit
5bf6ea7cff
@ -1,6 +0,0 @@
|
|||||||
.loading {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
height: 100dvh;
|
|
||||||
}
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
import { Spinner } from "@/components/ui";
|
|
||||||
|
|
||||||
import styles from "./loading.module.scss";
|
|
||||||
|
|
||||||
export default function AddConsultantLoading() {
|
|
||||||
return (
|
|
||||||
<div className={styles.loading}>
|
|
||||||
<Spinner />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,53 +0,0 @@
|
|||||||
import { Suspense } from "react";
|
|
||||||
import { useTranslations } from "next-intl";
|
|
||||||
|
|
||||||
import {
|
|
||||||
AddConsultantButton,
|
|
||||||
Caution,
|
|
||||||
ConsultationTable,
|
|
||||||
ConsultationTableSkeleton,
|
|
||||||
} from "@/components/domains/additional-purchases";
|
|
||||||
import { Card, Typography } from "@/components/ui";
|
|
||||||
import {
|
|
||||||
loadFunnelProducts,
|
|
||||||
loadFunnelProperties,
|
|
||||||
} from "@/entities/session/funnel/loaders";
|
|
||||||
import { ELocalesPlacement } from "@/types";
|
|
||||||
|
|
||||||
import styles from "./page.module.scss";
|
|
||||||
|
|
||||||
const payload = {
|
|
||||||
funnel: ELocalesPlacement.CompatibilityV2,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function AddConsultant() {
|
|
||||||
const t = useTranslations("AdditionalPurchases.add-consultant");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Caution />
|
|
||||||
<Typography as="h2" size="xl" weight="semiBold" className={styles.title}>
|
|
||||||
{t("title")}
|
|
||||||
</Typography>
|
|
||||||
<Typography
|
|
||||||
as="p"
|
|
||||||
size="sm"
|
|
||||||
color="black"
|
|
||||||
className={styles.exclusiveOffer}
|
|
||||||
>
|
|
||||||
{t("exclusive_offer")}
|
|
||||||
</Typography>
|
|
||||||
<Suspense fallback={<ConsultationTableSkeleton />}>
|
|
||||||
<Card className={styles.consultationTable}>
|
|
||||||
<ConsultationTable
|
|
||||||
products={loadFunnelProducts(payload, "add_consultant")}
|
|
||||||
properties={loadFunnelProperties(payload, "add_consultant")}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</Suspense>
|
|
||||||
<AddConsultantButton
|
|
||||||
products={loadFunnelProducts(payload, "add_consultant")}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
|
import { MultiPageNavigationProvider } from "@/components/domains/additional-purchases";
|
||||||
|
import { loadFunnelPaymentById } from "@/entities/session/funnel/loaders";
|
||||||
|
import { ROUTES } from "@/shared/constants/client-routes";
|
||||||
|
import { ELocalesPlacement } from "@/types";
|
||||||
|
|
||||||
|
interface LayoutProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
params: Promise<{ pageType: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
funnel: ELocalesPlacement.CompatibilityV2,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function MultiPageLayout({
|
||||||
|
children,
|
||||||
|
params,
|
||||||
|
}: LayoutProps) {
|
||||||
|
const { pageType } = await params;
|
||||||
|
const pages = await loadFunnelPaymentById(payload, "additionalProducts");
|
||||||
|
|
||||||
|
const allProducts = Array.isArray(pages) ? pages : pages ? [pages] : [];
|
||||||
|
const currentProduct = allProducts.find(page => page.type === pageType);
|
||||||
|
|
||||||
|
if (!currentProduct) {
|
||||||
|
return redirect(ROUTES.home());
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MultiPageNavigationProvider data={allProducts} currentType={pageType}>
|
||||||
|
{children}
|
||||||
|
</MultiPageNavigationProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
|
import {
|
||||||
|
AddConsultantPage,
|
||||||
|
AddGuidesPage,
|
||||||
|
} from "@/components/domains/additional-purchases";
|
||||||
|
import { ROUTES } from "@/shared/constants/client-routes";
|
||||||
|
|
||||||
|
interface AdditionalProductPageProps {
|
||||||
|
params: Promise<{ pageType: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function AdditionalProductPage({
|
||||||
|
params,
|
||||||
|
}: AdditionalProductPageProps) {
|
||||||
|
const { pageType } = await params;
|
||||||
|
|
||||||
|
switch (pageType) {
|
||||||
|
case "add_consultant":
|
||||||
|
return <AddConsultantPage />;
|
||||||
|
case "add_guides":
|
||||||
|
return <AddGuidesPage />;
|
||||||
|
default:
|
||||||
|
return redirect(ROUTES.home());
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/app/[locale]/(additional-purchases)/ap/page.tsx
Normal file
19
src/app/[locale]/(additional-purchases)/ap/page.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
|
import { loadFunnelPaymentById } from "@/entities/session/funnel/loaders";
|
||||||
|
import { ROUTES } from "@/shared/constants/client-routes";
|
||||||
|
import { ELocalesPlacement } from "@/types";
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
funnel: ELocalesPlacement.CompatibilityV2,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function AdditionalPurchasesPage() {
|
||||||
|
const pages = await loadFunnelPaymentById(payload, "additionalProducts");
|
||||||
|
|
||||||
|
if (!pages || !Array.isArray(pages) || pages.length === 0) {
|
||||||
|
return redirect(ROUTES.home());
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect(ROUTES.additionalPurchases(pages[0].type));
|
||||||
|
}
|
||||||
@ -1,5 +1,4 @@
|
|||||||
import { DrawerProvider, Header } from "@/components/layout";
|
import { DrawerProvider, Header, NavigationBar } from "@/components/layout";
|
||||||
import NavigationBar from "@/components/layout/NavigationBar/NavigationBar";
|
|
||||||
|
|
||||||
import styles from "./layout.module.scss";
|
import styles from "./layout.module.scss";
|
||||||
|
|
||||||
|
|||||||
11
src/app/[locale]/(payment)/layout.module.scss
Normal file
11
src/app/[locale]/(payment)/layout.module.scss
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
.main {
|
||||||
|
padding: 16px;
|
||||||
|
padding-bottom: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navBar {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 7777;
|
||||||
|
background: var(--background);
|
||||||
|
}
|
||||||
16
src/app/[locale]/(payment)/layout.tsx
Normal file
16
src/app/[locale]/(payment)/layout.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { DrawerProvider, Header } from "@/components/layout";
|
||||||
|
|
||||||
|
import styles from "./layout.module.scss";
|
||||||
|
|
||||||
|
export default function CoreLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) {
|
||||||
|
return (
|
||||||
|
<DrawerProvider>
|
||||||
|
<Header className={styles.navBar} />
|
||||||
|
<main className={styles.main}>{children}</main>
|
||||||
|
</DrawerProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -24,8 +24,8 @@ export default function Metrics({
|
|||||||
|
|
||||||
const [isButtonVisible, setIsButtonVisible] = useState(false);
|
const [isButtonVisible, setIsButtonVisible] = useState(false);
|
||||||
|
|
||||||
const navigateToHome = () => {
|
const handleNext = () => {
|
||||||
window.location.href = ROUTES.home();
|
window.location.href = ROUTES.additionalPurchases();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Yandex Metrica
|
// Yandex Metrica
|
||||||
@ -186,7 +186,7 @@ fbq('track', 'Purchase', { value: ${productPrice}, currency: "${currency}" });`}
|
|||||||
))}
|
))}
|
||||||
|
|
||||||
{isButtonVisible && (
|
{isButtonVisible && (
|
||||||
<Button onClick={navigateToHome} className={styles.button}>
|
<Button onClick={handleNext} className={styles.button}>
|
||||||
<Typography color="white">{t("button")}</Typography>
|
<Typography color="white">{t("button")}</Typography>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
@ -1,34 +1,27 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { use } from "react";
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
import { Button, Spinner, Typography } from "@/components/ui";
|
import { Button, Spinner, Typography } from "@/components/ui";
|
||||||
import { BlurComponent } from "@/components/widgets";
|
import { BlurComponent } from "@/components/widgets";
|
||||||
import { IFunnelPaymentVariant } from "@/entities/session/funnel/types";
|
|
||||||
import { useSingleCheckout } from "@/hooks/payment/useSingleCheckout";
|
import { useSingleCheckout } from "@/hooks/payment/useSingleCheckout";
|
||||||
import { useToast } from "@/providers/toast-provider";
|
import { useToast } from "@/providers/toast-provider";
|
||||||
import { ROUTES } from "@/shared/constants/client-routes";
|
|
||||||
|
|
||||||
import styles from "./AddConsultantButton.module.scss";
|
import styles from "./AddConsultantButton.module.scss";
|
||||||
|
|
||||||
interface AddConsultantButtonProps {
|
import { useMultiPageNavigationContext } from "..";
|
||||||
products: Promise<IFunnelPaymentVariant[]>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AddConsultantButton({
|
export default function AddConsultantButton() {
|
||||||
products,
|
|
||||||
}: AddConsultantButtonProps) {
|
|
||||||
const router = useRouter();
|
|
||||||
const t = useTranslations("AdditionalPurchases.add-consultant");
|
const t = useTranslations("AdditionalPurchases.add-consultant");
|
||||||
const { addToast } = useToast();
|
const { addToast } = useToast();
|
||||||
|
const { navigation } = useMultiPageNavigationContext();
|
||||||
|
const data = navigation.currentItem;
|
||||||
|
|
||||||
const product = use(products)?.[0];
|
const product = data?.variants?.[0];
|
||||||
|
|
||||||
const { handleSingleCheckout, isLoading } = useSingleCheckout({
|
const { handleSingleCheckout, isLoading } = useSingleCheckout({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
router.push(ROUTES.addGuides());
|
navigation.goToNext();
|
||||||
},
|
},
|
||||||
onError: _error => {
|
onError: _error => {
|
||||||
addToast({
|
addToast({
|
||||||
@ -56,7 +49,7 @@ export default function AddConsultantButton({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSkipOffer = () => {
|
const handleSkipOffer = () => {
|
||||||
router.push(ROUTES.addGuides());
|
navigation.goToNext();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -0,0 +1,35 @@
|
|||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
import {
|
||||||
|
AddConsultantButton,
|
||||||
|
Caution,
|
||||||
|
ConsultationTable,
|
||||||
|
} from "@/components/domains/additional-purchases";
|
||||||
|
import { Card, Typography } from "@/components/ui";
|
||||||
|
|
||||||
|
import styles from "./AddConsultantPage.module.scss";
|
||||||
|
|
||||||
|
export default function AddConsultantPage() {
|
||||||
|
const t = useTranslations("AdditionalPurchases.add-consultant");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Caution />
|
||||||
|
<Typography as="h2" size="xl" weight="semiBold" className={styles.title}>
|
||||||
|
{t("title")}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
as="p"
|
||||||
|
size="sm"
|
||||||
|
color="black"
|
||||||
|
className={styles.exclusiveOffer}
|
||||||
|
>
|
||||||
|
{t("exclusive_offer")}
|
||||||
|
</Typography>
|
||||||
|
<Card className={styles.consultationTable}>
|
||||||
|
<ConsultationTable />
|
||||||
|
</Card>
|
||||||
|
<AddConsultantButton />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,27 +1,27 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
import { Button, Spinner, Typography } from "@/components/ui";
|
import { Button, Spinner, Typography } from "@/components/ui";
|
||||||
import { BlurComponent } from "@/components/widgets";
|
import { BlurComponent } from "@/components/widgets";
|
||||||
import { useSingleCheckout } from "@/hooks/payment/useSingleCheckout";
|
import { useSingleCheckout } from "@/hooks/payment/useSingleCheckout";
|
||||||
import { useToast } from "@/providers/toast-provider";
|
import { useToast } from "@/providers/toast-provider";
|
||||||
import { ROUTES } from "@/shared/constants/client-routes";
|
|
||||||
|
|
||||||
import { useProductSelection } from "../ProductSelectionContext";
|
import { useProductSelection } from "../ProductSelectionProvider";
|
||||||
|
|
||||||
import styles from "./AddGuidesButton.module.scss";
|
import styles from "./AddGuidesButton.module.scss";
|
||||||
|
|
||||||
|
import { useMultiPageNavigationContext } from "..";
|
||||||
|
|
||||||
export default function AddGuidesButton() {
|
export default function AddGuidesButton() {
|
||||||
const t = useTranslations("AdditionalPurchases.add-guides");
|
const t = useTranslations("AdditionalPurchases.add-guides");
|
||||||
const router = useRouter();
|
|
||||||
const { addToast } = useToast();
|
const { addToast } = useToast();
|
||||||
const { selectedProduct } = useProductSelection();
|
const { selectedProduct } = useProductSelection();
|
||||||
|
const { navigation } = useMultiPageNavigationContext();
|
||||||
|
|
||||||
const { handleSingleCheckout, isLoading } = useSingleCheckout({
|
const { handleSingleCheckout, isLoading } = useSingleCheckout({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
router.push(ROUTES.home());
|
navigation.goToNext();
|
||||||
},
|
},
|
||||||
onError: _error => {
|
onError: _error => {
|
||||||
addToast({
|
addToast({
|
||||||
@ -49,7 +49,7 @@ export default function AddGuidesButton() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSkipOffer = () => {
|
const handleSkipOffer = () => {
|
||||||
router.push(ROUTES.home());
|
navigation.goToNext();
|
||||||
};
|
};
|
||||||
|
|
||||||
const isSkipOffer = selectedProduct?.id === "main_skip_offer";
|
const isSkipOffer = selectedProduct?.id === "main_skip_offer";
|
||||||
|
|||||||
@ -9,16 +9,10 @@ import {
|
|||||||
ProductSelectionProvider,
|
ProductSelectionProvider,
|
||||||
} from "@/components/domains/additional-purchases";
|
} from "@/components/domains/additional-purchases";
|
||||||
import { Typography } from "@/components/ui";
|
import { Typography } from "@/components/ui";
|
||||||
import { loadFunnelProducts } from "@/entities/session/funnel/loaders";
|
|
||||||
import { ELocalesPlacement } from "@/types";
|
|
||||||
|
|
||||||
import styles from "./page.module.scss";
|
import styles from "./AddGuidesPage.module.scss";
|
||||||
|
|
||||||
const payload = {
|
export default function AddGuidesPage() {
|
||||||
funnel: ELocalesPlacement.CompatibilityV2,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function AddGuides() {
|
|
||||||
const t = useTranslations("AdditionalPurchases.add-guides");
|
const t = useTranslations("AdditionalPurchases.add-guides");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -31,7 +25,7 @@ export default function AddGuides() {
|
|||||||
{t("subtitle")}
|
{t("subtitle")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Suspense fallback={<OffersSkeleton />}>
|
<Suspense fallback={<OffersSkeleton />}>
|
||||||
<Offers products={loadFunnelProducts(payload, "add_guides")} />
|
<Offers />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
<Typography align="left" color="secondary" className={styles.description}>
|
<Typography align="left" color="secondary" className={styles.description}>
|
||||||
{t("description")}
|
{t("description")}
|
||||||
@ -1,31 +1,26 @@
|
|||||||
import Image from "next/image";
|
"use client";
|
||||||
import { getTranslations } from "next-intl/server";
|
|
||||||
|
|
||||||
import { Skeleton, Typography } from "@/components/ui";
|
import Image from "next/image";
|
||||||
import {
|
import { useTranslations } from "next-intl";
|
||||||
IFunnelPaymentProperty,
|
|
||||||
IFunnelPaymentVariant,
|
import { Typography } from "@/components/ui";
|
||||||
} from "@/entities/session/funnel/types";
|
|
||||||
import { getFormattedPrice } from "@/shared/utils/price";
|
import { getFormattedPrice } from "@/shared/utils/price";
|
||||||
import { Currency } from "@/types";
|
import { Currency } from "@/types";
|
||||||
|
|
||||||
import styles from "./ConsultationTable.module.scss";
|
import styles from "./ConsultationTable.module.scss";
|
||||||
|
|
||||||
interface ConsultationTableProps {
|
import { useMultiPageNavigationContext } from "..";
|
||||||
products: Promise<IFunnelPaymentVariant[]>;
|
|
||||||
properties: Promise<IFunnelPaymentProperty[]>;
|
export default function ConsultationTable() {
|
||||||
}
|
const t = useTranslations("AdditionalPurchases.add-consultant");
|
||||||
|
const { navigation } = useMultiPageNavigationContext();
|
||||||
|
const data = navigation.currentItem;
|
||||||
|
|
||||||
export default async function ConsultationTable({
|
|
||||||
products,
|
|
||||||
properties,
|
|
||||||
}: ConsultationTableProps) {
|
|
||||||
const t = await getTranslations("AdditionalPurchases.add-consultant");
|
|
||||||
const currency = Currency.USD;
|
const currency = Currency.USD;
|
||||||
|
|
||||||
const product = (await products)?.[0];
|
const product = data?.variants?.[0];
|
||||||
const discount =
|
const discount =
|
||||||
(await properties)?.find(p => p.key === "discount")?.value ?? 0;
|
data?.properties?.find(p => p.key === "discount")?.value ?? 0;
|
||||||
|
|
||||||
const price = getFormattedPrice(product?.price ?? 0, currency);
|
const price = getFormattedPrice(product?.price ?? 0, currency);
|
||||||
const oldPrice = getFormattedPrice(
|
const oldPrice = getFormattedPrice(
|
||||||
@ -109,7 +104,3 @@ export default async function ConsultationTable({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ConsultationTableSkeleton() {
|
|
||||||
return <Skeleton style={{ height: "300px", marginTop: "24px" }} />;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -0,0 +1,60 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { createContext, ReactNode, useContext } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
|
import { IFunnelPaymentPlacement } from "@/entities/session/funnel/types";
|
||||||
|
import { useMultiPageNavigation } from "@/hooks/multiPages/useMultiPageNavigation";
|
||||||
|
import { ROUTES } from "@/shared/constants/client-routes";
|
||||||
|
|
||||||
|
interface MultiPageNavigationContextType {
|
||||||
|
navigation: ReturnType<
|
||||||
|
typeof useMultiPageNavigation<IFunnelPaymentPlacement>
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MultiPageNavigationContext = createContext<
|
||||||
|
MultiPageNavigationContextType | undefined
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
|
interface MultiPageNavigationProviderProps {
|
||||||
|
children: ReactNode;
|
||||||
|
data: IFunnelPaymentPlacement[];
|
||||||
|
currentType: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MultiPageNavigationProvider({
|
||||||
|
children,
|
||||||
|
data,
|
||||||
|
currentType,
|
||||||
|
}: MultiPageNavigationProviderProps) {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const navigation = useMultiPageNavigation<IFunnelPaymentPlacement>({
|
||||||
|
data,
|
||||||
|
currentType,
|
||||||
|
getTypeFromItem: item => item.type ?? "",
|
||||||
|
navigateToItemByType: type => {
|
||||||
|
router.push(ROUTES.additionalPurchases(type));
|
||||||
|
},
|
||||||
|
onComplete: () => {
|
||||||
|
router.push(ROUTES.home());
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MultiPageNavigationContext.Provider value={{ navigation }}>
|
||||||
|
{children}
|
||||||
|
</MultiPageNavigationContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMultiPageNavigationContext() {
|
||||||
|
const context = useContext(MultiPageNavigationContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(
|
||||||
|
"useMultiPageNavigationContext must be used within MultiPageNavigationProvider"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
@ -1,22 +1,21 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { use, useEffect, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
import { Skeleton } from "@/components/ui";
|
import { Skeleton } from "@/components/ui";
|
||||||
import { IFunnelPaymentVariant } from "@/entities/session/funnel/types";
|
import { IFunnelPaymentVariant } from "@/entities/session/funnel/types";
|
||||||
|
|
||||||
import { useProductSelection } from "../ProductSelectionContext";
|
import { useProductSelection } from "../ProductSelectionProvider";
|
||||||
|
|
||||||
import styles from "./Offers.module.scss";
|
import styles from "./Offers.module.scss";
|
||||||
|
|
||||||
import { Offer } from "..";
|
import { Offer, useMultiPageNavigationContext } from "..";
|
||||||
|
|
||||||
interface OffersProps {
|
export default function Offers() {
|
||||||
products: Promise<IFunnelPaymentVariant[]>;
|
const { navigation } = useMultiPageNavigationContext();
|
||||||
}
|
const data = navigation.currentItem;
|
||||||
|
|
||||||
export default function Offers({ products }: OffersProps) {
|
const offers = useMemo(() => data?.variants ?? [], [data]);
|
||||||
const offers = use(products);
|
|
||||||
const [allOffers, setAllOffers] = useState<IFunnelPaymentVariant[]>([]);
|
const [allOffers, setAllOffers] = useState<IFunnelPaymentVariant[]>([]);
|
||||||
const [activeOffer, setActiveOffer] = useState<string>("");
|
const [activeOffer, setActiveOffer] = useState<string>("");
|
||||||
const { setSelectedProduct } = useProductSelection();
|
const { setSelectedProduct } = useProductSelection();
|
||||||
|
|||||||
@ -1,13 +1,16 @@
|
|||||||
export { default as AddConsultantButton } from "./AddConsultantButton/AddConsultantButton";
|
export { default as AddConsultantButton } from "./AddConsultantButton/AddConsultantButton";
|
||||||
|
export { default as AddConsultantPage } from "./AddConsultantPage/AddConsultantPage";
|
||||||
export { default as AddGuidesButton } from "./AddGuidesButton/AddGuidesButton";
|
export { default as AddGuidesButton } from "./AddGuidesButton/AddGuidesButton";
|
||||||
|
export { default as AddGuidesPage } from "./AddGuidesPage/AddGuidesPage";
|
||||||
export { default as Caution } from "./Caution/Caution";
|
export { default as Caution } from "./Caution/Caution";
|
||||||
|
export { default as ConsultationTable } from "./ConsultationTable/ConsultationTable";
|
||||||
export {
|
export {
|
||||||
default as ConsultationTable,
|
MultiPageNavigationProvider,
|
||||||
ConsultationTableSkeleton,
|
useMultiPageNavigationContext,
|
||||||
} from "./ConsultationTable/ConsultationTable";
|
} from "./MultiPageNavigationProvider";
|
||||||
export { default as Offer } from "./Offer/Offer";
|
export { default as Offer } from "./Offer/Offer";
|
||||||
export { default as Offers, OffersSkeleton } from "./Offers/Offers";
|
export { default as Offers, OffersSkeleton } from "./Offers/Offers";
|
||||||
export {
|
export {
|
||||||
ProductSelectionProvider,
|
ProductSelectionProvider,
|
||||||
useProductSelection,
|
useProductSelection,
|
||||||
} from "./ProductSelectionContext";
|
} from "./ProductSelectionProvider";
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
export { DrawerProvider, useDrawer } from "./Drawer/DrawerContext";
|
export { DrawerProvider, useDrawer } from "./Drawer/DrawerContext";
|
||||||
export { default as Header } from "./Header/Header";
|
export { default as Header } from "./Header/Header";
|
||||||
export { default as Logo } from "./Logo/Logo";
|
export { default as Logo } from "./Logo/Logo";
|
||||||
|
export { default as NavigationBar } from "./NavigationBar/NavigationBar";
|
||||||
export { default as StepperBar } from "./StepperBar/StepperBar";
|
export { default as StepperBar } from "./StepperBar/StepperBar";
|
||||||
|
|||||||
@ -30,12 +30,12 @@ export const loadFunnelPaymentById = cache(
|
|||||||
loadFunnelData(payload).then(d => d.payment[paymentId])
|
loadFunnelData(payload).then(d => d.payment[paymentId])
|
||||||
);
|
);
|
||||||
|
|
||||||
export const loadFunnelProducts = cache(
|
// export const loadFunnelProducts = cache(
|
||||||
(payload: FunnelRequest, paymentId: string) =>
|
// (payload: FunnelRequest, paymentId: string) =>
|
||||||
loadFunnelPaymentById(payload, paymentId).then(d => d?.variants ?? [])
|
// loadFunnelPaymentById(payload, paymentId).then(d => d?.variants ?? [])
|
||||||
);
|
// );
|
||||||
|
|
||||||
export const loadFunnelProperties = cache(
|
// export const loadFunnelProperties = cache(
|
||||||
(payload: FunnelRequest, paymentId: string) =>
|
// (payload: FunnelRequest, paymentId: string) =>
|
||||||
loadFunnelPaymentById(payload, paymentId).then(d => d?.properties ?? [])
|
// loadFunnelPaymentById(payload, paymentId).then(d => d?.properties ?? [])
|
||||||
);
|
// );
|
||||||
|
|||||||
@ -34,13 +34,20 @@ export const FunnelPaymentPlacementSchema = z.object({
|
|||||||
properties: z.array(FunnelPaymentPropertySchema).optional(),
|
properties: z.array(FunnelPaymentPropertySchema).optional(),
|
||||||
variants: z.array(FunnelPaymentVariantSchema).optional(),
|
variants: z.array(FunnelPaymentVariantSchema).optional(),
|
||||||
paymentUrl: z.string().optional(),
|
paymentUrl: z.string().optional(),
|
||||||
|
type: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const FunnelSchema = z.object({
|
export const FunnelSchema = z.object({
|
||||||
currency: z.nativeEnum(Currency),
|
currency: z.nativeEnum(Currency),
|
||||||
funnel: z.nativeEnum(ELocalesPlacement),
|
funnel: z.nativeEnum(ELocalesPlacement),
|
||||||
locale: z.string(),
|
locale: z.string(),
|
||||||
payment: z.record(z.string(), FunnelPaymentPlacementSchema.nullable()),
|
payment: z.record(
|
||||||
|
z.string(),
|
||||||
|
z.union([
|
||||||
|
FunnelPaymentPlacementSchema.nullable(),
|
||||||
|
z.array(FunnelPaymentPlacementSchema),
|
||||||
|
])
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const FunnelResponseSchema = z.object({
|
export const FunnelResponseSchema = z.object({
|
||||||
|
|||||||
142
src/hooks/multiPages/useMultiPageNavigation.ts
Normal file
142
src/hooks/multiPages/useMultiPageNavigation.ts
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useCallback, useMemo } from "react";
|
||||||
|
|
||||||
|
interface PageNavigationOptions<T> {
|
||||||
|
data: T[];
|
||||||
|
currentType: string;
|
||||||
|
getTypeFromItem: (item: T) => string;
|
||||||
|
navigateToItemByType: (type: string) => void;
|
||||||
|
onBeforeNext?: (nextItem: T) => boolean | Promise<boolean>;
|
||||||
|
onBeforePrevious?: (prevItem: T) => boolean | Promise<boolean>;
|
||||||
|
onComplete: () => void;
|
||||||
|
onStart?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PageNavigationReturn<T> {
|
||||||
|
currentItem: T | undefined;
|
||||||
|
currentIndex: number;
|
||||||
|
isFirst: boolean;
|
||||||
|
isLast: boolean;
|
||||||
|
hasNext: boolean;
|
||||||
|
hasPrevious: boolean;
|
||||||
|
goToNext: () => Promise<void>;
|
||||||
|
goToPrevious: () => Promise<void>;
|
||||||
|
goToFirst: () => Promise<void>;
|
||||||
|
goToLast: () => Promise<void>;
|
||||||
|
goToIndex: (index: number) => Promise<void>;
|
||||||
|
totalPages: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMultiPageNavigation<T>({
|
||||||
|
data,
|
||||||
|
currentType,
|
||||||
|
getTypeFromItem,
|
||||||
|
navigateToItemByType,
|
||||||
|
onBeforeNext,
|
||||||
|
onBeforePrevious,
|
||||||
|
onComplete,
|
||||||
|
onStart,
|
||||||
|
}: PageNavigationOptions<T>): PageNavigationReturn<T> {
|
||||||
|
const currentIndex = useMemo(
|
||||||
|
() => data.findIndex(item => getTypeFromItem(item) === currentType),
|
||||||
|
[data, currentType, getTypeFromItem]
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentItem = useMemo(() => data[currentIndex], [data, currentIndex]);
|
||||||
|
|
||||||
|
const isFirst = currentIndex === 0;
|
||||||
|
const isLast = currentIndex === data.length - 1;
|
||||||
|
const hasNext = !isLast;
|
||||||
|
const hasPrevious = !isFirst;
|
||||||
|
const totalPages = data.length;
|
||||||
|
|
||||||
|
const navigateToItem = useCallback(
|
||||||
|
async (item: T) => {
|
||||||
|
const type = getTypeFromItem(item);
|
||||||
|
navigateToItemByType(type);
|
||||||
|
},
|
||||||
|
[navigateToItemByType, getTypeFromItem]
|
||||||
|
);
|
||||||
|
|
||||||
|
const goToNext = useCallback(async () => {
|
||||||
|
if (!hasNext) return onComplete();
|
||||||
|
|
||||||
|
const nextItem = data[currentIndex + 1];
|
||||||
|
|
||||||
|
if (onBeforeNext) {
|
||||||
|
const shouldProceed = await onBeforeNext(nextItem);
|
||||||
|
if (!shouldProceed) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await navigateToItem(nextItem);
|
||||||
|
}, [hasNext, data, currentIndex, onBeforeNext, onComplete, navigateToItem]);
|
||||||
|
|
||||||
|
const goToPrevious = useCallback(async () => {
|
||||||
|
if (!hasPrevious) return;
|
||||||
|
|
||||||
|
const prevItem = data[currentIndex - 1];
|
||||||
|
|
||||||
|
if (onBeforePrevious) {
|
||||||
|
const shouldProceed = await onBeforePrevious(prevItem);
|
||||||
|
if (!shouldProceed) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await navigateToItem(prevItem);
|
||||||
|
}, [hasPrevious, data, currentIndex, onBeforePrevious, navigateToItem]);
|
||||||
|
|
||||||
|
const goToFirst = useCallback(async () => {
|
||||||
|
if (isFirst) return;
|
||||||
|
|
||||||
|
if (onStart) {
|
||||||
|
onStart();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await navigateToItem(data[0]);
|
||||||
|
}, [isFirst, onStart, navigateToItem, data]);
|
||||||
|
|
||||||
|
const goToLast = useCallback(async () => {
|
||||||
|
if (isLast) return;
|
||||||
|
await navigateToItem(data[data.length - 1]);
|
||||||
|
}, [isLast, navigateToItem, data]);
|
||||||
|
|
||||||
|
const goToIndex = useCallback(
|
||||||
|
async (index: number) => {
|
||||||
|
if (index < 0 || index >= data.length || index === currentIndex) return;
|
||||||
|
await navigateToItem(data[index]);
|
||||||
|
},
|
||||||
|
[data, currentIndex, navigateToItem]
|
||||||
|
);
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => ({
|
||||||
|
currentItem,
|
||||||
|
currentIndex,
|
||||||
|
isFirst,
|
||||||
|
isLast,
|
||||||
|
hasNext,
|
||||||
|
hasPrevious,
|
||||||
|
goToNext,
|
||||||
|
goToPrevious,
|
||||||
|
goToFirst,
|
||||||
|
goToLast,
|
||||||
|
goToIndex,
|
||||||
|
totalPages,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
currentItem,
|
||||||
|
currentIndex,
|
||||||
|
isFirst,
|
||||||
|
isLast,
|
||||||
|
hasNext,
|
||||||
|
hasPrevious,
|
||||||
|
goToNext,
|
||||||
|
goToPrevious,
|
||||||
|
goToFirst,
|
||||||
|
goToLast,
|
||||||
|
goToIndex,
|
||||||
|
totalPages,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -66,6 +66,7 @@ export const ROUTES = {
|
|||||||
// Additional Purchases
|
// Additional Purchases
|
||||||
addConsultant: () => createRoute(["add-consultant"]),
|
addConsultant: () => createRoute(["add-consultant"]),
|
||||||
addGuides: () => createRoute(["add-guides"]),
|
addGuides: () => createRoute(["add-guides"]),
|
||||||
|
additionalPurchases: (type?: string) => createRoute(["ap", type]),
|
||||||
|
|
||||||
// // Compatibility
|
// // Compatibility
|
||||||
// compatibilities: () => createRoute(["compatibilities"]),
|
// compatibilities: () => createRoute(["compatibilities"]),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user