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 { Skeleton } from '@/components/ui/skeleton';
|
||||||
import { LocaleLink } from '@/i18n/navigation';
|
import { LocaleLink } from '@/i18n/navigation';
|
||||||
import { PLACEHOLDER_IMAGE } from '@/lib/constants';
|
|
||||||
import { formatDate } from '@/lib/formatter';
|
import { formatDate } from '@/lib/formatter';
|
||||||
import { type BlogType, authorSource, categorySource } from '@/lib/source';
|
import { type BlogType, authorSource, categorySource } from '@/lib/source';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
import BlogImage from './blog-image';
|
||||||
|
|
||||||
interface BlogCardProps {
|
interface BlogCardProps {
|
||||||
locale: string;
|
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">
|
<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 */}
|
{/* Image container - fixed aspect ratio */}
|
||||||
<div className="group overflow-hidden relative aspect-16/9 w-full">
|
<div className="group overflow-hidden relative aspect-16/9 w-full">
|
||||||
{image && (
|
<div className="relative w-full h-full">
|
||||||
<div className="relative w-full h-full">
|
<BlogImage
|
||||||
<Image
|
src={image}
|
||||||
src={image}
|
alt={title || 'image for blog post'}
|
||||||
alt={title || 'image for blog post'}
|
title={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
|
|
||||||
/>
|
|
||||||
|
|
||||||
{blogCategories && blogCategories.length > 0 && (
|
{/* categories */}
|
||||||
<div className="absolute left-2 bottom-2 opacity-100 transition-opacity duration-300">
|
{blogCategories && blogCategories.length > 0 && (
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="absolute left-2 bottom-2 opacity-100 transition-opacity duration-300 z-20">
|
||||||
{blogCategories.map((category, index) => (
|
<div className="flex flex-wrap gap-1">
|
||||||
<span
|
{blogCategories.map((category, index) => (
|
||||||
key={`${category?.slugs[0]}-${index}`}
|
<span
|
||||||
className="text-xs font-medium text-white bg-black/50 bg-opacity-50 px-2 py-1 rounded-md"
|
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>
|
{category?.data.name}
|
||||||
))}
|
</span>
|
||||||
</div>
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Post info container */}
|
{/* Post info container */}
|
||||||
@ -99,12 +94,7 @@ export function BlogCardSkeleton() {
|
|||||||
return (
|
return (
|
||||||
<div className="border border-gray-200 dark:border-gray-800 rounded-lg overflow-hidden h-full">
|
<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">
|
<div className="overflow-hidden relative aspect-16/9 w-full">
|
||||||
<Image
|
<Skeleton className="h-full w-full rounded-b-none" />
|
||||||
src={PLACEHOLDER_IMAGE}
|
|
||||||
alt="Loading placeholder"
|
|
||||||
className="object-cover"
|
|
||||||
fill
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4 flex flex-col justify-between flex-1">
|
<div className="p-4 flex flex-col justify-between flex-1">
|
||||||
<div>
|
<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>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4 flex-1">
|
<CardContent className="space-y-4 flex-1">
|
||||||
<div className="flex items-center justify-start space-x-4">
|
<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>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter className="px-6 py-4 flex justify-between items-center bg-muted rounded-none">
|
<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>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
@ -4,7 +4,7 @@ function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="skeleton"
|
data-slot="skeleton"
|
||||||
className={cn("bg-accent animate-pulse rounded-md", className)}
|
className={cn("bg-muted animate-pulse rounded-md", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
@ -2,9 +2,3 @@
|
|||||||
* in next 30 days for credits expiration
|
* in next 30 days for credits expiration
|
||||||
*/
|
*/
|
||||||
export const CREDITS_EXPIRATION_DAYS = 30;
|
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