diff --git a/lib/dev_round/events.ex b/lib/dev_round/events.ex index 758ff66..44349e9 100644 --- a/lib/dev_round/events.ex +++ b/lib/dev_round/events.ex @@ -40,6 +40,16 @@ defmodule DevRound.Events do |> Repo.all() end + def list_registered_events_for_user(user_id) do + from(e in Event, + join: ea in assoc(e, :events_attendees), + where: ea.user_id == ^user_id and e.published, + order_by: [asc: e.begin] + ) + |> Repo.all() + |> Repo.preload([:langs, sessions: from(s in EventSession, order_by: s.begin)]) + end + def get_event_archival_datetime_utc do tz = Application.get_env(:dev_round, :time_zone) diff --git a/lib/dev_round/formats.ex b/lib/dev_round/formats.ex index 41a4347..82685b4 100644 --- a/lib/dev_round/formats.ex +++ b/lib/dev_round/formats.ex @@ -138,6 +138,40 @@ defmodule DevRound.Formats do "#{format_time(dt1)} – #{format_time(dt2)}" end + @doc """ + Formats a datetime range into a compact string. + + The formatting adapts based on whether the dates are the same and match the current date: + - Today and same day: Shows time range (e.g., "16:00 – 18:00") + - Otherwise: Shows full datetime range (see `format_datetime_range/2`) + + ## Examples + + iex> format_datetime_range_compact(~U[2026-03-24 16:00:00Z], ~U[2026-03-24 18:00:00Z]) + "16:00 – 18:00" + + iex> format_datetime_range_compact(~U[2025-03-14 16:00:00Z], ~U[2025-03-15 18:00:00Z]) + "14.03.2025 16:00 – 15.03.2025 18:00" + + ## Parameters + + - `dt1`: The start datetime + - `dt2`: The end datetime + + ## Returns + + A formatted string representing the date/time range or just the time range. + """ + def format_datetime_range_compact(dt1, dt2) do + {:ok, now} = DateTime.now(dt1.time_zone) + + if Date.compare(now, dt1) == :eq and Date.compare(dt1, dt2) == :eq do + format_time_range(dt1, dt2) + else + format_datetime_range(dt1, dt2) + end + end + @doc """ Generates an avatar placeholder from a user's full name. diff --git a/lib/dev_round/hosting.ex b/lib/dev_round/hosting.ex index 9463fca..0b67247 100644 --- a/lib/dev_round/hosting.ex +++ b/lib/dev_round/hosting.ex @@ -183,6 +183,20 @@ defmodule DevRound.Hosting do |> Repo.preload(members: {member_query, [:user, :langs]}) end + @doc """ + Lists all teams for a given user across multiple sessions. + Returns a map of session_id => team + """ + def list_teams_for_user_in_sessions(user_id, session_ids) do + from(t in Team, + join: m in assoc(t, :members), + where: t.session_id in ^session_ids and m.user_id == ^user_id + ) + |> Repo.all() + |> Repo.preload([:lang, members: [:user, :langs]]) + |> Enum.into(%{}, fn team -> {team.session_id, team} end) + end + def build_teams_for_session(%EventSession{} = session, attendees, team_names) do attendees = filter_checked(attendees) {:ok, []} = validate_team_generation_constraints(attendees, team_names) diff --git a/lib/dev_round_web/components/event_components.ex b/lib/dev_round_web/components/event_components.ex index 3adbc56..e8cf74f 100644 --- a/lib/dev_round_web/components/event_components.ex +++ b/lib/dev_round_web/components/event_components.ex @@ -8,8 +8,64 @@ defmodule DevRoundWeb.EventComponents do use Phoenix.Component use DevRoundWeb, :verified_routes + import DevRoundWeb.CoreComponents + alias DevRound.Events.EventSession alias DevRound.Hosting.Team + attr :events, :list, required: true + attr :title, :string, required: true + attr :accent_class, :string, required: true + slot :placeholder + + def event_grid_listing(assigns) do + ~H""" +
+
+

{@title}

