first commit

This commit is contained in:
2025-10-09 20:05:39 +05:30
commit d4fcb658e3
69 changed files with 13582 additions and 0 deletions

View File

@ -0,0 +1,372 @@
// components/BlogListing.tsx
'use client';
import { useState, useEffect } from 'react';
import Image from 'next/image';
import Link from 'next/link';
import { ChevronRight } from 'lucide-react';
import { blogService, Blog } from '../../services/blogService'; // Adjust path as needed
const BlogListing: React.FC = () => {
const [blogs, setBlogs] = useState<Blog[]>([]);
const [filteredBlogs, setFilteredBlogs] = useState<Blog[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [mounted, setMounted] = useState(false);
const [selectedCategory, setSelectedCategory] = useState('All Categories');
const [searchQuery, setSearchQuery] = useState('');
const [displayCount, setDisplayCount] = useState(6);
const [tagCounts, setTagCounts] = useState<{ [key: string]: number }>({});
// Get unique categories from blogs with their counts
const getCategories = () => {
const allTags = blogs.flatMap(blog => blog.tags);
const uniqueTags = Array.from(new Set(allTags));
return ['All Categories', ...uniqueTags];
};
// Filter blogs based on category and search query
const filterBlogs = () => {
let filtered = blogs;
// Filter by category
if (selectedCategory !== 'All Categories') {
filtered = filtered.filter(blog =>
blog.tags.some(tag =>
tag.toLowerCase().includes(selectedCategory.toLowerCase())
)
);
}
// Filter by search query
if (searchQuery.trim()) {
const query = searchQuery.toLowerCase().trim();
filtered = filtered.filter(blog =>
blog.title.toLowerCase().includes(query) ||
blog.excerpt.toLowerCase().includes(query) ||
blog.tags.some(tag => tag.toLowerCase().includes(query)) ||
(blog.professors && blog.professors.some(prof =>
prof.firstName?.toLowerCase().includes(query) ||
prof.name?.toLowerCase().includes(query)
))
);
}
setFilteredBlogs(filtered);
};
// Load blogs from API
useEffect(() => {
const loadBlogs = async () => {
try {
setLoading(true);
setError(null);
// Load both posted blogs and tag counts
const [fetchedBlogs, fetchedTagCounts] = await Promise.all([
blogService.getPostedBlogs(),
blogService.getTagsWithCount()
]);
setBlogs(fetchedBlogs);
setFilteredBlogs(fetchedBlogs);
setTagCounts(fetchedTagCounts);
} catch (err) {
setError('Failed to load blogs. Please try again later.');
console.error('Error loading blogs:', err);
} finally {
setLoading(false);
}
};
if (mounted) {
loadBlogs();
}
}, [mounted]);
// Filter blogs when category or search changes
useEffect(() => {
if (mounted && blogs.length > 0) {
filterBlogs();
}
}, [selectedCategory, searchQuery, blogs, mounted]);
// Handle mount
useEffect(() => {
setMounted(true);
}, []);
const handleCategoryChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setSelectedCategory(e.target.value);
setDisplayCount(6);
};
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchQuery(e.target.value);
setDisplayCount(6);
};
const handleLoadMore = () => {
setDisplayCount(prev => prev + 6);
};
const handleImageError = (event: React.SyntheticEvent<HTMLImageElement>) => {
// Fallback to default image if the uploaded image fails to load
const target = event.target as HTMLImageElement;
target.src = '/images/default-blog-image.jpg';
};
if (!mounted) {
return null;
}
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-900 mx-auto mb-4"></div>
<p className="text-gray-600">Loading blogs...</p>
</div>
</div>
);
}
if (error) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center text-red-600">
<p className="text-xl mb-4"> {error}</p>
<button
onClick={() => window.location.reload()}
className="px-4 py-2 bg-blue-900 text-white rounded hover:bg-blue-800"
>
Retry
</button>
</div>
</div>
);
}
const categories = getCategories();
const blogsToShow = filteredBlogs.slice(0, displayCount);
const hasMoreBlogs = filteredBlogs.length > displayCount;
return (
<div className="min-h-screen bg-white">
{/* Breadcrumb Section */}
<section className="py-4" style={{ backgroundColor: '#f4f4f4' }}>
<div className="max-w-7xl mx-auto px-4">
<nav className="flex items-center space-x-2 text-sm">
<Link
href="/"
className="hover:opacity-70 transition-opacity duration-200"
style={{ color: '#012068' }}
>
Home
</Link>
<ChevronRight className="w-4 h-4" style={{ color: '#012068' }} />
<span className="font-medium" style={{ color: '#e64838' }}>
Trauma Care Resources
</span>
</nav>
{/* Page Header */}
<div className="mt-6">
<div className="flex items-center mb-4">
<h1 className="text-3xl font-bold" style={{ color: '#012068' }}>
Trauma Care Resources
</h1>
</div>
<p className="text-base max-w-2xl leading-relaxed" style={{ color: '#333' }}>
Expert insights, healing strategies, and support resources for trauma recovery
</p>
</div>
</div>
</section>
{/* Filters Section */}
<div className="flex justify-end items-center gap-4 max-w-7xl mx-auto px-4 py-8" style={{ backgroundColor: '#fff' }}>
<div className="relative">
<select
value={selectedCategory}
onChange={handleCategoryChange}
className="appearance-none bg-gray-100 text-sm border border-blue-900 rounded-lg px-4 py-2 pr-8 focus:outline-none focus:border-blue-900"
style={{ color: '#333' }}
>
{categories.map((category, index) => (
<option key={index} value={category}>
{category}
{category !== 'All Categories' && tagCounts[category] ? ` (${tagCounts[category]})` : ''}
</option>
))}
</select>
<div className="absolute inset-y-0 right-0 flex items-center px-2 pointer-events-none">
<svg className="w-4 h-4 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</div>
</div>
<div className="relative">
<input
type="text"
value={searchQuery}
onChange={handleSearchChange}
placeholder="Search blogs..."
className="border border-blue-900 rounded-lg px-4 py-2 pl-4 pr-10 text-sm focus:outline-none focus:border-blue-900 w-64 text-gray-700"
/>
<button className="absolute inset-y-0 right-0 flex items-center px-3 bg-blue-900 rounded-r-lg">
<svg className="w-4 h-4 text-gray-100" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</button>
</div>
</div>
{/* Results Info */}
{(selectedCategory !== 'All Categories' || searchQuery.trim()) && (
<div className="max-w-7xl mx-auto px-4 pb-4">
<p className="text-sm text-gray-600">
{filteredBlogs.length === 0
? 'No blogs found matching your criteria.'
: `Showing ${blogsToShow.length} of ${filteredBlogs.length} blog${filteredBlogs.length !== 1 ? 's' : ''}`
}
</p>
</div>
)}
{/* Blog Grid Section */}
<section className="py-4" style={{ backgroundColor: '#fff' }}>
<div className="max-w-7xl mx-auto px-4">
{blogsToShow.length === 0 ? (
<div className="text-center py-12">
<p className="text-gray-500 text-lg mb-4">
{searchQuery.trim() || selectedCategory !== 'All Categories'
? 'No blogs match your search criteria.'
: 'No blogs available at the moment.'
}
</p>
{(searchQuery.trim() || selectedCategory !== 'All Categories') && (
<button
onClick={() => {
setSearchQuery('');
setSelectedCategory('All Categories');
}}
className="px-4 py-2 text-sm border border-blue-900 text-blue-900 rounded hover:bg-blue-50"
>
Clear Filters
</button>
)}
</div>
) : (
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{blogsToShow.map((blog) => (
<div
key={blog.id}
className="group relative bg-white border border-gray-300 rounded-lg overflow-hidden hover:shadow-lg transition-all duration-300 flex flex-col"
>
{/* All cards redirect to blog detail page with ID */}
<Link
href={`/blog-detail/${blog.id}`}
className="absolute top-0 left-0 h-full w-full z-10"
aria-label={`Read article: ${blog.title}`}
/>
{/* Blog Image */}
<div className="relative h-48 w-full overflow-hidden">
<Image
src={blog.image}
alt={blog.title}
fill
className="object-cover transition-transform duration-300 group-hover:scale-105"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
onError={handleImageError}
/>
</div>
{/* Blog Content */}
<div className="p-6 flex flex-col flex-1">
{/* Tags */}
<div className="flex flex-wrap gap-2 mb-3">
{blog.tags.slice(0, 3).map((tag, tagIndex) => (
<span
key={tagIndex}
className="px-2 py-1 text-xs font-medium rounded"
style={{
backgroundColor: '#f4f4f4',
color: '#e64838'
}}
>
{tag}
</span>
))}
{blog.tags.length > 3 && (
<span
className="px-2 py-1 text-xs font-medium rounded"
style={{
backgroundColor: '#f4f4f4',
color: '#666'
}}
>
+{blog.tags.length - 3}
</span>
)}
</div>
{/* Title */}
<h3
className="text-lg font-medium mb-2 line-clamp-2 group-hover:opacity-70 transition-opacity duration-300"
style={{ color: '#012068' }}
>
{blog.title}
</h3>
{/* Excerpt */}
<p
className="text-xs leading-relaxed line-clamp-3 mb-4 flex-1"
style={{ opacity: 0.8, color: "#333" }}
>
{blog.excerpt}
</p>
{/* Authors (if available) */}
{blog.professors && blog.professors.length > 0 && (
<div className="mb-2">
<p className="text-xs text-gray-600">
By: {blog.professors.map(prof => prof.firstName || prof.name).join(', ')}
</p>
</div>
)}
{/* Meta Information */}
<div className="flex items-center justify-between text-xs mt-auto" style={{ color: '#333' }}>
<span>{new Date(blog.publishDate).toLocaleDateString()}</span>
<span>{blog.readTime}</span>
</div>
</div>
</div>
))}
</div>
)}
{/* Load More Button */}
{hasMoreBlogs && (
<div className="text-center mt-12">
<button
onClick={handleLoadMore}
className="px-6 py-2 text-sm font-medium rounded hover:opacity-90 transition-opacity duration-300 focus:outline-none focus:ring-2 focus:ring-offset-2"
style={{
backgroundColor: '#012068',
color: '#f4f4f4',
'--tw-ring-color': '#012068'
} as React.CSSProperties}
>
Load More Articles ({filteredBlogs.length - displayCount} remaining)
</button>
</div>
)}
</div>
</section>
</div>
);
};
export default BlogListing;