All implemaentation and 360deg tour

This commit is contained in:
priyanshuvish
2026-03-20 19:43:27 +05:30
parent 3e1f2ca425
commit 6f72f1c828
38 changed files with 2759 additions and 1460 deletions

View File

@@ -0,0 +1,78 @@
import { useState, useEffect, useRef } from 'react';
interface UseAnimatedCounterOptions {
start?: number;
end: number;
duration?: number;
decimals?: number;
suffix?: string;
}
export function useAnimatedCounter({
start = 0,
end,
duration = 2000,
decimals = 0,
suffix = ''
}: UseAnimatedCounterOptions) {
const [count, setCount] = useState(start);
const [isInView, setIsInView] = useState(false);
const countRef = useRef<HTMLSpanElement>(null);
const frameRef = useRef<number>();
const startTimeRef = useRef<number>();
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && !isInView) {
setIsInView(true);
}
},
{ threshold: 0.1 }
);
if (countRef.current) {
observer.observe(countRef.current);
}
return () => observer.disconnect();
}, [isInView]);
useEffect(() => {
if (!isInView) return;
const animate = (currentTime: number) => {
if (!startTimeRef.current) startTimeRef.current = currentTime;
const elapsed = currentTime - startTimeRef.current;
const progress = Math.min(elapsed / duration, 1);
// Easing function for smooth animation
const easeOutQuart = 1 - Math.pow(1 - progress, 4);
const currentCount = start + (end - start) * easeOutQuart;
setCount(currentCount);
if (progress < 1) {
frameRef.current = requestAnimationFrame(animate);
}
};
frameRef.current = requestAnimationFrame(animate);
return () => {
if (frameRef.current) {
cancelAnimationFrame(frameRef.current);
}
};
}, [isInView, start, end, duration]);
const formattedCount = decimals > 0
? count.toFixed(decimals)
: Math.floor(count).toLocaleString();
return {
count: formattedCount + suffix,
ref: countRef
};
}

View File

@@ -0,0 +1,82 @@
import { createApi } from "@reduxjs/toolkit/query/react";
import baseQueryWithReauth from "./baseQuery";
export interface HeroSection {
id: string;
background_image_url: string;
background_image_alt_text: string;
headline: string;
subtext: string;
cta_text: string;
cta_destination: string;
}
export interface HowWeWorkItem {
id: string;
title: string;
description: string;
image_url: string;
display_order: number;
}
export interface StatItem {
id: string;
number: number;
suffix: string;
label: string;
display_order: number;
}
export interface TeamMember {
id: string;
display_order: number;
name_role: string;
photo_url: string;
alt_text: string;
bio: string;
}
export interface AboutUsData {
hero_section: HeroSection;
our_promise_title: string;
how_we_work_title: string;
who_we_are_title: string;
our_team_title: string;
how_we_work: HowWeWorkItem[];
stat_section: StatItem[];
our_team: TeamMember[];
}
export interface AboutUsResponse {
success: boolean;
status: number;
message: string;
data: AboutUsData;
}
export const aboutUsApi = createApi({
reducerPath: "aboutUsApi",
baseQuery: baseQueryWithReauth,
tagTypes: ["AboutUs"],
endpoints: (builder) => ({
// ✅ GET About Us
getAboutUs: builder.query<AboutUsData, void>({
query: () => ({
url: "/admin/about-us",
method: "GET",
}),
// 🔥 extract only useful data
transformResponse: (response: AboutUsResponse) => response.data,
providesTags: ["AboutUs"],
}),
}),
});
export const {
useGetAboutUsQuery,
} = aboutUsApi;

View File

@@ -0,0 +1,13 @@
import { fetchBaseQuery } from '@reduxjs/toolkit/query/react';
const rawBaseQuery = fetchBaseQuery({
baseUrl: import.meta.env.VITE_API_URL,
});
const baseQueryWithReauth = async (args: any, api: any, extraOptions: any) => {
const result = await rawBaseQuery(args, api, extraOptions);
return result;
};
export default baseQueryWithReauth;

View File

@@ -0,0 +1,126 @@
import { createApi } from "@reduxjs/toolkit/query/react";
import baseQueryWithReauth from "./baseQuery";
export interface BlogTag {
id: string;
blog_xid: string;
tag_name: string;
display_order: number;
}
export interface BlogItem {
id: string;
content_category: string;
content_type: string;
title: string;
slug_name: string;
content: string;
banner_img: string;
meta_title: string;
meta_description: string;
content_status: string;
updated_at: string;
blog_tags: BlogTag[];
short_description: string | null;
content_category_id?: string; // Add this field to store the category ID
}
export interface Pagination {
limit: number;
offset: number;
total: number;
}
export interface BlogListResponse {
success: boolean;
status: number;
message: string;
data: {
pagination: Pagination;
items: BlogItem[];
};
errors: any;
correlation_id: string;
}
export interface BlogListParams {
limit?: number;
offset?: number;
search?: string;
content_status?: string;
content_type?: string;
date_range?:
| "all_time"
| "last_7_days"
| "last_30_days"
| "last_3_months"
| "last_6_months";
sort_by?: "most_recent" | "oldest_first" | "title_az";
content_category_id?: string; // Changed from category to content_category_id
tag_id?: string;
}
export interface BlogByIdResponse {
success: boolean;
status: number;
message: string;
data: BlogItem;
errors: any;
correlation_id: string;
}
export const blogApi = createApi({
reducerPath: "blogApi",
baseQuery: baseQueryWithReauth,
tagTypes: ["blog"],
endpoints: (builder) => ({
// ✅ GET BLOGS LIST
getBlogs: builder.query<BlogListResponse, BlogListParams>({
query: ({
limit = 10,
offset = 0,
search,
content_status = "publish",
content_type,
date_range,
sort_by = "most_recent",
content_category_id,
tag_id,
}) => {
// Build params object
const params: Record<string, any> = {
limit,
offset,
sort_by,
};
// Only add params if they have values
if (search) params.search = search;
if (content_status) params.content_status = content_status;
if (content_type) params.content_type = content_type;
if (date_range) params.date_range = date_range;
if (content_category_id)
params.content_category_id = content_category_id; // Send UUID
if (tag_id && tag_id !== "all") params.tag_id = tag_id;
return {
url: "/admin/blogs/list",
method: "GET",
params,
};
},
providesTags: ["blog"],
}),
getBlogByID: builder.query<BlogItem, string>({
query: (id) => ({
url: `/admin/blogs/list/${id}`,
method: "GET",
}),
transformResponse: (response: BlogByIdResponse) => response.data,
providesTags: ["blog"],
}),
}),
});
export const { useGetBlogsQuery, useGetBlogByIDQuery } = blogApi;

