Created
October 17, 2024 02:40
-
-
Save matsubo/de038bd556b24a659f800a1a98665ce2 to your computer and use it in GitHub Desktop.
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> | |
) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment