From 4e642b44db521c647768a1aef3cf26b9063dd29b Mon Sep 17 00:00:00 2001 From: Axel Clark Date: Sun, 6 Oct 2024 16:08:25 -0700 Subject: [PATCH] Add fantasy league calendar download --- lib/ex338/fantasy_leagues.ex | 69 ++++++++++++++++++ lib/ex338/icalendar.ex | 73 +++++++++++++++++++ .../calendar_download_controller.ex | 16 ++++ .../live/fantasy_league_live/show.ex | 27 +++++++ lib/ex338_web/router.ex | 1 + .../calendar_download_controller_test.exs | 26 +++++++ 6 files changed, 212 insertions(+) create mode 100644 lib/ex338/icalendar.ex create mode 100644 lib/ex338_web/controllers/fantasy_league/calendar_download_controller.ex create mode 100644 test/ex338_web/controllers/fantasy_league/calendar_download_controller_test.exs diff --git a/lib/ex338/fantasy_leagues.ex b/lib/ex338/fantasy_leagues.ex index 1f569a27..84a99a79 100644 --- a/lib/ex338/fantasy_leagues.ex +++ b/lib/ex338/fantasy_leagues.ex @@ -3,6 +3,7 @@ defmodule Ex338.FantasyLeagues do import Ecto.Query + alias Ex338.Championships alias Ex338.Championships.Championship alias Ex338.Chats alias Ex338.DraftPicks @@ -187,4 +188,72 @@ defmodule Ex338.FantasyLeagues do Repo.one(query) end + + def generate_calendar(fantasy_league_id) do + fantasy_league = get(fantasy_league_id) + dues_due_date = generate_dues_deadline(fantasy_league) + + championships = + fantasy_league_id + |> Championships.all_for_league() + |> generate_events_for_championships() + + events = + [dues_due_date | championships] + + events + |> Enum.map(&add_uid_and_dtstamp/1) + |> Ex338.ICalendar.to_ics() + end + + defp generate_dues_deadline(fantasy_league) do + %Ex338.ICalendar.Event{ + summary: "Deadline to submit entry fee for The 338 Challenge", + dtstart: Date.new!(fantasy_league.championships_start_at.year, 11, 1) + } + end + + defp generate_events_for_championships(championships) do + Enum.flat_map(championships, &generate_events_for_championship/1) + end + + defp generate_events_for_championship(championship) do + %{ + trade_deadline_at: trade_deadline_at, + waiver_deadline_at: waiver_deadline_at, + championship_at: championship_at + } = championship + + championship_date = + %Ex338.ICalendar.Event{ + summary: "338 Championship: #{championship.title}", + dtstart: DateTime.to_date(championship_at) + } + + if DateTime.compare(trade_deadline_at, waiver_deadline_at) == :eq do + [ + %Ex338.ICalendar.Event{ + summary: "#{championship.title} 338 Waiver and Trade Deadline", + dtstart: DateTime.to_date(championship.waiver_deadline_at) + }, + championship_date + ] + else + [ + %Ex338.ICalendar.Event{ + summary: "#{championship.title} 338 Waiver Deadline", + dtstart: DateTime.to_date(championship.waiver_deadline_at) + }, + %Ex338.ICalendar.Event{ + summary: "#{championship.title} 338 Trade Deadline", + dtstart: DateTime.to_date(championship.trade_deadline_at) + }, + championship_date + ] + end + end + + defp add_uid_and_dtstamp(event) do + %{event | uid: Ecto.UUID.generate(), dtstamp: DateTime.utc_now()} + end end diff --git a/lib/ex338/icalendar.ex b/lib/ex338/icalendar.ex new file mode 100644 index 00000000..d38519fd --- /dev/null +++ b/lib/ex338/icalendar.ex @@ -0,0 +1,73 @@ +defmodule Ex338.ICalendar do + @moduledoc false + + defmodule Event do + @moduledoc false + defstruct summary: nil, + dtstart: nil, + dtend: nil, + dtstamp: nil, + description: nil, + uid: nil + end + + def to_ics(events) when is_list(events) do + events = Enum.map(events, &to_ics/1) + + """ + BEGIN:VCALENDAR + CALSCALE:GREGORIAN + VERSION:2.0 + PRODID:-//Ex338 ICalendar//EN + #{events}END:VCALENDAR + """ + end + + def to_ics(event) do + contents = to_kvs(event) + + """ + BEGIN:VEVENT + #{contents}END:VEVENT + """ + end + + defp to_kvs(event) do + event + |> Map.from_struct() + |> Enum.map(&to_kv/1) + |> List.flatten() + |> Enum.sort() + |> Enum.join() + end + + defp to_kv({key, value}) do + name = + key + |> to_string() + |> String.upcase() + + build(name, value) + end + + def build(_key, nil) do + "" + end + + def build(key, %Date{} = date) do + "#{key}:#{Date.to_iso8601(date, :basic)}\n" + end + + def build(key, %DateTime{} = datetime) do + datetime = + datetime + |> DateTime.truncate(:second) + |> DateTime.to_iso8601(:basic) + + "#{key}:#{datetime}\n" + end + + def build(key, value) do + "#{key}:#{value}\n" + end +end diff --git a/lib/ex338_web/controllers/fantasy_league/calendar_download_controller.ex b/lib/ex338_web/controllers/fantasy_league/calendar_download_controller.ex new file mode 100644 index 00000000..97f2198b --- /dev/null +++ b/lib/ex338_web/controllers/fantasy_league/calendar_download_controller.ex @@ -0,0 +1,16 @@ +defmodule Ex338Web.FantasyLeague.CalendarDownloadController do + use Ex338Web, :controller + + alias Ex338.FantasyLeagues + + def show(conn, %{"fantasy_league_id" => fantasy_league_id}) do + calendar = FantasyLeagues.generate_calendar(fantasy_league_id) + + send_download( + conn, + {:binary, calendar}, + content_type: "text/calendar", + filename: "338_calendar.ics" + ) + end +end diff --git a/lib/ex338_web/live/fantasy_league_live/show.ex b/lib/ex338_web/live/fantasy_league_live/show.ex index 38894972..b3c8fb6f 100644 --- a/lib/ex338_web/live/fantasy_league_live/show.ex +++ b/lib/ex338_web/live/fantasy_league_live/show.ex @@ -92,6 +92,9 @@ defmodule Ex338Web.FantasyLeagueLive.Show do +
+ <.calendar_download fantasy_league={@fantasy_league} /> +
<.live_component module={Ex338Web.FantasyLeagueLive.StandingsChartComponent} @@ -101,4 +104,28 @@ defmodule Ex338Web.FantasyLeagueLive.Show do """ end + + defp calendar_download(assigns) do + ~H""" +
+
+
+

