chore: replace Image component with BlogImage for improved loading handling in BlogCard

This commit is contained in:
javayhu 2025-08-27 00:45:09 +08:00
parent de7e87e5b8
commit 422c323467
3 changed files with 63 additions and 39 deletions

View File

@ -1,9 +1,9 @@
import { Skeleton } from '@/components/ui/skeleton';
import { LocaleLink } from '@/i18n/navigation';
import { PLACEHOLDER_IMAGE } from '@/lib/constants';
import { formatDate } from '@/lib/formatter';
import { type BlogType, authorSource, categorySource } from '@/lib/source';
import Image from 'next/image';
import BlogImage from './blog-image';
interface BlogCardProps {
locale: string;
@ -23,34 +23,29 @@ export default function BlogCard({ locale, post }: BlogCardProps) {
<div className="group flex flex-col border border-border rounded-lg overflow-hidden h-full transition-all duration-300 ease-in-out hover:border-primary hover:shadow-lg hover:shadow-primary/20">
{/* Image container - fixed aspect ratio */}
<div className="group overflow-hidden relative aspect-16/9 w-full">
{image && (
<div className="relative w-full h-full">
<Image
src={image}
alt={title || 'image for blog post'}
title={title || 'image for blog post'}
className="object-cover hover:scale-105 transition-transform duration-300"
placeholder="blur"
blurDataURL={PLACEHOLDER_IMAGE}
fill
/>
<div className="relative w-full h-full">
<BlogImage
src={image}
alt={title || 'image for blog post'}
title={title || 'image for blog post'}
/>
{blogCategories && blogCategories.length > 0 && (
<div className="absolute left-2 bottom-2 opacity-100 transition-opacity duration-300">
<div className="flex flex-wrap gap-1">
{blogCategories.map((category, index) => (
<span
key={`${category?.slugs[0]}-${index}`}
className="text-xs font-medium text-white bg-black/50 bg-opacity-50 px-2 py-1 rounded-md"
>
{category?.data.name}
</span>
))}
</div>
{/* categories */}
{blogCategories && blogCategories.length > 0 && (
<div className="absolute left-2 bottom-2 opacity-100 transition-opacity duration-300 z-20">
<div className="flex flex-wrap gap-1">
{blogCategories.map((category, index) => (
<span
key={`${category?.slugs[0]}-${index}`}
className="text-xs font-medium text-white bg-black/50 bg-opacity-50 px-2 py-1 rounded-md"
>
{category?.data.name}
</span>
))}
</div>
)}
</div>
)}
</div>
)}
</div>
</div>
{/* Post info container */}
@ -99,12 +94,7 @@ export function BlogCardSkeleton() {
return (
<div className="border border-gray-200 dark:border-gray-800 rounded-lg overflow-hidden h-full">
<div className="overflow-hidden relative aspect-16/9 w-full">
<Image
src={PLACEHOLDER_IMAGE}
alt="Loading placeholder"
className="object-cover"
fill
/>
<Skeleton className="h-full w-full rounded-b-none" />
</div>
<div className="p-4 flex flex-col justify-between flex-1">
<div>

View File

@ -0,0 +1,40 @@
'use client';
import { Skeleton } from '@/components/ui/skeleton';
import Image from 'next/image';
import { useState } from 'react';
interface BlogImageProps {
src: string;
alt: string;
title?: string;
}
export default function BlogImage({ src, alt, title }: BlogImageProps) {
const [imageLoading, setImageLoading] = useState(true);
const handleImageLoad = () => {
setImageLoading(false);
};
return (
<div className="relative w-full h-full">
{/* loading skeleton */}
{imageLoading && (
<Skeleton className="absolute inset-0 h-full w-full rounded-b-none z-10" />
)}
{/* actual image */}
<Image
src={src}
alt={alt}
title={title || alt}
className={`object-cover hover:scale-105 transition-transform duration-300 ${
imageLoading ? 'opacity-0' : 'opacity-100'
}`}
fill
onLoad={handleImageLoad}
/>
</div>
);
}

View File

@ -2,9 +2,3 @@
* in next 30 days for credits expiration
*/
export const CREDITS_EXPIRATION_DAYS = 30;
/**
* placeholder image for blog post card
*/
export const PLACEHOLDER_IMAGE =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAoJJREFUWEfFl4lu4zAMRO3cx/9/au6reMaOdkxTTl0grQFCRoqaT+SQotq2bV9N8rRt28xms87m83l553eZ/9vr9Wpkz+ezkT0ej+6dv1X81AFw7M4FBACPVn2c1Z3zLgDeJwHgeLFYdAARYioAEAKJEG2WAjl3gCwNYymQQ9b7/V4spmIAwO6Wy2VnAMikBWlDURBELf8CuN1uHQSrPwMAHK5WqwFELQ01AIXdAa7XawfAb3p6AOwK5+v1ugAoEq4FRSFLgavfQ49jAGQpAE5wjgGCeRrGdBArwHOPcwFcLpcGU1X0IsBuN5tNgYhaiFFwHTiAwq8I+O5xfj6fOz38K+X/fYAdb7fbAgFAjIJ6Aav3AYlQ6nfnDoDz0+lUxNiLALvf7XaDNGQ6GANQBKR85V27B4D3QQRw7hGIYlQKWGM79hSweyCUe1blXhEAogfABwHAXAcqSYkxCtHLUK3XBajSc4Dj8dilAeiSAgD2+30BAEKV4GKcAuDqB4TdYwBgPQByCgApUBoE4EJUGvxUjF3Q69/zLw3g/HA45ABKgdIQu+JPIyDnisCfAxAFNFM0EFNQ64gfS0EUoQP8ighrZSjn3oziZEQpauyKbfjbZchHUL/3AS/Dd30gAkxuRACgfO+EWQW8qwI1o+wseNuKcQiESjALvwNoMI0TcRzD4lFcPYwIM+JTF5x6HOs8yI7jeB5oKhpMRFH9UwaSCDB2Jmg4rc6E2TT0biIaG0rQhNqyhpHBcayTTSXH6vcDL7/sdqRK8LkwTsU499E8vRcAojHcZ4AxABdilgrp4lsXk8oVqgwh7+6H3phqd8J0Kk4vbx/+sZqCD/vNLya/5dT9fAH8g1WdNGgwbQAAAABJRU5ErkJggg==';