CONTENT
ここから
10. 検索コンポーネントを作る
components/BlogSearch.tsx を作成します。
// components/BlogSearch.tsx
'use client';
import { BlogCard } from '@/components/BlogCard';
import { searchPosts } from '@/data/posts';
import type { BlogPost } from '@/types/blogs';
import { useMemo, useState } from 'react';
type BlogSearchProps = {
initialPosts: BlogPost[];
};
/**
* 役割: 記事検索フォームと検索結果一覧を表示する
* 入力: 初期表示するブログ記事配列
* 出力: 検索可能な記事一覧UI
*/
export function BlogSearch({ initialPosts }: BlogSearchProps) {
const [keyword, setKeyword] = useState<string>('');
const filteredPosts = useMemo<BlogPost[]>(() => {
if (keyword.trim().length === 0) {
return initialPosts;
}
return searchPosts(keyword);
}, [keyword, initialPosts]);
return (
<section id="posts" className="mx-auto max-w-6xl px-5 py-14">
<div className="mb-8 flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
<div>
<p className="text-sm font-bold uppercase tracking-[0.24em] text-slate-400">
Articles
</p>
<h2 className="mt-2 text-3xl font-black tracking-tight text-slate-950">
最新ニュース
</h2>
</div>
<label className="w-full md:w-96">
<span className="mb-2 block text-sm font-bold text-slate-600">
記事を検索
</span>
<input
value={keyword}
onChange={(event) => setKeyword(event.target.value)}
placeholder="例: 岐阜市 / 音楽 / 大垣"
className="w-full rounded-2xl border border-slate-200 bg-white px-4 py-3 text-sm font-medium text-slate-900 outline-none transition placeholder:text-slate-400 focus:border-slate-950 focus:ring-4 focus:ring-slate-200"
type="search"
/>
</label>
</div>
{filteredPosts.length > 0 ? (
<div className="grid gap-5 md:grid-cols-2">
{filteredPosts.map((post) => (
<BlogCard key={post.id} post={post} />
))}
</div>
) : (
<div className="rounded-3xl border border-dashed border-slate-300 bg-white p-10 text-center">
<p className="text-base font-bold text-slate-800">
該当する記事が見つかりませんでした。
</p>
<p className="mt-2 text-sm text-slate-500">
キーワードを変えて検索してください。
</p>
</div>
)}
</section>
);
}
なぜ 'use client' が必要なのか
検索欄は、ユーザーが文字を入力します。
つまり、画面上で状態が変わります。
Reactで状態を扱うには useState を使います。
そのため、このファイルには 'use client' が必要です。
'use client';
Next.jsでは、何でもクライアント側にするのではなく、必要な部分だけクライアント側にするのが基本です。
11. 全体レイアウトを作る
app/layout.tsx を編集します。
// app/layout.tsx
import { Header } from '@/components/Header';
import type { Metadata } from 'next';
import './globals.css';
export const metadata: Metadata = {
title: '岐阜ローカルエンタメ通信',
description:
'岐阜県で起きた架空のローカルエンタメニュースを紹介するブログアプリです。',
};
type RootLayoutProps = {
children: React.ReactNode;
};
/**
* 役割: アプリ全体のHTML構造と共通レイアウトを定義する
* 入力: children
* 出力: ヘッダー付きのHTMLレイアウト
*/
export default function RootLayout({ children }: RootLayoutProps) {
return (
<html lang="ja">
<body>
<Header />
{children}
</body>
</html>
);
}
layout.tsx は、全ページ共通の土台です。
ここに Header を置くことで、トップページにも記事詳細ページにもヘッダーが表示されます。
12. CSSを整える
app/globals.css を編集します。
@import "tailwindcss";
:root {
--background: #f8fafc;
--foreground: #0f172a;
}
html {
scroll-behavior: smooth;
}
body {
min-height: 100vh;
background: var(--background);
color: var(--foreground);
font-family:
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
sans-serif;
}
::selection {
background: #0f172a;
color: #ffffff;
}
Tailwind CSS v4では、基本的に以下を書きます。
@import "tailwindcss";
これだけでTailwindのクラスが使えるようになります。