📌
欧州サッカーの過密日程が酷いので試合日程管理アプリを作ってみた
背景
欧州サッカーの過密日程
欧州サッカーに明るい方であれば、近年の過密日程問題についてはご存知かと思います。
これによって選手の怪我、試合の質低下などさまざまな問題が指摘されていますが
観る側も追いきれなくなっています。
複数チームの日程を横断的にみたい
贔屓のチームが一つだけの方はチーム名検索を行えば日程が確認できます。
しかし、私のようなサッカー好きは
- 様々なチームの日程を調べて
- 観戦スケジュールを自分で計画して
- 睡眠時間を削って
観戦をしています。
流石にめんどくさいので今回、複数チームの試合日程を横断的に確認できるアプリを作成しました。
アプリ作る方がめんどくさい?それ、禁句ね。
仕様
データ取得
データ取得はFootball-data.org( https://www.football-data.org/ )からAPI経由で行いました。欧州リーグとかカップ戦など様々なデータの取得ができます。
技術
以下の技術スタックで作成しました。
Rust製GUIフレームワークTauriでビルドを行っています。
データのキャッシュ
チーム情報、試合日程は以下のように取得し、自前のDBに保存しています。
// fetch-data
pub async fn fetch_fixtures(
api_key: &str,
league: &str,
) -> Result<FixturesResponse, Box<dyn std::error::Error>> {
let client = reqwest::Client::new();
let mut headers = HeaderMap::new();
headers.insert("X-Auth-Token", HeaderValue::from_str(api_key)?);
let url = format!(
"https://api.football-data.org/v4/competitions/{}/matches",
league
);
let res = client
.get(url)
.headers(headers)
.send()
.await?
.json::<FixturesResponse>()
.await?;
Ok(res)
}
// save-data
pub async fn save_fixtures_to_db(fixtures: &Vec<Fixture>, client: &Client) -> Result<(), Error> {
client
.query(
"CREATE TABLE IF NOT EXISTS fixtures (
id BIGINT PRIMARY KEY,
competition TEXT,
date TEXT,
home_team TEXT,
away_team TEXT,
status TEXT
)",
&[],
)
.await?;
// delete extisting data
for m in fixtures {
// delete extisting data
client
.query("DELETE FROM fixtures WHERE id = $1", &[&m.id])
.await?;
// insert data
client
.query(
"INSERT INTO fixtures (id, competition, date, home_team, away_team, status)
VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT (id) DO NOTHING",
&[
&m.id,
&m.competition.name,
&m.utc_date,
&m.home_team.short_name,
&m.away_team.short_name,
&m.status,
],
)
.await?;
}
println!(
"{} Fixtures saved to database successfully.",
fixtures[0].competition.name
);
Ok(())
}
アプリ実装
バックエンド
#[command]
async fn get_fixtures(
db_client: State<'_, Arc<Mutex<Client>>>,
selected_teams: Vec<Team>,
) -> Result<Vec<Fixture>, String> {
let client = db_client.lock().await;
let mut fixtures = Vec::new();
for team in selected_teams {
let rows = client
.query(
"SELECT id, competition, date, home_team, away_team, status
FROM fixtures
WHERE (home_team = $1 OR away_team = $1)
AND status = 'TIMED'
ORDER BY date ASC",
&[&team.short_name],
)
.await
.map_err(|e| e.to_string())?;
let team_fixtures: Vec<Fixture> = rows
.into_iter()
.map(|row| Fixture {
id: row.get("id"),
competition: row.get("competition"),
date: row.get("date"),
home_team: row.get("home_team"),
away_team: row.get("away_team"),
status: row.get("status"),
selected_team: team.clone(),
is_home: team.short_name == row.get::<_, String>("home_team"),
})
.collect();
fixtures.extend(team_fixtures);
}
// Return the result
Ok(fixtures)
}
フロントエンド
試合日程はこんな感じで表示しました。
フロントはかなり改善の余地ありかなと思っています。
import { FixtureType } from '@/App';
import { Button, Flex, Text, VStack } from '@chakra-ui/react';
import React from 'react';
import { Tooltip } from './ui/tooltip';
interface FixtureScheduleProps {
fixtures: Record<string, FixtureType[]>;
}
export const FixtureSchedule = (props: FixtureScheduleProps) => {
return (
<VStack gap={5}>
{Object.keys(props.fixtures).length === 0 && (
<Text>No Team Selected</Text>
)}
{Object.entries(props.fixtures).map(([date, fixtures]) => {
return (
<React.Fragment key={date}>
<Text colorPalette={'cyan'}>{date}</Text>
{fixtures.map((fixture) => {
const fixtureDate = new Date(fixture.date);
return (
<Tooltip
content={fixture.competition}
positioning={{ placement: 'bottom-start' }}
key={fixture.id}
>
<Button
colorPalette={'cyan'}
variant={'outline'}
paddingY={7}
width={'75%'}
maxW={500}
>
<Flex
direction='row'
justifyContent='space-between'
alignItems='center'
width='100%'
>
<Text
flexBasis={'30%'}
color={fixture.isHome ? 'yellow.100' : ''}
>
{fixture.homeTeam}
</Text>
<Text flexBasis={'20%'}>{`${fixtureDate
.getHours()
.toString()
.padStart(2, '0')}:${fixtureDate
.getMinutes()
.toString()
.padStart(2, '0')}`}</Text>{' '}
<Text
flexBasis={'30%'}
color={!fixture.isHome ? 'yellow.100' : ''}
>
{fixture.awayTeam}
</Text>
</Flex>
</Button>
</Tooltip>
);
})}
</React.Fragment>
);
})}
</VStack>
);
};
アプリ画面
チーム選択
普通にselectボックス使えばよかったですが
せっかくなのでゲームのFIFAよろしく、carouselで選択できるようにしました。
チーム管理
選択したチームを表示します
スケジュール表示
終わりに
まだまだ改善の余地ありそうです。(デザインとか)
需要があれば配布もしてみたいです!
github
Discussion