Browse Source

feat: add subscription purchase functionality and update related components

master
sina_sajjadi 3 weeks ago
parent
commit
b75d1457d1
  1. 1
      public/locales/en/common.json
  2. 119
      src/components/auth/registration-form.tsx
  3. 1
      src/components/shop/shop-form.tsx
  4. 6
      src/components/ui/logo.tsx
  5. 3
      src/data/client/api-endpoints.ts
  6. 60
      src/data/subscription.ts
  7. 28
      src/data/user.ts
  8. 5
      src/pages/shop/create.tsx
  9. 4
      src/pages/subscriptions/active-section.tsx
  10. 69
      src/pages/subscriptions/modal.tsx
  11. 138
      src/pages/subscriptions/plans-section.tsx
  12. 5
      src/settings/site.settings.ts

1
public/locales/en/common.json

@ -486,6 +486,7 @@
"text-inactive-shops": "Inactive/New shops",
"text-product-management": "Product management",
"text-all-products": "All Product",
"text-create-products": "Create Product",
"text-new-products": "Add new product",
"text-my-draft": "My Draft",
"text-my-draft-products": "My Draft products",

119
src/components/auth/registration-form.tsx

@ -3,7 +3,7 @@ import Button from '@/components/ui/button';
import Input from '@/components/ui/input';
import PasswordInput from '@/components/ui/password-input';
import { useRouter } from 'next/router';
import { useState } from 'react';
import { useState, useRef, useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { Routes } from '@/config/routes';
import { useTranslation } from 'next-i18next';
@ -17,8 +17,6 @@ import {
} from '@/utils/auth-utils';
import { Permission } from '@/types';
import { useOTPMutation, useRegisterMutation } from '@/data/user';
import { useRef } from 'react';
import { useEffect } from 'react';
type FormValues = {
fullname: string;
@ -28,15 +26,16 @@ type FormValues = {
password_confirmation: string;
user_type: 'merchant';
};
const registrationFormSchema = yup.object().shape({
fullname: yup.string().required('Full name is required'),
phone_number: yup.string().required('Phone number is required'),
range_phone: yup.string().required('Country code is required'),
password: yup.string().required('Password is required'),
fullname: yup.string().required('form:error-fullname-required'),
phone_number: yup.string().required('form:error-phone-required'),
range_phone: yup.string().required('form:error-country-code-required'),
password: yup.string().required('form:error-password-required'),
password_confirmation: yup
.string()
.oneOf([yup.ref('password')], 'Passwords must match')
.required('Confirm password is required'),
.oneOf([yup.ref('password')], 'form:error-passwords-must-match')
.required('form:error-confirm-password-required'),
permission: yup.string().default('store_owner').oneOf(['store_owner']),
});
@ -46,7 +45,7 @@ const RegistrationForm = () => {
const { mutate: confirmUser, isLoading: OTPloading } = useOTPMutation();
const [stage, setStage] = useState('signUp'); // Stage management
const [otp, setOtp] = useState(['', '', '', '', '']); // OTP state
const [time, setTime] = useState(3); // Timer for OTP resend
const [time, setTime] = useState(30); // Timer for OTP resend
const otpRefs = useRef<(HTMLInputElement | null)[]>([]); // Refs for OTP input focus
const firstOtpRef = useRef<HTMLInputElement | null>(null);
@ -77,14 +76,13 @@ const RegistrationForm = () => {
}
}, [stage]);
async function onSubmit({
const onSubmit = ({
fullname,
phone_number,
range_phone,
password,
password_confirmation,
}: FormValues) {
}: FormValues) => {
if (stage === 'signUp') {
registerUser(
{
@ -98,26 +96,33 @@ const RegistrationForm = () => {
},
{
onSuccess: (data) => {
// if (data?.token) {
setStage('OTP'); // Transition to OTP stage on successful registration
// } else {
// setErrorMessage('form:error-credential-wrong');
// }
},
onError: (error: any) => {
Object.keys(error?.response?.data).forEach((field: any) => {
// Handle field-specific errors
if (error?.response?.data) {
Object.keys(error.response.data).forEach((field: any) => {
setError(field, {
type: 'manual',
message: error?.response?.data[field],
message: error.response.data[field],
});
});
// Set general error message if available
if (error.response.data.message) {
setErrorMessage(error.response.data.message);
} else {
setErrorMessage(t('form:error-general'));
}
} else {
setErrorMessage(t('form:error-general'));
}
},
},
);
}
if (stage === 'OTP') {
if (stage === 'OTP') {
confirmUser(
{
method: 'register',
@ -127,27 +132,33 @@ const RegistrationForm = () => {
},
{
onSuccess: (data) => {
// if (data?.token) {
setAuthCredentials(data.token)
setAuthCredentials(data.token);
router.replace(Routes.dashboard);
// } else {
// setErrorMessage('form:error-credential-wrong');
// }
},
onError: (error: any) => {
console.log(error);
Object.keys(error?.response?.data).forEach((field: any) => {
// Handle field-specific errors
if (error?.response?.data) {
Object.keys(error.response.data).forEach((field: any) => {
setError(field, {
type: 'manual',
message: error?.response?.data[field],
message: error.response.data[field],
});
});
// Set general error message if available
if (error.response.data.message) {
setErrorMessage(error.response.data.message);
} else {
setErrorMessage(t('form:error-general'));
}
} else {
setErrorMessage(t('form:error-general'));
}
},
},
);
}
}
};
const handleOtpChange = (value: string, index: number) => {
if (/^[0-9]?$/.test(value)) {
@ -171,7 +182,7 @@ const RegistrationForm = () => {
const handleOTPSubmit = () => {
if (!otp.join('').trim()) {
setErrorMessage('Please enter the OTP');
setErrorMessage(t('form:error-otp-required'));
return;
}
@ -190,9 +201,9 @@ const RegistrationForm = () => {
className="mb-4"
error={t(errors?.fullname?.message!)}
/>
<label className="block">
<label className="block mb-4">
<span className="text-gray-600 font-semibold text-sm leading-none mb-3">
Phone Number
{t('form:input-label-phone')}
</span>
<div className="py-1.5 px-4 md:px-5 w-full appearance-none border text-input text-xs lg:text-sm font-body placeholder-body min-h-12 transition duration-200 ease-in-out border-gray-300 focus:shadow focus:bg-white focus:border-primary flex items-center rounded-md">
<span className="mr-[-10px] text-gray-700">+</span>
@ -211,6 +222,11 @@ const RegistrationForm = () => {
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none flex-1 bg-gray-100 border-none outline-none bg-transparent text-gray-800 p-2"
/>
</div>
{errors.phone_number && (
<p className="text-red-500 text-sm mt-1">
{t(errors.phone_number.message!)}
</p>
)}
</label>
<PasswordInput
label={t('form:input-label-password')}
@ -235,15 +251,15 @@ const RegistrationForm = () => {
{t('form:text-register')}
</Button>
{errorMessage ? (
{errorMessage && (
<Alert
message={t(errorMessage)}
variant="error"
closeable={true}
closeable
className="mt-5"
onClose={() => setErrorMessage(null)}
/>
) : null}
)}
</>
);
};
@ -252,10 +268,10 @@ const RegistrationForm = () => {
<div className="nc-PageSignUp">
<div className="flex gap-4 flex-col">
<h2 className="my-10 text-center text-2xl font-semibold text-neutral-900">
Verification Code
{t('form:otp-verification-title')}
</h2>
<p className="text-center text-sm text-neutral-500 mb-4">
Enter the 5-digit code we sent to your phone.
{t('form:otp-verification-description')}
</p>
<div className="max-w-sm mx-auto space-y-6">
<div className="flex justify-center space-x-2 mb-4">
@ -276,18 +292,20 @@ const RegistrationForm = () => {
))}
</div>
<p className="text-center text-sm text-neutral-500 mb-4">
Didnt receive the code?{' '}
{t('form:didnt-receive-code')}{' '}
<Button
type="button"
onClick={() => {
setTime(30)
setStage("signUp")
setTime(30);
setStage("signUp");
setErrorMessage(null); // Clear previous errors
}}
className={`text-primary-600 hover:underline p-0 bg-0 hover:bg-o disabled:border-none ${
time > 0 ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'
}`}
disabled={time > 0}
>
Resend
{t('form:resend-code')}
</Button>
{time > 0 && (
<span className="text-xs text-neutral-400">({time}s)</span>
@ -296,15 +314,26 @@ const RegistrationForm = () => {
<Button
type="submit"
className="h-11 md:h-12 w-full mt-2"
loading={OTPloading}
disabled={OTPloading}
>
Confirm
{t('form:confirm-otp')}
</Button>
{errorMessage && (
<Alert
message={t(errorMessage)}
variant="error"
closeable
className="mt-5"
onClose={() => setErrorMessage(null)}
/>
)}
</div>
</div>
</div>
);
return (
//@ts-ignore
<form onSubmit={handleSubmit(onSubmit)} noValidate>

1
src/components/shop/shop-form.tsx

@ -128,6 +128,7 @@ const ShopForm = ({ initialValues }: { initialValues?: Shop }) => {
...formDefaults,
logo: formDefaults.logo_url,
},
resolver: yupResolver(shopValidationSchema),
});
const router = useRouter();

6
src/components/ui/logo.tsx

@ -37,9 +37,7 @@ const Logo: React.FC<React.AnchorHTMLAttributes<{}>> = ({
}}
>
<Image
src={
settings?.options?.collapseLogo?.original ??
siteSettings.collapseLogo.url
src={siteSettings.collapseLogo.url
}
alt={settings?.options?.siteTitle ?? siteSettings.collapseLogo.alt}
fill
@ -57,7 +55,7 @@ const Logo: React.FC<React.AnchorHTMLAttributes<{}>> = ({
}}
>
<Image
src={settings?.options?.logo?.original ?? siteSettings.logo.url}
src={siteSettings.logo.url}
alt={settings?.options?.siteTitle ?? siteSettings.logo.alt}
fill
sizes="(max-width: 768px) 100vw"

3
src/data/client/api-endpoints.ts

@ -109,5 +109,6 @@ export const API_ENDPOINTS = {
OWNERSHIP_TRANSFER: 'ownership-transfer',
GET_ACTIVE_SUBSCRIPTION : "merchant-panel/subscriptions/active/",
GET_ALL_SUBSCRIPTIONS : "merchant-panel/subscriptions/list/",
GET_SUBSCRIPTIONS_HISTORY : "merchant-panel/subscriptions/history/list/"
GET_SUBSCRIPTIONS_HISTORY : "merchant-panel/subscriptions/history/list/",
PURCHASE_SUBSCRIPTION : "merchant-panel/subscriptions/subscription-orders/"
};

60
src/data/subscription.ts

@ -1,13 +1,61 @@
import { useQuery } from "react-query";
import { API_ENDPOINTS } from "./client/api-endpoints";
import { HttpClient } from "./client/http-client";
// hooks/subscriptionHooks.ts
import { useQuery, useMutation, QueryKey, UseQueryOptions, UseMutationOptions } from 'react-query';
import { API_ENDPOINTS } from './client/api-endpoints';
import { HttpClient } from './client/http-client';
// Define types for your responses and request data
interface Subscription {
id: number;
duration: string;
final_price: string;
discount_percentage: string;
// ... other subscription fields
}
interface PurchaseData {
subscription_id: number;
transaction_id: string;
}
interface PurchaseResponse {
// Define the structure based on your API response
success: boolean;
message: string;
// ... other fields
}
// Generic GET hook
const useApiGet = <T>(
endpoint: string,
queryKey?: QueryKey,
options?: UseQueryOptions<T, Error>
) => {
return useQuery<T, Error>(
queryKey || [endpoint],
() => HttpClient.get<T>(endpoint),
options
);
};
// Specific GET hooks
export const useGetActiveSubscription = () => {
return useQuery([API_ENDPOINTS.GET_ACTIVE_SUBSCRIPTION], () => HttpClient.get(API_ENDPOINTS.GET_ACTIVE_SUBSCRIPTION));
return useApiGet<Subscription>(API_ENDPOINTS.GET_ACTIVE_SUBSCRIPTION);
};
export const useGetAllSubscriptions = () => {
return useQuery([API_ENDPOINTS.GET_ALL_SUBSCRIPTIONS], () => HttpClient.get(API_ENDPOINTS.GET_ALL_SUBSCRIPTIONS));
return useApiGet<Subscription[]>(API_ENDPOINTS.GET_ALL_SUBSCRIPTIONS);
};
export const useGetSubscriptionsHistory = () => {
return useQuery([API_ENDPOINTS.GET_SUBSCRIPTIONS_HISTORY], () => HttpClient.get(API_ENDPOINTS.GET_SUBSCRIPTIONS_HISTORY));
return useApiGet<Subscription[]>(API_ENDPOINTS.GET_SUBSCRIPTIONS_HISTORY);
};
// Purchase Subscription using useMutation
export const usePurchaseSubscription = (
options?: UseMutationOptions<PurchaseResponse, Error, PurchaseData>
) => {
return useMutation<PurchaseResponse, Error, PurchaseData>(
(data: PurchaseData) => HttpClient.post<PurchaseResponse>(API_ENDPOINTS.PURCHASE_SUBSCRIPTION, data),
options
);
};

28
src/data/user.ts

@ -28,7 +28,6 @@ export const useMeQuery = () => {
retry: false,
onSuccess: (data) => {
if (router.pathname === Routes.verifyLicense) {
router.replace(Routes.dashboard);
}
@ -51,6 +50,7 @@ console.log(err);
router.replace(Routes.verifyEmail);
return;
}
Cookies.remove(AUTH_CRED);
queryClient.clear();
router.replace(Routes.login);
}
@ -89,7 +89,6 @@ export const useRegisterMutation = () => {
return useMutation(userClient.register, {
onError: (err) => {
console.log(err);
},
// Always refetch after error or success:
onSettled: () => {
@ -109,7 +108,6 @@ export const useOTPMutation = () => {
},
onError: (err) => {
console.log(err);
},
// Always refetch after error or success:
onSettled: () => {
@ -158,7 +156,6 @@ export const useChangePasswordMutation = () => {
return useMutation(userClient.changePassword);
};
export const useForgetPasswordMutation = () => {
return useMutation(userClient.forgetPassword);
};
@ -199,8 +196,6 @@ export const useResetPasswordMutation = () => {
return useMutation(userClient.resetPassword);
};
export const useMakeOrRevokeAdminMutation = () => {
const queryClient = useQueryClient();
const { t } = useTranslation();
@ -274,7 +269,7 @@ export const useUserQuery = ({ id }: { id: string }) => {
() => userClient.fetchUser({ id }),
{
enabled: Boolean(id),
}
},
);
};
@ -284,7 +279,7 @@ export const useUsersQuery = (params: Partial<QueryOptionsType>) => {
() => userClient.fetchUsers(params),
{
keepPreviousData: true,
}
},
);
return {
@ -301,7 +296,7 @@ export const useAdminsQuery = (params: Partial<QueryOptionsType>) => {
() => userClient.fetchAdmins(params),
{
keepPreviousData: true,
}
},
);
return {
@ -318,7 +313,7 @@ export const useVendorsQuery = (params: Partial<UserQueryOptions>) => {
() => userClient.fetchVendors(params),
{
keepPreviousData: true,
}
},
);
return {
@ -335,7 +330,7 @@ export const useCustomersQuery = (params: Partial<UserQueryOptions>) => {
() => userClient.fetchCustomers(params),
{
keepPreviousData: true,
}
},
);
return {
@ -346,14 +341,15 @@ export const useCustomersQuery = (params: Partial<UserQueryOptions>) => {
};
};
export const useMyStaffsQuery = (params: Partial<UserQueryOptions & { shop_id: string }>) => {
export const useMyStaffsQuery = (
params: Partial<UserQueryOptions & { shop_id: string }>,
) => {
const { data, isLoading, error } = useQuery<UserPaginator, Error>(
[API_ENDPOINTS.MY_STAFFS, params],
() => userClient.getMyStaffs(params),
{
keepPreviousData: true,
}
},
);
return {
@ -364,14 +360,13 @@ export const useMyStaffsQuery = (params: Partial<UserQueryOptions & { shop_id: s
};
};
export const useAllStaffsQuery = (params: Partial<UserQueryOptions>) => {
const { data, isLoading, error } = useQuery<UserPaginator, Error>(
[API_ENDPOINTS.ALL_STAFFS, params],
() => userClient.getAllStaffs(params),
{
keepPreviousData: true,
}
},
);
return {
@ -381,4 +376,3 @@ export const useAllStaffsQuery = (params: Partial<UserQueryOptions>) => {
error,
};
};

5
src/pages/shop/create.tsx

@ -1,4 +1,5 @@
import OwnerLayout from '@/components/layouts/owner';
import Layout from '@/components/layouts/admin';
import ShopForm from '@/components/shop/shop-form';
import { adminAndOwnerOnly } from '@/utils/auth-utils';
import { GetStaticProps } from 'next';
@ -21,7 +22,7 @@ export default function CreateShopPage() {
// CreateShopPage.authenticate = {
// permissions: adminAndOwnerOnly,
// };
CreateShopPage.Layout = OwnerLayout;
CreateShopPage.Layout = Layout;
export const getStaticProps: GetStaticProps = async ({ locale }) => ({
props: {

4
src/pages/subscriptions/active-section.tsx

@ -38,7 +38,7 @@ const ActiveSubscriptionSection: React.FC = () => {
}
return (
<div className="flex gap-7 flex-col lg:flex-row">
<div className="bg-white w-full p-8 rounded-xl lg:w-1/3">
<div className="bg-white w-full p-8 rounded-xl xl:w-1/3">
<div className="mb-8 border p-2 rounded-lg flex items-center gap-2 max-w-fit">
<FaStar
size={20}
@ -54,7 +54,7 @@ const ActiveSubscriptionSection: React.FC = () => {
<Image src={background} alt="subscription type" />
</div>
</div>
<div className="bg-white w-full p-8 rounded-xl gap-y-6 gap-x-8 lg:w-8/12">
<div className="bg-white w-full p-8 rounded-xl gap-y-6 gap-x-8 xl:w-8/12">
<div className="flex flex-col border w-full h-full rounded-lg justify-between p-4 text-sm font-light">
<div className="flex justify-between">
<p className="text-gray-500">Start Your Plan</p>

69
src/pages/subscriptions/modal.tsx

@ -1,69 +0,0 @@
// components/ui/Modal.tsx
import React, { useEffect } from 'react';
import { FaTimes } from 'react-icons/fa';
interface ModalProps {
display: boolean;
setDisplay: (display: boolean) => void;
children: React.ReactNode;
}
const Modal: React.FC<ModalProps> = ({ display, setDisplay, children }) => {
// Close the modal when the Escape key is pressed
useEffect(() => {
const handleEscape = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
setDisplay(false);
}
};
if (display) {
document.addEventListener('keydown', handleEscape);
} else {
document.removeEventListener('keydown', handleEscape);
}
return () => {
document.removeEventListener('keydown', handleEscape);
};
}, [display, setDisplay]);
// Prevent scrolling when modal is open
useEffect(() => {
if (display) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = 'auto';
}
return () => {
document.body.style.overflow = 'auto';
};
}, [display]);
if (!display) return null;
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50"
onClick={() => setDisplay(false)} // Close when clicking on the backdrop
>
<div
className="bg-white rounded-lg shadow-lg w-full max-w-lg p-6 relative"
onClick={(e) => e.stopPropagation()} // Prevent closing when clicking inside the modal
>
{/* Close Button */}
<button
className="absolute top-4 right-4 text-gray-500 hover:text-gray-700"
onClick={() => setDisplay(false)}
aria-label="Close Modal"
>
<FaTimes size={20} />
</button>
{/* Modal Content */}
<div>{children}</div>
</div>
</div>
);
};
export default Modal;

138
src/pages/subscriptions/plans-section.tsx

@ -1,19 +1,47 @@
import React from 'react';
import { useGetAllSubscriptions } from '@/data/subscription';
// components/PlansSection.tsx
import React, { useState } from 'react';
import { useGetAllSubscriptions , usePurchaseSubscription } from '@/data/subscription';
import { RiHeadphoneLine } from 'react-icons/ri';
import { FaRegCheckCircle } from 'react-icons/fa';
import Button from '@/components/ui/button';
import { useState } from 'react';
import Modal from '@/components/ui/modal/modal';
import { IoMdClose } from 'react-icons/io';
import payPal from '../../../public/image/payments/🦆 icon _PayPal_.svg';
import Stripe from '../../../public/image/payments/🦆 icon _Stripe_.svg';
import Image from 'next/image';
import { toast } from 'react-toastify'; // Import toast
interface Subscription {
id: number;
duration: string;
final_price: string;
discount_percentage: string;
// ... other subscription fields
}
const PlansSection: React.FC = () => {
const [display, setDisplay] = useState(false);
const [selected, setSelected] = useState({});
const { data: allSubscriptions, isLoading: isAllLoading } =
useGetAllSubscriptions();
const [isModalOpen, setIsModalOpen] = useState(false);
const [selected, setSelected] = useState<Subscription | null>(null);
const { data: allSubscriptions, isLoading: isAllLoading, refetch } = useGetAllSubscriptions();
// Initialize the mutation hook with callbacks
const purchaseMutation = usePurchaseSubscription({
onSuccess: (data) => {
toast.success('Subscription purchased successfully!', {
position: 'top-right',
autoClose: 5000,
});
setIsModalOpen(false); // Close the modal
refetch(); // Refetch subscriptions if needed
},
onError: (error: Error) => {
toast.error(`Failed to purchase subscription: ${error.message}`, {
position: 'top-right',
autoClose: 5000,
});
},
});
const formatPrice = (priceString: string): string => {
if (!priceString) return '';
@ -26,11 +54,11 @@ const PlansSection: React.FC = () => {
const formatTitle = (text: string) => {
if (!text) {
return;
return null;
}
const splitedText = text.split('');
const number = splitedText[0];
const duration = () => {
const duration = (() => {
const char = splitedText[1];
if (char === 'D') {
return 'Day';
@ -42,17 +70,36 @@ const PlansSection: React.FC = () => {
return 'Year';
}
return '';
};
})();
return (
<div>
<span className="text-3xl font-semibold ">{number}&nbsp;</span>
<span className="text-2xl font-medium text-[#666666] font-sans">
{duration()}
{duration}
</span>
</div>
);
};
const handlePurchaseClick = (subscription: Subscription) => {
setSelected(subscription);
setIsModalOpen(true);
};
const handlePurchase = () => {
if (!selected) return;
// Define the purchase data
const purchaseData = {
subscription_id: selected.id,
transaction_id: 'txn_' + Date.now(), // Generate a unique transaction ID
// Add other necessary fields if required
};
// Trigger the mutation
purchaseMutation.mutate(purchaseData);
};
if (isAllLoading) {
return <div>Loading...</div>;
}
@ -71,11 +118,11 @@ const PlansSection: React.FC = () => {
<RiHeadphoneLine />
</div>
</div>
<div className="grid grid-cols-1 gap-8 lg:grid-cols-4">
{allSubscriptions?.results.map((item) => (
<div className="grid grid-cols-1 gap-8 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
{allSubscriptions?.results.map((item: Subscription) => (
<div className="border p-6 rounded-lg" key={item.id}>
<div className="border bg-[#F3F4F6] p-2 rounded-xl flex gap-10 items-end mb-6">
<p>{formatTitle(item.duration)}</p>
{formatTitle(item.duration)}
<p className="text-sm text-[#666666]">User Per Month</p>
</div>
<div className="flex justify-between">
@ -85,7 +132,7 @@ const PlansSection: React.FC = () => {
</span>
<span className="text-sm text-[#666666]">User Per Month</span>
</div>
{!!formatPrice(item.discount_percentage) && (
{!!(item.discount_percentage && parseFloat(item.discount_percentage) > 0 )&& (
<div className="bg-green-100 p-1 rounded-md pt-[7px]">
<p className="text-[#1DCE1D] text-xs font-semibold">
Save {item.discount_percentage}%
@ -94,10 +141,7 @@ const PlansSection: React.FC = () => {
)}
</div>
<Button
onClick={() => {
setDisplay(true);
setSelected(item);
}}
onClick={() => handlePurchaseClick(item)}
className="w-full mt-6"
>
Purchase Subscription
@ -111,9 +155,7 @@ const PlansSection: React.FC = () => {
</h1>
<div className="border p-8 grid grid-cols-1 gap-8 text-[#666666] text-sm rounded-lg lg:grid-cols-2">
<div className="flex items-start gap-2">
<div className="mt-1">
<FaRegCheckCircle color="#1DCE1D" size={15} />
</div>
<p>
Customizable Storefront: Merchants can personalize their
storefront with custom branding, logos, and banners to create a
@ -121,9 +163,7 @@ const PlansSection: React.FC = () => {
</p>
</div>
<div className="flex items-start gap-2">
<div className="mt-1">
<FaRegCheckCircle color="#1DCE1D" size={15} />
</div>
<p>
Advanced Analytics Dashboard: Access detailed insights into sales,
traffic, and customer behavior to optimize listings and marketing
@ -131,18 +171,14 @@ const PlansSection: React.FC = () => {
</p>
</div>
<div className="flex items-start gap-2">
<div className="mt-1">
<FaRegCheckCircle color="#1DCE1D" size={15} />
</div>
<p>
Bulk Listing Management: Easily upload and manage multiple
gemstone listings at once with bulk editing features.
</p>
</div>
<div className="flex items-start gap-2">
<div className="mt-1">
<FaRegCheckCircle color="#1DCE1D" size={15} />
</div>
<p>
Real-Time Inventory Tracking: Merchants can track stock levels in
real-time, ensuring they never over-sell or run out of inventory.
@ -150,18 +186,19 @@ const PlansSection: React.FC = () => {
</div>
</div>
</div>
{/* Modal Component */}
<Modal
open={!!Object.keys(selected).length}
onClose={() => {
setSelected({});
}}
open={isModalOpen}
onClose={() => setIsModalOpen(false)}
>
{selected && (
<div className="bg-white py-10 px-12 w-[609px] rounded-2xl ">
<div className="flex items-center justify-between">
<h1 className="text-xl font-semibold mb-2">
One-year subscription
{formatTitle(selected.duration)} Subscription
</h1>
<button onClick={() => setSelected({})}>
<button onClick={() => setIsModalOpen(false)}>
<IoMdClose className="text-gray-500" size={25} />
</button>
</div>
@ -172,7 +209,7 @@ const PlansSection: React.FC = () => {
</p>
<div className="border w-80 p-6 rounded-lg mb-8">
<div className="border bg-[#F3F4F6] p-2 rounded-xl flex gap-10 items-end mb-6">
<p>{formatTitle(selected.duration)}</p>
{formatTitle(selected.duration)}
<p className="text-sm text-[#666666]">User Per Month</p>
</div>
<div className="flex justify-between">
@ -182,7 +219,7 @@ const PlansSection: React.FC = () => {
</span>
<span className="text-sm text-[#666666]">User Per Month</span>
</div>
{!!formatPrice(selected.discount_percentage) && (
{selected.discount_percentage && parseFloat(selected.discount_percentage) > 0 && (
<div className="bg-green-100 p-1 rounded-md pt-[7px]">
<p className="text-[#1DCE1D] text-xs font-semibold">
Save {selected.discount_percentage}%
@ -197,37 +234,42 @@ const PlansSection: React.FC = () => {
</h1>
<div className="text-[#666666] text-sm">
<div className="flex items-center gap-2 mb-4">
<div>
<FaRegCheckCircle color="#1DCE1D" size={15} />
</div>
<p>Joining Live Streams</p>
</div>{' '}
</div>
<div className="flex items-center gap-2 mb-4">
<div>
<FaRegCheckCircle color="#1DCE1D" size={15} />
<p>Exclusive Content Access</p>
</div>
<p>Joining Live Streams</p>
</div>{' '}
<div className="flex items-center gap-2">
<div>
<FaRegCheckCircle color="#1DCE1D" size={15} />
</div>
<p>Creating a Custom Profile</p>
</div>
</div>
</div>
<div className='flex gap-6 my-8'>
<button className="border py-6 px-16 w-full rounded-lg">
<Image src={payPal} alt="payPal" />
<button className="border py-6 px-16 w-full rounded-lg flex items-center justify-center">
<Image src={payPal} alt="PayPal" />
</button>
<button className="border py-6 px-16 w-full rounded-lg">
<button className="border py-6 px-16 w-full rounded-lg flex items-center justify-center">
<Image src={Stripe} alt="Stripe" />
</button>
</div>
<Button className='w-full'>
Purchase Subscription
<Button
onClick={handlePurchase}
className='w-full'
disabled={purchaseMutation.isLoading}
loading={purchaseMutation.isLoading}
>
{purchaseMutation.isLoading ? 'Purchasing...' : 'Purchase Subscription'}
</Button>
{purchaseMutation.isError && (
<div className="text-red-500 mt-2">
Error purchasing subscription: {purchaseMutation.error.message}
</div>
)}
</div>
)}
</Modal>
</div>
);

5
src/settings/site.settings.ts

@ -148,6 +148,11 @@ export const siteSettings = {
label: 'sidebar-nav-item-products',
icon: 'ProductsIcon',
childMenu: [
{
href: Routes.product.create,
label: 'text-create-products',
icon: 'ProductsIcon',
},
{
href: Routes.product.list,
label: 'text-all-products',

Loading…
Cancel
Save