Merge remote-tracking branch 'origin/main' into cloudflare

This commit is contained in:
javayhu 2025-08-27 00:53:02 +08:00
commit 658409cfbd
5 changed files with 66 additions and 42 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

@ -102,11 +102,11 @@ export default function CreditsBalanceCard() {
</CardHeader>
<CardContent className="space-y-4 flex-1">
<div className="flex items-center justify-start space-x-4">
<Skeleton className="h-8 w-1/5" />
<Skeleton className="h-9 w-1/5" />
</div>
</CardContent>
<CardFooter className="px-6 py-4 flex justify-between items-center bg-muted rounded-none">
<Skeleton className="h-6 w-3/5" />
<Skeleton className="h-4 w-3/5" />
</CardFooter>
</Card>
);

View File

@ -4,7 +4,7 @@ function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="skeleton"
className={cn("bg-accent animate-pulse rounded-md", className)}
className={cn("bg-muted animate-pulse rounded-md", className)}
{...props}
/>
)

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==';