added subscription form and payment proccess page

This commit is contained in:
pramod.mahajan 2025-10-27 02:22:35 +05:30
parent 11354c2c85
commit b28946333e
14 changed files with 884 additions and 370 deletions

View File

@ -40,6 +40,64 @@
.text-royalblue{ .text-royalblue{
color: #1796e3; color: #1796e3;
} }
.stepper-container {
position: relative;
}
.timeline-horizontal {
position: relative;
padding: 0;
margin: 0;
}
.timeline-item {
position: relative;
flex: 1;
}
.timeline-point {
width: 20px;
height: 20px;
border-radius: 50%;
background: #dee2e6;
color: #6c757d;
display: flex;
justify-content: center;
align-items: center;
font-weight: 600;
z-index: 2;
position: relative;
padding: 3px;
}
.timeline-point.completed {
background-color: var(--bs-success);
color: white;
}
.timeline-point.active {
background-color: var(--bs-info);
color: white;
transform: scale(1.1);
padding: 4px;
}
.timeline-line-horizontal {
content: "";
position: absolute;
top: 10px;
left: 50%;
width:100% ;
height: 2px;
background-color: #dee2e6;
z-index: 1;
}
.timeline-item.completed ~ .timeline-line-horizontal {
background-color: #28a745;
}
.text-md { .text-md {
font-size: 2rem; font-size: 2rem;

View File

@ -0,0 +1,295 @@
import React, { useState, useMemo } from "react";
import { useSubscription } from "../../hooks/useAuth";
import { useParams } from "react-router-dom";
import { useCreateTenant, useIndustries } from "../../hooks/useTenant";
import { frequencyLabel } from "../../utils/appUtils";
import { formatUTCToLocalTime } from "../../utils/dateUtils";
const ProcessedPayment = () => {
const { frequency, planName } = useParams();
const [selectedPlan, setSelectedPlan] = useState(planName);
const {
data: plans,
isError: isPlanError,
isLoading,
} = useSubscription(frequency);
const handleChange = (e) => {
setSelectedPlan(e.target.value);
};
const visiblePlans = useMemo(() => {
if (!plans) return [];
const planOrder = ["basic", "pro", "super"];
const currentIndex = planOrder.indexOf(planName?.toLowerCase());
if (currentIndex === -1) return plans; // fallback: show all
const visibleNames = planOrder.slice(currentIndex);
return plans.filter((p) =>
visibleNames.includes(p.planName?.toLowerCase())
);
}, [plans, selectedPlan]);
const clients = [
{
firstName: "Alice",
lastName: "Smith",
email: "alice.smith@example.com",
billingAddress:
"456 Elm Street, Metropolis, USA 456 Elm Street, Metropolis, USA 456 Elm Street, Metropolis, USA",
organizationName: "BetaTech Ltd.",
contactNumber: "+44 20 7946 0958",
onBoardingDate: "2025-09-15",
organizationSize: "10-50",
industryId: "c4d5e6f7-8901-2345-6789-abcdef123456",
reference: "d2e3f4a5-6789-0123-4567-89abcdef0123",
},
];
return (
<div className="container-md">
<div className="row">
<div className="col-12 col-md-6">
<div className="row">
<div className="col-12 mb-3 text-start">
<h4 className="fw-bold ">
Choose the Perfect Plan for Your Organization
</h4>
<p className="text-muted small mb-3">
Select a plan that fits your teams needs and unlock the
features that drive productivity.
</p>
</div>
{visiblePlans?.map((plan) => {
let colSize = "8"; // default 1 card full width
if (visiblePlans.length === 2) colSize = "6";
else if (visiblePlans.length === 3) colSize = "4";
else if (visiblePlans.length >= 4) colSize = "3";
return (
<div
key={plan?.id}
className={`col-12 col-md-${colSize} mb-md-3 mb-2`}
>
<div
className={`form-check custom-option custom-option-basic text-start w-100 bg-light-primary ${
selectedPlan === plan?.planName
? "border border-primary shadow-md"
: ""
}`}
>
<label
className="form-check-label custom-option-content w-100"
htmlFor={`customRadioTemp${plan?.id}`}
>
<input
name="customRadioTemp"
className="form-check-input"
type="radio"
value={plan?.planName}
id={`customRadioTemp${plan?.id}`}
checked={selectedPlan === plan?.planName}
onChange={handleChange}
/>
<span className="custom-option-header d-flex justify-content-between align-items-center">
<span className="h6 mb-0">{plan?.planName}</span>
<span>
{plan.currency?.symbol} {plan.price} /{" "}
{frequencyLabel(frequency)}
</span>
</span>
<span className="custom-option-body d-block mt-1">
<small>{plan?.description}</small>
</span>
</label>
</div>
</div>
);
})}
{selectedPlan && (
<div className="col-12 text-start">
<div className="border-warning p-3 mt-3">
{(() => {
const selected = plans?.find(
(p) => p.planName === selectedPlan
);
if (!selected) return null;
const {
planName,
description,
price,
frequency,
trialDays,
maxUser,
maxStorage,
currency,
features,
} = selected;
return (
<>
<div className="row g-2 mb-3">
<div className="col-sm-6 col-md-4">
<div className="border rounded-3 p-2 bg-light">
<i className="bx bx-user me-1 text-primary"></i>
<strong>Max Users:</strong> {maxUser}
</div>
</div>
<div className="col-sm-6 col-md-4">
<div className="border rounded-3 p-2 bg-light">
<i className="bx bx-hdd me-1 text-primary"></i>
<strong>Max Storage:</strong> {maxStorage} MB
</div>
</div>
<div className="col-sm-6 col-md-4">
<div className="border rounded-3 p-2 bg-light">
<i className="bx bx-time-five me-1 text-primary"></i>
<strong>Trial Days:</strong> {trialDays}
</div>
</div>
</div>
<h6 className="fw-bold text-secondary mb-2">
Included Features
</h6>
<div className="row">
{Object.entries(features?.modules || {}).map(
([key, mod]) => (
<div key={mod.id} className="col-md-6 mb-2">
<div
className={`border rounded-3 p-2 ${
mod.enabled
? "border-success bg-light-success"
: "border-light"
}`}
>
<div className="d-flex align-items-center justify-content-between">
<span>
<i
className={`bx bx-check-circle me-2 ${
mod.enabled
? "text-success"
: "text-muted"
}`}
></i>
{mod.name}
</span>
<small
className={`badge ${
mod.enabled
? "bg-success-subtle text-success"
: "bg-secondary-subtle text-muted"
}`}
>
{mod.enabled ? "Enabled" : "Disabled"}
</small>
</div>
</div>
</div>
)
)}
</div>
<h6 className="fw-bold text-secondary mt-3 mb-2">
Support
</h6>
<ul className="list-unstyled d-flex flex-wrap gap-3 align-items-center mb-0 small">
{features?.supports?.emailSupport && (
<li className="d-flex align-items-center">
<i className="bx bx-envelope text-primary me-1 fs-5"></i>
Email Support
</li>
)}
{features?.supports?.phoneSupport && (
<li className="d-flex align-items-center">
<i className="bx bx-phone text-primary me-1 fs-5"></i>
Phone Support
</li>
)}
{features?.supports?.prioritySupport && (
<li className="d-flex align-items-center">
<i className="bx bx-star text-warning me-1 fs-5"></i>
Priority Support
</li>
)}
</ul>
</>
);
})()}
</div>
</div>
)}
</div>
</div>
<div className="col-12 col-md-6 hv-100 ">
<h6>Client Info</h6>
{clients.map((client, idx) => (
<div key={idx} className="text-start">
<div className="row g-2">
<div className="col-sm-6 mb-2">
<strong>First Name:</strong>
</div>
<div className="col-sm-6 mb-2">{client.firstName}</div>
<div className="col-sm-6 mb-2">
<strong>Last Name:</strong>
</div>
<div className="col-sm-6 mb-2">{client.lastName}</div>
<div className="col-sm-6">
<strong>Email:</strong>
</div>
<div className="col-sm-6 mb-2">{client.email}</div>
<div className="col-sm-6 mb-2">
<strong>Contact Number:</strong>
</div>
<div className="col-sm-6 mb-2">{client.contactNumber}</div>
<div className="col-sm-6 mb-2">
<strong>Organization Name:</strong>
</div>
<div className="col-sm-6 mb-2">{client.organizationName}</div>
<div className="col-sm-6 mb-2">
<strong>Organization Size:</strong>
</div>
<div className="col-sm-6 mb-2">{client.organizationSize}</div>
<div className="col-sm-6 mb-2">
<strong>Onboarding Date:</strong>
</div>
<div className="col-sm-6">
{formatUTCToLocalTime(client.onBoardingDate)}
</div>
<div className="col-sm-6 mb-2">
<strong>Billing Address:</strong>
</div>
<div className="col-sm-6 mb-2">{client.billingAddress}</div>
<div className="col-sm-6 mb-2">
<strong>Industry ID:</strong>
</div>
<div className="col-sm-6 mb-2">{client.industryId}</div>
{/* <div className="col-sm-6">
<strong>Reference:</strong>
</div>
<div className="col-sm-6">{client.reference}</div> */}
</div>
</div>
))}
</div>
</div>
<div className="col-12 d-flex justify-content-end">
<button
type="submit"
className="btn btn-label-primary d-flex align-items-center me-2"
>
Processed To Payment
</button>
</div>
</div>
);
};
export default ProcessedPayment;

View File

@ -0,0 +1,376 @@
import React, { useState, useMemo } from "react";
import { useForm } from "react-hook-form";
import {
OrganizationDefaultValue,
OrganizationSchema,
} from "../../pages/Home/HomeSchema";
import { zodResolver } from "@hookform/resolvers/zod";
import Label from "../common/Label";
import { orgSize, reference } from "../../utils/constants";
import DatePicker from "../common/DatePicker";
import { useCreateTenant, useIndustries } from "../../hooks/useTenant";
const SubscriptionForm = ({onNext}) => {
const { data, isError, isLoading: industryLoading } = useIndustries();
const {
register,
handleSubmit,
control,
formState: { errors },
reset,
} = useForm({
resolver: zodResolver(OrganizationSchema),
defaultValues: OrganizationDefaultValue,
});
const { mutate: CreateTenant, isPending } = useCreateTenant(() => {
// nextstep
if (onNext) onNext();
});
const onSubmit = (data) => {
CreateTenant(data)
// reset();
};
return (
<div className="container-md">
<div className="row">
<div className="col-12 col-md-6">
<div className="row px-4">
<div className="text-start">
<form onSubmit={handleSubmit(onSubmit)}>
<div className="row">
{/* First Name */}
<div className="col-sm-6 mb-3">
<Label htmlFor="firstName" required>
First Name
</Label>
<input
id="firstName"
type="text"
className="form-control form-control-sm"
{...register("firstName")}
/>
{errors.firstName && (
<div className="danger-text">
{errors.firstName.message}
</div>
)}
</div>
{/* Last Name */}
<div className="col-sm-6 mb-3">
<Label htmlFor="lastName" required>
Last Name
</Label>
<input
id="lastName"
type="text"
className="form-control form-control-sm"
{...register("lastName")}
/>
{errors.lastName && (
<div className="danger-text">
{errors.lastName.message}
</div>
)}
</div>
{/* Email */}
<div className="col-sm-6 mb-3">
<Label htmlFor="email" required>
Email
</Label>
<input
id="email"
type="email"
className="form-control form-control-sm"
{...register("email")}
/>
{errors.email && (
<div className="danger-text">{errors.email.message}</div>
)}
</div>
{/* Contact Number */}
<div className="col-sm-6 mb-3">
<Label htmlFor="contactNumber" required>
Contact Number
</Label>
<input
id="contactNumber"
type="text"
className="form-control form-control-sm"
{...register("contactNumber")}
inputMode="tel"
placeholder="+91 9876543210"
/>
{errors.contactNumber && (
<div className="danger-text">
{errors.contactNumber.message}
</div>
)}
</div>
{/* Billing Address */}
<div className="col-12 mb-3">
<Label htmlFor="billingAddress" required>
Billing Address
</Label>
<textarea
id="billingAddress"
className="form-control"
{...register("billingAddress")}
rows={3}
/>
{errors.billingAddress && (
<div className="danger-text">
{errors.billingAddress.message}
</div>
)}
</div>
{/* Organization Name */}
<div className="col-sm-6 mb-3">
<Label htmlFor="organizationName" required>
Organization Name
</Label>
<input
id="organizationName"
className="form-control form-control-sm"
{...register("organizationName")}
/>
{errors.organizationName && (
<div className="danger-text">
{errors.organizationName.message}
</div>
)}
</div>
{/* Onboarding Date */}
<div className="col-sm-6 mb-3">
<Label htmlFor="onBoardingDate" required>
Onboarding Date
</Label>
<DatePicker
name="onBoardingDate"
control={control}
placeholder="DD-MM-YYYY"
maxDate={new Date()}
/>
{errors.onBoardingDate && (
<div className="invalid-feedback">
{errors.onBoardingDate.message}
</div>
)}
</div>
{/* Organization Size */}
<div className="col-sm-6 mb-3">
<Label htmlFor="organizationSize" required>
Organization Size
</Label>
<select
id="organizationSize"
className="form-select shadow-none border py-1 px-2"
style={{ fontSize: "0.875rem" }}
{...register("organizationSize", {
required: "Organization size is required",
})}
>
{orgSize.map((org) => (
<option key={org.val} value={org.val}>
{org.name}
</option>
))}
</select>
{errors.organizationSize && (
<div className="danger-text">
{errors.organizationSize.message}
</div>
)}
</div>
{/* Industry */}
<div className="col-sm-6 mb-3">
<Label htmlFor="industryId" required>
Industry
</Label>
<select
id="industryId"
className="form-select shadow-none border py-1 px-2 small"
{...register("industryId")}
>
{industryLoading ? (
<option value="">Loading...</option>
) : (
data?.map((indu) => (
<option key={indu.id} value={indu.id}>
{indu.name}
</option>
))
)}
</select>
{errors.industryId && (
<div className="danger-text">
{errors.industryId.message}
</div>
)}
</div>
{/* Reference */}
<div className="col-sm-6 mb-3">
<Label htmlFor="reference" required>
Reference
</Label>
<select
id="reference"
className="form-select shadow-none border py-1 px-2 small"
{...register("reference")}
>
{reference.map((org) => (
<option key={org.val} value={org.val}>
{org.name}
</option>
))}
</select>
{errors.reference && (
<div className="danger-text">
{errors.reference.message}
</div>
)}
</div>
</div>
<div className="d-flex justify-content-end mb-3">
<button
type="submit"
className="btn btn-label-primary d-flex align-items-center me-2"
>
{isPending ? (
"Please Wait..."
) : (
<>
<span className="me-1">Next</span>
<i className="bx bx-chevron-right"></i>
</>
)}
</button>
</div>
</form>
</div>
</div>
</div>
<div className="d-none d-md-block col-md-6">
{/* <div
id="carouselExample "
className="carousel slide col-md-8 offset-md-2 modal-min-h"
data-bs-ride="carousel"
>
<div class="carousel-indicators">
<button
type="button"
data-bs-target="#carouselExample"
data-bs-slide-to="0"
class="active"
></button>
<button
type="button"
data-bs-target="#carouselExample"
data-bs-slide-to="1"
></button>
<button
type="button"
data-bs-target="#carouselExample"
data-bs-slide-to="2"
></button>
</div>
<div className="carousel-inner">
<div className="carousel-item active">
<img
className="d-block w-100"
src="../../assets/img/elements/6.png"
alt="First slide"
/>
<div className="carousel-caption d-none d-md-block">
<h3>First slide</h3>
<p>
Eos mutat malis maluisset et, agam ancillae quo te, in vim
congue pertinacia.
</p>
</div>
</div>
<div className="carousel-item">
<img
className="d-block w-100"
src="../../assets/img/elements/7.png"
alt="Second slide"
/>
<div className="carousel-caption d-none d-md-block">
<h3>Second slide</h3>
<p>In numquam omittam sea.</p>
</div>
</div>
<div className="carousel-item">
<img
className="d-block w-100"
src="../../assets/img/elements/8.png"
alt="Third slide"
/>
<div className="carousel-caption d-none d-md-block">
<h3>Third slide</h3>
<p>
Lorem ipsum dolor sit amet, virtute consequat ea qui, minim
graeco mel no.
</p>
</div>
</div>
</div>
<a
className="carousel-control-prev"
href="#carouselExample"
role="button"
data-bs-slide="prev"
>
<span
className="carousel-control-prev-icon"
aria-hidden="true"
></span>
<span className="visually-hidden">Previous</span>
</a>
<a
className="carousel-control-next"
href="#carouselExample"
role="button"
data-bs-slide="next"
>
<span
className="carousel-control-next-icon"
aria-hidden="true"
></span>
<span className="visually-hidden">Next</span>
</a>
</div> */}
<div>
<p>
Provide organization information including name, size, industry,
and contact details.
</p>
</div>
</div>
</div>
</div>
);
};
export default SubscriptionForm;

View File

@ -0,0 +1,53 @@
import React from "react";
const SubscriptionLayout = ({ configStep = [], currentStep ,setCurrentStep }) => {
return (
<div className="stepper-container my-4 ">
<ul className="timeline-horizontal list-unstyled d-flex justify-content-between align-items-center position-relative w-100">
{configStep.map((step, index) => (
<li
key={step.name}
className="timeline-item text-center flex-fill position-relative"
>
<div
className={`timeline-point mx-auto mb-2 ${
index + 1 < currentStep
? "completed"
: index + 1 === currentStep
? "active"
: ""
}`}
>
{index + 1 < currentStep ? <i className='bx bx-check'></i>: index + 1 }
</div>
<h6
className={`fw-semibold mb-0 ${
index + 1 < currentStep
? "text-success"
: index + 1 === currentStep
? "text-primary"
: "text-muted"
}`}
>
{step.name}
</h6>
{index !== configStep.length - 1 && (
<div className={`timeline-line-horizontal `}></div>
)}
</li>
))}
</ul>
<div className="step-content mt-4">
{configStep[currentStep - 1]?.component()}
</div>
</div>
);
};
export default SubscriptionLayout;

View File

@ -14,8 +14,6 @@ import {
} from "../slices/localVariablesSlice.jsx"; } from "../slices/localVariablesSlice.jsx";
import { removeSession } from "../utils/authUtils.js"; import { removeSession } from "../utils/authUtils.js";
// ----------------------------Modal-------------------------- // ----------------------------Modal--------------------------
export const useModal = (modalType) => { export const useModal = (modalType) => {
@ -31,6 +29,16 @@ export const useModal = (modalType) => {
return { isOpen, onOpen, onClose, onToggle }; return { isOpen, onOpen, onClose, onToggle };
}; };
export const useSubscription = (frequency) => {
return useQuery({
queryKey: ["subscriptionPlans", frequency],
queryFn: async () => {
const resp = await AuthRepository.getSubscription(frequency);
return resp.data;
},
});
};
export const useTenants = () => { export const useTenants = () => {
return useQuery({ return useQuery({
queryKey: ["tenantlist"], queryKey: ["tenantlist"],
@ -79,18 +87,21 @@ export const useAuthModal = () => {
}; };
}; };
export const useLogout = () => { export const useLogout = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: async () => { mutationFn: async () => {
let payload = { refreshToken: localStorage.getItem("refreshToken") || sessionStorage.getItem("refreshToken") }; let payload = {
refreshToken:
localStorage.getItem("refreshToken") ||
sessionStorage.getItem("refreshToken"),
};
return await AuthRepository.logout(payload); return await AuthRepository.logout(payload);
}, },
onSuccess: (data) => { onSuccess: (data) => {
removeSession() removeSession();
window.location.href = "/auth/login"; window.location.href = "/auth/login";
if (onSuccessCallBack) onSuccessCallBack(); if (onSuccessCallBack) onSuccessCallBack();
@ -98,7 +109,7 @@ export const useLogout = () => {
onError: (error) => { onError: (error) => {
showToast(error.message || "Error while creating project", "error"); showToast(error.message || "Error while creating project", "error");
removeSession() removeSession();
}, },
}); });
}; };

View File

@ -72,7 +72,7 @@ export const useSubscriptionPlan = (freq) => {
// ------------Mutation--------------------- // ------------Mutation---------------------
export const useCreateTenant = (onSuccessCallback) => { export const useCreateTenant = (onSuccessCallback) => {
const clinet = queryClient() const clinet = useQueryClient()
const dispatch = useDispatch(); const dispatch = useDispatch();
return useMutation({ return useMutation({
mutationFn: async (tenantPayload) => { mutationFn: async (tenantPayload) => {
@ -88,7 +88,7 @@ export const useCreateTenant = (onSuccessCallback) => {
} else if (data && !data.subscriptionHistery) { } else if (data && !data.subscriptionHistery) {
operationMode = 2; // tenant exists but subscription not added yet operationMode = 2; // tenant exists but subscription not added yet
} }
debugger
clinet.invalidateQueries({queryKey:["Tenants"]}) clinet.invalidateQueries({queryKey:["Tenants"]})

View File

@ -18,7 +18,7 @@ export const OrganizationSchema = z.object({
.refine((d) => !Number.isNaN(d.getTime()), { message: "Invalid date" }), .refine((d) => !Number.isNaN(d.getTime()), { message: "Invalid date" }),
organizationSize: z.string().min(1, "Organization Size is required"), organizationSize: z.string().min(1, "Organization Size is required"),
industryId: z.string().uuid("Industry is required"), industryId: z.string().uuid("Industry is required"),
reference: z.string().optional(), reference: z.string().min(1,{message:"Reference is required"}),
}); });

View File

@ -6,6 +6,7 @@ import DashboardImage from "/img/hero/bg-01.jpg";
import { Swiper, SwiperSlide } from "swiper/react"; import { Swiper, SwiperSlide } from "swiper/react";
import { EffectFlip, Autoplay, Pagination, Navigation } from "swiper/modules"; import { EffectFlip, Autoplay, Pagination, Navigation } from "swiper/modules";
import SwaperSlideImages from "./SwaperSlideImages"; import SwaperSlideImages from "./SwaperSlideImages";
import SubscriptionPlans from "./SubscriptionPlans";
const LandingPage = () => { const LandingPage = () => {
return ( return (
@ -294,7 +295,7 @@ const LandingPage = () => {
No matter which plan you choose, youll get access to powerful No matter which plan you choose, youll get access to powerful
features. Choose the best plan to fit your needs. features. Choose the best plan to fit your needs.
</p> </p>
<div className="row g-4 justify-content-center"> {/* <div className="row g-4 justify-content-center">
<div className="col-md-4"> <div className="col-md-4">
<div className="card pricing-card border-0 shadow-sm"> <div className="card pricing-card border-0 shadow-sm">
<div className="card-body"> <div className="card-body">
@ -352,7 +353,8 @@ const LandingPage = () => {
</div> </div>
</div> </div>
</div> </div>
</div> </div> */}
<SubscriptionPlans/>
</div> </div>
</section> </section>
{/* <!-- About --> */} {/* <!-- About --> */}

View File

@ -1,306 +1,31 @@
import React, { useState } from "react"; import React, { useState, useMemo } from "react";
import { useForm } from "react-hook-form";
import { OrganizationDefaultValue, OrganizationSchema } from "./HomeSchema"; import SubscriptionLayout from "../../components/UserSubscription/SubscriptionLayout";
import { zodResolver } from "@hookform/resolvers/zod"; import SubscriptionForm from "../../components/UserSubscription/SubscriptionForm";
import Label from "../../components/common/Label"; import ProcessedPayment from "../../components/UserSubscription/ProcessedPayment";
import { orgSize, reference } from "../../utils/constants";
import DatePicker from "../../components/common/DatePicker";
import { useIndustries } from "../../hooks/useTenant";
const MakeSubscription = () => { const MakeSubscription = () => {
const [selectedPlan, setSelectedPlan] = useState("basic"); const [currentStep, setCurrentStep] = useState(2);
const checkOut_Steps = [
const handleChange = (e) => {
const value = e.target.value;
setSelectedPlan(value);
};
const { data, isError, isLoading: industryLoading } = useIndustries();
const options = [
{ {
id: 1, name: "Client Info",
title: "Basic", component: () => <SubscriptionForm onNext={() => setCurrentStep(prev => prev + 1)}/>,
value: "basic", // Added value
price: "Free",
description: "Get 1 project with 1 team member.",
}, },
{ {
id: 2, name: "Payment",
title: "Pro", component: () =><ProcessedPayment onNext={() => setCurrentStep(prev => prev + 1)}/>,
value: "pro", // Added value
price: "$10/mo",
description: "Up to 10 projects and 5 team members.",
}, },
{ {
id: 3, name: "Verified",
title: "Enterprise", component: () => <div>Verified</div>,
value: "enterprise", // Added value
price: "$30/mo",
description: "Unlimited projects and team members.",
}, },
]; ];
const {
register,
handleSubmit,
control,
formState: { errors },
reset,
} = useForm({
resolver: zodResolver(OrganizationSchema),
defaultValues: OrganizationDefaultValue,
});
const onSubmit = (data) => {
console.log("Form Submitted:", data);
alert("Form submitted successfully!");
reset();
};
return ( return (
<div className="container-fluid bg-light-secondary min-vh-100"> <div className="container-fluid bg-light-secondary min-vh-100 p-2">
<div className="row g-3"> <SubscriptionLayout configStep={checkOut_Steps}
<div className="col-12 col-md-6"> currentStep={currentStep}
<div className="row px-4"> setCurrentStep={setCurrentStep}/>
<div className="text-start ">
{/* <h4 className="mb-3 text-primary">Organization Onboarding Form</h4> */}
<form onSubmit={handleSubmit(onSubmit)}>
<div className="row">
<div className="col-sm-6 mb-3">
<Label htmlFor="firstName" required>
First Name
</Label>
<input
id="firstName"
type="text"
className={`form-control form-control-sm`}
{...register("firstName")}
/>
{errors.firstName && (
<div className="danger-text">
{errors.firstName.message}
</div>
)}
</div>
<div className="col-sm-6 mb-3">
<Label htmlFor="lastName" required>
Last Name
</Label>
<input
id="lastName"
type="text"
className={`form-control form-control-sm `}
{...register("lastName")}
/>
{errors.lastName && (
<div className="danger-text">
{errors.lastName.message}
</div>
)}
</div>
<div className="col-sm-6 mb-3">
<Label htmlFor="email" required>
Email
</Label>
<input
id="email"
type="email"
className={`form-control form-control-sm `}
{...register("email")}
/>
{errors.email && (
<div className="danger-text">{errors.email.message}</div>
)}
</div>
<div className="col-sm-6 mb-3">
<Label htmlFor="contactNumber" required>
Contact Number
</Label>
<input
id="contactNumber"
type="text"
className={`form-control form-control-sm `}
{...register("contactNumber")}
inputMode="tel"
placeholder="+91 9876543210"
/>
{errors.contactNumber && (
<div className="danger-text">
{errors.contactNumber.message}
</div>
)}
</div>
<div className="col-12 mb-3">
<Label htmlFor="billingAddress" required>
Billing Address
</Label>
<textarea
id="billingAddress"
className={`form-control `}
{...register("billingAddress")}
rows={3}
/>
{errors.billingAddress && (
<div className="danger-text">
{errors.billingAddress.message}
</div>
)}
</div>
<div className="col-sm-6 mb-3">
<Label htmlFor="organizationName" required>
Organization Name
</Label>
<input
id="organizationName"
className={`form-control form-control-sm `}
{...register("organizationName")}
/>
{errors.organizationName && (
<div className="danger-text">
{errors.organizationName.message}
</div>
)}
</div>
<div className="col-sm-6 mb-3">
<Label htmlFor="onBoardingDate" required>
Onboarding Date
</Label>
<DatePicker
name="onBoardingDate"
control={control}
placeholder="DD-MM-YYYY"
maxDate={new Date()}
/>
{errors.onBoardingDate && (
<div className="invalid-feedback">
{errors.onBoardingDate.message}
</div>
)}
</div>
<div className="col-sm-6 mb-3">
<Label htmlFor="organizationSize" required>
Organization Size
</Label>
<select
id="organizationSize"
className="form-select shadow-none border py-1 px-2"
style={{ fontSize: "0.875rem" }} // Bootstrap's small text size
{...register("organizationSize", {
required: "Organization size is required",
})}
>
{orgSize.map((org) => (
<option key={org.val} value={org.val}>
{org.name}
</option>
))}
</select>
{errors.organizationSize && (
<div className="danger-text">
{errors.organizationSize.message}
</div>
)}
</div>
<div className="col-sm-6 mb-3">
<Label htmlFor="industryId" required>
Industry
</Label>
<select
id="industryId"
className="form-select shadow-none border py-1 px-2 small"
{...register("industryId")}
>
{industryLoading ? (
<option value="">Loading...</option>
) : (
data?.map((indu) => (
<option key={indu.id} value={indu.id}>
{indu.name}
</option>
))
)}
</select>
{errors.industryId && (
<div className="danger-text">
{errors.industryId.message}
</div>
)}
</div>
<div className="col-sm-6 mb-3">
<Label htmlFor="reference" required>
Reference
</Label>
<select
id="reference"
className="form-select shadow-none border py-1 px-2 small"
{...register("reference")}
>
{reference.map((org) => (
<option key={org.val} value={org.val}>
{org.name}
</option>
))}
</select>
{errors.reference && (
<div className="danger-text">
{errors.reference.message}
</div>
)}
</div>
</div>
<div className="text-end mb-3">
<button
type="submit"
className="btn btn-label-primary d-flex align-items-center me-2"
>
<span className="me-1">Next</span>
<i className="bx bx-chevron-right"></i>
</button>
</div>
</form>
</div>
</div>
</div>
<div className="col-12 col-md-6 ">
<div className="row">
{options.map((opt) => (
<div key={opt.id} className="col-md-4 mb-md-3 mb-2">
<div className={`form-check custom-option custom-option-basic text-start w-100 bg-light-primary ${selectedPlan === opt.value ? "border border-primary shadow-md":""}`}>
<label
className="form-check-label custom-option-content w-100"
htmlFor={`customRadioTemp${opt.id}`}
>
<input
name="customRadioTemp"
className="form-check-input"
type="radio"
value={opt.value}
id={`customRadioTemp${opt.id}`}
checked={selectedPlan === opt.value}
onChange={handleChange}
/>
<span className="custom-option-header d-flex justify-content-between align-items-center">
<span className="h6 mb-0">{opt.title}</span>
<span>{opt.price}</span>
</span>
<span className="custom-option-body d-block mt-1">
<small>{opt.description}</small>
</span>
</label>
</div>
</div>
))}
</div>
</div>
</div>
</div> </div>
); );
}; };

View File

@ -2,62 +2,20 @@ import React, { useState, useEffect } from "react";
import axios from "axios"; import axios from "axios";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import PlanCardSkeleton from "./PlanCardSkeleton"; import PlanCardSkeleton from "./PlanCardSkeleton";
import { useSubscription } from "../../hooks/useAuth";
import { frequencyLabel } from "../../utils/appUtils";
const SubscriptionPlans = () => { const SubscriptionPlans = () => {
const [plans, setPlans] = useState([]); const [plans, setPlans] = useState([]);
const [frequency, setFrequency] = useState(1); const [frequency, setFrequency] = useState(1);
const { data, isLoading, isError, error } = useSubscription(frequency);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
useEffect(() => {
const fetchPlans = async () => {
try {
setLoading(true);
const res = await axios.get(
`http://localhost:5032/api/market/list/subscription-plan?frequency=${frequency}`,
{ headers: { "Content-Type": "application/json" } }
);
setPlans(res.data?.data || []);
} catch (err) {
console.error("Error fetching plans:", err);
} finally {
setLoading(false);
}
};
fetchPlans();
}, [frequency]);
const frequencyLabel = (freq) => {
switch (freq) {
case 0:
return "1 mo";
case 1:
return "3 mo";
case 2:
return "6 mo";
case 3:
return "1 yr";
default:
return "mo";
}
};
return ( return (
<div className="container py-5"> <div className="container py-5">
<h4 className="text-center mb-1">
<span className="position-relative fw-extrabold z-1 me-2">
Tailored pricing plans
{/* <img
src="./../../public/img/icons/section-title-icon.png"
alt="laptop charging"
className="section-title-img position-absolute object-fit-contain bottom-0 z-n1"
/> */}
</span>
designed for you
</h4>
<p className="text-center pb-2 mb-7">
No matter which plan you choose, youll get access to powerful features.{" "}
<strong>Choose the best plan to fit your needs.</strong>
</p>
{/* Frequency Switcher */} {/* Frequency Switcher */}
<div className="text-center mb-4"> <div className="text-center mb-4">
<div className="btn-group" role="group" aria-label="Plan frequency"> <div className="btn-group" role="group" aria-label="Plan frequency">
@ -80,17 +38,22 @@ const SubscriptionPlans = () => {
{/* Cards */} {/* Cards */}
<div className="row g-4 mt-10"> <div className="row g-4 mt-10">
{loading ? ( {isLoading ? (
// Show 3 skeletons // Show 3 skeletons
<> <>
<PlanCardSkeleton /> <PlanCardSkeleton />
<PlanCardSkeleton /> <PlanCardSkeleton />
<PlanCardSkeleton /> <PlanCardSkeleton />
</> </>
) : plans.length === 0 ? ( ) : data.length === 0 ? (
<div className="text-center">No plans found</div> <div className="text-center">No plans found</div>
) : isError ? (
<div className="text-start bg-light">
<p>{error.message}</p>
<p>{error.name}</p>
</div>
) : ( ) : (
plans.map((plan) => ( data.map((plan) => (
<div key={plan.id} className="col-xl-4 col-lg-6 col-md-6"> <div key={plan.id} className="col-xl-4 col-lg-6 col-md-6">
<div className="card h-100 shadow-lg border-0 p-3 text-center p-10"> <div className="card h-100 shadow-lg border-0 p-3 text-center p-10">
{/* Header */} {/* Header */}
@ -149,13 +112,20 @@ const SubscriptionPlans = () => {
{/* Button */} {/* Button */}
<div className="mt-auto"> <div className="mt-auto">
<Link
to={`/auth/subscripe/${frequency}/${plan.planName}`}
className="btn btn-outline-primary w-100 fw-bold mb-2"
>
Subscribe
</Link>
<Link <Link
to="/auth/reqest/demo" to="/auth/reqest/demo"
className="btn btn-outline-primary w-100 fw-bold" className="btn btn-outline-primary w-100 fw-bold"
> >
Request a Demo Request a Demo
</Link> </Link>
</div> </div>
</div> </div>
</div> </div>
)) ))

View File

@ -10,15 +10,16 @@ const AuthRepository = {
verifyOTP: (data) => api.postPublic("/api/auth/login-otp/v1", data), verifyOTP: (data) => api.postPublic("/api/auth/login-otp/v1", data),
register: (data) => api.postPublic("/api/auth/register", data), register: (data) => api.postPublic("/api/auth/register", data),
sendMail: (data) => api.postPublic("/api/auth/sendmail", data), sendMail: (data) => api.postPublic("/api/auth/sendmail", data),
getSubscription: (frequency) =>
api.getPublic(`/api/market/list/subscription-plan?frequency=${frequency}`),
// Protected routes (require auth token) // Protected routes (require auth token)
logout: (data) => api.post("/api/auth/logout", data), logout: (data) => api.post("/api/auth/logout", data),
profile: () => api.get("/api/user/profile"), profile: () => api.get("/api/user/profile"),
changepassword: (data) => api.post("/api/auth/change-password", data), changepassword: (data) => api.post("/api/auth/change-password", data),
appmenu: () => api.get('/api/appmenu/get/menu'), appmenu: () => api.get("/api/appmenu/get/menu"),
selectTenant: (tenantId) => api.post(`/api/Auth/select-tenant/${tenantId}`), selectTenant: (tenantId) => api.post(`/api/Auth/select-tenant/${tenantId}`),
getTenantList: () => api.get("/api/Auth/get/user/tenants"), getTenantList: () => api.get("/api/Auth/get/user/tenants"),
}; };
export default AuthRepository; export default AuthRepository;

View File

@ -74,7 +74,7 @@ const router = createBrowserRouter(
], ],
}, },
{ path: "/auth/switch/org", element: <TenantSelectionPage /> }, { path: "/auth/switch/org", element: <TenantSelectionPage /> },
{ path: "/request", element: <MakeSubscription /> }, { path: "/auth/subscripe/:frequency/:planName", element: <MakeSubscription /> },
{ {
element: <ProtectedRoute />, element: <ProtectedRoute />,
errorElement: <ErrorPage />, errorElement: <ErrorPage />,

View File

@ -135,3 +135,18 @@ export const formatFigure = (
return new Intl.NumberFormat(locale, formatterOptions).format(amount); return new Intl.NumberFormat(locale, formatterOptions).format(amount);
}; };
export const frequencyLabel = (freq) => {
switch (freq) {
case 0:
return "1 mo";
case 1:
return "3 mo";
case 2:
return "6 mo";
case 3:
return "1 yr";
default:
return "mo";
}
};

View File

@ -73,15 +73,16 @@ axiosClient.interceptors.response.use(
if (status === 401 && !isRefreshRequest) { if (status === 401 && !isRefreshRequest) {
originalRequest._retry = true; originalRequest._retry = true;
const refreshToken = localStorage.getItem("refreshToken") || sessionStorage.getItem("refreshToken"); const refreshToken =
localStorage.getItem("refreshToken") ||
sessionStorage.getItem("refreshToken");
if ( if (
!refreshToken || !refreshToken ||
error.response.data?.errors === "Invalid or expired refresh token." error.response.data?.errors === "Invalid or expired refresh token."
) { ) {
redirectToLogin(); redirectToLogin();
removeSession() removeSession();
return Promise.reject(error); return Promise.reject(error);
} }
@ -90,7 +91,9 @@ axiosClient.interceptors.response.use(
try { try {
// Refresh token call // Refresh token call
const res = await axiosClient.post("/api/Auth/refresh-token", { const res = await axiosClient.post("/api/Auth/refresh-token", {
token: localStorage.getItem("jwtToken") || sessionStorage.getItem("jwtToken"), token:
localStorage.getItem("jwtToken") ||
sessionStorage.getItem("jwtToken"),
refreshToken, refreshToken,
}); });
@ -110,7 +113,7 @@ axiosClient.interceptors.response.use(
originalRequest.headers["Authorization"] = `Bearer ${token}`; originalRequest.headers["Authorization"] = `Bearer ${token}`;
return axiosClient(originalRequest); return axiosClient(originalRequest);
} catch (refreshError) { } catch (refreshError) {
removeSession() removeSession();
redirectToLogin(); redirectToLogin();
return Promise.reject(refreshError); return Promise.reject(refreshError);
} }
@ -148,6 +151,11 @@ export const api = {
headers: { ...customHeaders }, headers: { ...customHeaders },
authRequired: false, authRequired: false,
}), }),
getPublic: (url, data = {}, customHeaders = {}) =>
apiRequest("get", url, data, {
headers: { ...customHeaders },
authRequired: false,
}),
// Authenticated routes // Authenticated routes
get: (url, params = {}, customHeaders = {}) => get: (url, params = {}, customHeaders = {}) =>