遊戯王OCG (トレーディングカードゲーム) において、デッキに投入できるカードパーツの組、いわゆる「出張セット」のメモを簡単に作成できるWebサイトを考えています。 1つの「出張セット」には「1枚以上の遊戯王カードの組 (種類および枚数)」と「出張セットの名前」と「その出張セットの説明文」をセットできます。 ユーザーは、出張セットを作成・編集・閲覧・削除することができます。 主に必要な画面として、「出張セットの一覧を表示する画面」「選択した出張セットの詳細を表示する画面」「選択した出張セットを編集する画面」「新しい出張セットを作成する画面」「出張セットを削除する際の確認画面」を予定しています。 自分だけ使う想定なので、認証機能を付ける予定はありません。 WebサイトはPCおよびスマホで表示することを想定しています。 データベースとしてはsupabaseを使うことを予定しています。 プログラミング言語としてはTypeScriptでお願いします。
はい、承知しました。遊戯王OCGの「出張セット」管理のためのWebサイトを作成するプロジェクトですね。TypeScriptを使用し、supabaseをデータベースとして利用する予定とのことです。それでは、このプロジェクトの基本的な構造と主要なコンポーネントを作成していきましょう。
まず、プロジェクトの基本構造を設定し、必要なコンポーネントを作成します。
まず、プロジェクトの基本構造を設定しましょう。
npx create-next-app@latest yugioh-deck-parts --typescript --tailwind --eslint
cd yugioh-deck-parts
npm install @supabase/supabase-js
lib/supabaseClient.ts
ファイルを作成し、以下のコードを追加します:
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
.env.local
ファイルをプロジェクトのルートに作成し、Supabaseの認証情報を追加します:
NEXT_PUBLIC_SUPABASE_URL=あなたのSupabase URLをここに入れてください
NEXT_PUBLIC_SUPABASE_ANON_KEY=あなたのSupabase Anon Keyをここに入れてください
次に、主要なコンポーネントを作成していきます。
import React from 'react'
import Link from 'next/link'
import { supabase } from '../lib/supabaseClient'
type DeckPart = {
id: number
name: string
description: string
}
export default function DeckPartsList() {
const [deckParts, setDeckParts] = React.useState<DeckPart[]>([])
React.useEffect(() => {
fetchDeckParts()
}, [])
async function fetchDeckParts() {
const { data, error } = await supabase
.from('deck_parts')
.select('*')
if (error) console.error('Error fetching deck parts:', error)
else setDeckParts(data || [])
}
return (
<div className="container mx-auto px-4">
<h1 className="text-2xl font-bold my-4">出張セット一覧</h1>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{deckParts.map((part) => (
<Link href={`/deck-part/${part.id}`} key={part.id}>
<div className="border p-4 rounded-lg hover:shadow-lg transition-shadow">
<h2 className="text-xl font-semibold">{part.name}</h2>
<p className="text-gray-600 truncate">{part.description}</p>
</div>
</Link>
))}
</div>
<Link href="/create">
<button className="mt-4 bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
新規作成
</button>
</Link>
</div>
)
}
import React from 'react'
import { useRouter } from 'next/router'
import Link from 'next/link'
import { supabase } from '../../lib/supabaseClient'
type DeckPart = {
id: number
name: string
description: string
cards: { name: string; count: number }[]
}
export default function DeckPartDetail() {
const router = useRouter()
const { id } = router.query
const [deckPart, setDeckPart] = React.useState<DeckPart | null>(null)
React.useEffect(() => {
if (id) fetchDeckPart()
}, [id])
async function fetchDeckPart() {
const { data, error } = await supabase
.from('deck_parts')
.select('*')
.eq('id', id)
.single()
if (error) console.error('Error fetching deck part:', error)
else setDeckPart(data)
}
if (!deckPart) return <div>Loading...</div>
return (
<div className="container mx-auto px-4">
<h1 className="text-2xl font-bold my-4">{deckPart.name}</h1>
<p className="text-gray-600 mb-4">{deckPart.description}</p>
<h2 className="text-xl font-semibold mb-2">カード一覧</h2>
<ul className="list-disc pl-5">
{deckPart.cards.map((card, index) => (
<li key={index}>
{card.name} x {card.count}
</li>
))}
</ul>
<div className="mt-4 space-x-2">
<Link href={`/edit/${deckPart.id}`}>
<button className="bg-yellow-500 text-white px-4 py-2 rounded hover:bg-yellow-600">
編集
</button>
</Link>
<button className="bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600">
削除
</button>
</div>
</div>
)
}
import React from 'react'
import { useRouter } from 'next/router'
import { supabase } from '../../lib/supabaseClient'
type DeckPart = {
id: number
name: string
description: string
cards: { name: string; count: number }[]
}
export default function EditDeckPart() {
const router = useRouter()
const { id } = router.query
const [deckPart, setDeckPart] = React.useState<DeckPart | null>(null)
React.useEffect(() => {
if (id) fetchDeckPart()
}, [id])
async function fetchDeckPart() {
const { data, error } = await supabase
.from('deck_parts')
.select('*')
.eq('id', id)
.single()
if (error) console.error('Error fetching deck part:', error)
else setDeckPart(data)
}
async function handleSubmit(event: React.FormEvent) {
event.preventDefault()
if (!deckPart) return
const { error } = await supabase
.from('deck_parts')
.update(deckPart)
.eq('id', deckPart.id)
if (error) console.error('Error updating deck part:', error)
else router.push(`/deck-part/${deckPart.id}`)
}
if (!deckPart) return <div>Loading...</div>
return (
<div className="container mx-auto px-4">
<h1 className="text-2xl font-bold my-4">出張セットを編集</h1>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
名前
</label>
<input
type="text"
id="name"
value={deckPart.name}
onChange={(e) => setDeckPart({ ...deckPart, name: e.target.value })}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
/>
</div>
<div>
<label htmlFor="description" className="block text-sm font-medium text-gray-700">
説明
</label>
<textarea
id="description"
value={deckPart.description}
onChange={(e) => setDeckPart({ ...deckPart, description: e.target.value })}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
/>
</div>
{/* カードの編集機能はここに追加 */}
<button type="submit" className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
更新
</button>
</form>
</div>
)
}
import React from 'react'
import { useRouter } from 'next/router'
import { supabase } from '../lib/supabaseClient'
type DeckPart = {
name: string
description: string
cards: { name: string; count: number }[]
}
export default function CreateDeckPart() {
const router = useRouter()
const [deckPart, setDeckPart] = React.useState<DeckPart>({
name: '',
description: '',
cards: []
})
async function handleSubmit(event: React.FormEvent) {
event.preventDefault()
const { data, error } = await supabase
.from('deck_parts')
.insert(deckPart)
if (error) console.error('Error creating deck part:', error)
else router.push('/')
}
return (
<div className="container mx-auto px-4">
<h1 className="text-2xl font-bold my-4">新しい出張セットを作成</h1>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
名前
</label>
<input
type="text"
id="name"
value={deckPart.name}
onChange={(e) => setDeckPart({ ...deckPart, name: e.target.value })}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
/>
</div>
<div>
<label htmlFor="description" className="block text-sm font-medium text-gray-700">
説明
</label>
<textarea
id="description"
value={deckPart.description}
onChange={(e) => setDeckPart({ ...deckPart, description: e.target.value })}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
/>
</div>
{/* カードの追加機能はここに実装 */}
<button type="submit" className="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600">
作成
</button>
</form>
</div>
)
}
import React from 'react'
import { supabase } from '../lib/supabaseClient'
type DeleteModalProps = {
isOpen: boolean
onClose: () => void
deckPartId: number
onDelete: () => void
}
export default function DeleteModal({ isOpen, onClose, deckPartId, onDelete }: DeleteModalProps) {
async function handleDelete() {
const { error } = await supabase
.from('deck_parts')
.delete()
.eq('id', deckPartId)
if (error) console.error('Error deleting deck part:', error)
else {
onDelete()
onClose()
}
}
if (!isOpen) return null
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center">
<div className="bg-white p-4 rounded-lg">
<h2 className="text-xl font-bold mb-4">出張セットを削除しますか?</h2>
<p className="mb-4">この操作は取り消せません。</p>
<div className="flex justify-end space-x-2">
<button onClick={onClose} className="bg-gray-300 text-black px-4 py-2 rounded hover:bg-gray-400">
キャンセル
</button>
<button onClick={handleDelete} className="bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600">
削除
</button>
</div>
</div>
</div>
)
}
これらのコンポーネントを使用して、遊戯王OCGの「出張セット」管理Webサイトの基本的な機能を実装できます。各コンポーネントは、それぞれの画面や機能に対応しています。
DeckPartsList
: 出張セットの一覧を表示する画面DeckPartDetail
: 選択した出張セットの詳細を表示する画面EditDeckPart
: 選択した出張セットを編集する画面CreateDeckPart
: 新しい出張セットを作成する画面DeleteModal
: 出張セットを削除する際の確認モーダルこれらのコンポーネントを適切にルーティングし、必要に応じて追加の機能(例:カードの追加・削除、バリデーション、エラーハンドリングなど)を実装することで、完全な「出張セット」管理Webサイトを作成できます。
また、レスポンシブデザインについては、Tailwind CSSのクラスを使用して実装しています。例えば、grid-cols-1 md:grid-cols-2 lg:grid-cols-3
のようなクラスを使用して、画面サイズに応じてレイアウトが変更されるようにしています。
このプロジェクトをさらに発展させるためには、以下のような機能を追加することができます:
これらの機能を順次実装していくことで、より使いやすく機能的な遊戯王OCGの「出張セット」管理Webサイトを作成できるでしょう。