All implemaentation and 360deg tour
This commit is contained in:
78
src/redux/hooks/useAnimatedCounter.tsx
Normal file
78
src/redux/hooks/useAnimatedCounter.tsx
Normal 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
|
||||
};
|
||||
}
|
||||
82
src/redux/services/aboutUsApi.ts
Normal file
82
src/redux/services/aboutUsApi.ts
Normal 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;
|
||||
13
src/redux/services/baseQuery.ts
Normal file
13
src/redux/services/baseQuery.ts
Normal 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;
|
||||
126
src/redux/services/blogApi.ts
Normal file
126
src/redux/services/blogApi.ts
Normal 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;
|
||||
39
src/redux/services/contactUsApi.ts
Normal file
39
src/redux/services/contactUsApi.ts
Normal 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;
|
||||
44
src/redux/services/faqApi.ts
Normal file
44
src/redux/services/faqApi.ts
Normal 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;
|
||||
92
src/redux/services/homepageApi.ts
Normal file
92
src/redux/services/homepageApi.ts
Normal 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
27
src/redux/store/Store.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user