101 lines
3.0 KiB
TypeScript
101 lines
3.0 KiB
TypeScript
// src/components/news/NewsCard.tsx
|
||
"use client";
|
||
|
||
import Image from "next/image";
|
||
import Link from "next/link";
|
||
import dayjs from "dayjs";
|
||
import { useMemo } from "react";
|
||
import type { NewsItem } from "@/types/news";
|
||
|
||
const stripTags = (html?: string | null) => (html ? html.replace(/<[^>]+>/g, "") : "");
|
||
|
||
// HTMLエンティティをデコード
|
||
const decodeEntities = (text: string) => {
|
||
const textarea = document.createElement("textarea");
|
||
textarea.innerHTML = text;
|
||
return textarea.value;
|
||
};
|
||
|
||
export default function NewsCard({ item }: { item: NewsItem }) {
|
||
const summary = useMemo(() => {
|
||
if (!item.content) return "";
|
||
const plain = decodeEntities(stripTags(item.content));
|
||
return plain.length > 80 ? plain.slice(0, 80) + "…" : plain;
|
||
}, [item.content]);
|
||
const href = (() => {
|
||
const url = item.linkurl?.trim();
|
||
|
||
// リンクなし(none/空)の場合は詳細ページへ
|
||
if (!url || url.toLowerCase() === "none") {
|
||
return `/news/${item.id}`;
|
||
}
|
||
|
||
// 外部URLはそのまま
|
||
if (/^https?:\/\//i.test(url)) {
|
||
return url;
|
||
}
|
||
|
||
// 内部スラッグ(相対指定)の場合
|
||
return `/news/${url}`;
|
||
})();
|
||
|
||
const target = item.link_target === "_blank" ? "_blank" : "_self";
|
||
const isExternal = href?.startsWith("http");
|
||
|
||
const imageId = typeof item.thumbnail === "string" ? item.thumbnail : item.thumbnail?.id;
|
||
const imageSrc = imageId
|
||
? `${process.env.NEXT_PUBLIC_DIRECTUS_ASSETS_URL}/assets/${imageId}?fit=cover`
|
||
: "/dummy_thumb.png";
|
||
const date = dayjs(item.published_at).format("YYYY.MM.DD");
|
||
|
||
const cardInner = (
|
||
<article className="flex flex-col justify-between h-full bg-stone-50 border border-stone-200 rounded-2xl overflow-hidden shadow-sm hover:shadow-md transition-all duration-300 hover:-translate-y-1">
|
||
<div className="relative w-full aspect-[4/3] bg-stone-100 overflow-hidden">
|
||
<Image
|
||
src={imageSrc}
|
||
alt={stripTags(item.title) || "NEWS"}
|
||
fill
|
||
className="object-cover transition-transform duration-700 group-hover:scale-105"
|
||
/>
|
||
</div>
|
||
|
||
<div className="p-5 flex flex-col flex-1 font-sans">
|
||
<p className="text-xs text-stone-500 mb-1">{date}</p>
|
||
<h3 className="font-heading text-lg text-stone-800 mb-2 leading-tight line-clamp-2 min-h-[3rem]">
|
||
{stripTags(item.title)}
|
||
</h3>
|
||
{summary && <p className="text-sm text-stone-600 truncate mb-3">{summary}</p>}
|
||
<div className="mt-auto pt-2">
|
||
{Array.isArray(item.tags) && item.tags.length > 0 && (
|
||
<div className="flex flex-wrap gap-2">
|
||
{item.tags.map((tag, i) => (
|
||
<span
|
||
key={i}
|
||
className="text-xs bg-stone-200 text-stone-700 px-2 py-1 rounded-full"
|
||
>
|
||
#{tag}
|
||
</span>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</article>
|
||
);
|
||
|
||
if (!href) return cardInner;
|
||
|
||
if (isExternal)
|
||
return (
|
||
<a href={href} target={target} rel="noopener noreferrer" className="group block h-full">
|
||
{cardInner}
|
||
</a>
|
||
);
|
||
|
||
return (
|
||
<Link href={href} target={target} className="group block h-full">
|
||
{cardInner}
|
||
</Link>
|
||
);
|
||
}
|