こんにちは!前回のエピソードでは、Supabase Authを使ってユーザー認証システムを構築し、メールとGoogle OAuthでの登録・ログイン機能を実装しました。今回は、Supabase Databaseを活用してコース管理機能を作成します。courses
テーブルを設計し、コース一覧ページ(Course Listing Page - CLP)を構築します。Next.jsのgetStaticProps
とTailwind CSSを使って、レスポンシブで高速なUIを設計し、ページネーションも追加します!
このエピソードのゴール
- Supabaseに
courses
テーブルを設計。 -
getStaticProps
を使ってコースデータを取得。 - Tailwind CSSでコース一覧ページ(CLP)を構築。
- ページネーションまたは無限スクロールを実装。
必要なもの
- 前回のプロジェクト(
next-lms
)がセットアップ済み。 - Supabaseプロジェクト(データベース設定済み)。
- 基本的なTypeScript、React、Next.jsの知識。
ステップ1: Supabaseにcourses
テーブルの設計
Supabaseのデータベースにコース情報を保存するテーブルを作成します。
-
Supabaseダッシュボードでテーブル作成:
- Supabaseダッシュボードにログイン。
- 「Table Editor」→「New Table」で
courses
テーブルを作成。 - 以下のカラムを定義:
カラム名 | タイプ | 説明 |
---|---|---|
id | uuid | 主キー(自動生成) |
title | text | コースタイトル |
description | text | コース説明 |
price | numeric | 価格(円) |
instructor_id | uuid | 講師のユーザーID(外部キー) |
created_at | timestamptz | 作成日時(自動生成) |
updated_at | timestamptz | 更新日時(自動生成) |
- 「Enable Row Level Security (RLS)」を有効化し、以下のようなポリシーを追加:
-
Select Policy: 誰でもコースを閲覧可能(
true
)。 -
Insert/Update Policy: 認証済みユーザーのみ(
auth.uid() = instructor_id
)。
-
Select Policy: 誰でもコースを閲覧可能(
-
テストデータの挿入:
Supabaseの「Table Editor」またはSQL Editorでテストデータを追加:
INSERT INTO courses (title, description, price, instructor_id)
VALUES
('React入門', 'Reactを使ったWeb開発を学びます。', 5000, 'あなたのユーザーID'),
('TypeScriptマスター', 'TypeScriptの基礎から応用まで。', 8000, 'あなたのユーザーID');
注意: instructor_id
はSupabase AuthのユーザーID(ダッシュボードの「Authentication」→「Users」で確認)を使用してください。
ステップ2: コースデータの取得
Next.jsのgetStaticProps
を使って、コースデータを取得します。
-
Supabaseクエリ関数の作成:
src/lib/supabase.ts
にコース取得関数を追加:
import { createClient } from '@supabase/supabase-js';
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs';
import { cookies } from 'next/headers';
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
export const supabase = createClient(supabaseUrl, supabaseKey);
export const supabaseServer = () =>
createServerComponentClient({ cookies }, { supabaseUrl, supabaseKey });
export interface Course {
id: string;
title: string;
description: string;
price: number;
instructor_id: string;
created_at: string;
updated_at: string;
}
export async function getCourses(page: number = 1, limit: number = 10) {
const start = (page - 1) * limit;
const end = start + limit - 1;
const { data, error } = await supabase
.from('courses')
.select('*')
.range(start, end)
.order('created_at', { ascending: false });
if (error) throw error;
return data as Course[];
}
export async function getCoursesCount() {
const { count, error } = await supabase
.from('courses')
.select('*', { count: 'exact', head: true });
if (error) throw error;
return count || 0;
}
このコードは:
-
getCourses
: 指定されたページと制限に基づいてコースを取得。 -
getCoursesCount
: 総コース数を返し、ページネーションに使用。
ステップ3: コース一覧ページ(CLP)の構築
コース一覧ページを構築し、Tailwind CSSでレスポンシブなデザインを適用します。src/app/courses/page.tsx
を作成:
import Link from 'next/link';
import Image from 'next/image';
import { getCourses, getCoursesCount, Course } from '@/lib/supabase';
interface CoursesPageProps {
searchParams: { page?: string };
}
export default async function CoursesPage({ searchParams }: CoursesPageProps) {
const page = parseInt(searchParams.page || '1', 10);
const limit = 6; // 1ページあたり6コース
const courses = await getCourses(page, limit);
const totalCourses = await getCoursesCount();
const totalPages = Math.ceil(totalCourses / limit);
return (
<main className="container mx-auto p-4">
<h1 className="text-3xl font-bold mb-6">コース一覧</h1>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
{courses.map((course) => (
<Link href={`/courses/${course.id}`} key={course.id}>
<div className="border rounded-lg p-4 hover:shadow-lg transition">
<Image
src={`/placeholder-course-${course.id}.jpg`} // 仮の画像(後でSupabase Storageに置き換え)
alt={course.title}
width={300}
height={200}
className="w-full h-40 object-cover rounded"
loading="lazy"
/>
<h2 className="text-xl font-semibold mt-2">{course.title}</h2>
<p className="text-gray-600 line-clamp-2">{course.description}</p>
<p className="text-primary font-bold mt-2">{course.price.toLocaleString()} 円</p>
</div>
</Link>
))}
</div>
{/* ページネーション */}
<div className="flex justify-center gap-4 mt-8">
{page > 1 && (
<Link href={`/courses?page=${page - 1}`} className="px-4 py-2 bg-gray-200 rounded hover:bg-gray-300">
前へ
</Link>
)}
{page < totalPages && (
<Link href={`/courses?page=${page + 1}`} className="px-4 py-2 bg-gray-200 rounded hover:bg-gray-300">
次へ
</Link>
)}
</div>
</main>
);
}
このコードは:
-
getStaticProps
の代わりにサーバーコンポーネントでデータ取得(App Router)。 - コースをカード形式で表示し、Tailwind CSSでレスポンシブデザインを適用。
- ページネーションを実装(前へ/次へボタン)。
- 仮の画像を使用(次のエピソードでSupabase Storageに置き換え)。
ステップ4: ナビゲーションの更新
ヘッダーに「コース一覧」リンクを追加します。src/app/layout.tsx
を更新:
import '../styles/globals.css';
import Link from 'next/link';
import { supabaseServer } from '@/lib/supabase';
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const supabase = supabaseServer();
const { data: { user } } = await supabase.auth.getUser();
return (
<html lang="ja">
<body className="bg-gray-100">
<header className="bg-primary text-white p-4">
<div className="container mx-auto flex justify-between items-center">
<h1 className="text-2xl font-bold">
<Link href="/">Next.js LMS</Link>
</h1>
<nav className="flex gap-4">
<Link href="/courses" className="hover:underline">コース</Link>
{user ? (
<>
<Link href="/dashboard" className="hover:underline">ダッシュボード</Link>
<form
action={async () => {
'use server';
await supabase.auth.signOut();
}}
className="inline"
>
<button className="hover:underline">ログアウト</button>
</form>
</>
) : (
<>
<Link href="/login" className="hover:underline">ログイン</Link>
<Link href="/register" className="hover:underline">登録</Link>
</>
)}
</nav>
</div>
</header>
{children}
</body>
</html>
);
}
ステップ5: 動作確認
- Supabaseダッシュボードで
courses
テーブルとテストデータが正しいことを確認。 - 開発サーバーを起動:
npm run dev
-
http://localhost:3000/courses
にアクセスし、以下の点を確認:- コース一覧がカード形式で表示される。
- タイトル、説明、価格が正しく表示される。
- ページネーションの「前へ」「次へ」ボタンが機能する(データが多い場合)。
- モバイルデバイスでもレスポンシブに表示される。
- ヘッダーの「コース」リンクをクリックし、
/courses
に遷移することを確認。
エラーがあれば、Supabaseのテーブル設定やクエリログを確認してください。
まとめと次のステップ
このエピソードでは、Supabase Databaseにcourses
テーブルを設計し、コース一覧ページ(CLP)を構築しました。getStaticProps
の代わりにサーバーコンポーネントを活用し、ページネーションを追加することで、ユーザーがコースを簡単に閲覧できるようにしました。
次回のエピソードでは、コース詳細ページ(CDP)を構築し、SEO最適化を行います。getStaticPaths
とgetStaticProps
を使って静的ページを生成し、Next.js Imageで画像を最適化しますので、引き続きお楽しみに!
この記事が役に立ったと思ったら、ぜひ「いいね」を押して、ストックしていただければ嬉しいです!次回のエピソードもお楽しみに!