+
+ {length(@events)} +
+
+ + <%= if @events == [] do %> + {render_slot(@placeholder)} + <% else %> +
+ <.link + :for={event <- @events} + :key={event.id} + patch={~p"/events/#{event}"} + class="card card-sm bg-base-300 shadow-md transition-shadow duration-200 border border-base-content/10 hover:border-primary/50" + > +
+

+ {event.title} +

+ +

{event.teaser}

+ +
+
+ <.icon name="hero-calendar" class="w-4 h-4" /> + {DevRound.Formats.format_datetime_range( + event.begin |> DateTime.shift_zone!(DevRound.Formats.time_zone()), + event.end |> DateTime.shift_zone!(DevRound.Formats.time_zone()) + )} +
+ +
+ <.icon name="hero-map-pin" class="w-4 h-4" /> + {event.location} +
+
+
+ +
+ <% end %> +
+ """ + end + attr :team, Team, required: true attr :show_member_experience_level, :boolean, required: true attr :show_member_langs, :boolean, required: true @@ -64,4 +120,80 @@ defmodule DevRoundWeb.EventComponents do """ end + + attr :session, EventSession, required: true + attr :class, :string, default: nil + + def session(assigns) do + ~H""" +
+
+

+ + {@session.title} + + <.live_badge :if={@session.live} /> +

+
+
+
+ Begin & End +
+
+ {DevRound.Formats.format_datetime_range_compact(@session.begin, @session.end)} +
+
+ + <%= if @session.live do %> +
+
+ Time Remaining +
+ <.live_component + module={DevRoundWeb.EventSessionCountdownLive} + id={"countdown-#{@session.id}"} + event_session={@session} + class="text-lg font-mono" + style={nil} + /> +
+ <% end %> +
+
+
+
+ """ + end + + def live_badge(assigns) do + ~H""" +
+ + + + + + Live +
+ """ + end + + attr :title, :string, required: false + slot :inner_block + + def content_placeholder(assigns) do + ~H""" +
+
+
+ +
+

{@title}

+

+ {render_slot(@inner_block)} +

+
+
+ """ + end end diff --git a/lib/dev_round_web/components/event_session_countdown_live.ex b/lib/dev_round_web/components/event_session_countdown_live.ex index cc12b4b..dafbe1a 100644 --- a/lib/dev_round_web/components/event_session_countdown_live.ex +++ b/lib/dev_round_web/components/event_session_countdown_live.ex @@ -43,7 +43,7 @@ defmodule DevRoundWeb.EventSessionCountdownLive do @impl true def render(assigns) do ~H""" -
+
<%= if @time_remaining do %>
<%= if @time_remaining.day_digits > 0 do %> diff --git a/lib/dev_round_web/components/event_session_teams_slide_live.ex b/lib/dev_round_web/components/event_session_teams_slide_live.ex index c6a32c8..42ad2c5 100644 --- a/lib/dev_round_web/components/event_session_teams_slide_live.ex +++ b/lib/dev_round_web/components/event_session_teams_slide_live.ex @@ -73,21 +73,10 @@ defmodule DevRoundWeb.EventSessionTeamsSlideLive do end defp assign_dates(socket, event_session) do - {:ok, now} = DateTime.now(Formats.time_zone()) - - if Date.compare(now, event_session.begin_local) == :eq and - Date.compare(event_session.begin_local, event_session.end_local) == :eq do - socket - |> assign( - :time, - Formats.format_time_range(event_session.begin_local, event_session.end_local) - ) - else - socket - |> assign( - :time, - Formats.format_datetime_range(event_session.begin_local, event_session.end_local) - ) - end + assign( + socket, + :time, + Formats.format_datetime_range_compact(event_session.begin, event_session.end) + ) end end diff --git a/lib/dev_round_web/components/layouts.ex b/lib/dev_round_web/components/layouts.ex index 5759698..648ef5c 100644 --- a/lib/dev_round_web/components/layouts.ex +++ b/lib/dev_round_web/components/layouts.ex @@ -163,6 +163,11 @@ defmodule DevRoundWeb.Layouts do /> <:menu class="w-52 p-2"> +
  • + <.link href={~p"/user/events"}> + Your Events + +
  • <.link href={~p"/users/settings"}> Settings diff --git a/lib/dev_round_web/live/event_live/index.ex b/lib/dev_round_web/live/event_live/index.ex index 6ff6137..5a809c9 100644 --- a/lib/dev_round_web/live/event_live/index.ex +++ b/lib/dev_round_web/live/event_live/index.ex @@ -14,53 +14,4 @@ defmodule DevRoundWeb.EventLive.Index do def handle_params(_params, _url, socket) do {:noreply, socket} end - - def event_list(assigns) do - ~H""" -
    -
    -
    - {length(@events)} -
    -

    {@title}

    -
    - - <%= if @events == [] do %> - {render_slot(@placeholder)} - <% else %> -
    - <.link - :for={event <- @events} - :key={event.id} - patch={~p"/events/#{event}"} - class="card card-sm bg-base-300 shadow-md transition-shadow duration-200 border border-base-300 hover:border-primary/50" - > -
    -

    - {event.title} -

    - -

    {event.teaser}

    - -
    -
    - <.icon name="hero-calendar" class="w-4 h-4" /> - {DevRound.Formats.format_datetime_range( - event.begin |> DateTime.shift_zone!(DevRound.Formats.time_zone()), - event.end |> DateTime.shift_zone!(DevRound.Formats.time_zone()) - )} -
    - -
    - <.icon name="hero-map-pin" class="w-4 h-4" /> - {event.location} -
    -
    -
    - -
    - <% end %> -
    - """ - end end diff --git a/lib/dev_round_web/live/event_live/index.html.heex b/lib/dev_round_web/live/event_live/index.html.heex index 450844b..5cf74eb 100644 --- a/lib/dev_round_web/live/event_live/index.html.heex +++ b/lib/dev_round_web/live/event_live/index.html.heex @@ -4,21 +4,25 @@ <:subtitle>Discover current and archived events - <.event_list events={@current_events} title="Current Events" accent_class="primary"> + <:placeholder>
    -

    No upcoming events

    +

    No current events

    Check back soon for new events!

    - +
    - <.event_list +
    -
    +
    Scheduled Begin
    diff --git a/lib/dev_round_web/live/user_events_live.ex b/lib/dev_round_web/live/user_events_live.ex new file mode 100644 index 0000000..3ad9b1f --- /dev/null +++ b/lib/dev_round_web/live/user_events_live.ex @@ -0,0 +1,275 @@ +defmodule DevRoundWeb.UserEventsLive do + use DevRoundWeb, :live_view + use DevRoundWeb.EventSessionCountdownLive, :relay_countdown_ticks + + alias DevRound.Events + alias DevRound.Events.Event + alias DevRoundWeb.Layouts + + def mount(_params, _session, socket) do + if connected?(socket) do + DevRoundWeb.Endpoint.subscribe("registrations") + DevRoundWeb.Endpoint.subscribe("admin.events") + DevRoundWeb.Endpoint.subscribe("event_sessions") + + # Schedule midnight rollover check + schedule_midnight_check() + end + + {:ok, socket} + end + + def handle_params(params, _url, socket) do + expanded_ids = + case params["expanded"] do + ids when is_list(ids) -> ids + id when is_binary(id) -> [id] + _ -> [] + end + |> Enum.map(&String.to_integer/1) + |> MapSet.new() + + {:noreply, + socket + |> assign(:expanded_ids, expanded_ids) + |> assign_events()} + end + + def render(assigns) do + ~H""" + + <.header> + Your Events + + +
    + <%!-- underway Section --%> +
    +

    + Up Next +

    +
    + <.event_card :for={event <- @underway_events} event={event} teams_map={@teams_map} /> +
    +
    + + <%!-- Upcoming Section --%> +
    + +
    + + <%!-- Archived Section --%> +
    +
    +

    Archived Events

    +
    + {length(@archived_events)} +
    +
    +
    + <.event_card + :for={event <- @archived_events} + event={event} + teams_map={@teams_map} + collapsable={true} + expanded={MapSet.member?(@expanded_ids, event.id)} + /> +
    +
    + + + Check out the <.link patch={~p"/events"} class="link">events page + to find your first event! + +
    +
    + """ + end + + attr :event, Event, required: true + attr :teams_map, :map, required: true + attr :collapsable, :boolean, default: false + attr :expanded, :boolean, default: true + + defp event_card(assigns) do + ~H""" +
    +
    +
    +

    + <.link patch={~p"/events/#{@event.slug}"}> + {@event.title} + +

    +

    {@event.teaser}

    +
    + + <.icon name="hero-calendar" /> + {DevRound.Formats.format_datetime_range(@event.begin_local, @event.end_local)} + + + <.icon name="hero-map-pin" /> {@event.location} + +
    +
    +
    + + <.button patch={~p"/events/#{@event.slug}"} variant="primary"> + View Event + +
    +
    +
    +

    + <.icon name="hero-list-bullet" class="size-4" /> Sessions & Teams +

    + <%= for session <- @event.sessions do %> +
    + +
    +
    + <%= if Map.has_key?(@teams_map, session.id) do %> + + <% else %> +
    + The teams for this session have not been assigned yet. +
    + <% end %> +
    + <% end %> +
    +
    + """ + end + + defp toggle_expand(expanded, event_id) do + JS.push("toggle_expand", value: %{id: event_id, expanded: expanded}) + end + + def handle_event("toggle_expand", %{"id" => id, "expanded" => expanded}, socket) do + expanded_ids = + if expanded do + MapSet.delete(socket.assigns.expanded_ids, id) + else + MapSet.put(socket.assigns.expanded_ids, id) + end + + params = + if Enum.empty?(expanded_ids), + do: %{}, + else: %{expanded: MapSet.to_list(expanded_ids)} + + {:noreply, push_patch(socket, to: ~p"/user/events?#{params}")} + end + + def handle_info({:registrations, _operation, _event, attendee}, socket) do + if attendee.user_id == socket.assigns.current_user.id do + {:noreply, socket |> assign_events()} + else + {:noreply, socket} + end + end + + def handle_info(_, socket) do + {:noreply, socket |> assign_events()} + end + + defp assign_events(socket) do + user_id = socket.assigns.current_user.id + events = Events.list_registered_events_for_user(user_id) + + tz = Application.get_env(:dev_round, :time_zone) + {:ok, now} = DateTime.now(tz) + + underway = + Enum.filter(events, fn e -> + begin_local = e.begin |> DateTime.shift_zone!(tz) + end_local = e.end |> DateTime.shift_zone!(tz) + + archival_datetime = + end_local + |> DateTime.to_date() + |> Date.add(1) + |> DateTime.new!(~T[00:00:00], tz) + + DateTime.compare(now, begin_local) in [:gt, :eq] and + DateTime.compare(now, archival_datetime) == :lt + end) + + upcoming = + Enum.filter(events, fn e -> + begin_local = e.begin |> DateTime.shift_zone!(tz) + DateTime.compare(begin_local, now) == :gt + end) + + archived = + Enum.filter(events, fn e -> + end_local = e.end |> DateTime.shift_zone!(tz) + + archival_datetime = + end_local + |> DateTime.to_date() + |> Date.add(1) + |> DateTime.new!(~T[00:00:00], tz) + + DateTime.compare(now, archival_datetime) in [:gt, :eq] + end) + + # Fetch teams for underway sessions + expanded archived sessions + expanded_ids = socket.assigns[:expanded_ids] || MapSet.new() + + session_ids = + Enum.flat_map(underway, & &1.sessions) + |> Enum.concat( + Enum.filter(archived, &MapSet.member?(expanded_ids, &1.id)) + |> Enum.flat_map(& &1.sessions) + ) + |> Enum.filter(fn session -> session.teams_locked end) + |> Enum.map(& &1.id) + + teams_map = DevRound.Hosting.list_teams_for_user_in_sessions(user_id, session_ids) + + socket + |> assign(:underway_events, underway) + |> assign(:upcoming_events, upcoming) + |> assign(:archived_events, archived) + |> assign(:teams_map, teams_map) + end + + defp schedule_midnight_check do + tz = Application.get_env(:dev_round, :time_zone) + {:ok, now} = DateTime.now(tz) + + next_midnight = + now + |> DateTime.to_date() + |> Date.add(1) + |> DateTime.new!(~T[00:00:01], tz) + + diff_ms = DateTime.diff(next_midnight, now, :millisecond) + Process.send_after(self(), :refresh_events, diff_ms) + end +end diff --git a/lib/dev_round_web/router.ex b/lib/dev_round_web/router.ex index 46b8c44..596f190 100644 --- a/lib/dev_round_web/router.ex +++ b/lib/dev_round_web/router.ex @@ -30,6 +30,7 @@ defmodule DevRoundWeb.Router do live_session :main, on_mount: [{DevRoundWeb.UserAuth, :ensure_authenticated}] do + live "/user/events", UserEventsLive, :show live "/users/settings", UserSettingsLive, :edit live "/events", EventLive.Index, :index live "/events/:slug", EventLive.Show, :show diff --git a/test/dev_round/events_test.exs b/test/dev_round/events_test.exs index 923baa3..0a43aff 100644 --- a/test/dev_round/events_test.exs +++ b/test/dev_round/events_test.exs @@ -53,6 +53,23 @@ defmodule DevRound.EventsTest do event_fixture(%{published: false}) assert Events.list_events(:current) == [] end + + test "list_registered_events_for_user/1 returns registered events" do + future_deadline = NaiveDateTime.add(NaiveDateTime.local_now(), 12, :hour) + event = event_fixture(%{registration_deadline_local: future_deadline}) + user = user_fixture() + + {:ok, _attendee} = + Events.create_event_attendee(event, user, %{ + "lang_ids" => [Enum.at(event.langs, 0).id] + }) + + registered_events = Events.list_registered_events_for_user(user.id) + assert length(registered_events) == 1 + assert hd(registered_events).id == event.id + assert hd(registered_events).sessions != [] + assert hd(registered_events).langs != [] + end end describe "get_event!" do diff --git a/test/dev_round/formats_test.exs b/test/dev_round/formats_test.exs index 06636ba..46ba447 100644 --- a/test/dev_round/formats_test.exs +++ b/test/dev_round/formats_test.exs @@ -106,6 +106,51 @@ defmodule DevRound.FormatsTest do end end + describe "format_datetime_range_compact/2" do + test "formats only time range if datetime is today" do + tz = Formats.time_zone() + {:ok, now} = DateTime.now(tz) + dt1 = DateTime.to_naive(now) |> NaiveDateTime.truncate(:second) |> DateTime.from_naive!(tz) + dt1 = %{dt1 | hour: 16, minute: 0, second: 0} + dt2 = %{dt1 | hour: 18, minute: 0, second: 0} + + assert Formats.format_datetime_range_compact(dt1, dt2) == "16:00 – 18:00" + end + + test "formats full datetime range if datetime is NOT today" do + tz = Formats.time_zone() + {:ok, now} = DateTime.now(tz) + yesterday = DateTime.add(now, -1, :day) + + dt1 = + DateTime.to_naive(yesterday) + |> NaiveDateTime.truncate(:second) + |> DateTime.from_naive!(tz) + + dt1 = %{dt1 | hour: 16, minute: 0, second: 0} + dt2 = %{dt1 | hour: 18, minute: 0, second: 0} + + expected = Formats.format_datetime_range(dt1, dt2) + assert Formats.format_datetime_range_compact(dt1, dt2) == expected + end + + test "formats full datetime range if start and end are on different days" do + tz = Formats.time_zone() + {:ok, now} = DateTime.now(tz) + dt1 = DateTime.to_naive(now) |> NaiveDateTime.truncate(:second) |> DateTime.from_naive!(tz) + dt1 = %{dt1 | hour: 23, minute: 0, second: 0} + tomorrow = DateTime.add(now, 1, :day) + + dt2 = + DateTime.to_naive(tomorrow) |> NaiveDateTime.truncate(:second) |> DateTime.from_naive!(tz) + + dt2 = %{dt2 | hour: 1, minute: 0, second: 0} + + expected = Formats.format_datetime_range(dt1, dt2) + assert Formats.format_datetime_range_compact(dt1, dt2) == expected + end + end + describe "format_avatar_placeholder/1" do test "generates initials from first and last name" do user = %User{avatar: nil, full_name: "John Doe"} diff --git a/test/dev_round_web/live/user_events_live_test.exs b/test/dev_round_web/live/user_events_live_test.exs new file mode 100644 index 0000000..72e0541 --- /dev/null +++ b/test/dev_round_web/live/user_events_live_test.exs @@ -0,0 +1,224 @@ +defmodule DevRoundWeb.UserEventsLiveTest do + use DevRoundWeb.ConnCase + + import Phoenix.LiveViewTest + import DevRound.EventsFixtures + import DevRound.AccountsFixtures + alias DevRound.Events + + setup %{conn: conn} do + user = user_fixture() + %{conn: log_in_user(conn, user), user: user} + end + + describe "User Events" do + test "renders empty state when no registrations", %{conn: conn} do + {:ok, _view, html} = live(conn, ~p"/user/events") + assert html =~ "No registrations found" + end + + test "renders underway, upcoming and archived events with multi-day support", %{ + conn: conn, + user: user + } do + tz = Application.get_env(:dev_round, :time_zone) + {:ok, now} = DateTime.now(tz) + + # Underway: multi-day event starting yesterday, ending tomorrow + begin_underway = NaiveDateTime.add(DateTime.to_naive(now), -24, :hour) + end_underway = NaiveDateTime.add(DateTime.to_naive(now), 24, :hour) + + event_underway = + event_fixture(%{ + title: "Multi-day Underway Event", + begin_local: begin_underway, + end_local: end_underway, + registration_deadline_local: NaiveDateTime.add(begin_underway, -1, :day) + }) + + # Upcoming: starts in 2 days + begin_upcoming = NaiveDateTime.add(DateTime.to_naive(now), 2, :day) + + event_upcoming = + event_fixture(%{ + title: "Future Event", + begin_local: begin_upcoming, + registration_deadline_local: NaiveDateTime.add(begin_upcoming, -1, :day) + }) + + # Archived: ended 2 days ago + begin_archived = NaiveDateTime.add(DateTime.to_naive(now), -5, :day) + end_archived = NaiveDateTime.add(DateTime.to_naive(now), -2, :day) + + event_archived = + event_fixture(%{ + title: "Past Multi-day Event", + begin_local: begin_archived, + end_local: end_archived, + registration_deadline_local: NaiveDateTime.add(begin_archived, -1, :day) + }) + + # Register user for all + for event <- [event_underway, event_upcoming, event_archived] do + Events.create_event_attendee( + event, + user, + %{ + "lang_ids" => [Enum.at(event.langs, 0).id] + }, + :host + ) + end + + {:ok, view, _html} = live(conn, ~p"/user/events") + + assert has_element?(view, "#underway-events") + assert has_element?(view, "a", "Multi-day Underway Event") + + assert has_element?(view, "#upcoming-events") + assert has_element?(view, "a", "Future Event") + + assert has_element?(view, "#archived-events") + assert has_element?(view, "a", "Past Multi-day Event") + end + + test "updates in real-time on new registration", %{conn: conn, user: user} do + {:ok, view, html} = live(conn, ~p"/user/events") + assert html =~ "No registrations found" + + future_begin = NaiveDateTime.add(NaiveDateTime.local_now(), 2, :day) + + event = + event_fixture(%{ + title: "New Registration", + begin_local: future_begin, + registration_deadline_local: NaiveDateTime.add(future_begin, -1, :day) + }) + + # Simulate registration + {:ok, attendee} = + Events.create_event_attendee(event, user, %{ + "lang_ids" => [Enum.at(event.langs, 0).id] + }) + + # Broadcast + DevRoundWeb.Endpoint.broadcast("registrations", "created", {:created, event, attendee}) + + assert render(view) =~ "New Registration" + end + + test "renders team information for underway sessions", %{conn: conn, user: user} do + tz = Application.get_env(:dev_round, :time_zone) + {:ok, now} = DateTime.now(tz) + + # Underway event + begin = NaiveDateTime.add(DateTime.to_naive(now), -1, :hour) + + event = + event_fixture(%{ + title: "Team Display Event", + begin_local: begin, + end_local: NaiveDateTime.add(begin, 24, :hour), + registration_deadline_local: NaiveDateTime.add(begin, -1, :day) + }) + + # bypass deadline check for testing team display + %DevRound.Events.EventAttendee{} + |> Ecto.Changeset.change(%{ + event_id: event.id, + user_id: user.id, + experience_level: 3, + is_remote: false + }) + |> DevRound.Repo.insert!() + + session = hd(event.sessions) + # Mark session as team locked so teams are loaded + session |> Ecto.Changeset.change(%{teams_locked: true}) |> DevRound.Repo.update!() + + lang = Enum.at(event.langs, 0) + + # Create team manually + {:ok, team} = + %DevRound.Hosting.Team{} + |> Ecto.Changeset.change(%{ + name: "The Dream Team", + slug: "dream-team", + is_remote: false, + session_id: session.id, + lang_id: lang.id + }) + |> DevRound.Repo.insert() + + # Add user to team + %DevRound.Hosting.TeamMember{} + |> Ecto.Changeset.change(%{ + team_id: team.id, + user_id: user.id, + experience_level: 3, + is_remote: false + }) + |> DevRound.Repo.insert!() + + {:ok, _view, html} = live(conn, ~p"/user/events") + + assert html =~ "The Dream Team" + end + + test "expands multiple archived events", %{conn: conn, user: user} do + tz = Application.get_env(:dev_round, :time_zone) + {:ok, now} = DateTime.now(tz) + + # Archived events + begin_archived = NaiveDateTime.add(DateTime.to_naive(now), -10, :day) + end_archived = NaiveDateTime.add(DateTime.to_naive(now), -8, :day) + + archived_attrs = %{ + begin_local: begin_archived, + end_local: end_archived, + registration_deadline_local: NaiveDateTime.add(begin_archived, -1, :day) + } + + event1 = event_fixture(Map.merge(archived_attrs, %{title: "Event 1"})) + event2 = event_fixture(Map.merge(archived_attrs, %{title: "Event 2"})) + + for {event, index} <- Enum.with_index([event1, event2]) do + Events.create_event_attendee( + event, + user, + %{"lang_ids" => [Enum.at(event.langs, 0).id]}, + :host + ) + + # Mark sessions as team locked and give unique title + for {session, s_index} <- Enum.with_index(event.sessions) do + session + |> Ecto.Changeset.change(%{ + teams_locked: true, + title: "Archived Event #{index + 1} Session #{s_index + 1}" + }) + |> DevRound.Repo.update!() + end + end + + {:ok, view, _html} = live(conn, ~p"/user/events") + + # Initially not expanded + refute has_element?(view, "h2", "Archived Event 1 Session 1") + refute has_element?(view, "h2", "Archived Event 1 Session 1") + refute has_element?(view, "h2", "Archived Event 2 Session 1") + + # Expand event 1 + view |> render_click("toggle_expand", %{"id" => to_string(event1.id), "expanded" => false}) + assert has_element?(view, "h2", "Archived Event 1 Session 1") + assert has_element?(view, "h2", "Archived Event 1 Session 1") + refute has_element?(view, "h2", "Archived Event 2 Session 1") + + # Expand event 2 + view |> render_click("toggle_expand", %{"id" => to_string(event2.id), "expanded" => false}) + assert has_element?(view, "h2", "Archived Event 1 Session 1") + assert has_element?(view, "h2", "Archived Event 1 Session 1") + assert has_element?(view, "h2", "Archived Event 2 Session 1") + end + end +end