forked from UKSOURCE/hailearning.edu.vn
feat: Refactor blog components and add pagination
This commit is contained in:
@@ -1,92 +1,62 @@
|
||||
import Link from "next/link";
|
||||
import type { BlogPost } from "@/types/blog";
|
||||
import { getCmsImageUrl } from "@/utils";
|
||||
|
||||
interface NewsListProps {
|
||||
blogs?: BlogPost[];
|
||||
categorySlug?: string;
|
||||
tagSlug?: string;
|
||||
}
|
||||
|
||||
export default function NewsList({ blogs = [], categorySlug, tagSlug }: NewsListProps) {
|
||||
// Use blogs from props (already filtered by API)
|
||||
const posts = blogs;
|
||||
|
||||
// Additional client-side filtering if needed (though API should handle this)
|
||||
if (categorySlug || tagSlug) {
|
||||
// If filters are provided but blogs are not pre-filtered, filter here
|
||||
// This is a fallback - ideally API should handle filtering
|
||||
}
|
||||
|
||||
export default function NewsList() {
|
||||
return (
|
||||
<div className="col-lg-8 col-12">
|
||||
{/* News Post 1 */}
|
||||
<div className="news-standard-post">
|
||||
<div className="news-image">
|
||||
<img src="/assets/img/home-1/news/news-13.jpg" alt="img" />
|
||||
{posts.map((post, index) => (
|
||||
<div
|
||||
key={post.slug}
|
||||
className={`news-standard-post ${index === posts.length - 1 ? "mb-0" : ""}`}
|
||||
>
|
||||
<div className="news-image">
|
||||
<img
|
||||
src={
|
||||
post.featuredImage
|
||||
? getCmsImageUrl(post.featuredImage)
|
||||
: "/assets/img/inner-page/news-details/details-1.jpg"
|
||||
}
|
||||
alt={post.title}
|
||||
/>
|
||||
</div>
|
||||
<div className="news-content">
|
||||
<ul className="news-list">
|
||||
<li>
|
||||
<i className="fa-solid fa-user"></i> By {post.author}
|
||||
</li>
|
||||
<li>
|
||||
<i className="fa-solid fa-calendar-days"></i> {post.publishedAt}
|
||||
</li>
|
||||
<li>
|
||||
<i className="fa-solid fa-comments"></i> {post.commentsCount} Comments
|
||||
</li>
|
||||
</ul>
|
||||
<h3>
|
||||
<Link href={`/blog/${post.slug}`}>{post.title}</Link>
|
||||
</h3>
|
||||
<p>{post.excerpt}</p>
|
||||
<Link href={`/blog/${post.slug}`} className="theme-btn">
|
||||
VIEW MORE <i className="fa-solid fa-arrow-right"></i>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="news-content">
|
||||
<ul className="news-list">
|
||||
<li>
|
||||
<i className="fa-solid fa-user"></i> By Admin
|
||||
</li>
|
||||
<li>
|
||||
<i className="fa-solid fa-calendar-days"></i> 11 March 2025
|
||||
</li>
|
||||
<li>
|
||||
<i className="fa-solid fa-comments"></i> 0 Comments
|
||||
</li>
|
||||
</ul>
|
||||
<h3>
|
||||
<Link href="/news-details">How to Avoid Common Mistakes in Visa Applications</Link>
|
||||
</h3>
|
||||
<p>
|
||||
A business consultant provides expert guidance, strategic planning, and problem-solving support—helping startups avoid mistakes, grow faster, and operate more efficiently from day one.
|
||||
</p>
|
||||
<Link href="/blog/news-details" className="theme-btn">
|
||||
VIEW MORE <i className="fa-solid fa-arrow-right"></i>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
{/* News Post 2 */}
|
||||
<div className="news-standard-post">
|
||||
<div className="news-image">
|
||||
<img src="/assets/img/home-1/news/news-14.jpg" alt="img" />
|
||||
</div>
|
||||
<div className="news-content">
|
||||
<ul className="news-list">
|
||||
<li>
|
||||
<i className="fa-solid fa-user"></i> By Admin
|
||||
</li>
|
||||
<li>
|
||||
<i className="fa-solid fa-calendar-days"></i> 11 March 2025
|
||||
</li>
|
||||
<li>
|
||||
<i className="fa-solid fa-comments"></i> 0 Comments
|
||||
</li>
|
||||
</ul>
|
||||
<h3>
|
||||
<Link href="/news-details">The Role of Immigration Consultants in Your Journey</Link>
|
||||
</h3>
|
||||
<p>
|
||||
Immigration consultants play a vital role in guiding applicants, simplifying complex processes, offering expert advice, and ensuring successful outcomes for study, work, or permanent residency abroad.
|
||||
</p>
|
||||
<Link href="/blog/news-details" className="theme-btn">
|
||||
VIEW MORE <i className="fa-solid fa-arrow-right"></i>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
{/* News Post 3 */}
|
||||
<div className="news-standard-post mb-0">
|
||||
<div className="news-image">
|
||||
<img src="/assets/img/home-1/news/news-15.jpg" alt="img" />
|
||||
</div>
|
||||
<div className="news-content">
|
||||
<ul className="news-list">
|
||||
<li>
|
||||
<i className="fa-solid fa-user"></i> By Admin
|
||||
</li>
|
||||
<li>
|
||||
<i className="fa-solid fa-calendar-days"></i> 11 March 2025
|
||||
</li>
|
||||
<li>
|
||||
<i className="fa-solid fa-comments"></i> 0 Comments
|
||||
</li>
|
||||
</ul>
|
||||
<h3>
|
||||
<Link href="/news-details">Latest Immigration Policy Updates You Should Know</Link>
|
||||
</h3>
|
||||
<p>
|
||||
Stay informed with the latest immigration policy updates, ensuring you understand new rules, visa requirements, and opportunities that impact your study, work, or migration journey abroad.
|
||||
</p>
|
||||
<Link href="/blog/news-details" className="theme-btn">
|
||||
VIEW MORE <i className="fa-solid fa-arrow-right"></i>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +1,38 @@
|
||||
import NewsList from "./NewsList";
|
||||
import Sidebar from "./Sidebar";
|
||||
import Pagination from "./Pagination";
|
||||
import type { BlogPost, BlogPagination } from "@/types";
|
||||
|
||||
export default function NewsSection() {
|
||||
interface NewsSectionProps {
|
||||
blogs?: BlogPost[];
|
||||
categorySlug?: string;
|
||||
tagSlug?: string;
|
||||
searchQuery?: string;
|
||||
pagination?: BlogPagination;
|
||||
}
|
||||
|
||||
export default function NewsSection({
|
||||
blogs,
|
||||
categorySlug,
|
||||
tagSlug,
|
||||
searchQuery,
|
||||
pagination,
|
||||
}: NewsSectionProps) {
|
||||
return (
|
||||
<section className="news-standard-section section-padding fix">
|
||||
<div className="container">
|
||||
<div className="news-standard-wrapper">
|
||||
<div className="row g-4">
|
||||
<NewsList />
|
||||
<Sidebar />
|
||||
<NewsList blogs={blogs} categorySlug={categorySlug} tagSlug={tagSlug} />
|
||||
<Sidebar searchQuery={searchQuery} />
|
||||
</div>
|
||||
{pagination && pagination.total > 1 && (
|
||||
<div className="row g-4 mt-4">
|
||||
<div className="col-12">
|
||||
<Pagination basePath="/blog" pagination={pagination} searchQuery={searchQuery} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
79
app/blog/components/Pagination.tsx
Normal file
79
app/blog/components/Pagination.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import Link from "next/link";
|
||||
import type { BlogPagination } from "@/types";
|
||||
|
||||
interface PaginationProps {
|
||||
basePath: string;
|
||||
pagination: BlogPagination;
|
||||
searchQuery?: string;
|
||||
}
|
||||
|
||||
export default function Pagination({ basePath, pagination, searchQuery }: PaginationProps) {
|
||||
const { current, total } = pagination;
|
||||
|
||||
if (total <= 1) return null;
|
||||
|
||||
const makeHref = (page: number) => {
|
||||
const params = new URLSearchParams();
|
||||
if (page > 1) params.set("page", page.toString());
|
||||
if (searchQuery) params.set("search", searchQuery);
|
||||
const qs = params.toString();
|
||||
return qs ? `${basePath}?${qs}` : basePath;
|
||||
};
|
||||
|
||||
const pages: number[] = [];
|
||||
for (let i = 1; i <= total; i++) {
|
||||
if (i === 1 || i === total || (i >= current - 2 && i <= current + 2)) {
|
||||
pages.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
const items: (number | "...")[] = [];
|
||||
for (let i = 0; i < pages.length; i++) {
|
||||
const page = pages[i];
|
||||
items.push(page);
|
||||
if (i < pages.length - 1 && pages[i + 1] !== page + 1) {
|
||||
items.push("...");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<nav aria-label="Blog pagination" className="mt-4">
|
||||
<ul className="pagination justify-content-center">
|
||||
{current > 1 && (
|
||||
<li className="page-item">
|
||||
<Link className="page-link" href={makeHref(current - 1)}>
|
||||
Previous
|
||||
</Link>
|
||||
</li>
|
||||
)}
|
||||
|
||||
{items.map((item, idx) =>
|
||||
item === "..." ? (
|
||||
<li key={`ellipsis-${idx}`} className="page-item disabled">
|
||||
<span className="page-link">...</span>
|
||||
</li>
|
||||
) : (
|
||||
<li key={item} className={`page-item ${item === current ? "active" : ""}`}>
|
||||
{item === current ? (
|
||||
<span className="page-link">{item}</span>
|
||||
) : (
|
||||
<Link className="page-link" href={makeHref(item as number)}>
|
||||
{item}
|
||||
</Link>
|
||||
)}
|
||||
</li>
|
||||
),
|
||||
)}
|
||||
|
||||
{current < total && (
|
||||
<li className="page-item">
|
||||
<Link className="page-link" href={makeHref(current + 1)}>
|
||||
Next
|
||||
</Link>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,35 @@
|
||||
import Link from "next/link";
|
||||
import { fetchCategories, fetchRecentBlogs, fetchPopularTags } from "@/api/blog";
|
||||
|
||||
interface SidebarProps {
|
||||
searchQuery?: string;
|
||||
}
|
||||
|
||||
export default async function Sidebar({ searchQuery }: SidebarProps) {
|
||||
// Fetch data from API
|
||||
const [categoriesResponse, recentBlogsResponse, tagsResponse] = await Promise.all([
|
||||
fetchCategories(),
|
||||
fetchRecentBlogs(5),
|
||||
fetchPopularTags(10),
|
||||
]);
|
||||
|
||||
const categories = categoriesResponse.data;
|
||||
const recentPosts = recentBlogsResponse.data;
|
||||
const tags = tagsResponse.data;
|
||||
|
||||
export default function Sidebar() {
|
||||
return (
|
||||
<div className="col-lg-4 col-12">
|
||||
<div className="main-sideber">
|
||||
{/* Search Widget */}
|
||||
<div className="news-sideber-box">
|
||||
<div className="search-widget">
|
||||
<form action="#">
|
||||
<input type="text" placeholder="Search Blog" />
|
||||
<form action="/blog" method="GET">
|
||||
<input
|
||||
type="text"
|
||||
name="search"
|
||||
placeholder="Search Blog"
|
||||
defaultValue={searchQuery || ""}
|
||||
/>
|
||||
<button type="submit">
|
||||
<i className="fa-solid fa-magnifying-glass"></i>
|
||||
</button>
|
||||
@@ -22,11 +43,17 @@ export default function Sidebar() {
|
||||
</div>
|
||||
<div className="news-widget-categories">
|
||||
<ul>
|
||||
<li><Link href="/news-details"><i className="fa-solid fa-chevrons-right"></i> Permanent Residency (PR)</Link><span>(04)</span></li>
|
||||
<li><Link href="/news-details"><i className="fa-solid fa-chevrons-right"></i> Immigration Policy Updates</Link><span>(09)</span></li>
|
||||
<li><Link href="/news-details"><i className="fa-solid fa-chevrons-right"></i> Scholarships & Grants</Link><span>(00)</span></li>
|
||||
<li><Link href="/news-details"><i className="fa-solid fa-chevrons-right"></i> Citizenship & Naturalization</Link><span>(04)</span></li>
|
||||
<li><Link href="/news-details"><i className="fa-solid fa-chevrons-right"></i> Visa Interview Preparation</Link><span>(01)</span></li>
|
||||
{categories.map((category) => {
|
||||
const postCount = category.postCount || 0;
|
||||
return (
|
||||
<li key={category.slug}>
|
||||
<Link href={`/blog/category/${category.slug}`}>
|
||||
<i className="fa-solid fa-chevrons-right"></i> {category.name}
|
||||
</Link>
|
||||
<span>({String(postCount).padStart(2, "0")})</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -36,45 +63,27 @@ export default function Sidebar() {
|
||||
<h3>Recent Post</h3>
|
||||
</div>
|
||||
<div className="recent-post-area">
|
||||
<div className="recent-items">
|
||||
<div className="recent-thumb">
|
||||
<img src="/assets/img/inner-page/news-details/post-1.jpg" alt="img" />
|
||||
{recentPosts.map((post) => (
|
||||
<div key={post.slug} className="recent-items">
|
||||
<div className="recent-thumb">
|
||||
<img
|
||||
src={post.thumbnail || "/assets/img/inner-page/news-details/details-1.jpg"}
|
||||
alt={post.title}
|
||||
width={88}
|
||||
height={80}
|
||||
style={{ objectFit: 'cover' }}
|
||||
/>
|
||||
</div>
|
||||
<div className="recent-content">
|
||||
<h6>
|
||||
<Link href={`/blog/${post.slug}`}>{post.title}</Link>
|
||||
</h6>
|
||||
<ul>
|
||||
<li>{post.publishedAt}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className="recent-content">
|
||||
<h6>
|
||||
<Link href="/news-details">Top Countries for Higher Education in 2025</Link>
|
||||
</h6>
|
||||
<ul>
|
||||
<li>March 26, 2025</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className="recent-items">
|
||||
<div className="recent-thumb">
|
||||
<img src="/assets/img/inner-page/news-details/post-2.jpg" alt="img" />
|
||||
</div>
|
||||
<div className="recent-content">
|
||||
<h6>
|
||||
<Link href="/news-details">The Benefits of Hiring a Visa Consultant</Link>
|
||||
</h6>
|
||||
<ul>
|
||||
<li>March 26, 2025</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className="recent-items">
|
||||
<div className="recent-thumb">
|
||||
<img src="/assets/img/inner-page/news-details/post-3.jpg" alt="img" />
|
||||
</div>
|
||||
<div className="recent-content">
|
||||
<h6>
|
||||
<Link href="/news-details">How to Prepare for Your Immigration Interview</Link>
|
||||
</h6>
|
||||
<ul>
|
||||
<li>March 26, 2025</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{/* Tag Cloud */}
|
||||
@@ -84,12 +93,11 @@ export default function Sidebar() {
|
||||
</div>
|
||||
<div className="news-widget-categories">
|
||||
<div className="tagcloud">
|
||||
<Link href="/news-details">WorkVisa</Link>
|
||||
<Link href="/news-details">FamilyVisa</Link>
|
||||
<Link href="/news-details">StudentVisa</Link>
|
||||
<Link href="/news-details">VisaUpdates</Link>
|
||||
<Link href="/news-details">TravelVisa</Link>
|
||||
<Link href="/news-details">StudyAbroad</Link>
|
||||
{tags.map((tag) => (
|
||||
<Link key={tag.slug} href={`/blog/tag/${tag.slug}`}>
|
||||
{tag.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user