home page insight and corses filter implementation

This commit is contained in:
priyanshuvish
2026-03-27 15:37:16 +05:30
parent b907162457
commit 637ca3bc03
4 changed files with 193 additions and 101 deletions

View File

@@ -3,47 +3,36 @@ import { ImageWithFallback } from "./figma/ImageWithFallback";
import { BrandedTag } from "./about/BrandedTag";
import { StandardCTAButton } from "./StandardCTAButton";
import { navigateTo } from "./Router";
import { useGetFeaturedBlogsQuery } from "../redux/services/homepageApi";
import { FullScreenLoader } from "./FullScreenLoader";
import { getSlugWithId } from "../utils/urlHelpers";
interface InsightCard {
id: number;
// Interface for featured blog items from API
interface FeaturedBlog {
id: string;
title: string;
short_description: string | null;
slug_name: string;
banner_img: string | null;
updated_at: string;
content_category: string;
content_type: string;
tags: string[];
featured: boolean;
featured_order: number;
}
// Insight Card Component Interface
interface InsightCardData {
id: string;
title: string;
description?: string;
date: string;
tags: string[];
image: string;
slug?: string;
slug: string;
}
const insightCards: InsightCard[] = [
{
id: 1,
title: "A New Lens on Leadership",
description: "Your leadership calls, and how you interpret opportunities and threats, are influenced by your lenses, which are unique and personal to you.",
date: "16-08-2016",
tags: ["Leadership Lens", "Perspective"],
image: "https://images.unsplash.com/photo-1560550900-5c10828c40aa?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxsZWFkZXJzaGlwJTIwbGVucyUyMHBlcnNwZWN0aXZlfGVufDF8fHx8MTc1OTk5NTg0N3ww&ixlib=rb-4.1.0&q=80&w=1080",
slug: "new-lens-on-leadership"
},
{
id: 2,
title: "Putting Psychometry in perspective",
description: "An in-depth exploration of the limitations and appropriate use of psychometric tools in leadership assessment and talent selection.",
date: "17-12-2016",
tags: ["Psychometry", "Assessment"],
image: "https://images.unsplash.com/photo-1460925895917-afdab827c52f?w=600&h=400&fit=crop",
slug: "putting-psychometry-in-perspective"
},
{
id: 3,
title: "The Busby Way to Talent Management: The Making of \"Busby's Babes\" Part 1",
description: "How Matt Busby transformed Manchester United through visionary talent management and youth development, creating a legacy that shaped modern football leadership.",
date: "05-12-2013",
tags: ["Talent Management", "Youth Development"],
image: "https://images.unsplash.com/photo-1574629810360-7efbbe195018?w=600&h=400&fit=crop",
slug: "busby-way-talent-management-part-1"
}
];
// Insight Tag Component
function InsightTag({ text }: { text: string }) {
return (
@@ -78,7 +67,7 @@ function CalendarIcon() {
);
}
// Explore All Button Component - Updated to redirect to articles page
// Explore All Button Component
function ExploreAllButton() {
return (
<StandardCTAButton
@@ -90,14 +79,15 @@ function ExploreAllButton() {
}
// Large Insight Card Component
function LargeInsightCard({ card }: { card: InsightCard }) {
function LargeInsightCard({ card }: { card: InsightCardData }) {
const handleClick = () => {
if (card.slug) {
navigateTo(`/learning/articles/${card.slug}`);
if (card.slug && card.id) {
const url = getSlugWithId(card.slug, card.id);
navigateTo(`/learning/articles/${url}`);
}
};
const hasLink = !!card.slug;
const hasLink = !!(card.slug && card.id);
return (
<div
@@ -125,8 +115,8 @@ function LargeInsightCard({ card }: { card: InsightCard }) {
<div className="insight-card-white-box absolute bottom-6 left-6 right-6 bg-white rounded-xl p-6 shadow-lg transition-all duration-300 group-hover:shadow-xl">
{/* Top section with tags and arrow */}
<div className="insight-card-header flex items-start justify-between mb-4">
<div className="insight-card-tags flex gap-2">
{card.tags.map((tag, index) => (
<div className="insight-card-tags flex gap-2 flex-wrap">
{card.tags.slice(0, 2).map((tag, index) => (
<InsightTag key={index} text={tag} />
))}
</div>
@@ -158,14 +148,15 @@ function LargeInsightCard({ card }: { card: InsightCard }) {
}
// Small Insight Card Component
function SmallInsightCard({ card }: { card: InsightCard }) {
function SmallInsightCard({ card }: { card: InsightCardData }) {
const handleClick = () => {
if (card.slug) {
navigateTo(`/learning/articles/${card.slug}`);
if (card.slug && card.id) {
const url = getSlugWithId(card.slug, card.id);
navigateTo(`/learning/articles/${url}`);
}
};
const hasLink = !!card.slug;
const hasLink = !!(card.slug && card.id);
return (
<div
@@ -193,8 +184,8 @@ function SmallInsightCard({ card }: { card: InsightCard }) {
<div className="insight-card-white-box absolute bottom-4 left-4 right-4 bg-white rounded-xl p-4 shadow-lg transition-all duration-300 group-hover:shadow-xl">
{/* Top section with tags and arrow */}
<div className="insight-card-header flex items-start justify-between mb-3">
<div className="insight-card-tags flex gap-2">
{card.tags.map((tag, index) => (
<div className="insight-card-tags flex gap-2 flex-wrap">
{card.tags.slice(0, 2).map((tag, index) => (
<span
key={index}
className="px-2.5 py-1 text-xs font-medium rounded-full"
@@ -232,7 +223,97 @@ function SmallInsightCard({ card }: { card: InsightCard }) {
);
}
// Format date function
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
};
export function InsightsSection() {
// Fetch featured blogs from API
const { data: featuredBlogs, isLoading, isError } = useGetFeaturedBlogsQuery({ limit: 3 });
// Transform API data to match InsightCardData format
const transformToInsightCard = (blog: FeaturedBlog): InsightCardData => ({
id: blog.id,
title: blog.title,
description: blog.short_description || undefined,
date: formatDate(blog.updated_at),
tags: blog.tags || [],
image: blog.banner_img || 'https://images.unsplash.com/photo-1481627834876-b7833e8f5570?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&w=1080',
slug: blog.slug_name,
});
// Handle loading state
if (isLoading) {
return (
<section
className="py-24"
style={{
backgroundColor: '#F7F7FD',
paddingTop: '8.75rem',
paddingBottom: '8.75rem'
}}
>
<div className="max-w-7xl mx-auto section-margin-x">
<div className="flex items-center justify-center min-h-[400px]">
<FullScreenLoader text="Loading insights..." />
</div>
</div>
</section>
);
}
// Handle error or no data state
if (isError || !featuredBlogs || featuredBlogs.length === 0) {
return (
<section
className="py-24"
style={{
backgroundColor: '#F7F7FD',
paddingTop: '8.75rem',
paddingBottom: '8.75rem'
}}
>
<div className="max-w-7xl mx-auto section-margin-x">
<BrandedTag text="Leadership Insights" />
<div className="insights-container">
{/* Header */}
<div className="insights-header flex flex-col lg:flex-row lg:items-center lg:justify-between mb-16 gap-8 text-center lg:text-left">
<h2
className="insights-title text-3xl md:text-4xl lg:text-5xl font-bold leading-tight"
style={{ color: 'var(--color-brand-black)' }}
>
Leadership Insights & Ideas
</h2>
<ExploreAllButton />
</div>
{/* Show message when no featured blogs available */}
<div className="text-center py-12">
<p className="text-gray-600">No featured insights available at the moment. Check back soon!</p>
</div>
</div>
</div>
</section>
);
}
// Ensure we have at least 3 blogs for the layout (use placeholders if needed)
const cards = featuredBlogs.map(transformToInsightCard);
// The layout expects: first card (large) + two small cards
const largeCard = cards[0];
const smallCards = cards.slice(1, 3);
// If we don't have enough cards, we can still render with what we have
const hasLargeCard = !!largeCard;
const hasSmallCards = smallCards.length > 0;
return (
<section
className="py-24"
@@ -243,7 +324,6 @@ export function InsightsSection() {
}}
>
<div className="max-w-7xl mx-auto section-margin-x">
{/* Branded Tag */}
<BrandedTag text="Leadership Insights" />
<div className="insights-container">
@@ -261,15 +341,20 @@ export function InsightsSection() {
{/* Main Grid Layout */}
<div className="insights-grid grid grid-cols-1 lg:grid-cols-2 gap-4 md:gap-6 lg:gap-8">
{/* Large Card - Takes full height on left */}
<div className="lg:row-span-2">
<LargeInsightCard card={insightCards[0]} />
</div>
{hasLargeCard && (
<div className="lg:row-span-2">
<LargeInsightCard card={largeCard} />
</div>
)}
{/* Small Cards - Stack on right */}
<div className="flex flex-col gap-4 md:gap-6 lg:gap-8">
<SmallInsightCard card={insightCards[1]} />
<SmallInsightCard card={insightCards[2]} />
</div>
{hasSmallCards && (
<div className="flex flex-col gap-4 md:gap-6 lg:gap-8">
{smallCards.map((card:any, index:any) => (
<SmallInsightCard key={card.id || index} card={card} />
))}
</div>
)}
</div>
</div>
</div>

View File

@@ -53,7 +53,7 @@ export function LearningOnline() {
const [selectedPriceRange, setSelectedPriceRange] = useState('All Prices');
const [selectedDuration, setSelectedDuration] = useState('All Durations');
const [selectedRating, setSelectedRating] = useState('All Ratings');
const [sortBy, setSortBy] = useState('Most Popular');
const [sortBy, setSortBy] = useState('most_popular'); // ✅ Changed to match API value
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
const [currentPage, setCurrentPage] = useState(1);
const coursesPerPage = 9;
@@ -68,14 +68,15 @@ export function LearningOnline() {
offset: 0
});
// ✅ Updated sort options to match backend API values
const sortOptions = [
{ value: 'most_popular', label: 'Most Popular' },
{ value: 'newest', label: 'Newest First' },
{ value: 'title_asc', label: 'Title A-Z' },
{ value: 'price_asc', label: 'Price: Low to High' },
{ value: 'price_desc', label: 'Price: High to Low' },
{ value: 'rating_desc', label: 'Highest Rated' },
{ value: 'duration_asc', label: 'Duration' }
{ value: 'price_low', label: 'Price: Low to High' },
{ value: 'price_high', label: 'Price: High to Low' },
{ value: 'highest_rated', label: 'Highest Rated' },
{ value: 'duration', label: 'Duration' }
];
const priceRanges = [
@@ -112,33 +113,33 @@ export function LearningOnline() {
return cats;
}, [categoriesData]);
// Helper function to convert UI price range to API format
// Helper function to convert UI price range to API format
const getPriceRangeForApi = useCallback((priceRange: string): string | undefined => {
switch (priceRange) {
case 'Under ₹20,000':
return '0-20000';
return 'under_20000';
case '₹20,000 - ₹35,000':
return '20000-35000';
return '20000_35000';
case '₹35,000 - ₹50,000':
return '35000-50000';
return '35000_50000';
case 'Over ₹50,000':
return '50000-999999';
return 'above_50000';
default:
return undefined;
}
}, []);
// Helper function to convert UI duration to API format
// Helper function to convert UI duration to API format
const getDurationForApi = useCallback((duration: string): string | undefined => {
switch (duration) {
case 'Under 6 hours':
return '0-6';
return 'under_6';
case '6-10 hours':
return '6-10';
return '6_10';
case '10-15 hours':
return '10-15';
return '10_15';
case 'Over 15 hours':
return '15-999';
return 'above_15';
default:
return undefined;
}
@@ -158,26 +159,11 @@ export function LearningOnline() {
}
}, []);
// Helper function to convert sort option to API format
// Helper function to convert sort option to API format
const getSortByForApi = useCallback((sort: string): string | undefined => {
switch (sort) {
case 'Most Popular':
return 'popular';
case 'newest':
return 'newest';
case 'title':
return 'title_asc';
case 'price_low':
return 'price_asc';
case 'price_high':
return 'price_desc';
case 'rating':
return 'rating_desc';
case 'duration':
return 'duration_asc';
default:
return undefined;
}
// sort is already in API format (most_popular, newest, title_asc, etc.)
// Just return it as is
return sort;
}, []);
// Build API filters based on current UI state
@@ -304,7 +290,7 @@ export function LearningOnline() {
setSelectedPriceRange('All Prices');
setSelectedDuration('All Durations');
setSelectedRating('All Ratings');
setSortBy('Most Popular');
setSortBy('most_popular'); // ✅ Updated to match API value
};
const hasActiveFilters = searchTerm ||
@@ -361,7 +347,7 @@ export function LearningOnline() {
return (
<div style={{ backgroundColor: '#FFFFFF' }}>
{/* Hero Banner (keep as is) */}
{/* Hero Banner */}
<section className="relative py-16 overflow-hidden">
<div
className="absolute inset-0"
@@ -398,7 +384,7 @@ export function LearningOnline() {
</div>
</section>
{/* Search and Controls Section (keep as is) */}
{/* Search and Controls Section */}
<section className="py-8" style={{ backgroundColor: '#FFFFFF' }}>
<div className="section-margin-x">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-6">
@@ -453,7 +439,7 @@ export function LearningOnline() {
<SelectValue placeholder="Sort by" />
</SelectTrigger>
<SelectContent>
{sortOptions.map((option: any) => (
{sortOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
@@ -597,6 +583,15 @@ export function LearningOnline() {
<p className="text-body-lg text-muted">
No courses found matching your criteria.
</p>
{hasActiveFilters && (
<Button
variant="outline"
onClick={clearAllFilters}
className="mt-4"
>
Clear Filters
</Button>
)}
</div>
) : (
<>
@@ -693,18 +688,17 @@ export function LearningOnline() {
<Button
variant="outline"
size="sm"
onClick={(e: any) => {
onClick={(e: React.MouseEvent) => {
e.stopPropagation();
handleAddToCart({
id: course.id,
title: course.title,
price: course.price,
originalPrice: course.originalPrice,
thumbnail: course.thumbnail,
category: course.category, // ✅ FIX
level: course.level, // ✅ FIX
type: 'course' // optional (if you added in interface)
category: course.category,
level: course.level,
type: 'course'
});
}}
className="flex items-center gap-2 hover:bg-blue-50 hover:border-blue-300"
@@ -775,4 +769,4 @@ export function LearningOnline() {
/>
</div>
);
}
}

View File

@@ -321,8 +321,8 @@ export const courseApi = createApi({
const queryString = searchParams.toString();
return queryString
? `admin/course/list?${queryString}`
: `admin/course/list`;
? `admin/course/public/list?${queryString}`
: `admin/course/public/list`;
},
providesTags: (result) =>

View File

@@ -112,9 +112,22 @@ export const homepageApi = createApi({
providesTags: [{ type: "Homepage", id: "LIST" }],
}),
getFeaturedBlogs: builder.query({
query: ({ limit = 3 }) => ({
url: `/admin/blogs/featured?limit=${limit}`,
method: 'GET',
}),
transformResponse: (response: any) => {
if (response?.success && response?.data) {
return response.data;
}
return [];
},
}),
}),
});
/* ================= HOOKS ================= */
export const { useGetHomepageQuery } = homepageApi;
export const { useGetHomepageQuery, useGetFeaturedBlogsQuery } = homepageApi;