📌

欧州サッカーの過密日程が酷いので試合日程管理アプリを作ってみた

2024/11/23に公開

背景

欧州サッカーの過密日程

欧州サッカーに明るい方であれば、近年の過密日程問題についてはご存知かと思います。
これによって選手の怪我、試合の質低下などさまざまな問題が指摘されていますが
観る側も追いきれなくなっています。

複数チームの日程を横断的にみたい

贔屓のチームが一つだけの方はチーム名検索を行えば日程が確認できます。
Screenshot 2024-11-23 at 16.17.55.png
しかし、私のようなサッカー好きは

  • 様々なチームの日程を調べて
  • 観戦スケジュールを自分で計画して
  • 睡眠時間を削って

観戦をしています。
流石にめんどくさいので今回、複数チームの試合日程を横断的に確認できるアプリを作成しました。
アプリ作る方がめんどくさい?それ、禁句ね。

仕様

データ取得

データ取得はFootball-data.org( https://www.football-data.org/ )からAPI経由で行いました。欧州リーグとかカップ戦など様々なデータの取得ができます。

技術

以下の技術スタックで作成しました。
Rust製GUIフレームワークTauriでビルドを行っています。
Screenshot 2024-11-23 at 16.07.35.png

データのキャッシュ

チーム情報、試合日程は以下のように取得し、自前の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で選択できるようにしました。
Screenshot 2024-11-23 at 15.42.43.png

チーム管理

選択したチームを表示します
Screenshot 2024-11-23 at 16.36.38.png

スケジュール表示

Screenshot 2024-11-23 at 15.43.17.png

終わりに

まだまだ改善の余地ありそうです。(デザインとか)
需要があれば配布もしてみたいです!

github
https://github.com/ReoMash/football-schedule-app

Discussion