概要
vercelのv0が引き続き性能が上がっているとのことなので試してみました。
早速作ってみた
以前作ってみたアスリートトラッカーと同じようなものを作らせてみました。
まず、たった5分で以下のクオリティのものができました。
Triathlon Results - v0 by Vercel
Chat with v0. Generate UI with simple text prompts. Copy, paste, ship.
b_qO7mpEnCZYe.v0.build
v3で上記の物ができたので、3回のプロンプトだけで作れています。
それ以降、デザインを調整したり機能を追加したりすると自分のやりたい方向とはズレてきたり、基本機能がリグレッションしたりするので諦めました。
たとえば、v11は以下のような感じ。
v3のときのソースコード
こちらにおいておきます。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"use client" | |
import { useState, useEffect } from "react" | |
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" | |
import { Input } from "@/components/ui/input" | |
import { Button } from "@/components/ui/button" | |
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" | |
import { ArrowUpDown } from "lucide-react" | |
// 選手データの型定義 | |
type Athlete = { | |
rank: number | |
number: number | |
name: string | |
gender: string | |
age: number | |
country: string | |
swim: string | |
bike: string | |
run: string | |
total: string | |
} | |
// レース種別の定義 | |
const raceTypes = [ | |
{ id: "sado_1", name: "佐渡タイプA", url: "https://gist.githubusercontent.com/matsubo/b81e4b71f3ea280278ef532ec6a1c781/raw/f19260686d4ab24d6b947ca1204557a9d1e572eb/sado_1.csv" }, | |
{ id: "sado_2", name: "佐渡タイプA リレー", url: "https://gist.githubusercontent.com/matsubo/b81e4b71f3ea280278ef532ec6a1c781/raw/f19260686d4ab24d6b947ca1204557a9d1e572eb/sado_2.csv" }, | |
{ id: "sado_3", name: "佐渡タイプB", url: "https://gist.githubusercontent.com/matsubo/b81e4b71f3ea280278ef532ec6a1c781/raw/f19260686d4ab24d6b947ca1204557a9d1e572eb/sado_3.csv" }, | |
{ id: "sado_4", name: "佐渡タイプB リレー", url: "https://gist.githubusercontent.com/matsubo/b81e4b71f3ea280278ef532ec6a1c781/raw/f19260686d4ab24d6b947ca1204557a9d1e572eb/sado_4.csv" }, | |
] | |
// CSVデータを取得し解析する関数 | |
async function fetchAndParseCSV(url: string): Promise<Athlete[]> { | |
const response = await fetch(url) | |
const csvText = await response.text() | |
const lines = csvText.split('\n') | |
const headers = lines[0].split(',') | |
return lines.slice(1).map(line => { | |
const values = line.split(',') | |
return { | |
rank: parseInt(values[0]), | |
number: parseInt(values[1]), | |
name: values[2], | |
gender: values[3], | |
age: parseInt(values[4]), | |
country: values[5], | |
swim: values[6], | |
bike: values[7], | |
run: values[8], | |
total: values[9] | |
} | |
}).filter(athlete => athlete.rank) // 空の行を除外 | |
} | |
export default function TriathlonResults() { | |
const [athletes, setAthletes] = useState<Athlete[]>([]) | |
const [sortColumn, setSortColumn] = useState<keyof Athlete>("rank") | |
const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc") | |
const [searchTerm, setSearchTerm] = useState("") | |
const [selectedRaceType, setSelectedRaceType] = useState(raceTypes[0].id) | |
useEffect(() => { | |
const selectedRace = raceTypes.find(race => race.id === selectedRaceType) | |
if (selectedRace) { | |
fetchAndParseCSV(selectedRace.url).then(setAthletes) | |
} | |
}, [selectedRaceType]) | |
// ソート関数 | |
const sortData = (column: keyof Athlete) => { | |
const newDirection = column === sortColumn && sortDirection === "asc" ? "desc" : "asc" | |
const sortedData = […athletes].sort((a, b) => { | |
if (a[column] < b[column]) return newDirection === "asc" ? -1 : 1 | |
if (a[column] > b[column]) return newDirection === "asc" ? 1 : -1 | |
return 0 | |
}) | |
setAthletes(sortedData) | |
setSortColumn(column) | |
setSortDirection(newDirection) | |
} | |
// 検索関数 | |
const handleSearch = (event: React.ChangeEvent<HTMLInputElement>) => { | |
setSearchTerm(event.target.value) | |
} | |
// フィルタリングされたデータ | |
const filteredAthletes = athletes.filter((athlete) => | |
athlete.name.toLowerCase().includes(searchTerm.toLowerCase()) || | |
athlete.country.toLowerCase().includes(searchTerm.toLowerCase()) | |
) | |
return ( | |
<div className="container mx-auto p-4"> | |
<h1 className="text-2xl font-bold mb-4">トライアスロン リザルト</h1> | |
<div className="flex flex-col md:flex-row gap-4 mb-4"> | |
<Select value={selectedRaceType} onValueChange={setSelectedRaceType}> | |
<SelectTrigger className="w-full md:w-[300px]"> | |
<SelectValue placeholder="レース種別を選択" /> | |
</SelectTrigger> | |
<SelectContent> | |
{raceTypes.map((race) => ( | |
<SelectItem key={race.id} value={race.id}> | |
{race.name} | |
</SelectItem> | |
))} | |
</SelectContent> | |
</Select> | |
<Input | |
type="search" | |
placeholder="選手名または国名で検索…" | |
className="w-full md:w-[300px]" | |
value={searchTerm} | |
onChange={handleSearch} | |
/> | |
</div> | |
<div className="overflow-x-auto"> | |
<Table> | |
<TableHeader> | |
<TableRow> | |
{["rank", "number", "name", "gender", "age", "country", "swim", "bike", "run", "total"].map((column) => ( | |
<TableHead key={column}> | |
{column.charAt(0).toUpperCase() + column.slice(1)} | |
<Button variant="ghost" onClick={() => sortData(column as keyof Athlete)}> | |
<ArrowUpDown className="h-4 w-4" /> | |
</Button> | |
</TableHead> | |
))} | |
</TableRow> | |
</TableHeader> | |
<TableBody> | |
{filteredAthletes.map((athlete) => ( | |
<TableRow key={athlete.number}> | |
{["rank", "number", "name", "gender", "age", "country", "swim", "bike", "run", "total"].map((column) => ( | |
<TableCell key={column}>{athlete[column as keyof Athlete]}</TableCell> | |
))} | |
</TableRow> | |
))} | |
</TableBody> | |
</Table> | |
</div> | |
</div> | |
) | |
} |
コメント