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 NavigationBar from "@/components/layout/NavigationBar/NavigationBar";
|
||||
import { DrawerProvider, Header, NavigationBar } from "@/components/layout";
|
||||
|
||||
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 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 && (
|
||||
<Button onClick={navigateToHome} className={styles.button}>
|
||||
<Button onClick={handleNext} className={styles.button}>
|
||||
<Typography color="white">{t("button")}</Typography>
|
||||
</Button>
|
||||
)}
|
||||
@ -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<IFunnelPaymentVariant[]>;
|
||||
}
|
||||
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 (
|
||||
|
||||
@ -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";
|
||||
|
||||
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";
|
||||
|
||||
@ -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")}
|
||||
</Typography>
|
||||
<Suspense fallback={<OffersSkeleton />}>
|
||||
<Offers products={loadFunnelProducts(payload, "add_guides")} />
|
||||
<Offers />
|
||||
</Suspense>
|
||||
<Typography align="left" color="secondary" className={styles.description}>
|
||||
{t("description")}
|
||||
@ -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<IFunnelPaymentVariant[]>;
|
||||
properties: Promise<IFunnelPaymentProperty[]>;
|
||||
}
|
||||
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({
|
||||
</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";
|
||||
|
||||
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<IFunnelPaymentVariant[]>;
|
||||
}
|
||||
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<IFunnelPaymentVariant[]>([]);
|
||||
const [activeOffer, setActiveOffer] = useState<string>("");
|
||||
const { setSelectedProduct } = useProductSelection();
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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 ?? [])
|
||||
// );
|
||||
|
||||
@ -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({
|
||||
|
||||
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
|
||||
addConsultant: () => createRoute(["add-consultant"]),
|
||||
addGuides: () => createRoute(["add-guides"]),
|
||||
additionalPurchases: (type?: string) => createRoute(["ap", type]),
|
||||
|
||||
// // Compatibility
|
||||
// compatibilities: () => createRoute(["compatibilities"]),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user