feat: ArticleCard component
This commit is contained in:
@@ -0,0 +1,43 @@
|
|||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
import ArticleCard from '@/components/article/ArticleCard'
|
||||||
|
import type { Article } from '@/lib/types'
|
||||||
|
|
||||||
|
const mockArticle: Article = {
|
||||||
|
id: '1',
|
||||||
|
title: 'Frieren Season 2 Officially Announced',
|
||||||
|
slug: 'frieren-season-2-announced',
|
||||||
|
status: 'published',
|
||||||
|
content: null,
|
||||||
|
excerpt: 'The beloved series returns.',
|
||||||
|
featured_image: null,
|
||||||
|
published_at: '2026-05-28T00:00:00Z',
|
||||||
|
is_featured: false,
|
||||||
|
seo_title: null,
|
||||||
|
seo_description: null,
|
||||||
|
category: { id: '1', name: 'Anime', slug: 'anime', description: null },
|
||||||
|
tags: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ArticleCard', () => {
|
||||||
|
it('renders the article title', () => {
|
||||||
|
render(<ArticleCard article={mockArticle} />)
|
||||||
|
expect(screen.getByText('Frieren Season 2 Officially Announced')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders the category name', () => {
|
||||||
|
render(<ArticleCard article={mockArticle} />)
|
||||||
|
expect(screen.getByText('Anime')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('links to the correct article URL', () => {
|
||||||
|
render(<ArticleCard article={mockArticle} />)
|
||||||
|
const link = screen.getByRole('link')
|
||||||
|
expect(link).toHaveAttribute('href', '/anime/frieren-season-2-announced')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders the Read -> CTA', () => {
|
||||||
|
render(<ArticleCard article={mockArticle} />)
|
||||||
|
expect(screen.getByText('Read →')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import Image from 'next/image'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { getAssetUrl } from '@/lib/directus'
|
||||||
|
import type { Article } from '@/lib/types'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
article: Article
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ArticleCard({ article }: Props) {
|
||||||
|
const href = `/${article.category.slug}/${article.slug}`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={href}
|
||||||
|
className="group block bg-bg-card border border-border rounded-md overflow-hidden hover:border-[#3a4560] transition-colors"
|
||||||
|
>
|
||||||
|
<div className="relative aspect-video bg-bg-elevated overflow-hidden">
|
||||||
|
{article.featured_image ? (
|
||||||
|
<Image
|
||||||
|
src={getAssetUrl(article.featured_image, { width: 640, quality: 80 })}
|
||||||
|
alt={article.title}
|
||||||
|
fill
|
||||||
|
className="object-cover"
|
||||||
|
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="w-full h-full bg-bg-elevated" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="p-3">
|
||||||
|
<p className="text-[10px] font-semibold text-text-muted uppercase tracking-widest mb-1.5">
|
||||||
|
{article.category.name}
|
||||||
|
</p>
|
||||||
|
<h3 className="text-sm font-semibold text-text-primary leading-snug mb-3 line-clamp-2 group-hover:text-accent transition-colors">
|
||||||
|
{article.title}
|
||||||
|
</h3>
|
||||||
|
<span className="text-xs font-semibold text-accent">Read →</span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
+18
-11
@@ -8,12 +8,19 @@ import {
|
|||||||
} from '@directus/sdk'
|
} from '@directus/sdk'
|
||||||
import type { Article, Category, SiteSettings } from './types'
|
import type { Article, Category, SiteSettings } from './types'
|
||||||
|
|
||||||
const directus = createDirectus(process.env.DIRECTUS_URL!)
|
let _client: ReturnType<typeof createDirectus> | null = null
|
||||||
.with(staticToken(process.env.DIRECTUS_TOKEN!))
|
|
||||||
.with(rest())
|
function getClient() {
|
||||||
|
if (!_client) {
|
||||||
|
_client = createDirectus(process.env.DIRECTUS_URL!)
|
||||||
|
.with(staticToken(process.env.DIRECTUS_TOKEN!))
|
||||||
|
.with(rest())
|
||||||
|
}
|
||||||
|
return _client
|
||||||
|
}
|
||||||
|
|
||||||
export async function getAllCategories(): Promise<Category[]> {
|
export async function getAllCategories(): Promise<Category[]> {
|
||||||
return directus.request(
|
return getClient().request(
|
||||||
readItems('categories', {
|
readItems('categories', {
|
||||||
fields: ['id', 'name', 'slug', 'description'],
|
fields: ['id', 'name', 'slug', 'description'],
|
||||||
sort: ['name'],
|
sort: ['name'],
|
||||||
@@ -22,7 +29,7 @@ export async function getAllCategories(): Promise<Category[]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getCategoryBySlug(slug: string): Promise<Category | null> {
|
export async function getCategoryBySlug(slug: string): Promise<Category | null> {
|
||||||
const results = await directus.request(
|
const results = await getClient().request(
|
||||||
readItems('categories', {
|
readItems('categories', {
|
||||||
fields: ['id', 'name', 'slug', 'description'],
|
fields: ['id', 'name', 'slug', 'description'],
|
||||||
filter: { slug: { _eq: slug } },
|
filter: { slug: { _eq: slug } },
|
||||||
@@ -42,7 +49,7 @@ export async function getArticles(options: {
|
|||||||
if (categorySlug) {
|
if (categorySlug) {
|
||||||
filter['category'] = { slug: { _eq: categorySlug } }
|
filter['category'] = { slug: { _eq: categorySlug } }
|
||||||
}
|
}
|
||||||
return directus.request(
|
return getClient().request(
|
||||||
readItems('articles', {
|
readItems('articles', {
|
||||||
fields: [
|
fields: [
|
||||||
'id', 'title', 'slug', 'status', 'excerpt', 'featured_image',
|
'id', 'title', 'slug', 'status', 'excerpt', 'featured_image',
|
||||||
@@ -58,7 +65,7 @@ export async function getArticles(options: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getArticleBySlug(slug: string): Promise<Article | null> {
|
export async function getArticleBySlug(slug: string): Promise<Article | null> {
|
||||||
const results = await directus.request(
|
const results = await getClient().request(
|
||||||
readItems('articles', {
|
readItems('articles', {
|
||||||
fields: [
|
fields: [
|
||||||
'id', 'title', 'slug', 'status', 'content', 'excerpt',
|
'id', 'title', 'slug', 'status', 'content', 'excerpt',
|
||||||
@@ -79,7 +86,7 @@ export async function getArticlePathById(id: string): Promise<{
|
|||||||
category: { slug: string }
|
category: { slug: string }
|
||||||
} | null> {
|
} | null> {
|
||||||
try {
|
try {
|
||||||
return await directus.request(
|
return await getClient().request(
|
||||||
readItem('articles', id, {
|
readItem('articles', id, {
|
||||||
fields: ['slug', 'category.slug'],
|
fields: ['slug', 'category.slug'],
|
||||||
})
|
})
|
||||||
@@ -93,7 +100,7 @@ export async function getRelatedArticles(
|
|||||||
categorySlug: string,
|
categorySlug: string,
|
||||||
excludeSlug: string,
|
excludeSlug: string,
|
||||||
): Promise<Article[]> {
|
): Promise<Article[]> {
|
||||||
return directus.request(
|
return getClient().request(
|
||||||
readItems('articles', {
|
readItems('articles', {
|
||||||
fields: [
|
fields: [
|
||||||
'id', 'title', 'slug', 'excerpt', 'featured_image',
|
'id', 'title', 'slug', 'excerpt', 'featured_image',
|
||||||
@@ -111,7 +118,7 @@ export async function getRelatedArticles(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getSiteSettings(): Promise<SiteSettings> {
|
export async function getSiteSettings(): Promise<SiteSettings> {
|
||||||
return directus.request(
|
return getClient().request(
|
||||||
readSingleton('site_settings', {
|
readSingleton('site_settings', {
|
||||||
fields: [
|
fields: [
|
||||||
'id', 'site_name',
|
'id', 'site_name',
|
||||||
@@ -125,7 +132,7 @@ export async function getSiteSettings(): Promise<SiteSettings> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function searchArticles(query: string): Promise<Article[]> {
|
export async function searchArticles(query: string): Promise<Article[]> {
|
||||||
return directus.request(
|
return getClient().request(
|
||||||
readItems('articles', {
|
readItems('articles', {
|
||||||
fields: ['id', 'title', 'slug', 'category.slug', 'category.name'],
|
fields: ['id', 'title', 'slug', 'category.slug', 'category.name'],
|
||||||
search: query,
|
search: query,
|
||||||
|
|||||||
Reference in New Issue
Block a user