Download League Calendar

+

+ Download an ICS file to add key dates to your calendar. These are all-day events, so + you'll need to check the championships page for the specific deadline times. +

+
+
+ <.link + href={~p"/fantasy_leagues/#{@fantasy_league.id}/calendar_download"} + class="w-full relative inline-flex items-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" + > + Download + +
+
+
+ """ + end end diff --git a/lib/ex338_web/router.ex b/lib/ex338_web/router.ex index f4cbbb0e..f34b3939 100644 --- a/lib/ex338_web/router.ex +++ b/lib/ex338_web/router.ex @@ -105,6 +105,7 @@ defmodule Ex338Web.Router do resources("/waivers", WaiverController, only: [:index]) resources("/trades", TradeController, only: [:index]) resources("/injured_reserves", InjuredReserveController, only: [:index]) + get("/calendar_download", FantasyLeague.CalendarDownloadController, :show) end resources("/archived_leagues", ArchivedLeagueController, only: [:index]) diff --git a/test/ex338_web/controllers/fantasy_league/calendar_download_controller_test.exs b/test/ex338_web/controllers/fantasy_league/calendar_download_controller_test.exs new file mode 100644 index 00000000..fa7affb1 --- /dev/null +++ b/test/ex338_web/controllers/fantasy_league/calendar_download_controller_test.exs @@ -0,0 +1,26 @@ +defmodule Ex338Web.FantasyLeague.CalendarDownloadTest do + use Ex338Web.ConnCase + + setup :register_and_log_in_user + + describe "get/2" do + test "allows a user to download the calendar of events for a fantasy league", %{conn: conn} do + fantasy_league = insert(:fantasy_league, year: 2017) + sports_league = insert(:sports_league) + insert(:league_sport, fantasy_league: fantasy_league, sports_league: sports_league) + insert(:league_sport, fantasy_league: fantasy_league, sports_league: sports_league) + championship = insert(:championship, sports_league: sports_league) + + _championship_event = + insert(:championship, + sports_league: sports_league, + overall: championship, + category: "event" + ) + + conn = get(conn, ~p"/fantasy_leagues/#{fantasy_league.id}/calendar_download") + + assert response_content_type(conn, :ics) + end + end +end