View File

@@ -0,0 +1,39 @@
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
import baseQueryWithReauth from "./baseQuery";
export const contactUsApi = createApi({
reducerPath: "contactUsApi",
baseQuery: baseQueryWithReauth,
tagTypes: ["LeadCategories", "Leads"],
endpoints: (builder) => ({
// GET Lead Categories
getLeadCategories: builder.query({
query: ({ limit = 10, offset = 0, status = "active" }) => ({
url: "admin/prepopulate/lead-categories/list",
params: {
limit,
offset,
status,
},
}),
providesTags: ["LeadCategories"],
}),
// CREATE Lead
createLead: builder.mutation({
query: (body) => ({
url: "admin/leads/create",
method: "POST",
body,
}),
invalidatesTags: ["Leads"],
}),
}),
});
export const {
useGetLeadCategoriesQuery,
useCreateLeadMutation,
} = contactUsApi;

View File

@@ -0,0 +1,44 @@
import { createApi } from "@reduxjs/toolkit/query/react";
import baseQueryWithReauth from "./baseQuery";
export const faqApi = createApi({
reducerPath: "faqApi",
baseQuery: baseQueryWithReauth,
tagTypes: ["Faq", "FaqTags"],
endpoints: (builder) => ({
// GET FAQs LIST
getFaqs: builder.query({
query: ({ limit = 10, offset = 0, search_term, content_status, content_category_xid }) => ({
url: "admin/faq/list",
params: {
limit,
offset,
search_term,
content_status,
content_category_xid,
},
}),
providesTags: ["Faq"],
}),
// GET category TAGS LIST
getFaqCategories: builder.query({
query: ({ limit = 10, offset = 0, search_query }) => ({
url: "admin/prepopulate/content-categories/list",
params: {
limit,
offset,
search_query,
},
}),
providesTags: ["FaqTags"],
}),
}),
});
export const {
useGetFaqsQuery,
useGetFaqCategoriesQuery,
} = faqApi;

View File

@@ -0,0 +1,92 @@
import { createApi } from "@reduxjs/toolkit/query/react";
import baseQueryWithReauth from "./baseQuery";
/* ================= HERO TYPES ================= */
export interface HeroSection {
id: string;
landing_page_type: string;
background_image_url: string;
background_image_alt_text: string;
headline: string;
subtext: string;
cta_text: string;
cta_destination: string;
}
/* ================= STATS TYPES ================= */
export interface StatItem {
id: string;
landing_page_type: string;
number: number;
suffix: string;
label: string;
display_order: number;
}
/* ================= HIGHLIGHT CARD ================= */
export interface HighlightCard {
card_title: string;
icon_url: string;
accessible_label: string;
body_text: string;
display_order: number;
}
/* ================= CTA BAND ================= */
export interface CtaBand {
id: string;
background_image_url: string;
background_image_alt_text: string;
text: string;
cta_text: string;
cta_destination: string;
}
/* ================= RESPONSE ================= */
export interface HomePageResponse {
success: boolean;
status: number;
message: string;
data: {
hero_sections: HeroSection[];
stats_sections: StatItem[];
highlight_cards: HighlightCard[];
cta_bands: CtaBand[];
};
errors: any;
correlation_id: string;
}
/* ================= API ================= */
export const homepageApi = createApi({
reducerPath: "homepageApi",
baseQuery: baseQueryWithReauth,
tagTypes: ["Homepage"],
endpoints: (builder) => ({
getHomepage: builder.query<
HomePageResponse["data"],
{ landing_page_type: "home" | "services" | "about_us" }
>({
query: ({ landing_page_type }) => ({
url: "/admin/home-page/list",
params: { landing_page_type },
}),
transformResponse: (response: HomePageResponse) => response.data,
providesTags: [{ type: "Homepage", id: "LIST" }],
}),
}),
});
/* ================= HOOKS ================= */
export const { useGetHomepageQuery } = homepageApi;

27
src/redux/store/Store.tsx Normal file
View File

@@ -0,0 +1,27 @@
import { configureStore } from "@reduxjs/toolkit";
import { homepageApi } from "../services/homepageApi";
import { faqApi } from "../services/faqApi";
import { contactUsApi } from "../services/contactUsApi";
import { blogApi } from "../services/blogApi";
import { aboutUsApi } from "../services/aboutUsApi";
export const store = configureStore({
reducer: {
[homepageApi.reducerPath]: homepageApi.reducer,
[faqApi.reducerPath]: faqApi.reducer,
[contactUsApi.reducerPath]: contactUsApi.reducer,
[blogApi.reducerPath]: blogApi.reducer,
[aboutUsApi.reducerPath]: aboutUsApi.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(
homepageApi.middleware,
faqApi.middleware,
contactUsApi.middleware,
blogApi.middleware,
aboutUsApi.middleware,
),
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;