Merge remote-tracking branch 'origin/main' into cloudflare
This commit is contained in:
commit
658409cfbd
@ -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>
|
||||
|
40
src/components/blog/blog-image.tsx
Normal file
40
src/components/blog/blog-image.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
|
@ -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}
|
||||
/>
|
||||
)
|
||||
|
@ -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==';
|
||||
|
Loading…
Reference in New Issue
Block a user