From 5bf6ea7cff54b5967b9d44a5752f9f06204c6cc3 Mon Sep 17 00:00:00 2001 From: gofnnp Date: Wed, 9 Jul 2025 14:56:20 +0400 Subject: [PATCH] AW-493-additional-purchases navigation --- .../add-consultant/loading.module.scss | 6 - .../add-consultant/loading.tsx | 11 -- .../add-consultant/page.tsx | 53 ------- .../ap/[pageType]/layout.tsx | 36 +++++ .../ap/[pageType]/page.tsx | 26 ++++ .../(additional-purchases)/ap/page.tsx | 19 +++ src/app/[locale]/(core)/layout.tsx | 3 +- src/app/[locale]/(payment)/layout.module.scss | 11 ++ src/app/[locale]/(payment)/layout.tsx | 16 ++ .../payment/failed/page.tsx | 0 .../{(core) => (payment)}/payment/route.ts | 0 .../payment/success/Metrics.module.scss | 0 .../payment/success/Metrics.tsx | 6 +- .../payment/success/page.tsx | 0 .../AddConsultantButton.tsx | 21 +-- .../AddConsultantPage.module.scss} | 0 .../AddConsultantPage/AddConsultantPage.tsx | 35 +++++ .../AddGuidesButton/AddGuidesButton.tsx | 12 +- .../AddGuidesPage/AddGuidesPage.module.scss} | 0 .../AddGuidesPage/AddGuidesPage.tsx} | 12 +- .../ConsultationTable/ConsultationTable.tsx | 35 ++--- .../MultiPageNavigationProvider.tsx | 60 ++++++++ .../additional-purchases/Offers/Offers.tsx | 15 +- ...ntext.tsx => ProductSelectionProvider.tsx} | 0 .../domains/additional-purchases/index.ts | 11 +- src/components/layout/index.ts | 1 + src/entities/session/funnel/loaders.ts | 16 +- src/entities/session/funnel/types.ts | 9 +- .../multiPages/useMultiPageNavigation.ts | 142 ++++++++++++++++++ src/shared/constants/client-routes.ts | 1 + 30 files changed, 410 insertions(+), 147 deletions(-) delete mode 100644 src/app/[locale]/(additional-purchases)/add-consultant/loading.module.scss delete mode 100644 src/app/[locale]/(additional-purchases)/add-consultant/loading.tsx delete mode 100644 src/app/[locale]/(additional-purchases)/add-consultant/page.tsx create mode 100644 src/app/[locale]/(additional-purchases)/ap/[pageType]/layout.tsx create mode 100644 src/app/[locale]/(additional-purchases)/ap/[pageType]/page.tsx create mode 100644 src/app/[locale]/(additional-purchases)/ap/page.tsx create mode 100644 src/app/[locale]/(payment)/layout.module.scss create mode 100644 src/app/[locale]/(payment)/layout.tsx rename src/app/[locale]/{(core) => (payment)}/payment/failed/page.tsx (100%) rename src/app/[locale]/{(core) => (payment)}/payment/route.ts (100%) rename src/app/[locale]/{(core) => (payment)}/payment/success/Metrics.module.scss (100%) rename src/app/[locale]/{(core) => (payment)}/payment/success/Metrics.tsx (97%) rename src/app/[locale]/{(core) => (payment)}/payment/success/page.tsx (100%) rename src/{app/[locale]/(additional-purchases)/add-consultant/page.module.scss => components/domains/additional-purchases/AddConsultantPage/AddConsultantPage.module.scss} (100%) create mode 100644 src/components/domains/additional-purchases/AddConsultantPage/AddConsultantPage.tsx rename src/{app/[locale]/(additional-purchases)/add-guides/page.module.scss => components/domains/additional-purchases/AddGuidesPage/AddGuidesPage.module.scss} (100%) rename src/{app/[locale]/(additional-purchases)/add-guides/page.tsx => components/domains/additional-purchases/AddGuidesPage/AddGuidesPage.tsx} (72%) create mode 100644 src/components/domains/additional-purchases/MultiPageNavigationProvider.tsx rename src/components/domains/additional-purchases/{ProductSelectionContext.tsx => ProductSelectionProvider.tsx} (100%) create mode 100644 src/hooks/multiPages/useMultiPageNavigation.ts diff --git a/src/app/[locale]/(additional-purchases)/add-consultant/loading.module.scss b/src/app/[locale]/(additional-purchases)/add-consultant/loading.module.scss deleted file mode 100644 index 9a4e274..0000000 --- a/src/app/[locale]/(additional-purchases)/add-consultant/loading.module.scss +++ /dev/null @@ -1,6 +0,0 @@ -.loading { - display: flex; - justify-content: center; - align-items: center; - height: 100dvh; -} diff --git a/src/app/[locale]/(additional-purchases)/add-consultant/loading.tsx b/src/app/[locale]/(additional-purchases)/add-consultant/loading.tsx deleted file mode 100644 index 0cd6c20..0000000 --- a/src/app/[locale]/(additional-purchases)/add-consultant/loading.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { Spinner } from "@/components/ui"; - -import styles from "./loading.module.scss"; - -export default function AddConsultantLoading() { - return ( -
- -
- ); -} diff --git a/src/app/[locale]/(additional-purchases)/add-consultant/page.tsx b/src/app/[locale]/(additional-purchases)/add-consultant/page.tsx deleted file mode 100644 index 91dadbc..0000000 --- a/src/app/[locale]/(additional-purchases)/add-consultant/page.tsx +++ /dev/null @@ -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 ( - <> - - - {t("title")} - - - {t("exclusive_offer")} - - }> - - - - - - - ); -} diff --git a/src/app/[locale]/(additional-purchases)/ap/[pageType]/layout.tsx b/src/app/[locale]/(additional-purchases)/ap/[pageType]/layout.tsx new file mode 100644 index 0000000..8a311ed --- /dev/null +++ b/src/app/[locale]/(additional-purchases)/ap/[pageType]/layout.tsx @@ -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 ( + + {children} + + ); +} diff --git a/src/app/[locale]/(additional-purchases)/ap/[pageType]/page.tsx b/src/app/[locale]/(additional-purchases)/ap/[pageType]/page.tsx new file mode 100644 index 0000000..9e7d979 --- /dev/null +++ b/src/app/[locale]/(additional-purchases)/ap/[pageType]/page.tsx @@ -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 ; + case "add_guides": + return ; + default: + return redirect(ROUTES.home()); + } +} diff --git a/src/app/[locale]/(additional-purchases)/ap/page.tsx b/src/app/[locale]/(additional-purchases)/ap/page.tsx new file mode 100644 index 0000000..20edb35 --- /dev/null +++ b/src/app/[locale]/(additional-purchases)/ap/page.tsx @@ -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)); +} diff --git a/src/app/[locale]/(core)/layout.tsx b/src/app/[locale]/(core)/layout.tsx index 2d4389c..1a066e9 100644 --- a/src/app/[locale]/(core)/layout.tsx +++ b/src/app/[locale]/(core)/layout.tsx @@ -1,5 +1,4 @@ -import { DrawerProvider, Header } from "@/components/layout"; -import NavigationBar from "@/components/layout/NavigationBar/NavigationBar"; +import { DrawerProvider, Header, NavigationBar } from "@/components/layout"; import styles from "./layout.module.scss"; diff --git a/src/app/[locale]/(payment)/layout.module.scss b/src/app/[locale]/(payment)/layout.module.scss new file mode 100644 index 0000000..3577e50 --- /dev/null +++ b/src/app/[locale]/(payment)/layout.module.scss @@ -0,0 +1,11 @@ +.main { + padding: 16px; + padding-bottom: 120px; +} + +.navBar { + position: sticky; + top: 0; + z-index: 7777; + background: var(--background); +} diff --git a/src/app/[locale]/(payment)/layout.tsx b/src/app/[locale]/(payment)/layout.tsx new file mode 100644 index 0000000..f8c99ba --- /dev/null +++ b/src/app/[locale]/(payment)/layout.tsx @@ -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 ( + +
+
{children}
+ + ); +} diff --git a/src/app/[locale]/(core)/payment/failed/page.tsx b/src/app/[locale]/(payment)/payment/failed/page.tsx similarity index 100% rename from src/app/[locale]/(core)/payment/failed/page.tsx rename to src/app/[locale]/(payment)/payment/failed/page.tsx diff --git a/src/app/[locale]/(core)/payment/route.ts b/src/app/[locale]/(payment)/payment/route.ts similarity index 100% rename from src/app/[locale]/(core)/payment/route.ts rename to src/app/[locale]/(payment)/payment/route.ts diff --git a/src/app/[locale]/(core)/payment/success/Metrics.module.scss b/src/app/[locale]/(payment)/payment/success/Metrics.module.scss similarity index 100% rename from src/app/[locale]/(core)/payment/success/Metrics.module.scss rename to src/app/[locale]/(payment)/payment/success/Metrics.module.scss diff --git a/src/app/[locale]/(core)/payment/success/Metrics.tsx b/src/app/[locale]/(payment)/payment/success/Metrics.tsx similarity index 97% rename from src/app/[locale]/(core)/payment/success/Metrics.tsx rename to src/app/[locale]/(payment)/payment/success/Metrics.tsx index e16fda8..eb12a63 100644 --- a/src/app/[locale]/(core)/payment/success/Metrics.tsx +++ b/src/app/[locale]/(payment)/payment/success/Metrics.tsx @@ -24,8 +24,8 @@ export default function Metrics({ const [isButtonVisible, setIsButtonVisible] = useState(false); - const navigateToHome = () => { - window.location.href = ROUTES.home(); + const handleNext = () => { + window.location.href = ROUTES.additionalPurchases(); }; // Yandex Metrica @@ -186,7 +186,7 @@ fbq('track', 'Purchase', { value: ${productPrice}, currency: "${currency}" });`} ))} {isButtonVisible && ( - )} diff --git a/src/app/[locale]/(core)/payment/success/page.tsx b/src/app/[locale]/(payment)/payment/success/page.tsx similarity index 100% rename from src/app/[locale]/(core)/payment/success/page.tsx rename to src/app/[locale]/(payment)/payment/success/page.tsx diff --git a/src/components/domains/additional-purchases/AddConsultantButton/AddConsultantButton.tsx b/src/components/domains/additional-purchases/AddConsultantButton/AddConsultantButton.tsx index cfa4659..b33607a 100644 --- a/src/components/domains/additional-purchases/AddConsultantButton/AddConsultantButton.tsx +++ b/src/components/domains/additional-purchases/AddConsultantButton/AddConsultantButton.tsx @@ -1,34 +1,27 @@ "use client"; -import { use } from "react"; -import { useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; import { Button, Spinner, Typography } from "@/components/ui"; import { BlurComponent } from "@/components/widgets"; -import { IFunnelPaymentVariant } from "@/entities/session/funnel/types"; import { useSingleCheckout } from "@/hooks/payment/useSingleCheckout"; import { useToast } from "@/providers/toast-provider"; -import { ROUTES } from "@/shared/constants/client-routes"; import styles from "./AddConsultantButton.module.scss"; -interface AddConsultantButtonProps { - products: Promise; -} +import { useMultiPageNavigationContext } from ".."; -export default function AddConsultantButton({ - products, -}: AddConsultantButtonProps) { - const router = useRouter(); +export default function AddConsultantButton() { const t = useTranslations("AdditionalPurchases.add-consultant"); const { addToast } = useToast(); + const { navigation } = useMultiPageNavigationContext(); + const data = navigation.currentItem; - const product = use(products)?.[0]; + const product = data?.variants?.[0]; const { handleSingleCheckout, isLoading } = useSingleCheckout({ onSuccess: () => { - router.push(ROUTES.addGuides()); + navigation.goToNext(); }, onError: _error => { addToast({ @@ -56,7 +49,7 @@ export default function AddConsultantButton({ }; const handleSkipOffer = () => { - router.push(ROUTES.addGuides()); + navigation.goToNext(); }; return ( diff --git a/src/app/[locale]/(additional-purchases)/add-consultant/page.module.scss b/src/components/domains/additional-purchases/AddConsultantPage/AddConsultantPage.module.scss similarity index 100% rename from src/app/[locale]/(additional-purchases)/add-consultant/page.module.scss rename to src/components/domains/additional-purchases/AddConsultantPage/AddConsultantPage.module.scss diff --git a/src/components/domains/additional-purchases/AddConsultantPage/AddConsultantPage.tsx b/src/components/domains/additional-purchases/AddConsultantPage/AddConsultantPage.tsx new file mode 100644 index 0000000..1b7eeb8 --- /dev/null +++ b/src/components/domains/additional-purchases/AddConsultantPage/AddConsultantPage.tsx @@ -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 ( + <> + + + {t("title")} + + + {t("exclusive_offer")} + + + + + + + ); +} diff --git a/src/components/domains/additional-purchases/AddGuidesButton/AddGuidesButton.tsx b/src/components/domains/additional-purchases/AddGuidesButton/AddGuidesButton.tsx index f6855c1..7f8fdb3 100644 --- a/src/components/domains/additional-purchases/AddGuidesButton/AddGuidesButton.tsx +++ b/src/components/domains/additional-purchases/AddGuidesButton/AddGuidesButton.tsx @@ -1,27 +1,27 @@ "use client"; -import { useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; import { Button, Spinner, Typography } from "@/components/ui"; import { BlurComponent } from "@/components/widgets"; import { useSingleCheckout } from "@/hooks/payment/useSingleCheckout"; 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 { useMultiPageNavigationContext } from ".."; + export default function AddGuidesButton() { const t = useTranslations("AdditionalPurchases.add-guides"); - const router = useRouter(); const { addToast } = useToast(); const { selectedProduct } = useProductSelection(); + const { navigation } = useMultiPageNavigationContext(); const { handleSingleCheckout, isLoading } = useSingleCheckout({ onSuccess: () => { - router.push(ROUTES.home()); + navigation.goToNext(); }, onError: _error => { addToast({ @@ -49,7 +49,7 @@ export default function AddGuidesButton() { }; const handleSkipOffer = () => { - router.push(ROUTES.home()); + navigation.goToNext(); }; const isSkipOffer = selectedProduct?.id === "main_skip_offer"; diff --git a/src/app/[locale]/(additional-purchases)/add-guides/page.module.scss b/src/components/domains/additional-purchases/AddGuidesPage/AddGuidesPage.module.scss similarity index 100% rename from src/app/[locale]/(additional-purchases)/add-guides/page.module.scss rename to src/components/domains/additional-purchases/AddGuidesPage/AddGuidesPage.module.scss diff --git a/src/app/[locale]/(additional-purchases)/add-guides/page.tsx b/src/components/domains/additional-purchases/AddGuidesPage/AddGuidesPage.tsx similarity index 72% rename from src/app/[locale]/(additional-purchases)/add-guides/page.tsx rename to src/components/domains/additional-purchases/AddGuidesPage/AddGuidesPage.tsx index f4bc1fa..c44f400 100644 --- a/src/app/[locale]/(additional-purchases)/add-guides/page.tsx +++ b/src/components/domains/additional-purchases/AddGuidesPage/AddGuidesPage.tsx @@ -9,16 +9,10 @@ import { ProductSelectionProvider, } from "@/components/domains/additional-purchases"; 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 = { - funnel: ELocalesPlacement.CompatibilityV2, -}; - -export default function AddGuides() { +export default function AddGuidesPage() { const t = useTranslations("AdditionalPurchases.add-guides"); return ( @@ -31,7 +25,7 @@ export default function AddGuides() { {t("subtitle")} }> - + {t("description")} diff --git a/src/components/domains/additional-purchases/ConsultationTable/ConsultationTable.tsx b/src/components/domains/additional-purchases/ConsultationTable/ConsultationTable.tsx index f1883a3..258942d 100644 --- a/src/components/domains/additional-purchases/ConsultationTable/ConsultationTable.tsx +++ b/src/components/domains/additional-purchases/ConsultationTable/ConsultationTable.tsx @@ -1,31 +1,26 @@ -import Image from "next/image"; -import { getTranslations } from "next-intl/server"; +"use client"; -import { Skeleton, Typography } from "@/components/ui"; -import { - IFunnelPaymentProperty, - IFunnelPaymentVariant, -} from "@/entities/session/funnel/types"; +import Image from "next/image"; +import { useTranslations } from "next-intl"; + +import { Typography } from "@/components/ui"; import { getFormattedPrice } from "@/shared/utils/price"; import { Currency } from "@/types"; import styles from "./ConsultationTable.module.scss"; -interface ConsultationTableProps { - products: Promise; - properties: Promise; -} +import { useMultiPageNavigationContext } from ".."; + +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 product = (await products)?.[0]; + const product = data?.variants?.[0]; 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 oldPrice = getFormattedPrice( @@ -109,7 +104,3 @@ export default async function ConsultationTable({ ); } - -export function ConsultationTableSkeleton() { - return ; -} diff --git a/src/components/domains/additional-purchases/MultiPageNavigationProvider.tsx b/src/components/domains/additional-purchases/MultiPageNavigationProvider.tsx new file mode 100644 index 0000000..108f6f2 --- /dev/null +++ b/src/components/domains/additional-purchases/MultiPageNavigationProvider.tsx @@ -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 + >; +} + +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({ + data, + currentType, + getTypeFromItem: item => item.type ?? "", + navigateToItemByType: type => { + router.push(ROUTES.additionalPurchases(type)); + }, + onComplete: () => { + router.push(ROUTES.home()); + }, + }); + + return ( + + {children} + + ); +} + +export function useMultiPageNavigationContext() { + const context = useContext(MultiPageNavigationContext); + if (!context) { + throw new Error( + "useMultiPageNavigationContext must be used within MultiPageNavigationProvider" + ); + } + return context; +} diff --git a/src/components/domains/additional-purchases/Offers/Offers.tsx b/src/components/domains/additional-purchases/Offers/Offers.tsx index 754e754..9be439f 100644 --- a/src/components/domains/additional-purchases/Offers/Offers.tsx +++ b/src/components/domains/additional-purchases/Offers/Offers.tsx @@ -1,22 +1,21 @@ "use client"; -import { use, useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { Skeleton } from "@/components/ui"; import { IFunnelPaymentVariant } from "@/entities/session/funnel/types"; -import { useProductSelection } from "../ProductSelectionContext"; +import { useProductSelection } from "../ProductSelectionProvider"; import styles from "./Offers.module.scss"; -import { Offer } from ".."; +import { Offer, useMultiPageNavigationContext } from ".."; -interface OffersProps { - products: Promise; -} +export default function Offers() { + const { navigation } = useMultiPageNavigationContext(); + const data = navigation.currentItem; -export default function Offers({ products }: OffersProps) { - const offers = use(products); + const offers = useMemo(() => data?.variants ?? [], [data]); const [allOffers, setAllOffers] = useState([]); const [activeOffer, setActiveOffer] = useState(""); const { setSelectedProduct } = useProductSelection(); diff --git a/src/components/domains/additional-purchases/ProductSelectionContext.tsx b/src/components/domains/additional-purchases/ProductSelectionProvider.tsx similarity index 100% rename from src/components/domains/additional-purchases/ProductSelectionContext.tsx rename to src/components/domains/additional-purchases/ProductSelectionProvider.tsx diff --git a/src/components/domains/additional-purchases/index.ts b/src/components/domains/additional-purchases/index.ts index b0f90ff..360f109 100644 --- a/src/components/domains/additional-purchases/index.ts +++ b/src/components/domains/additional-purchases/index.ts @@ -1,13 +1,16 @@ export { default as AddConsultantButton } from "./AddConsultantButton/AddConsultantButton"; +export { default as AddConsultantPage } from "./AddConsultantPage/AddConsultantPage"; export { default as AddGuidesButton } from "./AddGuidesButton/AddGuidesButton"; +export { default as AddGuidesPage } from "./AddGuidesPage/AddGuidesPage"; export { default as Caution } from "./Caution/Caution"; +export { default as ConsultationTable } from "./ConsultationTable/ConsultationTable"; export { - default as ConsultationTable, - ConsultationTableSkeleton, -} from "./ConsultationTable/ConsultationTable"; + MultiPageNavigationProvider, + useMultiPageNavigationContext, +} from "./MultiPageNavigationProvider"; export { default as Offer } from "./Offer/Offer"; export { default as Offers, OffersSkeleton } from "./Offers/Offers"; export { ProductSelectionProvider, useProductSelection, -} from "./ProductSelectionContext"; +} from "./ProductSelectionProvider"; diff --git a/src/components/layout/index.ts b/src/components/layout/index.ts index c711061..9d62205 100644 --- a/src/components/layout/index.ts +++ b/src/components/layout/index.ts @@ -1,4 +1,5 @@ export { DrawerProvider, useDrawer } from "./Drawer/DrawerContext"; export { default as Header } from "./Header/Header"; export { default as Logo } from "./Logo/Logo"; +export { default as NavigationBar } from "./NavigationBar/NavigationBar"; export { default as StepperBar } from "./StepperBar/StepperBar"; diff --git a/src/entities/session/funnel/loaders.ts b/src/entities/session/funnel/loaders.ts index a803266..875cf3f 100644 --- a/src/entities/session/funnel/loaders.ts +++ b/src/entities/session/funnel/loaders.ts @@ -30,12 +30,12 @@ export const loadFunnelPaymentById = cache( loadFunnelData(payload).then(d => d.payment[paymentId]) ); -export const loadFunnelProducts = cache( - (payload: FunnelRequest, paymentId: string) => - loadFunnelPaymentById(payload, paymentId).then(d => d?.variants ?? []) -); +// export const loadFunnelProducts = cache( +// (payload: FunnelRequest, paymentId: string) => +// loadFunnelPaymentById(payload, paymentId).then(d => d?.variants ?? []) +// ); -export const loadFunnelProperties = cache( - (payload: FunnelRequest, paymentId: string) => - loadFunnelPaymentById(payload, paymentId).then(d => d?.properties ?? []) -); +// export const loadFunnelProperties = cache( +// (payload: FunnelRequest, paymentId: string) => +// loadFunnelPaymentById(payload, paymentId).then(d => d?.properties ?? []) +// ); diff --git a/src/entities/session/funnel/types.ts b/src/entities/session/funnel/types.ts index f6916a3..9231dcd 100644 --- a/src/entities/session/funnel/types.ts +++ b/src/entities/session/funnel/types.ts @@ -34,13 +34,20 @@ export const FunnelPaymentPlacementSchema = z.object({ properties: z.array(FunnelPaymentPropertySchema).optional(), variants: z.array(FunnelPaymentVariantSchema).optional(), paymentUrl: z.string().optional(), + type: z.string().optional(), }); export const FunnelSchema = z.object({ currency: z.nativeEnum(Currency), funnel: z.nativeEnum(ELocalesPlacement), 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({ diff --git a/src/hooks/multiPages/useMultiPageNavigation.ts b/src/hooks/multiPages/useMultiPageNavigation.ts new file mode 100644 index 0000000..71ec504 --- /dev/null +++ b/src/hooks/multiPages/useMultiPageNavigation.ts @@ -0,0 +1,142 @@ +"use client"; + +import { useCallback, useMemo } from "react"; + +interface PageNavigationOptions { + data: T[]; + currentType: string; + getTypeFromItem: (item: T) => string; + navigateToItemByType: (type: string) => void; + onBeforeNext?: (nextItem: T) => boolean | Promise; + onBeforePrevious?: (prevItem: T) => boolean | Promise; + onComplete: () => void; + onStart?: () => void; +} + +interface PageNavigationReturn { + currentItem: T | undefined; + currentIndex: number; + isFirst: boolean; + isLast: boolean; + hasNext: boolean; + hasPrevious: boolean; + goToNext: () => Promise; + goToPrevious: () => Promise; + goToFirst: () => Promise; + goToLast: () => Promise; + goToIndex: (index: number) => Promise; + totalPages: number; +} + +export function useMultiPageNavigation({ + data, + currentType, + getTypeFromItem, + navigateToItemByType, + onBeforeNext, + onBeforePrevious, + onComplete, + onStart, +}: PageNavigationOptions): PageNavigationReturn { + 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, + ] + ); +} diff --git a/src/shared/constants/client-routes.ts b/src/shared/constants/client-routes.ts index 9b78da3..6f77228 100644 --- a/src/shared/constants/client-routes.ts +++ b/src/shared/constants/client-routes.ts @@ -66,6 +66,7 @@ export const ROUTES = { // Additional Purchases addConsultant: () => createRoute(["add-consultant"]), addGuides: () => createRoute(["add-guides"]), + additionalPurchases: (type?: string) => createRoute(["ap", type]), // // Compatibility // compatibilities: () => createRoute(["compatibilities"]),