From ebf93181776984786d46742acc1a49197d4c9cdc Mon Sep 17 00:00:00 2001 From: Adrian Salamon Date: Tue, 22 Nov 2022 21:16:25 +0100 Subject: [PATCH 01/56] Generated event crud operations --- lib/haj/events.ex | 294 ++++++++++++++++++ lib/haj/events/event.ex | 22 ++ lib/haj/events/event_registration.ex | 18 ++ lib/haj/events/ticket_type.ex | 21 ++ lib/haj_web/live/event_live/form_component.ex | 86 +++++ lib/haj_web/live/event_live/index.ex | 46 +++ lib/haj_web/live/event_live/index.html.heex | 44 +++ lib/haj_web/live/event_live/show.ex | 21 ++ lib/haj_web/live/event_live/show.html.heex | 36 +++ lib/haj_web/router.ex | 7 + .../20221122195103_create_events.exs | 16 + .../20221122200036_create_ticket_types.exs | 16 + ...21122200318_create_event_registrations.exs | 15 + test/haj/events_test.exs | 179 +++++++++++ test/haj_web/live/event_live_test.exs | 110 +++++++ test/support/fixtures/events_fixtures.ex | 55 ++++ 16 files changed, 986 insertions(+) create mode 100644 lib/haj/events.ex create mode 100644 lib/haj/events/event.ex create mode 100644 lib/haj/events/event_registration.ex create mode 100644 lib/haj/events/ticket_type.ex create mode 100644 lib/haj_web/live/event_live/form_component.ex create mode 100644 lib/haj_web/live/event_live/index.ex create mode 100644 lib/haj_web/live/event_live/index.html.heex create mode 100644 lib/haj_web/live/event_live/show.ex create mode 100644 lib/haj_web/live/event_live/show.html.heex create mode 100644 priv/repo/migrations/20221122195103_create_events.exs create mode 100644 priv/repo/migrations/20221122200036_create_ticket_types.exs create mode 100644 priv/repo/migrations/20221122200318_create_event_registrations.exs create mode 100644 test/haj/events_test.exs create mode 100644 test/haj_web/live/event_live_test.exs create mode 100644 test/support/fixtures/events_fixtures.ex diff --git a/lib/haj/events.ex b/lib/haj/events.ex new file mode 100644 index 0000000..32efb2b --- /dev/null +++ b/lib/haj/events.ex @@ -0,0 +1,294 @@ +defmodule Haj.Events do + @moduledoc """ + The Events context. + """ + + import Ecto.Query, warn: false + alias Haj.Repo + + alias Haj.Events.Event + alias Haj.Events.TicketType + alias Haj.Events.EventRegistration + + @doc """ + Returns the list of events. + + ## Examples + + iex> list_events() + [%Event{}, ...] + + """ + def list_events do + Repo.all(Event) + end + + @doc """ + Gets a single event. + + Raises `Ecto.NoResultsError` if the Event does not exist. + + ## Examples + + iex> get_event!(123) + %Event{} + + iex> get_event!(456) + ** (Ecto.NoResultsError) + + """ + def get_event!(id), do: Repo.get!(Event, id) + + @doc """ + Creates a event. + + ## Examples + + iex> create_event(%{field: value}) + {:ok, %Event{}} + + iex> create_event(%{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def create_event(attrs \\ %{}) do + %Event{} + |> Event.changeset(attrs) + |> Repo.insert() + end + + @doc """ + Updates a event. + + ## Examples + + iex> update_event(event, %{field: new_value}) + {:ok, %Event{}} + + iex> update_event(event, %{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def update_event(%Event{} = event, attrs) do + event + |> Event.changeset(attrs) + |> Repo.update() + end + + @doc """ + Deletes a event. + + ## Examples + + iex> delete_event(event) + {:ok, %Event{}} + + iex> delete_event(event) + {:error, %Ecto.Changeset{}} + + """ + def delete_event(%Event{} = event) do + Repo.delete(event) + end + + @doc """ + Returns an `%Ecto.Changeset{}` for tracking event changes. + + ## Examples + + iex> change_event(event) + %Ecto.Changeset{data: %Event{}} + + """ + def change_event(%Event{} = event, attrs \\ %{}) do + Event.changeset(event, attrs) + end + + @doc """ + Returns the list of ticket_types. + + ## Examples + + iex> list_ticket_types() + [%TicketType{}, ...] + + """ + def list_ticket_types do + Repo.all(TicketType) + end + + @doc """ + Gets a single ticket_type. + + Raises `Ecto.NoResultsError` if the Ticket type does not exist. + + ## Examples + + iex> get_ticket_type!(123) + %TicketType{} + + iex> get_ticket_type!(456) + ** (Ecto.NoResultsError) + + """ + def get_ticket_type!(id), do: Repo.get!(TicketType, id) + + @doc """ + Creates a ticket_type. + + ## Examples + + iex> create_ticket_type(%{field: value}) + {:ok, %TicketType{}} + + iex> create_ticket_type(%{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def create_ticket_type(attrs \\ %{}) do + %TicketType{} + |> TicketType.changeset(attrs) + |> Repo.insert() + end + + @doc """ + Updates a ticket_type. + + ## Examples + + iex> update_ticket_type(ticket_type, %{field: new_value}) + {:ok, %TicketType{}} + + iex> update_ticket_type(ticket_type, %{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def update_ticket_type(%TicketType{} = ticket_type, attrs) do + ticket_type + |> TicketType.changeset(attrs) + |> Repo.update() + end + + @doc """ + Deletes a ticket_type. + + ## Examples + + iex> delete_ticket_type(ticket_type) + {:ok, %TicketType{}} + + iex> delete_ticket_type(ticket_type) + {:error, %Ecto.Changeset{}} + + """ + def delete_ticket_type(%TicketType{} = ticket_type) do + Repo.delete(ticket_type) + end + + @doc """ + Returns an `%Ecto.Changeset{}` for tracking ticket_type changes. + + ## Examples + + iex> change_ticket_type(ticket_type) + %Ecto.Changeset{data: %TicketType{}} + + """ + def change_ticket_type(%TicketType{} = ticket_type, attrs \\ %{}) do + TicketType.changeset(ticket_type, attrs) + end + + @doc """ + Returns the list of event_registrations. + + ## Examples + + iex> list_event_registrations() + [%EventRegistration{}, ...] + + """ + def list_event_registrations do + Repo.all(EventRegistration) + end + + @doc """ + Gets a single event_registration. + + Raises `Ecto.NoResultsError` if the Event registration does not exist. + + ## Examples + + iex> get_event_registration!(123) + %EventRegistration{} + + iex> get_event_registration!(456) + ** (Ecto.NoResultsError) + + """ + def get_event_registration!(id), do: Repo.get!(EventRegistration, id) + + @doc """ + Creates a event_registration. + + ## Examples + + iex> create_event_registration(%{field: value}) + {:ok, %EventRegistration{}} + + iex> create_event_registration(%{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def create_event_registration(attrs \\ %{}) do + %EventRegistration{} + |> EventRegistration.changeset(attrs) + |> Repo.insert() + end + + @doc """ + Updates a event_registration. + + ## Examples + + iex> update_event_registration(event_registration, %{field: new_value}) + {:ok, %EventRegistration{}} + + iex> update_event_registration(event_registration, %{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def update_event_registration(%EventRegistration{} = event_registration, attrs) do + event_registration + |> EventRegistration.changeset(attrs) + |> Repo.update() + end + + @doc """ + Deletes a event_registration. + + ## Examples + + iex> delete_event_registration(event_registration) + {:ok, %EventRegistration{}} + + iex> delete_event_registration(event_registration) + {:error, %Ecto.Changeset{}} + + """ + def delete_event_registration(%EventRegistration{} = event_registration) do + Repo.delete(event_registration) + end + + @doc """ + Returns an `%Ecto.Changeset{}` for tracking event_registration changes. + + ## Examples + + iex> change_event_registration(event_registration) + %Ecto.Changeset{data: %EventRegistration{}} + + """ + def change_event_registration(%EventRegistration{} = event_registration, attrs \\ %{}) do + EventRegistration.changeset(event_registration, attrs) + end +end diff --git a/lib/haj/events/event.ex b/lib/haj/events/event.ex new file mode 100644 index 0000000..fecb07c --- /dev/null +++ b/lib/haj/events/event.ex @@ -0,0 +1,22 @@ +defmodule Haj.Events.Event do + use Ecto.Schema + import Ecto.Changeset + + schema "events" do + field :description, :string + field :event_date, :utc_datetime + field :image, :string + field :name, :string + field :purchase_deadline, :utc_datetime + field :ticket_limit, :integer + + timestamps() + end + + @doc false + def changeset(event, attrs) do + event + |> cast(attrs, [:name, :description, :image, :ticket_limit, :event_date, :purchase_deadline]) + |> validate_required([:name, :description, :image, :ticket_limit, :event_date, :purchase_deadline]) + end +end diff --git a/lib/haj/events/event_registration.ex b/lib/haj/events/event_registration.ex new file mode 100644 index 0000000..c0cdd83 --- /dev/null +++ b/lib/haj/events/event_registration.ex @@ -0,0 +1,18 @@ +defmodule Haj.Events.EventRegistration do + use Ecto.Schema + import Ecto.Changeset + + schema "event_registrations" do + belongs_to :ticket_type, Haj.Events.TicketType + belongs_to :user, Haj.Accounts.User + + timestamps() + end + + @doc false + def changeset(event_registration, attrs) do + event_registration + |> cast(attrs, []) + |> validate_required([]) + end +end diff --git a/lib/haj/events/ticket_type.ex b/lib/haj/events/ticket_type.ex new file mode 100644 index 0000000..92b5468 --- /dev/null +++ b/lib/haj/events/ticket_type.ex @@ -0,0 +1,21 @@ +defmodule Haj.Events.TicketType do + use Ecto.Schema + import Ecto.Changeset + + schema "ticket_types" do + field :description, :string + field :name, :string + field :price, :integer + + belongs_to :event, Haj.Events.Event + + timestamps() + end + + @doc false + def changeset(ticket_type, attrs) do + ticket_type + |> cast(attrs, [:price, :name, :description]) + |> validate_required([:price, :name, :description]) + end +end diff --git a/lib/haj_web/live/event_live/form_component.ex b/lib/haj_web/live/event_live/form_component.ex new file mode 100644 index 0000000..bf27e95 --- /dev/null +++ b/lib/haj_web/live/event_live/form_component.ex @@ -0,0 +1,86 @@ +defmodule HajWeb.EventLive.FormComponent do + use HajWeb, :live_component + + alias Haj.Events + + @impl true + def render(assigns) do + ~H""" +
+ <.header> + <%= @title %> + <:subtitle>Use this form to manage event records in your database. + + + <.simple_form + :let={f} + for={@changeset} + id="event-form" + phx-target={@myself} + phx-change="validate" + phx-submit="save" + > + <.input field={{f, :name}} type="text" label="name" /> + <.input field={{f, :description}} type="text" label="description" /> + <.input field={{f, :image}} type="text" label="image" /> + <.input field={{f, :ticket_limit}} type="number" label="ticket_limit" /> + <.input field={{f, :event_date}} type="datetime-local" label="event_date" /> + <.input field={{f, :purchase_deadline}} type="datetime-local" label="purchase_deadline" /> + <:actions> + <.button phx-disable-with="Saving...">Save Event + + +
+ """ + end + + @impl true + def update(%{event: event} = assigns, socket) do + changeset = Events.change_event(event) + + {:ok, + socket + |> assign(assigns) + |> assign(:changeset, changeset)} + end + + @impl true + def handle_event("validate", %{"event" => event_params}, socket) do + changeset = + socket.assigns.event + |> Events.change_event(event_params) + |> Map.put(:action, :validate) + + {:noreply, assign(socket, :changeset, changeset)} + end + + def handle_event("save", %{"event" => event_params}, socket) do + save_event(socket, socket.assigns.action, event_params) + end + + defp save_event(socket, :edit, event_params) do + case Events.update_event(socket.assigns.event, event_params) do + {:ok, _event} -> + {:noreply, + socket + |> put_flash(:info, "Event updated successfully") + |> push_navigate(to: socket.assigns.navigate)} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, assign(socket, :changeset, changeset)} + end + end + + defp save_event(socket, :new, event_params) do + case Events.create_event(event_params) do + {:ok, _event} -> + {:noreply, + socket + |> put_flash(:info, "Event created successfully") + |> push_navigate(to: socket.assigns.navigate)} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, assign(socket, changeset: changeset)} + end + end +end diff --git a/lib/haj_web/live/event_live/index.ex b/lib/haj_web/live/event_live/index.ex new file mode 100644 index 0000000..66f0bcd --- /dev/null +++ b/lib/haj_web/live/event_live/index.ex @@ -0,0 +1,46 @@ +defmodule HajWeb.EventLive.Index do + use HajWeb, :live_view + + alias Haj.Events + alias Haj.Events.Event + + @impl true + def mount(_params, _session, socket) do + {:ok, assign(socket, :events, list_events())} + end + + @impl true + def handle_params(params, _url, socket) do + {:noreply, apply_action(socket, socket.assigns.live_action, params)} + end + + defp apply_action(socket, :edit, %{"id" => id}) do + socket + |> assign(:page_title, "Edit Event") + |> assign(:event, Events.get_event!(id)) + end + + defp apply_action(socket, :new, _params) do + socket + |> assign(:page_title, "New Event") + |> assign(:event, %Event{}) + end + + defp apply_action(socket, :index, _params) do + socket + |> assign(:page_title, "Listing Events") + |> assign(:event, nil) + end + + @impl true + def handle_event("delete", %{"id" => id}, socket) do + event = Events.get_event!(id) + {:ok, _} = Events.delete_event(event) + + {:noreply, assign(socket, :events, list_events())} + end + + defp list_events do + Events.list_events() + end +end diff --git a/lib/haj_web/live/event_live/index.html.heex b/lib/haj_web/live/event_live/index.html.heex new file mode 100644 index 0000000..eb30828 --- /dev/null +++ b/lib/haj_web/live/event_live/index.html.heex @@ -0,0 +1,44 @@ +<.header> + Listing Events + <:actions> + <.link patch={~p"/live/events/new"}> + <.button>New Event + + + + +<.table id="events" rows={@events} row_click={&JS.navigate(~p"/live/events/#{&1}")}> + <:col :let={event} label="Name"><%= event.name %> + <:col :let={event} label="Description"><%= event.description %> + <:col :let={event} label="Image"><%= event.image %> + <:col :let={event} label="Ticket limit"><%= event.ticket_limit %> + <:col :let={event} label="Event date"><%= event.event_date %> + <:col :let={event} label="Purchase deadline"><%= event.purchase_deadline %> + <:action :let={event}> +
+ <.link navigate={~p"/live/events/#{event}"}>Show +
+ <.link patch={~p"/live/events/#{event}/edit"}>Edit + + <:action :let={event}> + <.link phx-click={JS.push("delete", value: %{id: event.id})} data-confirm="Are you sure?"> + Delete + + + + +<.modal + :if={@live_action in [:new, :edit]} + id="event-modal" + show + on_cancel={JS.navigate(~p"/live/events")} +> + <.live_component + module={HajWeb.EventLive.FormComponent} + id={@event.id || :new} + title={@page_title} + action={@live_action} + event={@event} + navigate={~p"/live/events"} + /> + diff --git a/lib/haj_web/live/event_live/show.ex b/lib/haj_web/live/event_live/show.ex new file mode 100644 index 0000000..6696652 --- /dev/null +++ b/lib/haj_web/live/event_live/show.ex @@ -0,0 +1,21 @@ +defmodule HajWeb.EventLive.Show do + use HajWeb, :live_view + + alias Haj.Events + + @impl true + def mount(_params, _session, socket) do + {:ok, socket} + end + + @impl true + def handle_params(%{"id" => id}, _, socket) do + {:noreply, + socket + |> assign(:page_title, page_title(socket.assigns.live_action)) + |> assign(:event, Events.get_event!(id))} + end + + defp page_title(:show), do: "Show Event" + defp page_title(:edit), do: "Edit Event" +end diff --git a/lib/haj_web/live/event_live/show.html.heex b/lib/haj_web/live/event_live/show.html.heex new file mode 100644 index 0000000..1d8eb1d --- /dev/null +++ b/lib/haj_web/live/event_live/show.html.heex @@ -0,0 +1,36 @@ +<.header> + Event <%= @event.id %> + <:subtitle>This is a event record from your database. + <:actions> + <.link patch={~p"/live/events/#{@event}/show/edit"} phx-click={JS.push_focus()}> + <.button>Edit event + + + + +<.list> + <:item title="Name"><%= @event.name %> + <:item title="Description"><%= @event.description %> + <:item title="Image"><%= @event.image %> + <:item title="Ticket limit"><%= @event.ticket_limit %> + <:item title="Event date"><%= @event.event_date %> + <:item title="Purchase deadline"><%= @event.purchase_deadline %> + + +<.back navigate={~p"/live/events"}>Back to events + +<.modal + :if={@live_action == :edit} + id="event-modal" + show + on_cancel={JS.patch(~p"/live/events/#{@event}")} +> + <.live_component + module={HajWeb.EventLive.FormComponent} + id={@event.id} + title={@page_title} + action={@live_action} + event={@event} + navigate={~p"/live/events/#{@event}"} + /> + diff --git a/lib/haj_web/router.ex b/lib/haj_web/router.ex index 29bd101..350b667 100644 --- a/lib/haj_web/router.ex +++ b/lib/haj_web/router.ex @@ -50,6 +50,13 @@ defmodule HajWeb.Router do live "/merch-admin", MerchAdminLive.Index, :index live "/merch-admin/new", MerchAdminLive.Index, :new live "/merch-admin/:id/edit", MerchAdminLive.Index, :edit + + live "/events", EventLive.Index, :index + live "/events/new", EventLive.Index, :new + live "/events/:id/edit", EventLive.Index, :edit + + live "/events/:id", EventLive.Show, :show + live "/events/:id/show/edit", EventLive.Show, :edit end end diff --git a/priv/repo/migrations/20221122195103_create_events.exs b/priv/repo/migrations/20221122195103_create_events.exs new file mode 100644 index 0000000..d448de1 --- /dev/null +++ b/priv/repo/migrations/20221122195103_create_events.exs @@ -0,0 +1,16 @@ +defmodule Haj.Repo.Migrations.CreateEvents do + use Ecto.Migration + + def change do + create table(:events) do + add :name, :string + add :description, :text + add :image, :string + add :ticket_limit, :integer + add :event_date, :utc_datetime + add :purchase_deadline, :utc_datetime + + timestamps() + end + end +end diff --git a/priv/repo/migrations/20221122200036_create_ticket_types.exs b/priv/repo/migrations/20221122200036_create_ticket_types.exs new file mode 100644 index 0000000..a5d94fb --- /dev/null +++ b/priv/repo/migrations/20221122200036_create_ticket_types.exs @@ -0,0 +1,16 @@ +defmodule Haj.Repo.Migrations.CreateTicketTypes do + use Ecto.Migration + + def change do + create table(:ticket_types) do + add(:price, :integer) + add(:name, :string) + add(:description, :text) + add(:event_id, references(:events, on_delete: :delete_all)) + + timestamps() + end + + create(index(:ticket_types, [:event_id])) + end +end diff --git a/priv/repo/migrations/20221122200318_create_event_registrations.exs b/priv/repo/migrations/20221122200318_create_event_registrations.exs new file mode 100644 index 0000000..d250844 --- /dev/null +++ b/priv/repo/migrations/20221122200318_create_event_registrations.exs @@ -0,0 +1,15 @@ +defmodule Haj.Repo.Migrations.CreateEventRegistrations do + use Ecto.Migration + + def change do + create table(:event_registrations) do + add(:ticket_type_id, references(:ticket_types, on_delete: :delete_all)) + add(:user_id, references(:users, on_delete: :delete_all)) + + timestamps() + end + + create(index(:event_registrations, [:ticket_type_id])) + create(index(:event_registrations, [:user_id])) + end +end diff --git a/test/haj/events_test.exs b/test/haj/events_test.exs new file mode 100644 index 0000000..9f5a5a2 --- /dev/null +++ b/test/haj/events_test.exs @@ -0,0 +1,179 @@ +defmodule Haj.EventsTest do + use Haj.DataCase + + alias Haj.Events + + describe "events" do + alias Haj.Events.Event + + import Haj.EventsFixtures + + @invalid_attrs %{description: nil, event_date: nil, image: nil, name: nil, purchase_deadline: nil, ticket_limit: nil} + + test "list_events/0 returns all events" do + event = event_fixture() + assert Events.list_events() == [event] + end + + test "get_event!/1 returns the event with given id" do + event = event_fixture() + assert Events.get_event!(event.id) == event + end + + test "create_event/1 with valid data creates a event" do + valid_attrs = %{description: "some description", event_date: ~U[2022-11-21 19:51:00Z], image: "some image", name: "some name", purchase_deadline: ~U[2022-11-21 19:51:00Z], ticket_limit: 42} + + assert {:ok, %Event{} = event} = Events.create_event(valid_attrs) + assert event.description == "some description" + assert event.event_date == ~U[2022-11-21 19:51:00Z] + assert event.image == "some image" + assert event.name == "some name" + assert event.purchase_deadline == ~U[2022-11-21 19:51:00Z] + assert event.ticket_limit == 42 + end + + test "create_event/1 with invalid data returns error changeset" do + assert {:error, %Ecto.Changeset{}} = Events.create_event(@invalid_attrs) + end + + test "update_event/2 with valid data updates the event" do + event = event_fixture() + update_attrs = %{description: "some updated description", event_date: ~U[2022-11-22 19:51:00Z], image: "some updated image", name: "some updated name", purchase_deadline: ~U[2022-11-22 19:51:00Z], ticket_limit: 43} + + assert {:ok, %Event{} = event} = Events.update_event(event, update_attrs) + assert event.description == "some updated description" + assert event.event_date == ~U[2022-11-22 19:51:00Z] + assert event.image == "some updated image" + assert event.name == "some updated name" + assert event.purchase_deadline == ~U[2022-11-22 19:51:00Z] + assert event.ticket_limit == 43 + end + + test "update_event/2 with invalid data returns error changeset" do + event = event_fixture() + assert {:error, %Ecto.Changeset{}} = Events.update_event(event, @invalid_attrs) + assert event == Events.get_event!(event.id) + end + + test "delete_event/1 deletes the event" do + event = event_fixture() + assert {:ok, %Event{}} = Events.delete_event(event) + assert_raise Ecto.NoResultsError, fn -> Events.get_event!(event.id) end + end + + test "change_event/1 returns a event changeset" do + event = event_fixture() + assert %Ecto.Changeset{} = Events.change_event(event) + end + end + + describe "ticket_types" do + alias Haj.Events.TicketType + + import Haj.EventsFixtures + + @invalid_attrs %{description: nil, name: nil, price: nil} + + test "list_ticket_types/0 returns all ticket_types" do + ticket_type = ticket_type_fixture() + assert Events.list_ticket_types() == [ticket_type] + end + + test "get_ticket_type!/1 returns the ticket_type with given id" do + ticket_type = ticket_type_fixture() + assert Events.get_ticket_type!(ticket_type.id) == ticket_type + end + + test "create_ticket_type/1 with valid data creates a ticket_type" do + valid_attrs = %{description: "some description", name: "some name", price: 42} + + assert {:ok, %TicketType{} = ticket_type} = Events.create_ticket_type(valid_attrs) + assert ticket_type.description == "some description" + assert ticket_type.name == "some name" + assert ticket_type.price == 42 + end + + test "create_ticket_type/1 with invalid data returns error changeset" do + assert {:error, %Ecto.Changeset{}} = Events.create_ticket_type(@invalid_attrs) + end + + test "update_ticket_type/2 with valid data updates the ticket_type" do + ticket_type = ticket_type_fixture() + update_attrs = %{description: "some updated description", name: "some updated name", price: 43} + + assert {:ok, %TicketType{} = ticket_type} = Events.update_ticket_type(ticket_type, update_attrs) + assert ticket_type.description == "some updated description" + assert ticket_type.name == "some updated name" + assert ticket_type.price == 43 + end + + test "update_ticket_type/2 with invalid data returns error changeset" do + ticket_type = ticket_type_fixture() + assert {:error, %Ecto.Changeset{}} = Events.update_ticket_type(ticket_type, @invalid_attrs) + assert ticket_type == Events.get_ticket_type!(ticket_type.id) + end + + test "delete_ticket_type/1 deletes the ticket_type" do + ticket_type = ticket_type_fixture() + assert {:ok, %TicketType{}} = Events.delete_ticket_type(ticket_type) + assert_raise Ecto.NoResultsError, fn -> Events.get_ticket_type!(ticket_type.id) end + end + + test "change_ticket_type/1 returns a ticket_type changeset" do + ticket_type = ticket_type_fixture() + assert %Ecto.Changeset{} = Events.change_ticket_type(ticket_type) + end + end + + describe "event_registrations" do + alias Haj.Events.EventRegistration + + import Haj.EventsFixtures + + @invalid_attrs %{} + + test "list_event_registrations/0 returns all event_registrations" do + event_registration = event_registration_fixture() + assert Events.list_event_registrations() == [event_registration] + end + + test "get_event_registration!/1 returns the event_registration with given id" do + event_registration = event_registration_fixture() + assert Events.get_event_registration!(event_registration.id) == event_registration + end + + test "create_event_registration/1 with valid data creates a event_registration" do + valid_attrs = %{} + + assert {:ok, %EventRegistration{} = event_registration} = Events.create_event_registration(valid_attrs) + end + + test "create_event_registration/1 with invalid data returns error changeset" do + assert {:error, %Ecto.Changeset{}} = Events.create_event_registration(@invalid_attrs) + end + + test "update_event_registration/2 with valid data updates the event_registration" do + event_registration = event_registration_fixture() + update_attrs = %{} + + assert {:ok, %EventRegistration{} = event_registration} = Events.update_event_registration(event_registration, update_attrs) + end + + test "update_event_registration/2 with invalid data returns error changeset" do + event_registration = event_registration_fixture() + assert {:error, %Ecto.Changeset{}} = Events.update_event_registration(event_registration, @invalid_attrs) + assert event_registration == Events.get_event_registration!(event_registration.id) + end + + test "delete_event_registration/1 deletes the event_registration" do + event_registration = event_registration_fixture() + assert {:ok, %EventRegistration{}} = Events.delete_event_registration(event_registration) + assert_raise Ecto.NoResultsError, fn -> Events.get_event_registration!(event_registration.id) end + end + + test "change_event_registration/1 returns a event_registration changeset" do + event_registration = event_registration_fixture() + assert %Ecto.Changeset{} = Events.change_event_registration(event_registration) + end + end +end diff --git a/test/haj_web/live/event_live_test.exs b/test/haj_web/live/event_live_test.exs new file mode 100644 index 0000000..62a9c55 --- /dev/null +++ b/test/haj_web/live/event_live_test.exs @@ -0,0 +1,110 @@ +defmodule HajWeb.EventLiveTest do + use HajWeb.ConnCase + + import Phoenix.LiveViewTest + import Haj.EventsFixtures + + @create_attrs %{description: "some description", event_date: "2022-11-21T19:51:00Z", image: "some image", name: "some name", purchase_deadline: "2022-11-21T19:51:00Z", ticket_limit: 42} + @update_attrs %{description: "some updated description", event_date: "2022-11-22T19:51:00Z", image: "some updated image", name: "some updated name", purchase_deadline: "2022-11-22T19:51:00Z", ticket_limit: 43} + @invalid_attrs %{description: nil, event_date: nil, image: nil, name: nil, purchase_deadline: nil, ticket_limit: nil} + + defp create_event(_) do + event = event_fixture() + %{event: event} + end + + describe "Index" do + setup [:create_event] + + test "lists all events", %{conn: conn, event: event} do + {:ok, _index_live, html} = live(conn, ~p"/events") + + assert html =~ "Listing Events" + assert html =~ event.description + end + + test "saves new event", %{conn: conn} do + {:ok, index_live, _html} = live(conn, ~p"/events") + + assert index_live |> element("a", "New Event") |> render_click() =~ + "New Event" + + assert_patch(index_live, ~p"/events/new") + + assert index_live + |> form("#event-form", event: @invalid_attrs) + |> render_change() =~ "can't be blank" + + {:ok, _, html} = + index_live + |> form("#event-form", event: @create_attrs) + |> render_submit() + |> follow_redirect(conn, ~p"/events") + + assert html =~ "Event created successfully" + assert html =~ "some description" + end + + test "updates event in listing", %{conn: conn, event: event} do + {:ok, index_live, _html} = live(conn, ~p"/events") + + assert index_live |> element("#events-#{event.id} a", "Edit") |> render_click() =~ + "Edit Event" + + assert_patch(index_live, ~p"/events/#{event}/edit") + + assert index_live + |> form("#event-form", event: @invalid_attrs) + |> render_change() =~ "can't be blank" + + {:ok, _, html} = + index_live + |> form("#event-form", event: @update_attrs) + |> render_submit() + |> follow_redirect(conn, ~p"/events") + + assert html =~ "Event updated successfully" + assert html =~ "some updated description" + end + + test "deletes event in listing", %{conn: conn, event: event} do + {:ok, index_live, _html} = live(conn, ~p"/events") + + assert index_live |> element("#events-#{event.id} a", "Delete") |> render_click() + refute has_element?(index_live, "#event-#{event.id}") + end + end + + describe "Show" do + setup [:create_event] + + test "displays event", %{conn: conn, event: event} do + {:ok, _show_live, html} = live(conn, ~p"/events/#{event}") + + assert html =~ "Show Event" + assert html =~ event.description + end + + test "updates event within modal", %{conn: conn, event: event} do + {:ok, show_live, _html} = live(conn, ~p"/events/#{event}") + + assert show_live |> element("a", "Edit") |> render_click() =~ + "Edit Event" + + assert_patch(show_live, ~p"/events/#{event}/show/edit") + + assert show_live + |> form("#event-form", event: @invalid_attrs) + |> render_change() =~ "can't be blank" + + {:ok, _, html} = + show_live + |> form("#event-form", event: @update_attrs) + |> render_submit() + |> follow_redirect(conn, ~p"/events/#{event}") + + assert html =~ "Event updated successfully" + assert html =~ "some updated description" + end + end +end diff --git a/test/support/fixtures/events_fixtures.ex b/test/support/fixtures/events_fixtures.ex new file mode 100644 index 0000000..ab6b96d --- /dev/null +++ b/test/support/fixtures/events_fixtures.ex @@ -0,0 +1,55 @@ +defmodule Haj.EventsFixtures do + @moduledoc """ + This module defines test helpers for creating + entities via the `Haj.Events` context. + """ + + @doc """ + Generate a event. + """ + def event_fixture(attrs \\ %{}) do + {:ok, event} = + attrs + |> Enum.into(%{ + description: "some description", + event_date: ~U[2022-11-21 19:51:00Z], + image: "some image", + name: "some name", + purchase_deadline: ~U[2022-11-21 19:51:00Z], + ticket_limit: 42 + }) + |> Haj.Events.create_event() + + event + end + + @doc """ + Generate a ticket_type. + """ + def ticket_type_fixture(attrs \\ %{}) do + {:ok, ticket_type} = + attrs + |> Enum.into(%{ + description: "some description", + name: "some name", + price: 42 + }) + |> Haj.Events.create_ticket_type() + + ticket_type + end + + @doc """ + Generate a event_registration. + """ + def event_registration_fixture(attrs \\ %{}) do + {:ok, event_registration} = + attrs + |> Enum.into(%{ + + }) + |> Haj.Events.create_event_registration() + + event_registration + end +end From afb5e32d93a6e83759c3865ac8ef093a67f7a0e1 Mon Sep 17 00:00:00 2001 From: Hampus Hallkvist Date: Fri, 25 Nov 2022 20:34:53 +0100 Subject: [PATCH 02/56] fix: Date inputs in cronological order --- lib/haj_web/live/event_live/form_component.ex | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/haj_web/live/event_live/form_component.ex b/lib/haj_web/live/event_live/form_component.ex index bf27e95..3ebbd91 100644 --- a/lib/haj_web/live/event_live/form_component.ex +++ b/lib/haj_web/live/event_live/form_component.ex @@ -17,15 +17,14 @@ defmodule HajWeb.EventLive.FormComponent do for={@changeset} id="event-form" phx-target={@myself} - phx-change="validate" phx-submit="save" > <.input field={{f, :name}} type="text" label="name" /> <.input field={{f, :description}} type="text" label="description" /> <.input field={{f, :image}} type="text" label="image" /> <.input field={{f, :ticket_limit}} type="number" label="ticket_limit" /> - <.input field={{f, :event_date}} type="datetime-local" label="event_date" /> <.input field={{f, :purchase_deadline}} type="datetime-local" label="purchase_deadline" /> + <.input field={{f, :event_date}} type="datetime-local" label="event_date" /> <:actions> <.button phx-disable-with="Saving...">Save Event From 343307122eb3e4bb3c15a135e4f3262f84ef4613 Mon Sep 17 00:00:00 2001 From: Hampus Hallkvist Date: Mon, 26 Dec 2022 20:02:34 +0100 Subject: [PATCH 03/56] feat: Add events page and move events-admin to separate route --- .../form_component.ex | 2 +- lib/haj_web/live/event_admin_live/index.ex | 46 +++++++++++++++ .../live/event_admin_live/index.html.heex | 44 ++++++++++++++ .../{event_live => event_admin_live}/show.ex | 2 +- .../show.html.heex | 0 lib/haj_web/live/event_live/index.ex | 58 +++++++++---------- lib/haj_web/live/event_live/index.html.heex | 47 ++------------- lib/haj_web/router.ex | 9 +-- 8 files changed, 129 insertions(+), 79 deletions(-) rename lib/haj_web/live/{event_live => event_admin_live}/form_component.ex (98%) create mode 100644 lib/haj_web/live/event_admin_live/index.ex create mode 100644 lib/haj_web/live/event_admin_live/index.html.heex rename lib/haj_web/live/{event_live => event_admin_live}/show.ex (91%) rename lib/haj_web/live/{event_live => event_admin_live}/show.html.heex (100%) diff --git a/lib/haj_web/live/event_live/form_component.ex b/lib/haj_web/live/event_admin_live/form_component.ex similarity index 98% rename from lib/haj_web/live/event_live/form_component.ex rename to lib/haj_web/live/event_admin_live/form_component.ex index 3ebbd91..1a6f2b7 100644 --- a/lib/haj_web/live/event_live/form_component.ex +++ b/lib/haj_web/live/event_admin_live/form_component.ex @@ -1,4 +1,4 @@ -defmodule HajWeb.EventLive.FormComponent do +defmodule HajWeb.EventAdminLive.FormComponent do use HajWeb, :live_component alias Haj.Events diff --git a/lib/haj_web/live/event_admin_live/index.ex b/lib/haj_web/live/event_admin_live/index.ex new file mode 100644 index 0000000..207d7fa --- /dev/null +++ b/lib/haj_web/live/event_admin_live/index.ex @@ -0,0 +1,46 @@ +defmodule HajWeb.EventAdminLive.Index do + use HajWeb, :live_view + + alias Haj.Events + alias Haj.Events.Event + + @impl true + def mount(_params, _session, socket) do + {:ok, assign(socket, :events, list_events())} + end + + @impl true + def handle_params(params, _url, socket) do + {:noreply, apply_action(socket, socket.assigns.live_action, params)} + end + + defp apply_action(socket, :edit, %{"id" => id}) do + socket + |> assign(:page_title, "Edit Event") + |> assign(:event, Events.get_event!(id)) + end + + defp apply_action(socket, :new, _params) do + socket + |> assign(:page_title, "New Event") + |> assign(:event, %Event{}) + end + + defp apply_action(socket, :index, _params) do + socket + |> assign(:page_title, "Listing Events") + |> assign(:event, nil) + end + + @impl true + def handle_event("delete", %{"id" => id}, socket) do + event = Events.get_event!(id) + {:ok, _} = Events.delete_event(event) + + {:noreply, assign(socket, :events, list_events())} + end + + defp list_events do + Events.list_events() + end +end diff --git a/lib/haj_web/live/event_admin_live/index.html.heex b/lib/haj_web/live/event_admin_live/index.html.heex new file mode 100644 index 0000000..eb30828 --- /dev/null +++ b/lib/haj_web/live/event_admin_live/index.html.heex @@ -0,0 +1,44 @@ +<.header> + Listing Events + <:actions> + <.link patch={~p"/live/events/new"}> + <.button>New Event + + + + +<.table id="events" rows={@events} row_click={&JS.navigate(~p"/live/events/#{&1}")}> + <:col :let={event} label="Name"><%= event.name %> + <:col :let={event} label="Description"><%= event.description %> + <:col :let={event} label="Image"><%= event.image %> + <:col :let={event} label="Ticket limit"><%= event.ticket_limit %> + <:col :let={event} label="Event date"><%= event.event_date %> + <:col :let={event} label="Purchase deadline"><%= event.purchase_deadline %> + <:action :let={event}> +
+ <.link navigate={~p"/live/events/#{event}"}>Show +
+ <.link patch={~p"/live/events/#{event}/edit"}>Edit + + <:action :let={event}> + <.link phx-click={JS.push("delete", value: %{id: event.id})} data-confirm="Are you sure?"> + Delete + + + + +<.modal + :if={@live_action in [:new, :edit]} + id="event-modal" + show + on_cancel={JS.navigate(~p"/live/events")} +> + <.live_component + module={HajWeb.EventLive.FormComponent} + id={@event.id || :new} + title={@page_title} + action={@live_action} + event={@event} + navigate={~p"/live/events"} + /> + diff --git a/lib/haj_web/live/event_live/show.ex b/lib/haj_web/live/event_admin_live/show.ex similarity index 91% rename from lib/haj_web/live/event_live/show.ex rename to lib/haj_web/live/event_admin_live/show.ex index 6696652..645fa81 100644 --- a/lib/haj_web/live/event_live/show.ex +++ b/lib/haj_web/live/event_admin_live/show.ex @@ -1,4 +1,4 @@ -defmodule HajWeb.EventLive.Show do +defmodule HajWeb.EventAdminLive.Show do use HajWeb, :live_view alias Haj.Events diff --git a/lib/haj_web/live/event_live/show.html.heex b/lib/haj_web/live/event_admin_live/show.html.heex similarity index 100% rename from lib/haj_web/live/event_live/show.html.heex rename to lib/haj_web/live/event_admin_live/show.html.heex diff --git a/lib/haj_web/live/event_live/index.ex b/lib/haj_web/live/event_live/index.ex index 66f0bcd..9ea8c64 100644 --- a/lib/haj_web/live/event_live/index.ex +++ b/lib/haj_web/live/event_live/index.ex @@ -6,41 +6,39 @@ defmodule HajWeb.EventLive.Index do @impl true def mount(_params, _session, socket) do - {:ok, assign(socket, :events, list_events())} + {:ok, assign(socket, events: list_events(), counter: 0)} end - @impl true - def handle_params(params, _url, socket) do - {:noreply, apply_action(socket, socket.assigns.live_action, params)} - end - - defp apply_action(socket, :edit, %{"id" => id}) do - socket - |> assign(:page_title, "Edit Event") - |> assign(:event, Events.get_event!(id)) - end - - defp apply_action(socket, :new, _params) do - socket - |> assign(:page_title, "New Event") - |> assign(:event, %Event{}) - end - - defp apply_action(socket, :index, _params) do - socket - |> assign(:page_title, "Listing Events") - |> assign(:event, nil) - end - - @impl true - def handle_event("delete", %{"id" => id}, socket) do - event = Events.get_event!(id) - {:ok, _} = Events.delete_event(event) - - {:noreply, assign(socket, :events, list_events())} + def handle_params(_params, _url, socket) do + IO.inspect(socket.assigns.live_action) + {:noreply, socket} end defp list_events do Events.list_events() end + + defp event_card(assigns) do + ~H""" +
+
+ +
+

<%= @event.ticket_limit %>

+

Available tickets

+
+
+
+
+
+

<%= @event.name %>

+
<%= @event.event_date %>
+
+
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Facere non perferendis libero quia quisquam dolores reiciendis excepturi molestiae odio quos dignissimos iure obcaecati, quaerat nobis! Perspiciatis inventore eveniet commodi et.
+
+
+
+
+ """ + end end diff --git a/lib/haj_web/live/event_live/index.html.heex b/lib/haj_web/live/event_live/index.html.heex index eb30828..cb6d47f 100644 --- a/lib/haj_web/live/event_live/index.html.heex +++ b/lib/haj_web/live/event_live/index.html.heex @@ -1,44 +1,5 @@ -<.header> - Listing Events - <:actions> - <.link patch={~p"/live/events/new"}> - <.button>New Event - - - +

Events

-<.table id="events" rows={@events} row_click={&JS.navigate(~p"/live/events/#{&1}")}> - <:col :let={event} label="Name"><%= event.name %> - <:col :let={event} label="Description"><%= event.description %> - <:col :let={event} label="Image"><%= event.image %> - <:col :let={event} label="Ticket limit"><%= event.ticket_limit %> - <:col :let={event} label="Event date"><%= event.event_date %> - <:col :let={event} label="Purchase deadline"><%= event.purchase_deadline %> - <:action :let={event}> -
- <.link navigate={~p"/live/events/#{event}"}>Show -
- <.link patch={~p"/live/events/#{event}/edit"}>Edit - - <:action :let={event}> - <.link phx-click={JS.push("delete", value: %{id: event.id})} data-confirm="Are you sure?"> - Delete - - - - -<.modal - :if={@live_action in [:new, :edit]} - id="event-modal" - show - on_cancel={JS.navigate(~p"/live/events")} -> - <.live_component - module={HajWeb.EventLive.FormComponent} - id={@event.id || :new} - title={@page_title} - action={@live_action} - event={@event} - navigate={~p"/live/events"} - /> - +<%= for event <- @events do %> + <.event_card event={event} title="hej" /> +<% end %> \ No newline at end of file diff --git a/lib/haj_web/router.ex b/lib/haj_web/router.ex index 350b667..7935c0f 100644 --- a/lib/haj_web/router.ex +++ b/lib/haj_web/router.ex @@ -52,11 +52,12 @@ defmodule HajWeb.Router do live "/merch-admin/:id/edit", MerchAdminLive.Index, :edit live "/events", EventLive.Index, :index - live "/events/new", EventLive.Index, :new - live "/events/:id/edit", EventLive.Index, :edit + live "/events-admin", EventAdminLive.Index, :index + live "/events-admin/new", EventAdminLive.Index, :new + live "/events-admin/:id/edit", EventAdminLive.Index, :edit - live "/events/:id", EventLive.Show, :show - live "/events/:id/show/edit", EventLive.Show, :edit + live "/events-admin/:id", EventAdminLive.Show, :show + live "/events-admin/:id/show/edit", EventAdminLive.Show, :edit end end From cc4e56e88209ab64ac446de082c99aa8a5c2b66c Mon Sep 17 00:00:00 2001 From: Adrian Salamon Date: Tue, 31 Jan 2023 21:47:55 +0100 Subject: [PATCH 04/56] Fixed some naming of modules, logic integration for events, including pubsub Co-authored-by: Hampus Hallkvist --- lib/haj/events.ex | 30 +++++++- lib/haj/events/event.ex | 11 ++- lib/haj/events/event_registration.ex | 4 +- lib/haj_web/channels/presence.ex | 3 + .../live/event_admin_live/index.html.heex | 15 ++-- lib/haj_web/live/event_admin_live/show.ex | 21 ------ .../live/event_admin_live/show.html.heex | 36 ---------- lib/haj_web/live/event_live/index.ex | 71 ++++++++++++++----- lib/haj_web/router.ex | 3 - 9 files changed, 102 insertions(+), 92 deletions(-) create mode 100644 lib/haj_web/channels/presence.ex delete mode 100644 lib/haj_web/live/event_admin_live/show.ex delete mode 100644 lib/haj_web/live/event_admin_live/show.html.heex diff --git a/lib/haj/events.ex b/lib/haj/events.ex index 32efb2b..ff7d581 100644 --- a/lib/haj/events.ex +++ b/lib/haj/events.ex @@ -10,6 +10,8 @@ defmodule Haj.Events do alias Haj.Events.TicketType alias Haj.Events.EventRegistration + @topic inspect(__MODULE__) + @doc """ Returns the list of events. @@ -20,7 +22,11 @@ defmodule Haj.Events do """ def list_events do - Repo.all(Event) + query = + from e in Event, + preload: [:ticket_types] + + Repo.all(query) end @doc """ @@ -243,6 +249,7 @@ defmodule Haj.Events do %EventRegistration{} |> EventRegistration.changeset(attrs) |> Repo.insert() + |> notify_subscribers([:registration, :created]) end @doc """ @@ -277,6 +284,7 @@ defmodule Haj.Events do """ def delete_event_registration(%EventRegistration{} = event_registration) do Repo.delete(event_registration) + |> notify_subscribers([:registration, :deleted]) end @doc """ @@ -291,4 +299,24 @@ defmodule Haj.Events do def change_event_registration(%EventRegistration{} = event_registration, attrs \\ %{}) do EventRegistration.changeset(event_registration, attrs) end + + @doc """ + Retuns the number of tickets sold for an event. + """ + def tickets_sold(event_id) do + Repo.aggregate(EventRegistration, :count, :id, event_id: event_id) + end + + def subscribe do + Phoenix.PubSub.subscribe(Haj.PubSub, @topic) + end + + defp notify_subscribers({:ok, result}, event) do + Phoenix.PubSub.broadcast(Haj.PubSub, @topic, {__MODULE__, event, result}) + {:ok, result} + end + + defp notify_subscribers({:error, reason}, _) do + {:error, reason} + end end diff --git a/lib/haj/events/event.ex b/lib/haj/events/event.ex index fecb07c..0a86b4e 100644 --- a/lib/haj/events/event.ex +++ b/lib/haj/events/event.ex @@ -10,6 +10,8 @@ defmodule Haj.Events.Event do field :purchase_deadline, :utc_datetime field :ticket_limit, :integer + has_many :ticket_types, Haj.Events.TicketType + timestamps() end @@ -17,6 +19,13 @@ defmodule Haj.Events.Event do def changeset(event, attrs) do event |> cast(attrs, [:name, :description, :image, :ticket_limit, :event_date, :purchase_deadline]) - |> validate_required([:name, :description, :image, :ticket_limit, :event_date, :purchase_deadline]) + |> validate_required([ + :name, + :description, + :image, + :ticket_limit, + :event_date, + :purchase_deadline + ]) end end diff --git a/lib/haj/events/event_registration.ex b/lib/haj/events/event_registration.ex index c0cdd83..771e83a 100644 --- a/lib/haj/events/event_registration.ex +++ b/lib/haj/events/event_registration.ex @@ -12,7 +12,7 @@ defmodule Haj.Events.EventRegistration do @doc false def changeset(event_registration, attrs) do event_registration - |> cast(attrs, []) - |> validate_required([]) + |> cast(attrs, [:ticket_type_id, :user_id]) + |> validate_required([:ticket_type_id, :user_id]) end end diff --git a/lib/haj_web/channels/presence.ex b/lib/haj_web/channels/presence.ex new file mode 100644 index 0000000..80c6e23 --- /dev/null +++ b/lib/haj_web/channels/presence.ex @@ -0,0 +1,3 @@ +defmodule HajWeb.Presence do + +end diff --git a/lib/haj_web/live/event_admin_live/index.html.heex b/lib/haj_web/live/event_admin_live/index.html.heex index eb30828..f5f84ad 100644 --- a/lib/haj_web/live/event_admin_live/index.html.heex +++ b/lib/haj_web/live/event_admin_live/index.html.heex @@ -1,13 +1,13 @@ <.header> Listing Events <:actions> - <.link patch={~p"/live/events/new"}> + <.link patch={~p"/live/events-admin/new"}> <.button>New Event -<.table id="events" rows={@events} row_click={&JS.navigate(~p"/live/events/#{&1}")}> +<.table id="events" rows={@events}> <:col :let={event} label="Name"><%= event.name %> <:col :let={event} label="Description"><%= event.description %> <:col :let={event} label="Image"><%= event.image %> @@ -15,10 +15,7 @@ <:col :let={event} label="Event date"><%= event.event_date %> <:col :let={event} label="Purchase deadline"><%= event.purchase_deadline %> <:action :let={event}> -
- <.link navigate={~p"/live/events/#{event}"}>Show -
- <.link patch={~p"/live/events/#{event}/edit"}>Edit + <.link patch={~p"/live/events-admin/#{event}/edit"}>Edit <:action :let={event}> <.link phx-click={JS.push("delete", value: %{id: event.id})} data-confirm="Are you sure?"> @@ -31,14 +28,14 @@ :if={@live_action in [:new, :edit]} id="event-modal" show - on_cancel={JS.navigate(~p"/live/events")} + on_cancel={JS.navigate(~p"/live/events-admin")} > <.live_component - module={HajWeb.EventLive.FormComponent} + module={HajWeb.EventAdminLive.FormComponent} id={@event.id || :new} title={@page_title} action={@live_action} event={@event} - navigate={~p"/live/events"} + navigate={~p"/live/events-admin"} /> diff --git a/lib/haj_web/live/event_admin_live/show.ex b/lib/haj_web/live/event_admin_live/show.ex deleted file mode 100644 index 645fa81..0000000 --- a/lib/haj_web/live/event_admin_live/show.ex +++ /dev/null @@ -1,21 +0,0 @@ -defmodule HajWeb.EventAdminLive.Show do - use HajWeb, :live_view - - alias Haj.Events - - @impl true - def mount(_params, _session, socket) do - {:ok, socket} - end - - @impl true - def handle_params(%{"id" => id}, _, socket) do - {:noreply, - socket - |> assign(:page_title, page_title(socket.assigns.live_action)) - |> assign(:event, Events.get_event!(id))} - end - - defp page_title(:show), do: "Show Event" - defp page_title(:edit), do: "Edit Event" -end diff --git a/lib/haj_web/live/event_admin_live/show.html.heex b/lib/haj_web/live/event_admin_live/show.html.heex deleted file mode 100644 index 1d8eb1d..0000000 --- a/lib/haj_web/live/event_admin_live/show.html.heex +++ /dev/null @@ -1,36 +0,0 @@ -<.header> - Event <%= @event.id %> - <:subtitle>This is a event record from your database. - <:actions> - <.link patch={~p"/live/events/#{@event}/show/edit"} phx-click={JS.push_focus()}> - <.button>Edit event - - - - -<.list> - <:item title="Name"><%= @event.name %> - <:item title="Description"><%= @event.description %> - <:item title="Image"><%= @event.image %> - <:item title="Ticket limit"><%= @event.ticket_limit %> - <:item title="Event date"><%= @event.event_date %> - <:item title="Purchase deadline"><%= @event.purchase_deadline %> - - -<.back navigate={~p"/live/events"}>Back to events - -<.modal - :if={@live_action == :edit} - id="event-modal" - show - on_cancel={JS.patch(~p"/live/events/#{@event}")} -> - <.live_component - module={HajWeb.EventLive.FormComponent} - id={@event.id} - title={@page_title} - action={@live_action} - event={@event} - navigate={~p"/live/events/#{@event}"} - /> - diff --git a/lib/haj_web/live/event_live/index.ex b/lib/haj_web/live/event_live/index.ex index 9ea8c64..1a3bd35 100644 --- a/lib/haj_web/live/event_live/index.ex +++ b/lib/haj_web/live/event_live/index.ex @@ -6,39 +6,72 @@ defmodule HajWeb.EventLive.Index do @impl true def mount(_params, _session, socket) do + Events.subscribe() {:ok, assign(socket, events: list_events(), counter: 0)} end - def handle_params(_params, _url, socket) do - IO.inspect(socket.assigns.live_action) - {:noreply, socket} + @impl true + def handle_info({Events, [:registration, _], _}, socket) do + {:noreply, assign(socket, events: list_events())} end defp list_events do + # Todo: maybe fetch in one query Events.list_events() + |> Enum.map(fn event -> + ticked_sold = Events.tickets_sold(event.id) + Map.put(event, :availible, event.ticket_limit - ticked_sold) + end) + end + + @impl true + def handle_event("register", %{"id" => id}, socket) do + case Events.create_event_registration(%{ + "user_id" => socket.assigns.current_user.id, + "ticket_type_id" => id + }) do + {:ok, _} -> + {:noreply, socket |> put_flash(:info, "Purchased")} + + {:error, _} -> + {:noreply, socket |> put_flash(:error, "Error")} + end end defp event_card(assigns) do ~H""" -
-
- -
-

<%= @event.ticket_limit %>

-

Available tickets

-
+
+
+ +
+

<%= @event.availible %>

+

Available tickets

-
-
-
-

<%= @event.name %>

-
<%= @event.event_date %>
-
-
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Facere non perferendis libero quia quisquam dolores reiciendis excepturi molestiae odio quos dignissimos iure obcaecati, quaerat nobis! Perspiciatis inventore eveniet commodi et.
+
+
+
+
+

<%= @event.name %>

+
<%= @event.event_date %>
+
<%= @event.description %>
-
-
+
+ +
+
""" end end diff --git a/lib/haj_web/router.ex b/lib/haj_web/router.ex index 7935c0f..e0410af 100644 --- a/lib/haj_web/router.ex +++ b/lib/haj_web/router.ex @@ -55,9 +55,6 @@ defmodule HajWeb.Router do live "/events-admin", EventAdminLive.Index, :index live "/events-admin/new", EventAdminLive.Index, :new live "/events-admin/:id/edit", EventAdminLive.Index, :edit - - live "/events-admin/:id", EventAdminLive.Show, :show - live "/events-admin/:id/show/edit", EventAdminLive.Show, :edit end end From fb8b40b8482d154cbe115ed19a9ec05a233bcd34 Mon Sep 17 00:00:00 2001 From: Hampus Hallkvist Date: Tue, 14 Feb 2023 21:05:22 +0100 Subject: [PATCH 05/56] fix: Add date format to events on events page --- lib/haj_web/live/event_live/index.ex | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/haj_web/live/event_live/index.ex b/lib/haj_web/live/event_live/index.ex index 1a3bd35..8ec75cd 100644 --- a/lib/haj_web/live/event_live/index.ex +++ b/lib/haj_web/live/event_live/index.ex @@ -38,6 +38,13 @@ defmodule HajWeb.EventLive.Index do end end + defp ticket_format_date(event_date) do + Calendar.strftime(event_date, "%d %B %Y") + end + defp ticket_format_time(event_date) do + Calendar.strftime(event_date, "%H:%M") + end + defp event_card(assigns) do ~H"""
@@ -55,7 +62,10 @@ defmodule HajWeb.EventLive.Index do

<%= @event.name %>

-
<%= @event.event_date %>
+
+
<%= ticket_format_date(@event.event_date) %>
+
<%= ticket_format_time(@event.event_date) %>
+
<%= @event.description %>
From 2791c9cf64156ec3bea869286e6f342639ff0eee Mon Sep 17 00:00:00 2001 From: Hampus Hallkvist Date: Tue, 14 Feb 2023 22:34:44 +0100 Subject: [PATCH 06/56] feat: Add live counter to event page --- lib/haj/application.ex | 4 ++- lib/haj/events/presence.ex | 3 ++ .../event_live/components/viewer_count.ex | 15 +++++++++ lib/haj_web/live/event_live/index.ex | 31 ++++++++++++++++++- lib/haj_web/live/event_live/index.html.heex | 2 ++ 5 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 lib/haj/events/presence.ex create mode 100644 lib/haj_web/live/event_live/components/viewer_count.ex diff --git a/lib/haj/application.ex b/lib/haj/application.ex index b9d433e..a7a49b4 100644 --- a/lib/haj/application.ex +++ b/lib/haj/application.ex @@ -15,7 +15,9 @@ defmodule Haj.Application do # Start the PubSub system {Phoenix.PubSub, name: Haj.PubSub}, # Start the Endpoint (http/https) - HajWeb.Endpoint + HajWeb.Endpoint, + # Start the presence tracker (for online users) + Haj.Presence, # Start a worker by calling: Haj.Worker.start_link(arg) # {Haj.Worker, arg} ] diff --git a/lib/haj/events/presence.ex b/lib/haj/events/presence.ex new file mode 100644 index 0000000..8815c58 --- /dev/null +++ b/lib/haj/events/presence.ex @@ -0,0 +1,3 @@ +defmodule Haj.Presence do + use Phoenix.Presence, otp_app: :event_page_otpapp, pubsub_server: Haj.PubSub +end diff --git a/lib/haj_web/live/event_live/components/viewer_count.ex b/lib/haj_web/live/event_live/components/viewer_count.ex new file mode 100644 index 0000000..2163c20 --- /dev/null +++ b/lib/haj_web/live/event_live/components/viewer_count.ex @@ -0,0 +1,15 @@ +defmodule Haj.Live.ReaderCount do + use Phoenix.LiveView + + @moduledoc """ + A small LiveView that shows the number of readers of a post using Phoenix Presence + """ + + def render(assigns) do + + end + + def mount(_session, socket) do + {:ok, assign(socket, :online_count, 0)} + end +end diff --git a/lib/haj_web/live/event_live/index.ex b/lib/haj_web/live/event_live/index.ex index 8ec75cd..b08a67c 100644 --- a/lib/haj_web/live/event_live/index.ex +++ b/lib/haj_web/live/event_live/index.ex @@ -3,11 +3,21 @@ defmodule HajWeb.EventLive.Index do alias Haj.Events alias Haj.Events.Event + alias Haj.Presence @impl true def mount(_params, _session, socket) do Events.subscribe() - {:ok, assign(socket, events: list_events(), counter: 0)} + initial_count = Presence.list("custom_channel") |> map_size + HajWeb.Endpoint.subscribe("custom_channel") + + Presence.track( + self(), + "custom_channel", + socket.id, + %{} + ) + {:ok, assign(socket, events: list_events(), online_count: initial_count)} end @impl true @@ -15,6 +25,14 @@ defmodule HajWeb.EventLive.Index do {:noreply, assign(socket, events: list_events())} end + def handle_info( + %{event: "presence_diff", payload: %{joins: joins, leaves: leaves}}, + %{assigns: %{online_count: count}} = socket + ) do + online_count = count + map_size(joins) - map_size(leaves) + {:noreply, assign(socket, :online_count, online_count)} + end + defp list_events do # Todo: maybe fetch in one query Events.list_events() @@ -45,6 +63,17 @@ defmodule HajWeb.EventLive.Index do Calendar.strftime(event_date, "%H:%M") end + defp online_count(assigns) do + ~H""" +
+
+

<%= max(@online_count - 1, 0) %>

+
+

andra dataloger som väntar på biljetter

+
+ """ + end + defp event_card(assigns) do ~H"""
diff --git a/lib/haj_web/live/event_live/index.html.heex b/lib/haj_web/live/event_live/index.html.heex index cb6d47f..d317ab2 100644 --- a/lib/haj_web/live/event_live/index.html.heex +++ b/lib/haj_web/live/event_live/index.html.heex @@ -1,5 +1,7 @@

Events

+<.online_count online_count={@online_count} /> + <%= for event <- @events do %> <.event_card event={event} title="hej" /> <% end %> \ No newline at end of file From b039b0ff75719f0957f1a39babe64fdd449eb622 Mon Sep 17 00:00:00 2001 From: Hampus Hallkvist Date: Tue, 14 Feb 2023 22:35:45 +0100 Subject: [PATCH 07/56] fix: Fix typo remove dataloger, add metaloger --- lib/haj_web/live/event_live/index.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/haj_web/live/event_live/index.ex b/lib/haj_web/live/event_live/index.ex index b08a67c..bf5ce04 100644 --- a/lib/haj_web/live/event_live/index.ex +++ b/lib/haj_web/live/event_live/index.ex @@ -69,7 +69,7 @@ defmodule HajWeb.EventLive.Index do

<%= max(@online_count - 1, 0) %>

-

andra dataloger som väntar på biljetter

+

andra metaloger som väntar på biljetter

""" end From 3eac45ce37471aac0855a96d926a254877b4811f Mon Sep 17 00:00:00 2001 From: Hampus Hallkvist Date: Tue, 14 Feb 2023 22:36:37 +0100 Subject: [PATCH 08/56] fix: Remove unused module --- .../live/event_live/components/viewer_count.ex | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 lib/haj_web/live/event_live/components/viewer_count.ex diff --git a/lib/haj_web/live/event_live/components/viewer_count.ex b/lib/haj_web/live/event_live/components/viewer_count.ex deleted file mode 100644 index 2163c20..0000000 --- a/lib/haj_web/live/event_live/components/viewer_count.ex +++ /dev/null @@ -1,15 +0,0 @@ -defmodule Haj.Live.ReaderCount do - use Phoenix.LiveView - - @moduledoc """ - A small LiveView that shows the number of readers of a post using Phoenix Presence - """ - - def render(assigns) do - - end - - def mount(_session, socket) do - {:ok, assign(socket, :online_count, 0)} - end -end From f7edd43ee3158cb6196528f9dfbba3ed283e215f Mon Sep 17 00:00:00 2001 From: Hampus Hallkvist Date: Tue, 28 Feb 2023 21:47:37 +0100 Subject: [PATCH 09/56] feat: Add editable event types and event ticket types Co-authored-by: Adrian Salamon --- lib/haj/events.ex | 16 +++-- lib/haj/events/event.ex | 7 ++ .../live/event_admin_live/form_component.ex | 66 +++++++++++-------- lib/haj_web/live/event_admin_live/index.ex | 3 +- 4 files changed, 60 insertions(+), 32 deletions(-) diff --git a/lib/haj/events.ex b/lib/haj/events.ex index ff7d581..0c71dc8 100644 --- a/lib/haj/events.ex +++ b/lib/haj/events.ex @@ -75,10 +75,18 @@ defmodule Haj.Events do {:error, %Ecto.Changeset{}} """ - def update_event(%Event{} = event, attrs) do - event - |> Event.changeset(attrs) - |> Repo.update() + def update_event(%Event{} = event, attrs, opts \\ []) do + with_ticets = Keyword.get(opts, :with_tickets, false) + + if with_ticets do + event + |> Event.changeset_ticket_types(attrs) + |> Repo.update() + else + event + |> Event.changeset(attrs) + |> Repo.update() + end end @doc """ diff --git a/lib/haj/events/event.ex b/lib/haj/events/event.ex index 0a86b4e..79a808e 100644 --- a/lib/haj/events/event.ex +++ b/lib/haj/events/event.ex @@ -28,4 +28,11 @@ defmodule Haj.Events.Event do :purchase_deadline ]) end + + @doc false + def changeset_ticket_types(event, attrs) do + event + |> changeset(attrs) + |> cast_assoc(:ticket_types) + end end diff --git a/lib/haj_web/live/event_admin_live/form_component.ex b/lib/haj_web/live/event_admin_live/form_component.ex index 1a6f2b7..9298bb4 100644 --- a/lib/haj_web/live/event_admin_live/form_component.ex +++ b/lib/haj_web/live/event_admin_live/form_component.ex @@ -3,32 +3,40 @@ defmodule HajWeb.EventAdminLive.FormComponent do alias Haj.Events + @impl true + def mount(socket) do + {:ok, socket} + end + @impl true def render(assigns) do ~H"""
- <.header> - <%= @title %> - <:subtitle>Use this form to manage event records in your database. - - - <.simple_form - :let={f} - for={@changeset} - id="event-form" - phx-target={@myself} - phx-submit="save" - > - <.input field={{f, :name}} type="text" label="name" /> - <.input field={{f, :description}} type="text" label="description" /> - <.input field={{f, :image}} type="text" label="image" /> - <.input field={{f, :ticket_limit}} type="number" label="ticket_limit" /> - <.input field={{f, :purchase_deadline}} type="datetime-local" label="purchase_deadline" /> - <.input field={{f, :event_date}} type="datetime-local" label="event_date" /> - <:actions> - <.button phx-disable-with="Saving...">Save Event - - +
+ <.header> + <%= @title %> + <:subtitle>Use this form to manage event records in your database. + + + <.simple_form for={@form} id="event-form" phx-target={@myself} phx-submit="save"> + <.input field={@form[:name]} type="text" label="name" /> + <.input field={@form[:description]} type="text" label="description" /> + <.input field={@form[:image]} type="text" label="image" /> + <.input field={@form[:ticket_limit]} type="number" label="ticket_limit" /> + <.input field={@form[:purchase_deadline]} type="datetime-local" label="purchase_deadline" /> + <.input field={@form[:event_date]} type="datetime-local" label="event_date" /> + + <.inputs_for :let={f_nested} field={@form[:ticket_types]}> + <.input field={f_nested[:name]} type="text" label="name" /> + <.input field={f_nested[:price]} type="number" label="price" /> + <.input field={f_nested[:description]} type="text" label="description" /> + + + <:actions> + <.button phx-disable-with="Saving...">Save Event + + +
""" end @@ -40,7 +48,7 @@ defmodule HajWeb.EventAdminLive.FormComponent do {:ok, socket |> assign(assigns) - |> assign(:changeset, changeset)} + |> assign_form(changeset)} end @impl true @@ -50,7 +58,11 @@ defmodule HajWeb.EventAdminLive.FormComponent do |> Events.change_event(event_params) |> Map.put(:action, :validate) - {:noreply, assign(socket, :changeset, changeset)} + {:noreply, assign_form(socket, changeset)} + end + + defp assign_form(socket, %Ecto.Changeset{} = changeset) do + assign(socket, :form, to_form(changeset)) end def handle_event("save", %{"event" => event_params}, socket) do @@ -58,7 +70,7 @@ defmodule HajWeb.EventAdminLive.FormComponent do end defp save_event(socket, :edit, event_params) do - case Events.update_event(socket.assigns.event, event_params) do + case Events.update_event(socket.assigns.event, event_params, with_tickets: true) do {:ok, _event} -> {:noreply, socket @@ -66,7 +78,7 @@ defmodule HajWeb.EventAdminLive.FormComponent do |> push_navigate(to: socket.assigns.navigate)} {:error, %Ecto.Changeset{} = changeset} -> - {:noreply, assign(socket, :changeset, changeset)} + {:noreply, assign_form(socket, changeset)} end end @@ -79,7 +91,7 @@ defmodule HajWeb.EventAdminLive.FormComponent do |> push_navigate(to: socket.assigns.navigate)} {:error, %Ecto.Changeset{} = changeset} -> - {:noreply, assign(socket, changeset: changeset)} + {:noreply, assign_form(socket, changeset)} end end end diff --git a/lib/haj_web/live/event_admin_live/index.ex b/lib/haj_web/live/event_admin_live/index.ex index 207d7fa..afaca87 100644 --- a/lib/haj_web/live/event_admin_live/index.ex +++ b/lib/haj_web/live/event_admin_live/index.ex @@ -3,6 +3,7 @@ defmodule HajWeb.EventAdminLive.Index do alias Haj.Events alias Haj.Events.Event + alias Haj.Repo @impl true def mount(_params, _session, socket) do @@ -17,7 +18,7 @@ defmodule HajWeb.EventAdminLive.Index do defp apply_action(socket, :edit, %{"id" => id}) do socket |> assign(:page_title, "Edit Event") - |> assign(:event, Events.get_event!(id)) + |> assign(:event, Events.get_event!(id) |> Repo.preload(:ticket_types)) end defp apply_action(socket, :new, _params) do From 33e68246721112c287ee9230943a383f637b1e11 Mon Sep 17 00:00:00 2001 From: Hampus Hallkvist Date: Sun, 26 Mar 2023 16:40:46 +0200 Subject: [PATCH 10/56] fix: Make form component deletable --- .../live/event_admin_live/form_component.ex | 49 ++++++++++++++++--- 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/lib/haj_web/live/event_admin_live/form_component.ex b/lib/haj_web/live/event_admin_live/form_component.ex index 9298bb4..6404033 100644 --- a/lib/haj_web/live/event_admin_live/form_component.ex +++ b/lib/haj_web/live/event_admin_live/form_component.ex @@ -1,3 +1,5 @@ +require Logger + defmodule HajWeb.EventAdminLive.FormComponent do use HajWeb, :live_component @@ -25,12 +27,30 @@ defmodule HajWeb.EventAdminLive.FormComponent do <.input field={@form[:ticket_limit]} type="number" label="ticket_limit" /> <.input field={@form[:purchase_deadline]} type="datetime-local" label="purchase_deadline" /> <.input field={@form[:event_date]} type="datetime-local" label="event_date" /> - - <.inputs_for :let={f_nested} field={@form[:ticket_types]}> - <.input field={f_nested[:name]} type="text" label="name" /> - <.input field={f_nested[:price]} type="number" label="price" /> - <.input field={f_nested[:description]} type="text" label="description" /> - + +
+
+

Ticket Types

+ <.inputs_for :let={f_nested} field={@form[:ticket_types]}> +
+
+
+ <.input field={f_nested[:name]} type="text" label="name" /> +
+
+ <.input field={f_nested[:price]} type="number" label="price" /> +
+
+ <.input field={f_nested[:description]} type="text" label="description" /> +
+
+
+ <.button phx-click="delete" phx-value-ticket={@myself}>Delete +
+
+
+ +
<:actions> <.button phx-disable-with="Saving...">Save Event @@ -69,6 +89,10 @@ defmodule HajWeb.EventAdminLive.FormComponent do save_event(socket, socket.assigns.action, event_params) end + def handle_event("delete", %{"ticket" => ticket_type}, socket) do + delete_ticket_type(socket, ticket_type) + end + defp save_event(socket, :edit, event_params) do case Events.update_event(socket.assigns.event, event_params, with_tickets: true) do {:ok, _event} -> @@ -94,4 +118,17 @@ defmodule HajWeb.EventAdminLive.FormComponent do {:noreply, assign_form(socket, changeset)} end end + + defp delete_ticket_type(socket, ticket_type) do + case Events.delete_ticket_type(ticket_type) do + {:ok, _ticket_type} -> + {:noreply, + socket + |> put_flash(:info, "Ticket type deleted successfully") + |> push_navigate(to: socket.assigns.navigate)} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, assign_form(socket, changeset)} + end + end end From 38a5d31e2800c21bb88460a7f687f08b0efd4e38 Mon Sep 17 00:00:00 2001 From: Hampus Hallkvist Date: Sun, 26 Mar 2023 17:20:27 +0200 Subject: [PATCH 11/56] fix: Available tickets are now tied to a specific event and not a sum of all events --- lib/haj/events.ex | 10 +++++++- lib/haj/events/ticket_type.ex | 1 + lib/haj_web/live/event_live/index.ex | 35 ++++++++++++++-------------- 3 files changed, 27 insertions(+), 19 deletions(-) diff --git a/lib/haj/events.ex b/lib/haj/events.ex index 0c71dc8..5ce055b 100644 --- a/lib/haj/events.ex +++ b/lib/haj/events.ex @@ -312,7 +312,15 @@ defmodule Haj.Events do Retuns the number of tickets sold for an event. """ def tickets_sold(event_id) do - Repo.aggregate(EventRegistration, :count, :id, event_id: event_id) + # Count number of registrations for a given event + q = + from e in Event, + join: t in assoc(e, :ticket_types), + join: r in assoc(t, :registrations), + where: e.id == ^event_id, + select: count(r.id) + + Repo.one(q) end def subscribe do diff --git a/lib/haj/events/ticket_type.ex b/lib/haj/events/ticket_type.ex index 92b5468..552e00a 100644 --- a/lib/haj/events/ticket_type.ex +++ b/lib/haj/events/ticket_type.ex @@ -8,6 +8,7 @@ defmodule Haj.Events.TicketType do field :price, :integer belongs_to :event, Haj.Events.Event + has_many :registrations, Haj.Events.EventRegistration timestamps() end diff --git a/lib/haj_web/live/event_live/index.ex b/lib/haj_web/live/event_live/index.ex index bf5ce04..1326e7e 100644 --- a/lib/haj_web/live/event_live/index.ex +++ b/lib/haj_web/live/event_live/index.ex @@ -17,6 +17,7 @@ defmodule HajWeb.EventLive.Index do socket.id, %{} ) + {:ok, assign(socket, events: list_events(), online_count: initial_count)} end @@ -26,9 +27,9 @@ defmodule HajWeb.EventLive.Index do end def handle_info( - %{event: "presence_diff", payload: %{joins: joins, leaves: leaves}}, - %{assigns: %{online_count: count}} = socket - ) do + %{event: "presence_diff", payload: %{joins: joins, leaves: leaves}}, + %{assigns: %{online_count: count}} = socket + ) do online_count = count + map_size(joins) - map_size(leaves) {:noreply, assign(socket, :online_count, online_count)} end @@ -59,37 +60,35 @@ defmodule HajWeb.EventLive.Index do defp ticket_format_date(event_date) do Calendar.strftime(event_date, "%d %B %Y") end + defp ticket_format_time(event_date) do Calendar.strftime(event_date, "%H:%M") end defp online_count(assigns) do ~H""" -
-
-

<%= max(@online_count - 1, 0) %>

-
-

andra metaloger som väntar på biljetter

+
+
+

<%= max(@online_count - 1, 0) %>

+

andra metaloger som väntar på biljetter

+
""" end defp event_card(assigns) do ~H""" -
-
- -
-

<%= @event.availible %>

+
+
+ +
+

<%= @event.availible %>

Available tickets

-
+
-
+

<%= @event.name %>

<%= ticket_format_date(@event.event_date) %>
From 4a7938d069e4b432ecc0418f7f5f6472e4a517e2 Mon Sep 17 00:00:00 2001 From: Adrian Salamon Date: Thu, 30 Mar 2023 21:13:05 +0200 Subject: [PATCH 12/56] feat: crud for events --- lib/haj_web/components/layouts.ex | 6 ++ lib/haj_web/live/nav_live.ex | 89 ++++++++++------- .../settings_live/event/form_component.ex | 99 +++++++++++++++++++ lib/haj_web/live/settings_live/event/index.ex | 47 +++++++++ .../live/settings_live/event/index.html.heex | 50 ++++++++++ lib/haj_web/live/settings_live/event/show.ex | 22 +++++ .../live/settings_live/event/show.html.heex | 27 +++++ lib/haj_web/router.ex | 7 ++ 8 files changed, 310 insertions(+), 37 deletions(-) create mode 100644 lib/haj_web/live/settings_live/event/form_component.ex create mode 100644 lib/haj_web/live/settings_live/event/index.ex create mode 100644 lib/haj_web/live/settings_live/event/index.html.heex create mode 100644 lib/haj_web/live/settings_live/event/show.ex create mode 100644 lib/haj_web/live/settings_live/event/show.html.heex diff --git a/lib/haj_web/components/layouts.ex b/lib/haj_web/components/layouts.ex index 758ff92..5d96330 100644 --- a/lib/haj_web/components/layouts.ex +++ b/lib/haj_web/components/layouts.ex @@ -64,6 +64,12 @@ defmodule HajWeb.Layouts do active={@active_tab == {:setting, :foods}} /> + <:sub_link + navigate={~p"/live/settings/events"} + title="Event" + active={@active_tab == {:setting, :event}} + /> + <:sub_link navigate={~p"/live/settings/users"} title="Användare" diff --git a/lib/haj_web/live/nav_live.ex b/lib/haj_web/live/nav_live.ex index 5a5bb62..e1ac459 100644 --- a/lib/haj_web/live/nav_live.ex +++ b/lib/haj_web/live/nav_live.ex @@ -1,12 +1,36 @@ +defmodule HajWeb.Nav.ActiveTab do + defmacro __using__(_opts) do + quote do + import HajWeb.Nav.ActiveTab + + @tabs [] + @before_compile HajWeb.Nav.ActiveTab + end + end + + defmacro tab(module, tab_val) do + quote do + @tabs [{HajWeb.unquote(module), unquote(tab_val)} | @tabs] + end + end + + defmacro __before_compile__(_env) do + quote do + def active_tab(view) do + Enum.reduce_while(@tabs, nil, fn {mod, tab}, found -> + case view == mod do + true -> {:halt, tab} + false -> {:cont, found} + end + end) + end + end + end +end + defmodule HajWeb.Nav do use HajWeb, :component - - alias HajWeb.UserLive - alias HajWeb.GroupLive - alias HajWeb.GroupsLive - alias HajWeb.MembersLive - alias HajWeb.DashboardLive - alias HajWeb.SettingsLive + use HajWeb.Nav.ActiveTab def on_mount(:default, _params, _session, socket) do {:cont, @@ -22,38 +46,29 @@ defmodule HajWeb.Nav do |> Phoenix.LiveView.attach_hook(:active_tab, :handle_params, &set_active_tab/3)} end + # Small macros for setting tabs. Syntax: tab ModuleName, :active_tab, :expanded_tab + tab(DashboardLive.Index, :dashboard) + tab(MembersLive, :members) + tab(GroupsLive, :groups) + tab(MerchLive.Index, :merch) + tab(MerchAdminLive.Index, :merch_admin) + tab(MerchAdminLive.Orders, :merch_orders) + tab(ResponsibilityLive.Index, :responsibilities) + tab(ResponsibilityLive.History, :responsibility_history) + tab(SettingsLive.Index, :settings) + tab(SettingsLive.Show.Index, {:setting, :shows}) + tab(SettingsLive.Group.Index, {:setting, :groups}) + tab(SettingsLive.Food.Index, {:setting, :foods}) + tab(SettingsLive.User.Index, {:setting, :users}) + tab(SettingsLive.Merch.Index, {:setting, :merch}) + tab(SettingsLive.Event.Index, {:setting, :event}) + defp set_active_tab(params, _url, socket) do active_tab = - case {socket.view, socket.assigns.live_action} do - {DashboardLive.Index, _} -> - :dashboard - - {MembersLive, _} -> - :members - - {GroupsLive, _} -> - :groups - - {GroupLive, _} -> - {:group, String.to_integer(params["show_group_id"])} - - {SettingsLive.Index, _} -> - :settings - - {SettingsLive.Show.Index, _} -> - {:setting, :shows} - - {SettingsLive.Group.Index, _} -> - {:setting, :groups} - - {SettingsLive.Food.Index, _} -> - {:setting, :foods} - - {SettingsLive.User.Index, _} -> - {:setting, :users} - - {_, _} -> - nil + if socket.view == HajWeb.GroupLive do + {:group, String.to_integer(params["show_group_id"])} + else + active_tab(socket.view) end {:cont, assign(socket, active_tab: active_tab)} diff --git a/lib/haj_web/live/settings_live/event/form_component.ex b/lib/haj_web/live/settings_live/event/form_component.ex new file mode 100644 index 0000000..6c21b4a --- /dev/null +++ b/lib/haj_web/live/settings_live/event/form_component.ex @@ -0,0 +1,99 @@ +defmodule HajWeb.SettingsLive.Event.FormComponent do + use HajWeb, :live_component + + alias Haj.Events + + @impl true + def render(assigns) do + ~H""" +
+ <.header> + <%= @title %> + <:subtitle>Ett event i databasen. + + + <.simple_form + for={@form} + id="event-form" + phx-target={@myself} + phx-change="validate" + phx-submit="save" + > + <.input field={@form[:name]} type="text" label="Namn" /> + <.input field={@form[:description]} type="text" label="Beskrivning" /> + <.input field={@form[:image]} type="text" label="Bild" /> + <.input field={@form[:ticket_limit]} type="number" label="Biljettgräns" /> + <.input field={@form[:event_date]} type="datetime-local" label="Eventdatum" /> + <.input field={@form[:purchase_deadline]} type="datetime-local" label="Biljettsläppsdatum" /> + + <:actions> + <.button phx-disable-with="Sparar...">Spara event + + +
+ """ + end + + @impl true + def update(%{event: event} = assigns, socket) do + changeset = Events.change_event(event) + + {:ok, + socket + |> assign(assigns) + |> assign_form(changeset)} + end + + @impl true + def handle_event("validate", %{"event" => event_params}, socket) do + changeset = + socket.assigns.event + |> Events.change_event(event_params) + |> Map.put(:action, :validate) + + {:noreply, assign_form(socket, changeset)} + end + + def handle_event("save", %{"event" => event_params}, socket) do + save_event(socket, socket.assigns.action, event_params) + end + + defp save_event(socket, :edit, event_params) do + case Events.update_event(socket.assigns.event, event_params) do + {:ok, event} -> + notify_parent({:saved, event}) + + {:noreply, + socket + |> put_flash(:info, "Event uppdaterades") + |> push_patch(to: socket.assigns.patch)} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, assign_form(socket, changeset)} + end + end + + defp save_event(socket, :new, event_params) do + case Events.create_event(event_params) do + {:ok, event} -> + notify_parent({:saved, event}) + + {:noreply, + socket + |> put_flash(:info, "Event skapades") + |> push_patch(to: socket.assigns.patch)} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, + socket + |> put_flash(:error, "Något gick fel") + |> assign_form(changeset)} + end + end + + defp assign_form(socket, %Ecto.Changeset{} = changeset) do + assign(socket, :form, to_form(changeset)) + end + + defp notify_parent(msg), do: send(self(), {__MODULE__, msg}) +end diff --git a/lib/haj_web/live/settings_live/event/index.ex b/lib/haj_web/live/settings_live/event/index.ex new file mode 100644 index 0000000..ee006a7 --- /dev/null +++ b/lib/haj_web/live/settings_live/event/index.ex @@ -0,0 +1,47 @@ +defmodule HajWeb.SettingsLive.Event.Index do + use HajWeb, :live_view + + alias Haj.Events + alias Haj.Events.Event + + @impl true + def mount(_params, _session, socket) do + {:ok, stream(socket, :events, Events.list_events())} + end + + @impl true + def handle_params(params, _url, socket) do + {:noreply, apply_action(socket, socket.assigns.live_action, params)} + end + + defp apply_action(socket, :edit, %{"id" => id}) do + socket + |> assign(:page_title, "Redigera event") + |> assign(:event, Events.get_event!(id)) + end + + defp apply_action(socket, :new, _params) do + socket + |> assign(:page_title, "Nytt event") + |> assign(:event, %Event{}) + end + + defp apply_action(socket, :index, _params) do + socket + |> assign(:page_title, "Alla event") + |> assign(:event, nil) + end + + @impl true + def handle_info({HajWeb.SettingsLive.Event.FormComponent, {:saved, event}}, socket) do + {:noreply, stream_insert(socket, :events, event)} + end + + @impl true + def handle_event("delete", %{"id" => id}, socket) do + event = Events.get_event!(id) + {:ok, _} = Events.delete_event(event) + + {:noreply, stream_delete(socket, :events, event)} + end +end diff --git a/lib/haj_web/live/settings_live/event/index.html.heex b/lib/haj_web/live/settings_live/event/index.html.heex new file mode 100644 index 0000000..bfe76b9 --- /dev/null +++ b/lib/haj_web/live/settings_live/event/index.html.heex @@ -0,0 +1,50 @@ +<.header> + Alla event + <:actions> + <.link patch={~p"/live/settings/events/new"}> + <.button>Nytt event + + + <:subtitle> + Alla alternativ som användare kan välja mellan när de anger sina matpreferenser. Det går inte att ta bort en matpreferens som någon har valt. + + + +<.table + small + id="events" + rows={@streams.events} + row_click={fn {_id, event} -> JS.navigate(~p"/live/settings/events/#{event}") end} +> + <:col :let={{_id, event}} label="Namn"><%= event.name %> + <:action :let={{_id, event}}> +
+ <.link navigate={~p"/live/settings/events/#{event}"}>Visa +
+ <.link patch={~p"/live/settings/events/#{event}/edit"}>Redigera + + <:action :let={{id, event}}> + <.link + phx-click={JS.push("delete", value: %{id: event.id}) |> hide("##{id}")} + data-confirm="Är du säker?" + > + Radera + + + + +<.modal + :if={@live_action in [:new, :edit]} + id="event-modal" + show + on_cancel={JS.navigate(~p"/live/settings/events")} +> + <.live_component + module={HajWeb.SettingsLive.Event.FormComponent} + id={@event.id || :new} + title={@page_title} + action={@live_action} + event={@event} + patch={~p"/live/settings/events"} + /> + diff --git a/lib/haj_web/live/settings_live/event/show.ex b/lib/haj_web/live/settings_live/event/show.ex new file mode 100644 index 0000000..15021b0 --- /dev/null +++ b/lib/haj_web/live/settings_live/event/show.ex @@ -0,0 +1,22 @@ +defmodule HajWeb.SettingsLive.Event.Show do + use HajWeb, :live_view + + alias Haj.Events + + @impl true + def mount(%{"id" => id}, _session, socket) do + event = Events.get_event!(id) + {:ok, assign(socket, event: event)} + end + + @impl true + def handle_params(%{"id" => id}, _, socket) do + {:noreply, + socket + |> assign(:page_title, page_title(socket.assigns.live_action)) + |> assign(:food, Events.get_event!(id))} + end + + defp page_title(:show), do: "Mat" + defp page_title(:edit), do: "Redigera mat" +end diff --git a/lib/haj_web/live/settings_live/event/show.html.heex b/lib/haj_web/live/settings_live/event/show.html.heex new file mode 100644 index 0000000..1f57a17 --- /dev/null +++ b/lib/haj_web/live/settings_live/event/show.html.heex @@ -0,0 +1,27 @@ +<.header> + Event + <:subtitle><%= @event.name %> + <:actions> + <.link patch={~p"/live/settings/events/#{@event}/show/edit"} phx-click={JS.push_focus()}> + <.button>Redigera event + + + + +<.back navigate={~p"/live/settings/events"}>Tillbaka till events + +<.modal + :if={@live_action == :edit} + id="event-modal" + show + on_cancel={JS.patch(~p"/live/settings/events/#{@event}")} +> + <.live_component + module={HajWeb.SettingsLive.Event.FormComponent} + id={@event.id} + title={@page_title} + action={@live_action} + event={@event} + patch={~p"/live/settings/events/#{@event}"} + /> + diff --git a/lib/haj_web/router.ex b/lib/haj_web/router.ex index 2f21e5c..abe003c 100644 --- a/lib/haj_web/router.ex +++ b/lib/haj_web/router.ex @@ -98,6 +98,13 @@ defmodule HajWeb.Router do live "/foods/:id", SettingsLive.Food.Show, :show live "/foods/:id/show/edit", SettingsLive.Food.Show, :edit + live "/events", SettingsLive.Event.Index, :index + live "/events/new", SettingsLive.Event.Index, :new + live "/events/:id/edit", SettingsLive.Event.Index, :edit + + live "/events/:id", SettingsLive.Event.Show, :show + live "/events/:id/show/edit", SettingsLive.Event.Show, :edit + live "/users", SettingsLive.User.Index, :index live "/users/:id/edit", SettingsLive.User.Index, :edit end From 86950a7eb0a169b912b25bfd2248f14ac20897a8 Mon Sep 17 00:00:00 2001 From: Hampus Hallkvist Date: Thu, 30 Mar 2023 22:16:27 +0200 Subject: [PATCH 13/56] Add events to sidebar, and add single event page Co-authored-by: Adrian Salamon --- .formatter.exs | 3 +- lib/haj/events.ex | 8 + lib/haj_web/components/layouts.ex | 27 +- .../live/event_admin_live/index.html.heex | 8 +- lib/haj_web/live/event_single_live/index.ex | 16 + .../live/event_single_live/index.html.heex | 1 + .../live/merch_admin_live/index.html.heex | 8 +- lib/haj_web/live/nav_live.ex | 4 +- .../settings_live/event/form_component.ex | 99 ---- lib/haj_web/live/settings_live/event/index.ex | 47 -- .../live/settings_live/event/index.html.heex | 50 -- lib/haj_web/live/settings_live/event/show.ex | 22 - .../live/settings_live/event/show.html.heex | 27 - lib/haj_web/live/settings_live/index.ex | 3 + lib/haj_web/router.ex | 483 +++++++++--------- 15 files changed, 297 insertions(+), 509 deletions(-) create mode 100644 lib/haj_web/live/event_single_live/index.ex create mode 100644 lib/haj_web/live/event_single_live/index.html.heex delete mode 100644 lib/haj_web/live/settings_live/event/form_component.ex delete mode 100644 lib/haj_web/live/settings_live/event/index.ex delete mode 100644 lib/haj_web/live/settings_live/event/index.html.heex delete mode 100644 lib/haj_web/live/settings_live/event/show.ex delete mode 100644 lib/haj_web/live/settings_live/event/show.html.heex diff --git a/.formatter.exs b/.formatter.exs index 07e8207..6a99704 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -2,5 +2,6 @@ import_deps: [:ecto, :phoenix], plugins: [TailwindFormatter.MultiFormatter], inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs,heex}"], - subdirectories: ["priv/*/migrations"] + subdirectories: ["priv/*/migrations"], + locals_without_parens: [tab: 2] ] diff --git a/lib/haj/events.ex b/lib/haj/events.ex index 5ce055b..053f8f6 100644 --- a/lib/haj/events.ex +++ b/lib/haj/events.ex @@ -29,6 +29,14 @@ defmodule Haj.Events do Repo.all(query) end + def get_event do + query = + from e in Event, + preload: [:ticket_types] + + Repo.one(query) + end + @doc """ Gets a single event. diff --git a/lib/haj_web/components/layouts.ex b/lib/haj_web/components/layouts.ex index 5d96330..6105196 100644 --- a/lib/haj_web/components/layouts.ex +++ b/lib/haj_web/components/layouts.ex @@ -2,7 +2,7 @@ defmodule HajWeb.Layouts do use HajWeb, :html require Logger - embed_templates "layouts/*" + embed_templates("layouts/*") alias HajWeb.Endpoint @@ -38,6 +38,13 @@ defmodule HajWeb.Layouts do /> + <.nav_link + navigate={~p"/live/events"} + icon_name={:calendar_days} + title="Events" + active={@active_tab == :events} + /> + <.nav_link_group :if={@current_user.role == :admin} navigate={~p"/live/settings"} @@ -65,7 +72,7 @@ defmodule HajWeb.Layouts do /> <:sub_link - navigate={~p"/live/settings/events"} + navigate={~p"/live/settings/events-admin"} title="Event" active={@active_tab == {:setting, :event}} /> @@ -88,16 +95,16 @@ defmodule HajWeb.Layouts do end) end - attr :active, :boolean, default: false - attr :expanded, :boolean, default: false - attr :navigate, :any, required: true - attr :icon_name, :atom, required: true - attr :title, :string, required: true + attr(:active, :boolean, default: false) + attr(:expanded, :boolean, default: false) + attr(:navigate, :any, required: true) + attr(:icon_name, :atom, required: true) + attr(:title, :string, required: true) slot :sub_link do - attr :navigate, :any, required: true - attr :title, :string, required: true - attr :active, :boolean + attr(:navigate, :any, required: true) + attr(:title, :string, required: true) + attr(:active, :boolean) end def nav_link_group(assigns) do diff --git a/lib/haj_web/live/event_admin_live/index.html.heex b/lib/haj_web/live/event_admin_live/index.html.heex index f5f84ad..b4bdbf8 100644 --- a/lib/haj_web/live/event_admin_live/index.html.heex +++ b/lib/haj_web/live/event_admin_live/index.html.heex @@ -1,7 +1,7 @@ <.header> Listing Events <:actions> - <.link patch={~p"/live/events-admin/new"}> + <.link patch={~p"/live/settings/events-admin/new"}> <.button>New Event @@ -15,7 +15,7 @@ <:col :let={event} label="Event date"><%= event.event_date %> <:col :let={event} label="Purchase deadline"><%= event.purchase_deadline %> <:action :let={event}> - <.link patch={~p"/live/events-admin/#{event}/edit"}>Edit + <.link patch={~p"/live/settings/events-admin/#{event}/edit"}>Edit <:action :let={event}> <.link phx-click={JS.push("delete", value: %{id: event.id})} data-confirm="Are you sure?"> @@ -28,7 +28,7 @@ :if={@live_action in [:new, :edit]} id="event-modal" show - on_cancel={JS.navigate(~p"/live/events-admin")} + on_cancel={JS.navigate(~p"/live/settings/events-admin")} > <.live_component module={HajWeb.EventAdminLive.FormComponent} @@ -36,6 +36,6 @@ title={@page_title} action={@live_action} event={@event} - navigate={~p"/live/events-admin"} + navigate={~p"/live/settings/events-admin"} /> diff --git a/lib/haj_web/live/event_single_live/index.ex b/lib/haj_web/live/event_single_live/index.ex new file mode 100644 index 0000000..b574336 --- /dev/null +++ b/lib/haj_web/live/event_single_live/index.ex @@ -0,0 +1,16 @@ +defmodule HajWeb.EventSingleLive.Index do + use HajWeb, :live_view + + alias Haj.Events + alias Haj.Events.Event + + @impl true + def mount(_params, _session, socket) do + {:ok, socket} + end + + @impl true + def handle_info({Events, _, _}, socket) do + {:noreply, socket} + end +end diff --git a/lib/haj_web/live/event_single_live/index.html.heex b/lib/haj_web/live/event_single_live/index.html.heex new file mode 100644 index 0000000..9cd17bf --- /dev/null +++ b/lib/haj_web/live/event_single_live/index.html.heex @@ -0,0 +1 @@ +

Events

diff --git a/lib/haj_web/live/merch_admin_live/index.html.heex b/lib/haj_web/live/merch_admin_live/index.html.heex index 99b06f0..b1a0712 100644 --- a/lib/haj_web/live/merch_admin_live/index.html.heex +++ b/lib/haj_web/live/merch_admin_live/index.html.heex @@ -11,7 +11,7 @@ class: "mt-1 block w-full rounded-md border border-gray-300 bg-white px-3 py-2 shadow-sm focus:border-zinc-500 focus:outline-none focus:ring-zinc-500 sm:text-sm" ) %>
- <.link patch={~p"/live/merch-admin/new"}> + <.link patch={~p"/live/settings/merch-admin/new"}>
""" diff --git a/lib/haj_web/router.ex b/lib/haj_web/router.ex index abe003c..7c6c316 100644 --- a/lib/haj_web/router.ex +++ b/lib/haj_web/router.ex @@ -1,246 +1,241 @@ defmodule HajWeb.Router do - @moduledoc """ - Router for Haj, the internal system. - """ - use HajWeb, :router - - import HajWeb.UserAuth - - pipeline :browser do - plug :accepts, ["html"] - plug :fetch_session - plug :fetch_live_flash - plug :put_root_layout, {HajWeb.Layouts, :root} - plug :put_layout, {HajWeb.Layouts, :haj} - plug :protect_from_forgery - plug :put_secure_browser_headers - plug :fetch_current_user - end - - pipeline :api do - plug :accepts, ["json"] - end - - scope "/", HajWeb do - pipe_through :browser - - get "/login", SessionController, :login - get "/login/callback", SessionController, :callback - get "/logout", SessionController, :logout - get "/login/via-api", SessionController, :login_api - - live_session :default, on_mount: [{HajWeb.UserAuth, :current_user}] do - live "/signin", SignInLive, :index - end - - get "/", LoginController, :login - get "/unauthorized", LoginController, :unauthorized - end - - scope "/live", HajWeb do - pipe_through :browser - - live_session :authenticated, on_mount: [{HajWeb.UserAuth, :ensure_authenticated}, HajWeb.Nav] do - live "/", DashboardLive.Index, :index - live "/unauthorized", DashboardLive.Unauthorized, :index - - live "/user-settings", UserSettingsLive, :index - live "/members", MembersLive, :index - live "/user/:username", UserLive, :index - live "/groups", GroupsLive, :index - live "/group/:show_group_id", GroupLive, :index - - live "/merch", MerchLive.Index, :index - live "/merch/new", MerchLive.Index, :new - live "/merch/:merch_order_item_id/edit", MerchLive.Index, :edit - - live "/merch-admin", MerchAdminLive.Index, :index - live "/merch-admin/new", MerchAdminLive.Index, :new - live "/merch-admin/:id/edit", MerchAdminLive.Index, :edit - - live "/events", EventLive.Index, :index - live "/events-admin", EventAdminLive.Index, :index - live "/events-admin/new", EventAdminLive.Index, :new - live "/events-admin/:id/edit", EventAdminLive.Index, :edit - end - - # Admin only! - live_session :admin, on_mount: [{HajWeb.UserAuth, :ensure_admin}, {HajWeb.Nav, :settings}] do - scope "/settings" do - live "/", SettingsLive.Index, :index - live "/shows", SettingsLive.Show.Index, :index - live "/shows/new", SettingsLive.Show.Index, :new - live "/shows/:id/edit", SettingsLive.Show.Index, :edit - - live "/shows/:id", SettingsLive.Show.Show, :show - live "/shows/:id/show/edit", SettingsLive.Show.Show, :edit - - live "/shows/:id/show-groups/:show_group_id/edit", - SettingsLive.Show.Show, - :edit_show_group - - live "/groups", SettingsLive.Group.Index, :index - live "/groups/new", SettingsLive.Group.Index, :new - live "/groups/:id/edit", SettingsLive.Group.Index, :edit - live "/groups/:id", SettingsLive.Group.Show, :show - live "/groups/:id/show/edit", SettingsLive.Group.Show, :edit - - live "/groups/:id/show-groups/new", SettingsLive.Group.Show, :new_show_group - - live "/groups/:id/show-groups/:show_group_id/edit", - SettingsLive.Group.Show, - :edit_show_group - - live "/foods", SettingsLive.Food.Index, :index - live "/foods/new", SettingsLive.Food.Index, :new - live "/foods/:id/edit", SettingsLive.Food.Index, :edit - - live "/foods/:id", SettingsLive.Food.Show, :show - live "/foods/:id/show/edit", SettingsLive.Food.Show, :edit - - live "/events", SettingsLive.Event.Index, :index - live "/events/new", SettingsLive.Event.Index, :new - live "/events/:id/edit", SettingsLive.Event.Index, :edit - - live "/events/:id", SettingsLive.Event.Show, :show - live "/events/:id/show/edit", SettingsLive.Event.Show, :edit - - live "/users", SettingsLive.User.Index, :index - live "/users/:id/edit", SettingsLive.User.Index, :edit - end - end - end - - scope "/", HajWeb do - pipe_through [:browser, :require_authenticated_user, :require_spex_access] - - scope "/dashboard" do - get "/", DashboardController, :index - - # Obsolete - get "/my-data", DashboardController, :edit_user - put "/my-data", DashboardController, :update_user - - # Merch stuff, also obsolete - get "/order-merch", DashboardController, :order_merch - get "/order-item/new", DashboardController, :new_order_item - get "/order-item/new/:item_id", DashboardController, :new_order_item - post "/order-item/new", DashboardController, :create_order_item - get "/order-item/:id/edit", DashboardController, :edit_order_item - put "/order-item/:id/edit", DashboardController, :update_order_item - delete "/order-item/:id", DashboardController, :delete_order_item - end - - # Obsolete - get "/user/:username", UserController, :index - get "/user/:username/groups", UserController, :groups - - # Obsolete - get "/members", MembersController, :index - - scope "/show-groups" do - # Obsolete - get "/", GroupController, :index - - get "/edit/:show_group_id", GroupController, :edit - - # Obsolete - get "/:show_group_id", GroupController, :group - - get "/:show_group_id/vcard", GroupController, :vcard - get "/:show_group_id/csv", GroupController, :csv - get "/:show_group_id/applications", GroupController, :applications - post "/:show_group_id/accept/:user_id", GroupController, :accept_user - end - - get "/applications", ApplicationController, :index - get "/applications/export", ApplicationController, :export - - get "/merch-admin/:show_id", MerchAdminController, :index - get "/merch-admin/:show_id/orders", MerchAdminController, :orders - get "/merch-admin/:show_id/csv", MerchAdminController, :csv - get "/merch-admin/:show_id/new", MerchAdminController, :new - post "/merch-admin/:show_id/new", MerchAdminController, :create - get "/merch-admin/:id/edit", MerchAdminController, :edit - put "/merch-admin/:id/edit", MerchAdminController, :update - delete "/merch-admin/:show_id/:id", MerchAdminController, :delete - end - - scope "/settings", HajWeb do - pipe_through [:browser, :require_authenticated_user, :require_admin_access] - - get "/", SettingsController, :index - get "/groups", SettingsController, :groups - get "/groups/new", SettingsController, :new_group - post "/groups", SettingsController, :create_group - get "/groups/:id", SettingsController, :edit_group - put "/groups/:id", SettingsController, :update_group - delete "/groups/:id", SettingsController, :delete_group - - get "/show/:show_id/groups", SettingsController, :show_groups - get "/show-group/:id", SettingsController, :edit_show_group - delete "/show-group/:id", SettingsController, :delete_show_group - post "/show/:show_id/groups", SettingsController, :add_show_group - - get "/shows", SettingsController, :shows - get "/show/new", SettingsController, :new_show - post "/show", SettingsController, :create_show - get "/show/:id", SettingsController, :edit_show - put "/show/:id", SettingsController, :update_show - get "/show/:id/csv", SettingsController, :csv - - get "/users", SettingsController, :users - get "/user/new", SettingsController, :new_user - get "/users/:id", SettingsController, :edit_user - put "/users/:id", SettingsController, :update_user - - get "/foods", SettingsController, :foods - get "/foods/new", SettingsController, :new_food - post "/foods/new", SettingsController, :create_food - get "/foods/:id", SettingsController, :edit_food - put "/foods/:id", SettingsController, :update_food - delete "/foods/:id", SettingsController, :delete_food - end - - scope "/sok", HajWeb do - pipe_through [:browser, :require_authenticated_user] - - get "/", ApplyController, :index - get "/sucess", ApplyController, :created - post "/", ApplyController, :apply - end - - # Other scopes may use custom stacks. - # scope "/api", HajWeb do - # pipe_through :api - # end - - # Enables LiveDashboard only for development - # - # If you want to use the LiveDashboard in production, you should put - # it behind authentication and allow only admins to access it. - # If your application does not have an admins-only section yet, - # you can use Plug.BasicAuth to set up some basic authentication - # as long as you are also using SSL (which you should anyway). - if Mix.env() in [:dev, :test] do - import Phoenix.LiveDashboard.Router - - scope "/" do - pipe_through :browser - - live_dashboard "/live-dashboard", metrics: HajWeb.Telemetry - end - end - - # Enables the Swoosh mailbox preview in development. - # - # Note that preview only shows emails that were sent by the same - # node running the Phoenix server. - if Mix.env() == :dev do - scope "/dev" do - pipe_through :browser - - forward "/mailbox", Plug.Swoosh.MailboxPreview - end - end +@moduledoc """ +Router for Haj, the internal system. +""" +use HajWeb, :router + +import HajWeb.UserAuth + +pipeline :browser do +plug :accepts, ["html"] +plug :fetch_session +plug :fetch_live_flash +plug :put_root_layout, {HajWeb.Layouts, :root} +plug :put_layout, {HajWeb.Layouts, :haj} +plug :protect_from_forgery +plug :put_secure_browser_headers +plug :fetch_current_user +end + +pipeline :api do +plug :accepts, ["json"] +end + +scope "/", HajWeb do +pipe_through :browser + +get "/login", SessionController, :login +get "/login/callback", SessionController, :callback +get "/logout", SessionController, :logout +get "/login/via-api", SessionController, :login_api + +live_session :default, on_mount: [{HajWeb.UserAuth, :current_user}] do +live "/signin", SignInLive, :index +end + +get "/", LoginController, :login +get "/unauthorized", LoginController, :unauthorized +end + +scope "/live", HajWeb do +pipe_through :browser + +live_session :authenticated, on_mount: [{HajWeb.UserAuth, :ensure_authenticated}, HajWeb.Nav] do +live "/", DashboardLive.Index, :index +live "/unauthorized", DashboardLive.Unauthorized, :index + +live "/user-settings", UserSettingsLive, :index +live "/members", MembersLive, :index +live "/user/:username", UserLive, :index +live "/groups", GroupsLive, :index +live "/group/:show_group_id", GroupLive, :index + +live "/merch", MerchLive.Index, :index +live "/merch/new", MerchLive.Index, :new +live "/merch/:merch_order_item_id/edit", MerchLive.Index, :edit + +live "/merch-admin", MerchAdminLive.Index, :index +live "/merch-admin/new", MerchAdminLive.Index, :new +live "/merch-admin/:id/edit", MerchAdminLive.Index, :edit + +live "/events", EventLive.Index, :index +live "/events/:id", EventSingleLive.Index, :index +end + +# Admin only! +live_session :admin, on_mount: [{HajWeb.UserAuth, :ensure_admin}, {HajWeb.Nav, :settings}] do +scope "/settings" do +live "/", SettingsLive.Index, :index +live "/shows", SettingsLive.Show.Index, :index +live "/shows/new", SettingsLive.Show.Index, :new +live "/shows/:id/edit", SettingsLive.Show.Index, :edit + +live "/events-admin", EventAdminLive.Index, :index +live "/events-admin/new", EventAdminLive.Index, :new +live "/events-admin/:id/edit", EventAdminLive.Index, :edit + +live "/shows/:id", SettingsLive.Show.Show, :show +live "/shows/:id/show/edit", SettingsLive.Show.Show, :edit + +live "/shows/:id/show-groups/:show_group_id/edit", +SettingsLive.Show.Show, +:edit_show_group + +live "/groups", SettingsLive.Group.Index, :index +live "/groups/new", SettingsLive.Group.Index, :new +live "/groups/:id/edit", SettingsLive.Group.Index, :edit +live "/groups/:id", SettingsLive.Group.Show, :show +live "/groups/:id/show/edit", SettingsLive.Group.Show, :edit + +live "/groups/:id/show-groups/new", SettingsLive.Group.Show, :new_show_group + +live "/groups/:id/show-groups/:show_group_id/edit", +SettingsLive.Group.Show, +:edit_show_group + +live "/foods", SettingsLive.Food.Index, :index +live "/foods/new", SettingsLive.Food.Index, :new +live "/foods/:id/edit", SettingsLive.Food.Index, :edit + +live "/foods/:id", SettingsLive.Food.Show, :show +live "/foods/:id/show/edit", SettingsLive.Food.Show, :edit + +live "/users", SettingsLive.User.Index, :index +live "/users/:id/edit", SettingsLive.User.Index, :edit +end +end +end + +scope "/", HajWeb do +pipe_through [:browser, :require_authenticated_user, :require_spex_access] + +scope "/dashboard" do +get "/", DashboardController, :index + +# Obsolete +get "/my-data", DashboardController, :edit_user +put "/my-data", DashboardController, :update_user + +# Merch stuff, also obsolete +get "/order-merch", DashboardController, :order_merch +get "/order-item/new", DashboardController, :new_order_item +get "/order-item/new/:item_id", DashboardController, :new_order_item +post "/order-item/new", DashboardController, :create_order_item +get "/order-item/:id/edit", DashboardController, :edit_order_item +put "/order-item/:id/edit", DashboardController, :update_order_item +delete "/order-item/:id", DashboardController, :delete_order_item +end + +# Obsolete +get "/user/:username", UserController, :index +get "/user/:username/groups", UserController, :groups + +# Obsolete +get "/members", MembersController, :index + +scope "/show-groups" do +# Obsolete +get "/", GroupController, :index + +get "/edit/:show_group_id", GroupController, :edit + +# Obsolete +get "/:show_group_id", GroupController, :group + +get "/:show_group_id/vcard", GroupController, :vcard +get "/:show_group_id/csv", GroupController, :csv +get "/:show_group_id/applications", GroupController, :applications +post "/:show_group_id/accept/:user_id", GroupController, :accept_user +end + +get "/applications", ApplicationController, :index +get "/applications/export", ApplicationController, :export + +get "/merch-admin/:show_id", MerchAdminController, :index +get "/merch-admin/:show_id/orders", MerchAdminController, :orders +get "/merch-admin/:show_id/csv", MerchAdminController, :csv +get "/merch-admin/:show_id/new", MerchAdminController, :new +post "/merch-admin/:show_id/new", MerchAdminController, :create +get "/merch-admin/:id/edit", MerchAdminController, :edit +put "/merch-admin/:id/edit", MerchAdminController, :update +delete "/merch-admin/:show_id/:id", MerchAdminController, :delete +end + +scope "/settings", HajWeb do +pipe_through [:browser, :require_authenticated_user, :require_admin_access] + +get "/", SettingsController, :index +get "/groups", SettingsController, :groups +get "/groups/new", SettingsController, :new_group +post "/groups", SettingsController, :create_group +get "/groups/:id", SettingsController, :edit_group +put "/groups/:id", SettingsController, :update_group +delete "/groups/:id", SettingsController, :delete_group + +get "/show/:show_id/groups", SettingsController, :show_groups +get "/show-group/:id", SettingsController, :edit_show_group +delete "/show-group/:id", SettingsController, :delete_show_group +post "/show/:show_id/groups", SettingsController, :add_show_group + +get "/shows", SettingsController, :shows +get "/show/new", SettingsController, :new_show +post "/show", SettingsController, :create_show +get "/show/:id", SettingsController, :edit_show +put "/show/:id", SettingsController, :update_show +get "/show/:id/csv", SettingsController, :csv + +get "/users", SettingsController, :users +get "/user/new", SettingsController, :new_user +get "/users/:id", SettingsController, :edit_user +put "/users/:id", SettingsController, :update_user + +get "/foods", SettingsController, :foods +get "/foods/new", SettingsController, :new_food +post "/foods/new", SettingsController, :create_food +get "/foods/:id", SettingsController, :edit_food +put "/foods/:id", SettingsController, :update_food +delete "/foods/:id", SettingsController, :delete_food +end + +scope "/sok", HajWeb do +pipe_through [:browser, :require_authenticated_user] + +get "/", ApplyController, :index +get "/sucess", ApplyController, :created +post "/", ApplyController, :apply +end + +# Other scopes may use custom stacks. +# scope "/api", HajWeb do +# pipe_through :api +# end + +# Enables LiveDashboard only for development +# +# If you want to use the LiveDashboard in production, you should put +# it behind authentication and allow only admins to access it. +# If your application does not have an admins-only section yet, +# you can use Plug.BasicAuth to set up some basic authentication +# as long as you are also using SSL (which you should anyway). +if Mix.env() in [:dev, :test] do +import Phoenix.LiveDashboard.Router + +scope "/" do +pipe_through :browser + +live_dashboard "/live-dashboard", metrics: HajWeb.Telemetry +end +end + +# Enables the Swoosh mailbox preview in development. +# +# Note that preview only shows emails that were sent by the same +# node running the Phoenix server. +if Mix.env() == :dev do +scope "/dev" do +pipe_through :browser + +forward "/mailbox", Plug.Swoosh.MailboxPreview +end +end end From 3f5ab48ffbfa1f65ffa94351b3e992d734fbc337 Mon Sep 17 00:00:00 2001 From: Hampus Hallkvist Date: Thu, 30 Mar 2023 23:27:50 +0200 Subject: [PATCH 14/56] Add data fetching for single page event, and improved layout of events Co-authored-by: Adrian Salamon --- lib/haj/events.ex | 8 - lib/haj_web/live/event_live/index.ex | 272 ++++++----- lib/haj_web/live/event_live/index.html.heex | 6 +- lib/haj_web/live/event_live/show.ex | 18 + lib/haj_web/live/event_live/show.html.heex | 9 + lib/haj_web/router.ex | 478 ++++++++++---------- 6 files changed, 434 insertions(+), 357 deletions(-) create mode 100644 lib/haj_web/live/event_live/show.ex create mode 100644 lib/haj_web/live/event_live/show.html.heex diff --git a/lib/haj/events.ex b/lib/haj/events.ex index 053f8f6..5ce055b 100644 --- a/lib/haj/events.ex +++ b/lib/haj/events.ex @@ -29,14 +29,6 @@ defmodule Haj.Events do Repo.all(query) end - def get_event do - query = - from e in Event, - preload: [:ticket_types] - - Repo.one(query) - end - @doc """ Gets a single event. diff --git a/lib/haj_web/live/event_live/index.ex b/lib/haj_web/live/event_live/index.ex index 1326e7e..53c517d 100644 --- a/lib/haj_web/live/event_live/index.ex +++ b/lib/haj_web/live/event_live/index.ex @@ -1,115 +1,173 @@ defmodule HajWeb.EventLive.Index do - use HajWeb, :live_view - - alias Haj.Events - alias Haj.Events.Event - alias Haj.Presence - - @impl true - def mount(_params, _session, socket) do - Events.subscribe() - initial_count = Presence.list("custom_channel") |> map_size - HajWeb.Endpoint.subscribe("custom_channel") - - Presence.track( - self(), - "custom_channel", - socket.id, - %{} - ) - - {:ok, assign(socket, events: list_events(), online_count: initial_count)} - end - - @impl true - def handle_info({Events, [:registration, _], _}, socket) do - {:noreply, assign(socket, events: list_events())} - end - - def handle_info( - %{event: "presence_diff", payload: %{joins: joins, leaves: leaves}}, - %{assigns: %{online_count: count}} = socket - ) do - online_count = count + map_size(joins) - map_size(leaves) - {:noreply, assign(socket, :online_count, online_count)} - end - - defp list_events do - # Todo: maybe fetch in one query - Events.list_events() - |> Enum.map(fn event -> - ticked_sold = Events.tickets_sold(event.id) - Map.put(event, :availible, event.ticket_limit - ticked_sold) - end) - end - - @impl true - def handle_event("register", %{"id" => id}, socket) do - case Events.create_event_registration(%{ - "user_id" => socket.assigns.current_user.id, - "ticket_type_id" => id - }) do - {:ok, _} -> - {:noreply, socket |> put_flash(:info, "Purchased")} - - {:error, _} -> - {:noreply, socket |> put_flash(:error, "Error")} - end - end - - defp ticket_format_date(event_date) do - Calendar.strftime(event_date, "%d %B %Y") - end - - defp ticket_format_time(event_date) do - Calendar.strftime(event_date, "%H:%M") - end - - defp online_count(assigns) do - ~H""" -
-
-

<%= max(@online_count - 1, 0) %>

-
-

andra metaloger som väntar på biljetter

+use HajWeb, :live_view + +alias Haj.Events +alias Haj.Events.Event +alias Haj.Presence + +@impl true +def mount(_params, _session, socket) do +Events.subscribe() +initial_count = Presence.list("custom_channel") |> map_size +HajWeb.Endpoint.subscribe("custom_channel") + +Presence.track( +self(), +"custom_channel", +socket.id, +%{} +) + +{:ok, assign(socket, events: list_events(), online_count: initial_count)} +end + +@impl true +def handle_info({Events, [:registration, _], _}, socket) do +{:noreply, assign(socket, events: list_events())} +end + +def handle_info( +%{event: "presence_diff", payload: %{joins: joins, leaves: leaves}}, +%{assigns: %{online_count: count}} = socket +) do +online_count = count + map_size(joins) - map_size(leaves) +{:noreply, assign(socket, :online_count, online_count)} +end + +defp list_events do +# Todo: maybe fetch in one query +Events.list_events() +|> Enum.map(fn event -> +ticked_sold = Events.tickets_sold(event.id) +Map.put(event, :availible, event.ticket_limit - ticked_sold) +end) +end + +@impl true +def handle_event("register", %{"id" => id}, socket) do +case Events.create_event_registration(%{ +"user_id" => socket.assigns.current_user.id, +"ticket_type_id" => id +}) do +{:ok, _} -> +{:noreply, socket |> put_flash(:info, "Purchased")} + +{:error, _} -> +{:noreply, socket |> put_flash(:error, "Error")} +end +end + +defp ticket_format_date(event_date) do +Calendar.strftime(event_date, "%d %B %Y") +end + +defp ticket_format_time(event_date) do +Calendar.strftime(event_date, "%H:%M") +end + +defp online_count(assigns) do +~H""" +
+
+

<%= max(@online_count - 1, 0) %>

+
+

andra metaloger som väntar på biljetter

+
+""" +end + +defp month_name(month) do +{"januari", "februari", "mars", "april", "maj", "juni", "juli", "augusti", "september", +"oktober", "november", "december"} +|> elem(month - 1) +end + +defp event_card(assigns) do +~H""" +<.link navigate={~p"/live/events/#{@event}"}> +
+
+ {"Picture
- """ - end - - defp event_card(assigns) do - ~H""" -
-
- -
-

<%= @event.availible %>

-

Available tickets

+
+ Priser från 128 kr +
+ +
+
+
<%= @event.name %>
+
+ <%= Calendar.strftime(@event.event_date, "%d %B %H:%M", month_names: &month_name/1) %>
-
-
-
-

<%= @event.name %>

-
-
<%= ticket_format_date(@event.event_date) %>
-
<%= ticket_format_time(@event.event_date) %>
+
+
+ + +<%!-- + +
+ +
+ Priser från {props.event.price} kr +
+
+
+
{props.event.shortTitle}
+
{props.event.location.title}
+
{`${props.event.startTime + .toTimeString() + .substr(0, 5)}`}
+
+
+ {props.event.startTime.getDate()} + + {props.event.startTime.toLocaleString("sv-SE", { + month: "short", + })}{" "} +
-
<%= @event.description %>
-
- -
-
- """ - end + --%> +<%!--
+
+ +
+

<%= @event.availible %>

+

Available tickets

+
+
+
+
+
+

<%= @event.name %>

+
+
<%= ticket_format_date(@event.event_date) %>
+
<%= ticket_format_time(@event.event_date) %>
+
+
+
<%= @event.description %>
+
+
+ +
--%> +""" +end end diff --git a/lib/haj_web/live/event_live/index.html.heex b/lib/haj_web/live/event_live/index.html.heex index d317ab2..9a23c67 100644 --- a/lib/haj_web/live/event_live/index.html.heex +++ b/lib/haj_web/live/event_live/index.html.heex @@ -2,6 +2,6 @@ <.online_count online_count={@online_count} /> -<%= for event <- @events do %> - <.event_card event={event} title="hej" /> -<% end %> \ No newline at end of file +
+ <.event_card :for={event <- @events} event={event} /> +
diff --git a/lib/haj_web/live/event_live/show.ex b/lib/haj_web/live/event_live/show.ex new file mode 100644 index 0000000..16f2815 --- /dev/null +++ b/lib/haj_web/live/event_live/show.ex @@ -0,0 +1,18 @@ +defmodule HajWeb.EventLive.Show do + use HajWeb, :live_view + + alias Haj.Events + alias Haj.Events.Event + + @impl true + def mount(%{"id" => id}, _session, socket) do + event = Events.get_event!(id) + + {:ok, assign(socket, event: event)} + end + + @impl true + def handle_info({Events, _, _}, socket) do + {:noreply, socket} + end +end diff --git a/lib/haj_web/live/event_live/show.html.heex b/lib/haj_web/live/event_live/show.html.heex new file mode 100644 index 0000000..29a1445 --- /dev/null +++ b/lib/haj_web/live/event_live/show.html.heex @@ -0,0 +1,9 @@ +
+

<%= @event.name %>

+

<%= @event.description %>

+ {"Picture +
diff --git a/lib/haj_web/router.ex b/lib/haj_web/router.ex index 7c6c316..7d082af 100644 --- a/lib/haj_web/router.ex +++ b/lib/haj_web/router.ex @@ -1,241 +1,241 @@ defmodule HajWeb.Router do -@moduledoc """ -Router for Haj, the internal system. -""" -use HajWeb, :router - -import HajWeb.UserAuth - -pipeline :browser do -plug :accepts, ["html"] -plug :fetch_session -plug :fetch_live_flash -plug :put_root_layout, {HajWeb.Layouts, :root} -plug :put_layout, {HajWeb.Layouts, :haj} -plug :protect_from_forgery -plug :put_secure_browser_headers -plug :fetch_current_user -end - -pipeline :api do -plug :accepts, ["json"] -end - -scope "/", HajWeb do -pipe_through :browser - -get "/login", SessionController, :login -get "/login/callback", SessionController, :callback -get "/logout", SessionController, :logout -get "/login/via-api", SessionController, :login_api - -live_session :default, on_mount: [{HajWeb.UserAuth, :current_user}] do -live "/signin", SignInLive, :index -end - -get "/", LoginController, :login -get "/unauthorized", LoginController, :unauthorized -end - -scope "/live", HajWeb do -pipe_through :browser - -live_session :authenticated, on_mount: [{HajWeb.UserAuth, :ensure_authenticated}, HajWeb.Nav] do -live "/", DashboardLive.Index, :index -live "/unauthorized", DashboardLive.Unauthorized, :index - -live "/user-settings", UserSettingsLive, :index -live "/members", MembersLive, :index -live "/user/:username", UserLive, :index -live "/groups", GroupsLive, :index -live "/group/:show_group_id", GroupLive, :index - -live "/merch", MerchLive.Index, :index -live "/merch/new", MerchLive.Index, :new -live "/merch/:merch_order_item_id/edit", MerchLive.Index, :edit - -live "/merch-admin", MerchAdminLive.Index, :index -live "/merch-admin/new", MerchAdminLive.Index, :new -live "/merch-admin/:id/edit", MerchAdminLive.Index, :edit - -live "/events", EventLive.Index, :index -live "/events/:id", EventSingleLive.Index, :index -end - -# Admin only! -live_session :admin, on_mount: [{HajWeb.UserAuth, :ensure_admin}, {HajWeb.Nav, :settings}] do -scope "/settings" do -live "/", SettingsLive.Index, :index -live "/shows", SettingsLive.Show.Index, :index -live "/shows/new", SettingsLive.Show.Index, :new -live "/shows/:id/edit", SettingsLive.Show.Index, :edit - -live "/events-admin", EventAdminLive.Index, :index -live "/events-admin/new", EventAdminLive.Index, :new -live "/events-admin/:id/edit", EventAdminLive.Index, :edit - -live "/shows/:id", SettingsLive.Show.Show, :show -live "/shows/:id/show/edit", SettingsLive.Show.Show, :edit - -live "/shows/:id/show-groups/:show_group_id/edit", -SettingsLive.Show.Show, -:edit_show_group - -live "/groups", SettingsLive.Group.Index, :index -live "/groups/new", SettingsLive.Group.Index, :new -live "/groups/:id/edit", SettingsLive.Group.Index, :edit -live "/groups/:id", SettingsLive.Group.Show, :show -live "/groups/:id/show/edit", SettingsLive.Group.Show, :edit - -live "/groups/:id/show-groups/new", SettingsLive.Group.Show, :new_show_group - -live "/groups/:id/show-groups/:show_group_id/edit", -SettingsLive.Group.Show, -:edit_show_group - -live "/foods", SettingsLive.Food.Index, :index -live "/foods/new", SettingsLive.Food.Index, :new -live "/foods/:id/edit", SettingsLive.Food.Index, :edit - -live "/foods/:id", SettingsLive.Food.Show, :show -live "/foods/:id/show/edit", SettingsLive.Food.Show, :edit - -live "/users", SettingsLive.User.Index, :index -live "/users/:id/edit", SettingsLive.User.Index, :edit -end -end -end - -scope "/", HajWeb do -pipe_through [:browser, :require_authenticated_user, :require_spex_access] - -scope "/dashboard" do -get "/", DashboardController, :index - -# Obsolete -get "/my-data", DashboardController, :edit_user -put "/my-data", DashboardController, :update_user - -# Merch stuff, also obsolete -get "/order-merch", DashboardController, :order_merch -get "/order-item/new", DashboardController, :new_order_item -get "/order-item/new/:item_id", DashboardController, :new_order_item -post "/order-item/new", DashboardController, :create_order_item -get "/order-item/:id/edit", DashboardController, :edit_order_item -put "/order-item/:id/edit", DashboardController, :update_order_item -delete "/order-item/:id", DashboardController, :delete_order_item -end - -# Obsolete -get "/user/:username", UserController, :index -get "/user/:username/groups", UserController, :groups - -# Obsolete -get "/members", MembersController, :index - -scope "/show-groups" do -# Obsolete -get "/", GroupController, :index - -get "/edit/:show_group_id", GroupController, :edit - -# Obsolete -get "/:show_group_id", GroupController, :group - -get "/:show_group_id/vcard", GroupController, :vcard -get "/:show_group_id/csv", GroupController, :csv -get "/:show_group_id/applications", GroupController, :applications -post "/:show_group_id/accept/:user_id", GroupController, :accept_user -end - -get "/applications", ApplicationController, :index -get "/applications/export", ApplicationController, :export - -get "/merch-admin/:show_id", MerchAdminController, :index -get "/merch-admin/:show_id/orders", MerchAdminController, :orders -get "/merch-admin/:show_id/csv", MerchAdminController, :csv -get "/merch-admin/:show_id/new", MerchAdminController, :new -post "/merch-admin/:show_id/new", MerchAdminController, :create -get "/merch-admin/:id/edit", MerchAdminController, :edit -put "/merch-admin/:id/edit", MerchAdminController, :update -delete "/merch-admin/:show_id/:id", MerchAdminController, :delete -end - -scope "/settings", HajWeb do -pipe_through [:browser, :require_authenticated_user, :require_admin_access] - -get "/", SettingsController, :index -get "/groups", SettingsController, :groups -get "/groups/new", SettingsController, :new_group -post "/groups", SettingsController, :create_group -get "/groups/:id", SettingsController, :edit_group -put "/groups/:id", SettingsController, :update_group -delete "/groups/:id", SettingsController, :delete_group - -get "/show/:show_id/groups", SettingsController, :show_groups -get "/show-group/:id", SettingsController, :edit_show_group -delete "/show-group/:id", SettingsController, :delete_show_group -post "/show/:show_id/groups", SettingsController, :add_show_group - -get "/shows", SettingsController, :shows -get "/show/new", SettingsController, :new_show -post "/show", SettingsController, :create_show -get "/show/:id", SettingsController, :edit_show -put "/show/:id", SettingsController, :update_show -get "/show/:id/csv", SettingsController, :csv - -get "/users", SettingsController, :users -get "/user/new", SettingsController, :new_user -get "/users/:id", SettingsController, :edit_user -put "/users/:id", SettingsController, :update_user - -get "/foods", SettingsController, :foods -get "/foods/new", SettingsController, :new_food -post "/foods/new", SettingsController, :create_food -get "/foods/:id", SettingsController, :edit_food -put "/foods/:id", SettingsController, :update_food -delete "/foods/:id", SettingsController, :delete_food -end - -scope "/sok", HajWeb do -pipe_through [:browser, :require_authenticated_user] - -get "/", ApplyController, :index -get "/sucess", ApplyController, :created -post "/", ApplyController, :apply -end - -# Other scopes may use custom stacks. -# scope "/api", HajWeb do -# pipe_through :api -# end - -# Enables LiveDashboard only for development -# -# If you want to use the LiveDashboard in production, you should put -# it behind authentication and allow only admins to access it. -# If your application does not have an admins-only section yet, -# you can use Plug.BasicAuth to set up some basic authentication -# as long as you are also using SSL (which you should anyway). -if Mix.env() in [:dev, :test] do -import Phoenix.LiveDashboard.Router - -scope "/" do -pipe_through :browser - -live_dashboard "/live-dashboard", metrics: HajWeb.Telemetry -end -end - -# Enables the Swoosh mailbox preview in development. -# -# Note that preview only shows emails that were sent by the same -# node running the Phoenix server. -if Mix.env() == :dev do -scope "/dev" do -pipe_through :browser - -forward "/mailbox", Plug.Swoosh.MailboxPreview -end -end + @moduledoc """ + Router for Haj, the internal system. + """ + use HajWeb, :router + + import HajWeb.UserAuth + + pipeline :browser do + plug :accepts, ["html"] + plug :fetch_session + plug :fetch_live_flash + plug :put_root_layout, {HajWeb.Layouts, :root} + plug :put_layout, {HajWeb.Layouts, :haj} + plug :protect_from_forgery + plug :put_secure_browser_headers + plug :fetch_current_user + end + + pipeline :api do + plug :accepts, ["json"] + end + + scope "/", HajWeb do + pipe_through :browser + + get "/login", SessionController, :login + get "/login/callback", SessionController, :callback + get "/logout", SessionController, :logout + get "/login/via-api", SessionController, :login_api + + live_session :default, on_mount: [{HajWeb.UserAuth, :current_user}] do + live "/signin", SignInLive, :index + end + + get "/", LoginController, :login + get "/unauthorized", LoginController, :unauthorized + end + + scope "/live", HajWeb do + pipe_through :browser + + live_session :authenticated, on_mount: [{HajWeb.UserAuth, :ensure_authenticated}, HajWeb.Nav] do + live "/", DashboardLive.Index, :index + live "/unauthorized", DashboardLive.Unauthorized, :index + + live "/user-settings", UserSettingsLive, :index + live "/members", MembersLive, :index + live "/user/:username", UserLive, :index + live "/groups", GroupsLive, :index + live "/group/:show_group_id", GroupLive, :index + + live "/merch", MerchLive.Index, :index + live "/merch/new", MerchLive.Index, :new + live "/merch/:merch_order_item_id/edit", MerchLive.Index, :edit + + live "/merch-admin", MerchAdminLive.Index, :index + live "/merch-admin/new", MerchAdminLive.Index, :new + live "/merch-admin/:id/edit", MerchAdminLive.Index, :edit + + live "/events", EventLive.Index, :index + live "/events/:id", EventLive.Show, :index + end + + # Admin only! + live_session :admin, on_mount: [{HajWeb.UserAuth, :ensure_admin}, {HajWeb.Nav, :settings}] do + scope "/settings" do + live "/", SettingsLive.Index, :index + live "/shows", SettingsLive.Show.Index, :index + live "/shows/new", SettingsLive.Show.Index, :new + live "/shows/:id/edit", SettingsLive.Show.Index, :edit + + live "/events-admin", EventAdminLive.Index, :index + live "/events-admin/new", EventAdminLive.Index, :new + live "/events-admin/:id/edit", EventAdminLive.Index, :edit + + live "/shows/:id", SettingsLive.Show.Show, :show + live "/shows/:id/show/edit", SettingsLive.Show.Show, :edit + + live "/shows/:id/show-groups/:show_group_id/edit", + SettingsLive.Show.Show, + :edit_show_group + + live "/groups", SettingsLive.Group.Index, :index + live "/groups/new", SettingsLive.Group.Index, :new + live "/groups/:id/edit", SettingsLive.Group.Index, :edit + live "/groups/:id", SettingsLive.Group.Show, :show + live "/groups/:id/show/edit", SettingsLive.Group.Show, :edit + + live "/groups/:id/show-groups/new", SettingsLive.Group.Show, :new_show_group + + live "/groups/:id/show-groups/:show_group_id/edit", + SettingsLive.Group.Show, + :edit_show_group + + live "/foods", SettingsLive.Food.Index, :index + live "/foods/new", SettingsLive.Food.Index, :new + live "/foods/:id/edit", SettingsLive.Food.Index, :edit + + live "/foods/:id", SettingsLive.Food.Show, :show + live "/foods/:id/show/edit", SettingsLive.Food.Show, :edit + + live "/users", SettingsLive.User.Index, :index + live "/users/:id/edit", SettingsLive.User.Index, :edit + end + end + end + + scope "/", HajWeb do + pipe_through [:browser, :require_authenticated_user, :require_spex_access] + + scope "/dashboard" do + get "/", DashboardController, :index + + # Obsolete + get "/my-data", DashboardController, :edit_user + put "/my-data", DashboardController, :update_user + + # Merch stuff, also obsolete + get "/order-merch", DashboardController, :order_merch + get "/order-item/new", DashboardController, :new_order_item + get "/order-item/new/:item_id", DashboardController, :new_order_item + post "/order-item/new", DashboardController, :create_order_item + get "/order-item/:id/edit", DashboardController, :edit_order_item + put "/order-item/:id/edit", DashboardController, :update_order_item + delete "/order-item/:id", DashboardController, :delete_order_item + end + + # Obsolete + get "/user/:username", UserController, :index + get "/user/:username/groups", UserController, :groups + + # Obsolete + get "/members", MembersController, :index + + scope "/show-groups" do + # Obsolete + get "/", GroupController, :index + + get "/edit/:show_group_id", GroupController, :edit + + # Obsolete + get "/:show_group_id", GroupController, :group + + get "/:show_group_id/vcard", GroupController, :vcard + get "/:show_group_id/csv", GroupController, :csv + get "/:show_group_id/applications", GroupController, :applications + post "/:show_group_id/accept/:user_id", GroupController, :accept_user + end + + get "/applications", ApplicationController, :index + get "/applications/export", ApplicationController, :export + + get "/merch-admin/:show_id", MerchAdminController, :index + get "/merch-admin/:show_id/orders", MerchAdminController, :orders + get "/merch-admin/:show_id/csv", MerchAdminController, :csv + get "/merch-admin/:show_id/new", MerchAdminController, :new + post "/merch-admin/:show_id/new", MerchAdminController, :create + get "/merch-admin/:id/edit", MerchAdminController, :edit + put "/merch-admin/:id/edit", MerchAdminController, :update + delete "/merch-admin/:show_id/:id", MerchAdminController, :delete + end + + scope "/settings", HajWeb do + pipe_through [:browser, :require_authenticated_user, :require_admin_access] + + get "/", SettingsController, :index + get "/groups", SettingsController, :groups + get "/groups/new", SettingsController, :new_group + post "/groups", SettingsController, :create_group + get "/groups/:id", SettingsController, :edit_group + put "/groups/:id", SettingsController, :update_group + delete "/groups/:id", SettingsController, :delete_group + + get "/show/:show_id/groups", SettingsController, :show_groups + get "/show-group/:id", SettingsController, :edit_show_group + delete "/show-group/:id", SettingsController, :delete_show_group + post "/show/:show_id/groups", SettingsController, :add_show_group + + get "/shows", SettingsController, :shows + get "/show/new", SettingsController, :new_show + post "/show", SettingsController, :create_show + get "/show/:id", SettingsController, :edit_show + put "/show/:id", SettingsController, :update_show + get "/show/:id/csv", SettingsController, :csv + + get "/users", SettingsController, :users + get "/user/new", SettingsController, :new_user + get "/users/:id", SettingsController, :edit_user + put "/users/:id", SettingsController, :update_user + + get "/foods", SettingsController, :foods + get "/foods/new", SettingsController, :new_food + post "/foods/new", SettingsController, :create_food + get "/foods/:id", SettingsController, :edit_food + put "/foods/:id", SettingsController, :update_food + delete "/foods/:id", SettingsController, :delete_food + end + + scope "/sok", HajWeb do + pipe_through [:browser, :require_authenticated_user] + + get "/", ApplyController, :index + get "/sucess", ApplyController, :created + post "/", ApplyController, :apply + end + + # Other scopes may use custom stacks. + # scope "/api", HajWeb do + # pipe_through :api + # end + + # Enables LiveDashboard only for development + # + # If you want to use the LiveDashboard in production, you should put + # it behind authentication and allow only admins to access it. + # If your application does not have an admins-only section yet, + # you can use Plug.BasicAuth to set up some basic authentication + # as long as you are also using SSL (which you should anyway). + if Mix.env() in [:dev, :test] do + import Phoenix.LiveDashboard.Router + + scope "/" do + pipe_through :browser + + live_dashboard "/live-dashboard", metrics: HajWeb.Telemetry + end + end + + # Enables the Swoosh mailbox preview in development. + # + # Note that preview only shows emails that were sent by the same + # node running the Phoenix server. + if Mix.env() == :dev do + scope "/dev" do + pipe_through :browser + + forward "/mailbox", Plug.Swoosh.MailboxPreview + end + end end From fa8db4440401365833c7a51e895b53020219cef9 Mon Sep 17 00:00:00 2001 From: Adrian Salamon Date: Wed, 7 Feb 2024 19:37:46 -0800 Subject: [PATCH 15/56] bump deps --- mix.exs | 6 ++--- mix.lock | 82 ++++++++++++++++++++++++++++---------------------------- 2 files changed, 44 insertions(+), 44 deletions(-) diff --git a/mix.exs b/mix.exs index a462564..3fcab15 100644 --- a/mix.exs +++ b/mix.exs @@ -35,11 +35,11 @@ defmodule Haj.MixProject do {:phoenix, "~> 1.7"}, {:phoenix_view, "~> 2.0"}, {:phoenix_ecto, "~> 4.4"}, - {:ecto_sql, "~> 3.6"}, + {:ecto_sql, "~> 3.11"}, {:postgrex, ">= 0.0.0"}, - {:phoenix_html, "~> 3.0"}, + {:phoenix_html, "~> 3.3"}, {:phoenix_live_reload, "~> 1.2", only: :dev}, - {:phoenix_live_view, "~> 0.18"}, + {:phoenix_live_view, "~> 0.20"}, {:floki, ">= 0.30.0", only: :test}, {:phoenix_live_dashboard, "~> 0.7"}, {:esbuild, "~> 0.4", runtime: Mix.env() == :dev}, diff --git a/mix.lock b/mix.lock index 9fad105..fac2fa0 100644 --- a/mix.lock +++ b/mix.lock @@ -1,68 +1,68 @@ %{ - "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, - "castore": {:hex, :castore, "1.0.3", "7130ba6d24c8424014194676d608cb989f62ef8039efd50ff4b3f33286d06db8", [:mix], [], "hexpm", "680ab01ef5d15b161ed6a95449fac5c6b8f60055677a8e79acf01b27baa4390b"}, - "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"}, + "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, + "castore": {:hex, :castore, "1.0.5", "9eeebb394cc9a0f3ae56b813459f990abb0a3dedee1be6b27fdb50301930502f", [:mix], [], "hexpm", "8d7c597c3e4a64c395980882d4bca3cebb8d74197c590dc272cfd3b6a6310578"}, + "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"}, "cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, "cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"}, - "credo": {:hex, :credo, "1.7.0", "6119bee47272e85995598ee04f2ebbed3e947678dee048d10b5feca139435f75", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "6839fcf63d1f0d1c0f450abc8564a57c43d644077ab96f2934563e68b8a769d7"}, + "credo": {:hex, :credo, "1.7.3", "05bb11eaf2f2b8db370ecaa6a6bda2ec49b2acd5e0418bc106b73b07128c0436", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "35ea675a094c934c22fb1dca3696f3c31f2728ae6ef5a53b5d648c11180a4535"}, "csv": {:hex, :csv, "2.5.0", "c47b5a5221bf2e56d6e8eb79e77884046d7fd516280dc7d9b674251e0ae46246", [:mix], [{:parallel_stream, "~> 1.0.4 or ~> 1.1.0", [hex: :parallel_stream, repo: "hexpm", optional: false]}], "hexpm", "e821f541487045c7591a1963eeb42afff0dfa99bdcdbeb3410795a2f59c77d34"}, - "db_connection": {:hex, :db_connection, "2.5.0", "bb6d4f30d35ded97b29fe80d8bd6f928a1912ca1ff110831edcd238a1973652c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c92d5ba26cd69ead1ff7582dbb860adeedfff39774105a4f1c92cbb654b55aa2"}, + "db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"}, "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, - "earmark": {:hex, :earmark, "1.4.38", "ba8fda946c259c6e8f6759d3647d448e9216e2c0afed8c6ae7f8ce1f7072a497", [:mix], [{:earmark_parser, "~> 1.4.32", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "f938e30de4167e7d8f3bf588b01dc041138278dda1e5a13fb9ec89b43dd5ec7f"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.32", "fa739a0ecfa34493de19426681b23f6814573faee95dfd4b4aafe15a7b5b32c6", [:mix], [], "hexpm", "b8b0dd77d60373e77a3d7e8afa598f325e49e8663a51bcc2b88ef41838cca755"}, - "ecto": {:hex, :ecto, "3.10.2", "6b887160281a61aa16843e47735b8a266caa437f80588c3ab80a8a960e6abe37", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6a895778f0d7648a4b34b486af59a1c8009041fbdf2b17f1ac215eb829c60235"}, - "ecto_sql": {:hex, :ecto_sql, "3.10.1", "6ea6b3036a0b0ca94c2a02613fd9f742614b5cfe494c41af2e6571bb034dd94c", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.10.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f6a25bdbbd695f12c8171eaff0851fa4c8e72eec1e98c7364402dda9ce11c56b"}, - "esbuild": {:hex, :esbuild, "0.7.1", "fa0947e8c3c3c2f86c9bf7e791a0a385007ccd42b86885e8e893bdb6631f5169", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "66661cdf70b1378ee4dc16573fcee67750b59761b2605a0207c267ab9d19f13c"}, + "earmark": {:hex, :earmark, "1.4.46", "8c7287bd3137e99d26ae4643e5b7ef2129a260e3dcf41f251750cb4563c8fb81", [:mix], [], "hexpm", "798d86db3d79964e759ddc0c077d5eb254968ed426399fbf5a62de2b5ff8910a"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, + "ecto": {:hex, :ecto, "3.11.1", "4b4972b717e7ca83d30121b12998f5fcdc62ba0ed4f20fd390f16f3270d85c3e", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ebd3d3772cd0dfcd8d772659e41ed527c28b2a8bde4b00fe03e0463da0f1983b"}, + "ecto_sql": {:hex, :ecto_sql, "3.11.1", "e9abf28ae27ef3916b43545f9578b4750956ccea444853606472089e7d169470", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ce14063ab3514424276e7e360108ad6c2308f6d88164a076aac8a387e1fea634"}, + "esbuild": {:hex, :esbuild, "0.8.1", "0cbf919f0eccb136d2eeef0df49c4acf55336de864e63594adcea3814f3edf41", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "25fc876a67c13cb0a776e7b5d7974851556baeda2085296c14ab48555ea7560f"}, "ex_aws": {:hex, :ex_aws, "2.1.9", "dc4865ecc20a05190a34a0ac5213e3e5e2b0a75a0c2835e923ae7bfeac5e3c31", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "3e6c776703c9076001fbe1f7c049535f042cb2afa0d2cbd3b47cbc4e92ac0d10"}, - "ex_aws_s3": {:hex, :ex_aws_s3, "2.4.0", "ce8decb6b523381812798396bc0e3aaa62282e1b40520125d1f4eff4abdff0f4", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "85dda6e27754d94582869d39cba3241d9ea60b6aa4167f9c88e309dc687e56bb"}, - "ex_doc": {:hex, :ex_doc, "0.29.4", "6257ecbb20c7396b1fe5accd55b7b0d23f44b6aa18017b415cb4c2b91d997729", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "2c6699a737ae46cb61e4ed012af931b57b699643b24dabe2400a8168414bc4f5"}, - "expo": {:hex, :expo, "0.4.1", "1c61d18a5df197dfda38861673d392e642649a9cef7694d2f97a587b2cfb319b", [:mix], [], "hexpm", "2ff7ba7a798c8c543c12550fa0e2cbc81b95d4974c65855d8d15ba7b37a1ce47"}, + "ex_aws_s3": {:hex, :ex_aws_s3, "2.5.3", "422468e5c3e1a4da5298e66c3468b465cfd354b842e512cb1f6fbbe4e2f5bdaf", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "4f09dd372cc386550e484808c5ac5027766c8d0cd8271ccc578b82ee6ef4f3b8"}, + "ex_doc": {:hex, :ex_doc, "0.31.1", "8a2355ac42b1cc7b2379da9e40243f2670143721dd50748bf6c3b1184dae2089", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "3178c3a407c557d8343479e1ff117a96fd31bafe52a039079593fb0524ef61b0"}, + "expo": {:hex, :expo, "0.5.1", "249e826a897cac48f591deba863b26c16682b43711dd15ee86b92f25eafd96d9", [:mix], [], "hexpm", "68a4233b0658a3d12ee00d27d37d856b1ba48607e7ce20fd376958d0ba6ce92b"}, "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, - "floki": {:hex, :floki, "0.34.3", "5e2dcaec5d7c228ce5b1d3501502e308b2d79eb655e4191751a1fe491c37feac", [:mix], [], "hexpm", "9577440eea5b97924b4bf3c7ea55f7b8b6dce589f9b28b096cc294a8dc342341"}, - "gettext": {:hex, :gettext, "0.22.3", "c8273e78db4a0bb6fba7e9f0fd881112f349a3117f7f7c598fa18c66c888e524", [:mix], [{:expo, "~> 0.4.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "935f23447713954a6866f1bb28c3a878c4c011e802bcd68a726f5e558e4b64bd"}, - "hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~> 2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"}, + "floki": {:hex, :floki, "0.35.3", "0c8c6234aa71cb2b069cf801e8f8f30f8d096eb452c3dae2ccc409510ec32720", [:mix], [], "hexpm", "6d9f07f3fc76599f3b66c39f4a81ac62c8f4d9631140268db92aacad5d0e56d4"}, + "gettext": {:hex, :gettext, "0.24.0", "6f4d90ac5f3111673cbefc4ebee96fe5f37a114861ab8c7b7d5b30a1108ce6d8", [:mix], [{:expo, "~> 0.5.1", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "bdf75cdfcbe9e4622dd18e034b227d77dd17f0f133853a1c73b97b3d6c770e8b"}, + "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"}, "heroicons": {:hex, :heroicons, "0.5.3", "ee8ae8335303df3b18f2cc07f46e1cb6e761ba4cf2c901623fbe9a28c0bc51dd", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:phoenix_live_view, ">= 0.18.2", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "a210037e8a09ac17e2a0a0779d729e89c821c944434c3baa7edfc1f5b32f3502"}, "html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"}, - "html_sanitize_ex": {:hex, :html_sanitize_ex, "1.4.2", "c479398b6de798c03eb5d04a0a9a9159d73508f83f6590a00b8eacba3619cf4c", [:mix], [{:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm", "aef6c28585d06a9109ad591507e508854c5559561f950bbaea773900dd369b0e"}, + "html_sanitize_ex": {:hex, :html_sanitize_ex, "1.4.3", "67b3d9fa8691b727317e0cc96b9b3093be00ee45419ffb221cdeee88e75d1360", [:mix], [{:mochiweb, "~> 2.15 or ~> 3.1", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm", "87748d3c4afe949c7c6eb7150c958c2bcba43fc5b2a02686af30e636b74bccb7"}, "httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "imgproxy": {:hex, :imgproxy, "3.0.1", "1789da712c1630648884de032cb43deed9ea5b9637c51e94b7bb3a43dafce00e", [:mix], [], "hexpm", "ac8bb4b7b3c5edb21e7ef0644525bba4528eca4b291ff9db5dd327ab40dc8fde"}, - "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"}, - "let_me": {:hex, :let_me, "1.2.2", "1b7be945ce454d783cbb1d682db0b52fb74ec2ad394b084eebd0011759d4abf7", [:mix], [], "hexpm", "8641770eeb21c177bf3e9d59386d59197d38f8ef35220e0b05e1d7de8f5210f2"}, - "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, + "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, + "let_me": {:hex, :let_me, "1.2.3", "cf58f8265020b565dc2bb4f5339f8d2cf4d462689a8c9b80f54171b057bd0467", [:mix], [], "hexpm", "78ca04516dc9ad714fd53dd56227ac8b992d1998a5f97f4ec9be567244ee8550"}, + "makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"}, "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"}, - "makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"}, + "makeup_erlang": {:hex, :makeup_erlang, "0.1.4", "29563475afa9b8a2add1b7a9c8fb68d06ca7737648f28398e04461f008b69521", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f4ed47ecda66de70dd817698a703f8816daa91272e7e45812469498614ae8b29"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, - "mochiweb": {:hex, :mochiweb, "2.22.0", "f104d6747c01a330c38613561977e565b788b9170055c5241ac9dd6e4617cba5", [:rebar3], [], "hexpm", "cbbd1fd315d283c576d1c8a13e0738f6dafb63dc840611249608697502a07655"}, - "nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"}, + "mochiweb": {:hex, :mochiweb, "3.2.1", "ff287e1ec653a0828f226cd5a009d52be74537dc3fc274b765525a77ce01f8ec", [:rebar3], [], "hexpm", "975466d335403a78cd58186636b8e960e3c84c4d9c1a85eb7fe53b6a5dd54de7"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, "parallel_stream": {:hex, :parallel_stream, "1.1.0", "f52f73eb344bc22de335992377413138405796e0d0ad99d995d9977ac29f1ca9", [:mix], [], "hexpm", "684fd19191aedfaf387bbabbeb8ff3c752f0220c8112eb907d797f4592d6e871"}, - "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, - "phoenix": {:hex, :phoenix, "1.7.6", "61f0625af7c1d1923d582470446de29b008c0e07ae33d7a3859ede247ddaf59a", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "f6b4be7780402bb060cbc6e83f1b6d3f5673b674ba73cc4a7dd47db0322dfb88"}, - "phoenix_ecto": {:hex, :phoenix_ecto, "4.4.2", "b21bd01fdeffcfe2fab49e4942aa938b6d3e89e93a480d4aee58085560a0bc0d", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "70242edd4601d50b69273b057ecf7b684644c19ee750989fd555625ae4ce8f5d"}, - "phoenix_html": {:hex, :phoenix_html, "3.3.1", "4788757e804a30baac6b3fc9695bf5562465dd3f1da8eb8460ad5b404d9a2178", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "bed1906edd4906a15fd7b412b85b05e521e1f67c9a85418c55999277e553d0d3"}, - "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.0", "0b3158b5b198aa444473c91d23d79f52fb077e807ffad80dacf88ce078fa8df2", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "87785a54474fed91a67a1227a741097eb1a42c2e49d3c0d098b588af65cd410d"}, + "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, + "phoenix": {:hex, :phoenix, "1.7.11", "1d88fc6b05ab0c735b250932c4e6e33bfa1c186f76dcf623d8dd52f07d6379c7", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "b1ec57f2e40316b306708fe59b92a16b9f6f4bf50ccfa41aa8c7feb79e0ec02a"}, + "phoenix_ecto": {:hex, :phoenix_ecto, "4.4.3", "86e9878f833829c3f66da03d75254c155d91d72a201eb56ae83482328dc7ca93", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d36c401206f3011fefd63d04e8ef626ec8791975d9d107f9a0817d426f61ac07"}, + "phoenix_html": {:hex, :phoenix_html, "3.3.3", "380b8fb45912b5638d2f1d925a3771b4516b9a78587249cabe394e0a5d579dc9", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "923ebe6fec6e2e3b3e569dfbdc6560de932cd54b000ada0208b5f45024bdd76c"}, + "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.3", "7ff51c9b6609470f681fbea20578dede0e548302b0c8bdf338b5a753a4f045bf", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "f9470a0a8bae4f56430a23d42f977b5a6205fdba6559d76f932b876bfaec652d"}, "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.4.1", "2aff698f5e47369decde4357ba91fc9c37c6487a512b41732818f2204a8ef1d3", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "9bffb834e7ddf08467fe54ae58b5785507aaba6255568ae22b4d46e2bb3615ab"}, - "phoenix_live_view": {:hex, :phoenix_live_view, "0.19.3", "3918c1b34df8ac71a9a636806ba5b7f053349a0392b312e16f35b0bf4d070aab", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "545626887948495fd8ea23d83b75bd7aaf9dc4221563e158d2c4b52ea1dd7e00"}, + "phoenix_live_view": {:hex, :phoenix_live_view, "0.20.4", "0dc21e89dbf5b1f3a69090a92d1a2724bfa951d5cbccff6c5b318e12eac107e3", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d8930c9c79dd25775646874abdf3d8d24356b88d58fa14f637c8e3418d36bce3"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, - "phoenix_template": {:hex, :phoenix_template, "1.0.1", "85f79e3ad1b0180abb43f9725973e3b8c2c3354a87245f91431eec60553ed3ef", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "157dc078f6226334c91cb32c1865bf3911686f8bcd6bcff86736f6253e6993ee"}, - "phoenix_view": {:hex, :phoenix_view, "2.0.2", "6bd4d2fd595ef80d33b439ede6a19326b78f0f1d8d62b9a318e3d9c1af351098", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "a929e7230ea5c7ee0e149ffcf44ce7cf7f4b6d2bfe1752dd7c084cdff152d36f"}, - "plug": {:hex, :plug, "1.14.2", "cff7d4ec45b4ae176a227acd94a7ab536d9b37b942c8e8fa6dfc0fff98ff4d80", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "842fc50187e13cf4ac3b253d47d9474ed6c296a8732752835ce4a86acdf68d13"}, - "plug_cowboy": {:hex, :plug_cowboy, "2.6.1", "9a3bbfceeb65eff5f39dab529e5cd79137ac36e913c02067dba3963a26efe9b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "de36e1a21f451a18b790f37765db198075c25875c64834bcc82d90b309eb6613"}, - "plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"}, - "postgrex": {:hex, :postgrex, "0.17.1", "01c29fd1205940ee55f7addb8f1dc25618ca63a8817e56fac4f6846fc2cddcbe", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "14b057b488e73be2beee508fb1955d8db90d6485c6466428fe9ccf1d6692a555"}, + "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, + "phoenix_view": {:hex, :phoenix_view, "2.0.3", "4d32c4817fce933693741deeb99ef1392619f942633dde834a5163124813aad3", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "cd34049af41be2c627df99cd4eaa71fc52a328c0c3d8e7d4aa28f880c30e7f64"}, + "plug": {:hex, :plug, "1.15.3", "712976f504418f6dff0a3e554c40d705a9bcf89a7ccef92fc6a5ef8f16a30a97", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cc4365a3c010a56af402e0809208873d113e9c38c401cabd88027ef4f5c01fd2"}, + "plug_cowboy": {:hex, :plug_cowboy, "2.7.0", "3ae9369c60641084363b08fe90267cbdd316df57e3557ea522114b30b63256ea", [:mix], [{:cowboy, "~> 2.7.0 or ~> 2.8.0 or ~> 2.9.0 or ~> 2.10.0", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d85444fb8aa1f2fc62eabe83bbe387d81510d773886774ebdcb429b3da3c1a4a"}, + "plug_crypto": {:hex, :plug_crypto, "2.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"}, + "postgrex": {:hex, :postgrex, "0.17.4", "5777781f80f53b7c431a001c8dad83ee167bcebcf3a793e3906efff680ab62b3", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "6458f7d5b70652bc81c3ea759f91736c16a31be000f306d3c64bcdfe9a18b3cc"}, "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, - "swoosh": {:hex, :swoosh, "1.11.2", "39dd1e44f75bc03a34366d5f830599d248de2b9caaf05704dc76c0507a58c6a1", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4c43f4591503e7d5bf028314af8ac7c06d1c4d340aa23faeefabfa2543fa726e"}, - "tailwind": {:hex, :tailwind, "0.2.1", "83d8eadbe71a8e8f67861fe7f8d51658ecfb258387123afe4d9dc194eddc36b0", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "e8a13f6107c95f73e58ed1b4221744e1eb5a093cd1da244432067e19c8c9a277"}, - "tailwind_formatter": {:hex, :tailwind_formatter, "0.3.6", "f3b02687a79a99106f2cee604d36561091ab5b9c9d16a97ae5901d91b3357047", [:mix], [{:phoenix_live_view, ">= 0.17.6", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}], "hexpm", "3a0d75dad1700f9fa9394185c4ce0eb0eff2b1a0eb9aef66b4b382eae657bded"}, + "swoosh": {:hex, :swoosh, "1.15.2", "490ea85a98e8fb5178c07039e0d8519839e38127724a58947a668c00db7574ee", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.4 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9f7739c02f6c7c0ca82ee397f3bfe0465dbe4c8a65372ac2a5584bf147dd5831"}, + "tailwind": {:hex, :tailwind, "0.2.2", "9e27288b568ede1d88517e8c61259bc214a12d7eed271e102db4c93fcca9b2cd", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "ccfb5025179ea307f7f899d1bb3905cd0ac9f687ed77feebc8f67bdca78565c4"}, + "tailwind_formatter": {:hex, :tailwind_formatter, "0.3.7", "2728d031e6803dfddf63f1dd7c64b5b9fd70ffdf635709c50f47589f4fb48861", [:mix], [{:phoenix_live_view, ">= 0.17.6", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}], "hexpm", "3d91ac4d4622505b09c0f4678512281515b4fbe7644f012da1bd2722f5880185"}, "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, - "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"}, + "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.2", "2caabe9344ec17eafe5403304771c3539f3b6e2f7fb6a6f602558c825d0d0bfb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9b43db0dc33863930b9ef9d27137e78974756f5f198cae18409970ed6fa5b561"}, "telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, - "websock": {:hex, :websock, "0.5.2", "b3c08511d8d79ed2c2f589ff430bd1fe799bb389686dafce86d28801783d8351", [:mix], [], "hexpm", "925f5de22fca6813dfa980fb62fd542ec43a2d1a1f83d2caec907483fe66ff05"}, - "websock_adapter": {:hex, :websock_adapter, "0.5.3", "4908718e42e4a548fc20e00e70848620a92f11f7a6add8cf0886c4232267498d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "cbe5b814c1f86b6ea002b52dd99f345aeecf1a1a6964e209d208fb404d930d3d"}, + "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, + "websock_adapter": {:hex, :websock_adapter, "0.5.5", "9dfeee8269b27e958a65b3e235b7e447769f66b5b5925385f5a569269164a210", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "4b977ba4a01918acbf77045ff88de7f6972c2a009213c515a445c48f224ffce9"}, } From 499f358853c58f9ffd96afd5299d0d220e3f35f6 Mon Sep 17 00:00:00 2001 From: Adrian Salamon Date: Wed, 7 Feb 2024 19:38:33 -0800 Subject: [PATCH 16/56] feat: working events settings --- lib/haj/events.ex | 29 +++--- lib/haj/events/event.ex | 28 ++++-- lib/haj/events/ticket_type.ex | 1 + lib/haj_web/components/layouts.ex | 2 +- .../live/event_admin_live/index.html.heex | 41 --------- .../event}/form_component.ex | 90 ++++++++++++------- .../event}/index.ex | 8 +- .../live/settings_live/event/index.html.heex | 40 +++++++++ lib/haj_web/live/settings_live/index.ex | 4 +- lib/haj_web/router.ex | 6 +- 10 files changed, 144 insertions(+), 105 deletions(-) delete mode 100644 lib/haj_web/live/event_admin_live/index.html.heex rename lib/haj_web/live/{event_admin_live => settings_live/event}/form_component.ex (53%) rename lib/haj_web/live/{event_admin_live => settings_live/event}/index.ex (84%) create mode 100644 lib/haj_web/live/settings_live/event/index.html.heex diff --git a/lib/haj/events.ex b/lib/haj/events.ex index 5ce055b..e8e1635 100644 --- a/lib/haj/events.ex +++ b/lib/haj/events.ex @@ -57,9 +57,11 @@ defmodule Haj.Events do {:error, %Ecto.Changeset{}} """ - def create_event(attrs \\ %{}) do + def create_event(attrs \\ %{}, opts \\ []) do + with_tickets = Keyword.get(opts, :with_tickets, false) + %Event{} - |> Event.changeset(attrs) + |> Event.changeset(attrs, with_tickets: with_tickets) |> Repo.insert() end @@ -76,17 +78,11 @@ defmodule Haj.Events do """ def update_event(%Event{} = event, attrs, opts \\ []) do - with_ticets = Keyword.get(opts, :with_tickets, false) - - if with_ticets do - event - |> Event.changeset_ticket_types(attrs) - |> Repo.update() - else - event - |> Event.changeset(attrs) - |> Repo.update() - end + with_tickets = Keyword.get(opts, :with_tickets, false) + + event + |> Event.changeset(attrs, with_tickets: with_tickets) + |> Repo.update() end @doc """ @@ -114,8 +110,11 @@ defmodule Haj.Events do %Ecto.Changeset{data: %Event{}} """ - def change_event(%Event{} = event, attrs \\ %{}) do - Event.changeset(event, attrs) + def change_event(%Event{} = event, attrs \\ %{}, opts \\ []) do + with_tickets = Keyword.get(opts, :with_tickets, false) + + event + |> Event.changeset(attrs, with_tickets: with_tickets) end @doc """ diff --git a/lib/haj/events/event.ex b/lib/haj/events/event.ex index 79a808e..fd7d38e 100644 --- a/lib/haj/events/event.ex +++ b/lib/haj/events/event.ex @@ -10,29 +10,41 @@ defmodule Haj.Events.Event do field :purchase_deadline, :utc_datetime field :ticket_limit, :integer - has_many :ticket_types, Haj.Events.TicketType + has_many :ticket_types, Haj.Events.TicketType, on_replace: :delete timestamps() end @doc false - def changeset(event, attrs) do + def changeset(event, attrs, opts \\ []) do + with_tickets = Keyword.get(opts, :with_tickets, false) + + if with_tickets do + changeset_ticket_types(event, attrs) + else + default_changeset(event, attrs) + end + end + + defp default_changeset(event, attrs) do event |> cast(attrs, [:name, :description, :image, :ticket_limit, :event_date, :purchase_deadline]) |> validate_required([ :name, :description, - :image, :ticket_limit, - :event_date, - :purchase_deadline + :event_date ]) + |> validate_number(:ticket_limit, greater_than: 0) end @doc false - def changeset_ticket_types(event, attrs) do + defp changeset_ticket_types(event, attrs) do event - |> changeset(attrs) - |> cast_assoc(:ticket_types) + |> default_changeset(attrs) + |> cast_assoc(:ticket_types, + sort_param: :tickets_sort, + drop_param: :tickets_drop + ) end end diff --git a/lib/haj/events/ticket_type.ex b/lib/haj/events/ticket_type.ex index 552e00a..5881ea4 100644 --- a/lib/haj/events/ticket_type.ex +++ b/lib/haj/events/ticket_type.ex @@ -18,5 +18,6 @@ defmodule Haj.Events.TicketType do ticket_type |> cast(attrs, [:price, :name, :description]) |> validate_required([:price, :name, :description]) + |> validate_number(:price, greater_than_or_equal_to: 0) end end diff --git a/lib/haj_web/components/layouts.ex b/lib/haj_web/components/layouts.ex index 493d4ac..cbb5f8c 100644 --- a/lib/haj_web/components/layouts.ex +++ b/lib/haj_web/components/layouts.ex @@ -132,7 +132,7 @@ defmodule HajWeb.Layouts do /> <:sub_link - navigate={~p"/settings/events-admin"} + navigate={~p"/settings/events"} title="Event" active={@active_tab == {:setting, :event}} /> diff --git a/lib/haj_web/live/event_admin_live/index.html.heex b/lib/haj_web/live/event_admin_live/index.html.heex deleted file mode 100644 index c4d3968..0000000 --- a/lib/haj_web/live/event_admin_live/index.html.heex +++ /dev/null @@ -1,41 +0,0 @@ -<.header> - Listing Events - <:actions> - <.link patch={~p"/settings/events-admin/new"}> - <.button>New Event - - - - -<.table id="events" rows={@events}> - <:col :let={event} label="Name"><%= event.name %> - <:col :let={event} label="Description"><%= event.description %> - <:col :let={event} label="Image"><%= event.image %> - <:col :let={event} label="Ticket limit"><%= event.ticket_limit %> - <:col :let={event} label="Event date"><%= event.event_date %> - <:col :let={event} label="Purchase deadline"><%= event.purchase_deadline %> - <:action :let={event}> - <.link patch={~p"/settings/events-admin/#{event}/edit"}>Edit - - <:action :let={event}> - <.link phx-click={JS.push("delete", value: %{id: event.id})} data-confirm="Are you sure?"> - Delete - - - - -<.modal - :if={@live_action in [:new, :edit]} - id="event-modal" - show - on_cancel={JS.navigate(~p"/settings/events-admin")} -> - <.live_component - module={HajWeb.EventAdminLive.FormComponent} - id={@event.id || :new} - title={@page_title} - action={@live_action} - event={@event} - navigate={~p"/settings/events-admin"} - /> - diff --git a/lib/haj_web/live/event_admin_live/form_component.ex b/lib/haj_web/live/settings_live/event/form_component.ex similarity index 53% rename from lib/haj_web/live/event_admin_live/form_component.ex rename to lib/haj_web/live/settings_live/event/form_component.ex index 6404033..b6fa398 100644 --- a/lib/haj_web/live/event_admin_live/form_component.ex +++ b/lib/haj_web/live/settings_live/event/form_component.ex @@ -1,6 +1,6 @@ require Logger -defmodule HajWeb.EventAdminLive.FormComponent do +defmodule HajWeb.SettingsLive.Event.FormComponent do use HajWeb, :live_component alias Haj.Events @@ -17,43 +17,69 @@ defmodule HajWeb.EventAdminLive.FormComponent do
<.header> <%= @title %> - <:subtitle>Use this form to manage event records in your database. + <:subtitle>Redigera event. - <.simple_form for={@form} id="event-form" phx-target={@myself} phx-submit="save"> - <.input field={@form[:name]} type="text" label="name" /> - <.input field={@form[:description]} type="text" label="description" /> - <.input field={@form[:image]} type="text" label="image" /> - <.input field={@form[:ticket_limit]} type="number" label="ticket_limit" /> - <.input field={@form[:purchase_deadline]} type="datetime-local" label="purchase_deadline" /> - <.input field={@form[:event_date]} type="datetime-local" label="event_date" /> - -
-
-

Ticket Types

+ <.simple_form + for={@form} + id="event-form" + phx-target={@myself} + phx-submit="save" + phx-change="validate" + > + <.input field={@form[:name]} type="text" label="Namn" /> + <.input field={@form[:description]} type="text" label="Beskrivning" /> + <.input field={@form[:ticket_limit]} type="number" label="Biljettgräns" /> + <.input field={@form[:event_date]} type="datetime-local" label="Datum" /> + <.input field={@form[:purchase_deadline]} type="datetime-local" label="Köpdeadline" /> + <.input field={@form[:image]} type="text" label="Bild" /> + +
+

Biljettyper

<.inputs_for :let={f_nested} field={@form[:ticket_types]}> -
-
-
- <.input field={f_nested[:name]} type="text" label="name" /> -
-
- <.input field={f_nested[:price]} type="number" label="price" /> -
-
- <.input field={f_nested[:description]} type="text" label="description" /> -
-
-
- <.button phx-click="delete" phx-value-ticket={@myself}>Delete -
+ +
+
+ <.input field={f_nested[:name]} type="text" label="Namn" /> +
+
+ <.input field={f_nested[:price]} type="number" label="Pris" /> +
+
+ <.input field={f_nested[:description]} type="text" label="Beskrivning" /> +
+
+
+
+ + + +
+ +
<:actions> - <.button phx-disable-with="Saving...">Save Event + <.button phx-disable-with="Saving...">Spara event
@@ -75,9 +101,11 @@ defmodule HajWeb.EventAdminLive.FormComponent do def handle_event("validate", %{"event" => event_params}, socket) do changeset = socket.assigns.event - |> Events.change_event(event_params) + |> Events.change_event(event_params, with_tickets: true) |> Map.put(:action, :validate) + IO.inspect(assign_form(socket, changeset)) + {:noreply, assign_form(socket, changeset)} end @@ -107,7 +135,7 @@ defmodule HajWeb.EventAdminLive.FormComponent do end defp save_event(socket, :new, event_params) do - case Events.create_event(event_params) do + case Events.create_event(event_params, with_tickets: true) do {:ok, _event} -> {:noreply, socket diff --git a/lib/haj_web/live/event_admin_live/index.ex b/lib/haj_web/live/settings_live/event/index.ex similarity index 84% rename from lib/haj_web/live/event_admin_live/index.ex rename to lib/haj_web/live/settings_live/event/index.ex index afaca87..d62a730 100644 --- a/lib/haj_web/live/event_admin_live/index.ex +++ b/lib/haj_web/live/settings_live/event/index.ex @@ -1,4 +1,4 @@ -defmodule HajWeb.EventAdminLive.Index do +defmodule HajWeb.SettingsLive.Event.Index do use HajWeb, :live_view alias Haj.Events @@ -17,19 +17,19 @@ defmodule HajWeb.EventAdminLive.Index do defp apply_action(socket, :edit, %{"id" => id}) do socket - |> assign(:page_title, "Edit Event") + |> assign(:page_title, "Redigera event") |> assign(:event, Events.get_event!(id) |> Repo.preload(:ticket_types)) end defp apply_action(socket, :new, _params) do socket - |> assign(:page_title, "New Event") + |> assign(:page_title, "Nytt event") |> assign(:event, %Event{}) end defp apply_action(socket, :index, _params) do socket - |> assign(:page_title, "Listing Events") + |> assign(:page_title, "Event") |> assign(:event, nil) end diff --git a/lib/haj_web/live/settings_live/event/index.html.heex b/lib/haj_web/live/settings_live/event/index.html.heex new file mode 100644 index 0000000..dac1a67 --- /dev/null +++ b/lib/haj_web/live/settings_live/event/index.html.heex @@ -0,0 +1,40 @@ +<.header> + Event + <:actions> + <.link patch={~p"/settings/events/new"}> + <.button>Nytt event + + + + +<.table id="events" rows={@events}> + <:col :let={event} label="Namn"><%= event.name %> + <:col :let={event} label="Biljettgräns"><%= event.ticket_limit %> + <:col :let={event} label="Datum"><%= format_date(event.event_date) %> + <:col :let={event} label="Köpdeadline"><%= format_date(event.purchase_deadline) %> + + <:action :let={event}> + <.link patch={~p"/settings/events/#{event}/edit"}>Edit + + <:action :let={event}> + <.link phx-click={JS.push("delete", value: %{id: event.id})} data-confirm="Are you sure?"> + Delete + + + + +<.modal + :if={@live_action in [:new, :edit]} + id="event-modal" + show + on_cancel={JS.navigate(~p"/settings/events")} +> + <.live_component + module={HajWeb.SettingsLive.Event.FormComponent} + id={@event.id || :new} + title={@page_title} + action={@live_action} + event={@event} + navigate={~p"/settings/events"} + /> + diff --git a/lib/haj_web/live/settings_live/index.ex b/lib/haj_web/live/settings_live/index.ex index b0e621e..42a6ad2 100644 --- a/lib/haj_web/live/settings_live/index.ex +++ b/lib/haj_web/live/settings_live/index.ex @@ -3,7 +3,7 @@ defmodule HajWeb.SettingsLive.Index do @impl true def mount(_params, _session, socket) do - {:ok, socket} + {:ok, socket |> assign(:page_title, "Administrera")} end @impl true @@ -30,7 +30,7 @@ defmodule HajWeb.SettingsLive.Index do <.setting_card name="Användare" navigate={~p"/settings/users"}> Redigera användare och användaruppgifter - <.setting_card name="Events" navigate={~p"/settings/events-admin"}> + <.setting_card name="Events" navigate={~p"/settings/events"}> Redigera events <.setting_card name="Ansvar" navigate={~p"/settings/responsibilities"}> diff --git a/lib/haj_web/router.ex b/lib/haj_web/router.ex index 1ba10be..0671a0e 100644 --- a/lib/haj_web/router.ex +++ b/lib/haj_web/router.ex @@ -118,9 +118,9 @@ defmodule HajWeb.Router do live "/shows/new", SettingsLive.Show.Index, :new live "/shows/:id/edit", SettingsLive.Show.Index, :edit - live "/events-admin", EventAdminLive.Index, :index - live "/events-admin/new", EventAdminLive.Index, :new - live "/events-admin/:id/edit", EventAdminLive.Index, :edit + live "/events", SettingsLive.Event.Index, :index + live "/events/new", SettingsLive.Event.Index, :new + live "/events/:id/edit", SettingsLive.Event.Index, :edit live "/shows/:id", SettingsLive.Show.Show, :show live "/shows/:id/show/edit", SettingsLive.Show.Show, :edit From d2ca9a0d86bdb572f46431498036ac29a8eaa70d Mon Sep 17 00:00:00 2001 From: Adrian Salamon Date: Thu, 8 Feb 2024 00:32:27 -0800 Subject: [PATCH 17/56] first stab att event ui --- lib/haj/events.ex | 24 ++-- lib/haj_web/components/components.ex | 27 +++++ lib/haj_web/components/layouts/live.html.heex | 6 +- lib/haj_web/components/live_helpers.ex | 74 ++---------- lib/haj_web/live/dashboard_live/index.ex | 11 -- .../live/dashboard_live/index.html.heex | 28 +++-- lib/haj_web/live/event_live/index.ex | 107 +++--------------- lib/haj_web/live/event_live/index.html.heex | 13 ++- lib/haj_web/live/event_live/show.ex | 63 ++++++++++- lib/haj_web/live/event_live/show.html.heex | 103 +++++++++++++++-- lib/haj_web/live/event_single_live/index.ex | 15 --- .../live/event_single_live/index.html.heex | 1 - lib/haj_web/live/group_live/index.ex | 19 ---- lib/haj_web/live/group_live/index.html.heex | 16 ++- .../live/responsibility_live/history.ex | 14 --- .../responsibility_live/history.html.heex | 34 ++---- .../live/responsibility_live/index.html.heex | 42 ++++--- .../settings_live/event/form_component.ex | 2 +- lib/haj_web/live/settings_live/index.ex | 16 +-- lib/haj_web/live/show_live/index.ex | 19 ---- lib/haj_web/live/show_live/index.html.heex | 8 +- .../20221122195103_create_events.exs | 2 +- 22 files changed, 305 insertions(+), 339 deletions(-) delete mode 100644 lib/haj_web/live/event_single_live/index.ex delete mode 100644 lib/haj_web/live/event_single_live/index.html.heex diff --git a/lib/haj/events.ex b/lib/haj/events.ex index e8e1635..c110425 100644 --- a/lib/haj/events.ex +++ b/lib/haj/events.ex @@ -43,7 +43,13 @@ defmodule Haj.Events do ** (Ecto.NoResultsError) """ - def get_event!(id), do: Repo.get!(Event, id) + def get_event!(id) do + Repo.one!( + from e in Event, + where: e.id == ^id, + preload: :ticket_types + ) + end @doc """ Creates a event. @@ -256,7 +262,7 @@ defmodule Haj.Events do %EventRegistration{} |> EventRegistration.changeset(attrs) |> Repo.insert() - |> notify_subscribers([:registration, :created]) + |> notify_subscribers({:registration, :created}) end @doc """ @@ -289,9 +295,9 @@ defmodule Haj.Events do {:error, %Ecto.Changeset{}} """ - def delete_event_registration(%EventRegistration{} = event_registration) do + def delete_event_registration(%EventRegistration{id: id} = event_registration) do Repo.delete(event_registration) - |> notify_subscribers([:registration, :deleted]) + |> notify_subscribers({:registration, :deleted}) end @doc """ @@ -322,12 +328,14 @@ defmodule Haj.Events do Repo.one(q) end - def subscribe do - Phoenix.PubSub.subscribe(Haj.PubSub, @topic) + def subscribe(event_id) do + Phoenix.PubSub.subscribe(Haj.PubSub, "event:#{event_id}") end - defp notify_subscribers({:ok, result}, event) do - Phoenix.PubSub.broadcast(Haj.PubSub, @topic, {__MODULE__, event, result}) + defp notify_subscribers({:ok, result}, action) do + topic = "event:#{result.id}" + + Phoenix.PubSub.broadcast(Haj.PubSub, topic, {__MODULE__, action, result}) {:ok, result} end diff --git a/lib/haj_web/components/components.ex b/lib/haj_web/components/components.ex index a07e36e..c0c4864 100644 --- a/lib/haj_web/components/components.ex +++ b/lib/haj_web/components/components.ex @@ -48,4 +48,31 @@ defmodule HajWeb.Components do """ end + + @doc """ + A generic card component with a title, subtitle and inner block. + """ + attr :navigate, :any, required: true + slot :title, required: true + slot :subtitle + slot :inner_block, required: true + + def generic_card(assigns) do + ~H""" + <.link + navigate={@navigate} + class="flex flex-col gap-1 rounded-lg border bg-white px-4 py-4 shadow-sm duration-150 hover:bg-gray-50 sm:gap-1.5" + > +
+ <%= render_slot(@title) %> +
+ +
+ <%= render_slot(@subtitle) %> +
+ + <%= render_slot(@inner_block) %> + + """ + end end diff --git a/lib/haj_web/components/layouts/live.html.heex b/lib/haj_web/components/layouts/live.html.heex index a3bf706..34cc2f0 100644 --- a/lib/haj_web/components/layouts/live.html.heex +++ b/lib/haj_web/components/layouts/live.html.heex @@ -105,8 +105,10 @@
-
- <%= @inner_content %> +
+
+ <%= @inner_content %> +
diff --git a/lib/haj_web/components/live_helpers.ex b/lib/haj_web/components/live_helpers.ex index e2bd70c..91c3f82 100644 --- a/lib/haj_web/components/live_helpers.ex +++ b/lib/haj_web/components/live_helpers.ex @@ -118,69 +118,6 @@ defmodule HajWeb.LiveHelpers do |> JS.dispatch("js:exec", to: "#show-mobile-sidebar", detail: %{call: "focus", args: []}) end - # @doc """ - # Renders a flash, maybe this looks better on top than bottom? - # """ - - # attr :flash, :map - # attr :kind, :atom - - # def flash(%{kind: :error} = assigns) do - # ~H""" - # <%= if live_flash(@flash, @kind) do %> - #
JS.remove_class("fade-in-scale", to: "#flash") - # |> hide("#flash") - # } - # phx-hook="Flash" - # > - #
- # <.icon name={:exclamation_circle} /> - #

- # <%= live_flash(@flash, @kind) %> - #

- # - #
- #
- # <% end %> - # """ - # end - - # def flash(%{kind: :info} = assigns) do - # ~H""" - # <%= if live_flash(@flash, @kind) do %> - #
JS.remove_class("fade-in-scale", to: "#flash") - # |> hide("#flash") - # } - # phx-hook="Flash" - # > - #
- # <.icon name={:exclamation_circle} /> - #

- # <%= live_flash(@flash, @kind) %> - #

- # - #
- #
- # <% end %> - # """ - # end - def full_name(%Accounts.User{} = user), do: "#{user.first_name} #{user.last_name}" def format_date(%struct{} = date) when struct in [NaiveDateTime, DateTime] do @@ -231,4 +168,15 @@ defmodule HajWeb.LiveHelpers do :math.pow((c + 0.055) / 1.055, 2.4) end end + + def swe_month_name(month) do + {"januari", "februari", "mars", "april", "maj", "juni", "juli", "augusti", "september", + "oktober", "november", "december"} + |> elem(month - 1) + end + + def swe_day_name(day) do + {"Måndag", "Tisdag", "Onsdag", "Torsdag", "Fredag", "Lördag", "Söndag"} + |> elem(day - 1) + end end diff --git a/lib/haj_web/live/dashboard_live/index.ex b/lib/haj_web/live/dashboard_live/index.ex index 617df0a..f0ae99d 100644 --- a/lib/haj_web/live/dashboard_live/index.ex +++ b/lib/haj_web/live/dashboard_live/index.ex @@ -34,17 +34,6 @@ defmodule HajWeb.DashboardLive.Index do attr :navigate, :any, required: true slot :inner_block, required: true - defp card(assigns) do - ~H""" - <.link - navigate={@navigate} - class="flex flex-col gap-1 rounded-lg border px-4 py-4 shadow-sm hover:bg-gray-50 sm:gap-1.5" - > - <%= render_slot(@inner_block) %> - - """ - end - defp merch_card(assigns) do ~H""" <.link diff --git a/lib/haj_web/live/dashboard_live/index.html.heex b/lib/haj_web/live/dashboard_live/index.html.heex index 19c8b2b..587a435 100644 --- a/lib/haj_web/live/dashboard_live/index.html.heex +++ b/lib/haj_web/live/dashboard_live/index.html.heex @@ -11,22 +11,20 @@

Dina grupper

- <.card :for={show_group <- @user_groups} navigate={~p"/group/#{show_group}"}> -
+ <.generic_card :for={show_group <- @user_groups} navigate={~p"/group/#{show_group}"}> + <:title> <.icon name={:user_group} solid /> <%= show_group.group.name %> -
-
+ + <:subtitle> <%= length(show_group.group_memberships) %> medlemmar -
-
- Du är <%= Enum.find(show_group.group_memberships, fn %{user_id: user_id} -> - user_id == @current_user.id - end).role %> -
- + + Du är <%= Enum.find(show_group.group_memberships, fn %{user_id: user_id} -> + user_id == @current_user.id + end).role %> +
@@ -39,17 +37,17 @@ <.icon name={:arrow_right} mini class="inline-block h-5 group-hover:fill-burgandy-600" />
- <.card + <.generic_card :for={res <- @responsibilities} navigate={~p"/responsibilities/#{res.responsibility}"} > -
+ <:title> <.icon name={:briefcase} solid /> <%= res.responsibility.name %> -
- + +
diff --git a/lib/haj_web/live/event_live/index.ex b/lib/haj_web/live/event_live/index.ex index 44f3a86..d5790b0 100644 --- a/lib/haj_web/live/event_live/index.ex +++ b/lib/haj_web/live/event_live/index.ex @@ -7,18 +7,7 @@ defmodule HajWeb.EventLive.Index do @impl true def mount(_params, _session, socket) do - Events.subscribe() - initial_count = Presence.list("custom_channel") |> map_size - HajWeb.Endpoint.subscribe("custom_channel") - - Presence.track( - self(), - "custom_channel", - socket.id, - %{} - ) - - {:ok, assign(socket, events: list_events(), online_count: initial_count)} + {:ok, assign(socket, events: list_events())} end @impl true @@ -26,14 +15,6 @@ defmodule HajWeb.EventLive.Index do {:noreply, assign(socket, events: list_events())} end - def handle_info( - %{event: "presence_diff", payload: %{joins: joins, leaves: leaves}}, - %{assigns: %{online_count: count}} = socket - ) do - online_count = count + map_size(joins) - map_size(leaves) - {:noreply, assign(socket, :online_count, online_count)} - end - defp list_events do # Todo: maybe fetch in one query Events.list_events() @@ -76,98 +57,36 @@ defmodule HajWeb.EventLive.Index do """ end - defp month_name(month) do - {"januari", "februari", "mars", "april", "maj", "juni", "juli", "augusti", "september", - "oktober", "november", "december"} - |> elem(month - 1) - end - defp event_card(assigns) do ~H""" <.link navigate={~p"/events/#{@event}"}> -
-
+
+
{"Picture +
-
-
- Priser från 128 kr
<%= @event.name %>
- <%= Calendar.strftime(@event.event_date, "%d %B %H:%M", month_names: &month_name/1) %> + <%= Calendar.strftime(@event.event_date, "%A %d %B %H:%M", + month_names: &swe_month_name/1, + day_of_week_names: &swe_day_name/1 + ) %>
- - <%!-- - -
- -
- Priser från {props.event.price} kr -
-
-
-
{props.event.shortTitle}
-
{props.event.location.title}
-
{`${props.event.startTime - .toTimeString() - .substr(0, 5)}`}
-
-
- {props.event.startTime.getDate()} - - {props.event.startTime.toLocaleString("sv-SE", { - month: "short", - })}{" "} - -
-
-
- --%> - <%!--
-
- -
-

<%= @event.availible %>

-

Available tickets

-
-
-
-
-
-

<%= @event.name %>

-
-
<%= ticket_format_date(@event.event_date) %>
-
<%= ticket_format_time(@event.event_date) %>
-
-
-
<%= @event.description %>
-
-
- -
--%> """ end end diff --git a/lib/haj_web/live/event_live/index.html.heex b/lib/haj_web/live/event_live/index.html.heex index 9a23c67..5a8beda 100644 --- a/lib/haj_web/live/event_live/index.html.heex +++ b/lib/haj_web/live/event_live/index.html.heex @@ -1,7 +1,10 @@ -

Events

+
+
+ Events + Partaj och annat skoj +
-<.online_count online_count={@online_count} /> - -
- <.event_card :for={event <- @events} event={event} /> +
+ <.event_card :for={event <- @events} event={event} /> +
diff --git a/lib/haj_web/live/event_live/show.ex b/lib/haj_web/live/event_live/show.ex index 16f2815..b6e4626 100644 --- a/lib/haj_web/live/event_live/show.ex +++ b/lib/haj_web/live/event_live/show.ex @@ -2,17 +2,76 @@ defmodule HajWeb.EventLive.Show do use HajWeb, :live_view alias Haj.Events - alias Haj.Events.Event + alias Haj.Presence @impl true def mount(%{"id" => id}, _session, socket) do + initial_count = Presence.list("present:event:#{id}") |> map_size + + if connected?(socket) do + Events.subscribe(id) + HajWeb.Endpoint.subscribe("present:event:#{id}") + + Presence.track( + self(), + "present:event:#{id}", + socket.id, + %{} + ) + end + event = Events.get_event!(id) - {:ok, assign(socket, event: event)} + {:ok, + assign(socket, + event: event, + online_count: initial_count, + price: 0 + ) + |> assign_selected_tickets(event.ticket_types)} + end + + def handle_info( + %{event: "presence_diff", payload: %{joins: joins, leaves: leaves}}, + %{assigns: %{online_count: count}} = socket + ) do + online_count = count + map_size(joins) - map_size(leaves) + {:noreply, assign(socket, :online_count, online_count)} end @impl true def handle_info({Events, _, _}, socket) do {:noreply, socket} end + + def handle_event("inc", %{"id" => id}, socket) do + selected = Map.update(socket.assigns.selected, id, 0, &(&1 + 1)) + price = socket.assigns.price + socket.assigns.ticket_types[id].price + + {:noreply, assign(socket, selected: selected, price: price)} + end + + def handle_event("dec", %{"id" => id}, socket) do + selected = Map.update(socket.assigns.selected, id, 0, &(&1 - 1)) + price = socket.assigns.price - socket.assigns.ticket_types[id].price + + {:noreply, assign(socket, selected: selected, price: price)} + end + + defp assign_selected_tickets(socket, ticket_types) do + socket + |> assign_new(:selected, fn -> + Map.new(ticket_types, fn %{id: id} -> {id, 0} end) + end) + |> assign_new(:ticket_types, fn -> + Map.new(ticket_types, fn %{id: id} = tt -> {id, tt} end) + end) + end + + defp toggle_mobile_tickets(js \\ %JS{}) do + js + |> JS.toggle_class("hidden", to: "#tickets") + |> JS.toggle_class("flex", to: "#tickets") + |> JS.toggle_class("hidden", to: "#bottom-bar") + end end diff --git a/lib/haj_web/live/event_live/show.html.heex b/lib/haj_web/live/event_live/show.html.heex index 29a1445..5fedfbc 100644 --- a/lib/haj_web/live/event_live/show.html.heex +++ b/lib/haj_web/live/event_live/show.html.heex @@ -1,9 +1,98 @@ -
-

<%= @event.name %>

-

<%= @event.description %>

- {"Picture +
+ {"Picture
+
+ <%!-- Main content --%> +
+
+
+ <%= Calendar.strftime(@event.event_date, "%A %d %B %H:%M", + month_names: &swe_month_name/1, + day_of_week_names: &swe_day_name/1 + ) %> +
+ +
+ <%= @event.name %> +
+ +
<%= @event.description %>
+
+
+ + <%!-- Right side bar --%> + +
+ +
+ <.button class="flex flex-row items-center gap-2" phx-click={toggle_mobile_tickets()}> + Biljetter + +
+ +<%!-- <.online_count online_count={@online_count} /> --%> diff --git a/lib/haj_web/live/event_single_live/index.ex b/lib/haj_web/live/event_single_live/index.ex deleted file mode 100644 index cdf55d1..0000000 --- a/lib/haj_web/live/event_single_live/index.ex +++ /dev/null @@ -1,15 +0,0 @@ -defmodule HajWeb.EventSingleLive.Index do - use HajWeb, :live_view - - alias Haj.Events - - @impl true - def mount(_params, _session, socket) do - {:ok, socket} - end - - @impl true - def handle_info({Events, _, _}, socket) do - {:noreply, socket} - end -end diff --git a/lib/haj_web/live/event_single_live/index.html.heex b/lib/haj_web/live/event_single_live/index.html.heex deleted file mode 100644 index 9cd17bf..0000000 --- a/lib/haj_web/live/event_single_live/index.html.heex +++ /dev/null @@ -1 +0,0 @@ -

Events

diff --git a/lib/haj_web/live/group_live/index.ex b/lib/haj_web/live/group_live/index.ex index 23a9ea0..3889d09 100644 --- a/lib/haj_web/live/group_live/index.ex +++ b/lib/haj_web/live/group_live/index.ex @@ -10,25 +10,6 @@ defmodule HajWeb.GroupLive.Index do {:ok, assign(socket, page_title: "Grupper", groups: groups)} end - defp group_card(assigns) do - ~H""" - <.link - navigate={~p"/group/#{@show_group.id}"} - class="flex flex-col gap-1 rounded-lg border px-4 py-4 hover:bg-gray-50 sm:gap-1.5" - > -
- <%= @show_group.group.name %> -
-
- <%= @members %> medlemmar -
-
- <%= chefer(@show_group) %> -
- - """ - end - defp chefer(group) do chefer = Enum.filter(group.group_memberships, fn %{role: role} -> role == :chef end) diff --git a/lib/haj_web/live/group_live/index.html.heex b/lib/haj_web/live/group_live/index.html.heex index 4376115..c3750ad 100644 --- a/lib/haj_web/live/group_live/index.html.heex +++ b/lib/haj_web/live/group_live/index.html.heex @@ -5,8 +5,18 @@
- <%= for group <- @groups do %> - <.group_card show_group={group} members={Enum.count(group.group_memberships)} /> - <% end %> + <.generic_card + :for={sg <- @groups} + navigate={~p"/group/#{sg.id}"} + class="flex flex-col gap-1 rounded-lg border px-4 py-4 hover:bg-gray-50 sm:gap-1.5" + > + <:title> + <%= sg.group.name %> + + <:subtitle class="text-gray-500"> + <%= Enum.count(sg.group_memberships) %> medlemmar + + <%= chefer(sg) %> +
diff --git a/lib/haj_web/live/responsibility_live/history.ex b/lib/haj_web/live/responsibility_live/history.ex index 956c0e9..cca6f83 100644 --- a/lib/haj_web/live/responsibility_live/history.ex +++ b/lib/haj_web/live/responsibility_live/history.ex @@ -20,18 +20,4 @@ defmodule HajWeb.ResponsibilityLive.History do prev_responsibilities: prev )} end - - attr :navigate, :any, required: true - slot :inner_block, required: true - - defp card(assigns) do - ~H""" - <.link - navigate={@navigate} - class="flex flex-col gap-1 rounded-lg border px-4 py-4 shadow-sm hover:bg-gray-50 sm:gap-1.5" - > - <%= render_slot(@inner_block) %> - - """ - end end diff --git a/lib/haj_web/live/responsibility_live/history.html.heex b/lib/haj_web/live/responsibility_live/history.html.heex index 18339a7..d37fb67 100644 --- a/lib/haj_web/live/responsibility_live/history.html.heex +++ b/lib/haj_web/live/responsibility_live/history.html.heex @@ -9,21 +9,16 @@

Nuvarande ansvar:

- <.card + <.generic_card :for={res <- @current_responsibilities} navigate={~p"/responsibilities/#{res.responsibility}"} > -
+ <:title> <.icon name={:briefcase} solid /> - - <%= res.responsibility.name %> - -
- -
- Ansvarig sedan <%= Calendar.strftime(res.inserted_at, "%Y-%m-%d") %> -
- + <%= res.responsibility.name %> + + Ansvarig sedan <%= Calendar.strftime(res.inserted_at, "%Y-%m-%d") %> +
@@ -31,20 +26,15 @@

Alla tidigare ansvar:

- <.card + <.generic_card :for={res <- @prev_responsibilities} navigate={~p"/responsibilities/#{res.responsibility}"} > -
+ <:title> <.icon name={:briefcase} solid /> - - <%= res.responsibility.name %> - -
- -
- Du var ansvarig år <%= res.show.year.year %> -
- + <%= res.responsibility.name %> + + Du var ansvarig år <%= res.show.year.year %> +
diff --git a/lib/haj_web/live/responsibility_live/index.html.heex b/lib/haj_web/live/responsibility_live/index.html.heex index beb30de..7a265e3 100644 --- a/lib/haj_web/live/responsibility_live/index.html.heex +++ b/lib/haj_web/live/responsibility_live/index.html.heex @@ -4,32 +4,28 @@
- <%= for responsibility <- @responsibilities do %> - <.link - navigate={~p"/responsibilities/#{responsibility}"} - class="flex flex-col gap-1 rounded-lg border px-4 py-4 hover:bg-gray-50 sm:gap-1.5" - > -
- <%= responsibility.name %> + <.generic_card + :for={responsibility <- @responsibilities} + navigate={~p"/responsibilities/#{responsibility}"} + > + <:title><%= responsibility.name %> +
+
+ + <%= full_name(user) %>
-
- <%= if responsibility.responsible_users == [] do %> - Ingen har detta ansvar i år. - <% else %> - <%= for user <- responsibility.responsible_users do %> -
- - <%= full_name(user) %> -
- <% end %> - <% end %> +
+ Ingen har detta ansvar i år.
- - <% end %> +
+
<.modal diff --git a/lib/haj_web/live/settings_live/event/form_component.ex b/lib/haj_web/live/settings_live/event/form_component.ex index b6fa398..85f3a43 100644 --- a/lib/haj_web/live/settings_live/event/form_component.ex +++ b/lib/haj_web/live/settings_live/event/form_component.ex @@ -28,7 +28,7 @@ defmodule HajWeb.SettingsLive.Event.FormComponent do phx-change="validate" > <.input field={@form[:name]} type="text" label="Namn" /> - <.input field={@form[:description]} type="text" label="Beskrivning" /> + <.input field={@form[:description]} type="textarea" label="Beskrivning" /> <.input field={@form[:ticket_limit]} type="number" label="Biljettgräns" /> <.input field={@form[:event_date]} type="datetime-local" label="Datum" /> <.input field={@form[:purchase_deadline]} type="datetime-local" label="Köpdeadline" /> diff --git a/lib/haj_web/live/settings_live/index.ex b/lib/haj_web/live/settings_live/index.ex index 42a6ad2..2009560 100644 --- a/lib/haj_web/live/settings_live/index.ex +++ b/lib/haj_web/live/settings_live/index.ex @@ -50,17 +50,11 @@ defmodule HajWeb.SettingsLive.Index do defp setting_card(assigns) do ~H""" - <.link - navigate={@navigate} - class="flex flex-col gap-1 rounded-lg border px-4 py-4 hover:bg-gray-50 sm:gap-1.5" - > -
- <%= @name %> -
-
- <%= render_slot(@inner_block) %> -
- + <.generic_card navigate={@navigate}> + <:title><%= @name %> + + <%= render_slot(@inner_block) %> + """ end end diff --git a/lib/haj_web/live/show_live/index.ex b/lib/haj_web/live/show_live/index.ex index 1cb78ba..2e0c608 100644 --- a/lib/haj_web/live/show_live/index.ex +++ b/lib/haj_web/live/show_live/index.ex @@ -8,23 +8,4 @@ defmodule HajWeb.ShowLive.Index do {:ok, assign(socket, page_title: "Alla spex", shows: shows)} end - - defp show_card(assigns) do - ~H""" - <.link - navigate={~p"/shows/#{@show.id}"} - class="flex flex-col gap-1 rounded-lg border px-4 py-4 hover:bg-gray-50 sm:gap-1.5" - > -
- <%= @show.title %> -
-
- eller <%= @show.or_title %> -
-
- METAspexet <%= @show.year.year %> -
- - """ - end end diff --git a/lib/haj_web/live/show_live/index.html.heex b/lib/haj_web/live/show_live/index.html.heex index 2b38b1a..f260323 100644 --- a/lib/haj_web/live/show_live/index.html.heex +++ b/lib/haj_web/live/show_live/index.html.heex @@ -5,8 +5,10 @@
- <%= for show <- @shows do %> - <.show_card show={show} /> - <% end %> + <.generic_card :for={show <- @shows} navigate={~p"/shows/#{show.id}"}> + <:title><%= show.title %> + <:subtitle :if={show.or_title}>eller <%= show.or_title %> + METAspexet <%= show.year.year %> +
diff --git a/priv/repo/migrations/20221122195103_create_events.exs b/priv/repo/migrations/20221122195103_create_events.exs index d448de1..da3e203 100644 --- a/priv/repo/migrations/20221122195103_create_events.exs +++ b/priv/repo/migrations/20221122195103_create_events.exs @@ -5,7 +5,7 @@ defmodule Haj.Repo.Migrations.CreateEvents do create table(:events) do add :name, :string add :description, :text - add :image, :string + add :image, :text add :ticket_limit, :integer add :event_date, :utc_datetime add :purchase_deadline, :utc_datetime From 55c2188ed2f7e6755f36c736b8fa7a086547f0f4 Mon Sep 17 00:00:00 2001 From: Adrian Salamon Date: Sat, 10 Feb 2024 15:49:12 -0800 Subject: [PATCH 18/56] added ticketless events and formatting --- .formatter.exs | 2 +- lib/haj/events/event.ex | 12 +++++++++++- lib/haj/events/event_registration.ex | 3 ++- lib/haj_web/live/event_live/show.ex | 2 ++ lib/haj_web/live/event_live/show.html.heex | 11 ++++++++--- .../live/settings_live/event/form_component.ex | 5 ++--- .../20221122200036_create_ticket_types.exs | 10 +++++----- ...20221122200318_create_event_registrations.exs | 8 ++++---- ...1215151001_create_responsibility_comments.exs | 14 +++++++------- ...add_html_to_responsibilities_and_comments.exs | 4 ++-- .../20230219172426_add_search_index_to_users.exs | 16 ++++++++-------- .../20230404185516_add_group_permission.exs | 2 +- .../migrations/20230608125112_create_songs.exs | 16 ++++++++-------- .../20230702141835_add_group_descriptions.exs | 2 +- .../20230912101144_add_application_status.exs | 2 +- .../20240210231704_add_ticketless_events.exs | 15 +++++++++++++++ 16 files changed, 78 insertions(+), 46 deletions(-) create mode 100644 priv/repo/migrations/20240210231704_add_ticketless_events.exs diff --git a/.formatter.exs b/.formatter.exs index 15b3ed4..0b83fb2 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -1,5 +1,5 @@ [ - import_deps: [:ecto, :phoenix, :let_me], + import_deps: [:ecto, :ecto_sql, :phoenix, :let_me], plugins: [TailwindFormatter.MultiFormatter], inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs,heex}"], subdirectories: ["priv/*/migrations"], diff --git a/lib/haj/events/event.ex b/lib/haj/events/event.ex index fd7d38e..8b7bec0 100644 --- a/lib/haj/events/event.ex +++ b/lib/haj/events/event.ex @@ -9,8 +9,10 @@ defmodule Haj.Events.Event do field :name, :string field :purchase_deadline, :utc_datetime field :ticket_limit, :integer + field :has_tickets, :boolean, default: true has_many :ticket_types, Haj.Events.TicketType, on_replace: :delete + has_many :event_registrations, Haj.Events.EventRegistration, on_replace: :delete timestamps() end @@ -28,7 +30,15 @@ defmodule Haj.Events.Event do defp default_changeset(event, attrs) do event - |> cast(attrs, [:name, :description, :image, :ticket_limit, :event_date, :purchase_deadline]) + |> cast(attrs, [ + :name, + :description, + :image, + :ticket_limit, + :event_date, + :purchase_deadline, + :has_tickets + ]) |> validate_required([ :name, :description, diff --git a/lib/haj/events/event_registration.ex b/lib/haj/events/event_registration.ex index 771e83a..63d563f 100644 --- a/lib/haj/events/event_registration.ex +++ b/lib/haj/events/event_registration.ex @@ -5,6 +5,7 @@ defmodule Haj.Events.EventRegistration do schema "event_registrations" do belongs_to :ticket_type, Haj.Events.TicketType belongs_to :user, Haj.Accounts.User + belongs_to :event, Haj.Events.Event timestamps() end @@ -12,7 +13,7 @@ defmodule Haj.Events.EventRegistration do @doc false def changeset(event_registration, attrs) do event_registration - |> cast(attrs, [:ticket_type_id, :user_id]) + |> cast(attrs, [:ticket_type_id, :user_id, :event_id]) |> validate_required([:ticket_type_id, :user_id]) end end diff --git a/lib/haj_web/live/event_live/show.ex b/lib/haj_web/live/event_live/show.ex index b6e4626..1032273 100644 --- a/lib/haj_web/live/event_live/show.ex +++ b/lib/haj_web/live/event_live/show.ex @@ -44,6 +44,7 @@ defmodule HajWeb.EventLive.Show do {:noreply, socket} end + @impl true def handle_event("inc", %{"id" => id}, socket) do selected = Map.update(socket.assigns.selected, id, 0, &(&1 + 1)) price = socket.assigns.price + socket.assigns.ticket_types[id].price @@ -51,6 +52,7 @@ defmodule HajWeb.EventLive.Show do {:noreply, assign(socket, selected: selected, price: price)} end + @impl true def handle_event("dec", %{"id" => id}, socket) do selected = Map.update(socket.assigns.selected, id, 0, &(&1 - 1)) price = socket.assigns.price - socket.assigns.ticket_types[id].price diff --git a/lib/haj_web/live/event_live/show.html.heex b/lib/haj_web/live/event_live/show.html.heex index 5fedfbc..427cf4a 100644 --- a/lib/haj_web/live/event_live/show.html.heex +++ b/lib/haj_web/live/event_live/show.html.heex @@ -44,7 +44,11 @@
-
+
<%= ticket.name %>
@@ -78,8 +82,9 @@ Totalt: <%= @price %> kr
- <.button class="mt-auto"> - Fortsätt + <.button class="mt-auto" phx-click="register"> + Köp biljetter + Anmälan
diff --git a/lib/haj_web/live/settings_live/event/form_component.ex b/lib/haj_web/live/settings_live/event/form_component.ex index 85f3a43..8621b20 100644 --- a/lib/haj_web/live/settings_live/event/form_component.ex +++ b/lib/haj_web/live/settings_live/event/form_component.ex @@ -33,8 +33,9 @@ defmodule HajWeb.SettingsLive.Event.FormComponent do <.input field={@form[:event_date]} type="datetime-local" label="Datum" /> <.input field={@form[:purchase_deadline]} type="datetime-local" label="Köpdeadline" /> <.input field={@form[:image]} type="text" label="Bild" /> + <.input field={@form[:has_tickets]} type="checkbox" label="Har biljetter" /> -
+

Biljettyper

<.inputs_for :let={f_nested} field={@form[:ticket_types]}> @@ -104,8 +105,6 @@ defmodule HajWeb.SettingsLive.Event.FormComponent do |> Events.change_event(event_params, with_tickets: true) |> Map.put(:action, :validate) - IO.inspect(assign_form(socket, changeset)) - {:noreply, assign_form(socket, changeset)} end diff --git a/priv/repo/migrations/20221122200036_create_ticket_types.exs b/priv/repo/migrations/20221122200036_create_ticket_types.exs index a5d94fb..746a15a 100644 --- a/priv/repo/migrations/20221122200036_create_ticket_types.exs +++ b/priv/repo/migrations/20221122200036_create_ticket_types.exs @@ -3,14 +3,14 @@ defmodule Haj.Repo.Migrations.CreateTicketTypes do def change do create table(:ticket_types) do - add(:price, :integer) - add(:name, :string) - add(:description, :text) - add(:event_id, references(:events, on_delete: :delete_all)) + add :price, :integer + add :name, :string + add :description, :text + add :event_id, references(:events, on_delete: :delete_all) timestamps() end - create(index(:ticket_types, [:event_id])) + create index(:ticket_types, [:event_id]) end end diff --git a/priv/repo/migrations/20221122200318_create_event_registrations.exs b/priv/repo/migrations/20221122200318_create_event_registrations.exs index d250844..0970668 100644 --- a/priv/repo/migrations/20221122200318_create_event_registrations.exs +++ b/priv/repo/migrations/20221122200318_create_event_registrations.exs @@ -3,13 +3,13 @@ defmodule Haj.Repo.Migrations.CreateEventRegistrations do def change do create table(:event_registrations) do - add(:ticket_type_id, references(:ticket_types, on_delete: :delete_all)) - add(:user_id, references(:users, on_delete: :delete_all)) + add :ticket_type_id, references(:ticket_types, on_delete: :delete_all) + add :user_id, references(:users, on_delete: :delete_all) timestamps() end - create(index(:event_registrations, [:ticket_type_id])) - create(index(:event_registrations, [:user_id])) + create index(:event_registrations, [:ticket_type_id]) + create index(:event_registrations, [:user_id]) end end diff --git a/priv/repo/migrations/20221215151001_create_responsibility_comments.exs b/priv/repo/migrations/20221215151001_create_responsibility_comments.exs index 421a68e..acda58a 100644 --- a/priv/repo/migrations/20221215151001_create_responsibility_comments.exs +++ b/priv/repo/migrations/20221215151001_create_responsibility_comments.exs @@ -3,16 +3,16 @@ defmodule Haj.Repo.Migrations.CreateResponsibilityComments do def change do create table(:responsibility_comments) do - add(:text, :text) - add(:show_id, references(:shows, on_delete: :nothing)) - add(:user_id, references(:users, on_delete: :nothing)) - add(:responsibility_id, references(:responsibilities, on_delete: :nothing)) + add :text, :text + add :show_id, references(:shows, on_delete: :nothing) + add :user_id, references(:users, on_delete: :nothing) + add :responsibility_id, references(:responsibilities, on_delete: :nothing) timestamps() end - create(index(:responsibility_comments, [:show_id])) - create(index(:responsibility_comments, [:user_id])) - create(index(:responsibility_comments, [:responsibility_id])) + create index(:responsibility_comments, [:show_id]) + create index(:responsibility_comments, [:user_id]) + create index(:responsibility_comments, [:responsibility_id]) end end diff --git a/priv/repo/migrations/20230213212415_add_html_to_responsibilities_and_comments.exs b/priv/repo/migrations/20230213212415_add_html_to_responsibilities_and_comments.exs index 9c0a3d7..42ec812 100644 --- a/priv/repo/migrations/20230213212415_add_html_to_responsibilities_and_comments.exs +++ b/priv/repo/migrations/20230213212415_add_html_to_responsibilities_and_comments.exs @@ -3,11 +3,11 @@ defmodule Haj.Repo.Migrations.AddHtmlToResponsibilitiesAndComments do def change do alter table(:responsibilities) do - add(:description_html, :text) + add :description_html, :text end alter table(:responsibility_comments) do - add(:text_html, :text) + add :text_html, :text end end end diff --git a/priv/repo/migrations/20230219172426_add_search_index_to_users.exs b/priv/repo/migrations/20230219172426_add_search_index_to_users.exs index 31f8b70..e62081b 100644 --- a/priv/repo/migrations/20230219172426_add_search_index_to_users.exs +++ b/priv/repo/migrations/20230219172426_add_search_index_to_users.exs @@ -2,24 +2,24 @@ defmodule Haj.Repo.Migrations.AddSearchIndexToUsers do use Ecto.Migration def up do - execute(""" + execute """ ALTER TABLE users ADD COLUMN full_name TEXT GENERATED ALWAYS AS (first_name || ' ' || last_name) STORED; - """) + """ - execute(""" + execute """ CREATE INDEX users_name_gin_trgm_idx ON users USING GIN (full_name gin_trgm_ops); - """) + """ end def down do - execute(""" + execute """ DROP INDEX users_name_gin_trgm_idx; - """) + """ - execute(""" + execute """ ALTER TABLE users DROP COLUMN full_name; - """) + """ end end diff --git a/priv/repo/migrations/20230404185516_add_group_permission.exs b/priv/repo/migrations/20230404185516_add_group_permission.exs index 4deffa9..11ecdb5 100644 --- a/priv/repo/migrations/20230404185516_add_group_permission.exs +++ b/priv/repo/migrations/20230404185516_add_group_permission.exs @@ -3,7 +3,7 @@ defmodule Haj.Repo.Migrations.AddGroupPermission do def change do alter table(:groups) do - add(:permission_group, :string) + add :permission_group, :string end end end diff --git a/priv/repo/migrations/20230608125112_create_songs.exs b/priv/repo/migrations/20230608125112_create_songs.exs index c376cd1..346aa96 100644 --- a/priv/repo/migrations/20230608125112_create_songs.exs +++ b/priv/repo/migrations/20230608125112_create_songs.exs @@ -3,17 +3,17 @@ defmodule Haj.Repo.Migrations.CreateSongs do def change do create table(:songs) do - add(:original_name, :string) - add(:name, :string) - add(:number, :string) - add(:text, :text) - add(:show_id, references(:shows, on_delete: :nothing)) - add(:file, :string) - add(:line_timings, {:array, :integer}) + add :original_name, :string + add :name, :string + add :number, :string + add :text, :text + add :show_id, references(:shows, on_delete: :nothing) + add :file, :string + add :line_timings, {:array, :integer} timestamps() end - create(index(:songs, [:show_id])) + create index(:songs, [:show_id]) end end diff --git a/priv/repo/migrations/20230702141835_add_group_descriptions.exs b/priv/repo/migrations/20230702141835_add_group_descriptions.exs index 986352d..89a6af4 100644 --- a/priv/repo/migrations/20230702141835_add_group_descriptions.exs +++ b/priv/repo/migrations/20230702141835_add_group_descriptions.exs @@ -3,7 +3,7 @@ defmodule Haj.Repo.Migrations.AddGroupDescriptions do def change do alter table(:groups) do - add(:description, :text) + add :description, :text end end end diff --git a/priv/repo/migrations/20230912101144_add_application_status.exs b/priv/repo/migrations/20230912101144_add_application_status.exs index 24102fd..74f45f0 100644 --- a/priv/repo/migrations/20230912101144_add_application_status.exs +++ b/priv/repo/migrations/20230912101144_add_application_status.exs @@ -3,7 +3,7 @@ defmodule Haj.Repo.Migrations.AddApplicationStatus do def change do alter table(:applications) do - add(:status, :string, default: "pending") + add :status, :string, default: "pending" end end end diff --git a/priv/repo/migrations/20240210231704_add_ticketless_events.exs b/priv/repo/migrations/20240210231704_add_ticketless_events.exs new file mode 100644 index 0000000..6e5dc0d --- /dev/null +++ b/priv/repo/migrations/20240210231704_add_ticketless_events.exs @@ -0,0 +1,15 @@ +defmodule Haj.Repo.Migrations.AddTicketlessEvents do + use Ecto.Migration + + def change do + alter table(:events) do + add :has_tickets, :boolean, default: true + end + + alter table(:event_registrations) do + add :event_id, references(:events, on_delete: :delete_all) + end + + create index(:event_registrations, [:event_id]) + end +end From b32d3b712116efccc48e4932f065a8f36efdde1b Mon Sep 17 00:00:00 2001 From: Adrian Salamon Date: Tue, 7 Mar 2023 00:52:57 +0100 Subject: [PATCH 19/56] checkpoint: added basic forms --- lib/haj/forms.ex | 296 ++++++++++++++++++ lib/haj/forms/form.ex | 21 ++ lib/haj/forms/question.ex | 23 ++ lib/haj/forms/response.ex | 21 ++ .../live/settings_live/form/form_component.ex | 91 ++++++ lib/haj_web/live/settings_live/form/index.ex | 47 +++ .../live/settings_live/form/index.html.heex | 47 +++ lib/haj_web/live/settings_live/form/show.ex | 21 ++ .../live/settings_live/form/show.html.heex | 32 ++ lib/haj_web/live/settings_live/index.ex | 3 + lib/haj_web/router.ex | 7 + .../20230306225544_create_forms.exs | 12 + .../20230306231612_create_form_questions.exs | 17 + .../20230306232121_create_form_responses.exs | 18 ++ priv/repo/seeds.exs | 33 ++ test/haj/forms_test.exs | 191 +++++++++++ test/haj_web/live/form_live_test.exs | 113 +++++++ test/support/fixtures/forms_fixtures.ex | 52 +++ 18 files changed, 1045 insertions(+) create mode 100644 lib/haj/forms.ex create mode 100644 lib/haj/forms/form.ex create mode 100644 lib/haj/forms/question.ex create mode 100644 lib/haj/forms/response.ex create mode 100644 lib/haj_web/live/settings_live/form/form_component.ex create mode 100644 lib/haj_web/live/settings_live/form/index.ex create mode 100644 lib/haj_web/live/settings_live/form/index.html.heex create mode 100644 lib/haj_web/live/settings_live/form/show.ex create mode 100644 lib/haj_web/live/settings_live/form/show.html.heex create mode 100644 priv/repo/migrations/20230306225544_create_forms.exs create mode 100644 priv/repo/migrations/20230306231612_create_form_questions.exs create mode 100644 priv/repo/migrations/20230306232121_create_form_responses.exs create mode 100644 test/haj/forms_test.exs create mode 100644 test/haj_web/live/form_live_test.exs create mode 100644 test/support/fixtures/forms_fixtures.ex diff --git a/lib/haj/forms.ex b/lib/haj/forms.ex new file mode 100644 index 0000000..54c3a4e --- /dev/null +++ b/lib/haj/forms.ex @@ -0,0 +1,296 @@ +defmodule Haj.Forms do + @moduledoc """ + The Forms context. + """ + + import Ecto.Query, warn: false + alias Haj.Repo + + alias Haj.Forms.Form + + @doc """ + Returns the list of forms. + + ## Examples + + iex> list_forms() + [%Form{}, ...] + + """ + def list_forms do + Repo.all(Form) + end + + @doc """ + Gets a single form. + + Raises `Ecto.NoResultsError` if the Form does not exist. + + ## Examples + + iex> get_form!(123) + %Form{} + + iex> get_form!(456) + ** (Ecto.NoResultsError) + + """ + def get_form!(id), do: Repo.get!(Form, id) + + @doc """ + Creates a form. + + ## Examples + + iex> create_form(%{field: value}) + {:ok, %Form{}} + + iex> create_form(%{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def create_form(attrs \\ %{}) do + %Form{} + |> Form.changeset(attrs) + |> Repo.insert() + end + + @doc """ + Updates a form. + + ## Examples + + iex> update_form(form, %{field: new_value}) + {:ok, %Form{}} + + iex> update_form(form, %{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def update_form(%Form{} = form, attrs) do + form + |> Form.changeset(attrs) + |> Repo.update() + end + + @doc """ + Deletes a form. + + ## Examples + + iex> delete_form(form) + {:ok, %Form{}} + + iex> delete_form(form) + {:error, %Ecto.Changeset{}} + + """ + def delete_form(%Form{} = form) do + Repo.delete(form) + end + + @doc """ + Returns an `%Ecto.Changeset{}` for tracking form changes. + + ## Examples + + iex> change_form(form) + %Ecto.Changeset{data: %Form{}} + + """ + def change_form(%Form{} = form, attrs \\ %{}) do + Form.changeset(form, attrs) + end + + alias Haj.Forms.Question + + @doc """ + Returns the list of form_questions. + + ## Examples + + iex> list_form_questions() + [%Question{}, ...] + + """ + def list_form_questions do + Repo.all(Question) + end + + @doc """ + Gets a single form_question. + + Raises `Ecto.NoResultsError` if the Form question does not exist. + + ## Examples + + iex> get_form_question!(123) + %Question{} + + iex> get_form_question!(456) + ** (Ecto.NoResultsError) + + """ + def get_form_question!(id), do: Repo.get!(Question, id) + + @doc """ + Creates a form_question. + + ## Examples + + iex> create_form_question(%{field: value}) + {:ok, %Question{}} + + iex> create_form_question(%{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def create_form_question(attrs \\ %{}) do + %Question{} + |> Question.changeset(attrs) + |> Repo.insert() + end + + @doc """ + Updates a form_question. + + ## Examples + + iex> update_form_question(form_question, %{field: new_value}) + {:ok, %Question{}} + + iex> update_form_question(form_question, %{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def update_form_question(%Question{} = form_question, attrs) do + form_question + |> Question.changeset(attrs) + |> Repo.update() + end + + @doc """ + Deletes a form_question. + + ## Examples + + iex> delete_form_question(form_question) + {:ok, %Question{}} + + iex> delete_form_question(form_question) + {:error, %Ecto.Changeset{}} + + """ + def delete_form_question(%Question{} = form_question) do + Repo.delete(form_question) + end + + @doc """ + Returns an `%Ecto.Changeset{}` for tracking form_question changes. + + ## Examples + + iex> change_form_question(form_question) + %Ecto.Changeset{data: %Question{}} + + """ + def change_form_question(%Question{} = form_question, attrs \\ %{}) do + Question.changeset(form_question, attrs) + end + + alias Haj.Forms.Response + + @doc """ + Returns the list of form_responses. + + ## Examples + + iex> list_form_responses() + [%Response{}, ...] + + """ + def list_form_responses do + Repo.all(Response) + end + + @doc """ + Gets a single response. + + Raises `Ecto.NoResultsError` if the Response does not exist. + + ## Examples + + iex> get_response!(123) + %Response{} + + iex> get_response!(456) + ** (Ecto.NoResultsError) + + """ + def get_response!(id), do: Repo.get!(Response, id) + + @doc """ + Creates a response. + + ## Examples + + iex> create_response(%{field: value}) + {:ok, %Response{}} + + iex> create_response(%{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def create_response(attrs \\ %{}) do + %Response{} + |> Response.changeset(attrs) + |> Repo.insert() + end + + @doc """ + Updates a response. + + ## Examples + + iex> update_response(response, %{field: new_value}) + {:ok, %Response{}} + + iex> update_response(response, %{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def update_response(%Response{} = response, attrs) do + response + |> Response.changeset(attrs) + |> Repo.update() + end + + @doc """ + Deletes a response. + + ## Examples + + iex> delete_response(response) + {:ok, %Response{}} + + iex> delete_response(response) + {:error, %Ecto.Changeset{}} + + """ + def delete_response(%Response{} = response) do + Repo.delete(response) + end + + @doc """ + Returns an `%Ecto.Changeset{}` for tracking response changes. + + ## Examples + + iex> change_response(response) + %Ecto.Changeset{data: %Response{}} + + """ + def change_response(%Response{} = response, attrs \\ %{}) do + Response.changeset(response, attrs) + end +end diff --git a/lib/haj/forms/form.ex b/lib/haj/forms/form.ex new file mode 100644 index 0000000..921571a --- /dev/null +++ b/lib/haj/forms/form.ex @@ -0,0 +1,21 @@ +defmodule Haj.Forms.Form do + use Ecto.Schema + import Ecto.Changeset + + schema "forms" do + field :description, :string + field :name, :string + + has_many :questions, Haj.Forms.Question + has_many :responses, Haj.Forms.Response + + timestamps() + end + + @doc false + def changeset(form, attrs) do + form + |> cast(attrs, [:name, :description]) + |> validate_required([:name, :description]) + end +end diff --git a/lib/haj/forms/question.ex b/lib/haj/forms/question.ex new file mode 100644 index 0000000..cfe2253 --- /dev/null +++ b/lib/haj/forms/question.ex @@ -0,0 +1,23 @@ +defmodule Haj.Forms.Question do + use Ecto.Schema + import Ecto.Changeset + + schema "form_questions" do + field :description, :string + field :name, :string + field :required, :boolean, default: false + field :type, Ecto.Enum, values: [:text, :select, :multi_select] + + belongs_to :form, Haj.Forms.Form + has_many :responses, Haj.Forms.Response + + timestamps() + end + + @doc false + def changeset(form_question, attrs) do + form_question + |> cast(attrs, [:name, :type, :description, :required, :form_id]) + |> validate_required([:name, :type, :description, :required]) + end +end diff --git a/lib/haj/forms/response.ex b/lib/haj/forms/response.ex new file mode 100644 index 0000000..fde6c45 --- /dev/null +++ b/lib/haj/forms/response.ex @@ -0,0 +1,21 @@ +defmodule Haj.Forms.Response do + use Ecto.Schema + import Ecto.Changeset + + schema "form_responses" do + field :value, :string + + belongs_to :form, Haj.Forms.Form + belongs_to :question, Haj.Forms.Question + belongs_to :user, Haj.Accounts.User + + timestamps() + end + + @doc false + def changeset(response, attrs) do + response + |> cast(attrs, [:value, :form_id, :user_id, :question_id]) + |> validate_required([:value]) + end +end diff --git a/lib/haj_web/live/settings_live/form/form_component.ex b/lib/haj_web/live/settings_live/form/form_component.ex new file mode 100644 index 0000000..6dc7860 --- /dev/null +++ b/lib/haj_web/live/settings_live/form/form_component.ex @@ -0,0 +1,91 @@ +defmodule HajWeb.SettingsLive.Form.FormComponent do + use HajWeb, :live_component + + alias Haj.Forms + + @impl true + def render(assigns) do + ~H""" +
+ <.header> + <%= @title %> + <:subtitle>Use this form to manage form records in your database. + + + <.simple_form + for={@client_form} + id="form-form" + phx-target={@myself} + phx-change="validate" + phx-submit="save" + > + <.input field={@client_form[:name]} type="text" label="Name" /> + <.input field={@client_form[:description]} type="text" label="Description" /> + <:actions> + <.button phx-disable-with="Saving...">Save Form + + +
+ """ + end + + @impl true + def update(%{form: form} = assigns, socket) do + changeset = Forms.change_form(form) + + {:ok, + socket + |> assign(assigns) + |> assign_form(changeset)} + end + + @impl true + def handle_event("validate", %{"form" => form_params}, socket) do + changeset = + socket.assigns.form + |> Forms.change_form(form_params) + |> Map.put(:action, :validate) + + {:noreply, assign_form(socket, changeset)} + end + + def handle_event("save", %{"form" => form_params}, socket) do + save_form(socket, socket.assigns.action, form_params) + end + + defp save_form(socket, :edit, form_params) do + case Forms.update_form(socket.assigns.form, form_params) do + {:ok, form} -> + notify_parent({:saved, form}) + + {:noreply, + socket + |> put_flash(:info, "Form updated successfully") + |> push_patch(to: socket.assigns.patch)} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, assign_form(socket, changeset)} + end + end + + defp save_form(socket, :new, form_params) do + case Forms.create_form(form_params) do + {:ok, form} -> + notify_parent({:saved, form}) + + {:noreply, + socket + |> put_flash(:info, "Form created successfully") + |> push_patch(to: socket.assigns.patch)} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, assign_form(socket, changeset)} + end + end + + defp assign_form(socket, %Ecto.Changeset{} = changeset) do + assign(socket, :client_form, to_form(changeset)) + end + + defp notify_parent(msg), do: send(self(), {__MODULE__, msg}) +end diff --git a/lib/haj_web/live/settings_live/form/index.ex b/lib/haj_web/live/settings_live/form/index.ex new file mode 100644 index 0000000..806a42a --- /dev/null +++ b/lib/haj_web/live/settings_live/form/index.ex @@ -0,0 +1,47 @@ +defmodule HajWeb.SettingsLive.Form.Index do + use HajWeb, :live_view + + alias Haj.Forms + alias Haj.Forms.Form + + @impl true + def mount(_params, _session, socket) do + {:ok, stream(socket, :forms, Forms.list_forms())} + end + + @impl true + def handle_params(params, _url, socket) do + {:noreply, apply_action(socket, socket.assigns.live_action, params)} + end + + defp apply_action(socket, :edit, %{"id" => id}) do + socket + |> assign(:page_title, "Edit Form") + |> assign(:form, Forms.get_form!(id)) + end + + defp apply_action(socket, :new, _params) do + socket + |> assign(:page_title, "New Form") + |> assign(:form, %Form{}) + end + + defp apply_action(socket, :index, _params) do + socket + |> assign(:page_title, "Listing Forms") + |> assign(:form, nil) + end + + @impl true + def handle_info({HajWeb.SettingsLive.Form.FormComponent, {:saved, form}}, socket) do + {:noreply, stream_insert(socket, :forms, form)} + end + + @impl true + def handle_event("delete", %{"id" => id}, socket) do + form = Forms.get_form!(id) + {:ok, _} = Forms.delete_form(form) + + {:noreply, stream_delete(socket, :forms, form)} + end +end diff --git a/lib/haj_web/live/settings_live/form/index.html.heex b/lib/haj_web/live/settings_live/form/index.html.heex new file mode 100644 index 0000000..c2cab01 --- /dev/null +++ b/lib/haj_web/live/settings_live/form/index.html.heex @@ -0,0 +1,47 @@ +<.header> + Listing Forms + <:actions> + <.link patch={~p"/live/settings/forms/new"}> + <.button>New Form + + + + +<.table + id="forms" + rows={@streams.forms} + row_click={fn {_id, form} -> JS.navigate(~p"/live/settings/forms/#{form}") end} +> + <:col :let={{_id, form}} label="Name"><%= form.name %> + <:col :let={{_id, form}} label="Description"><%= form.description %> + <:action :let={{_id, form}}> +
+ <.link navigate={~p"/live/settings/forms/#{form}"}>Show +
+ <.link patch={~p"/live/settings/forms/#{form}/edit"}>Edit + + <:action :let={{id, form}}> + <.link + phx-click={JS.push("delete", value: %{id: form.id}) |> hide("##{id}")} + data-confirm="Are you sure?" + > + Delete + + + + +<.modal + :if={@live_action in [:new, :edit]} + id="form-modal" + show + on_cancel={JS.navigate(~p"/live/settings/forms")} +> + <.live_component + module={HajWeb.SettingsLive.Form.FormComponent} + id={@form.id || :new} + title={@page_title} + action={@live_action} + form={@form} + patch={~p"/live/settings/forms"} + /> + diff --git a/lib/haj_web/live/settings_live/form/show.ex b/lib/haj_web/live/settings_live/form/show.ex new file mode 100644 index 0000000..8a5ceae --- /dev/null +++ b/lib/haj_web/live/settings_live/form/show.ex @@ -0,0 +1,21 @@ +defmodule HajWeb.SettingsLive.Form.Show do + use HajWeb, :live_view + + alias Haj.Forms + + @impl true + def mount(_params, _session, socket) do + {:ok, socket} + end + + @impl true + def handle_params(%{"id" => id}, _, socket) do + {:noreply, + socket + |> assign(:page_title, page_title(socket.assigns.live_action)) + |> assign(:form, Forms.get_form!(id))} + end + + defp page_title(:show), do: "Show Form" + defp page_title(:edit), do: "Edit Form" +end diff --git a/lib/haj_web/live/settings_live/form/show.html.heex b/lib/haj_web/live/settings_live/form/show.html.heex new file mode 100644 index 0000000..256ae67 --- /dev/null +++ b/lib/haj_web/live/settings_live/form/show.html.heex @@ -0,0 +1,32 @@ +<.header> + Form <%= @form.id %> + <:subtitle>This is a form record from your database. + <:actions> + <.link patch={~p"/live/settings/forms/#{@form}/show/edit"} phx-click={JS.push_focus()}> + <.button>Edit form + + + + +<.list> + <:item title="Name"><%= @form.name %> + <:item title="Description"><%= @form.description %> + + +<.back navigate={~p"/live/settings/forms"}>Back to forms + +<.modal + :if={@live_action == :edit} + id="form-modal" + show + on_cancel={JS.patch(~p"/live/settings/forms/#{@form}")} +> + <.live_component + module={HajWeb.SettingsLive.Form.FormComponent} + id={@form.id} + title={@page_title} + action={@live_action} + form={@form} + patch={~p"/live/settings/forms/#{@form}"} + /> + diff --git a/lib/haj_web/live/settings_live/index.ex b/lib/haj_web/live/settings_live/index.ex index 2009560..1f733c6 100644 --- a/lib/haj_web/live/settings_live/index.ex +++ b/lib/haj_web/live/settings_live/index.ex @@ -30,6 +30,9 @@ defmodule HajWeb.SettingsLive.Index do <.setting_card name="Användare" navigate={~p"/settings/users"}> Redigera användare och användaruppgifter + <.setting_card name="Formulär" navigate={~p"/live/settings/forms"}> + Redigera och lägg till formulär + <.setting_card name="Events" navigate={~p"/settings/events"}> Redigera events diff --git a/lib/haj_web/router.ex b/lib/haj_web/router.ex index 0671a0e..d175152 100644 --- a/lib/haj_web/router.ex +++ b/lib/haj_web/router.ex @@ -169,6 +169,13 @@ defmodule HajWeb.Router do live "/songs/:id", SettingsLive.Song.Show, :show live "/songs/:id/show/edit", SettingsLive.Song.Show, :edit + + live "/forms", SettingsLive.Form.Index, :index + live "/forms/new", SettingsLive.Form.Index, :new + live "/forms/:id/edit", SettingsLive.Form.Index, :edit + + live "/forms/:id", SettingsLive.Form.Show, :show + live "/forms/:id/show/edit", SettingsLive.Form.Show, :edit end end end diff --git a/priv/repo/migrations/20230306225544_create_forms.exs b/priv/repo/migrations/20230306225544_create_forms.exs new file mode 100644 index 0000000..c2f7c46 --- /dev/null +++ b/priv/repo/migrations/20230306225544_create_forms.exs @@ -0,0 +1,12 @@ +defmodule Haj.Repo.Migrations.CreateForms do + use Ecto.Migration + + def change do + create table(:forms) do + add(:name, :string) + add(:description, :string) + + timestamps() + end + end +end diff --git a/priv/repo/migrations/20230306231612_create_form_questions.exs b/priv/repo/migrations/20230306231612_create_form_questions.exs new file mode 100644 index 0000000..a05153f --- /dev/null +++ b/priv/repo/migrations/20230306231612_create_form_questions.exs @@ -0,0 +1,17 @@ +defmodule Haj.Repo.Migrations.CreateQuestions do + use Ecto.Migration + + def change do + create table(:form_questions) do + add(:name, :string) + add(:type, :string) + add(:description, :text) + add(:required, :boolean, default: false, null: false) + add(:form_id, references(:forms, on_delete: :nothing)) + + timestamps() + end + + create(index(:form_questions, [:form_id])) + end +end diff --git a/priv/repo/migrations/20230306232121_create_form_responses.exs b/priv/repo/migrations/20230306232121_create_form_responses.exs new file mode 100644 index 0000000..99a9fce --- /dev/null +++ b/priv/repo/migrations/20230306232121_create_form_responses.exs @@ -0,0 +1,18 @@ +defmodule Haj.Repo.Migrations.CreateFormResponses do + use Ecto.Migration + + def change do + create table(:form_responses) do + add :value, :string + add :user_id, references(:users, on_delete: :nothing) + add :form_id, references(:forms, on_delete: :nothing) + add :question_id, references(:form_questions, on_delete: :nothing) + + timestamps() + end + + create index(:form_responses, [:user_id]) + create index(:form_responses, [:form_id]) + create index(:form_responses, [:question_id]) + end +end diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index 727c4f0..2eaf18d 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -138,3 +138,36 @@ Enum.each(applicants, fn user -> end) end) end) + +# Creates a form and one response + +alias Haj.Forms.Form +alias Haj.Forms.Question +alias Haj.Forms.Response + +form = Repo.insert!(%Form{name: "Formulär för TB2", description: "Fyll i!"}) + +answers = ["Adrian", "Hundar", "Ja"] + +questions = + [ + %Question{form_id: form.id, name: "namn", required: true, type: :text}, + %Question{form_id: form.id, name: "favoritdjur", required: false, type: :text}, + %Question{ + form_id: form.id, + name: "GDPR?", + description: "Jag godkänner att sälja min själ till Metaspexet", + required: false, + type: :select + } + ] + |> Enum.map(fn q -> Repo.insert!(q) end) + |> Enum.with_index() + |> Enum.each(fn {q, index} -> + Repo.insert!(%Response{ + user_id: hd(users).id, + form_id: form.id, + question_id: q.id, + value: Enum.at(answers, index) + }) + end) diff --git a/test/haj/forms_test.exs b/test/haj/forms_test.exs new file mode 100644 index 0000000..4dae3b6 --- /dev/null +++ b/test/haj/forms_test.exs @@ -0,0 +1,191 @@ +defmodule Haj.FormsTest do + use Haj.DataCase + + alias Haj.Forms + + describe "forms" do + alias Haj.Forms.Form + + import Haj.FormsFixtures + + @invalid_attrs %{description: nil, name: nil} + + test "list_forms/0 returns all forms" do + form = form_fixture() + assert Forms.list_forms() == [form] + end + + test "get_form!/1 returns the form with given id" do + form = form_fixture() + assert Forms.get_form!(form.id) == form + end + + test "create_form/1 with valid data creates a form" do + valid_attrs = %{description: "some description", name: "some name"} + + assert {:ok, %Form{} = form} = Forms.create_form(valid_attrs) + assert form.description == "some description" + assert form.name == "some name" + end + + test "create_form/1 with invalid data returns error changeset" do + assert {:error, %Ecto.Changeset{}} = Forms.create_form(@invalid_attrs) + end + + test "update_form/2 with valid data updates the form" do + form = form_fixture() + update_attrs = %{description: "some updated description", name: "some updated name"} + + assert {:ok, %Form{} = form} = Forms.update_form(form, update_attrs) + assert form.description == "some updated description" + assert form.name == "some updated name" + end + + test "update_form/2 with invalid data returns error changeset" do + form = form_fixture() + assert {:error, %Ecto.Changeset{}} = Forms.update_form(form, @invalid_attrs) + assert form == Forms.get_form!(form.id) + end + + test "delete_form/1 deletes the form" do + form = form_fixture() + assert {:ok, %Form{}} = Forms.delete_form(form) + assert_raise Ecto.NoResultsError, fn -> Forms.get_form!(form.id) end + end + + test "change_form/1 returns a form changeset" do + form = form_fixture() + assert %Ecto.Changeset{} = Forms.change_form(form) + end + end + + describe "form_questions" do + alias Haj.Forms.Question + + import Haj.FormsFixtures + + @invalid_attrs %{description: nil, name: nil, required: nil, type: nil} + + test "list_form_questions/0 returns all form_questions" do + form_question = form_question_fixture() + assert Forms.list_form_questions() == [form_question] + end + + test "get_form_question!/1 returns the form_question with given id" do + form_question = form_question_fixture() + assert Forms.get_form_question!(form_question.id) == form_question + end + + test "create_form_question/1 with valid data creates a form_question" do + valid_attrs = %{ + description: "some description", + name: "some name", + required: true, + type: :text + } + + assert {:ok, %Question{} = form_question} = Forms.create_form_question(valid_attrs) + assert form_question.description == "some description" + assert form_question.name == "some name" + assert form_question.required == true + assert form_question.type == :text + end + + test "create_form_question/1 with invalid data returns error changeset" do + assert {:error, %Ecto.Changeset{}} = Forms.create_form_question(@invalid_attrs) + end + + test "update_form_question/2 with valid data updates the form_question" do + form_question = form_question_fixture() + + update_attrs = %{ + description: "some updated description", + name: "some updated name", + required: false, + type: :select + } + + assert {:ok, %Question{} = form_question} = + Forms.update_form_question(form_question, update_attrs) + + assert form_question.description == "some updated description" + assert form_question.name == "some updated name" + assert form_question.required == false + assert form_question.type == :select + end + + test "update_form_question/2 with invalid data returns error changeset" do + form_question = form_question_fixture() + + assert {:error, %Ecto.Changeset{}} = + Forms.update_form_question(form_question, @invalid_attrs) + + assert form_question == Forms.get_form_question!(form_question.id) + end + + test "delete_form_question/1 deletes the form_question" do + form_question = form_question_fixture() + assert {:ok, %Question{}} = Forms.delete_form_question(form_question) + assert_raise Ecto.NoResultsError, fn -> Forms.get_form_question!(form_question.id) end + end + + test "change_form_question/1 returns a form_question changeset" do + form_question = form_question_fixture() + assert %Ecto.Changeset{} = Forms.change_form_question(form_question) + end + end + + describe "form_responses" do + alias Haj.Forms.Response + + import Haj.FormsFixtures + + @invalid_attrs %{value: nil} + + test "list_form_responses/0 returns all form_responses" do + response = response_fixture() + assert Forms.list_form_responses() == [response] + end + + test "get_response!/1 returns the response with given id" do + response = response_fixture() + assert Forms.get_response!(response.id) == response + end + + test "create_response/1 with valid data creates a response" do + valid_attrs = %{value: "some value"} + + assert {:ok, %Response{} = response} = Forms.create_response(valid_attrs) + assert response.value == "some value" + end + + test "create_response/1 with invalid data returns error changeset" do + assert {:error, %Ecto.Changeset{}} = Forms.create_response(@invalid_attrs) + end + + test "update_response/2 with valid data updates the response" do + response = response_fixture() + update_attrs = %{value: "some updated value"} + + assert {:ok, %Response{} = response} = Forms.update_response(response, update_attrs) + assert response.value == "some updated value" + end + + test "update_response/2 with invalid data returns error changeset" do + response = response_fixture() + assert {:error, %Ecto.Changeset{}} = Forms.update_response(response, @invalid_attrs) + assert response == Forms.get_response!(response.id) + end + + test "delete_response/1 deletes the response" do + response = response_fixture() + assert {:ok, %Response{}} = Forms.delete_response(response) + assert_raise Ecto.NoResultsError, fn -> Forms.get_response!(response.id) end + end + + test "change_response/1 returns a response changeset" do + response = response_fixture() + assert %Ecto.Changeset{} = Forms.change_response(response) + end + end +end diff --git a/test/haj_web/live/form_live_test.exs b/test/haj_web/live/form_live_test.exs new file mode 100644 index 0000000..740baa9 --- /dev/null +++ b/test/haj_web/live/form_live_test.exs @@ -0,0 +1,113 @@ +defmodule HajWeb.FormLiveTest do +use HajWeb.ConnCase + +import Phoenix.LiveViewTest +import Haj.FormsFixtures + +@create_attrs %{description: "some description", name: "some name"} +@update_attrs %{description: "some updated description", name: "some updated name"} +@invalid_attrs %{description: nil, name: nil} + +defp create_form(_) do +form = form_fixture() +%{form: form} +end + +describe "Index" do +setup [:create_form] + +test "lists all forms", %{conn: conn, form: form} do +{:ok, _index_live, html} = live(conn, ~p"/live/settings/forms") + +assert html =~ "Listing Forms" +assert html =~ form.description +end + +test "saves new form", %{conn: conn} do +{:ok, index_live, _html} = live(conn, ~p"/live/settings/forms") + +assert index_live |> element("a", "New Form") |> render_click() =~ +"New Form" + +assert_patch(index_live, ~p"/live/settings/forms/new") + +assert index_live +|> form("#form-form", form: @invalid_attrs) +|> render_change() =~ "can't be blank" + +assert index_live +|> form("#form-form", form: @create_attrs) +|> render_submit() + +assert_patch(index_live, ~p"/live/settings/forms") + +html = render(index_live) +assert html =~ "Form created successfully" +assert html =~ "some description" +end + +test "updates form in listing", %{conn: conn, form: form} do +{:ok, index_live, _html} = live(conn, ~p"/live/settings/forms") + +assert index_live |> element("#forms-#{form.id} a", "Edit") |> render_click() =~ +"Edit Form" + +assert_patch(index_live, ~p"/live/settings/forms/#{form}/edit") + +assert index_live +|> form("#form-form", form: @invalid_attrs) +|> render_change() =~ "can't be blank" + +assert index_live +|> form("#form-form", form: @update_attrs) +|> render_submit() + +assert_patch(index_live, ~p"/live/settings/forms") + +html = render(index_live) +assert html =~ "Form updated successfully" +assert html =~ "some updated description" +end + +test "deletes form in listing", %{conn: conn, form: form} do +{:ok, index_live, _html} = live(conn, ~p"/live/settings/forms") + +assert index_live |> element("#forms-#{form.id} a", "Delete") |> render_click() +refute has_element?(index_live, "#forms-#{form.id}") +end +end + +describe "Show" do +setup [:create_form] + +test "displays form", %{conn: conn, form: form} do +{:ok, _show_live, html} = live(conn, ~p"/live/settings/forms/#{form}") + +assert html =~ "Show Form" +assert html =~ form.description +end + +test "updates form within modal", %{conn: conn, form: form} do +{:ok, show_live, _html} = live(conn, ~p"/live/settings/forms/#{form}") + +assert show_live |> element("a", "Edit") |> render_click() =~ +"Edit Form" + +assert_patch(show_live, ~p"/live/settings/forms/#{form}/show/edit") + +assert show_live +|> form("#form-form", form: @invalid_attrs) +|> render_change() =~ "can't be blank" + +assert show_live +|> form("#form-form", form: @update_attrs) +|> render_submit() + +assert_patch(show_live, ~p"/live/settings/forms/#{form}") + +html = render(show_live) +assert html =~ "Form updated successfully" +assert html =~ "some updated description" +end +end +end diff --git a/test/support/fixtures/forms_fixtures.ex b/test/support/fixtures/forms_fixtures.ex new file mode 100644 index 0000000..ce73860 --- /dev/null +++ b/test/support/fixtures/forms_fixtures.ex @@ -0,0 +1,52 @@ +defmodule Haj.FormsFixtures do + @moduledoc """ + This module defines test helpers for creating + entities via the `Haj.Forms` context. + """ + + @doc """ + Generate a form. + """ + def form_fixture(attrs \\ %{}) do + {:ok, form} = + attrs + |> Enum.into(%{ + description: "some description", + name: "some name" + }) + |> Haj.Forms.create_form() + + form + end + + @doc """ + Generate a form_question. + """ + def form_question_fixture(attrs \\ %{}) do + {:ok, form_question} = + attrs + |> Enum.into(%{ + description: "some description", + name: "some name", + required: true, + type: :text + }) + |> Haj.Forms.create_form_question() + + form_question + end + + @doc """ + Generate a response. + """ + def response_fixture(attrs \\ %{}) do + {:ok, response} = + attrs + |> Enum.into(%{ + value: "some value" + }) + |> Haj.Forms.create_response() + + response + end +end From 2c3bf107a88d0e2b99f98e5fed16cf561748f4d5 Mon Sep 17 00:00:00 2001 From: Adrian Salamon Date: Sat, 10 Feb 2024 15:58:01 -0800 Subject: [PATCH 20/56] formatting --- priv/repo/migrations/20230306225544_create_forms.exs | 4 ++-- .../20230306231612_create_form_questions.exs | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/priv/repo/migrations/20230306225544_create_forms.exs b/priv/repo/migrations/20230306225544_create_forms.exs index c2f7c46..adf7670 100644 --- a/priv/repo/migrations/20230306225544_create_forms.exs +++ b/priv/repo/migrations/20230306225544_create_forms.exs @@ -3,8 +3,8 @@ defmodule Haj.Repo.Migrations.CreateForms do def change do create table(:forms) do - add(:name, :string) - add(:description, :string) + add :name, :string + add :description, :string timestamps() end diff --git a/priv/repo/migrations/20230306231612_create_form_questions.exs b/priv/repo/migrations/20230306231612_create_form_questions.exs index a05153f..aa37baa 100644 --- a/priv/repo/migrations/20230306231612_create_form_questions.exs +++ b/priv/repo/migrations/20230306231612_create_form_questions.exs @@ -3,15 +3,15 @@ defmodule Haj.Repo.Migrations.CreateQuestions do def change do create table(:form_questions) do - add(:name, :string) - add(:type, :string) - add(:description, :text) - add(:required, :boolean, default: false, null: false) - add(:form_id, references(:forms, on_delete: :nothing)) + add :name, :string + add :type, :string + add :description, :text + add :required, :boolean, default: false, null: false + add :form_id, references(:forms, on_delete: :nothing) timestamps() end - create(index(:form_questions, [:form_id])) + create index(:form_questions, [:form_id]) end end From 0ef2bfae5c3f15056fa160e7a4d24baba1a913bd Mon Sep 17 00:00:00 2001 From: Adrian Salamon Date: Tue, 7 Mar 2023 13:22:58 +0100 Subject: [PATCH 21/56] checkpoint: it is now possible to submit forms --- lib/haj/forms.ex | 208 +++++++++++++++++- lib/haj/forms/question.ex | 1 + lib/haj/forms/question_response.ex | 21 ++ lib/haj/forms/response.ex | 9 +- lib/haj_web/live/form_live/index.ex | 88 ++++++++ lib/haj_web/live/form_live/index.html.heex | 9 + lib/haj_web/router.ex | 2 + .../20230306231612_create_form_questions.exs | 1 + .../20230306232121_create_form_responses.exs | 3 - ...7085255_create_form_question_responses.exs | 17 ++ priv/repo/seeds.exs | 3 +- test/haj/forms_test.exs | 56 +++++ test/support/fixtures/forms_fixtures.ex | 15 ++ 13 files changed, 423 insertions(+), 10 deletions(-) create mode 100644 lib/haj/forms/question_response.ex create mode 100644 lib/haj_web/live/form_live/index.ex create mode 100644 lib/haj_web/live/form_live/index.html.heex create mode 100644 priv/repo/migrations/20230307085255_create_form_question_responses.exs diff --git a/lib/haj/forms.ex b/lib/haj/forms.ex index 54c3a4e..cf33fa6 100644 --- a/lib/haj/forms.ex +++ b/lib/haj/forms.ex @@ -4,7 +4,9 @@ defmodule Haj.Forms do """ import Ecto.Query, warn: false + alias Ecto.Changeset alias Haj.Repo + alias Ecto.Multi alias Haj.Forms.Form @@ -35,7 +37,14 @@ defmodule Haj.Forms do ** (Ecto.NoResultsError) """ - def get_form!(id), do: Repo.get!(Form, id) + def get_form!(id) do + Repo.one!( + from f in Form, + where: f.id == ^id, + join: q in assoc(f, :questions), + preload: [questions: q] + ) + end @doc """ Creates a form. @@ -293,4 +302,201 @@ defmodule Haj.Forms do def change_response(%Response{} = response, attrs \\ %{}) do Response.changeset(response, attrs) end + + @doc """ + Retruns a changeset for a form submission + """ + def get_form_changeset!(form_id, attrs) do + query = + from f in Form, + where: f.id == ^form_id, + join: q in assoc(f, :questions), + preload: [questions: q] + + form = Repo.one!(query) + + data = %{} + + types = + Enum.reduce(form.questions, %{}, fn q, acc -> + case q.type do + :multi_select -> + Map.put(acc, String.to_atom("#{q.id}"), {:array, :string}) + + _ -> + Map.put(acc, String.to_atom("#{q.id}"), :string) + end + end) + + {data, types} + |> Ecto.Changeset.cast(attrs, Map.keys(types)) + |> validate_required(form.questions) + |> validate_options(form.questions) + end + + defp validate_required(changeset, questions) do + required = + Enum.filter(questions, fn %{required: req} -> req end) + |> Enum.map(fn %{id: id} -> String.to_atom("#{id}") end) + + Ecto.Changeset.validate_required(changeset, required) + end + + defp validate_options(changeset, questions) do + Enum.reduce(questions, changeset, fn q, acc -> + field = String.to_atom("#{q.id}") + + case q.type do + :select -> + acc + |> Ecto.Changeset.validate_change(field, fn field, value -> + case value in q.options do + true -> [] + false -> [{field, "Value must be an availible option"}] + end + end) + + :multi_select -> + acc + |> Ecto.Changeset.validate_change(field, fn field, values -> + case Enum.all?(values, &Enum.member?(q.options, &1)) do + true -> [] + false -> [{field, "All values must be availible options"}] + end + end) + + _ -> + acc + end + end) + end + + alias Haj.Forms.QuestionResponse + + @doc """ + Returns the list of form_question_responses. + + ## Examples + + iex> list_form_question_responses() + [%QuestionResponse{}, ...] + + """ + def list_form_question_responses do + Repo.all(QuestionResponse) + end + + @doc """ + Gets a single question_response. + + Raises `Ecto.NoResultsError` if the Question response does not exist. + + ## Examples + + iex> get_question_response!(123) + %QuestionResponse{} + + iex> get_question_response!(456) + ** (Ecto.NoResultsError) + + """ + def get_question_response!(id), do: Repo.get!(QuestionResponse, id) + + @doc """ + Creates a question_response. + + ## Examples + + iex> create_question_response(%{field: value}) + {:ok, %QuestionResponse{}} + + iex> create_question_response(%{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def create_question_response(attrs \\ %{}) do + %QuestionResponse{} + |> QuestionResponse.changeset(attrs) + |> Repo.insert() + end + + @doc """ + Updates a question_response. + + ## Examples + + iex> update_question_response(question_response, %{field: new_value}) + {:ok, %QuestionResponse{}} + + iex> update_question_response(question_response, %{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def update_question_response(%QuestionResponse{} = question_response, attrs) do + question_response + |> QuestionResponse.changeset(attrs) + |> Repo.update() + end + + @doc """ + Deletes a question_response. + + ## Examples + + iex> delete_question_response(question_response) + {:ok, %QuestionResponse{}} + + iex> delete_question_response(question_response) + {:error, %Ecto.Changeset{}} + + """ + def delete_question_response(%QuestionResponse{} = question_response) do + Repo.delete(question_response) + end + + @doc """ + Returns an `%Ecto.Changeset{}` for tracking question_response changes. + + ## Examples + + iex> change_question_response(question_response) + %Ecto.Changeset{data: %QuestionResponse{}} + + """ + def change_question_response(%QuestionResponse{} = question_response, attrs \\ %{}) do + QuestionResponse.changeset(question_response, attrs) + end + + def submit_form(form_id, user_id, attrs) do + changeset = get_form_changeset!(form_id, attrs) + + case changeset |> Ecto.Changeset.apply_action(:create) do + {:ok, data} -> + question_responses = + Enum.map(data, fn {key, val} -> + id = Atom.to_string(key) |> String.to_integer() + + case val do + ans when is_list(ans) -> %QuestionResponse{question_id: id, multi_answer: ans} + ans -> %QuestionResponse{question_id: id, answer: ans} + end + end) + + multi = + Multi.new() |> Multi.insert(:response, %Response{user_id: user_id, form_id: form_id}) + + {_n, multi} = + Enum.reduce(question_responses, {0, multi}, fn qr, {n, m} -> + {n + 1, + Multi.insert(m, n, fn %{response: %{id: id}} -> + %QuestionResponse{qr | response_id: id} + end)} + end) + + Repo.transaction(multi) + + {:error, changeset} -> + {:error, changeset} + end + end end diff --git a/lib/haj/forms/question.ex b/lib/haj/forms/question.ex index cfe2253..d500602 100644 --- a/lib/haj/forms/question.ex +++ b/lib/haj/forms/question.ex @@ -7,6 +7,7 @@ defmodule Haj.Forms.Question do field :name, :string field :required, :boolean, default: false field :type, Ecto.Enum, values: [:text, :select, :multi_select] + field :options, {:array, :string} belongs_to :form, Haj.Forms.Form has_many :responses, Haj.Forms.Response diff --git a/lib/haj/forms/question_response.ex b/lib/haj/forms/question_response.ex new file mode 100644 index 0000000..1e0a780 --- /dev/null +++ b/lib/haj/forms/question_response.ex @@ -0,0 +1,21 @@ +defmodule Haj.Forms.QuestionResponse do + use Ecto.Schema + import Ecto.Changeset + + schema "form_question_responses" do + field :answer, :string + field :multi_answer, {:array, :string} + + belongs_to :response, Haj.Forms.Response + belongs_to :question, Haj.Forms.Question + + timestamps() + end + + @doc false + def changeset(question_response, attrs) do + question_response + |> cast(attrs, [:answer, :multi_answer, :response_id, :question_id]) + |> validate_required([]) + end +end diff --git a/lib/haj/forms/response.ex b/lib/haj/forms/response.ex index fde6c45..61adc0a 100644 --- a/lib/haj/forms/response.ex +++ b/lib/haj/forms/response.ex @@ -3,19 +3,18 @@ defmodule Haj.Forms.Response do import Ecto.Changeset schema "form_responses" do - field :value, :string - belongs_to :form, Haj.Forms.Form - belongs_to :question, Haj.Forms.Question belongs_to :user, Haj.Accounts.User + has_many :question_responses, Haj.Forms.QuestionResponse + timestamps() end @doc false def changeset(response, attrs) do response - |> cast(attrs, [:value, :form_id, :user_id, :question_id]) - |> validate_required([:value]) + |> cast(attrs, [:form_id, :user_id, :question_id]) + |> validate_required([]) end end diff --git a/lib/haj_web/live/form_live/index.ex b/lib/haj_web/live/form_live/index.ex new file mode 100644 index 0000000..1b016e5 --- /dev/null +++ b/lib/haj_web/live/form_live/index.ex @@ -0,0 +1,88 @@ +defmodule HajWeb.FormLive.Index do + alias Haj.Forms + use HajWeb, :live_view + + def mount(%{"id" => id}, _session, socket) do + changeset = Forms.get_form_changeset!(id, %{}) + form = Forms.get_form!(id) + {:ok, assign(socket, form: form) |> assign_form(changeset)} + end + + def handle_event("validate", %{"form_response" => response}, socket) do + # We need to flatten all mult-responses to a list + response = + Enum.reduce(response, response, fn {q, a}, acc -> + case a do + a when is_map(a) -> + list = Map.filter(a, fn {_, sel} -> sel == "true" end) |> Map.keys() + Map.put(acc, q, list) + + _ -> + acc + end + end) + + changeset = + Forms.get_form_changeset!(socket.assigns.form.id, response) |> Map.put(:action, :validate) + + {:noreply, assign_form(socket, changeset)} + end + + def handle_event("save", %{"form_response" => response}, socket) do + # We need to flatten all mult-responses to a list + response = + Enum.reduce(response, response, fn {q, a}, acc -> + case a do + a when is_map(a) -> + list = Map.filter(a, fn {_, sel} -> sel == "true" end) |> Map.keys() + Map.put(acc, q, list) + + _ -> + acc + end + end) + + case Forms.submit_form(socket.assigns.form.id, socket.assigns.current_user.id, response) do + {:ok, _} -> + {:noreply, put_flash(socket, :info, "Skickade svar")} + + {:error, changeset} -> + {:noreply, assign_form(socket, changeset)} + end + end + + attr :question, :any, required: true + attr :field, :any, required: true + + defp form_input(%{question: %{type: :select}} = assigns) do + ~H""" + <.input field={@field} type="select" options={@question.options} label={@question.name} /> + """ + end + + defp form_input(%{question: %{type: :multi_select}} = assigns) do + ~H""" + +
+ <.input + name={"#{@field.name}[#{option}]"} + type="checkbox" + value={option in Ecto.Changeset.get_field(assigns.field.form.source, assigns.field.field, [])} + label={option} + /> +
+ """ + end + + defp form_input(assigns) do + ~H""" + <.input field={@field} type="text" label={@question.name} /> + """ + end + + defp assign_form(socket, %Ecto.Changeset{} = changeset) do + assign(socket, :response_form, to_form(changeset, as: :form_response)) + end +end diff --git a/lib/haj_web/live/form_live/index.html.heex b/lib/haj_web/live/form_live/index.html.heex new file mode 100644 index 0000000..08fa408 --- /dev/null +++ b/lib/haj_web/live/form_live/index.html.heex @@ -0,0 +1,9 @@ +<.form for={@response_form} phx-submit="save" phx-change="validate"> + <.form_input + :for={question <- @form.questions} + question={question} + field={@response_form[String.to_atom("#{question.id}")]} + /> + + <.button phx-disable-with="Sparar...">Spara + diff --git a/lib/haj_web/router.ex b/lib/haj_web/router.ex index d175152..2e26943 100644 --- a/lib/haj_web/router.ex +++ b/lib/haj_web/router.ex @@ -103,6 +103,8 @@ defmodule HajWeb.Router do ## Events live "/events", EventLive.Index, :index live "/events/:id", EventLive.Show, :index + + live "/forms/:id", FormLive.Index, :index end # Admin only! diff --git a/priv/repo/migrations/20230306231612_create_form_questions.exs b/priv/repo/migrations/20230306231612_create_form_questions.exs index aa37baa..cc2ee39 100644 --- a/priv/repo/migrations/20230306231612_create_form_questions.exs +++ b/priv/repo/migrations/20230306231612_create_form_questions.exs @@ -8,6 +8,7 @@ defmodule Haj.Repo.Migrations.CreateQuestions do add :description, :text add :required, :boolean, default: false, null: false add :form_id, references(:forms, on_delete: :nothing) + add :options, {:array, :string} timestamps() end diff --git a/priv/repo/migrations/20230306232121_create_form_responses.exs b/priv/repo/migrations/20230306232121_create_form_responses.exs index 99a9fce..d370474 100644 --- a/priv/repo/migrations/20230306232121_create_form_responses.exs +++ b/priv/repo/migrations/20230306232121_create_form_responses.exs @@ -3,16 +3,13 @@ defmodule Haj.Repo.Migrations.CreateFormResponses do def change do create table(:form_responses) do - add :value, :string add :user_id, references(:users, on_delete: :nothing) add :form_id, references(:forms, on_delete: :nothing) - add :question_id, references(:form_questions, on_delete: :nothing) timestamps() end create index(:form_responses, [:user_id]) create index(:form_responses, [:form_id]) - create index(:form_responses, [:question_id]) end end diff --git a/priv/repo/migrations/20230307085255_create_form_question_responses.exs b/priv/repo/migrations/20230307085255_create_form_question_responses.exs new file mode 100644 index 0000000..017ebc5 --- /dev/null +++ b/priv/repo/migrations/20230307085255_create_form_question_responses.exs @@ -0,0 +1,17 @@ +defmodule Haj.Repo.Migrations.CreateFormQuestionResponses do + use Ecto.Migration + + def change do + create table(:form_question_responses) do + add :answer, :text + add :multi_answer, {:array, :string} + add :response_id, references(:form_responses, on_delete: :nothing) + add :question_id, references(:form_questions, on_delete: :nothing) + + timestamps() + end + + create index(:form_question_responses, [:response_id]) + create index(:form_question_responses, [:question_id]) + end +end diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index 2eaf18d..42cd22f 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -158,7 +158,8 @@ questions = name: "GDPR?", description: "Jag godkänner att sälja min själ till Metaspexet", required: false, - type: :select + type: :select, + options: ["Ja", "Nej"] } ] |> Enum.map(fn q -> Repo.insert!(q) end) diff --git a/test/haj/forms_test.exs b/test/haj/forms_test.exs index 4dae3b6..a705ec1 100644 --- a/test/haj/forms_test.exs +++ b/test/haj/forms_test.exs @@ -188,4 +188,60 @@ defmodule Haj.FormsTest do assert %Ecto.Changeset{} = Forms.change_response(response) end end + + describe "form_question_responses" do + alias Haj.Forms.QuestionResponse + + import Haj.FormsFixtures + + @invalid_attrs %{answer: nil, multi_answer: nil} + + test "list_form_question_responses/0 returns all form_question_responses" do + question_response = question_response_fixture() + assert Forms.list_form_question_responses() == [question_response] + end + + test "get_question_response!/1 returns the question_response with given id" do + question_response = question_response_fixture() + assert Forms.get_question_response!(question_response.id) == question_response + end + + test "create_question_response/1 with valid data creates a question_response" do + valid_attrs = %{answer: "some answer", multi_answer: ["option1", "option2"]} + + assert {:ok, %QuestionResponse{} = question_response} = Forms.create_question_response(valid_attrs) + assert question_response.answer == "some answer" + assert question_response.multi_answer == ["option1", "option2"] + end + + test "create_question_response/1 with invalid data returns error changeset" do + assert {:error, %Ecto.Changeset{}} = Forms.create_question_response(@invalid_attrs) + end + + test "update_question_response/2 with valid data updates the question_response" do + question_response = question_response_fixture() + update_attrs = %{answer: "some updated answer", multi_answer: ["option1"]} + + assert {:ok, %QuestionResponse{} = question_response} = Forms.update_question_response(question_response, update_attrs) + assert question_response.answer == "some updated answer" + assert question_response.multi_answer == ["option1"] + end + + test "update_question_response/2 with invalid data returns error changeset" do + question_response = question_response_fixture() + assert {:error, %Ecto.Changeset{}} = Forms.update_question_response(question_response, @invalid_attrs) + assert question_response == Forms.get_question_response!(question_response.id) + end + + test "delete_question_response/1 deletes the question_response" do + question_response = question_response_fixture() + assert {:ok, %QuestionResponse{}} = Forms.delete_question_response(question_response) + assert_raise Ecto.NoResultsError, fn -> Forms.get_question_response!(question_response.id) end + end + + test "change_question_response/1 returns a question_response changeset" do + question_response = question_response_fixture() + assert %Ecto.Changeset{} = Forms.change_question_response(question_response) + end + end end diff --git a/test/support/fixtures/forms_fixtures.ex b/test/support/fixtures/forms_fixtures.ex index ce73860..7d18ab4 100644 --- a/test/support/fixtures/forms_fixtures.ex +++ b/test/support/fixtures/forms_fixtures.ex @@ -49,4 +49,19 @@ defmodule Haj.FormsFixtures do response end + + @doc """ + Generate a question_response. + """ + def question_response_fixture(attrs \\ %{}) do + {:ok, question_response} = + attrs + |> Enum.into(%{ + answer: "some answer", + multi_answer: ["option1", "option2"] + }) + |> Haj.Forms.create_question_response() + + question_response + end end From bcf999daddb3cee2c3ec3c74426a4bb7b9951119 Mon Sep 17 00:00:00 2001 From: Adrian Salamon Date: Sat, 10 Feb 2024 18:39:12 -0800 Subject: [PATCH 22/56] added functioning form creation --- lib/haj/forms.ex | 2 +- lib/haj/forms/form.ex | 5 +- lib/haj/forms/question.ex | 6 +- lib/haj_web/components/core_components.ex | 2 +- lib/haj_web/live/event_live/index.ex | 21 --- .../live/settings_live/event/index.html.heex | 4 +- .../live/settings_live/form/form_component.ex | 157 ++++++++++++++++- lib/haj_web/live/settings_live/form/index.ex | 6 +- .../live/settings_live/form/index.html.heex | 27 ++- lib/haj_web/live/settings_live/form/show.ex | 21 --- .../live/settings_live/form/show.html.heex | 32 ---- lib/haj_web/live/settings_live/index.ex | 2 +- test/haj_web/live/form_live_test.exs | 158 +++++++++--------- 13 files changed, 253 insertions(+), 190 deletions(-) delete mode 100644 lib/haj_web/live/settings_live/form/show.ex delete mode 100644 lib/haj_web/live/settings_live/form/show.html.heex diff --git a/lib/haj/forms.ex b/lib/haj/forms.ex index cf33fa6..75d3f7f 100644 --- a/lib/haj/forms.ex +++ b/lib/haj/forms.ex @@ -41,7 +41,7 @@ defmodule Haj.Forms do Repo.one!( from f in Form, where: f.id == ^id, - join: q in assoc(f, :questions), + left_join: q in assoc(f, :questions), preload: [questions: q] ) end diff --git a/lib/haj/forms/form.ex b/lib/haj/forms/form.ex index 921571a..730b62e 100644 --- a/lib/haj/forms/form.ex +++ b/lib/haj/forms/form.ex @@ -6,8 +6,8 @@ defmodule Haj.Forms.Form do field :description, :string field :name, :string - has_many :questions, Haj.Forms.Question - has_many :responses, Haj.Forms.Response + has_many :questions, Haj.Forms.Question, on_replace: :delete + has_many :responses, Haj.Forms.Response, on_replace: :delete timestamps() end @@ -17,5 +17,6 @@ defmodule Haj.Forms.Form do form |> cast(attrs, [:name, :description]) |> validate_required([:name, :description]) + |> cast_assoc(:questions, sort_param: :questions_sort, drop_param: :questions_drop) end end diff --git a/lib/haj/forms/question.ex b/lib/haj/forms/question.ex index d500602..8dd220d 100644 --- a/lib/haj/forms/question.ex +++ b/lib/haj/forms/question.ex @@ -6,7 +6,7 @@ defmodule Haj.Forms.Question do field :description, :string field :name, :string field :required, :boolean, default: false - field :type, Ecto.Enum, values: [:text, :select, :multi_select] + field :type, Ecto.Enum, values: [:text, :text_area, :select, :multi_select] field :options, {:array, :string} belongs_to :form, Haj.Forms.Form @@ -18,7 +18,7 @@ defmodule Haj.Forms.Question do @doc false def changeset(form_question, attrs) do form_question - |> cast(attrs, [:name, :type, :description, :required, :form_id]) - |> validate_required([:name, :type, :description, :required]) + |> cast(attrs, [:name, :type, :description, :required, :form_id, :options]) + |> validate_required([:name, :type]) end end diff --git a/lib/haj_web/components/core_components.ex b/lib/haj_web/components/core_components.ex index 737af0e..ee13711 100644 --- a/lib/haj_web/components/core_components.ex +++ b/lib/haj_web/components/core_components.ex @@ -350,7 +350,7 @@ defmodule HajWeb.CoreComponents do +
+ <.input field={f_nested[:name]} type="text" label="Fråga" class="col-span-4" /> + <.input + class="col-span-2" + field={f_nested[:type]} + type="select" + label="Frågetyp" + options={[ + {"Text", :text}, + {"Lång text", :text_area}, + {"Val", :select}, + {"Flerval", :multi_select} + ]} + /> + <.input + field={f_nested[:description]} + type="text" + label="Beskrivning" + class="col-span-6" + /> + +
+ Alternativ +
+ <.input + class="w-full" + type="text" + name={"form[questions][#{f_nested.index}][options][]"} + value={option} + /> + +
+ +
+ +
+ <.input field={f_nested[:required]} type="checkbox" label="Obligatorisk" class="px-2" /> + +
+
+
+ + + + <:actions> - <.button phx-disable-with="Saving...">Save Form + <.button phx-disable-with="Sparar...">Spara formulär
@@ -41,9 +124,14 @@ defmodule HajWeb.SettingsLive.Form.FormComponent do @impl true def handle_event("validate", %{"form" => form_params}, socket) do + params = + form_params + |> merge_options() + |> dbg() + changeset = socket.assigns.form - |> Forms.change_form(form_params) + |> Forms.change_form(params) |> Map.put(:action, :validate) {:noreply, assign_form(socket, changeset)} @@ -54,7 +142,7 @@ defmodule HajWeb.SettingsLive.Form.FormComponent do end defp save_form(socket, :edit, form_params) do - case Forms.update_form(socket.assigns.form, form_params) do + case Forms.update_form(socket.assigns.form, merge_options(form_params)) do {:ok, form} -> notify_parent({:saved, form}) @@ -84,8 +172,63 @@ defmodule HajWeb.SettingsLive.Form.FormComponent do end defp assign_form(socket, %Ecto.Changeset{} = changeset) do - assign(socket, :client_form, to_form(changeset)) + form = + to_form(changeset) + |> IO.inspect(label: "assign_form") + + assign(socket, :client_form, form) end defp notify_parent(msg), do: send(self(), {__MODULE__, msg}) + + defp is_select(form_field) do + form_field.value == "select" || form_field.value == "multi_select" || + form_field.value == :select || form_field.value == :multi_select + end + + defp merge_options(form_params) do + if form_params["questions"] do + Map.update!( + form_params, + "questions", + fn qs -> + Enum.map(qs, &replace_options/1) + |> Enum.into(%{}) + end + ) + else + form_params + end + end + + defp replace_options({index, question}) do + drop = question["options_drop"] || [] + sort = question["options_sort"] || [] + + question = + Map.update( + question, + "options", + [], + fn options -> + options + |> Enum.reject(fn option -> option in drop end) + |> Enum.map(fn option -> + if option == "" do + nil + else + option + end + end) + end + ) + + question = + case sort do + ["new" | _] -> Map.update!(question, "options", fn o -> o ++ [nil] end) + _ -> question + end + + {index, question} + end end diff --git a/lib/haj_web/live/settings_live/form/index.ex b/lib/haj_web/live/settings_live/form/index.ex index 806a42a..9f477f4 100644 --- a/lib/haj_web/live/settings_live/form/index.ex +++ b/lib/haj_web/live/settings_live/form/index.ex @@ -16,19 +16,19 @@ defmodule HajWeb.SettingsLive.Form.Index do defp apply_action(socket, :edit, %{"id" => id}) do socket - |> assign(:page_title, "Edit Form") + |> assign(:page_title, "Redigera formulär") |> assign(:form, Forms.get_form!(id)) end defp apply_action(socket, :new, _params) do socket - |> assign(:page_title, "New Form") + |> assign(:page_title, "Nytt formulär") |> assign(:form, %Form{}) end defp apply_action(socket, :index, _params) do socket - |> assign(:page_title, "Listing Forms") + |> assign(:page_title, "Formulär") |> assign(:form, nil) end diff --git a/lib/haj_web/live/settings_live/form/index.html.heex b/lib/haj_web/live/settings_live/form/index.html.heex index c2cab01..b6ade9e 100644 --- a/lib/haj_web/live/settings_live/form/index.html.heex +++ b/lib/haj_web/live/settings_live/form/index.html.heex @@ -1,31 +1,24 @@ <.header> - Listing Forms + Formulär <:actions> - <.link patch={~p"/live/settings/forms/new"}> - <.button>New Form + <.link patch={~p"/settings/forms/new"}> + <.button>Nytt formulär -<.table - id="forms" - rows={@streams.forms} - row_click={fn {_id, form} -> JS.navigate(~p"/live/settings/forms/#{form}") end} -> - <:col :let={{_id, form}} label="Name"><%= form.name %> - <:col :let={{_id, form}} label="Description"><%= form.description %> +<.table id="forms" rows={@streams.forms}> + <:col :let={{_id, form}} label="Namn"><%= form.name %> + <:col :let={{_id, form}} label="Beskrivning"><%= form.description %> <:action :let={{_id, form}}> -
- <.link navigate={~p"/live/settings/forms/#{form}"}>Show -
- <.link patch={~p"/live/settings/forms/#{form}/edit"}>Edit + <.link patch={~p"/settings/forms/#{form}/edit"}>Redigera <:action :let={{id, form}}> <.link phx-click={JS.push("delete", value: %{id: form.id}) |> hide("##{id}")} data-confirm="Are you sure?" > - Delete + Radera @@ -34,7 +27,7 @@ :if={@live_action in [:new, :edit]} id="form-modal" show - on_cancel={JS.navigate(~p"/live/settings/forms")} + on_cancel={JS.navigate(~p"/settings/forms")} > <.live_component module={HajWeb.SettingsLive.Form.FormComponent} @@ -42,6 +35,6 @@ title={@page_title} action={@live_action} form={@form} - patch={~p"/live/settings/forms"} + patch={~p"/settings/forms"} /> diff --git a/lib/haj_web/live/settings_live/form/show.ex b/lib/haj_web/live/settings_live/form/show.ex deleted file mode 100644 index 8a5ceae..0000000 --- a/lib/haj_web/live/settings_live/form/show.ex +++ /dev/null @@ -1,21 +0,0 @@ -defmodule HajWeb.SettingsLive.Form.Show do - use HajWeb, :live_view - - alias Haj.Forms - - @impl true - def mount(_params, _session, socket) do - {:ok, socket} - end - - @impl true - def handle_params(%{"id" => id}, _, socket) do - {:noreply, - socket - |> assign(:page_title, page_title(socket.assigns.live_action)) - |> assign(:form, Forms.get_form!(id))} - end - - defp page_title(:show), do: "Show Form" - defp page_title(:edit), do: "Edit Form" -end diff --git a/lib/haj_web/live/settings_live/form/show.html.heex b/lib/haj_web/live/settings_live/form/show.html.heex deleted file mode 100644 index 256ae67..0000000 --- a/lib/haj_web/live/settings_live/form/show.html.heex +++ /dev/null @@ -1,32 +0,0 @@ -<.header> - Form <%= @form.id %> - <:subtitle>This is a form record from your database. - <:actions> - <.link patch={~p"/live/settings/forms/#{@form}/show/edit"} phx-click={JS.push_focus()}> - <.button>Edit form - - - - -<.list> - <:item title="Name"><%= @form.name %> - <:item title="Description"><%= @form.description %> - - -<.back navigate={~p"/live/settings/forms"}>Back to forms - -<.modal - :if={@live_action == :edit} - id="form-modal" - show - on_cancel={JS.patch(~p"/live/settings/forms/#{@form}")} -> - <.live_component - module={HajWeb.SettingsLive.Form.FormComponent} - id={@form.id} - title={@page_title} - action={@live_action} - form={@form} - patch={~p"/live/settings/forms/#{@form}"} - /> - diff --git a/lib/haj_web/live/settings_live/index.ex b/lib/haj_web/live/settings_live/index.ex index 1f733c6..223a9e6 100644 --- a/lib/haj_web/live/settings_live/index.ex +++ b/lib/haj_web/live/settings_live/index.ex @@ -30,7 +30,7 @@ defmodule HajWeb.SettingsLive.Index do <.setting_card name="Användare" navigate={~p"/settings/users"}> Redigera användare och användaruppgifter - <.setting_card name="Formulär" navigate={~p"/live/settings/forms"}> + <.setting_card name="Formulär" navigate={~p"/settings/forms"}> Redigera och lägg till formulär <.setting_card name="Events" navigate={~p"/settings/events"}> diff --git a/test/haj_web/live/form_live_test.exs b/test/haj_web/live/form_live_test.exs index 740baa9..50f4513 100644 --- a/test/haj_web/live/form_live_test.exs +++ b/test/haj_web/live/form_live_test.exs @@ -1,113 +1,113 @@ defmodule HajWeb.FormLiveTest do -use HajWeb.ConnCase + use HajWeb.ConnCase -import Phoenix.LiveViewTest -import Haj.FormsFixtures + import Phoenix.LiveViewTest + import Haj.FormsFixtures -@create_attrs %{description: "some description", name: "some name"} -@update_attrs %{description: "some updated description", name: "some updated name"} -@invalid_attrs %{description: nil, name: nil} + @create_attrs %{description: "some description", name: "some name"} + @update_attrs %{description: "some updated description", name: "some updated name"} + @invalid_attrs %{description: nil, name: nil} -defp create_form(_) do -form = form_fixture() -%{form: form} -end + defp create_form(_) do + form = form_fixture() + %{form: form} + end -describe "Index" do -setup [:create_form] + describe "Index" do + setup [:create_form] -test "lists all forms", %{conn: conn, form: form} do -{:ok, _index_live, html} = live(conn, ~p"/live/settings/forms") + test "lists all forms", %{conn: conn, form: form} do + {:ok, _index_live, html} = live(conn, ~p"/settings/forms") -assert html =~ "Listing Forms" -assert html =~ form.description -end + assert html =~ "Listing Forms" + assert html =~ form.description + end -test "saves new form", %{conn: conn} do -{:ok, index_live, _html} = live(conn, ~p"/live/settings/forms") + test "saves new form", %{conn: conn} do + {:ok, index_live, _html} = live(conn, ~p"/settings/forms") -assert index_live |> element("a", "New Form") |> render_click() =~ -"New Form" + assert index_live |> element("a", "New Form") |> render_click() =~ + "New Form" -assert_patch(index_live, ~p"/live/settings/forms/new") + assert_patch(index_live, ~p"/settings/forms/new") -assert index_live -|> form("#form-form", form: @invalid_attrs) -|> render_change() =~ "can't be blank" + assert index_live + |> form("#form-form", form: @invalid_attrs) + |> render_change() =~ "can't be blank" -assert index_live -|> form("#form-form", form: @create_attrs) -|> render_submit() + assert index_live + |> form("#form-form", form: @create_attrs) + |> render_submit() -assert_patch(index_live, ~p"/live/settings/forms") + assert_patch(index_live, ~p"/settings/forms") -html = render(index_live) -assert html =~ "Form created successfully" -assert html =~ "some description" -end + html = render(index_live) + assert html =~ "Form created successfully" + assert html =~ "some description" + end -test "updates form in listing", %{conn: conn, form: form} do -{:ok, index_live, _html} = live(conn, ~p"/live/settings/forms") + test "updates form in listing", %{conn: conn, form: form} do + {:ok, index_live, _html} = live(conn, ~p"/settings/forms") -assert index_live |> element("#forms-#{form.id} a", "Edit") |> render_click() =~ -"Edit Form" + assert index_live |> element("#forms-#{form.id} a", "Edit") |> render_click() =~ + "Edit Form" -assert_patch(index_live, ~p"/live/settings/forms/#{form}/edit") + assert_patch(index_live, ~p"/settings/forms/#{form}/edit") -assert index_live -|> form("#form-form", form: @invalid_attrs) -|> render_change() =~ "can't be blank" + assert index_live + |> form("#form-form", form: @invalid_attrs) + |> render_change() =~ "can't be blank" -assert index_live -|> form("#form-form", form: @update_attrs) -|> render_submit() + assert index_live + |> form("#form-form", form: @update_attrs) + |> render_submit() -assert_patch(index_live, ~p"/live/settings/forms") + assert_patch(index_live, ~p"/settings/forms") -html = render(index_live) -assert html =~ "Form updated successfully" -assert html =~ "some updated description" -end + html = render(index_live) + assert html =~ "Form updated successfully" + assert html =~ "some updated description" + end -test "deletes form in listing", %{conn: conn, form: form} do -{:ok, index_live, _html} = live(conn, ~p"/live/settings/forms") + test "deletes form in listing", %{conn: conn, form: form} do + {:ok, index_live, _html} = live(conn, ~p"/settings/forms") -assert index_live |> element("#forms-#{form.id} a", "Delete") |> render_click() -refute has_element?(index_live, "#forms-#{form.id}") -end -end + assert index_live |> element("#forms-#{form.id} a", "Delete") |> render_click() + refute has_element?(index_live, "#forms-#{form.id}") + end + end -describe "Show" do -setup [:create_form] + describe "Show" do + setup [:create_form] -test "displays form", %{conn: conn, form: form} do -{:ok, _show_live, html} = live(conn, ~p"/live/settings/forms/#{form}") + test "displays form", %{conn: conn, form: form} do + {:ok, _show_live, html} = live(conn, ~p"/settings/forms/#{form}") -assert html =~ "Show Form" -assert html =~ form.description -end + assert html =~ "Show Form" + assert html =~ form.description + end -test "updates form within modal", %{conn: conn, form: form} do -{:ok, show_live, _html} = live(conn, ~p"/live/settings/forms/#{form}") + test "updates form within modal", %{conn: conn, form: form} do + {:ok, show_live, _html} = live(conn, ~p"/settings/forms/#{form}") -assert show_live |> element("a", "Edit") |> render_click() =~ -"Edit Form" + assert show_live |> element("a", "Edit") |> render_click() =~ + "Edit Form" -assert_patch(show_live, ~p"/live/settings/forms/#{form}/show/edit") + assert_patch(show_live, ~p"/settings/forms/#{form}/show/edit") -assert show_live -|> form("#form-form", form: @invalid_attrs) -|> render_change() =~ "can't be blank" + assert show_live + |> form("#form-form", form: @invalid_attrs) + |> render_change() =~ "can't be blank" -assert show_live -|> form("#form-form", form: @update_attrs) -|> render_submit() + assert show_live + |> form("#form-form", form: @update_attrs) + |> render_submit() -assert_patch(show_live, ~p"/live/settings/forms/#{form}") + assert_patch(show_live, ~p"/settings/forms/#{form}") -html = render(show_live) -assert html =~ "Form updated successfully" -assert html =~ "some updated description" -end -end + html = render(show_live) + assert html =~ "Form updated successfully" + assert html =~ "some updated description" + end + end end From 408d1289f3bb6863d06a362802b7408dcd96d072 Mon Sep 17 00:00:00 2001 From: Adrian Salamon Date: Sat, 10 Feb 2024 19:37:38 -0800 Subject: [PATCH 23/56] feat: add reusable search combobox component --- lib/haj/accounts/user.ex | 7 ++ .../components/search_combobox_component.ex | 112 ++++++++++++++++++ .../responsible_user_form_component.ex | 52 ++------ 3 files changed, 126 insertions(+), 45 deletions(-) create mode 100644 lib/haj_web/components/search_combobox_component.ex diff --git a/lib/haj/accounts/user.ex b/lib/haj/accounts/user.ex index 6c73850..06f236a 100644 --- a/lib/haj/accounts/user.ex +++ b/lib/haj/accounts/user.ex @@ -65,3 +65,10 @@ defmodule Haj.Accounts.User do end) end end + +defimpl Phoenix.HTML.Safe, for: Haj.Accounts.User do + def to_iodata(user) do + user.full_name + |> Phoenix.HTML.Engine.html_escape() + end +end diff --git a/lib/haj_web/components/search_combobox_component.ex b/lib/haj_web/components/search_combobox_component.ex new file mode 100644 index 0000000..24bac6c --- /dev/null +++ b/lib/haj_web/components/search_combobox_component.ex @@ -0,0 +1,112 @@ +defmodule HajWeb.Components.SearchComboboxComponent do + use HajWeb, :live_component + + @impl true + def update(assigns, socket) do + {:ok, socket |> assign(assigns) |> assign(query: "", results: [], chosen: nil)} + end + + @impl true + def handle_event("search", %{"q" => query}, socket) do + results = + socket.assigns.search_fn.(query) + |> Enum.take(5) + + IO.inspect(results) + {:noreply, assign(socket, results: results, query: query)} + end + + @impl true + def handle_event("chosen", %{"chosen" => chosen}, socket) do + chosen_result = + Enum.find(socket.assigns.results, fn result -> + Phoenix.HTML.Safe.to_iodata(result) == chosen + end) + + {:noreply, assign(socket, chosen: chosen_result.id, query: chosen, results: [])} + end + + @impl true + def render(assigns) do + ~H""" +
+
+ +
+ + + +
+
  • + + <%= result %> + + + + + +
  • +
    +
    +
    + + <.input field={@field} type="hidden" value={@chosen} /> +
    + """ + end +end diff --git a/lib/haj_web/live/settings_live/responsibility/responsible_user_form_component.ex b/lib/haj_web/live/settings_live/responsibility/responsible_user_form_component.ex index 47ff3b3..512b2d1 100644 --- a/lib/haj_web/live/settings_live/responsibility/responsible_user_form_component.ex +++ b/lib/haj_web/live/settings_live/responsibility/responsible_user_form_component.ex @@ -12,40 +12,16 @@ defmodule HajWeb.SettingsLive.Responsibility.ResponsibleUserFormComponent do <%= @title %> - <.simple_form for={} as={:search_form} phx-target={@myself} phx-change="search"> - <.input name="q" label="Sök efter användare" type="text" value={@query} autocomplete={:off} /> - - -
    -
    - <%= user.full_name %> -
    -
    - - <.simple_form - for={@form} - id="user-responsibility-form" - phx-target={@myself} - phx-change="validate" - phx-submit="save" - > - <.input - name="username" - label="Vald andvändare" - type="text" - disabled - value={@user && @user.full_name} + <.simple_form for={@form} id="user-responsibility-form" phx-target={@myself} phx-submit="save"> + <.live_component + id="search-component" + module={HajWeb.Components.SearchComboboxComponent} + search_fn={&Haj.Accounts.search_users/1} + field={@form[:user_id]} + label="Ansvarig" /> <.input field={@form[:show_id]} type="select" label="Spex" options={@shows} /> - <.input field={@form[:user_id]} type="hidden" value={@user && @user.id} /> <:actions> <.button phx-disable-with="Sparar...">Spara @@ -80,20 +56,6 @@ defmodule HajWeb.SettingsLive.Responsibility.ResponsibleUserFormComponent do {:noreply, assign_form(socket, changeset)} end - @impl true - def handle_event("search", %{"q" => query}, socket) do - users = Haj.Accounts.search_users(query) |> Enum.slice(0..5) - - {:noreply, assign(socket, users: users, query: query)} - end - - @impl true - def handle_event("select_user", %{"id" => user_id}, socket) do - user = Haj.Accounts.get_user!(user_id) - - {:noreply, assign(socket, user: user, users: [], query: "")} - end - @impl true def handle_event("save", %{"responsible_user" => params}, socket) do save_responsible_user(socket, params) From 614b6f363cacce1655e65031c9c046d9f7007bd9 Mon Sep 17 00:00:00 2001 From: Adrian Salamon Date: Sat, 10 Feb 2024 20:54:43 -0800 Subject: [PATCH 24/56] wip event registration with forms --- lib/haj/accounts/user.ex | 3 +- lib/haj/events.ex | 4 +- lib/haj/events/event.ex | 4 +- lib/haj/events/event_registration.ex | 2 + lib/haj/forms.ex | 12 +++- lib/haj/forms/form.ex | 7 ++ lib/haj/forms/question.ex | 1 - lib/haj/forms/response.ex | 1 + lib/haj_web/components/components.ex | 35 +++++++++ .../components/search_combobox_component.ex | 5 +- lib/haj_web/live/event_live/form_component.ex | 71 +++++++++++++++++++ lib/haj_web/live/event_live/show.ex | 11 +++ lib/haj_web/live/event_live/show.html.heex | 16 ++++- lib/haj_web/live/form_live/index.ex | 66 ++++------------- .../settings_live/event/form_component.ex | 20 ++++-- lib/haj_web/router.ex | 1 + .../20240211033841_add_event_forms.exs | 16 +++++ 17 files changed, 207 insertions(+), 68 deletions(-) create mode 100644 lib/haj_web/live/event_live/form_component.ex create mode 100644 priv/repo/migrations/20240211033841_add_event_forms.exs diff --git a/lib/haj/accounts/user.ex b/lib/haj/accounts/user.ex index 06f236a..d516ee9 100644 --- a/lib/haj/accounts/user.ex +++ b/lib/haj/accounts/user.ex @@ -68,7 +68,6 @@ end defimpl Phoenix.HTML.Safe, for: Haj.Accounts.User do def to_iodata(user) do - user.full_name - |> Phoenix.HTML.Engine.html_escape() + Phoenix.HTML.Engine.html_escape(user.full_name) end end diff --git a/lib/haj/events.ex b/lib/haj/events.ex index c110425..e275fd3 100644 --- a/lib/haj/events.ex +++ b/lib/haj/events.ex @@ -10,8 +10,6 @@ defmodule Haj.Events do alias Haj.Events.TicketType alias Haj.Events.EventRegistration - @topic inspect(__MODULE__) - @doc """ Returns the list of events. @@ -47,7 +45,7 @@ defmodule Haj.Events do Repo.one!( from e in Event, where: e.id == ^id, - preload: :ticket_types + preload: [:ticket_types, :form] ) end diff --git a/lib/haj/events/event.ex b/lib/haj/events/event.ex index 8b7bec0..0dae12e 100644 --- a/lib/haj/events/event.ex +++ b/lib/haj/events/event.ex @@ -13,6 +13,7 @@ defmodule Haj.Events.Event do has_many :ticket_types, Haj.Events.TicketType, on_replace: :delete has_many :event_registrations, Haj.Events.EventRegistration, on_replace: :delete + belongs_to :form, Haj.Forms.Form timestamps() end @@ -37,7 +38,8 @@ defmodule Haj.Events.Event do :ticket_limit, :event_date, :purchase_deadline, - :has_tickets + :has_tickets, + :form_id ]) |> validate_required([ :name, diff --git a/lib/haj/events/event_registration.ex b/lib/haj/events/event_registration.ex index 63d563f..a6606a2 100644 --- a/lib/haj/events/event_registration.ex +++ b/lib/haj/events/event_registration.ex @@ -7,6 +7,8 @@ defmodule Haj.Events.EventRegistration do belongs_to :user, Haj.Accounts.User belongs_to :event, Haj.Events.Event + has_one :form_response, Haj.Forms.Response + timestamps() end diff --git a/lib/haj/forms.ex b/lib/haj/forms.ex index 75d3f7f..e9ab898 100644 --- a/lib/haj/forms.ex +++ b/lib/haj/forms.ex @@ -4,7 +4,6 @@ defmodule Haj.Forms do """ import Ecto.Query, warn: false - alias Ecto.Changeset alias Haj.Repo alias Ecto.Multi @@ -23,6 +22,15 @@ defmodule Haj.Forms do Repo.all(Form) end + def search_forms(search_phrase) do + query = + from f in Form, + where: fragment("? %> ?", f.name, ^search_phrase), + order_by: {:desc, fragment("word_similarity(?, ?)", f.name, ^search_phrase)} + + Repo.all(query) + end + @doc """ Gets a single form. @@ -310,7 +318,7 @@ defmodule Haj.Forms do query = from f in Form, where: f.id == ^form_id, - join: q in assoc(f, :questions), + left_join: q in assoc(f, :questions), preload: [questions: q] form = Repo.one!(query) diff --git a/lib/haj/forms/form.ex b/lib/haj/forms/form.ex index 730b62e..c9a0d16 100644 --- a/lib/haj/forms/form.ex +++ b/lib/haj/forms/form.ex @@ -6,6 +6,7 @@ defmodule Haj.Forms.Form do field :description, :string field :name, :string + has_many :events, Haj.Events.Event has_many :questions, Haj.Forms.Question, on_replace: :delete has_many :responses, Haj.Forms.Response, on_replace: :delete @@ -20,3 +21,9 @@ defmodule Haj.Forms.Form do |> cast_assoc(:questions, sort_param: :questions_sort, drop_param: :questions_drop) end end + +defimpl Phoenix.HTML.Safe, for: Haj.Forms.Form do + def to_iodata(form) do + Phoenix.HTML.Engine.html_escape(form.name) + end +end diff --git a/lib/haj/forms/question.ex b/lib/haj/forms/question.ex index 8dd220d..5dee7be 100644 --- a/lib/haj/forms/question.ex +++ b/lib/haj/forms/question.ex @@ -10,7 +10,6 @@ defmodule Haj.Forms.Question do field :options, {:array, :string} belongs_to :form, Haj.Forms.Form - has_many :responses, Haj.Forms.Response timestamps() end diff --git a/lib/haj/forms/response.ex b/lib/haj/forms/response.ex index 61adc0a..a69eb7c 100644 --- a/lib/haj/forms/response.ex +++ b/lib/haj/forms/response.ex @@ -5,6 +5,7 @@ defmodule Haj.Forms.Response do schema "form_responses" do belongs_to :form, Haj.Forms.Form belongs_to :user, Haj.Accounts.User + belongs_to :event_registration, Haj.Events.EventRegistration has_many :question_responses, Haj.Forms.QuestionResponse diff --git a/lib/haj_web/components/components.ex b/lib/haj_web/components/components.ex index c0c4864..0decde0 100644 --- a/lib/haj_web/components/components.ex +++ b/lib/haj_web/components/components.ex @@ -4,6 +4,8 @@ defmodule HajWeb.Components do embed_templates "components/*" + import HajWeb.CoreComponents + attr :class, :string, default: "" slot :step, required: true do @@ -75,4 +77,37 @@ defmodule HajWeb.Components do """ end + + ## Form component + + attr :question, :any, required: true + attr :field, :any, required: true + + def form_input(%{question: %{type: :select}} = assigns) do + ~H""" + <.input field={@field} type="select" options={@question.options} label={@question.name} /> + """ + end + + def form_input(%{question: %{type: :multi_select}} = assigns) do + ~H""" + +
    + <.input + name={"#{@field.name}[#{option}]"} + type="checkbox" + value={option in Ecto.Changeset.get_field(assigns.field.form.source, assigns.field.field, [])} + label={option} + /> +
    + """ + end + + def form_input(assigns) do + ~H""" + <.input field={@field} type="text" label={@question.name} /> + """ + end end diff --git a/lib/haj_web/components/search_combobox_component.ex b/lib/haj_web/components/search_combobox_component.ex index 24bac6c..3efbaf9 100644 --- a/lib/haj_web/components/search_combobox_component.ex +++ b/lib/haj_web/components/search_combobox_component.ex @@ -3,7 +3,10 @@ defmodule HajWeb.Components.SearchComboboxComponent do @impl true def update(assigns, socket) do - {:ok, socket |> assign(assigns) |> assign(query: "", results: [], chosen: nil)} + chosen = Map.get(assigns, :chosen, nil) + placeholder = Map.get(assigns, :placeholder, "") + + {:ok, socket |> assign(assigns) |> assign(query: placeholder, results: [], chosen: chosen)} end @impl true diff --git a/lib/haj_web/live/event_live/form_component.ex b/lib/haj_web/live/event_live/form_component.ex new file mode 100644 index 0000000..ef4361d --- /dev/null +++ b/lib/haj_web/live/event_live/form_component.ex @@ -0,0 +1,71 @@ +defmodule HajWeb.EventLive.FormComponent do + use HajWeb, :live_component + alias Haj.Forms + + @impl true + def render(assigns) do + ~H""" +
    + <.form for={@response_form} phx-submit="save" phx-change="validate" phx-target={@myself}> + <.form_input + :for={question <- @form.questions} + question={question} + field={@response_form[String.to_atom("#{question.id}")]} + /> + + <.button phx-disable-with="Sparar...">Spara + +
    + """ + end + + @impl true + def update(%{event: event} = assigns, socket) do + changeset = Forms.get_form_changeset!(event.form_id, %{}) + form = Forms.get_form!(event.form_id) + + {:ok, assign(socket, assigns) |> assign(form: form) |> assign_form(changeset)} + end + + @impl true + def handle_event("validate", %{"form_response" => response}, socket) do + # We need to flatten all multi-responses to a list + response = flatten_response(response) + + changeset = + Forms.get_form_changeset!(socket.assigns.form.id, response) |> Map.put(:action, :validate) + + {:noreply, assign_form(socket, changeset)} + end + + @impl true + def handle_event("save", %{"form_response" => response}, socket) do + response = flatten_response(response) + + case Forms.submit_form(socket.assigns.form.id, socket.assigns.current_user.id, response) do + {:ok, _} -> + push_flash(:info, "Skickade svar") + {:noreply, socket} + + {:error, changeset} -> + {:noreply, assign_form(socket, changeset)} + end + end + + defp flatten_response(response) do + Enum.reduce(response, response, fn {q, a}, acc -> + case a do + a when is_map(a) -> + list = Map.filter(a, fn {_, sel} -> sel == "true" end) |> Map.keys() + Map.put(acc, q, list) + + _ -> + acc + end + end) + end + + defp assign_form(socket, %Ecto.Changeset{} = changeset) do + assign(socket, :response_form, to_form(changeset, as: :form_response)) + end +end diff --git a/lib/haj_web/live/event_live/show.ex b/lib/haj_web/live/event_live/show.ex index 1032273..c5fc19a 100644 --- a/lib/haj_web/live/event_live/show.ex +++ b/lib/haj_web/live/event_live/show.ex @@ -31,6 +31,12 @@ defmodule HajWeb.EventLive.Show do |> assign_selected_tickets(event.ticket_types)} end + @impl true + def handle_params(%{"id" => id}, _url, socket) do + {:noreply, assign(socket, event: Events.get_event!(id))} + end + + @impl true def handle_info( %{event: "presence_diff", payload: %{joins: joins, leaves: leaves}}, %{assigns: %{online_count: count}} = socket @@ -60,6 +66,11 @@ defmodule HajWeb.EventLive.Show do {:noreply, assign(socket, selected: selected, price: price)} end + @impl true + def handle_event("register", _, socket) do + {:noreply, push_patch(socket, to: ~p"/events/#{socket.assigns.event}/register")} + end + defp assign_selected_tickets(socket, ticket_types) do socket |> assign_new(:selected, fn -> diff --git a/lib/haj_web/live/event_live/show.html.heex b/lib/haj_web/live/event_live/show.html.heex index 427cf4a..becafc5 100644 --- a/lib/haj_web/live/event_live/show.html.heex +++ b/lib/haj_web/live/event_live/show.html.heex @@ -100,4 +100,18 @@
    -<%!-- <.online_count online_count={@online_count} /> --%> +<.modal + :if={@live_action == :register} + id="food-modal" + show + on_cancel={JS.patch(~p"/events/#{@event}")} +> + <.live_component + module={HajWeb.EventLive.FormComponent} + id={@event.id} + action={@live_action} + event={@event} + patch={~p"/events/#{@event}"} + current_user={@current_user} + /> + diff --git a/lib/haj_web/live/form_live/index.ex b/lib/haj_web/live/form_live/index.ex index 1b016e5..beb323f 100644 --- a/lib/haj_web/live/form_live/index.ex +++ b/lib/haj_web/live/form_live/index.ex @@ -9,18 +9,7 @@ defmodule HajWeb.FormLive.Index do end def handle_event("validate", %{"form_response" => response}, socket) do - # We need to flatten all mult-responses to a list - response = - Enum.reduce(response, response, fn {q, a}, acc -> - case a do - a when is_map(a) -> - list = Map.filter(a, fn {_, sel} -> sel == "true" end) |> Map.keys() - Map.put(acc, q, list) - - _ -> - acc - end - end) + response = flatten_response(response) changeset = Forms.get_form_changeset!(socket.assigns.form.id, response) |> Map.put(:action, :validate) @@ -29,18 +18,7 @@ defmodule HajWeb.FormLive.Index do end def handle_event("save", %{"form_response" => response}, socket) do - # We need to flatten all mult-responses to a list - response = - Enum.reduce(response, response, fn {q, a}, acc -> - case a do - a when is_map(a) -> - list = Map.filter(a, fn {_, sel} -> sel == "true" end) |> Map.keys() - Map.put(acc, q, list) - - _ -> - acc - end - end) + response = flatten_response(response) case Forms.submit_form(socket.assigns.form.id, socket.assigns.current_user.id, response) do {:ok, _} -> @@ -51,35 +29,17 @@ defmodule HajWeb.FormLive.Index do end end - attr :question, :any, required: true - attr :field, :any, required: true - - defp form_input(%{question: %{type: :select}} = assigns) do - ~H""" - <.input field={@field} type="select" options={@question.options} label={@question.name} /> - """ - end - - defp form_input(%{question: %{type: :multi_select}} = assigns) do - ~H""" - -
    - <.input - name={"#{@field.name}[#{option}]"} - type="checkbox" - value={option in Ecto.Changeset.get_field(assigns.field.form.source, assigns.field.field, [])} - label={option} - /> -
    - """ - end - - defp form_input(assigns) do - ~H""" - <.input field={@field} type="text" label={@question.name} /> - """ + defp flatten_response(response) do + Enum.reduce(response, response, fn {q, a}, acc -> + case a do + a when is_map(a) -> + list = Map.filter(a, fn {_, sel} -> sel == "true" end) |> Map.keys() + Map.put(acc, q, list) + + _ -> + acc + end + end) end defp assign_form(socket, %Ecto.Changeset{} = changeset) do diff --git a/lib/haj_web/live/settings_live/event/form_component.ex b/lib/haj_web/live/settings_live/event/form_component.ex index 8621b20..99b45a1 100644 --- a/lib/haj_web/live/settings_live/event/form_component.ex +++ b/lib/haj_web/live/settings_live/event/form_component.ex @@ -35,6 +35,16 @@ defmodule HajWeb.SettingsLive.Event.FormComponent do <.input field={@form[:image]} type="text" label="Bild" /> <.input field={@form[:has_tickets]} type="checkbox" label="Har biljetter" /> + <.live_component + id="search-component" + module={HajWeb.Components.SearchComboboxComponent} + search_fn={&Haj.Forms.search_forms/1} + field={@form[:form_id]} + label="Formulär" + chosen={@form[:form_id].value} + placeholder={@form[:form].value && @form[:form].value.name} + /> +

    Biljettyper

    <.inputs_for :let={f_nested} field={@form[:ticket_types]}> @@ -108,18 +118,20 @@ defmodule HajWeb.SettingsLive.Event.FormComponent do {:noreply, assign_form(socket, changeset)} end - defp assign_form(socket, %Ecto.Changeset{} = changeset) do - assign(socket, :form, to_form(changeset)) - end - + @impl true def handle_event("save", %{"event" => event_params}, socket) do save_event(socket, socket.assigns.action, event_params) end + @impl true def handle_event("delete", %{"ticket" => ticket_type}, socket) do delete_ticket_type(socket, ticket_type) end + defp assign_form(socket, %Ecto.Changeset{} = changeset) do + assign(socket, :form, to_form(changeset)) + end + defp save_event(socket, :edit, event_params) do case Events.update_event(socket.assigns.event, event_params, with_tickets: true) do {:ok, _event} -> diff --git a/lib/haj_web/router.ex b/lib/haj_web/router.ex index 2e26943..98d1c9a 100644 --- a/lib/haj_web/router.ex +++ b/lib/haj_web/router.ex @@ -103,6 +103,7 @@ defmodule HajWeb.Router do ## Events live "/events", EventLive.Index, :index live "/events/:id", EventLive.Show, :index + live "/events/:id/register", EventLive.Show, :register live "/forms/:id", FormLive.Index, :index end diff --git a/priv/repo/migrations/20240211033841_add_event_forms.exs b/priv/repo/migrations/20240211033841_add_event_forms.exs new file mode 100644 index 0000000..1296939 --- /dev/null +++ b/priv/repo/migrations/20240211033841_add_event_forms.exs @@ -0,0 +1,16 @@ +defmodule Haj.Repo.Migrations.AddEventForms do + use Ecto.Migration + + def change do + alter table(:events) do + add :form_id, references(:forms, on_delete: :nilify_all) + end + + alter table(:event_registrations) do + add :form_response_id, references(:form_responses, on_delete: :nilify_all) + end + + create index(:event_registrations, [:form_response_id]) + create index(:events, [:form_id]) + end +end From 61b5e9102c615978c83f201b54c48517fa3ad5b0 Mon Sep 17 00:00:00 2001 From: Adrian Salamon Date: Sat, 10 Feb 2024 21:09:32 -0800 Subject: [PATCH 25/56] fix creation of event when assoc not loaded --- lib/haj_web/live/settings_live/event/form_component.ex | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/haj_web/live/settings_live/event/form_component.ex b/lib/haj_web/live/settings_live/event/form_component.ex index 99b45a1..b16e4cb 100644 --- a/lib/haj_web/live/settings_live/event/form_component.ex +++ b/lib/haj_web/live/settings_live/event/form_component.ex @@ -42,7 +42,9 @@ defmodule HajWeb.SettingsLive.Event.FormComponent do field={@form[:form_id]} label="Formulär" chosen={@form[:form_id].value} - placeholder={@form[:form].value && @form[:form].value.name} + placeholder={ + Ecto.assoc_loaded?(@form.data.form) && @form[:form].value && @form[:form].value.name + } />
    From f8815ad952a02bfd2f0f96294582d0a8c92d2cd1 Mon Sep 17 00:00:00 2001 From: Adrian Salamon Date: Sun, 11 Feb 2024 16:02:28 -0800 Subject: [PATCH 26/56] minor improvement to queries on load --- lib/haj/accounts/user.ex | 1 + lib/haj/accounts/user_token.ex | 10 ++++++---- lib/haj/policy/policy.ex | 8 ++++---- lib/haj/spex.ex | 6 ++++-- lib/haj_web/controllers/user_auth.ex | 8 +++++--- lib/haj_web/live/event_live/show.ex | 8 +++++++- 6 files changed, 27 insertions(+), 14 deletions(-) diff --git a/lib/haj/accounts/user.ex b/lib/haj/accounts/user.ex index d516ee9..f541187 100644 --- a/lib/haj/accounts/user.ex +++ b/lib/haj/accounts/user.ex @@ -22,6 +22,7 @@ defmodule Haj.Accounts.User do many_to_many :foods, Haj.Foods.Food, join_through: "food_preferences", on_replace: :delete has_many :group_memberships, Haj.Spex.GroupMembership + has_many :user_tokens, Haj.Accounts.UserToken field :food_preference_other, :string diff --git a/lib/haj/accounts/user_token.ex b/lib/haj/accounts/user_token.ex index afb7e5e..b4b2bca 100644 --- a/lib/haj/accounts/user_token.ex +++ b/lib/haj/accounts/user_token.ex @@ -2,6 +2,7 @@ defmodule Haj.Accounts.UserToken do use Ecto.Schema import Ecto.Query alias Haj.Accounts.UserToken + alias Haj.Accounts.User @rand_size 32 @session_validity_in_days 60 @@ -49,10 +50,11 @@ defmodule Haj.Accounts.UserToken do """ def verify_session_token_query(token) do query = - from token in token_and_context_query(token, "session"), - join: user in assoc(token, :user), - where: token.inserted_at > ago(@session_validity_in_days, "day"), - select: user + from user in User, + join: ut in assoc(user, :user_tokens), + where: ut.token == ^token, + where: ut.context == "session", + where: ut.inserted_at > ago(@session_validity_in_days, "day") {:ok, query} end diff --git a/lib/haj/policy/policy.ex b/lib/haj/policy/policy.ex index 3dc89d8..4eb79ad 100644 --- a/lib/haj/policy/policy.ex +++ b/lib/haj/policy/policy.ex @@ -19,15 +19,15 @@ defmodule Haj.Policy do end action :admin do - allow current_group_member: :grafiq - allow role: :admin - end - action :list_orders do allow current_group_member: :grafiq + end + action :list_orders do allow role: :admin + + allow current_group_member: :grafiq end end diff --git a/lib/haj/spex.ex b/lib/haj/spex.ex index 66c0cfc..1be0d5c 100644 --- a/lib/haj/spex.ex +++ b/lib/haj/spex.ex @@ -513,10 +513,12 @@ defmodule Haj.Spex do query = from gm in GroupMembership, join: sg in assoc(gm, :show_group), + join: g in assoc(sg, :group), where: sg.show_id == ^show_id, - order_by: sg.id + order_by: sg.id, + preload: [show_group: {sg, group: g}] - Repo.preload(users, group_memberships: {query, [show_group: [group: []]]}) + Repo.preload(users, group_memberships: query) end def get_show_groups_for_user(userid) do diff --git a/lib/haj_web/controllers/user_auth.ex b/lib/haj_web/controllers/user_auth.ex index b043a79..c374a21 100644 --- a/lib/haj_web/controllers/user_auth.ex +++ b/lib/haj_web/controllers/user_auth.ex @@ -10,6 +10,7 @@ defmodule HajWeb.UserAuth do alias Haj.Accounts alias Haj.Accounts.User alias HajWeb.Router.Helpers, as: Routes + alias Haj.Spex # Make the remember me cookie valid for 60 days. # If you want bump or reduce this value, also change @@ -40,7 +41,7 @@ defmodule HajWeb.UserAuth do {:cont, Component.assign_new(socket, :current_user, fn -> Accounts.get_user_by_session_token(user_token) - |> Haj.Spex.preload_user_groups() + |> Spex.preload_user_groups() end)} %{} -> @@ -58,7 +59,7 @@ defmodule HajWeb.UserAuth do new_socket = Component.assign_new(socket, :current_user, fn -> Accounts.get_user_by_session_token(user_token) - |> Haj.Spex.preload_user_groups() + |> Spex.preload_user_groups() end) %Accounts.User{} = new_socket.assigns.current_user @@ -169,7 +170,8 @@ defmodule HajWeb.UserAuth do user = user_token && - Accounts.get_user_by_session_token(user_token) |> Haj.Spex.preload_user_groups() + Accounts.get_user_by_session_token(user_token) + |> Spex.preload_user_groups() assign(conn, :current_user, user) end diff --git a/lib/haj_web/live/event_live/show.ex b/lib/haj_web/live/event_live/show.ex index c5fc19a..904dcf4 100644 --- a/lib/haj_web/live/event_live/show.ex +++ b/lib/haj_web/live/event_live/show.ex @@ -68,7 +68,13 @@ defmodule HajWeb.EventLive.Show do @impl true def handle_event("register", _, socket) do - {:noreply, push_patch(socket, to: ~p"/events/#{socket.assigns.event}/register")} + if socket.assigns.event.has_tickets do + selected = Enum.filter(socket.assigns.selected, fn {_, count} -> count > 0 end) + IO.inspect(selected) + {:noreply, socket} + else + {:noreply, push_patch(socket, to: ~p"/events/#{socket.assigns.event}/register")} + end end defp assign_selected_tickets(socket, ticket_types) do From f7b662cb3ea5d10fd5acdd959b305a8a1769f2d4 Mon Sep 17 00:00:00 2001 From: Adrian Salamon Date: Sun, 11 Feb 2024 23:02:55 -0800 Subject: [PATCH 27/56] bump tailwind --- config/config.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.exs b/config/config.exs index b8da7da..94b4adc 100644 --- a/config/config.exs +++ b/config/config.exs @@ -53,7 +53,7 @@ config :logger, :console, config :phoenix, :json_library, Jason config :tailwind, - version: "3.0.24", + version: "3.4.1", default: [ args: ~w( --config=tailwind.config.js From 3d0478debe99f8980deb4128d8216e152b53f4ba Mon Sep 17 00:00:00 2001 From: Adrian Salamon Date: Sun, 11 Feb 2024 23:05:52 -0800 Subject: [PATCH 28/56] ui consistency fixes --- lib/haj_web/components/components.ex | 47 ++++++++++++++----- lib/haj_web/components/layouts.ex | 27 ++++++----- lib/haj_web/components/layouts/live.html.heex | 2 +- .../components/search_combobox_component.ex | 1 - .../live/applications_live/index.html.heex | 23 ++------- .../live/applications_live/show.html.heex | 2 +- .../live/dashboard_live/index.html.heex | 2 +- lib/haj_web/live/group_live/admin.html.heex | 2 +- lib/haj_web/live/group_live/index.html.heex | 8 +--- lib/haj_web/live/group_live/show.html.heex | 2 +- lib/haj_web/live/members_live.ex | 2 +- .../live/merch_admin_live/index.html.heex | 2 +- .../live/merch_admin_live/orders.html.heex | 2 +- lib/haj_web/live/merch_live/index.html.heex | 2 +- lib/haj_web/live/nav_live.ex | 10 ++-- .../responsibility_live/form_component.ex | 2 +- .../responsibility_live/history.html.heex | 2 +- .../live/responsibility_live/index.html.heex | 2 +- .../live/responsibility_live/show.html.heex | 2 +- lib/haj_web/live/settings_live/index.ex | 16 +++---- .../responsibility/form_component.ex | 2 +- .../live/settings_live/song/form_component.ex | 2 +- lib/haj_web/live/settings_live/song/index.ex | 6 +-- .../live/settings_live/song/index.html.heex | 8 ++-- lib/haj_web/live/show_live/index.html.heex | 2 +- lib/haj_web/live/show_live/show.html.heex | 2 +- lib/haj_web/live/song_live/index.html.heex | 2 +- lib/haj_web/live/song_live/show.html.heex | 2 +- lib/haj_web/live/user_live.ex | 2 +- lib/haj_web/live/user_settings_live.ex | 2 +- 30 files changed, 99 insertions(+), 89 deletions(-) diff --git a/lib/haj_web/components/components.ex b/lib/haj_web/components/components.ex index 0decde0..9c93933 100644 --- a/lib/haj_web/components/components.ex +++ b/lib/haj_web/components/components.ex @@ -78,7 +78,9 @@ defmodule HajWeb.Components do """ end - ## Form component + @doc """ + A generic form component that works with Changesets generated from Haj.Form. + """ attr :question, :any, required: true attr :field, :any, required: true @@ -91,16 +93,22 @@ defmodule HajWeb.Components do def form_input(%{question: %{type: :multi_select}} = assigns) do ~H""" - -
    - <.input - name={"#{@field.name}[#{option}]"} - type="checkbox" - value={option in Ecto.Changeset.get_field(assigns.field.form.source, assigns.field.field, [])} - label={option} - /> +
    + +
    +
    + <.input + name={"#{@field.name}[#{option}]"} + type="checkbox" + value={ + option in Ecto.Changeset.get_field(assigns.field.form.source, assigns.field.field, []) + } + label={option} + /> +
    +
    """ end @@ -110,4 +118,21 @@ defmodule HajWeb.Components do <.input field={@field} type="text" label={@question.name} /> """ end + + @doc """ + A generic stats card component with a title and value. + """ + attr :title, :string, required: true + attr :value, :string, required: true + + def stats_card(assigns) do + ~H""" +
    +
    <%= @title %>
    +
    + <%= @value %> +
    +
    + """ + end end diff --git a/lib/haj_web/components/layouts.ex b/lib/haj_web/components/layouts.ex index cbb5f8c..a68ee58 100644 --- a/lib/haj_web/components/layouts.ex +++ b/lib/haj_web/components/layouts.ex @@ -113,6 +113,12 @@ defmodule HajWeb.Layouts do active={@active_tab == :settings} expanded={@expanded_tab == :settings} > + <:sub_link + navigate={~p"/settings/users"} + title="Användare" + active={@active_tab == {:setting, :users}} + /> + <:sub_link navigate={~p"/settings/shows"} title="Spex" @@ -125,6 +131,11 @@ defmodule HajWeb.Layouts do active={@active_tab == {:setting, :groups}} /> + <:sub_link + navigate={~p"/settings/responsibilities"} + title="Ansvar" + active={@active_tab == {:setting, :responsibilities}} + /> <:sub_link navigate={~p"/settings/foods"} title="Mat" @@ -134,25 +145,19 @@ defmodule HajWeb.Layouts do <:sub_link navigate={~p"/settings/events"} title="Event" - active={@active_tab == {:setting, :event}} - /> - - <:sub_link - navigate={~p"/settings/users"} - title="Användare" - active={@active_tab == {:setting, :users}} + active={@active_tab == {:setting, :events}} /> <:sub_link - navigate={~p"/settings/responsibilities"} - title="Ansvar" - active={@active_tab == {:setting, :responsibilities}} + navigate={~p"/settings/forms"} + title="Formulär" + active={@active_tab == {:setting, :forms}} /> <:sub_link navigate={~p"/settings/songs"} title="Sånger" - active={@active_tab == {:setting, :song}} + active={@active_tab == {:setting, :songs}} />
    diff --git a/lib/haj_web/components/layouts/live.html.heex b/lib/haj_web/components/layouts/live.html.heex index 34cc2f0..fef059a 100644 --- a/lib/haj_web/components/layouts/live.html.heex +++ b/lib/haj_web/components/layouts/live.html.heex @@ -106,7 +106,7 @@
    -
    +
    <%= @inner_content %>
    diff --git a/lib/haj_web/components/search_combobox_component.ex b/lib/haj_web/components/search_combobox_component.ex index 3efbaf9..0d714d3 100644 --- a/lib/haj_web/components/search_combobox_component.ex +++ b/lib/haj_web/components/search_combobox_component.ex @@ -15,7 +15,6 @@ defmodule HajWeb.Components.SearchComboboxComponent do socket.assigns.search_fn.(query) |> Enum.take(5) - IO.inspect(results) {:noreply, assign(socket, results: results, query: query)} end diff --git a/lib/haj_web/live/applications_live/index.html.heex b/lib/haj_web/live/applications_live/index.html.heex index 2904990..ecfe0dc 100644 --- a/lib/haj_web/live/applications_live/index.html.heex +++ b/lib/haj_web/live/applications_live/index.html.heex @@ -1,28 +1,13 @@
    -
    +
    Ansökningar
    <.form :let={_f} for={%{}} as={:filter} phx-change="filter" class="mt-2 flex gap-2"> diff --git a/lib/haj_web/live/applications_live/show.html.heex b/lib/haj_web/live/applications_live/show.html.heex index 10be409..1f32d5c 100644 --- a/lib/haj_web/live/applications_live/show.html.heex +++ b/lib/haj_web/live/applications_live/show.html.heex @@ -1,5 +1,5 @@
    -
    +

    Ansökan

    diff --git a/lib/haj_web/live/dashboard_live/index.html.heex b/lib/haj_web/live/dashboard_live/index.html.heex index 587a435..4acd094 100644 --- a/lib/haj_web/live/dashboard_live/index.html.heex +++ b/lib/haj_web/live/dashboard_live/index.html.heex @@ -1,4 +1,4 @@ -

    +

    Hej,

    <%= @current_user.first_name %>!

    diff --git a/lib/haj_web/live/group_live/admin.html.heex b/lib/haj_web/live/group_live/admin.html.heex index f0426f2..1a9b72e 100644 --- a/lib/haj_web/live/group_live/admin.html.heex +++ b/lib/haj_web/live/group_live/admin.html.heex @@ -1,4 +1,4 @@ -

    +

    Redigera <%= @page_title %>

    <.simple_form for={@form} phx-submit="save" class="flex flex-col pb-2"> diff --git a/lib/haj_web/live/group_live/index.html.heex b/lib/haj_web/live/group_live/index.html.heex index c3750ad..a250b96 100644 --- a/lib/haj_web/live/group_live/index.html.heex +++ b/lib/haj_web/live/group_live/index.html.heex @@ -1,15 +1,11 @@
    -
    +
    Grupper Visar <%= length(@groups) %>
    - <.generic_card - :for={sg <- @groups} - navigate={~p"/group/#{sg.id}"} - class="flex flex-col gap-1 rounded-lg border px-4 py-4 hover:bg-gray-50 sm:gap-1.5" - > + <.generic_card :for={sg <- @groups} navigate={~p"/group/#{sg.id}"}> <:title> <%= sg.group.name %> diff --git a/lib/haj_web/live/group_live/show.html.heex b/lib/haj_web/live/group_live/show.html.heex index 29b0681..2d156c6 100644 --- a/lib/haj_web/live/group_live/show.html.heex +++ b/lib/haj_web/live/group_live/show.html.heex @@ -1,4 +1,4 @@ -
    +
    diff --git a/lib/haj_web/live/members_live.ex b/lib/haj_web/live/members_live.ex index 576e4f9..c4b0dde 100644 --- a/lib/haj_web/live/members_live.ex +++ b/lib/haj_web/live/members_live.ex @@ -53,7 +53,7 @@ defmodule HajWeb.MembersLive do phx-no-submit autocomplete={:off} onkeydown="return event.key != 'Enter';" - class="flex flex-col gap-4 pt-4 sm:flex-row sm:items-center" + class="flex flex-col gap-4 sm:flex-row sm:items-center" >
    Medlemmar diff --git a/lib/haj_web/live/merch_admin_live/index.html.heex b/lib/haj_web/live/merch_admin_live/index.html.heex index 983da6e..77821c8 100644 --- a/lib/haj_web/live/merch_admin_live/index.html.heex +++ b/lib/haj_web/live/merch_admin_live/index.html.heex @@ -1,5 +1,5 @@
    -

    Administrera merch

    +

    Administrera merch

    <.form :let={f} for={%{}} id="show-form" as={:show} phx-change="select_show" class="">
    <%= label(f, :show, "Välj spex", diff --git a/lib/haj_web/live/merch_admin_live/orders.html.heex b/lib/haj_web/live/merch_admin_live/orders.html.heex index 1960813..f423d75 100644 --- a/lib/haj_web/live/merch_admin_live/orders.html.heex +++ b/lib/haj_web/live/merch_admin_live/orders.html.heex @@ -1,5 +1,5 @@
    -

    Beställningar

    +

    Beställningar

    <.form id="show-form" as={:show} phx-change="select_show" for={%{}}> <.input name="show" label="Välj spex" type="select" value={@show.id} options={@show_options} /> diff --git a/lib/haj_web/live/merch_live/index.html.heex b/lib/haj_web/live/merch_live/index.html.heex index 99f52b1..8147ce4 100644 --- a/lib/haj_web/live/merch_live/index.html.heex +++ b/lib/haj_web/live/merch_live/index.html.heex @@ -1,5 +1,5 @@
    -
    +

    Tillgänglig merch

    <%= for merch_item <- @available_merch do %> diff --git a/lib/haj_web/live/nav_live.ex b/lib/haj_web/live/nav_live.ex index a0ba73b..9598c6f 100644 --- a/lib/haj_web/live/nav_live.ex +++ b/lib/haj_web/live/nav_live.ex @@ -61,15 +61,15 @@ defmodule HajWeb.Nav do tab ApplicationsLive.Index, :applications tab SettingsLive.Index, :settings + tab SettingsLive.User.Index, {:setting, :users} tab SettingsLive.Show.Index, {:setting, :shows} tab SettingsLive.Group.Index, {:setting, :groups} - tab SettingsLive.Food.Index, {:setting, :foods} - tab SettingsLive.User.Index, {:setting, :users} - tab SettingsLive.Merch.Index, {:setting, :merch} tab SettingsLive.Responsibility.Index, {:setting, :responsibilities} - tab SettingsLive.Song.Index, {:setting, :song} + tab SettingsLive.Food.Index, {:setting, :foods} + tab SettingsLive.Event.Index, {:setting, :events} + tab SettingsLive.Form.Index, {:setting, :forms} + tab SettingsLive.Song.Index, {:setting, :songs} - tab EventAdminLive.Index, {:setting, :event} tab EventLive.Index, :events defp set_active_tab(params, _url, socket) do diff --git a/lib/haj_web/live/responsibility_live/form_component.ex b/lib/haj_web/live/responsibility_live/form_component.ex index a09cfa4..2ece044 100644 --- a/lib/haj_web/live/responsibility_live/form_component.ex +++ b/lib/haj_web/live/responsibility_live/form_component.ex @@ -9,7 +9,7 @@ defmodule HajWeb.ResponsibilityLive.FormComponent do
    <.header> <%= @title %> - <:subtitle>Use this form to manage responsibility records in your database. + <:subtitle>Använd detta formulär för att redigera ansvar i databasen. <.simple_form diff --git a/lib/haj_web/live/responsibility_live/history.html.heex b/lib/haj_web/live/responsibility_live/history.html.heex index d37fb67..5d451c0 100644 --- a/lib/haj_web/live/responsibility_live/history.html.heex +++ b/lib/haj_web/live/responsibility_live/history.html.heex @@ -1,4 +1,4 @@ -
    +
    Dina ansvar Du har totalt haft <%= length(@current_responsibilities) + length(@prev_responsibilities) %> ansvar. Klicka på ett ansvar för att se mer information. diff --git a/lib/haj_web/live/responsibility_live/index.html.heex b/lib/haj_web/live/responsibility_live/index.html.heex index 7a265e3..b55814f 100644 --- a/lib/haj_web/live/responsibility_live/index.html.heex +++ b/lib/haj_web/live/responsibility_live/index.html.heex @@ -1,4 +1,4 @@ -
    +
    Ansvar Visar <%= length(@responsibilities) %>
    diff --git a/lib/haj_web/live/responsibility_live/show.html.heex b/lib/haj_web/live/responsibility_live/show.html.heex index 9068d92..8372811 100644 --- a/lib/haj_web/live/responsibility_live/show.html.heex +++ b/lib/haj_web/live/responsibility_live/show.html.heex @@ -1,4 +1,4 @@ -
    +

    <%= @responsibility.name %>

    Ansvar

    diff --git a/lib/haj_web/live/settings_live/index.ex b/lib/haj_web/live/settings_live/index.ex index 223a9e6..42f6d27 100644 --- a/lib/haj_web/live/settings_live/index.ex +++ b/lib/haj_web/live/settings_live/index.ex @@ -18,26 +18,26 @@ defmodule HajWeb.SettingsLive.Index do
    + <.setting_card name="Användare" navigate={~p"/settings/users"}> + Redigera användare och användaruppgifter + <.setting_card name="Spex" navigate={~p"/settings/shows"}> Redigera alla spex <.setting_card name="Grupper" navigate={~p"/settings/groups"}> Redigera grupper och spexgrupper + <.setting_card name="Ansvar" navigate={~p"/settings/responsibilities"}> + Redigera ansvar + <.setting_card name="Mat" navigate={~p"/settings/foods"}> Redigera matpreferenser - <.setting_card name="Användare" navigate={~p"/settings/users"}> - Redigera användare och användaruppgifter - - <.setting_card name="Formulär" navigate={~p"/settings/forms"}> - Redigera och lägg till formulär - <.setting_card name="Events" navigate={~p"/settings/events"}> Redigera events - <.setting_card name="Ansvar" navigate={~p"/settings/responsibilities"}> - Redigera ansvar + <.setting_card name="Formulär" navigate={~p"/settings/forms"}> + Redigera och skapa formulär <.setting_card name="Sånger" navigate={~p"/settings/songs"}> Redigera sånger diff --git a/lib/haj_web/live/settings_live/responsibility/form_component.ex b/lib/haj_web/live/settings_live/responsibility/form_component.ex index b8f3ab4..fb2cef2 100644 --- a/lib/haj_web/live/settings_live/responsibility/form_component.ex +++ b/lib/haj_web/live/settings_live/responsibility/form_component.ex @@ -9,7 +9,7 @@ defmodule HajWeb.SettingsLive.Responsibility.FormComponent do
    <.header> <%= @title %> - <:subtitle>Ändra datan kring ett ansvar i databasen.. + <:subtitle>Ändra datan kring ett ansvar i databasen. <.simple_form diff --git a/lib/haj_web/live/settings_live/song/form_component.ex b/lib/haj_web/live/settings_live/song/form_component.ex index 6ce3538..f121433 100644 --- a/lib/haj_web/live/settings_live/song/form_component.ex +++ b/lib/haj_web/live/settings_live/song/form_component.ex @@ -10,7 +10,7 @@ defmodule HajWeb.SettingsLive.Song.FormComponent do
    <.header> <%= @title %> - <:subtitle>Use this form to manage song records in your database. + <:subtitle>Använd detta formulär för att redigera sånger i databasen. <.simple_form diff --git a/lib/haj_web/live/settings_live/song/index.ex b/lib/haj_web/live/settings_live/song/index.ex index d5074a7..d51090d 100644 --- a/lib/haj_web/live/settings_live/song/index.ex +++ b/lib/haj_web/live/settings_live/song/index.ex @@ -17,19 +17,19 @@ defmodule HajWeb.SettingsLive.Song.Index do defp apply_action(socket, :edit, %{"id" => id}) do socket - |> assign(:page_title, "Edit Song") + |> assign(:page_title, "Redigera sång") |> assign(:song, Archive.get_song!(id)) end defp apply_action(socket, :new, _params) do socket - |> assign(:page_title, "New Song") + |> assign(:page_title, "New sång") |> assign(:song, %Song{}) end defp apply_action(socket, :index, _params) do socket - |> assign(:page_title, "Listing Songs") + |> assign(:page_title, "Sånger") |> assign(:song, nil) end diff --git a/lib/haj_web/live/settings_live/song/index.html.heex b/lib/haj_web/live/settings_live/song/index.html.heex index 539c8a4..e0aead8 100644 --- a/lib/haj_web/live/settings_live/song/index.html.heex +++ b/lib/haj_web/live/settings_live/song/index.html.heex @@ -1,8 +1,8 @@ <.header> - Listing Songs + Sånger <:actions> <.link patch={~p"/settings/songs/new"}> - <.button>New Song + <.button>Ny sång @@ -18,9 +18,9 @@ <:col :let={{_id, song}} label="Spex"><%= song.show.year.year %> <:action :let={{_id, song}}>
    - <.link navigate={~p"/settings/songs/#{song}"}>Show + <.link navigate={~p"/settings/songs/#{song}"}>Visa
    - <.link patch={~p"/settings/songs/#{song}/edit"}>Edit + <.link patch={~p"/settings/songs/#{song}/edit"}>Redigera <:action :let={{id, song}}> <.link diff --git a/lib/haj_web/live/show_live/index.html.heex b/lib/haj_web/live/show_live/index.html.heex index f260323..7a2eec9 100644 --- a/lib/haj_web/live/show_live/index.html.heex +++ b/lib/haj_web/live/show_live/index.html.heex @@ -1,5 +1,5 @@
    -
    +
    Alla spex Totalt <%= length(@shows) %> spex finns i Haj
    diff --git a/lib/haj_web/live/show_live/show.html.heex b/lib/haj_web/live/show_live/show.html.heex index e3c5e83..85e707c 100644 --- a/lib/haj_web/live/show_live/show.html.heex +++ b/lib/haj_web/live/show_live/show.html.heex @@ -6,7 +6,7 @@ phx-no-submit autocomplete={:off} onkeydown="return event.key != 'Enter';" - class="flex flex-col gap-4 pt-4 sm:flex-row sm:items-center" + class="flex flex-col gap-4 sm:flex-row sm:items-center" >
    <%= "Medlemmar i spexet #{@show.year.year}" %> diff --git a/lib/haj_web/live/song_live/index.html.heex b/lib/haj_web/live/song_live/index.html.heex index b12aa0a..ab6ff71 100644 --- a/lib/haj_web/live/song_live/index.html.heex +++ b/lib/haj_web/live/song_live/index.html.heex @@ -1,4 +1,4 @@ -
    +
    Sångarkiv Här finns alla sånger som har gjorts i spexet genom åren. Kika och lyssna in! diff --git a/lib/haj_web/live/song_live/show.html.heex b/lib/haj_web/live/song_live/show.html.heex index 988c392..f6d776a 100644 --- a/lib/haj_web/live/song_live/show.html.heex +++ b/lib/haj_web/live/song_live/show.html.heex @@ -1,4 +1,4 @@ -
    +

    Sång: <%= @song.name %>

    diff --git a/lib/haj_web/live/user_live.ex b/lib/haj_web/live/user_live.ex index 8810ffc..c19f197 100644 --- a/lib/haj_web/live/user_live.ex +++ b/lib/haj_web/live/user_live.ex @@ -13,7 +13,7 @@ defmodule HajWeb.UserLive do def render(assigns) do ~H""" -
    +

    Dina uppgifter

    From 0f11e54a57a4bab9e98479d2e46a4b679feacba1 Mon Sep 17 00:00:00 2001 From: Adrian Salamon Date: Sun, 11 Feb 2024 23:07:00 -0800 Subject: [PATCH 29/56] event signups now possible, and ui to view event registrations --- lib/haj/events.ex | 49 +++++- lib/haj/events/event.ex | 2 +- lib/haj/events/event_registration.ex | 8 +- lib/haj/forms.ex | 73 ++++++++- lib/haj/forms/response.ex | 2 +- lib/haj/policy/policy.ex | 7 + lib/haj_web/live/event_live/form_component.ex | 152 ++++++++++++++++-- lib/haj_web/live/event_live/index.html.heex | 2 +- lib/haj_web/live/event_live/registrations.ex | 85 ++++++++++ .../live/event_live/registrations.html.heex | 141 ++++++++++++++++ lib/haj_web/live/event_live/show.ex | 8 +- lib/haj_web/live/event_live/show.html.heex | 22 ++- .../live/settings_live/form/form_component.ex | 9 +- lib/haj_web/router.ex | 5 + .../20240211033841_add_event_forms.exs | 2 +- .../20240212001204_add_attending_field.exs | 9 ++ 16 files changed, 539 insertions(+), 37 deletions(-) create mode 100644 lib/haj_web/live/event_live/registrations.ex create mode 100644 lib/haj_web/live/event_live/registrations.html.heex create mode 100644 priv/repo/migrations/20240212001204_add_attending_field.exs diff --git a/lib/haj/events.ex b/lib/haj/events.ex index e275fd3..c69eb2d 100644 --- a/lib/haj/events.ex +++ b/lib/haj/events.ex @@ -41,11 +41,11 @@ defmodule Haj.Events do ** (Ecto.NoResultsError) """ - def get_event!(id) do + def get_event!(id, preloads \\ [:ticket_types, :form]) do Repo.one!( from e in Event, where: e.id == ^id, - preload: [:ticket_types, :form] + preload: ^preloads ) end @@ -228,6 +228,36 @@ defmodule Haj.Events do Repo.all(EventRegistration) end + @doc """ + Returns the list of event_registrations for a given event. Preloads litteraly everything. + """ + + def list_event_registrations(event_id) do + spex = Haj.Spex.current_spex() + + members_query = + from gm in Haj.Spex.GroupMembership, + join: sg in assoc(gm, :show_group), + join: g in assoc(sg, :group), + where: sg.show_id == ^spex.id, + order_by: sg.id, + preload: [show_group: {sg, group: g}] + + query = + from r in EventRegistration, + where: r.event_id == ^event_id, + join: u in assoc(r, :user), + left_join: fp in assoc(u, :foods), + left_join: fr in assoc(r, :response), + left_join: qr in assoc(fr, :question_responses), + preload: [ + user: {u, [group_memberships: ^members_query, foods: fp]}, + response: {fr, question_responses: qr} + ] + + Repo.all(query) + end + @doc """ Gets a single event_registration. @@ -293,7 +323,7 @@ defmodule Haj.Events do {:error, %Ecto.Changeset{}} """ - def delete_event_registration(%EventRegistration{id: id} = event_registration) do + def delete_event_registration(%EventRegistration{} = event_registration) do Repo.delete(event_registration) |> notify_subscribers({:registration, :deleted}) end @@ -326,6 +356,19 @@ defmodule Haj.Events do Repo.one(q) end + @doc """ + Returns a registration for an event for a user, if one exists. + """ + def get_registration_for_user(event_id, user_id) do + query = + from r in EventRegistration, + where: r.event_id == ^event_id and r.user_id == ^user_id + + # preload: [form_response: [question_responses: []]] + + Repo.one(query) + end + def subscribe(event_id) do Phoenix.PubSub.subscribe(Haj.PubSub, "event:#{event_id}") end diff --git a/lib/haj/events/event.ex b/lib/haj/events/event.ex index 0dae12e..7c48c7a 100644 --- a/lib/haj/events/event.ex +++ b/lib/haj/events/event.ex @@ -12,7 +12,7 @@ defmodule Haj.Events.Event do field :has_tickets, :boolean, default: true has_many :ticket_types, Haj.Events.TicketType, on_replace: :delete - has_many :event_registrations, Haj.Events.EventRegistration, on_replace: :delete + has_many :registrations, Haj.Events.EventRegistration, on_replace: :delete belongs_to :form, Haj.Forms.Form timestamps() diff --git a/lib/haj/events/event_registration.ex b/lib/haj/events/event_registration.ex index a6606a2..485e41f 100644 --- a/lib/haj/events/event_registration.ex +++ b/lib/haj/events/event_registration.ex @@ -7,7 +7,9 @@ defmodule Haj.Events.EventRegistration do belongs_to :user, Haj.Accounts.User belongs_to :event, Haj.Events.Event - has_one :form_response, Haj.Forms.Response + field :attending, :boolean, default: false + + belongs_to :response, Haj.Forms.Response timestamps() end @@ -15,7 +17,7 @@ defmodule Haj.Events.EventRegistration do @doc false def changeset(event_registration, attrs) do event_registration - |> cast(attrs, [:ticket_type_id, :user_id, :event_id]) - |> validate_required([:ticket_type_id, :user_id]) + |> cast(attrs, [:ticket_type_id, :user_id, :event_id, :response_id, :attending]) + |> validate_required([:user_id, :event_id]) end end diff --git a/lib/haj/forms.ex b/lib/haj/forms.ex index e9ab898..20de808 100644 --- a/lib/haj/forms.ex +++ b/lib/haj/forms.ex @@ -314,6 +314,16 @@ defmodule Haj.Forms do @doc """ Retruns a changeset for a form submission """ + def get_form_changeset!(form_id, %Response{} = response) do + attrs = + response.question_responses + |> Enum.reduce(%{}, fn qr, acc -> + Map.put(acc, String.to_atom("#{qr.question_id}"), qr.answer || qr.multi_answer) + end) + + get_form_changeset!(form_id, attrs) + end + def get_form_changeset!(form_id, attrs) do query = from f in Form, @@ -475,6 +485,10 @@ defmodule Haj.Forms do QuestionResponse.changeset(question_response, attrs) end + @doc """ + Submits a form response for a user and form. Creates a response, as well as + all the question responses for the form. + """ def submit_form(form_id, user_id, attrs) do changeset = get_form_changeset!(form_id, attrs) @@ -491,7 +505,11 @@ defmodule Haj.Forms do end) multi = - Multi.new() |> Multi.insert(:response, %Response{user_id: user_id, form_id: form_id}) + Multi.new() + |> Multi.insert(:response, %Response{ + user_id: user_id, + form_id: form_id + }) {_n, multi} = Enum.reduce(question_responses, {0, multi}, fn qr, {n, m} -> @@ -507,4 +525,57 @@ defmodule Haj.Forms do {:error, changeset} end end + + @doc """ + Updates a form response for a form. Modifies the response, as well as + replaces all the question responses with new answers. + """ + def update_form_response(form_id, form_response, attrs) do + changeset = get_form_changeset!(form_id, attrs) + + case changeset |> Ecto.Changeset.apply_action(:create) do + {:ok, data} -> + question_responses = + Enum.map(data, fn {key, val} -> + id = Atom.to_string(key) |> String.to_integer() + + case val do + ans when is_list(ans) -> %QuestionResponse{question_id: id, multi_answer: ans} + ans -> %QuestionResponse{question_id: id, answer: ans} + end + end) + + multi = + Multi.new() + |> Multi.delete_all( + :question_responses, + from(qr in QuestionResponse, where: qr.response_id == ^form_response.id) + ) + + {_n, multi} = + Enum.reduce(question_responses, {0, multi}, fn qr, {n, m} -> + {n + 1, + Multi.insert(m, n, fn _ -> + %QuestionResponse{qr | response_id: form_response.id} + end)} + end) + + Repo.transaction(multi) + + {:error, changeset} -> + {:error, changeset} + end + end + + @doc """ + Returns a form response for a user and form, if one exists. + """ + def get_response_for_user(form_id, user_id) do + query = + from r in Response, + where: r.form_id == ^form_id and r.user_id == ^user_id, + preload: [question_responses: []] + + Repo.one(query) + end end diff --git a/lib/haj/forms/response.ex b/lib/haj/forms/response.ex index a69eb7c..650a95d 100644 --- a/lib/haj/forms/response.ex +++ b/lib/haj/forms/response.ex @@ -5,7 +5,7 @@ defmodule Haj.Forms.Response do schema "form_responses" do belongs_to :form, Haj.Forms.Form belongs_to :user, Haj.Accounts.User - belongs_to :event_registration, Haj.Events.EventRegistration + has_one :event_registration, Haj.Events.EventRegistration has_many :question_responses, Haj.Forms.QuestionResponse diff --git a/lib/haj/policy/policy.ex b/lib/haj/policy/policy.ex index 4eb79ad..f5986bf 100644 --- a/lib/haj/policy/policy.ex +++ b/lib/haj/policy/policy.ex @@ -102,4 +102,11 @@ defmodule Haj.Policy do allow role: :admin end end + + object :event_registrations do + action :read do + allow role: :admin + allow current_group_member: :chefsgruppen + end + end end diff --git a/lib/haj_web/live/event_live/form_component.ex b/lib/haj_web/live/event_live/form_component.ex index ef4361d..b8ca57c 100644 --- a/lib/haj_web/live/event_live/form_component.ex +++ b/lib/haj_web/live/event_live/form_component.ex @@ -1,54 +1,180 @@ defmodule HajWeb.EventLive.FormComponent do use HajWeb, :live_component alias Haj.Forms + alias Haj.Events @impl true def render(assigns) do ~H"""
    - <.form for={@response_form} phx-submit="save" phx-change="validate" phx-target={@myself}> + <.header> + <%= @form.name %> + <:subtitle><%= @form.description %> + + <.form + for={@response_form} + phx-submit="save" + phx-change="validate" + phx-target={@myself} + class="mt-4 flex flex-col gap-4" + > + <.input + name="attending" + type="select" + options={[{"Ja", true}, {"Nej", false}]} + label="Kommer du" + value={@attending} + /> + <.form_input :for={question <- @form.questions} question={question} field={@response_form[String.to_atom("#{question.id}")]} /> - <.button phx-disable-with="Sparar...">Spara +
    + <.button phx-disable-with="Sparar...">Spara +
    """ end @impl true - def update(%{event: event} = assigns, socket) do - changeset = Forms.get_form_changeset!(event.form_id, %{}) - form = Forms.get_form!(event.form_id) + def update(%{event: event, current_user: user} = assigns, socket) do + registration = Events.get_registration_for_user(event.id, user.id) + attending = registration && registration.attending && true + + if event.form do + form_response = Forms.get_response_for_user(event.form_id, user.id) + + changeset = Forms.get_form_changeset!(event.form_id, form_response || %{}) + form = Forms.get_form!(event.form_id) - {:ok, assign(socket, assigns) |> assign(form: form) |> assign_form(changeset)} + {:ok, + assign(socket, assigns) + |> assign(registration: registration, form: form, attending: attending) + |> assign_form(changeset)} + else + {:ok, + assign(socket, assigns) + |> assign( + registration: registration, + attending: attending, + form: %{name: "Anmälan", description: "Anmäl dig till #{event.name}", questions: []}, + response_form: to_form(%{}) + )} + end + end + + @impl true + def handle_event( + "validate", + %{"attending" => attending}, + socket + ) do + {:noreply, assign(socket, attending: attending)} end @impl true - def handle_event("validate", %{"form_response" => response}, socket) do + def handle_event( + "validate", + %{"form_response" => response, "attending" => attending}, + socket + ) do # We need to flatten all multi-responses to a list response = flatten_response(response) changeset = Forms.get_form_changeset!(socket.assigns.form.id, response) |> Map.put(:action, :validate) - {:noreply, assign_form(socket, changeset)} + {:noreply, assign(socket, attending: attending) |> assign_form(changeset)} end @impl true - def handle_event("save", %{"form_response" => response}, socket) do + def handle_event("save", %{"form_response" => response, "attending" => attending}, socket) do response = flatten_response(response) - case Forms.submit_form(socket.assigns.form.id, socket.assigns.current_user.id, response) do + {:noreply, assign(socket, attending: attending) |> save(socket.assigns.registration)} + end + + @impl true + def handle_event("save", %{"attending" => attending}, socket) do + {:noreply, assign(socket, attending: attending) |> save(socket.assigns.registration)} + end + + defp save(socket, nil, response) do + %{current_user: user, event: event, attending: attending} = socket.assigns + + with {:ok, form_response} <- Forms.submit_form(socket.assigns.form.id, user.id, response), + {:ok, _} <- + Events.create_event_registration(%{ + event_id: event.id, + user_id: user.id, + form_response_id: form_response[:response].id, + attending: attending + }) do + push_flash(:info, "Du är nu anmäld!") + push_patch(socket, to: socket.assigns.patch) + else + {:error, changeset} -> + push_flash(:error, "Något gick fel.") + assign_form(socket, changeset) + end + end + + defp save(socket, registration, response) do + %{current_user: user, attending: attending} = socket.assigns + + with previous when not is_nil(previous) <- + Forms.get_response_for_user(socket.assigns.form.id, user.id), + {:ok, _} <- + Forms.update_form_response(socket.assigns.form.id, previous, response), + {:ok, _} <- + Events.update_event_registration(registration, %{ + form_response_id: previous.id, + attending: attending + }) do + push_flash(:info, "Uppdaterade anmälan!") + push_patch(socket, to: socket.assigns.patch) + else + nil -> + push_flash(:error, "Något gick fel.") + socket + + {:error, changeset} -> + push_flash(:error, "Något gick fel.") + assign_form(socket, changeset) + end + end + + defp save(socket, nil) do + %{current_user: user, event: event, attending: attending} = socket.assigns + + case Events.create_event_registration(%{ + event_id: event.id, + user_id: user.id, + attending: attending + }) do + {:ok, _} -> + push_flash(:info, "Du är nu anmäld!") + push_patch(socket, to: socket.assigns.patch) + + {:error, changeset} -> + push_flash(:error, "Något gick fel.") + assign_form(socket, changeset) + end + end + + defp save(socket, registration) do + case Events.update_event_registration(registration, %{attending: socket.assigns.attending}) do {:ok, _} -> - push_flash(:info, "Skickade svar") - {:noreply, socket} + push_flash(:info, "Du är nu anmäld!") + push_patch(socket, to: socket.assigns.patch) {:error, changeset} -> - {:noreply, assign_form(socket, changeset)} + push_flash(:error, "Något gick fel.") + assign_form(socket, changeset) end end diff --git a/lib/haj_web/live/event_live/index.html.heex b/lib/haj_web/live/event_live/index.html.heex index 5a8beda..c336964 100644 --- a/lib/haj_web/live/event_live/index.html.heex +++ b/lib/haj_web/live/event_live/index.html.heex @@ -1,5 +1,5 @@
    -
    +
    Events Partaj och annat skoj
    diff --git a/lib/haj_web/live/event_live/registrations.ex b/lib/haj_web/live/event_live/registrations.ex new file mode 100644 index 0000000..4ac87c6 --- /dev/null +++ b/lib/haj_web/live/event_live/registrations.ex @@ -0,0 +1,85 @@ +defmodule HajWeb.EventLive.Registrations do + use HajWeb, :live_view + + on_mount {HajWeb.UserAuth, {:authorize, :event_registrations_read}} + + alias Haj.Events + + @impl true + def mount(%{"id" => _}, _session, socket) do + {:ok, socket} + end + + @impl true + def handle_params(%{"id" => id} = params, _, socket) do + event = Events.get_event!(id, form: [:questions]) + registrations = Events.list_event_registrations(id) + + {:noreply, + assign(socket, event: event, registrations: registrations) + |> apply_action(socket.assigns.live_action, params)} + end + + defp apply_action(socket, :users, _params), do: assign(socket, page_title: "Anmälda") + + defp apply_action(socket, :food, _params) do + food_counts = + Enum.flat_map(socket.assigns.registrations, & &1.user.foods) + |> Enum.group_by(& &1.id, & &1.name) + |> Enum.flat_map(fn {key, values} -> Enum.frequencies(values) end) + |> Enum.sort_by(fn c -> c end, &>=/2) + + special_users = + Enum.map(socket.assigns.registrations, & &1.user) + |> Enum.reject(&(&1.food_preference_other == nil)) + + assign(socket, food_counts: food_counts, special_food_users: special_users) + end + + defp apply_action(socket, :questions, _params) do + response_counts = + Enum.map(socket.assigns.registrations, & &1.response) + |> Enum.reject(&is_nil/1) + |> Enum.flat_map(& &1.question_responses) + |> Enum.group_by(& &1.question_id, &(&1.answer || &1.multi_answer)) + |> Enum.map(fn {key, values} -> + {key, Enum.frequencies(List.flatten(values)) |> Enum.sort_by(fn {_, c} -> c end, &>=/2)} + end) + |> Enum.into(%{}) + + group_counts = + Enum.flat_map(socket.assigns.registrations, & &1.user.group_memberships) + |> Enum.map(& &1.show_group.group) + |> Enum.frequencies() + |> Enum.sort_by(fn {_, c} -> c end, &>=/2) + + class_counts = + Enum.map(socket.assigns.registrations, & &1.user.class) + |> Enum.frequencies() + |> Map.drop([nil]) + |> Enum.sort_by(fn {_, c} -> c end, &>=/2) + + assign(socket, + response_counts: response_counts, + group_counts: group_counts, + class_counts: class_counts + ) + end + + defp tab(assigns) do + ~H""" + <.link + patch={@navigate} + class={ + if @active, + do: + "whitespace-nowrap border-b-2 border-gray-500 px-1 py-4 text-sm font-medium text-gray-600", + else: + "whitespace-nowrap border-b-2 border-transparent px-1 py-4 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700" + } + > + <%= @name %> + + """ + end +end diff --git a/lib/haj_web/live/event_live/registrations.html.heex b/lib/haj_web/live/event_live/registrations.html.heex new file mode 100644 index 0000000..bc5ed08 --- /dev/null +++ b/lib/haj_web/live/event_live/registrations.html.heex @@ -0,0 +1,141 @@ +Anmälningar + + + +
    + +
    + + +<.table :if={@live_action == :users} small id="registrations_table" rows={@registrations}> + <:col :let={reg} label="Namn"> + <%= reg.user.full_name %> + + + <:col :let={reg} label="Kommer" class="hidden sm:table-cell"> + <%= if reg.attending, do: "Ja", else: "Nej" %> + + + <:col :let={reg} label="Anmälningstid" class="hidden sm:table-cell"> + <%= reg.inserted_at %> + + + <:col :let={reg} label="Grupper" class="hidden md:table-cell"> +
    + <.link :for={gm <- reg.user.group_memberships} navigate={~p"/group/#{gm.show_group_id}"}> +
    + <%= gm.show_group.group.name %> +
    + +
    + + + +
    +
    + Grupp +
    +
    +
    + <%= count %> +
    +
    + <%= group.name %> +
    +
    +
    +
    + +
    + Klass +
    +
    +
    + <%= count %> +
    +
    + <%= class %> +
    +
    +
    +
    + + <%= if @event.form do %> +
    + <%= question.name %> +
    +
    +
    + <%= count %> +
    +
    + <%= answer %> +
    +
    +
    +
    + <% end %> +
    + +
    + + Inga anmälda med matpreferenser + + + + +
    0} class="mt-4 rounded-lg border p-4"> + Övriga + + <.table + small + id="food_preferences_table" + rows={@special_food_users} + row_click={fn user -> JS.navigate(~p"/user/#{user.username}") end} + > + <:col :let={user} label="Namn"> + <%= user.full_name %> + + + <:col :let={user} label="Preferens" class=""> + <%= user.food_preference_other %> + + +
    +
    diff --git a/lib/haj_web/live/event_live/show.ex b/lib/haj_web/live/event_live/show.ex index 904dcf4..bf56575 100644 --- a/lib/haj_web/live/event_live/show.ex +++ b/lib/haj_web/live/event_live/show.ex @@ -33,7 +33,10 @@ defmodule HajWeb.EventLive.Show do @impl true def handle_params(%{"id" => id}, _url, socket) do - {:noreply, assign(socket, event: Events.get_event!(id))} + registration = Events.get_registration_for_user(id, socket.assigns.current_user.id) + event = Events.get_event!(id) + + {:noreply, assign(socket, page_title: event.name, event: event, registration: registration)} end @impl true @@ -69,8 +72,7 @@ defmodule HajWeb.EventLive.Show do @impl true def handle_event("register", _, socket) do if socket.assigns.event.has_tickets do - selected = Enum.filter(socket.assigns.selected, fn {_, count} -> count > 0 end) - IO.inspect(selected) + _selected = Enum.filter(socket.assigns.selected, fn {_, count} -> count > 0 end) {:noreply, socket} else {:noreply, push_patch(socket, to: ~p"/events/#{socket.assigns.event}/register")} diff --git a/lib/haj_web/live/event_live/show.html.heex b/lib/haj_web/live/event_live/show.html.heex index becafc5..bb11bcf 100644 --- a/lib/haj_web/live/event_live/show.html.heex +++ b/lib/haj_web/live/event_live/show.html.heex @@ -5,7 +5,7 @@ /> {"Picture
    -
    +
    <%!-- Main content --%>
    @@ -83,11 +83,27 @@
    <.button class="mt-auto" phx-click="register"> - Köp biljetter - Anmälan + <%= case {@registration, @event.has_tickets} do %> + <% {%Haj.Events.EventRegistration{}, _} -> %> + Ändra anmälan + <% {_, true} -> %> + Köp biljetter + <% {_, false} -> %> + Anmälan + <% end %>
    +
    + <.link + :if={Haj.Policy.authorize?(:event_registrations_read, @current_user)} + navigate={~p"/events/#{@event}/registrations"} + > + <.button> + Anmälningar + + +
    diff --git a/lib/haj_web/live/settings_live/form/form_component.ex b/lib/haj_web/live/settings_live/form/form_component.ex index 54ca359..51b45ae 100644 --- a/lib/haj_web/live/settings_live/form/form_component.ex +++ b/lib/haj_web/live/settings_live/form/form_component.ex @@ -124,10 +124,7 @@ defmodule HajWeb.SettingsLive.Form.FormComponent do @impl true def handle_event("validate", %{"form" => form_params}, socket) do - params = - form_params - |> merge_options() - |> dbg() + params = merge_options(form_params) changeset = socket.assigns.form @@ -172,9 +169,7 @@ defmodule HajWeb.SettingsLive.Form.FormComponent do end defp assign_form(socket, %Ecto.Changeset{} = changeset) do - form = - to_form(changeset) - |> IO.inspect(label: "assign_form") + form = to_form(changeset) assign(socket, :client_form, form) end diff --git a/lib/haj_web/router.ex b/lib/haj_web/router.ex index 98d1c9a..46c5a17 100644 --- a/lib/haj_web/router.ex +++ b/lib/haj_web/router.ex @@ -105,6 +105,11 @@ defmodule HajWeb.Router do live "/events/:id", EventLive.Show, :index live "/events/:id/register", EventLive.Show, :register + live "/events/:id/registrations", EventLive.Registrations, :users + live "/events/:id/registrations/users", EventLive.Registrations, :users + live "/events/:id/registrations/questions", EventLive.Registrations, :questions + live "/events/:id/registrations/food", EventLive.Registrations, :food + live "/forms/:id", FormLive.Index, :index end diff --git a/priv/repo/migrations/20240211033841_add_event_forms.exs b/priv/repo/migrations/20240211033841_add_event_forms.exs index 1296939..6ce29a7 100644 --- a/priv/repo/migrations/20240211033841_add_event_forms.exs +++ b/priv/repo/migrations/20240211033841_add_event_forms.exs @@ -7,7 +7,7 @@ defmodule Haj.Repo.Migrations.AddEventForms do end alter table(:event_registrations) do - add :form_response_id, references(:form_responses, on_delete: :nilify_all) + add :response_id, references(:form_responses, on_delete: :nilify_all) end create index(:event_registrations, [:form_response_id]) diff --git a/priv/repo/migrations/20240212001204_add_attending_field.exs b/priv/repo/migrations/20240212001204_add_attending_field.exs new file mode 100644 index 0000000..7d49b34 --- /dev/null +++ b/priv/repo/migrations/20240212001204_add_attending_field.exs @@ -0,0 +1,9 @@ +defmodule Haj.Repo.Migrations.AddAttendingField do + use Ecto.Migration + + def change do + alter table(:event_registrations) do + add :attending, :boolean, default: false + end + end +end From e5fa6538e37c95250f3dd68b1ee191284d378f50 Mon Sep 17 00:00:00 2001 From: Adrian Salamon Date: Mon, 12 Feb 2024 00:08:34 -0800 Subject: [PATCH 30/56] fix broken things --- lib/haj/events.ex | 2 -- lib/haj_web/live/event_live/form_component.ex | 23 +++++++------- .../live/event_live/registrations.html.heex | 30 +++++++++++++------ .../20240211033841_add_event_forms.exs | 2 +- 4 files changed, 34 insertions(+), 23 deletions(-) diff --git a/lib/haj/events.ex b/lib/haj/events.ex index c69eb2d..bacd041 100644 --- a/lib/haj/events.ex +++ b/lib/haj/events.ex @@ -364,8 +364,6 @@ defmodule Haj.Events do from r in EventRegistration, where: r.event_id == ^event_id and r.user_id == ^user_id - # preload: [form_response: [question_responses: []]] - Repo.one(query) end diff --git a/lib/haj_web/live/event_live/form_component.ex b/lib/haj_web/live/event_live/form_component.ex index b8ca57c..e3399fe 100644 --- a/lib/haj_web/live/event_live/form_component.ex +++ b/lib/haj_web/live/event_live/form_component.ex @@ -67,15 +67,6 @@ defmodule HajWeb.EventLive.FormComponent do end end - @impl true - def handle_event( - "validate", - %{"attending" => attending}, - socket - ) do - {:noreply, assign(socket, attending: attending)} - end - @impl true def handle_event( "validate", @@ -91,11 +82,21 @@ defmodule HajWeb.EventLive.FormComponent do {:noreply, assign(socket, attending: attending) |> assign_form(changeset)} end + @impl true + def handle_event( + "validate", + %{"attending" => attending}, + socket + ) do + {:noreply, assign(socket, attending: attending)} + end + @impl true def handle_event("save", %{"form_response" => response, "attending" => attending}, socket) do response = flatten_response(response) - {:noreply, assign(socket, attending: attending) |> save(socket.assigns.registration)} + {:noreply, + assign(socket, attending: attending) |> save(socket.assigns.registration, response)} end @impl true @@ -111,7 +112,7 @@ defmodule HajWeb.EventLive.FormComponent do Events.create_event_registration(%{ event_id: event.id, user_id: user.id, - form_response_id: form_response[:response].id, + response_id: form_response[:response].id, attending: attending }) do push_flash(:info, "Du är nu anmäld!") diff --git a/lib/haj_web/live/event_live/registrations.html.heex b/lib/haj_web/live/event_live/registrations.html.heex index bc5ed08..60831a7 100644 --- a/lib/haj_web/live/event_live/registrations.html.heex +++ b/lib/haj_web/live/event_live/registrations.html.heex @@ -74,6 +74,9 @@ <%= group.name %>
    +
    + Inga svar +
    @@ -88,6 +91,9 @@ <%= class %>
    +
    + Inga svar +
    @@ -95,17 +101,23 @@
    <%= question.name %>
    -
    -
    - <%= count %> + <%= if @response_counts[question.id] do %> +
    +
    + <%= count %> +
    +
    + <%= answer %> +
    -
    - <%= answer %> + <% else %> +
    + Inga svar
    -
    + <% end %>
    <% end %> diff --git a/priv/repo/migrations/20240211033841_add_event_forms.exs b/priv/repo/migrations/20240211033841_add_event_forms.exs index 6ce29a7..416a47a 100644 --- a/priv/repo/migrations/20240211033841_add_event_forms.exs +++ b/priv/repo/migrations/20240211033841_add_event_forms.exs @@ -10,7 +10,7 @@ defmodule Haj.Repo.Migrations.AddEventForms do add :response_id, references(:form_responses, on_delete: :nilify_all) end - create index(:event_registrations, [:form_response_id]) + create index(:event_registrations, [:response_id]) create index(:events, [:form_id]) end end From 8a44fdc610c965b83415bc88a8cca2ebbd08ae97 Mon Sep 17 00:00:00 2001 From: Adrian Salamon Date: Mon, 12 Feb 2024 00:54:44 -0800 Subject: [PATCH 31/56] mobile friendly --- lib/haj_web/live/event_live/registrations.ex | 27 +++++++++++++++++-- .../live/event_live/registrations.html.heex | 23 ++++++++++------ 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/lib/haj_web/live/event_live/registrations.ex b/lib/haj_web/live/event_live/registrations.ex index 4ac87c6..71f79e7 100644 --- a/lib/haj_web/live/event_live/registrations.ex +++ b/lib/haj_web/live/event_live/registrations.ex @@ -15,16 +15,39 @@ defmodule HajWeb.EventLive.Registrations do event = Events.get_event!(id, form: [:questions]) registrations = Events.list_event_registrations(id) + most_popular = + Enum.flat_map(registrations, & &1.user.group_memberships) + |> Enum.map(& &1.show_group.group) + |> Enum.frequencies() + |> Enum.sort_by(fn {_, c} -> c end, &>=/2) + |> Enum.take(1) + |> Enum.map(fn {group, _} -> group.name end) + {:noreply, - assign(socket, event: event, registrations: registrations) + assign(socket, event: event, registrations: registrations, most_popular: most_popular) |> apply_action(socket.assigns.live_action, params)} end + @impl true + def handle_event("select_tab", %{"tab" => tab}, socket) do + event = socket.assigns.event + + path = + case tab do + "users" -> ~p"/events/#{event}/registrations/users" + "food" -> ~p"/events/#{event}/registrations/food" + "questions" -> ~p"/events/#{event}/registrations/questions" + end + + {:noreply, push_patch(socket, to: path)} + end + defp apply_action(socket, :users, _params), do: assign(socket, page_title: "Anmälda") defp apply_action(socket, :food, _params) do food_counts = - Enum.flat_map(socket.assigns.registrations, & &1.user.foods) + Enum.filter(socket.assigns.registrations, & &1.attending) + |> Enum.flat_map(& &1.user.foods) |> Enum.group_by(& &1.id, & &1.name) |> Enum.flat_map(fn {key, values} -> Enum.frequencies(values) end) |> Enum.sort_by(fn c -> c end, &>=/2) diff --git a/lib/haj_web/live/event_live/registrations.html.heex b/lib/haj_web/live/event_live/registrations.html.heex index 60831a7..56ac3d5 100644 --- a/lib/haj_web/live/event_live/registrations.html.heex +++ b/lib/haj_web/live/event_live/registrations.html.heex @@ -3,14 +3,21 @@ -
    - +
    + <.form phx-change="select_tab"> + <.input + type="select" + name="tab" + options={[{"Anmälda", :users}, {"Frågor", :questions}, {"Matpreferenser", :food}]} + value={@live_action} + /> +
    """ diff --git a/lib/haj_web/live/settings_live/song/form_component.ex b/lib/haj_web/live/song_live/edit/form_component.ex similarity index 99% rename from lib/haj_web/live/settings_live/song/form_component.ex rename to lib/haj_web/live/song_live/edit/form_component.ex index 6ce3538..9297b03 100644 --- a/lib/haj_web/live/settings_live/song/form_component.ex +++ b/lib/haj_web/live/song_live/edit/form_component.ex @@ -1,4 +1,4 @@ -defmodule HajWeb.SettingsLive.Song.FormComponent do +defmodule HajWeb.SongLive.Edit.FormComponent do use HajWeb, :live_component alias Haj.Archive diff --git a/lib/haj_web/live/settings_live/song/index.ex b/lib/haj_web/live/song_live/edit/index.ex similarity index 85% rename from lib/haj_web/live/settings_live/song/index.ex rename to lib/haj_web/live/song_live/edit/index.ex index d5074a7..a495866 100644 --- a/lib/haj_web/live/settings_live/song/index.ex +++ b/lib/haj_web/live/song_live/edit/index.ex @@ -1,10 +1,12 @@ -defmodule HajWeb.SettingsLive.Song.Index do +defmodule HajWeb.SongLive.Edit.Index do use HajWeb, :live_view alias Haj.Archive alias Haj.Archive.Song alias Haj.Repo + on_mount {HajWeb.UserAuth, {:authorize, :songs_edit}} + @impl true def mount(_params, _session, socket) do {:ok, stream(socket, :songs, Archive.list_songs() |> Repo.preload(:show))} @@ -34,7 +36,7 @@ defmodule HajWeb.SettingsLive.Song.Index do end @impl true - def handle_info({HajWeb.SettingsLive.Song.FormComponent, {:saved, song}}, socket) do + def handle_info({HajWeb.SongLive.Edit.FormComponent, {:saved, song}}, socket) do {:noreply, stream_insert(socket, :songs, song |> Repo.preload(:show))} end diff --git a/lib/haj_web/live/settings_live/song/index.html.heex b/lib/haj_web/live/song_live/edit/index.html.heex similarity index 71% rename from lib/haj_web/live/settings_live/song/index.html.heex rename to lib/haj_web/live/song_live/edit/index.html.heex index 539c8a4..a7f8f10 100644 --- a/lib/haj_web/live/settings_live/song/index.html.heex +++ b/lib/haj_web/live/song_live/edit/index.html.heex @@ -1,7 +1,7 @@ <.header> Listing Songs <:actions> - <.link patch={~p"/settings/songs/new"}> + <.link patch={~p"/songs/edit/new"}> <.button>New Song @@ -10,7 +10,7 @@ <.table id="songs" rows={@streams.songs} - row_click={fn {_id, song} -> JS.navigate(~p"/settings/songs/#{song}") end} + row_click={fn {_id, song} -> JS.navigate(~p"/songs/edit/#{song}") end} > <:col :let={{_id, song}} label="Name"><%= song.name %> <:col :let={{_id, song}} label="Original name"><%= song.original_name %> @@ -18,9 +18,9 @@ <:col :let={{_id, song}} label="Spex"><%= song.show.year.year %> <:action :let={{_id, song}}>
    - <.link navigate={~p"/settings/songs/#{song}"}>Show + <.link navigate={~p"/songs/edit/#{song}"}>Show
    - <.link patch={~p"/settings/songs/#{song}/edit"}>Edit + <.link patch={~p"/songs/edit/#{song}/edit"}>Edit <:action :let={{id, song}}> <.link @@ -36,14 +36,14 @@ :if={@live_action in [:new, :edit]} id="song-modal" show - on_cancel={JS.navigate(~p"/settings/songs")} + on_cancel={JS.navigate(~p"/songs/edit")} > <.live_component - module={HajWeb.SettingsLive.Song.FormComponent} + module={HajWeb.SongLive.Edit.FormComponent} id={@song.id || :new} title={@page_title} action={@live_action} song={@song} - patch={~p"/settings/songs"} + patch={~p"/songs/edit"} /> diff --git a/lib/haj_web/live/settings_live/song/show.ex b/lib/haj_web/live/song_live/edit/show.ex similarity index 81% rename from lib/haj_web/live/settings_live/song/show.ex rename to lib/haj_web/live/song_live/edit/show.ex index 38c635e..236cbb9 100644 --- a/lib/haj_web/live/settings_live/song/show.ex +++ b/lib/haj_web/live/song_live/edit/show.ex @@ -1,7 +1,8 @@ -defmodule HajWeb.SettingsLive.Song.Show do +defmodule HajWeb.SongLive.Edit.Show do use HajWeb, :live_view alias Haj.Archive + on_mount {HajWeb.UserAuth, {:authorize, :songs_edit}} @impl true def mount(_params, _session, socket) do diff --git a/lib/haj_web/live/settings_live/song/show.html.heex b/lib/haj_web/live/song_live/edit/show.html.heex similarity index 71% rename from lib/haj_web/live/settings_live/song/show.html.heex rename to lib/haj_web/live/song_live/edit/show.html.heex index 1b8b5b7..bd2fee1 100644 --- a/lib/haj_web/live/settings_live/song/show.html.heex +++ b/lib/haj_web/live/song_live/edit/show.html.heex @@ -2,7 +2,7 @@ Song <%= @song.id %> <:subtitle>This is a song record from your database. <:actions> - <.link patch={~p"/settings/songs/#{@song}/show/edit"} phx-click={JS.push_focus()}> + <.link patch={~p"/songs/edit/#{@song}/show/edit"} phx-click={JS.push_focus()}> <.button>Edit song @@ -22,20 +22,20 @@
    -<.back navigate={~p"/settings/songs"}>Back to songs +<.back navigate={~p"/songs/edit"}>Back to songs <.modal :if={@live_action == :edit} id="song-modal" show - on_cancel={JS.patch(~p"/settings/songs/#{@song}")} + on_cancel={JS.patch(~p"/songs/edit/#{@song}")} > <.live_component - module={HajWeb.SettingsLive.Song.FormComponent} + module={HajWeb.SongLive.Edit.FormComponent} id={@song.id} title={@page_title} action={@live_action} song={@song} - patch={~p"/settings/songs/#{@song}"} + patch={~p"/songs/edit/#{@song}"} /> diff --git a/lib/haj_web/live/song_live/index.ex b/lib/haj_web/live/song_live/index.ex index 06a9499..ac52aba 100644 --- a/lib/haj_web/live/song_live/index.ex +++ b/lib/haj_web/live/song_live/index.ex @@ -3,6 +3,7 @@ defmodule HajWeb.SongLive.Index do alias Haj.Archive alias Haj.Spex + alias Haj.Policy def mount(_params, _session, socket) do show = Spex.current_spex() @@ -14,8 +15,16 @@ defmodule HajWeb.SongLive.Index do [key: "#{year.year}: #{title}", value: id] end) + authorized_admin = Policy.authorize?(:songs_edit, socket.assigns.current_user) + {:ok, - assign(socket, page_title: "Sånger", songs: songs, show: show, show_options: show_options)} + assign(socket, + page_title: "Sånger", + songs: songs, + show: show, + show_options: show_options, + authorized_admin: authorized_admin + )} end def handle_event("select_show", %{"show" => show_id}, socket) do diff --git a/lib/haj_web/live/song_live/index.html.heex b/lib/haj_web/live/song_live/index.html.heex index b12aa0a..d642cb8 100644 --- a/lib/haj_web/live/song_live/index.html.heex +++ b/lib/haj_web/live/song_live/index.html.heex @@ -1,8 +1,20 @@
    - Sångarkiv - - Här finns alla sånger som har gjorts i spexet genom åren. Kika och lyssna in! - +
    +
    + Sångarkiv + + Här finns alla sånger som har gjorts i spexet genom åren. Kika och lyssna in! + +
    + + <.link + :if={@authorized_admin} + navigate={~p"/songs/edit"} + class="text-md bg-burgandy-500 block rounded-md px-3 py-1 text-sm text-white hover:bg-burgandy-400" + > + Administrera + +
    <.form id="show-form" as={:show} for={%{}} phx-change="select_show" class="w-full pt-2"> <.input name="show" label="Välj spex" type="select" value={@show.id} options={@show_options} /> diff --git a/lib/haj_web/router.ex b/lib/haj_web/router.ex index 0095412..45f68f0 100644 --- a/lib/haj_web/router.ex +++ b/lib/haj_web/router.ex @@ -92,6 +92,13 @@ defmodule HajWeb.Router do live "/applications/:id", ApplicationsLive.Show, :show live "/applications/:id/confirm", ApplicationsLive.Show, :approve + ## Song administration + live "/songs/edit", SongLive.Edit.Index, :index + live "/songs/edit/new", SongLive.Edit.Index, :new + live "/songs/edit/:id/edit", SongLive.Edit.Index, :edit + live "/songs/edit/:id", SongLive.Edit.Show, :show + live "/songs/edit/:id/show/edit", SongLive.Edit.Show, :edit + ## Songs live "/songs", SongLive.Index, :index live "/songs/:id", SongLive.Show, :show @@ -154,13 +161,6 @@ defmodule HajWeb.Router do :new_responsible live "/responsibilities/:id/show/edit", SettingsLive.Responsibility.Show, :edit - - live "/songs", SettingsLive.Song.Index, :index - live "/songs/new", SettingsLive.Song.Index, :new - live "/songs/:id/edit", SettingsLive.Song.Index, :edit - - live "/songs/:id", SettingsLive.Song.Show, :show - live "/songs/:id/show/edit", SettingsLive.Song.Show, :edit end end end diff --git a/test/haj_web/live/song_live_test.exs b/test/haj_web/live/song_live_test.exs deleted file mode 100644 index 493ab0b..0000000 --- a/test/haj_web/live/song_live_test.exs +++ /dev/null @@ -1,123 +0,0 @@ -defmodule HajWeb.SettingsLive.SongTest do - use HajWeb.ConnCase - - import Phoenix.LiveViewTest - import Haj.ArchiveFixtures - - @create_attrs %{ - name: "some name", - number: 42, - original_name: "some original_name", - text: "some text" - } - @update_attrs %{ - name: "some updated name", - number: 43, - original_name: "some updated original_name", - text: "some updated text" - } - @invalid_attrs %{name: nil, number: nil, original_name: nil, text: nil} - - defp create_song(_) do - song = song_fixture() - %{song: song} - end - - describe "Index" do - setup [:create_song] - - test "lists all songs", %{conn: conn, song: song} do - {:ok, _index_live, html} = live(conn, ~p"/songs") - - assert html =~ "Listing Songs" - assert html =~ song.name - end - - test "saves new song", %{conn: conn} do - {:ok, index_live, _html} = live(conn, ~p"/songs") - - assert index_live |> element("a", "New Song") |> render_click() =~ - "New Song" - - assert_patch(index_live, ~p"/songs/new") - - assert index_live - |> form("#song-form", song: @invalid_attrs) - |> render_change() =~ "can't be blank" - - assert index_live - |> form("#song-form", song: @create_attrs) - |> render_submit() - - assert_patch(index_live, ~p"/songs") - - html = render(index_live) - assert html =~ "Song created successfully" - assert html =~ "some name" - end - - test "updates song in listing", %{conn: conn, song: song} do - {:ok, index_live, _html} = live(conn, ~p"/songs") - - assert index_live |> element("#songs-#{song.id} a", "Edit") |> render_click() =~ - "Edit Song" - - assert_patch(index_live, ~p"/songs/#{song}/edit") - - assert index_live - |> form("#song-form", song: @invalid_attrs) - |> render_change() =~ "can't be blank" - - assert index_live - |> form("#song-form", song: @update_attrs) - |> render_submit() - - assert_patch(index_live, ~p"/songs") - - html = render(index_live) - assert html =~ "Song updated successfully" - assert html =~ "some updated name" - end - - test "deletes song in listing", %{conn: conn, song: song} do - {:ok, index_live, _html} = live(conn, ~p"/songs") - - assert index_live |> element("#songs-#{song.id} a", "Delete") |> render_click() - refute has_element?(index_live, "#songs-#{song.id}") - end - end - - describe "Show" do - setup [:create_song] - - test "displays song", %{conn: conn, song: song} do - {:ok, _show_live, html} = live(conn, ~p"/songs/#{song}") - - assert html =~ "Show Song" - assert html =~ song.name - end - - test "updates song within modal", %{conn: conn, song: song} do - {:ok, show_live, _html} = live(conn, ~p"/songs/#{song}") - - assert show_live |> element("a", "Edit") |> render_click() =~ - "Edit Song" - - assert_patch(show_live, ~p"/songs/#{song}/show/edit") - - assert show_live - |> form("#song-form", song: @invalid_attrs) - |> render_change() =~ "can't be blank" - - assert show_live - |> form("#song-form", song: @update_attrs) - |> render_submit() - - assert_patch(show_live, ~p"/songs/#{song}") - - html = render(show_live) - assert html =~ "Song updated successfully" - assert html =~ "some updated name" - end - end -end From 28246574d20741f62a71241ab69943253a111b31 Mon Sep 17 00:00:00 2001 From: Adrian Salamon Date: Sun, 15 Sep 2024 09:42:28 +0200 Subject: [PATCH 37/56] fixes for 2025 application (#83) --- lib/haj/applications.ex | 2 +- lib/haj/spex.ex | 3 +-- lib/haj_web/live/apply_live/complete.ex | 2 +- lib/haj_web/live/apply_live/groups.ex | 3 +-- lib/haj_web/live/group_live/admin.html.heex | 6 +++++- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/haj/applications.ex b/lib/haj/applications.ex index e5d82f3..ad7d091 100644 --- a/lib/haj/applications.ex +++ b/lib/haj/applications.ex @@ -248,7 +248,7 @@ defmodule Haj.Applications do är du välkommen att kontakta Direqtionen på direqtionen@metaspexet.se.

    Hälsningar,

    - Chefsgruppen för METAspexet 2024 + Chefsgruppen för METAspexet #{spex.year.year} """ end diff --git a/lib/haj/spex.ex b/lib/haj/spex.ex index 66c0cfc..6738868 100644 --- a/lib/haj/spex.ex +++ b/lib/haj/spex.ex @@ -7,6 +7,7 @@ defmodule Haj.Spex do alias Haj.Repo alias Haj.Spex.Show + alias Haj.Spex.GroupMembership @doc """ Returns the list of shows. @@ -198,8 +199,6 @@ defmodule Haj.Spex do Group.changeset(group, attrs) end - alias Haj.Spex.GroupMembership - def member_of_spex?(show, user) do query = from gm in GroupMembership, diff --git a/lib/haj_web/live/apply_live/complete.ex b/lib/haj_web/live/apply_live/complete.ex index 935ae3c..5f802c3 100644 --- a/lib/haj_web/live/apply_live/complete.ex +++ b/lib/haj_web/live/apply_live/complete.ex @@ -111,7 +111,7 @@ defmodule HajWeb.ApplyLive.Complete do required class="mr-2 rounded border-zinc-300 text-zinc-900 focus:ring-zinc-900" /> - Jag godkänner att de här uppgiferna lagras av METAspexet i syfte för rekrytering enligt GDPR och kommer tas bort efter rekryteringen är färdig, senast 1a Januari 2024. + Jag godkänner att de här uppgiferna lagras av METAspexet i syfte för rekrytering enligt GDPR och kommer tas bort efter rekryteringen är färdig, senast 1a Januari 2025.
    diff --git a/lib/haj_web/live/apply_live/groups.ex b/lib/haj_web/live/apply_live/groups.ex index 2337424..bc2569e 100644 --- a/lib/haj_web/live/apply_live/groups.ex +++ b/lib/haj_web/live/apply_live/groups.ex @@ -41,8 +41,7 @@ defmodule HajWeb.ApplyLive.Groups do groups = Spex.get_show_groups_for_show(current_spex.id) |> Enum.filter(fn %{application_open: o} -> o end) - # Temp manus fix, should be removed - |> Enum.sort_by(fn %{group: %{name: n}} -> n == "Manus" end, :desc) + socket = if pre_filled? do diff --git a/lib/haj_web/live/group_live/admin.html.heex b/lib/haj_web/live/group_live/admin.html.heex index f0426f2..f119f06 100644 --- a/lib/haj_web/live/group_live/admin.html.heex +++ b/lib/haj_web/live/group_live/admin.html.heex @@ -2,7 +2,11 @@ Redigera <%= @page_title %> <.simple_form for={@form} phx-submit="save" class="flex flex-col pb-2"> - <.input field={@form[:application_description]} label="Beskrivning av gruppen" type="textarea" /> + <.input + field={@form[:application_description]} + label="Beskrivning av gruppen på ansökningssidan" + type="textarea" + /> <.input field={@form[:application_extra_question]} label="Extra fråga i ansökan. Om du lämnar detta blankt kommer ingen extra fråga visas." From 335333f4927b4907354f9d9c4552ec1ca07dc495 Mon Sep 17 00:00:00 2001 From: Adrian Salamon Date: Tue, 17 Sep 2024 09:04:33 +0200 Subject: [PATCH 38/56] =?UTF-8?q?t=C3=A4nk=20p=C3=A5=20medianerna?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/haj_web/live/apply_live/edit_info.ex | 2 +- lib/haj_web/live/user_settings_live.ex | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/haj_web/live/apply_live/edit_info.ex b/lib/haj_web/live/apply_live/edit_info.ex index cbd65c4..ea7b101 100644 --- a/lib/haj_web/live/apply_live/edit_info.ex +++ b/lib/haj_web/live/apply_live/edit_info.ex @@ -94,7 +94,7 @@ defmodule HajWeb.ApplyLive.EditInfo do <.input field={@form[:class]} type="text" - label="Årskurs (ex D-20)" + label="Årskurs (ex D-20 eller Me-22)" class="col-span-6 sm:col-span-2" /> <.input diff --git a/lib/haj_web/live/user_settings_live.ex b/lib/haj_web/live/user_settings_live.ex index 382f4ae..a477667 100644 --- a/lib/haj_web/live/user_settings_live.ex +++ b/lib/haj_web/live/user_settings_live.ex @@ -86,7 +86,7 @@ defmodule HajWeb.UserSettingsLive do
    - <.form_text_input for={f} input={:class} text="Årskurs (eg D-20 eller Media-21)" /> + <.form_text_input for={f} input={:class} text="Årskurs (eg D-20 eller Me-21)" />
    From 1285061a35f0e759319c660bd34522cf882d998e Mon Sep 17 00:00:00 2001 From: Julia <74326858+ziyi01@users.noreply.github.com> Date: Tue, 17 Sep 2024 09:29:52 +0200 Subject: [PATCH 39/56] Update index.ex link (#84) Fix broken link to https://styrdokument.datasektionen.se/pm/pm_informationshantering --- lib/haj_web/live/apply_live/index.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/haj_web/live/apply_live/index.ex b/lib/haj_web/live/apply_live/index.ex index 98866ca..6fbacf8 100644 --- a/lib/haj_web/live/apply_live/index.ex +++ b/lib/haj_web/live/apply_live/index.ex @@ -50,7 +50,7 @@ defmodule HajWeb.ApplyLive.Index do

    Informationen hanteras i enlighet med Datasektionens informationshanteringspolicy. Om du vill att uppgifterna ska tas bort kan du maila Date: Thu, 26 Sep 2024 11:46:40 +0200 Subject: [PATCH 40/56] display upload errors, and don't crash when no show (#85) --- .../live/merch_admin_live/form_component.ex | 10 ++++--- .../live/song_live/edit/form_component.ex | 29 +++++++++++++++---- .../live/song_live/edit/index.html.heex | 2 +- 3 files changed, 31 insertions(+), 10 deletions(-) diff --git a/lib/haj_web/live/merch_admin_live/form_component.ex b/lib/haj_web/live/merch_admin_live/form_component.ex index 8008543..4112ef2 100644 --- a/lib/haj_web/live/merch_admin_live/form_component.ex +++ b/lib/haj_web/live/merch_admin_live/form_component.ex @@ -70,7 +70,7 @@ defmodule HajWeb.MerchAdminLive.FormComponent do end def handle_event("cancel-upload", %{"ref" => ref}, socket) do - {:noreply, cancel_upload(socket, socket.assigns.upload_name, ref)} + {:noreply, cancel_upload(socket, :image, ref)} end defp save_merch(socket, :edit, merch_params) do @@ -167,10 +167,12 @@ defmodule HajWeb.MerchAdminLive.FormComponent do <%= for entry <- @uploads.image.entries do %>

    <% end %> @@ -199,7 +201,7 @@ defmodule HajWeb.MerchAdminLive.FormComponent do <% end %>
    - + Ladda upp bild <.live_file_input upload={@uploads.image} class="pt-2 text-sm" />
    diff --git a/lib/haj_web/live/song_live/edit/form_component.ex b/lib/haj_web/live/song_live/edit/form_component.ex index 9297b03..78f7512 100644 --- a/lib/haj_web/live/song_live/edit/form_component.ex +++ b/lib/haj_web/live/song_live/edit/form_component.ex @@ -47,8 +47,18 @@ defmodule HajWeb.SongLive.Edit.FormComponent do options={@show_options} prompt="Välj spex" /> - <.live_file_input upload={@uploads.file} /> +
    + + Fil + +
    + <.live_file_input upload={@uploads.file} /> +
    +
    + <.error><%= error_to_string(err) %> +
    +
    <:actions> <.button phx-disable-with="Saving...">Save Song @@ -70,7 +80,12 @@ defmodule HajWeb.SongLive.Edit.FormComponent do {:ok, socket |> assign(:uploaded_files, []) - |> allow_upload(:file, accept: :any, max_entries: 1, external: &presign_upload/2) + |> allow_upload(:file, + accept: [".mp4", ".mp3"], + max_file_size: 20_000_000, + max_entries: 1, + external: &presign_upload/2 + ) |> assign(assigns) |> assign(show_options: show_options, line_timings: line_timings) |> assign_form(changeset)} @@ -102,7 +117,7 @@ defmodule HajWeb.SongLive.Edit.FormComponent do end defp save_song(socket, :edit, song_params) do - case Archive.update_song(socket.assigns.song, song_params, &consume_images(socket, &1)) do + case Archive.update_song(socket.assigns.song, song_params, &consume_songs(socket, &1)) do {:ok, song} -> notify_parent({:saved, song}) @@ -117,7 +132,7 @@ defmodule HajWeb.SongLive.Edit.FormComponent do end defp save_song(socket, :new, song_params) do - case Archive.create_song(song_params, &consume_images(socket, &1)) do + case Archive.create_song(song_params, &consume_songs(socket, &1)) do {:ok, song} -> notify_parent({:saved, song}) @@ -177,7 +192,7 @@ defmodule HajWeb.SongLive.Edit.FormComponent do end end - defp consume_images(socket, %Haj.Archive.Song{} = song) do + defp consume_songs(socket, %Haj.Archive.Song{} = song) do consume_uploaded_entries(socket, :file, fn _meta, _entry -> :ok end) {:ok, song} @@ -196,4 +211,8 @@ defmodule HajWeb.SongLive.Edit.FormComponent do |> Enum.map(&String.to_integer/1) end end + + defp error_to_string(:too_large), do: "Too large, max size is 20MB" + defp error_to_string(:too_many_files), do: "You have selected too many files" + defp error_to_string(:not_accepted), do: "You have selected an unacceptable file type" end diff --git a/lib/haj_web/live/song_live/edit/index.html.heex b/lib/haj_web/live/song_live/edit/index.html.heex index a7f8f10..5c4a25b 100644 --- a/lib/haj_web/live/song_live/edit/index.html.heex +++ b/lib/haj_web/live/song_live/edit/index.html.heex @@ -15,7 +15,7 @@ <:col :let={{_id, song}} label="Name"><%= song.name %> <:col :let={{_id, song}} label="Original name"><%= song.original_name %> <:col :let={{_id, song}} label="Number"><%= song.number %> - <:col :let={{_id, song}} label="Spex"><%= song.show.year.year %> + <:col :let={{_id, song}} label="Spex"><%= song.show && song.show.year.year %> <:action :let={{_id, song}}>
    <.link navigate={~p"/songs/edit/#{song}"}>Show From 055ab836538270cd962a697f0de2c3700a6dc47d Mon Sep 17 00:00:00 2001 From: Adrian Salamon Date: Sat, 28 Sep 2024 11:55:14 +0200 Subject: [PATCH 41/56] fix logic when getting previous form submission --- lib/haj/forms.ex | 6 ++++-- lib/haj_web/live/event_live/form_component.ex | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/haj/forms.ex b/lib/haj/forms.ex index 20de808..4dceddd 100644 --- a/lib/haj/forms.ex +++ b/lib/haj/forms.ex @@ -570,10 +570,12 @@ defmodule Haj.Forms do @doc """ Returns a form response for a user and form, if one exists. """ - def get_response_for_user(form_id, user_id) do + def get_event_response_for_user(event_id, user_id) do query = from r in Response, - where: r.form_id == ^form_id and r.user_id == ^user_id, + join: er in assoc(r, :event_registration), + join: e in assoc(er, :event), + where: r.user_id == ^user_id and e.id == ^event_id, preload: [question_responses: []] Repo.one(query) diff --git a/lib/haj_web/live/event_live/form_component.ex b/lib/haj_web/live/event_live/form_component.ex index e3399fe..512e8de 100644 --- a/lib/haj_web/live/event_live/form_component.ex +++ b/lib/haj_web/live/event_live/form_component.ex @@ -46,7 +46,7 @@ defmodule HajWeb.EventLive.FormComponent do attending = registration && registration.attending && true if event.form do - form_response = Forms.get_response_for_user(event.form_id, user.id) + form_response = Forms.get_event_response_for_user(event.id, user.id) changeset = Forms.get_form_changeset!(event.form_id, form_response || %{}) form = Forms.get_form!(event.form_id) @@ -128,7 +128,7 @@ defmodule HajWeb.EventLive.FormComponent do %{current_user: user, attending: attending} = socket.assigns with previous when not is_nil(previous) <- - Forms.get_response_for_user(socket.assigns.form.id, user.id), + Forms.get_event_response_for_user(socket.assigns.registration.event_id, user.id), {:ok, _} <- Forms.update_form_response(socket.assigns.form.id, previous, response), {:ok, _} <- From 7fd09cd2c4b8c3ffdcdb7efc03c3a7ab779efe60 Mon Sep 17 00:00:00 2001 From: Adrian Salamon Date: Sat, 28 Sep 2024 13:51:08 +0200 Subject: [PATCH 42/56] Bump deps (#87) * bump deps * also bump package.json --- assets/package-lock.json | 1935 +++++++++++---------- assets/tailwind.config.js | 88 +- config/prod.exs | 1 + lib/haj_web/components/core_components.ex | 2 +- lib/haj_web/gettext.ex | 2 +- lib/haj_web/live/user_live.ex | 5 +- lib/haj_web/live/user_settings_live.ex | 2 +- mix.lock | 104 +- 8 files changed, 1106 insertions(+), 1033 deletions(-) diff --git a/assets/package-lock.json b/assets/package-lock.json index d486501..21a0850 100644 --- a/assets/package-lock.json +++ b/assets/package-lock.json @@ -1,6 +1,6 @@ { "name": "assets", - "lockfileVersion": 2, + "lockfileVersion": 3, "requires": true, "packages": { "": { @@ -14,21 +14,115 @@ "@tailwindcss/typography": "^0.5.9" } }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@alpinejs/collapse": { - "version": "3.10.2", - "resolved": "https://registry.npmjs.org/@alpinejs/collapse/-/collapse-3.10.2.tgz", - "integrity": "sha512-ZI7ysUXPjtrdI61dyF23vNRnDLWW/BMeqrdoQtUMENHx7lsYAKvG9asFUM8hV0eF7KVQiHz/K9oV3qX2i6UXhQ==" + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/@alpinejs/collapse/-/collapse-3.14.1.tgz", + "integrity": "sha512-aI0pq8SjK7c43/nMIVL1Lt8naowPRepqQGNSb9KaG7adEneOwj/vq4ZaeZYjuGbd8sq1LKPwWU+klIZIXXujUA==", + "license": "MIT" }, "node_modules/@alpinejs/intersect": { - "version": "3.10.2", - "resolved": "https://registry.npmjs.org/@alpinejs/intersect/-/intersect-3.10.2.tgz", - "integrity": "sha512-BP6rO7lwqDF2mpa6ami3FPPu8zK622sWLQ27Ru5Hu4YzFhMUnX05jwZc1pEsZzY46QWyeInDLgMTB+XyH5e6sQ==" + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/@alpinejs/intersect/-/intersect-3.14.1.tgz", + "integrity": "sha512-+qlQief0QWPqjFjoylr+CfKOkwvnxldRQg/WT1PscyPAeykK+wJOJAEQCimwwaLnYHxiXdF6whti5o7GYPYNxw==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@nodelib/fs.stat": "2.0.5", @@ -43,6 +137,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">= 8" @@ -53,6 +148,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@nodelib/fs.scandir": "2.1.5", @@ -62,11 +158,24 @@ "node": ">= 8" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@tailwindcss/typography": { - "version": "0.5.9", - "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.9.tgz", - "integrity": "sha512-t8Sg3DyynFysV9f4JDOVISGsjazNb48AeIYQwcL+Bsq5uf4RYL75C1giZ43KISjeDGBaTN3Kxh7Xj/vRSMJUUg==", + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.15.tgz", + "integrity": "sha512-AqhlCXl+8grUz8uqExv5OTtgpjuVIwFTSXTrh8y9/pw6q2ek7fJ+Y8ZEVw7EB2DCcuCOtEjf9w3+J3rzts01uA==", "dev": true, + "license": "MIT", "dependencies": { "lodash.castarray": "^4.4.0", "lodash.isplainobject": "^4.0.6", @@ -74,31 +183,35 @@ "postcss-selector-parser": "6.0.10" }, "peerDependencies": { - "tailwindcss": ">=3.0.0 || insiders" + "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20" } }, "node_modules/@types/codemirror": { - "version": "5.60.7", - "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.7.tgz", - "integrity": "sha512-QXIC+RPzt/1BGSuD6iFn6UMC9TDp+9hkOANYNPVsjjrDdzKphfRkwQDKGp2YaC54Yhz0g6P5uYTCCibZZEiMAA==", + "version": "5.60.15", + "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.15.tgz", + "integrity": "sha512-dTOvwEQ+ouKJ/rE9LT1Ue2hmP6H1mZv5+CCnNWu2qtiOe2LQa9lCprEY20HxiDmV/Bxh+dXjywmy5aKvoGjULA==", + "license": "MIT", "dependencies": { "@types/tern": "*" } }, "node_modules/@types/estree": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz", - "integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==" + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "license": "MIT" }, "node_modules/@types/marked": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.0.8.tgz", - "integrity": "sha512-HVNzMT5QlWCOdeuBsgXP8EZzKUf0+AXzN+sLmjvaB3ZlLqO+e4u0uXrdw9ub69wBKFs+c6/pA4r9sy6cCDvImw==" + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.3.2.tgz", + "integrity": "sha512-a79Yc3TOk6dGdituy8hmTTJXjOkZ7zsFYV10L337ttq/rec8lRMDBpV7fL3uLx6TgbFCa5DU/h8FmIBQPSbU0w==", + "license": "MIT" }, "node_modules/@types/tern": { - "version": "0.23.4", - "resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.4.tgz", - "integrity": "sha512-JAUw1iXGO1qaWwEOzxTKJZ/5JxVeON9kvGZ/osgZaJImBnyjyn0cjovPsf6FNLmyGY8Vw9DoXZCMlfMkMwHRWg==", + "version": "0.23.9", + "resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz", + "integrity": "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==", + "license": "MIT", "dependencies": { "@types/estree": "*" } @@ -107,6 +220,7 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.5.tgz", "integrity": "sha512-1tdfLmNjWG6t/CsPldh+foumYFo3cpyCHgBYQ34ylaMsJ+SNHQ1kApMIa8jN+i593zQuaw3AdWH0nJTARzCFhg==", + "license": "MIT", "dependencies": { "@vue/shared": "3.1.5" } @@ -114,56 +228,60 @@ "node_modules/@vue/shared": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.5.tgz", - "integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==" + "integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==", + "license": "MIT" }, - "node_modules/acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true, - "peer": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" + "node_modules/alpinejs": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.14.1.tgz", + "integrity": "sha512-ICar8UsnRZAYvv/fCNfNeKMXNoXGUfwHrjx7LqXd08zIP95G2d9bAOuaL97re+1mgt/HojqHsfdOLo/A5LuWgQ==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "~3.1.1" } }, - "node_modules/acorn-node": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", - "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, + "license": "MIT", "peer": true, - "dependencies": { - "acorn": "^7.0.0", - "acorn-walk": "^7.0.0", - "xtend": "^4.0.2" + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/acorn-walk": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", - "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", + "node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, + "license": "MIT", "peer": true, "engines": { - "node": ">=0.4.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/alpinejs": { - "version": "3.10.2", - "resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.10.2.tgz", - "integrity": "sha512-P6DTX78J94FgsskS3eS23s26hfb0NWQZUidBnkUK7fId1x64/kLm5E79lL3HNItUmHDCKOHvfP8EAcuCVab89w==", - "dependencies": { - "@vue/reactivity": "~3.1.1" - } + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT", + "peer": true }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, + "license": "ISC", "peer": true, "dependencies": { "normalize-path": "^3.0.0", @@ -178,26 +296,51 @@ "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT", "peer": true }, "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0" } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -208,22 +351,18 @@ "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">= 6" } }, "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], + "license": "MIT", "peer": true, "dependencies": { "anymatch": "~3.1.2", @@ -237,6 +376,9 @@ "engines": { "node": ">= 8.10.0" }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, "optionalDependencies": { "fsevents": "~2.3.2" } @@ -246,6 +388,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, + "license": "ISC", "peer": true, "dependencies": { "is-glob": "^4.0.1" @@ -255,63 +398,80 @@ } }, "node_modules/codemirror": { - "version": "5.65.11", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.11.tgz", - "integrity": "sha512-Gp62g2eKSCHYt10axmGhKq3WoJSvVpvhXmowNq7pZdRVowwtvBR/hi2LSP5srtctKkRT33T6/n8Kv1UGp7JW4A==" + "version": "5.65.18", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.18.tgz", + "integrity": "sha512-Gaz4gHnkbHMGgahNt3CA5HBk5lLQBqmD/pBgeB4kQU6OedZmqMBjlRF0LSrp2tJ4wlLNPm2FfaUd1pDy0mdlpA==", + "license": "MIT" }, "node_modules/codemirror-spell-checker": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/codemirror-spell-checker/-/codemirror-spell-checker-1.1.2.tgz", "integrity": "sha512-2Tl6n0v+GJRsC9K3MLCdLaMOmvWL0uukajNJseorZJsslaxZyZMgENocPU8R0DyoTAiKsyqiemSOZo7kjGV0LQ==", + "license": "MIT", "dependencies": { "typo-js": "*" } }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true, + "license": "MIT", "peer": true }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", "dev": true, - "bin": { - "cssesc": "bin/cssesc" - }, + "license": "MIT", + "peer": true, "engines": { - "node": ">=4" + "node": ">= 6" } }, - "node_modules/defined": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.1.tgz", - "integrity": "sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==", + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "dev": true, + "license": "MIT", "peer": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" } }, - "node_modules/detective": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.1.tgz", - "integrity": "sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==", + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "dev": true, - "peer": true, - "dependencies": { - "acorn-node": "^1.8.2", - "defined": "^1.0.0", - "minimist": "^1.2.6" - }, + "license": "MIT", "bin": { - "detective": "bin/detective.js" + "cssesc": "bin/cssesc" }, "engines": { - "node": ">=0.8.0" + "node": ">=4" } }, "node_modules/didyoumean": { @@ -319,6 +479,7 @@ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", "dev": true, + "license": "Apache-2.0", "peer": true }, "node_modules/dlv": { @@ -326,12 +487,22 @@ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT", "peer": true }, "node_modules/easymde": { "version": "2.18.0", "resolved": "https://registry.npmjs.org/easymde/-/easymde-2.18.0.tgz", "integrity": "sha512-IxVVUxNWIoXLeqtBU4BLc+eS/ScYhT1Dcb6yF5Wchoj1iXAV+TIIDWx+NCaZhY7RcSHqDPKllbYq7nwGKILnoA==", + "license": "MIT", "dependencies": { "@types/codemirror": "^5.60.4", "@types/marked": "^4.0.7", @@ -340,11 +511,20 @@ "marked": "^4.1.0" } }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/fast-glob": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -362,6 +542,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, + "license": "ISC", "peer": true, "dependencies": { "is-glob": "^4.0.1" @@ -371,20 +552,22 @@ } }, "node_modules/fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", "dev": true, + "license": "ISC", "peer": true, "dependencies": { "reusify": "^1.0.4" } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -393,12 +576,31 @@ "node": ">=8" } }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -409,17 +611,44 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true, - "peer": true + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, + "license": "ISC", "peer": true, "dependencies": { "is-glob": "^4.0.3" @@ -428,17 +657,18 @@ "node": ">=10.13.0" } }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { - "function-bind": "^1.1.1" + "function-bind": "^1.1.2" }, "engines": { - "node": ">= 0.4.0" + "node": ">= 0.4" } }, "node_modules/is-binary-path": { @@ -446,6 +676,7 @@ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "binary-extensions": "^2.0.0" @@ -455,13 +686,17 @@ } }, "node_modules/is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { - "has": "^1.0.3" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -472,16 +707,29 @@ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "is-extglob": "^2.1.1" @@ -495,43 +743,101 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=0.12.0" } }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "peer": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "1.21.6", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", + "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, "node_modules/lilconfig": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz", - "integrity": "sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=10" } }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/lodash.castarray": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz", "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC", + "peer": true }, "node_modules/marked": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/marked/-/marked-4.2.12.tgz", - "integrity": "sha512-yr8hSKa3Fv4D3jdZmtMMPghgVt6TWbk86WQaWhDloQjRSQhMMYCAro7jP7VDJrjjdV8pxVxMssXS8B8Y5DZ5aw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "license": "MIT", "bin": { "marked": "bin/marked.js" }, @@ -544,40 +850,80 @@ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">= 8" } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { "node": ">=8.6" } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, + "license": "ISC", "peer": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "peer": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" } }, "node_modules/nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", "peer": true, "bin": { "nanoid": "bin/nanoid.cjs" @@ -591,6 +937,18 @@ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=0.10.0" @@ -601,23 +959,63 @@ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">= 6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0", + "peer": true + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true, + "license": "MIT", "peer": true }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "peer": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", "dev": true, + "license": "ISC", "peer": true }, "node_modules/picomatch": { @@ -625,6 +1023,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=8.6" @@ -638,15 +1037,27 @@ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=0.10.0" } }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/postcss": { - "version": "8.4.21", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", - "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==", + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", "dev": true, "funding": [ { @@ -656,23 +1067,29 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "peer": true, "dependencies": { - "nanoid": "^3.3.4", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "nanoid": "^3.3.7", + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" } }, "node_modules/postcss-import": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz", - "integrity": "sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==", + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "postcss-value-parser": "^4.0.0", @@ -680,7 +1097,7 @@ "resolve": "^1.1.7" }, "engines": { - "node": ">=10.0.0" + "node": ">=14.0.0" }, "peerDependencies": { "postcss": "^8.0.0" @@ -691,6 +1108,7 @@ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "camelcase-css": "^2.0.1" @@ -707,21 +1125,28 @@ } }, "node_modules/postcss-load-config": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", - "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", "dev": true, - "peer": true, - "dependencies": { - "lilconfig": "^2.0.5", - "yaml": "^1.10.2" - }, - "engines": { - "node": ">= 10" + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" + "engines": { + "node": ">= 14" }, "peerDependencies": { "postcss": ">=8.0.9", @@ -736,31 +1161,68 @@ } } }, + "node_modules/postcss-load-config/node_modules/lilconfig": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", + "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, "node_modules/postcss-nested": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.0.tgz", - "integrity": "sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", "peer": true, "dependencies": { - "postcss-selector-parser": "^6.0.10" + "postcss-selector-parser": "^6.1.1" }, "engines": { "node": ">=12.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, "peerDependencies": { "postcss": "^8.2.14" } }, + "node_modules/postcss-nested/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/postcss-selector-parser": { "version": "6.0.10", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", "dev": true, + "license": "MIT", "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -774,6 +1236,7 @@ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true, + "license": "MIT", "peer": true }, "node_modules/queue-microtask": { @@ -795,26 +1258,15 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "peer": true }, - "node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "dev": true, - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "pify": "^2.3.0" @@ -825,6 +1277,7 @@ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "picomatch": "^2.2.1" @@ -834,13 +1287,14 @@ } }, "node_modules/resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { - "is-core-module": "^2.9.0", + "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -856,6 +1310,7 @@ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", "dev": true, + "license": "MIT", "peer": true, "engines": { "iojs": ">=1.0.0", @@ -881,26 +1336,204 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "peer": true, "dependencies": { "queue-microtask": "^1.2.2" } }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "peer": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, + "license": "BSD-3-Clause", "peer": true, "engines": { "node": ">=0.10.0" } }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">= 0.4" @@ -910,52 +1543,50 @@ } }, "node_modules/tailwindcss": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.6.tgz", - "integrity": "sha512-BfgQWZrtqowOQMC2bwaSNe7xcIjdDEgixWGYOd6AL0CbKHJlvhfdbINeAW76l1sO+1ov/MJ93ODJ9yluRituIw==", + "version": "3.4.13", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.13.tgz", + "integrity": "sha512-KqjHOJKogOUt5Bs752ykCeiwvi0fKVkr5oqsFNt/8px/tA8scFPIlkygsf6jXrfCqGHz7VflA6+yytWuM+XhFw==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { + "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.5.3", - "color-name": "^1.1.4", - "detective": "^5.2.1", "didyoumean": "^1.2.2", "dlv": "^1.1.3", - "fast-glob": "^3.2.12", + "fast-glob": "^3.3.0", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", - "lilconfig": "^2.0.6", + "jiti": "^1.21.0", + "lilconfig": "^2.1.0", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.0.0", - "postcss": "^8.0.9", - "postcss-import": "^14.1.0", - "postcss-js": "^4.0.0", - "postcss-load-config": "^3.1.4", - "postcss-nested": "6.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", "postcss-selector-parser": "^6.0.11", - "postcss-value-parser": "^4.2.0", - "quick-lru": "^5.1.1", - "resolve": "^1.22.1" + "resolve": "^1.22.2", + "sucrase": "^3.32.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" }, "engines": { - "node": ">=12.13.0" - }, - "peerDependencies": { - "postcss": "^8.0.9" + "node": ">=14.0.0" } }, "node_modules/tailwindcss/node_modules/postcss-selector-parser": { - "version": "6.0.11", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz", - "integrity": "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "cssesc": "^3.0.0", @@ -965,11 +1596,37 @@ "node": ">=4" } }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "is-number": "^7.0.0" @@ -978,762 +1635,162 @@ "node": ">=8.0" } }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0", + "peer": true + }, "node_modules/typo-js": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/typo-js/-/typo-js-1.2.2.tgz", - "integrity": "sha512-C7pYBQK17EjSg8tVNY91KHdUt5Nf6FMJ+c3js076quPmBML57PmNMzAcIq/2kf/hSYtFABNDIYNYlJRl5BJhGw==" + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/typo-js/-/typo-js-1.2.4.tgz", + "integrity": "sha512-Oy/k+tFle5NAA3J/yrrYGfvEnPVrDZ8s8/WCwjUE75k331QyKIsFss7byQ/PzBmXLY6h1moRnZbnaxWBe3I3CA==", + "license": "BSD-3-Clause" }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, + "license": "ISC", "peer": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, "engines": { - "node": ">=0.4" + "node": ">= 8" } }, - "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dev": true, + "license": "MIT", "peer": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, "engines": { - "node": ">= 6" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } - } - }, - "dependencies": { - "@alpinejs/collapse": { - "version": "3.10.2", - "resolved": "https://registry.npmjs.org/@alpinejs/collapse/-/collapse-3.10.2.tgz", - "integrity": "sha512-ZI7ysUXPjtrdI61dyF23vNRnDLWW/BMeqrdoQtUMENHx7lsYAKvG9asFUM8hV0eF7KVQiHz/K9oV3qX2i6UXhQ==" - }, - "@alpinejs/intersect": { - "version": "3.10.2", - "resolved": "https://registry.npmjs.org/@alpinejs/intersect/-/intersect-3.10.2.tgz", - "integrity": "sha512-BP6rO7lwqDF2mpa6ami3FPPu8zK622sWLQ27Ru5Hu4YzFhMUnX05jwZc1pEsZzY46QWyeInDLgMTB+XyH5e6sQ==" - }, - "@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, + "license": "MIT", "peer": true, - "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "peer": true - }, - "@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, + "license": "MIT", "peer": true, - "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "engines": { + "node": ">=8" } }, - "@tailwindcss/typography": { - "version": "0.5.9", - "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.9.tgz", - "integrity": "sha512-t8Sg3DyynFysV9f4JDOVISGsjazNb48AeIYQwcL+Bsq5uf4RYL75C1giZ43KISjeDGBaTN3Kxh7Xj/vRSMJUUg==", + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, - "requires": { - "lodash.castarray": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.merge": "^4.6.2", - "postcss-selector-parser": "6.0.10" - } - }, - "@types/codemirror": { - "version": "5.60.7", - "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.7.tgz", - "integrity": "sha512-QXIC+RPzt/1BGSuD6iFn6UMC9TDp+9hkOANYNPVsjjrDdzKphfRkwQDKGp2YaC54Yhz0g6P5uYTCCibZZEiMAA==", - "requires": { - "@types/tern": "*" - } - }, - "@types/estree": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz", - "integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==" - }, - "@types/marked": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.0.8.tgz", - "integrity": "sha512-HVNzMT5QlWCOdeuBsgXP8EZzKUf0+AXzN+sLmjvaB3ZlLqO+e4u0uXrdw9ub69wBKFs+c6/pA4r9sy6cCDvImw==" - }, - "@types/tern": { - "version": "0.23.4", - "resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.4.tgz", - "integrity": "sha512-JAUw1iXGO1qaWwEOzxTKJZ/5JxVeON9kvGZ/osgZaJImBnyjyn0cjovPsf6FNLmyGY8Vw9DoXZCMlfMkMwHRWg==", - "requires": { - "@types/estree": "*" - } - }, - "@vue/reactivity": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.5.tgz", - "integrity": "sha512-1tdfLmNjWG6t/CsPldh+foumYFo3cpyCHgBYQ34ylaMsJ+SNHQ1kApMIa8jN+i593zQuaw3AdWH0nJTARzCFhg==", - "requires": { - "@vue/shared": "3.1.5" + "license": "MIT", + "peer": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "@vue/shared": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.5.tgz", - "integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==" - }, - "acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, + "license": "MIT", "peer": true }, - "acorn-node": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", - "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", "peer": true, - "requires": { - "acorn": "^7.0.0", - "acorn-walk": "^7.0.0", - "xtend": "^4.0.2" + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" } }, - "acorn-walk": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", - "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, - "peer": true - }, - "alpinejs": { - "version": "3.10.2", - "resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.10.2.tgz", - "integrity": "sha512-P6DTX78J94FgsskS3eS23s26hfb0NWQZUidBnkUK7fId1x64/kLm5E79lL3HNItUmHDCKOHvfP8EAcuCVab89w==", - "requires": { - "@vue/reactivity": "~3.1.1" + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, - "anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "node_modules/yaml": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz", + "integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==", "dev": true, + "license": "ISC", "peer": true, - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" } - }, - "arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true, - "peer": true - }, - "binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true, - "peer": true - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "peer": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "dev": true, - "peer": true - }, - "chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "dev": true, - "peer": true, - "requires": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "fsevents": "~2.3.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "dependencies": { - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "peer": true, - "requires": { - "is-glob": "^4.0.1" - } - } - } - }, - "codemirror": { - "version": "5.65.11", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.11.tgz", - "integrity": "sha512-Gp62g2eKSCHYt10axmGhKq3WoJSvVpvhXmowNq7pZdRVowwtvBR/hi2LSP5srtctKkRT33T6/n8Kv1UGp7JW4A==" - }, - "codemirror-spell-checker": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/codemirror-spell-checker/-/codemirror-spell-checker-1.1.2.tgz", - "integrity": "sha512-2Tl6n0v+GJRsC9K3MLCdLaMOmvWL0uukajNJseorZJsslaxZyZMgENocPU8R0DyoTAiKsyqiemSOZo7kjGV0LQ==", - "requires": { - "typo-js": "*" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "peer": true - }, - "cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true - }, - "defined": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.1.tgz", - "integrity": "sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==", - "dev": true, - "peer": true - }, - "detective": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.1.tgz", - "integrity": "sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==", - "dev": true, - "peer": true, - "requires": { - "acorn-node": "^1.8.2", - "defined": "^1.0.0", - "minimist": "^1.2.6" - } - }, - "didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true, - "peer": true - }, - "dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true, - "peer": true - }, - "easymde": { - "version": "2.18.0", - "resolved": "https://registry.npmjs.org/easymde/-/easymde-2.18.0.tgz", - "integrity": "sha512-IxVVUxNWIoXLeqtBU4BLc+eS/ScYhT1Dcb6yF5Wchoj1iXAV+TIIDWx+NCaZhY7RcSHqDPKllbYq7nwGKILnoA==", - "requires": { - "@types/codemirror": "^5.60.4", - "@types/marked": "^4.0.7", - "codemirror": "^5.63.1", - "codemirror-spell-checker": "1.1.2", - "marked": "^4.1.0" - } - }, - "fast-glob": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", - "dev": true, - "peer": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "dependencies": { - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "peer": true, - "requires": { - "is-glob": "^4.0.1" - } - } - } - }, - "fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", - "dev": true, - "peer": true, - "requires": { - "reusify": "^1.0.4" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "peer": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "optional": true, - "peer": true - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true, - "peer": true - }, - "glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "peer": true, - "requires": { - "is-glob": "^4.0.3" - } - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "peer": true, - "requires": { - "function-bind": "^1.1.1" - } - }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "peer": true, - "requires": { - "binary-extensions": "^2.0.0" - } - }, - "is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", - "dev": true, - "peer": true, - "requires": { - "has": "^1.0.3" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "peer": true - }, - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "peer": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "peer": true - }, - "lilconfig": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz", - "integrity": "sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==", - "dev": true, - "peer": true - }, - "lodash.castarray": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz", - "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==", - "dev": true - }, - "lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "dev": true - }, - "lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, - "marked": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/marked/-/marked-4.2.12.tgz", - "integrity": "sha512-yr8hSKa3Fv4D3jdZmtMMPghgVt6TWbk86WQaWhDloQjRSQhMMYCAro7jP7VDJrjjdV8pxVxMssXS8B8Y5DZ5aw==" - }, - "merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "peer": true - }, - "micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, - "peer": true, - "requires": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - } - }, - "minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "peer": true - }, - "nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", - "dev": true, - "peer": true - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "peer": true - }, - "object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true, - "peer": true - }, - "path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "peer": true - }, - "picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true, - "peer": true - }, - "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "peer": true - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, - "peer": true - }, - "postcss": { - "version": "8.4.21", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", - "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==", - "dev": true, - "peer": true, - "requires": { - "nanoid": "^3.3.4", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - } - }, - "postcss-import": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz", - "integrity": "sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==", - "dev": true, - "peer": true, - "requires": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - } - }, - "postcss-js": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", - "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", - "dev": true, - "peer": true, - "requires": { - "camelcase-css": "^2.0.1" - } - }, - "postcss-load-config": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", - "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", - "dev": true, - "peer": true, - "requires": { - "lilconfig": "^2.0.5", - "yaml": "^1.10.2" - } - }, - "postcss-nested": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.0.tgz", - "integrity": "sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==", - "dev": true, - "peer": true, - "requires": { - "postcss-selector-parser": "^6.0.10" - } - }, - "postcss-selector-parser": { - "version": "6.0.10", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", - "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", - "dev": true, - "requires": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - } - }, - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, - "peer": true - }, - "queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "peer": true - }, - "quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "dev": true, - "peer": true - }, - "read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dev": true, - "peer": true, - "requires": { - "pify": "^2.3.0" - } - }, - "readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "peer": true, - "requires": { - "picomatch": "^2.2.1" - } - }, - "resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", - "dev": true, - "peer": true, - "requires": { - "is-core-module": "^2.9.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - } - }, - "reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "peer": true - }, - "run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "peer": true, - "requires": { - "queue-microtask": "^1.2.2" - } - }, - "source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "dev": true, - "peer": true - }, - "supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "peer": true - }, - "tailwindcss": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.6.tgz", - "integrity": "sha512-BfgQWZrtqowOQMC2bwaSNe7xcIjdDEgixWGYOd6AL0CbKHJlvhfdbINeAW76l1sO+1ov/MJ93ODJ9yluRituIw==", - "dev": true, - "peer": true, - "requires": { - "arg": "^5.0.2", - "chokidar": "^3.5.3", - "color-name": "^1.1.4", - "detective": "^5.2.1", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.2.12", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "lilconfig": "^2.0.6", - "micromatch": "^4.0.5", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.0.0", - "postcss": "^8.0.9", - "postcss-import": "^14.1.0", - "postcss-js": "^4.0.0", - "postcss-load-config": "^3.1.4", - "postcss-nested": "6.0.0", - "postcss-selector-parser": "^6.0.11", - "postcss-value-parser": "^4.2.0", - "quick-lru": "^5.1.1", - "resolve": "^1.22.1" - }, - "dependencies": { - "postcss-selector-parser": { - "version": "6.0.11", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz", - "integrity": "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==", - "dev": true, - "peer": true, - "requires": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - } - } - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "peer": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "typo-js": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/typo-js/-/typo-js-1.2.2.tgz", - "integrity": "sha512-C7pYBQK17EjSg8tVNY91KHdUt5Nf6FMJ+c3js076quPmBML57PmNMzAcIq/2kf/hSYtFABNDIYNYlJRl5BJhGw==" - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true - }, - "xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true, - "peer": true - }, - "yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true, - "peer": true } } } diff --git a/assets/tailwind.config.js b/assets/tailwind.config.js index 43ceeca..1de60c0 100644 --- a/assets/tailwind.config.js +++ b/assets/tailwind.config.js @@ -1,56 +1,68 @@ // See the Tailwind configuration guide for advanced usage // https://tailwindcss.com/docs/configuration -const plugin = require("tailwindcss/plugin") +const plugin = require("tailwindcss/plugin"); module.exports = { - content: [ - './js/**/*.js', - '../lib/*_web.ex', - '../lib/*_web/**/*.*ex' - ], + content: ["./js/**/*.js", "../lib/*_web.ex", "../lib/*_web/**/*.*ex"], theme: { extend: { screens: { - 'xs': '520px', + xs: "520px", }, colors: { - 'burgandy': '#6f1d1b', - 'orange': '#bc827a', - 'burgandy-light': "#F1E8E8", - 'burgandy': { - "50": "#F8E3E2", - "100": "#F0C2C1", - "200": "#E28A88", - "300": "#D34D4A", - "400": "#AC2C2A", - "500": "#6F1D1B", - "600": "#5A1716", - "700": "#421110", - "800": "#2D0C0B", - "900": "#150505" - } - }, backgroundSize: { - 'size-125': '125% 125%', + burgandy: "#6f1d1b", + orange: "#bc827a", + "burgandy-light": "#F1E8E8", + burgandy: { + 50: "#F8E3E2", + 100: "#F0C2C1", + 200: "#E28A88", + 300: "#D34D4A", + 400: "#AC2C2A", + 500: "#6F1D1B", + 600: "#5A1716", + 700: "#421110", + 800: "#2D0C0B", + 900: "#150505", + }, + }, + backgroundSize: { + "size-125": "125% 125%", }, backgroundPosition: { - 'pos-0': '0% 0%', - 'pos-100': '100% 100%', + "pos-0": "0% 0%", + "pos-100": "100% 100%", }, }, fontFamily: { - sans: ['Mukta', 'sans-serif'], - title: ['Yeseva One', 'cursive'] + sans: ["Mukta", "sans-serif"], + title: ["Yeseva One", "cursive"], }, - }, plugins: [ - require('@tailwindcss/forms'), - require('@tailwindcss/line-clamp'), - require('@tailwindcss/typography'), - plugin(({ addVariant }) => addVariant("phx-no-feedback", [".phx-no-feedback&", ".phx-no-feedback &"])), - plugin(({ addVariant }) => addVariant("phx-click-loading", [".phx-click-loading&", ".phx-click-loading &"])), - plugin(({ addVariant }) => addVariant("phx-submit-loading", [".phx-submit-loading&", ".phx-submit-loading &"])), - plugin(({ addVariant }) => addVariant("phx-change-loading", [".phx-change-loading&", ".phx-change-loading &"])) - ] -} + require("@tailwindcss/forms"), + require("@tailwindcss/typography"), + plugin(({ addVariant }) => + addVariant("phx-no-feedback", [".phx-no-feedback&", ".phx-no-feedback &"]) + ), + plugin(({ addVariant }) => + addVariant("phx-click-loading", [ + ".phx-click-loading&", + ".phx-click-loading &", + ]) + ), + plugin(({ addVariant }) => + addVariant("phx-submit-loading", [ + ".phx-submit-loading&", + ".phx-submit-loading &", + ]) + ), + plugin(({ addVariant }) => + addVariant("phx-change-loading", [ + ".phx-change-loading&", + ".phx-change-loading &", + ]) + ), + ], +}; diff --git a/config/prod.exs b/config/prod.exs index 2c0d7c1..38d8a7d 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -49,3 +49,4 @@ config :logger, level: :info # force_ssl: [hsts: true] # # Check `Plug.SSL` for all available options in `force_ssl`. +config :haj, HajWeb.Endpoint, force_ssl: [hsts: true] diff --git a/lib/haj_web/components/core_components.ex b/lib/haj_web/components/core_components.ex index 737af0e..975e5de 100644 --- a/lib/haj_web/components/core_components.ex +++ b/lib/haj_web/components/core_components.ex @@ -12,7 +12,7 @@ defmodule HajWeb.CoreComponents do use Phoenix.Component alias Phoenix.LiveView.JS - import HajWeb.Gettext + use Gettext, backend: HajWeb.Gettext @doc """ Renders a modal. diff --git a/lib/haj_web/gettext.ex b/lib/haj_web/gettext.ex index cdb2564..dd3166f 100644 --- a/lib/haj_web/gettext.ex +++ b/lib/haj_web/gettext.ex @@ -20,5 +20,5 @@ defmodule HajWeb.Gettext do See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage. """ - use Gettext, otp_app: :haj + use Gettext.Backend, otp_app: :my_app end diff --git a/lib/haj_web/live/user_live.ex b/lib/haj_web/live/user_live.ex index 8810ffc..c93d8de 100644 --- a/lib/haj_web/live/user_live.ex +++ b/lib/haj_web/live/user_live.ex @@ -6,7 +6,10 @@ defmodule HajWeb.UserLive do user = Accounts.get_user_by_username!(username) |> Accounts.preload(:foods) groups = Haj.Spex.get_show_groups_for_user(user.id) - groups_by_year = Enum.group_by(groups, fn %{show: show} -> show.year end) + + groups_by_year = + Enum.sort_by(groups, &{&1.show.year, &1.group.name}) + |> Enum.group_by(fn %{show: show} -> show.year end) {:ok, assign(socket, page_title: full_name(user), user: user, groups: groups_by_year)} end diff --git a/lib/haj_web/live/user_settings_live.ex b/lib/haj_web/live/user_settings_live.ex index a477667..e6d01e4 100644 --- a/lib/haj_web/live/user_settings_live.ex +++ b/lib/haj_web/live/user_settings_live.ex @@ -47,7 +47,7 @@ defmodule HajWeb.UserSettingsLive do {:noreply, socket |> put_flash(:info, "Ändringen sparades.") - |> push_redirect(to: Routes.user_path(Endpoint, :index, user.username))} + |> push_navigate(to: Routes.user_path(Endpoint, :index, user.username))} {:error, %Ecto.Changeset{} = changeset} -> socket = socket |> put_flash(:error, "Något gick fel, kolla att allt är ifyllt korrekt.") diff --git a/mix.lock b/mix.lock index 9fad105..a88442a 100644 --- a/mix.lock +++ b/mix.lock @@ -1,68 +1,68 @@ %{ - "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, - "castore": {:hex, :castore, "1.0.3", "7130ba6d24c8424014194676d608cb989f62ef8039efd50ff4b3f33286d06db8", [:mix], [], "hexpm", "680ab01ef5d15b161ed6a95449fac5c6b8f60055677a8e79acf01b27baa4390b"}, - "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"}, + "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, + "castore": {:hex, :castore, "1.0.9", "5cc77474afadf02c7c017823f460a17daa7908e991b0cc917febc90e466a375c", [:mix], [], "hexpm", "5ea956504f1ba6f2b4eb707061d8e17870de2bee95fb59d512872c2ef06925e7"}, + "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"}, - "cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"}, + "cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, - "cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"}, - "credo": {:hex, :credo, "1.7.0", "6119bee47272e85995598ee04f2ebbed3e947678dee048d10b5feca139435f75", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "6839fcf63d1f0d1c0f450abc8564a57c43d644077ab96f2934563e68b8a769d7"}, + "cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"}, + "credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"}, "csv": {:hex, :csv, "2.5.0", "c47b5a5221bf2e56d6e8eb79e77884046d7fd516280dc7d9b674251e0ae46246", [:mix], [{:parallel_stream, "~> 1.0.4 or ~> 1.1.0", [hex: :parallel_stream, repo: "hexpm", optional: false]}], "hexpm", "e821f541487045c7591a1963eeb42afff0dfa99bdcdbeb3410795a2f59c77d34"}, - "db_connection": {:hex, :db_connection, "2.5.0", "bb6d4f30d35ded97b29fe80d8bd6f928a1912ca1ff110831edcd238a1973652c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c92d5ba26cd69ead1ff7582dbb860adeedfff39774105a4f1c92cbb654b55aa2"}, + "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, - "earmark": {:hex, :earmark, "1.4.38", "ba8fda946c259c6e8f6759d3647d448e9216e2c0afed8c6ae7f8ce1f7072a497", [:mix], [{:earmark_parser, "~> 1.4.32", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "f938e30de4167e7d8f3bf588b01dc041138278dda1e5a13fb9ec89b43dd5ec7f"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.32", "fa739a0ecfa34493de19426681b23f6814573faee95dfd4b4aafe15a7b5b32c6", [:mix], [], "hexpm", "b8b0dd77d60373e77a3d7e8afa598f325e49e8663a51bcc2b88ef41838cca755"}, - "ecto": {:hex, :ecto, "3.10.2", "6b887160281a61aa16843e47735b8a266caa437f80588c3ab80a8a960e6abe37", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6a895778f0d7648a4b34b486af59a1c8009041fbdf2b17f1ac215eb829c60235"}, - "ecto_sql": {:hex, :ecto_sql, "3.10.1", "6ea6b3036a0b0ca94c2a02613fd9f742614b5cfe494c41af2e6571bb034dd94c", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.10.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f6a25bdbbd695f12c8171eaff0851fa4c8e72eec1e98c7364402dda9ce11c56b"}, - "esbuild": {:hex, :esbuild, "0.7.1", "fa0947e8c3c3c2f86c9bf7e791a0a385007ccd42b86885e8e893bdb6631f5169", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "66661cdf70b1378ee4dc16573fcee67750b59761b2605a0207c267ab9d19f13c"}, + "earmark": {:hex, :earmark, "1.4.47", "7e7596b84fe4ebeb8751e14cbaeaf4d7a0237708f2ce43630cfd9065551f94ca", [:mix], [], "hexpm", "3e96bebea2c2d95f3b346a7ff22285bc68a99fbabdad9b655aa9c6be06c698f8"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"}, + "ecto": {:hex, :ecto, "3.12.3", "1a9111560731f6c3606924c81c870a68a34c819f6d4f03822f370ea31a582208", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9efd91506ae722f95e48dc49e70d0cb632ede3b7a23896252a60a14ac6d59165"}, + "ecto_sql": {:hex, :ecto_sql, "3.12.0", "73cea17edfa54bde76ee8561b30d29ea08f630959685006d9c6e7d1e59113b7d", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dc9e4d206f274f3947e96142a8fdc5f69a2a6a9abb4649ef5c882323b6d512f0"}, + "esbuild": {:hex, :esbuild, "0.8.1", "0cbf919f0eccb136d2eeef0df49c4acf55336de864e63594adcea3814f3edf41", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "25fc876a67c13cb0a776e7b5d7974851556baeda2085296c14ab48555ea7560f"}, "ex_aws": {:hex, :ex_aws, "2.1.9", "dc4865ecc20a05190a34a0ac5213e3e5e2b0a75a0c2835e923ae7bfeac5e3c31", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "3e6c776703c9076001fbe1f7c049535f042cb2afa0d2cbd3b47cbc4e92ac0d10"}, - "ex_aws_s3": {:hex, :ex_aws_s3, "2.4.0", "ce8decb6b523381812798396bc0e3aaa62282e1b40520125d1f4eff4abdff0f4", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "85dda6e27754d94582869d39cba3241d9ea60b6aa4167f9c88e309dc687e56bb"}, - "ex_doc": {:hex, :ex_doc, "0.29.4", "6257ecbb20c7396b1fe5accd55b7b0d23f44b6aa18017b415cb4c2b91d997729", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "2c6699a737ae46cb61e4ed012af931b57b699643b24dabe2400a8168414bc4f5"}, - "expo": {:hex, :expo, "0.4.1", "1c61d18a5df197dfda38861673d392e642649a9cef7694d2f97a587b2cfb319b", [:mix], [], "hexpm", "2ff7ba7a798c8c543c12550fa0e2cbc81b95d4974c65855d8d15ba7b37a1ce47"}, - "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, - "floki": {:hex, :floki, "0.34.3", "5e2dcaec5d7c228ce5b1d3501502e308b2d79eb655e4191751a1fe491c37feac", [:mix], [], "hexpm", "9577440eea5b97924b4bf3c7ea55f7b8b6dce589f9b28b096cc294a8dc342341"}, - "gettext": {:hex, :gettext, "0.22.3", "c8273e78db4a0bb6fba7e9f0fd881112f349a3117f7f7c598fa18c66c888e524", [:mix], [{:expo, "~> 0.4.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "935f23447713954a6866f1bb28c3a878c4c011e802bcd68a726f5e558e4b64bd"}, - "hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~> 2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"}, - "heroicons": {:hex, :heroicons, "0.5.3", "ee8ae8335303df3b18f2cc07f46e1cb6e761ba4cf2c901623fbe9a28c0bc51dd", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:phoenix_live_view, ">= 0.18.2", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "a210037e8a09ac17e2a0a0779d729e89c821c944434c3baa7edfc1f5b32f3502"}, + "ex_aws_s3": {:hex, :ex_aws_s3, "2.5.4", "87aaf4a2f24a48f516d7f5aaced9d128dd5d0f655c4431f9037a11a85c71109c", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "c06e7f68b33f7c0acba1361dbd951c79661a28f85aa2e0582990fccca4425355"}, + "ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"}, + "expo": {:hex, :expo, "1.1.0", "f7b9ed7fb5745ebe1eeedf3d6f29226c5dd52897ac67c0f8af62a07e661e5c75", [:mix], [], "hexpm", "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960"}, + "file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"}, + "floki": {:hex, :floki, "0.36.2", "a7da0193538c93f937714a6704369711998a51a6164a222d710ebd54020aa7a3", [:mix], [], "hexpm", "a8766c0bc92f074e5cb36c4f9961982eda84c5d2b8e979ca67f5c268ec8ed580"}, + "gettext": {:hex, :gettext, "0.26.1", "38e14ea5dcf962d1fc9f361b63ea07c0ce715a8ef1f9e82d3dfb8e67e0416715", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "01ce56f188b9dc28780a52783d6529ad2bc7124f9744e571e1ee4ea88bf08734"}, + "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"}, + "heroicons": {:hex, :heroicons, "0.5.6", "95d730e7179c633df32d95c1fdaaecdf81b0da11010b89b737b843ac176a7eb5", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:phoenix_live_view, ">= 0.18.2", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "ca267f02a5fa695a4178a737b649fb6644a2e399639d4ba7964c18e8a58c2352"}, "html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"}, - "html_sanitize_ex": {:hex, :html_sanitize_ex, "1.4.2", "c479398b6de798c03eb5d04a0a9a9159d73508f83f6590a00b8eacba3619cf4c", [:mix], [{:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm", "aef6c28585d06a9109ad591507e508854c5559561f950bbaea773900dd369b0e"}, + "html_sanitize_ex": {:hex, :html_sanitize_ex, "1.4.3", "67b3d9fa8691b727317e0cc96b9b3093be00ee45419ffb221cdeee88e75d1360", [:mix], [{:mochiweb, "~> 2.15 or ~> 3.1", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm", "87748d3c4afe949c7c6eb7150c958c2bcba43fc5b2a02686af30e636b74bccb7"}, "httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, - "imgproxy": {:hex, :imgproxy, "3.0.1", "1789da712c1630648884de032cb43deed9ea5b9637c51e94b7bb3a43dafce00e", [:mix], [], "hexpm", "ac8bb4b7b3c5edb21e7ef0644525bba4528eca4b291ff9db5dd327ab40dc8fde"}, - "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"}, - "let_me": {:hex, :let_me, "1.2.2", "1b7be945ce454d783cbb1d682db0b52fb74ec2ad394b084eebd0011759d4abf7", [:mix], [], "hexpm", "8641770eeb21c177bf3e9d59386d59197d38f8ef35220e0b05e1d7de8f5210f2"}, - "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, - "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"}, - "makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"}, + "imgproxy": {:hex, :imgproxy, "3.0.2", "4ef2fcfe810fb3eda6e58589afd47714fafc850a5dd3f18e07a548c150307f2e", [:mix], [], "hexpm", "ff9f35ef6dd8d1a6ae034f6d5a353fd5a8d79971dddcc830cfb0c9676d0d09dc"}, + "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, + "let_me": {:hex, :let_me, "1.2.4", "147bc35180f729eee9054ac8075b55cf6f4459a2dd94396feb0ce9f386c3131c", [:mix], [], "hexpm", "ec72957f6717fa6d440b5a06e9f42a4a0f5973f1858d1b177cd17e1bd486917e"}, + "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, + "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, - "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, - "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, - "mochiweb": {:hex, :mochiweb, "2.22.0", "f104d6747c01a330c38613561977e565b788b9170055c5241ac9dd6e4617cba5", [:rebar3], [], "hexpm", "cbbd1fd315d283c576d1c8a13e0738f6dafb63dc840611249608697502a07655"}, - "nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"}, + "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, + "mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"}, + "mochiweb": {:hex, :mochiweb, "3.2.2", "bb435384b3b9fd1f92f2f3fe652ea644432877a3e8a81ed6459ce951e0482ad3", [:rebar3], [], "hexpm", "4114e51f1b44c270b3242d91294fe174ce1ed989100e8b65a1fab58e0cba41d5"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, "parallel_stream": {:hex, :parallel_stream, "1.1.0", "f52f73eb344bc22de335992377413138405796e0d0ad99d995d9977ac29f1ca9", [:mix], [], "hexpm", "684fd19191aedfaf387bbabbeb8ff3c752f0220c8112eb907d797f4592d6e871"}, - "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, - "phoenix": {:hex, :phoenix, "1.7.6", "61f0625af7c1d1923d582470446de29b008c0e07ae33d7a3859ede247ddaf59a", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "f6b4be7780402bb060cbc6e83f1b6d3f5673b674ba73cc4a7dd47db0322dfb88"}, - "phoenix_ecto": {:hex, :phoenix_ecto, "4.4.2", "b21bd01fdeffcfe2fab49e4942aa938b6d3e89e93a480d4aee58085560a0bc0d", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "70242edd4601d50b69273b057ecf7b684644c19ee750989fd555625ae4ce8f5d"}, - "phoenix_html": {:hex, :phoenix_html, "3.3.1", "4788757e804a30baac6b3fc9695bf5562465dd3f1da8eb8460ad5b404d9a2178", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "bed1906edd4906a15fd7b412b85b05e521e1f67c9a85418c55999277e553d0d3"}, - "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.0", "0b3158b5b198aa444473c91d23d79f52fb077e807ffad80dacf88ce078fa8df2", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "87785a54474fed91a67a1227a741097eb1a42c2e49d3c0d098b588af65cd410d"}, - "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.4.1", "2aff698f5e47369decde4357ba91fc9c37c6487a512b41732818f2204a8ef1d3", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "9bffb834e7ddf08467fe54ae58b5785507aaba6255568ae22b4d46e2bb3615ab"}, - "phoenix_live_view": {:hex, :phoenix_live_view, "0.19.3", "3918c1b34df8ac71a9a636806ba5b7f053349a0392b312e16f35b0bf4d070aab", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "545626887948495fd8ea23d83b75bd7aaf9dc4221563e158d2c4b52ea1dd7e00"}, + "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, + "phoenix": {:hex, :phoenix, "1.7.14", "a7d0b3f1bc95987044ddada111e77bd7f75646a08518942c72a8440278ae7825", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "c7859bc56cc5dfef19ecfc240775dae358cbaa530231118a9e014df392ace61a"}, + "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.2", "3b83b24ab5a2eb071a20372f740d7118767c272db386831b2e77638c4dcc606d", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "3f94d025f59de86be00f5f8c5dd7b5965a3298458d21ab1c328488be3b5fcd59"}, + "phoenix_html": {:hex, :phoenix_html, "3.3.4", "42a09fc443bbc1da37e372a5c8e6755d046f22b9b11343bf885067357da21cb3", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0249d3abec3714aff3415e7ee3d9786cb325be3151e6c4b3021502c585bf53fb"}, + "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.4", "4508e481f791ce62ec6a096e13b061387158cbeefacca68c6c1928e1305e23ed", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "2984aae96994fbc5c61795a73b8fb58153b41ff934019cfb522343d2d3817d59"}, + "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.5.3", "f2161c207fda0e4fb55165f650f7f8db23f02b29e3bff00ff7ef161d6ac1f09d", [:mix], [{:file_system, "~> 0.3 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b4ec9cd73cb01ff1bd1cac92e045d13e7030330b74164297d1aee3907b54803c"}, + "phoenix_live_view": {:hex, :phoenix_live_view, "0.20.17", "f396bbdaf4ba227b82251eb75ac0afa6b3da5e509bc0d030206374237dfc9450", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a61d741ffb78c85fdbca0de084da6a48f8ceb5261a79165b5a0b59e5f65ce98b"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, - "phoenix_template": {:hex, :phoenix_template, "1.0.1", "85f79e3ad1b0180abb43f9725973e3b8c2c3354a87245f91431eec60553ed3ef", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "157dc078f6226334c91cb32c1865bf3911686f8bcd6bcff86736f6253e6993ee"}, - "phoenix_view": {:hex, :phoenix_view, "2.0.2", "6bd4d2fd595ef80d33b439ede6a19326b78f0f1d8d62b9a318e3d9c1af351098", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "a929e7230ea5c7ee0e149ffcf44ce7cf7f4b6d2bfe1752dd7c084cdff152d36f"}, - "plug": {:hex, :plug, "1.14.2", "cff7d4ec45b4ae176a227acd94a7ab536d9b37b942c8e8fa6dfc0fff98ff4d80", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "842fc50187e13cf4ac3b253d47d9474ed6c296a8732752835ce4a86acdf68d13"}, - "plug_cowboy": {:hex, :plug_cowboy, "2.6.1", "9a3bbfceeb65eff5f39dab529e5cd79137ac36e913c02067dba3963a26efe9b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "de36e1a21f451a18b790f37765db198075c25875c64834bcc82d90b309eb6613"}, - "plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"}, - "postgrex": {:hex, :postgrex, "0.17.1", "01c29fd1205940ee55f7addb8f1dc25618ca63a8817e56fac4f6846fc2cddcbe", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "14b057b488e73be2beee508fb1955d8db90d6485c6466428fe9ccf1d6692a555"}, + "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, + "phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"}, + "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, + "plug_cowboy": {:hex, :plug_cowboy, "2.7.2", "fdadb973799ae691bf9ecad99125b16625b1c6039999da5fe544d99218e662e4", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "245d8a11ee2306094840c000e8816f0cbed69a23fc0ac2bcf8d7835ae019bb2f"}, + "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, + "postgrex": {:hex, :postgrex, "0.19.1", "73b498508b69aded53907fe48a1fee811be34cc720e69ef4ccd568c8715495ea", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "8bac7885a18f381e091ec6caf41bda7bb8c77912bb0e9285212829afe5d8a8f8"}, "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, - "swoosh": {:hex, :swoosh, "1.11.2", "39dd1e44f75bc03a34366d5f830599d248de2b9caaf05704dc76c0507a58c6a1", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4c43f4591503e7d5bf028314af8ac7c06d1c4d340aa23faeefabfa2543fa726e"}, - "tailwind": {:hex, :tailwind, "0.2.1", "83d8eadbe71a8e8f67861fe7f8d51658ecfb258387123afe4d9dc194eddc36b0", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "e8a13f6107c95f73e58ed1b4221744e1eb5a093cd1da244432067e19c8c9a277"}, - "tailwind_formatter": {:hex, :tailwind_formatter, "0.3.6", "f3b02687a79a99106f2cee604d36561091ab5b9c9d16a97ae5901d91b3357047", [:mix], [{:phoenix_live_view, ">= 0.17.6", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}], "hexpm", "3a0d75dad1700f9fa9394185c4ce0eb0eff2b1a0eb9aef66b4b382eae657bded"}, - "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, - "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"}, - "telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"}, + "swoosh": {:hex, :swoosh, "1.17.1", "01295a82bddd2c6cac1e65856e29444d7c23c4501e0ebc69cea8a82018227e25", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3b20d25e580cb79af631335a1bdcfbffd835c08ebcdc16e98577223a241a18a1"}, + "tailwind": {:hex, :tailwind, "0.2.3", "277f08145d407de49650d0a4685dc062174bdd1ae7731c5f1da86163a24dfcdb", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "8e45e7a34a676a7747d04f7913a96c770c85e6be810a1d7f91e713d3a3655b5d"}, + "tailwind_formatter": {:hex, :tailwind_formatter, "0.3.7", "2728d031e6803dfddf63f1dd7c64b5b9fd70ffdf635709c50f47589f4fb48861", [:mix], [{:phoenix_live_view, ">= 0.17.6", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}], "hexpm", "3d91ac4d4622505b09c0f4678512281515b4fbe7644f012da1bd2722f5880185"}, + "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, + "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.2", "2caabe9344ec17eafe5403304771c3539f3b6e2f7fb6a6f602558c825d0d0bfb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9b43db0dc33863930b9ef9d27137e78974756f5f198cae18409970ed6fa5b561"}, + "telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, - "websock": {:hex, :websock, "0.5.2", "b3c08511d8d79ed2c2f589ff430bd1fe799bb389686dafce86d28801783d8351", [:mix], [], "hexpm", "925f5de22fca6813dfa980fb62fd542ec43a2d1a1f83d2caec907483fe66ff05"}, - "websock_adapter": {:hex, :websock_adapter, "0.5.3", "4908718e42e4a548fc20e00e70848620a92f11f7a6add8cf0886c4232267498d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "cbe5b814c1f86b6ea002b52dd99f345aeecf1a1a6964e209d208fb404d930d3d"}, + "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, + "websock_adapter": {:hex, :websock_adapter, "0.5.7", "65fa74042530064ef0570b75b43f5c49bb8b235d6515671b3d250022cb8a1f9e", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "d0f478ee64deddfec64b800673fd6e0c8888b079d9f3444dd96d2a98383bdbd1"}, } From 2b4b40eed821126ff0aef4bcb410d4fbe4e0c963 Mon Sep 17 00:00:00 2001 From: Adrian Salamon Date: Sat, 28 Sep 2024 13:56:06 +0200 Subject: [PATCH 43/56] hotfix: don't force ssl --- config/prod.exs | 1 - 1 file changed, 1 deletion(-) diff --git a/config/prod.exs b/config/prod.exs index 38d8a7d..2c0d7c1 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -49,4 +49,3 @@ config :logger, level: :info # force_ssl: [hsts: true] # # Check `Plug.SSL` for all available options in `force_ssl`. -config :haj, HajWeb.Endpoint, force_ssl: [hsts: true] From fe5cb30a0a1342a2264636ab3df79a4cd1e65a43 Mon Sep 17 00:00:00 2001 From: Adrian Salamon Date: Sun, 29 Sep 2024 23:45:19 +0200 Subject: [PATCH 44/56] dockerize and modularize env variables --- Dockerfile | 19 +++ LICENSE | 2 +- README.md | 57 +++++--- config/.env.example | 17 ++- config/config.exs | 4 - config/dev.exs | 24 ++-- config/runtime.exs | 13 +- docker-compose.yml | 131 ++++++++++++++++++ entrypoint.sh | 40 ++++++ fly.production.toml | 1 + fly.staging.toml | 1 + lib/haj/archive.ex | 8 -- lib/haj/login.ex | 6 +- lib/haj/s3.ex | 17 +++ lib/haj_web/components/layouts/live.html.heex | 2 +- lib/haj_web/components/live_helpers.ex | 2 +- lib/haj_web/controllers/session_controller.ex | 8 +- lib/haj_web/controllers/user_auth.ex | 9 +- .../live/merch_admin_live/form_component.ex | 2 +- .../live/responsibility_live/index.html.heex | 2 +- .../live/responsibility_live/show.html.heex | 4 +- lib/haj_web/live/settings_live/user/index.ex | 2 +- .../live/song_live/edit/form_component.ex | 3 +- lib/haj_web/live/song_live/show.ex | 2 +- lib/haj_web/live/user_live.ex | 2 +- priv/repo/seeds.exs | 3 +- 26 files changed, 310 insertions(+), 71 deletions(-) create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100755 entrypoint.sh create mode 100644 lib/haj/s3.ex diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..834e5c8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +# DEVELOPMENT DOCKERFILE, NOT FOR PRODUCTION +# This Dockerfile is used to build a development image for the Phoenix application +# It only installs the necessary dependencies for development and testing +FROM elixir:latest + +EXPOSE 4000 + +RUN apt-get update && \ + apt-get install -y postgresql-client && \ + apt-get install -y inotify-tools && \ + apt-get install -y nodejs && \ + curl -L https://npmjs.org/install.sh | sh && \ + mix local.hex --force && \ + mix archive.install hex phx_new --force && \ + mix local.rebar --force + +ENV APP_HOME /app +RUN mkdir $APP_HOME +WORKDIR $APP_HOME \ No newline at end of file diff --git a/LICENSE b/LICENSE index 17a924e..ec2129e 100644 --- a/LICENSE +++ b/LICENSE @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. diff --git a/README.md b/README.md index 7d41311..1f6b7ff 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,60 @@ # Haj Public website of [metaspexet.se](https://metaspexet.se) and internal site [haj.metaspexet.se](https://haj.metaspexet.se). Still very much work in progress! -### How to run + +## Development + +### Using Docker + +A full development environment can be started using docker-compose. This will start a seeded postgres database, +a mock login server, a Minio server for file storage, a local imgproxy server and the main Phoenix server. + +```bash +docker-compose up +``` + +Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. + +### Using local environment + +This is considerably more complex, and requires a bit of setup. You will also need access to many environment variables. This is not recommended. Prerequisites: - * You will need an env variables `LOGIN_API_KEY`, and `LOGIN_HOST` set. You need to obtain an api key to `login` (https://login.datasektionen.se). - * In order to run on local machine, you will need to alias DNS records between `localhost.datasektionen.se` and `datasektionen.se` to localhost. - * Our images are served by an [imgproxy](https://github.com/imgproxy/imgproxy) server behind a Cloudfront CDN. You will need to use correct - `IMGPROXY_KEY` and `IMGPROXY_SALT` env variables set. +- You will need an env variables as defined in `config/.env.example` file. Most notably, you will need + a `LOGIN_API_KEY` to be able to login. If you wish to interact with media files (images, audio) you will + need either an S3 bucket or a Minio server running locally. See the `docker-compose.yml` file for an example + of how to run a Minio server locally. +- Our images are served by an [imgproxy](https://github.com/imgproxy/imgproxy) server behind a Cloudfront CDN. You will need to use correct `IMGPROXY_KEY` and `IMGPROXY_SALT` env variables set. You can also run a local imgproxy server by running `docker-compose up imgproxy`. To start your Phoenix server: #### Mac - This process requires docker, if you don't have it you can either install it [here](https://www.docker.com/products/docker-desktop/) or follow the general instructions below. - If you already have a normal postgres installation on your computer, use the general instructions below. +This process requires docker, if you don't have it you can either install it [here](https://www.docker.com/products/docker-desktop/) or follow the general instructions below. +If you already have a normal postgres installation on your computer, use the general instructions below. - 1. Setup environment with ```make mac-install-env``` - 2. Set correct data in the .env file in config/.env - 3. Run ```make start-dev```, this should open up the website (you will have to reload it the first time) +1. Setup environment with `make mac-install-env` +2. Set correct data in the .env file in config/.env +3. Run `make start-dev`, this should open up the website (you will have to reload it the first time) When you have your docker database running, you can use `mix phx.server` to start the server. #### Windows and general instructions - 1. Install dependencies with `mix deps.get` - 2. Install npm dependencies with `cd assets && npm install && cd ..` - 3. Create and migrate your database with `mix ecto.setup` - 4. Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server` +1. Install dependencies with `mix deps.get` +2. Install npm dependencies with `cd assets && npm install && cd ..` +3. Create and migrate your database with `mix ecto.setup` +4. Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server` -Now you can visit [`localhost.datasektionen.se:4001`](http://localhost.datasektionen.se:4001) from your browser. +Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html). ## Learn more - * Official website: https://www.phoenixframework.org/ - * Guides: https://hexdocs.pm/phoenix/overview.html - * Docs: https://hexdocs.pm/phoenix - * Forum: https://elixirforum.com/c/phoenix-forum - * Source: https://github.com/phoenixframework/phoenix +- Official website: https://www.phoenixframework.org/ +- Guides: https://hexdocs.pm/phoenix/overview.html +- Docs: https://hexdocs.pm/phoenix +- Forum: https://elixirforum.com/c/phoenix-forum +- Source: https://github.com/phoenixframework/phoenix diff --git a/config/.env.example b/config/.env.example index 0c48834..edcc5fc 100644 --- a/config/.env.example +++ b/config/.env.example @@ -1,9 +1,12 @@ -export LOGIN_HOST=HOST +export LOGIN_URL=https://login.datasektionen.se +export LOGIN_FRONTEND_URL=https://login.datasektionen.se export LOGIN_API_KEY=VERY_SECRET_API_KEY -export IMGPROXY_KEY=VERY_SECRET_IMGPROXY_KEY -export IMGPROXY_SALT=VERY_SECRET_IMGPROXY_SALT -export AWS_ACCESS_KEY_ID=VERY_SECRET_AWS_ACCESS_KEY_ID -export AWS_SECRET_ACCESS_KEY=VERY_SECRET_AWS_SECRET_ACCESS_KEY +export IMGPROXY_KEY=KINDA_SECRET_KEY +export IMGPROXY_SALT=KINDA_SECRET_SALT +export AWS_ACCESS_KEY_ID=VERY_VERY_SECRET_KEY +export AWS_SECRET_ACCESS_KEY=VERY_VERY_SECRET_SECRET +export AWS_LOCAL=false export API_LOGIN_SECRET=VERY_SECRET_AND_LONG_API_SECRET -export ZFINGER_URL=zfinger.datasektionen.se -export SPAM_API_KEY=VERY_SECRET_SPAM_API_KEY \ No newline at end of file +export ZFINGER_URL=https://zfinger.datasektionen.se +export SPAM_API_KEY=VERY_SECRET_SPAM_API_KEY +export IMAGE_URL=https://url-to-your-imgproxy-instance.com \ No newline at end of file diff --git a/config/config.exs b/config/config.exs index b8da7da..0d8e7f1 100644 --- a/config/config.exs +++ b/config/config.exs @@ -63,10 +63,6 @@ config :tailwind, cd: Path.expand("../assets", __DIR__) ] -config :imgproxy, - # Cloudfront URL - prefix: "https://d3874pm7xaa2tj.cloudfront.net" - # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. import_config "#{config_env()}.exs" diff --git a/config/dev.exs b/config/dev.exs index d54e2f8..a6e50ce 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -2,26 +2,34 @@ import Config # Configure your database config :haj, Haj.Repo, - username: "postgres", - password: "postgres", - hostname: "localhost", - database: "haj_dev", + username: System.get_env("POSTGRES_USER", "postgres"), + password: System.get_env("POSTGRES_PASSWORD", "postgres"), + hostname: System.get_env("POSTGRES_HOST", "localhost"), + database: System.get_env("POSTGRES_DB", "haj_dev"), stacktrace: true, show_sensitive_data_on_connection_error: true, pool_size: 10 config :haj, login_api_key: System.get_env("LOGIN_API_KEY"), - login_host: System.get_env("LOGIN_HOST"), + login_url: System.get_env("LOGIN_URL"), spam_api_key: System.get_env("SPAM_API_KEY"), - hostname: "localhost.datasektionen.se", + login_frontend_url: System.get_env("LOGIN_FRONTEND_URL", "http://localhost:7002"), port: 4001, - api_login_secret: System.get_env("API_LOGIN_SECRET"), + api_login_secret: "usemetologin", zfinger_url: System.get_env("ZFINGER_URL") config :imgproxy, key: System.get_env("IMGPROXY_KEY"), - salt: System.get_env("IMGPROXY_SALT") + salt: System.get_env("IMGPROXY_SALT"), + prefix: System.get_env("IMAGE_URL") + +if System.get_env("AWS_LOCAL") != "false" do + config :ex_aws, :s3, + scheme: System.get_env("AWS_S3_SCHEME", "http://"), + host: System.get_env("AWS_S3_HOST", "localhost"), + port: System.get_env("AWS_S3_PORT", "9000") +end # For development, we disable any cache and enable # debugging and code reloading. diff --git a/config/runtime.exs b/config/runtime.exs index 090b08b..1a44942 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -37,14 +37,19 @@ if config_env() == :prod || config_env() == :staging do socket_options: maybe_ipv6 login_api_key = System.get_env("LOGIN_API_KEY") || raise "LOGIN_API_KEY is missing" - login_host = System.get_env("LOGIN_HOST") || raise "LOGIN_HOST is missing" + login_url = System.get_env("LOGIN_URL") || raise "LOGIN_URL is missing" + + login_frontend_url = + System.get_env("LOGIN_FRONTEND_URL") || raise "LOGIN_FRONTEND_URL is missing" + api_login_secret = System.get_env("API_LOGIN_SECRET") || raise "API_LOGIN_SECRET is missing" zfinger_url = System.get_env("ZFINGER_URL") || "zfinger.datasektionen.se" spam_api_key = System.get_env("SPAM_API_KEY") || raise "SPAM_API_KEY is missing" config :haj, login_api_key: login_api_key, - login_host: login_host, + login_url: login_url, + login_frontend_url: login_frontend_url, api_login_secret: api_login_secret, zfinger_url: zfinger_url, spam_api_key: spam_api_key @@ -52,10 +57,12 @@ if config_env() == :prod || config_env() == :staging do # Variables for imgproxy imgproxy_key = System.get_env("IMGPROXY_KEY") || raise "IMGPROXY_KEY is missing" imgproxy_salt = System.get_env("IMGPROXY_SALT") || raise "IMGPROXY_SALT is missing" + image_url = System.get_env("IMAGE_URL") || raise "IMAGE_URL is missing" config :imgproxy, key: imgproxy_key, - salt: imgproxy_salt + salt: imgproxy_salt, + prefix: image_url # The secret key base is used to sign/encrypt cookies and other secrets. # A default value is used in config/dev.exs and config/test.exs but you diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..68e1e89 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,131 @@ +# Docker-compose file for setting up dependencies for a fully working development environment +# This file is used to start up the following services: +# - Postgres +# - Minio (S3 local development replacement) +# - Imgproxy (for local development) +# - Nyckeln under dörrmattan (login service local replacement) +# - The Phoenix application + +services: + db: + image: postgres:14 + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: postgres + volumes: + - _postgres:/var/lib/postgresql/data + healthcheck: + test: ["CMD", "pg_isready", "-U", "postgres"] + interval: 5s + timeout: 5s + retries: 5 + + minio: + image: minio/minio + environment: + MINIO_DOMAIN: localhost # Needed to enable virtual host style requests + # Encryption stuff + MINIO_KMS_KES_ENDPOINT: https://play.min.io:7373 + MINIO_KMS_KES_KEY_FILE: root.key + MINIO_KMS_KES_CERT_FILE: root.cert + MINIO_KMS_KES_KEY_NAME: my-minio-sse-kms-key + # Root admin user + MINIO_ROOT_USER: admin + MINIO_ROOT_PASSWORD: adminadmin + # We need to download the root.key and root.cert files before starting the server + entrypoint: > + bin/sh -c 'curl -sSL --tlsv1.2 -O "https://raw.githubusercontent.com/minio/kes/master/root.key" -O "https://raw.githubusercontent.com/minio/kes/master/root.cert"; + minio server /data --console-address ":9001";' + volumes: + - _minio:/data + ports: + - "9000:9000" + - "9001:9001" # Console port + healthcheck: + test: "mc ready local" + interval: 2s + timeout: 10s + retries: 5 + + # Service to create buckets and set up encryption on Minio + createbuckets: + image: minio/mc + depends_on: + minio: + condition: service_healthy + entrypoint: > + /bin/sh -c " + mc alias set myminio http://minio:9000 admin adminadmin; + mc admin user add myminio IPGAGmgxUBwvZoHowCez RNHuldJLrZSB0RxmpCEYzLOwN0HRIz9RnC2I4qQG; + mc admin policy attach myminio readwrite --user IPGAGmgxUBwvZoHowCez; + mc mb myminio/metaspexet-haj; + mc encrypt set sse-s3 myminio/metaspexet-haj; + exit 0; + " + + imgproxy: + image: darthsim/imgproxy + environment: + # Imgproxy encryption stuff + IMGPROXY_KEY: "aaaa" + IMGPROXY_SALT: "1b1b" + # Minio configuration + IMGPROXY_USE_S3: true + AWS_ACCESS_KEY_ID: IPGAGmgxUBwvZoHowCez + AWS_SECRET_ACCESS_KEY: RNHuldJLrZSB0RxmpCEYzLOwN0HRIz9RnC2I4qQG + IMGPROXY_S3_ENDPOINT: "http://minio:9000" + IMGPROXY_S3_REGION: "eu-north-1" + IMGPROXY_BASE_URL: "s3://metaspexet-haj" + # Imgproxy configuration + IMGPROXY_TTL: 31536000 + IMGPROXY_MAX_SRC_RESOLUTION: 30 + IMGPROXY_DEVELOPMENT_ERRORS_MODE: true + ports: + - "8080:8080" + + login: + image: ghcr.io/datasektionen/nyckeln-under-dorrmattan + ports: + - 7002:7002 + + app: + build: + context: . + dockerfile: Dockerfile + command: + - "./entrypoint.sh" + environment: + # Database stuff + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: haj_dev + POSTGRES_HOST: db + # Imgproxy stuff + IMGPROXY_KEY: aaaa + IMGPROXY_SALT: 1b1b + IMAGE_URL: http://localhost:8080 + # Minio/S3 stuff + AWS_ACCESS_KEY_ID: IPGAGmgxUBwvZoHowCez + AWS_SECRET_ACCESS_KEY: RNHuldJLrZSB0RxmpCEYzLOwN0HRIz9RnC2I4qQG + # Login stuff + LOGIN_URL: http://login:7002 + LOGIN_FRONTEND_URL: http://localhost:7002 + LOGIN_API_KEY: VERY_SECRET_API_KEY + # Misc + ZFINGER_URL: http://zfinger.datasektionen.se + ports: + - "4000:4000" + volumes: + - .:/app + - _elixir_build:/app/_build # So that we can have separate build files for the host and the container + depends_on: + - db + - minio + - imgproxy + - login + +volumes: + _postgres: + _minio: + _elixir_build: diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100755 index 0000000..57e84a1 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,40 @@ +#!/bin/bash + +# Only used in development as an entrypoint for the Docker container + +set -e + +# Ensure the dependencies are installed +mix deps.get + +if [[ -f assets/package.json ]]; then + # Install frontend dependencies with npm + cd assets + npm install + cd .. +fi + +echo + +# Wait until Postgres is ready +while ! pg_isready -q -h $POSTGRES_HOST -p 5432 -U $POSTGRES_USER +do + echo "$(date) - waiting for database to start" + sleep 2 +done + +export PGPASSWORD=$POSTGRES_PASSWORD + +# Create, migrate, and seed database if it doesn't exist. +if psql -lqt -U $POSTGRES_USER -h $POSTGRES_HOST | cut -d \| -f 1 | grep -qw $POSTGRES_DB; then + echo "Database $POSTGRES_DB exists." +else + echo "Database $POSTGRES_DB does not exist. Creating..." + mix ecto.create + mix ecto.migrate + mix run priv/repo/seeds.exs + echo "Database $POSTGRES_DB created." +fi + +# Start the Phoenix server +mix phx.server \ No newline at end of file diff --git a/fly.production.toml b/fly.production.toml index 51037e9..ba4bd91 100644 --- a/fly.production.toml +++ b/fly.production.toml @@ -14,6 +14,7 @@ processes = [] [env] PHX_HOST = "haj.metaspexet.se" PORT = "8080" + IMAGE_URL = "https://d3874pm7xaa2tj.cloudfront.net" [experimental] allowed_public_ports = [] diff --git a/fly.staging.toml b/fly.staging.toml index c392760..5869431 100644 --- a/fly.staging.toml +++ b/fly.staging.toml @@ -14,6 +14,7 @@ processes = [] [env] PHX_HOST = "betaspexet.se" PORT = "8080" + IMAGE_URL = "https://d3874pm7xaa2tj.cloudfront.net" [experimental] auto_rollback = true diff --git a/lib/haj/archive.ex b/lib/haj/archive.ex index 7f0dad4..c10dd6b 100644 --- a/lib/haj/archive.ex +++ b/lib/haj/archive.ex @@ -132,12 +132,4 @@ defmodule Haj.Archive do end defp after_save(error, _func), do: error - - def s3_url(path) do - {:ok, url} = - ExAws.Config.new(:s3, region: "eu-north-1") - |> ExAws.S3.presigned_url(:get, "metaspexet-haj", path, virtual_host: true) - - url - end end diff --git a/lib/haj/login.ex b/lib/haj/login.ex index efddef9..8868e9b 100644 --- a/lib/haj/login.ex +++ b/lib/haj/login.ex @@ -4,11 +4,11 @@ defmodule Haj.Login do """ def authorize_url() do - login_host = Application.get_env(:haj, :login_host) + login_url = Application.get_env(:haj, :login_url) hostname = Application.get_env(:haj, :hostname) port = Application.get_env(:haj, :port) - callback = URI.encode("https://#{hostname}:#{port}/login/callback/?token=") - "https://#{login_host}/login?callback=#{callback}" + callback = URI.encode("#{hostname}:#{port}/login/callback/?token=") + "#{login_url}/login?callback=#{callback}" end end diff --git a/lib/haj/s3.ex b/lib/haj/s3.ex new file mode 100644 index 0000000..4b6bb11 --- /dev/null +++ b/lib/haj/s3.ex @@ -0,0 +1,17 @@ +defmodule Haj.S3 do + def s3_url(path) do + {:ok, url} = + ExAws.Config.new(:s3, region: "eu-north-1") + |> ExAws.S3.presigned_url(:get, "metaspexet-haj", path, virtual_host: true) + + url + end + + def base_url() do + {:ok, url} = + ExAws.Config.new(:s3, region: "eu-north-1") + |> ExAws.S3.presigned_url(:get, "metaspexet-haj", "test", virtual_host: true) + + url |> String.split("test") |> List.first() + end +end diff --git a/lib/haj_web/components/layouts/live.html.heex b/lib/haj_web/components/layouts/live.html.heex index a3bf706..718abfd 100644 --- a/lib/haj_web/components/layouts/live.html.heex +++ b/lib/haj_web/components/layouts/live.html.heex @@ -93,7 +93,7 @@
    diff --git a/lib/haj_web/components/live_helpers.ex b/lib/haj_web/components/live_helpers.ex index e2bd70c..357ddca 100644 --- a/lib/haj_web/components/live_helpers.ex +++ b/lib/haj_web/components/live_helpers.ex @@ -78,7 +78,7 @@ defmodule HajWeb.LiveHelpers do class="group flex flex-row items-center" > diff --git a/lib/haj_web/controllers/session_controller.ex b/lib/haj_web/controllers/session_controller.ex index 7c55109..0742d04 100644 --- a/lib/haj_web/controllers/session_controller.ex +++ b/lib/haj_web/controllers/session_controller.ex @@ -18,7 +18,7 @@ defmodule HajWeb.SessionController do end defp do_login(conn, _params) do - host = Application.get_env(:haj, :login_host) + login_frontend_url = Application.get_env(:haj, :login_frontend_url) scheme = case get_req_header(conn, "x-forwarded-proto") do @@ -33,7 +33,7 @@ defmodule HajWeb.SessionController do end callback = URI.encode("#{scheme}://#{conn.host}:#{port}/login/callback/?token=") - url = "https://#{host}/login?callback=#{callback}" + url = "#{login_frontend_url}/login?callback=#{callback}" conn |> put_resp_header("location", url) @@ -42,11 +42,11 @@ defmodule HajWeb.SessionController do def callback(conn, %{"token" => token}) do # Gets the users data from the login server - host = Application.get_env(:haj, :login_host) + login_url = Application.get_env(:haj, :login_url) api_key = Application.get_env(:haj, :login_api_key) # TODO, move this to backend Haj.Login module - {:ok, response} = HTTPoison.get("https://#{host}/verify/#{token}.json?api_key=#{api_key}") + {:ok, response} = HTTPoison.get("#{login_url}/verify/#{token}.json?api_key=#{api_key}") case response do %{status_code: 200, body: data} -> diff --git a/lib/haj_web/controllers/user_auth.ex b/lib/haj_web/controllers/user_auth.ex index b043a79..c4412fc 100644 --- a/lib/haj_web/controllers/user_auth.ex +++ b/lib/haj_web/controllers/user_auth.ex @@ -61,8 +61,13 @@ defmodule HajWeb.UserAuth do |> Haj.Spex.preload_user_groups() end) - %Accounts.User{} = new_socket.assigns.current_user - {:cont, new_socket} + case new_socket.assigns.current_user do + %Accounts.User{} = user -> + {:cont, new_socket} + + _ -> + {:halt, redirect_require_login(socket, %{"return_url" => return_to})} + end %{} -> {:halt, redirect_require_login(socket, %{"return_url" => return_to})} diff --git a/lib/haj_web/live/merch_admin_live/form_component.ex b/lib/haj_web/live/merch_admin_live/form_component.ex index 4112ef2..5ed2459 100644 --- a/lib/haj_web/live/merch_admin_live/form_component.ex +++ b/lib/haj_web/live/merch_admin_live/form_component.ex @@ -154,7 +154,7 @@ defmodule HajWeb.MerchAdminLive.FormComponent do meta = %{ uploader: "S3", key: key, - url: "https://#{bucket}.s3-#{config.region}.amazonaws.com", + url: Haj.S3.base_url(), fields: fields } diff --git a/lib/haj_web/live/responsibility_live/index.html.heex b/lib/haj_web/live/responsibility_live/index.html.heex index beb30de..42859f8 100644 --- a/lib/haj_web/live/responsibility_live/index.html.heex +++ b/lib/haj_web/live/responsibility_live/index.html.heex @@ -20,7 +20,7 @@ <%= for user <- responsibility.responsible_users do %>
    <%= full_name(user) %> diff --git a/lib/haj_web/live/responsibility_live/show.html.heex b/lib/haj_web/live/responsibility_live/show.html.heex index 9068d92..87a025a 100644 --- a/lib/haj_web/live/responsibility_live/show.html.heex +++ b/lib/haj_web/live/responsibility_live/show.html.heex @@ -163,7 +163,7 @@
    @@ -296,7 +296,7 @@
    Ingen hade detta ansvar.
    <%= full_name(user) %> diff --git a/lib/haj_web/live/settings_live/user/index.ex b/lib/haj_web/live/settings_live/user/index.ex index 87da024..1e4b7bf 100644 --- a/lib/haj_web/live/settings_live/user/index.ex +++ b/lib/haj_web/live/settings_live/user/index.ex @@ -5,7 +5,7 @@ defmodule HajWeb.SettingsLive.User.Index do @impl true def mount(_params, _session, socket) do - login_secret = Application.get_env(:haj, :api_login_secret) + login_secret = Application.get_env(:haj, :api_login_secret, "secret") {:ok, assign( diff --git a/lib/haj_web/live/song_live/edit/form_component.ex b/lib/haj_web/live/song_live/edit/form_component.ex index 78f7512..d2742af 100644 --- a/lib/haj_web/live/song_live/edit/form_component.ex +++ b/lib/haj_web/live/song_live/edit/form_component.ex @@ -174,7 +174,7 @@ defmodule HajWeb.SongLive.Edit.FormComponent do meta = %{ uploader: "S3", key: key, - url: "https://#{bucket}.s3-#{config.region}.amazonaws.com", + url: Haj.S3.base_url(), fields: fields } @@ -215,4 +215,5 @@ defmodule HajWeb.SongLive.Edit.FormComponent do defp error_to_string(:too_large), do: "Too large, max size is 20MB" defp error_to_string(:too_many_files), do: "You have selected too many files" defp error_to_string(:not_accepted), do: "You have selected an unacceptable file type" + defp error_to_string(other), do: other end diff --git a/lib/haj_web/live/song_live/show.ex b/lib/haj_web/live/song_live/show.ex index 7fe5c4b..dabb9db 100644 --- a/lib/haj_web/live/song_live/show.ex +++ b/lib/haj_web/live/song_live/show.ex @@ -34,7 +34,7 @@ defmodule HajWeb.SongLive.Show do {:noreply, assign(socket, player: true, playing: false) - |> push_event("load", %{timings: song.line_timings, url: Haj.Archive.s3_url(song.file)})} + |> push_event("load", %{timings: song.line_timings, url: Haj.S3.s3_url(song.file)})} end def handle_event("loaded", _params, socket) do diff --git a/lib/haj_web/live/user_live.ex b/lib/haj_web/live/user_live.ex index c93d8de..db2f2bc 100644 --- a/lib/haj_web/live/user_live.ex +++ b/lib/haj_web/live/user_live.ex @@ -19,7 +19,7 @@ defmodule HajWeb.UserLive do
    diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index 727c4f0..ae9b05e 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -28,7 +28,8 @@ user_data = %User{first_name: "Adrian", last_name: "Salamon", username: "asalamon", role: :admin}, %User{first_name: "Hampus", last_name: "Hallkvist", username: "hallkvi", role: :admin}, %User{first_name: "Isak", last_name: "Lefevre", username: "lefevre", role: :admin}, - %User{first_name: "Martin", last_name: "Ryberg Laude", username: "mrl", role: :admin} + %User{first_name: "Martin", last_name: "Ryberg Laude", username: "mrl", role: :admin}, + %User{first_name: "Ture", last_name: "Teknokrat", username: "turetek", role: :admin} ] |> Enum.map(fn user -> %User{user | email: "#{user.username}@kth.se"} From f8627932a56e1612d1965d943c2b26c1fad0958a Mon Sep 17 00:00:00 2001 From: Adrian Salamon Date: Mon, 30 Sep 2024 00:08:58 +0200 Subject: [PATCH 45/56] fix seeds --- lib/haj_web/controllers/user_auth.ex | 2 +- priv/repo/seeds.exs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/haj_web/controllers/user_auth.ex b/lib/haj_web/controllers/user_auth.ex index c4412fc..3778782 100644 --- a/lib/haj_web/controllers/user_auth.ex +++ b/lib/haj_web/controllers/user_auth.ex @@ -62,7 +62,7 @@ defmodule HajWeb.UserAuth do end) case new_socket.assigns.current_user do - %Accounts.User{} = user -> + %Accounts.User{} -> {:cont, new_socket} _ -> diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index ae9b05e..6bcc790 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -127,7 +127,8 @@ Enum.each(applicants, fn user -> Repo.delete!(previous) end - application = Repo.insert!(%Haj.Applications.Application{user: user, show: show}) + application = + Repo.insert!(%Haj.Applications.Application{user: user, show: show, status: :submitted}) application_groups = show_groups |> Enum.shuffle() |> Enum.take(2) From 638fa90fa8468ab3f430add6427db6aac6f34917 Mon Sep 17 00:00:00 2001 From: Adrian Salamon Date: Mon, 30 Sep 2024 00:24:23 +0200 Subject: [PATCH 46/56] working staging config --- fly.production.toml | 3 +++ fly.staging.toml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/fly.production.toml b/fly.production.toml index ba4bd91..b17cbe9 100644 --- a/fly.production.toml +++ b/fly.production.toml @@ -15,6 +15,9 @@ processes = [] PHX_HOST = "haj.metaspexet.se" PORT = "8080" IMAGE_URL = "https://d3874pm7xaa2tj.cloudfront.net" + ZFINGER_URL = "https://d2imehpdli8mvq.cloudfront.net" + LOGIN_URL = "https://login.datasektionen.se" + LOGIN_FRONTEND_URL = "https://login.datasektionen.se" [experimental] allowed_public_ports = [] diff --git a/fly.staging.toml b/fly.staging.toml index 5869431..7c8dc3b 100644 --- a/fly.staging.toml +++ b/fly.staging.toml @@ -15,6 +15,9 @@ processes = [] PHX_HOST = "betaspexet.se" PORT = "8080" IMAGE_URL = "https://d3874pm7xaa2tj.cloudfront.net" + ZFINGER_URL = "https://d2imehpdli8mvq.cloudfront.net" + LOGIN_URL = "https://login.datasektionen.se" + LOGIN_FRONTEND_URL = "https://login.datasektionen.se" [experimental] auto_rollback = true From 8e384b5e7c0ba897a916bd4acf6e0518084b9521 Mon Sep 17 00:00:00 2001 From: Adrian Salamon Date: Mon, 30 Sep 2024 13:31:39 +0200 Subject: [PATCH 47/56] Order applications by updated at (#91) * order applications by updated at * update deploy env vars --- config/runtime.exs | 2 +- fly.production.toml | 2 ++ fly.staging.toml | 2 ++ lib/haj/applications.ex | 6 ++++-- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/config/runtime.exs b/config/runtime.exs index 090b08b..5d94143 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -39,7 +39,7 @@ if config_env() == :prod || config_env() == :staging do login_api_key = System.get_env("LOGIN_API_KEY") || raise "LOGIN_API_KEY is missing" login_host = System.get_env("LOGIN_HOST") || raise "LOGIN_HOST is missing" api_login_secret = System.get_env("API_LOGIN_SECRET") || raise "API_LOGIN_SECRET is missing" - zfinger_url = System.get_env("ZFINGER_URL") || "zfinger.datasektionen.se" + zfinger_url = System.get_env("ZFINGER_URL") || raise "ZFINGER_URL is missing" spam_api_key = System.get_env("SPAM_API_KEY") || raise "SPAM_API_KEY is missing" config :haj, diff --git a/fly.production.toml b/fly.production.toml index 51037e9..17bc634 100644 --- a/fly.production.toml +++ b/fly.production.toml @@ -13,6 +13,8 @@ processes = [] [env] PHX_HOST = "haj.metaspexet.se" + LOGIN_HOST = "login.datasektionen.se" + ZFINGER_URL = "d2imehpdli8mvq.cloudfront.net" PORT = "8080" [experimental] diff --git a/fly.staging.toml b/fly.staging.toml index c392760..c343c7b 100644 --- a/fly.staging.toml +++ b/fly.staging.toml @@ -13,6 +13,8 @@ processes = [] [env] PHX_HOST = "betaspexet.se" + LOGIN_HOST = "login.datasektionen.se" + ZFINGER_URL = "d2imehpdli8mvq.cloudfront.net" PORT = "8080" [experimental] diff --git a/lib/haj/applications.ex b/lib/haj/applications.ex index ad7d091..6133467 100644 --- a/lib/haj/applications.ex +++ b/lib/haj/applications.ex @@ -138,7 +138,8 @@ defmodule Haj.Applications do join: asg in assoc(a, :application_show_groups), where: asg.application_id == a.id, where: asg.show_group_id == ^show_group_id and a.status == ^:submitted, - preload: [user: [], application_show_groups: [show_group: [group: []]]] + preload: [user: [], application_show_groups: [show_group: [group: []]]], + order_by: [asc: a.updated_at] Repo.all(query) end @@ -202,7 +203,8 @@ defmodule Haj.Applications do query = from a in App, where: a.show_id == ^show_id and a.status == ^:submitted, - preload: [application_show_groups: [show_group: [group: []]], user: []] + preload: [application_show_groups: [show_group: [group: []]], user: []], + order_by: [asc: a.updated_at] Repo.all(query) end From 78248f7ec932bf1b0ce31f9ab8e4a8e1d3304523 Mon Sep 17 00:00:00 2001 From: Adrian Salamon Date: Mon, 30 Sep 2024 13:38:33 +0200 Subject: [PATCH 48/56] fix croll overflow issue on mobile (#89) --- lib/haj_web/components/core_components.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/haj_web/components/core_components.ex b/lib/haj_web/components/core_components.ex index 975e5de..c2c8a56 100644 --- a/lib/haj_web/components/core_components.ex +++ b/lib/haj_web/components/core_components.ex @@ -497,8 +497,8 @@ defmodule HajWeb.CoreComponents do end ~H""" -
    - +
    +
    @@ -582,7 +582,7 @@ defmodule HajWeb.CoreComponents do end ~H""" -
    +
    From 70346ded86a493630be902cb414be137d9276083 Mon Sep 17 00:00:00 2001 From: Adrian Salamon Date: Fri, 4 Oct 2024 11:20:39 +0200 Subject: [PATCH 49/56] hotfix: fix markdown parser call --- lib/haj/markdown.ex | 2 +- mix.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/haj/markdown.ex b/lib/haj/markdown.ex index 7cb6e8d..9bfbf34 100644 --- a/lib/haj/markdown.ex +++ b/lib/haj/markdown.ex @@ -1,6 +1,6 @@ defmodule Haj.Markdown do def to_html!(markdown, options \\ []) do - {:ok, ast, _} = EarmarkParser.as_ast(markdown, smartypants: true) + {:ok, ast, _} = Earmark.Parser.as_ast(markdown, smartypants: true) with_ids = Keyword.get(options, :with_ids, false) diff --git a/mix.lock b/mix.lock index a88442a..c76771f 100644 --- a/mix.lock +++ b/mix.lock @@ -6,7 +6,7 @@ "cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, "cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"}, - "credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"}, + "credo": {:hex, :credo, "1.7.8", "9722ba1681e973025908d542ec3d95db5f9c549251ba5b028e251ad8c24ab8c5", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cb9e87cc64f152f3ed1c6e325e7b894dea8f5ef2e41123bd864e3cd5ceb44968"}, "csv": {:hex, :csv, "2.5.0", "c47b5a5221bf2e56d6e8eb79e77884046d7fd516280dc7d9b674251e0ae46246", [:mix], [{:parallel_stream, "~> 1.0.4 or ~> 1.1.0", [hex: :parallel_stream, repo: "hexpm", optional: false]}], "hexpm", "e821f541487045c7591a1963eeb42afff0dfa99bdcdbeb3410795a2f59c77d34"}, "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, From 8a0035e0105578d651fe6badc0a3730bdea4a3a3 Mon Sep 17 00:00:00 2001 From: Adrian Salamon Date: Fri, 11 Oct 2024 15:50:55 +0200 Subject: [PATCH 50/56] hotfix: remove duplicate env var --- fly.production.toml | 1 - fly.staging.toml | 1 - 2 files changed, 2 deletions(-) diff --git a/fly.production.toml b/fly.production.toml index 4b6e792..0dd289f 100644 --- a/fly.production.toml +++ b/fly.production.toml @@ -14,7 +14,6 @@ processes = [] [env] PHX_HOST = "haj.metaspexet.se" LOGIN_HOST = "login.datasektionen.se" - ZFINGER_URL = "d2imehpdli8mvq.cloudfront.net" PORT = "8080" IMAGE_URL = "https://d3874pm7xaa2tj.cloudfront.net" ZFINGER_URL = "https://d2imehpdli8mvq.cloudfront.net" diff --git a/fly.staging.toml b/fly.staging.toml index 36a2bee..b689a1d 100644 --- a/fly.staging.toml +++ b/fly.staging.toml @@ -14,7 +14,6 @@ processes = [] [env] PHX_HOST = "betaspexet.se" LOGIN_HOST = "login.datasektionen.se" - ZFINGER_URL = "d2imehpdli8mvq.cloudfront.net" PORT = "8080" IMAGE_URL = "https://d3874pm7xaa2tj.cloudfront.net" ZFINGER_URL = "https://d2imehpdli8mvq.cloudfront.net" From ab994ffaade54a27e099b640398e67868006eeb8 Mon Sep 17 00:00:00 2001 From: Adrian Salamon Date: Sat, 19 Oct 2024 14:25:44 +0200 Subject: [PATCH 51/56] fix some minio issue --- docker-compose.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 68e1e89..d185788 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -35,8 +35,10 @@ services: MINIO_ROOT_PASSWORD: adminadmin # We need to download the root.key and root.cert files before starting the server entrypoint: > - bin/sh -c 'curl -sSL --tlsv1.2 -O "https://raw.githubusercontent.com/minio/kes/master/root.key" -O "https://raw.githubusercontent.com/minio/kes/master/root.cert"; - minio server /data --console-address ":9001";' + bin/sh -c ' + mkdir -p /root/.minio/certs && cd /root/.minio/certs; + curl -sSL --tlsv1.2 -O "https://raw.githubusercontent.com/minio/kes/master/root.key" -O "https://raw.githubusercontent.com/minio/kes/master/root.cert"; + /bin/minio server /data --console-address ":9001";' volumes: - _minio:/data ports: From 72e82faca45c58477e17f21d7a4fe827ac19e9d3 Mon Sep 17 00:00:00 2001 From: Adrian Salamon Date: Sat, 19 Oct 2024 15:03:51 +0200 Subject: [PATCH 52/56] fix docker compose build issue --- .vscode/extensions.json | 2 +- .vscode/settings.json | 4 ++-- README.md | 6 ++++++ entrypoint.sh | 2 +- priv/repo/seeds.exs | 11 +++++------ 5 files changed, 15 insertions(+), 10 deletions(-) diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 449594c..28933e6 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -4,7 +4,7 @@ // List of extensions which should be recommended for users of this workspace. "recommendations": [ - "JakeBecker.elixir-ls", + "elixir-tools.elixir-tools", "bradlc.vscode-tailwindcss", "phoenixframework.phoenix" ], diff --git a/.vscode/settings.json b/.vscode/settings.json index 3cd09a1..c97bdc4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -12,11 +12,11 @@ }, "[phoenix-heex]": { "editor.formatOnSave": true, - "editor.defaultFormatter": "animus-coop.vscode-elixir-mix-formatter", + "editor.defaultFormatter": "elixir-tools.elixir-tools", "editor.tabSize": 2 }, "[elixir]": { "editor.formatOnSave": true, - "editor.defaultFormatter": "animus-coop.vscode-elixir-mix-formatter" + "editor.defaultFormatter": "elixir-tools.elixir-tools" } } diff --git a/README.md b/README.md index 1f6b7ff..f041d8e 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,12 @@ docker-compose up Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. +If you want an iterative `IEX` shell, connected to the running server, you can run: + +```bash +docker compose exec app iex --sname console --remsh haj +``` + ### Using local environment This is considerably more complex, and requires a bit of setup. You will also need access to many environment variables. This is not recommended. diff --git a/entrypoint.sh b/entrypoint.sh index 57e84a1..31abf16 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -37,4 +37,4 @@ else fi # Start the Phoenix server -mix phx.server \ No newline at end of file +elixir --sname haj -S mix phx.server \ No newline at end of file diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index 6b0bf24..e6aef27 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -37,7 +37,7 @@ user_data = users = Enum.map(user_data, fn user -> - case Repo.one(from u in User, where: u.username == ^user.username) do + case Repo.one(from(u in User, where: u.username == ^user.username)) do nil -> Repo.insert!(user) u -> u end @@ -109,7 +109,7 @@ app_user_data = applicants = Enum.map(app_user_data, fn user -> - case Repo.one(from u in User, where: u.username == ^user.username) do + case Repo.one(from(u in User, where: u.username == ^user.username)) do nil -> Repo.insert!(user) u -> u end @@ -119,8 +119,9 @@ Enum.each(applicants, fn user -> Repo.transaction(fn -> previous = Repo.one( - from a in Haj.Applications.Application, + from(a in Haj.Applications.Application, where: a.show_id == ^show.id and a.user_id == ^user.id + ) ) if previous != nil do @@ -169,8 +170,6 @@ questions = |> Enum.each(fn {q, index} -> Repo.insert!(%Response{ user_id: hd(users).id, - form_id: form.id, - question_id: q.id, - value: Enum.at(answers, index) + form_id: form.id }) end) From 7f15085b12fb981f8617105e73166d83b2418bbe Mon Sep 17 00:00:00 2001 From: Adrian Salamon Date: Sat, 19 Oct 2024 15:04:06 +0200 Subject: [PATCH 53/56] fix credo warnings/suggestions --- lib/haj/accounts.ex | 3 +-- lib/haj/applications.ex | 11 ++++++---- lib/haj/events/event.ex | 2 +- lib/haj/policy/checks.ex | 4 ++-- lib/haj/policy/policy.ex | 6 ++--- lib/haj/slack.ex | 11 ++++++---- lib/haj/spex.ex | 6 ++--- .../controllers/application_controller.ex | 22 ++++++++++++------- .../applications_live/approve_component.ex | 2 +- lib/haj_web/live/apply_live/complete.ex | 3 +-- lib/haj_web/live/apply_live/groups.ex | 3 +-- lib/haj_web/live/group_live/admin.ex | 2 +- lib/haj_web/live/merch_admin_live/orders.ex | 7 ++++-- .../live/settings_live/form/form_component.ex | 4 ++-- lib/haj_web/live/user_live.ex | 2 +- 15 files changed, 50 insertions(+), 38 deletions(-) diff --git a/lib/haj/accounts.ex b/lib/haj/accounts.ex index 16456c2..4569db7 100644 --- a/lib/haj/accounts.ex +++ b/lib/haj/accounts.ex @@ -190,8 +190,7 @@ defmodule Haj.Accounts do Creates a vcard string based on users and show, the show is needed to create organization year """ def to_vcard(users, show) do - Enum.map(users, &user_vcard(&1, show)) - |> Enum.join("\r\n") + Enum.map_join(users, &user_vcard(&1, show), "\r\n") end defp user_vcard(user, show) do diff --git a/lib/haj/applications.ex b/lib/haj/applications.ex index 6133467..b2427c5 100644 --- a/lib/haj/applications.ex +++ b/lib/haj/applications.ex @@ -232,10 +232,13 @@ defmodule Haj.Applications do spex = Spex.current_spex() show_group_names = - Enum.map(application.application_show_groups, fn sg -> - show_groups[sg.show_group_id].group.name - end) - |> Enum.join(", ") + Enum.map_join( + application.application_show_groups, + fn sg -> + show_groups[sg.show_group_id].group.name + end, + ", " + ) """

    Tack för din ansökan!

    diff --git a/lib/haj/events/event.ex b/lib/haj/events/event.ex index 7c48c7a..837a1f3 100644 --- a/lib/haj/events/event.ex +++ b/lib/haj/events/event.ex @@ -13,7 +13,7 @@ defmodule Haj.Events.Event do has_many :ticket_types, Haj.Events.TicketType, on_replace: :delete has_many :registrations, Haj.Events.EventRegistration, on_replace: :delete - belongs_to :form, Haj.Forms.Form + has_one :form, Haj.Forms.Form timestamps() end diff --git a/lib/haj/policy/checks.ex b/lib/haj/policy/checks.ex index 7e440e5..8755367 100644 --- a/lib/haj/policy/checks.ex +++ b/lib/haj/policy/checks.ex @@ -29,7 +29,7 @@ defmodule Haj.Policy.Checks do def own_comment(%User{} = user, %Comment{} = comment), do: comment.user_id == user.id - def is_chef(%User{} = user, %ShowGroup{} = show_group) do - Spex.is_chef_of_show_group?(show_group, user) + def chef?(%User{} = user, %ShowGroup{} = show_group) do + Spex.chef_of_show_group?(show_group, user) end end diff --git a/lib/haj/policy/policy.ex b/lib/haj/policy/policy.ex index 758fc47..75a3631 100644 --- a/lib/haj/policy/policy.ex +++ b/lib/haj/policy/policy.ex @@ -65,12 +65,12 @@ defmodule Haj.Policy do object :show_group do action :edit do - allow :is_chef + allow :chef? allow role: :admin end action :export do - allow :is_chef + allow :chef? allow role: :admin end end @@ -105,7 +105,7 @@ defmodule Haj.Policy do end action :approve do - allow :is_chef + allow :chef? allow role: :admin end end diff --git a/lib/haj/slack.ex b/lib/haj/slack.ex index 0838635..e5eb328 100644 --- a/lib/haj/slack.ex +++ b/lib/haj/slack.ex @@ -19,10 +19,13 @@ defmodule Haj.Slack do def application_message(user, application, show_groups) do show_group_names = - Enum.map(application.application_show_groups, fn sg -> - show_groups[sg.show_group_id].group.name - end) - |> Enum.join(", ") + Enum.map_join( + application.application_show_groups, + fn sg -> + show_groups[sg.show_group_id].group.name + end, + ", " + ) """ #{user.first_name} #{user.last_name} (#{user.email}) sökte just till följande grupper: #{show_group_names}. diff --git a/lib/haj/spex.ex b/lib/haj/spex.ex index ba0f61b..d8a063f 100644 --- a/lib/haj/spex.ex +++ b/lib/haj/spex.ex @@ -359,11 +359,11 @@ defmodule Haj.Spex do ## Examples - iex> is_member_of_show_group?(user_id, show_group_id) + iex> member_of_show_group?(user_id, show_group_id) true """ - def is_member_of_show_group?(user_id, show_group_id) do + def member_of_show_group?(user_id, show_group_id) do Repo.exists?( from sg in ShowGroup, join: gm in assoc(sg, :group_memberships), @@ -371,7 +371,7 @@ defmodule Haj.Spex do ) end - def is_chef_of_show_group?(show_group, user) do + def chef_of_show_group?(show_group, user) do Repo.exists?( from sg in ShowGroup, join: gm in assoc(sg, :group_memberships), diff --git a/lib/haj_web/controllers/application_controller.ex b/lib/haj_web/controllers/application_controller.ex index b282469..f549912 100644 --- a/lib/haj_web/controllers/application_controller.ex +++ b/lib/haj_web/controllers/application_controller.ex @@ -59,16 +59,22 @@ defmodule HajWeb.ApplicationController do end defp all_groups(application) do - Enum.map(application.application_show_groups, fn %{show_group: %{group: group}} -> - group.name - end) - |> Enum.join(", ") + Enum.map_join( + application.application_show_groups, + fn %{show_group: %{group: group}} -> + group.name + end, + ", " + ) end defp special_text(appliaction) do - Enum.map(appliaction.application_show_groups, fn %{special_text: text} -> - text - end) - |> Enum.join(";") + Enum.map_join( + appliaction.application_show_groups, + fn %{special_text: text} -> + text + end, + ";" + ) end end diff --git a/lib/haj_web/live/applications_live/approve_component.ex b/lib/haj_web/live/applications_live/approve_component.ex index d33b045..c42f298 100644 --- a/lib/haj_web/live/applications_live/approve_component.ex +++ b/lib/haj_web/live/applications_live/approve_component.ex @@ -21,7 +21,7 @@ defmodule HajWeb.ApplicationsLive.ApproveComponent do socket.assigns.current_user, show_group ) do - if Spex.is_member_of_show_group?(application.user.id, show_group_id) do + if Spex.member_of_show_group?(application.user.id, show_group_id) do push_flash( :error, "#{application.user.full_name} är redan medlem i #{show_group.group.name}." diff --git a/lib/haj_web/live/apply_live/complete.ex b/lib/haj_web/live/apply_live/complete.ex index 5f802c3..273c0ce 100644 --- a/lib/haj_web/live/apply_live/complete.ex +++ b/lib/haj_web/live/apply_live/complete.ex @@ -125,7 +125,6 @@ defmodule HajWeb.ApplyLive.Complete do end defp group_names(show_groups) do - Enum.map(show_groups, fn {_, sg} -> sg.group.name end) - |> Enum.join(", ") + Enum.map_join(show_groups, fn {_, sg} -> sg.group.name end, ", ") end end diff --git a/lib/haj_web/live/apply_live/groups.ex b/lib/haj_web/live/apply_live/groups.ex index bc2569e..30210fc 100644 --- a/lib/haj_web/live/apply_live/groups.ex +++ b/lib/haj_web/live/apply_live/groups.ex @@ -42,7 +42,6 @@ defmodule HajWeb.ApplyLive.Groups do Spex.get_show_groups_for_show(current_spex.id) |> Enum.filter(fn %{application_open: o} -> o end) - socket = if pre_filled? do put_flash( @@ -108,7 +107,7 @@ defmodule HajWeb.ApplyLive.Groups do |> Enum.map(fn {k, _} -> String.to_integer(k) end) cond do - length(sgs) == 0 -> + Enum.empty?(sgs) -> {:noreply, socket |> put_flash(:error, "Du måste söka minst en grupp!")} !Applications.open?() -> diff --git a/lib/haj_web/live/group_live/admin.ex b/lib/haj_web/live/group_live/admin.ex index 94e676e..0333e71 100644 --- a/lib/haj_web/live/group_live/admin.ex +++ b/lib/haj_web/live/group_live/admin.ex @@ -78,7 +78,7 @@ defmodule HajWeb.GroupLive.Admin do end def add_user(id, socket) do - if Haj.Spex.is_member_of_show_group?(id, socket.assigns.show_group.id) do + if Haj.Spex.member_of_show_group?(id, socket.assigns.show_group.id) do {:noreply, socket |> put_flash(:error, "Användaren är redan med i gruppen.") |> assign(matches: [])} else diff --git a/lib/haj_web/live/merch_admin_live/orders.ex b/lib/haj_web/live/merch_admin_live/orders.ex index 9092247..4ea966f 100644 --- a/lib/haj_web/live/merch_admin_live/orders.ex +++ b/lib/haj_web/live/merch_admin_live/orders.ex @@ -44,7 +44,10 @@ defmodule HajWeb.MerchAdminLive.Orders do end defp list_items(order) do - Enum.map(order.merch_order_items, fn item -> "#{item.count} x #{item.merch_item.name}" end) - |> Enum.join(", ") + Enum.map_join( + order.merch_order_items, + fn item -> "#{item.count} x #{item.merch_item.name}" end, + ", " + ) end end diff --git a/lib/haj_web/live/settings_live/form/form_component.ex b/lib/haj_web/live/settings_live/form/form_component.ex index 37bab44..b877a98 100644 --- a/lib/haj_web/live/settings_live/form/form_component.ex +++ b/lib/haj_web/live/settings_live/form/form_component.ex @@ -47,7 +47,7 @@ defmodule HajWeb.SettingsLive.Form.FormComponent do class="col-span-6" /> -
    +
    Alternativ
    <.input @@ -176,7 +176,7 @@ defmodule HajWeb.SettingsLive.Form.FormComponent do defp notify_parent(msg), do: send(self(), {__MODULE__, msg}) - defp is_select(form_field) do + defp select?(form_field) do form_field.value == "select" || form_field.value == "multi_select" || form_field.value == :select || form_field.value == :multi_select end diff --git a/lib/haj_web/live/user_live.ex b/lib/haj_web/live/user_live.ex index 59ba243..28aca50 100644 --- a/lib/haj_web/live/user_live.ex +++ b/lib/haj_web/live/user_live.ex @@ -65,7 +65,7 @@ defmodule HajWeb.UserLive do end defp display_foods(user) do - prefs = Enum.map(user.foods, fn %{name: name} -> name end) |> Enum.join(", ") + prefs = Enum.map_join(user.foods, fn %{name: name} -> name end, ", ") case user.food_preference_other do nil -> prefs From e8946fd657433d23ad58dcfb64007c584347328b Mon Sep 17 00:00:00 2001 From: Adrian Salamon Date: Sat, 19 Oct 2024 15:41:06 +0200 Subject: [PATCH 54/56] map_join wrong arg order --- lib/haj/accounts.ex | 2 +- lib/haj/applications.ex | 4 ++-- lib/haj/slack.ex | 4 ++-- lib/haj_web/live/apply_live/complete.ex | 2 +- lib/haj_web/live/merch_admin_live/orders.ex | 4 ++-- lib/haj_web/live/user_live.ex | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/haj/accounts.ex b/lib/haj/accounts.ex index 4569db7..fe1e019 100644 --- a/lib/haj/accounts.ex +++ b/lib/haj/accounts.ex @@ -190,7 +190,7 @@ defmodule Haj.Accounts do Creates a vcard string based on users and show, the show is needed to create organization year """ def to_vcard(users, show) do - Enum.map_join(users, &user_vcard(&1, show), "\r\n") + Enum.map_join(users, "\r\n", &user_vcard(&1, show)) end defp user_vcard(user, show) do diff --git a/lib/haj/applications.ex b/lib/haj/applications.ex index b2427c5..19891d0 100644 --- a/lib/haj/applications.ex +++ b/lib/haj/applications.ex @@ -234,10 +234,10 @@ defmodule Haj.Applications do show_group_names = Enum.map_join( application.application_show_groups, + ", ", fn sg -> show_groups[sg.show_group_id].group.name - end, - ", " + end ) """ diff --git a/lib/haj/slack.ex b/lib/haj/slack.ex index e5eb328..cb8174f 100644 --- a/lib/haj/slack.ex +++ b/lib/haj/slack.ex @@ -21,10 +21,10 @@ defmodule Haj.Slack do show_group_names = Enum.map_join( application.application_show_groups, + ", ", fn sg -> show_groups[sg.show_group_id].group.name - end, - ", " + end ) """ diff --git a/lib/haj_web/live/apply_live/complete.ex b/lib/haj_web/live/apply_live/complete.ex index 273c0ce..5e2b793 100644 --- a/lib/haj_web/live/apply_live/complete.ex +++ b/lib/haj_web/live/apply_live/complete.ex @@ -125,6 +125,6 @@ defmodule HajWeb.ApplyLive.Complete do end defp group_names(show_groups) do - Enum.map_join(show_groups, fn {_, sg} -> sg.group.name end, ", ") + Enum.map_join(show_groups, ", ", fn {_, sg} -> sg.group.name end) end end diff --git a/lib/haj_web/live/merch_admin_live/orders.ex b/lib/haj_web/live/merch_admin_live/orders.ex index 4ea966f..c06dba9 100644 --- a/lib/haj_web/live/merch_admin_live/orders.ex +++ b/lib/haj_web/live/merch_admin_live/orders.ex @@ -46,8 +46,8 @@ defmodule HajWeb.MerchAdminLive.Orders do defp list_items(order) do Enum.map_join( order.merch_order_items, - fn item -> "#{item.count} x #{item.merch_item.name}" end, - ", " + ", ", + fn item -> "#{item.count} x #{item.merch_item.name}" end ) end end diff --git a/lib/haj_web/live/user_live.ex b/lib/haj_web/live/user_live.ex index 28aca50..8810d95 100644 --- a/lib/haj_web/live/user_live.ex +++ b/lib/haj_web/live/user_live.ex @@ -65,7 +65,7 @@ defmodule HajWeb.UserLive do end defp display_foods(user) do - prefs = Enum.map_join(user.foods, fn %{name: name} -> name end, ", ") + prefs = Enum.map_join(user.foods, ", ", fn %{name: name} -> name end) case user.food_preference_other do nil -> prefs From 3370066ccd0c75d4a17a3a08f12116c688da5edf Mon Sep 17 00:00:00 2001 From: Adrian Salamon Date: Sat, 19 Oct 2024 23:24:14 +0200 Subject: [PATCH 55/56] checkpoint --- .vscode/settings.json | 4 +- assets/js/app.js | 12 + config/dev.exs | 1 + lib/haj/application.ex | 2 +- lib/haj/events.ex | 73 ++++++- lib/haj/events/event.ex | 4 +- lib/haj/forms.ex | 46 +++- lib/haj/image.ex | 16 -- lib/haj/spex.ex | 70 +++++- lib/haj_web/channels/presence.ex | 1 - lib/haj_web/components/calendar_component.ex | 205 ++++++++++++++++++ lib/haj_web/components/components.ex | 22 +- lib/haj_web/components/layouts.ex | 1 - lib/haj_web/components/live_helpers.ex | 4 + .../controllers/application_controller.ex | 8 +- .../live/applications_live/show.html.heex | 7 +- lib/haj_web/live/dashboard_live/index.ex | 15 +- .../live/dashboard_live/index.html.heex | 6 +- lib/haj_web/live/event_live/index.ex | 91 ++++++-- lib/haj_web/live/event_live/index.html.heex | 10 - .../index.ex} | 5 +- .../index.html.heex} | 60 ++--- .../live/event_live/registrations/show.ex | 113 ++++++++++ lib/haj_web/live/event_live/show.html.heex | 8 +- lib/haj_web/live/form_live/index.ex | 37 ++++ lib/haj_web/live/form_live/index.html.heex | 9 - .../live/merch_admin_live/form_component.ex | 2 +- .../live/merch_admin_live/index.html.heex | 2 +- lib/haj_web/live/merch_live/index.ex | 4 +- lib/haj_web/live/merch_live/index.html.heex | 6 +- .../settings_live/event/form_component.ex | 111 +++++++++- lib/haj_web/live/settings_live/form/index.ex | 56 ++++- .../live/settings_live/form/index.html.heex | 40 ---- lib/haj_web/live/settings_live/form/show.ex | 120 ++++++++++ .../live/settings_live/group/show.html.heex | 5 +- lib/haj_web/live/show_live/show.html.heex | 2 +- .../live/song_live/edit/form_component.ex | 4 +- lib/haj_web/router.ex | 13 +- mix.exs | 2 +- test/haj/archive_test.exs | 15 +- test/haj/events_test.exs | 55 ++++- test/haj/forms_test.exs | 18 +- test/haj_web/live/event_live_test.exs | 27 ++- test/support/fixtures/events_fixtures.ex | 4 +- 44 files changed, 1109 insertions(+), 207 deletions(-) delete mode 100644 lib/haj/image.ex create mode 100644 lib/haj_web/components/calendar_component.ex delete mode 100644 lib/haj_web/live/event_live/index.html.heex rename lib/haj_web/live/event_live/{registrations.ex => registrations/index.ex} (95%) rename lib/haj_web/live/event_live/{registrations.html.heex => registrations/index.html.heex} (75%) create mode 100644 lib/haj_web/live/event_live/registrations/show.ex delete mode 100644 lib/haj_web/live/form_live/index.html.heex delete mode 100644 lib/haj_web/live/settings_live/form/index.html.heex create mode 100644 lib/haj_web/live/settings_live/form/show.ex diff --git a/.vscode/settings.json b/.vscode/settings.json index c97bdc4..3cd09a1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -12,11 +12,11 @@ }, "[phoenix-heex]": { "editor.formatOnSave": true, - "editor.defaultFormatter": "elixir-tools.elixir-tools", + "editor.defaultFormatter": "animus-coop.vscode-elixir-mix-formatter", "editor.tabSize": 2 }, "[elixir]": { "editor.formatOnSave": true, - "editor.defaultFormatter": "elixir-tools.elixir-tools" + "editor.defaultFormatter": "animus-coop.vscode-elixir-mix-formatter" } } diff --git a/assets/js/app.js b/assets/js/app.js index 86adf09..bd88be1 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -145,6 +145,18 @@ topbar.config({ barColors: { 0: "#6F1D1B" }, shadowColor: "rgba(0, 0, 0, .3)" }) window.addEventListener("phx:page-loading-start", (info) => topbar.show()); window.addEventListener("phx:page-loading-stop", (info) => topbar.hide()); + + +// Enable log streaming to browser console +window.addEventListener("phx:live_reload:attached", ({detail: reloader}) => { + // Enable server log streaming to client. + // Disable with reloader.disableServerLogs() + reloader.enableServerLogs() + window.liveReloader = reloader +}) + + + // connect if there are any LiveViews on the page liveSocket.connect(); diff --git a/config/dev.exs b/config/dev.exs index a6e50ce..e5b97f7 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -84,6 +84,7 @@ config :haj, HajWeb.Endpoint, # Watch static and templates for browser reloading. config :haj, HajWeb.Endpoint, live_reload: [ + web_console_logger: true, patterns: [ ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$", ~r"priv/gettext/.*(po)$", diff --git a/lib/haj/application.ex b/lib/haj/application.ex index a7a49b4..870e2ad 100644 --- a/lib/haj/application.ex +++ b/lib/haj/application.ex @@ -17,7 +17,7 @@ defmodule Haj.Application do # Start the Endpoint (http/https) HajWeb.Endpoint, # Start the presence tracker (for online users) - Haj.Presence, + Haj.Presence # Start a worker by calling: Haj.Worker.start_link(arg) # {Haj.Worker, arg} ] diff --git a/lib/haj/events.ex b/lib/haj/events.ex index bacd041..e61af9f 100644 --- a/lib/haj/events.ex +++ b/lib/haj/events.ex @@ -9,6 +9,7 @@ defmodule Haj.Events do alias Haj.Events.Event alias Haj.Events.TicketType alias Haj.Events.EventRegistration + alias Haj.Forms.QuestionResponse @doc """ Returns the list of events. @@ -22,11 +23,44 @@ defmodule Haj.Events do def list_events do query = from e in Event, - preload: [:ticket_types] + preload: [:ticket_types], + order_by: [asc: e.event_date] Repo.all(query) end + @doc """ + Returns a list of events in a given date range. + + ## Examples + + iex> list_events_between(~D[2020-01-01], ~D[2020-12-31]) + [%Event{}, ...] + + """ + + def list_events_between(start_date, end_date) do + start_date = to_datetime(start_date) + end_date = to_datetime(end_date) + + query = + from e in Event, + where: e.event_date >= ^start_date and e.event_date <= ^end_date, + preload: [:ticket_types], + order_by: [asc: e.event_date] + + Repo.all(query) + end + + defp to_datetime(%DateTime{} = dt), do: dt + + defp to_datetime(%Date{} = date) do + Date.to_gregorian_days(date) + |> Kernel.*(86_400) + |> Kernel.+(86_399) + |> DateTime.from_gregorian_seconds() + end + @doc """ Gets a single event. @@ -61,12 +95,13 @@ defmodule Haj.Events do {:error, %Ecto.Changeset{}} """ - def create_event(attrs \\ %{}, opts \\ []) do + def create_event(attrs \\ %{}, after_save \\ &{:ok, &1}, opts \\ []) do with_tickets = Keyword.get(opts, :with_tickets, false) %Event{} |> Event.changeset(attrs, with_tickets: with_tickets) |> Repo.insert() + |> after_save(after_save) end @doc """ @@ -81,12 +116,13 @@ defmodule Haj.Events do {:error, %Ecto.Changeset{}} """ - def update_event(%Event{} = event, attrs, opts \\ []) do + def update_event(%Event{} = event, attrs, after_save \\ &{:ok, &1}, opts \\ []) do with_tickets = Keyword.get(opts, :with_tickets, false) event |> Event.changeset(attrs, with_tickets: with_tickets) |> Repo.update() + |> after_save(after_save) end @doc """ @@ -259,7 +295,7 @@ defmodule Haj.Events do end @doc """ - Gets a single event_registration. + Gets a single event_registration. Loads, user, event, response, question_responses and questions. Raises `Ecto.NoResultsError` if the Event registration does not exist. @@ -272,7 +308,28 @@ defmodule Haj.Events do ** (Ecto.NoResultsError) """ - def get_event_registration!(id), do: Repo.get!(EventRegistration, id) + def get_event_registration!(id) do + query = + from er in EventRegistration, + where: er.id == ^id, + join: u in assoc(er, :user), + join: e in assoc(er, :event), + join: r in assoc(er, :response), + preload: [ + user: u, + event: e, + response: + {r, + question_responses: + ^from(qr in QuestionResponse, + join: q in assoc(qr, :question), + preload: [question: q], + order_by: q.id + )} + ] + + Repo.one!(query) + end @doc """ Creates a event_registration. @@ -381,4 +438,10 @@ defmodule Haj.Events do defp notify_subscribers({:error, reason}, _) do {:error, reason} end + + defp after_save({:ok, result}, func) do + {:ok, _result} = func.(result) + end + + defp after_save(error, _func), do: error end diff --git a/lib/haj/events/event.ex b/lib/haj/events/event.ex index 837a1f3..0ced025 100644 --- a/lib/haj/events/event.ex +++ b/lib/haj/events/event.ex @@ -9,11 +9,11 @@ defmodule Haj.Events.Event do field :name, :string field :purchase_deadline, :utc_datetime field :ticket_limit, :integer - field :has_tickets, :boolean, default: true + field :has_tickets, :boolean, default: false has_many :ticket_types, Haj.Events.TicketType, on_replace: :delete has_many :registrations, Haj.Events.EventRegistration, on_replace: :delete - has_one :form, Haj.Forms.Form + belongs_to :form, Haj.Forms.Form timestamps() end diff --git a/lib/haj/forms.ex b/lib/haj/forms.ex index 4dceddd..0bf2638 100644 --- a/lib/haj/forms.ex +++ b/lib/haj/forms.ex @@ -244,7 +244,17 @@ defmodule Haj.Forms do ** (Ecto.NoResultsError) """ - def get_response!(id), do: Repo.get!(Response, id) + def get_response!(id) do + query = + from r in Response, + where: r.id == ^id, + join: u in assoc(r, :user), + left_join: qr in assoc(r, :question_responses), + left_join: q in assoc(qr, :question), + preload: [user: u, question_responses: {qr, question: q}] + + Repo.one!(query) + end @doc """ Creates a response. @@ -349,6 +359,7 @@ defmodule Haj.Forms do {data, types} |> Ecto.Changeset.cast(attrs, Map.keys(types)) |> validate_required(form.questions) + |> validate_required_multi(form.questions) |> validate_options(form.questions) end @@ -360,6 +371,24 @@ defmodule Haj.Forms do Ecto.Changeset.validate_required(changeset, required) end + defp validate_required_multi(changeset, questions) do + Enum.reduce(questions, changeset, fn q, acc -> + field = String.to_atom("#{q.id}") + + if q.type == :multi_select && q.required do + acc + |> Ecto.Changeset.validate_change(field, fn field, values -> + case values do + [] -> [{field, "At least one value must be selected"}] + _ -> [] + end + end) + else + acc + end + end) + end + defp validate_options(changeset, questions) do Enum.reduce(questions, changeset, fn q, acc -> field = String.to_atom("#{q.id}") @@ -580,4 +609,19 @@ defmodule Haj.Forms do Repo.one(query) end + + @doc """ + Returns all responses for a form. + """ + def list_responses_for(form_id) do + query = + from r in Response, + where: r.form_id == ^form_id, + join: u in assoc(r, :user), + join: qr in assoc(r, :question_responses), + join: q in assoc(qr, :question), + preload: [user: u, question_responses: {qr, question: q}] + + Repo.all(query) + end end diff --git a/lib/haj/image.ex b/lib/haj/image.ex deleted file mode 100644 index 3e546b8..0000000 --- a/lib/haj/image.ex +++ /dev/null @@ -1,16 +0,0 @@ -defmodule Haj.Image do - @moduledoc """ - Module for interacting with images - """ - - def new(path) do - Imgproxy.new("#{path}") |> Imgproxy.set_extension("webp") - end - - # generates a url for path - def video_url(path) do - prefix = Application.get_env(:imgproxy, :prefix) - - "#{prefix}/videos#{path}" - end -end diff --git a/lib/haj/spex.ex b/lib/haj/spex.ex index d8a063f..174f0d5 100644 --- a/lib/haj/spex.ex +++ b/lib/haj/spex.ex @@ -421,6 +421,64 @@ defmodule Haj.Spex do Repo.one!(query) end + @doc """ + Returns map of show group ids and some metadata. The metadata is + the number of members in the show group, and the heads of the group. + + Returns 0 if no memberships are found (even if the show group does not exist). + + ## Examples + + iex> get_show_group_membership_counts([1, 2, 3]) + %{1 => 5, 2 => 3, 3 => 0} + """ + + def get_show_group_membership_metas(show_group_ids) when is_list(show_group_ids) do + count_query = + from sg in ShowGroup, + join: gm in assoc(sg, :group_memberships), + where: sg.id in ^show_group_ids, + group_by: sg.id, + select: {sg.id, count(gm.id)} + + chef_query = + from sg in ShowGroup, + join: gm in assoc(sg, :group_memberships), + join: u in assoc(gm, :user), + where: sg.id in ^show_group_ids and gm.role == :chef, + group_by: sg.id, + select: {sg.id, fragment("array_agg(?)", u.id)} + + counts = Repo.all(count_query) + chefs = Repo.all(chef_query) + + result_map = Enum.into(counts, %{}) + chefs = Enum.into(chefs, %{}) + + Enum.reduce(show_group_ids, %{}, fn show_group_id, acc -> + Map.put(acc, show_group_id, %{ + n_members: Map.get(result_map, show_group_id, 0), + chefs: Map.get(chefs, show_group_id, []) + }) + end) + end + + @doc """ + Returns the membership count for a single show group, or 0 + if it does not exist. + + ## Examples + + iex> get_show_group_membership_count(1) + 5 + + """ + + def get_show_group_membership_meta(show_group_id) do + get_show_group_membership_metas([show_group_id]) + |> Map.get(show_group_id, %{}) + end + @doc """ Creates a show_group. @@ -520,12 +578,22 @@ defmodule Haj.Spex do Repo.preload(users, group_memberships: query) end + @doc """ + Returns all group memberships for a given user. Preloads show group and group. + + ## Examples + + iex> get_group_memberships_for_user(user_id) + [%GroupMembership{}, ...] + """ def get_show_groups_for_user(userid) do query = from sg in ShowGroup, join: gm in assoc(sg, :group_memberships), + join: s in assoc(sg, :show), + join: g in assoc(sg, :group), where: gm.user_id == ^userid, - preload: [show: [], group: [], group_memberships: []] + preload: [show: s, group: g] Repo.all(query) end diff --git a/lib/haj_web/channels/presence.ex b/lib/haj_web/channels/presence.ex index 80c6e23..0802648 100644 --- a/lib/haj_web/channels/presence.ex +++ b/lib/haj_web/channels/presence.ex @@ -1,3 +1,2 @@ defmodule HajWeb.Presence do - end diff --git a/lib/haj_web/components/calendar_component.ex b/lib/haj_web/components/calendar_component.ex new file mode 100644 index 0000000..1195de7 --- /dev/null +++ b/lib/haj_web/components/calendar_component.ex @@ -0,0 +1,205 @@ +defmodule HajWeb.Components.CalendarComponent do + @moduledoc false + use Phoenix.LiveComponent + + @week_start :monday + + def mount(socket) do + current_date = Date.utc_today() + + assigns = [ + current_date: current_date, + selected_date: current_date, + highlighted_dates: [], + dates: dates(current_date), + on_date_pick: fn _ -> nil end + ] + + {:ok, assign(socket, assigns)} + end + + def update(assigns, socket) do + {:ok, assign(socket, assigns)} + end + + def handle_event("prev-month", _params, socket) do + new_date = socket.assigns.selected_date |> Date.beginning_of_month() |> Date.add(-1) + + assign_new_date(socket, new_date) + end + + def handle_event("next-month", _params, socket) do + new_date = socket.assigns.selected_date |> Date.end_of_month() |> Date.add(1) + + assign_new_date(socket, new_date) + end + + def handle_event("pick-date", %{"date" => date}, socket) do + date = Date.from_iso8601!(date) + + if date != socket.assigns.selected_date do + assign_new_date(socket, date) + else + {:noreply, socket} + end + end + + defp assign_new_date(socket, date) do + # Notify parent callback + socket.assigns.on_date_pick.(%{date: date}) + + {:noreply, assign(socket, selected_date: date, dates: dates(date))} + end + + def render(assigns) do + ~H""" +
    +
    + +
    + <%= Calendar.strftime(@selected_date, "%B %Y") %> +
    + +
    +
    +
    <%= day %>
    +
    + +
    + +
    +
    + """ + end + + defp dates(date) do + first = + Date.beginning_of_month(date) + |> Date.beginning_of_week(@week_start) + + last = Date.end_of_month(date) |> Date.end_of_week(@week_start) + + Date.range(first, last) + |> Enum.to_list() + end + + # Always include: "py-1.5 hover:bg-gray-100 focus:z-10" + # Is current month, include: "bg-white" + # Is not current month, include: "bg-gray-50" + # Is selected or is today, include: "font-semibold" + # Is selected, include: "text-white" + # Is not selected, is not today, and is current month, include: "text-gray-900" + # Is not selected, is not today, and is not current month, include: "text-gray-400" + # Is today and is not selected, include: "text-burgandy-600" + + defp button_class_styling(date, current_date, selected_date) do + current_month = Date.beginning_of_month(date) == Date.beginning_of_month(selected_date) + today = Date.to_string(date) == Date.to_string(current_date) + selected = selected_date == date + + ["py-1.5", "hover:bg-gray-100", "focus:z-10"] + |> add_class_if(current_month, "bg-white", "bg-gray-50") + |> add_class_if(selected || today, "font-semibold") + |> add_class_if(selected, "text-white") + |> add_class_if(!selected && !today && current_month, "text-gray-900") + |> add_class_if(!selected && !today && !current_month, "text-gray-400") + |> add_class_if(today && !selected, "text-burgandy-600") + |> Enum.join(" ") + end + + # Always include: "mx-auto flex h-7 w-7 items-center justify-center rounded-full" + # Is selected and is today, include: "bg-burgandy-600" + # Is selected and is not today, include: "bg-gray-900" + defp time_styling(date, current_date, selected_date, highlighted_dates \\ []) do + today = Date.to_string(date) == Date.to_string(current_date) + selected = selected_date == date + + ["mx-auto", "flex", "h-7", "w-7", "items-center", "justify-center", "rounded-full"] + |> add_class_if(date in highlighted_dates && !selected, "bg-burgandy-100") + |> add_class_if(selected && today, "bg-burgandy-600") + |> add_class_if(selected && !today, "bg-gray-900") + |> Enum.join(" ") + end + + # Top left day, include: "rounded-tl-lg" + # Top right day, include: "rounded-tr-lg" + # Bottom left day, include: "rounded-bl-lg" + # Bottom right day, include: "rounded-br-lg" + + defp button_corner_styling(index, total_dates) do + last_row = div(total_dates, 7) - 1 + + cond do + index == 0 -> "rounded-tl-lg" + index == 6 -> "rounded-tr-lg" + index == last_row * 7 -> "rounded-bl-lg" + index == last_row * 7 + 6 -> "rounded-br-lg" + true -> "" + end + end + + defp add_class_if(classes, condition, class, else_class \\ nil) do + cond do + condition -> [class | classes] + else_class != nil -> [else_class | classes] + true -> classes + end + end +end diff --git a/lib/haj_web/components/components.ex b/lib/haj_web/components/components.ex index 9e9a9fb..7ef9838 100644 --- a/lib/haj_web/components/components.ex +++ b/lib/haj_web/components/components.ex @@ -84,20 +84,25 @@ defmodule HajWeb.Components do @doc """ A generic form component that works with Changesets generated from Haj.Form. + + Question types are: :select, :multi_select, :text_area, :text """ attr :question, :any, required: true attr :field, :any, required: true + attr :class, :string, default: "" def form_input(%{question: %{type: :select}} = assigns) do ~H""" - <.input field={@field} type="select" options={@question.options} label={@question.name} /> +
    + <.input field={@field} type="select" options={@question.options} label={@question.name} /> +
    """ end def form_input(%{question: %{type: :multi_select}} = assigns) do ~H""" -
    +
    @@ -112,14 +117,25 @@ defmodule HajWeb.Components do label={option} />
    + <.error :for={msg <- Enum.map(assigns.field.errors, &translate_error(&1))}><%= msg %>
    """ end + def form_input(%{question: %{type: :text_area}} = assigns) do + ~H""" +
    + <.input field={@field} type="textarea" label={@question.name} /> +
    + """ + end + def form_input(assigns) do ~H""" - <.input field={@field} type="text" label={@question.name} /> +
    + <.input field={@field} type="text" label={@question.name} /> +
    """ end diff --git a/lib/haj_web/components/layouts.ex b/lib/haj_web/components/layouts.ex index 21d6791..d8d366a 100644 --- a/lib/haj_web/components/layouts.ex +++ b/lib/haj_web/components/layouts.ex @@ -153,7 +153,6 @@ defmodule HajWeb.Layouts do title="Formulär" active={@active_tab == {:setting, :forms}} /> -
    """ diff --git a/lib/haj_web/components/live_helpers.ex b/lib/haj_web/components/live_helpers.ex index 5d69828..7d3c7eb 100644 --- a/lib/haj_web/components/live_helpers.ex +++ b/lib/haj_web/components/live_helpers.ex @@ -179,4 +179,8 @@ defmodule HajWeb.LiveHelpers do {"Måndag", "Tisdag", "Onsdag", "Torsdag", "Fredag", "Lördag", "Söndag"} |> elem(day - 1) end + + def image_url(path, w, h, options \\ []) do + Imgproxy.new(path) |> Imgproxy.resize(w, h, options) |> to_string() + end end diff --git a/lib/haj_web/controllers/application_controller.ex b/lib/haj_web/controllers/application_controller.ex index f549912..0e5f684 100644 --- a/lib/haj_web/controllers/application_controller.ex +++ b/lib/haj_web/controllers/application_controller.ex @@ -61,20 +61,20 @@ defmodule HajWeb.ApplicationController do defp all_groups(application) do Enum.map_join( application.application_show_groups, + ", ", fn %{show_group: %{group: group}} -> group.name - end, - ", " + end ) end defp special_text(appliaction) do Enum.map_join( appliaction.application_show_groups, + ";", fn %{special_text: text} -> text - end, - ";" + end ) end end diff --git a/lib/haj_web/live/applications_live/show.html.heex b/lib/haj_web/live/applications_live/show.html.heex index 1f32d5c..895c25b 100644 --- a/lib/haj_web/live/applications_live/show.html.heex +++ b/lib/haj_web/live/applications_live/show.html.heex @@ -1,5 +1,5 @@
    -
    +

    Ansökan

    @@ -14,8 +14,8 @@

    - <.field name="Namn"> - <%= @application.user.full_name %> + <.field name="Person"> + <.user_card user={@application.user} /> <.field name="Klass"> <%= @application.user.class %> @@ -54,7 +54,6 @@ <.field large name="Övrigt"> Inget svar - <%= @application.other %> <.field diff --git a/lib/haj_web/live/dashboard_live/index.ex b/lib/haj_web/live/dashboard_live/index.ex index 9396dcd..b46c7bf 100644 --- a/lib/haj_web/live/dashboard_live/index.ex +++ b/lib/haj_web/live/dashboard_live/index.ex @@ -14,6 +14,9 @@ defmodule HajWeb.DashboardLive.Index do Spex.get_show_groups_for_user(user_id) |> Enum.filter(fn %{show: show} -> show.id == current_show.id end) + membership_meta = + Enum.map(user_groups, fn %{id: id} -> id end) |> Spex.get_show_group_membership_metas() + merch_order_items = Merch.get_current_merch_order_items(user_id) responsibilities = @@ -25,6 +28,7 @@ defmodule HajWeb.DashboardLive.Index do {:ok, assign(socket, user_groups: user_groups, + membership_meta: membership_meta, merch_order_items: merch_order_items, responsibilities: responsibilities, page_title: "Hem" @@ -41,7 +45,7 @@ defmodule HajWeb.DashboardLive.Index do > <%= if @order_item.merch_item.image do %> Imgproxy.resize(800, 800) |> to_string()} + src={image_url(@order_item.merch_item.image, 800, 800)} alt={@order_item.merch_item.name} class="absolute inset-0 h-full w-full object-cover brightness-50 duration-300 ease-in-out group-hover:scale-105" /> @@ -58,4 +62,13 @@ defmodule HajWeb.DashboardLive.Index do """ end + + defp get_role(user_id, show_group_id, membership_meta) do + chefs = Map.get(membership_meta, show_group_id, %{chefs: []})[:chefs] + + case user_id in chefs do + true -> "Chef" + false -> "Gruppis" + end + end end diff --git a/lib/haj_web/live/dashboard_live/index.html.heex b/lib/haj_web/live/dashboard_live/index.html.heex index 4acd094..96ebed8 100644 --- a/lib/haj_web/live/dashboard_live/index.html.heex +++ b/lib/haj_web/live/dashboard_live/index.html.heex @@ -19,11 +19,9 @@ <:subtitle> - <%= length(show_group.group_memberships) %> medlemmar + <%= @membership_meta[show_group.id].n_members %> medlemmar - Du är <%= Enum.find(show_group.group_memberships, fn %{user_id: user_id} -> - user_id == @current_user.id - end).role %> + Du är <%= get_role(@current_user.id, show_group.id, @membership_meta) %>
    diff --git a/lib/haj_web/live/event_live/index.ex b/lib/haj_web/live/event_live/index.ex index 78e8650..0e5c415 100644 --- a/lib/haj_web/live/event_live/index.ex +++ b/lib/haj_web/live/event_live/index.ex @@ -5,21 +5,40 @@ defmodule HajWeb.EventLive.Index do @impl true def mount(_params, _session, socket) do - {:ok, assign(socket, events: list_events())} + {:ok, socket} + end + + @impl true + def handle_params(%{"date" => date}, _url, socket) do + date = Date.from_iso8601!(date) + + {:noreply, + assign(socket, + selected_date: date, + events: events_in_month(date) + )} + end + + def handle_params(_params, _url, socket) do + date = Date.utc_today() + + {:noreply, + assign(socket, + selected_date: date, + events: events_in_month(date) + )} end @impl true def handle_info({Events, [:registration, _], _}, socket) do - {:noreply, assign(socket, events: list_events())} + {:noreply, assign(socket, events: events_in_month(socket.assigns.selected_date))} end - defp list_events do - # Todo: maybe fetch in one query - Events.list_events() - |> Enum.map(fn event -> - ticked_sold = Events.tickets_sold(event.id) - Map.put(event, :availible, event.ticket_limit - ticked_sold) - end) + def handle_info({:updated_date, %{date: date}}, socket) do + url = Routes.event_index_path(socket, :index, %{date: Date.to_iso8601(date)}) + socket = push_patch(socket, to: url) + + {:noreply, socket} end @impl true @@ -36,20 +55,45 @@ defmodule HajWeb.EventLive.Index do end end + @impl Phoenix.LiveView + def render(assigns) do + ~H""" +
    +
    + Events + Partaj och annat skoj +
    + +
    + <.live_component + module={HajWeb.Components.CalendarComponent} + id="event-calendar" + highlighted_dates={@events |> Enum.map(&DateTime.to_date(&1.event_date))} + on_date_pick={fn date -> send(self(), {:updated_date, date}) end} + /> + +
      +
      + <.event_card :for={event <- @events} event={event} /> +
      + Inga planerade events den här månaden! +
      +
      +
    +
    +
    + """ + end + defp event_card(assigns) do ~H""" - <.link navigate={~p"/events/#{@event}"}> -
    -
    - {"Picture + <.link navigate={~p"/events/#{@event}"} class="w-full"> +
    +
    @@ -68,4 +112,11 @@ defmodule HajWeb.EventLive.Index do """ end + + defp events_in_month(date) do + month_start = Date.beginning_of_month(date) + month_end = Date.end_of_month(date) + + Events.list_events_between(month_start, month_end) + end end diff --git a/lib/haj_web/live/event_live/index.html.heex b/lib/haj_web/live/event_live/index.html.heex deleted file mode 100644 index c336964..0000000 --- a/lib/haj_web/live/event_live/index.html.heex +++ /dev/null @@ -1,10 +0,0 @@ -
    -
    - Events - Partaj och annat skoj -
    - -
    - <.event_card :for={event <- @events} event={event} /> -
    -
    diff --git a/lib/haj_web/live/event_live/registrations.ex b/lib/haj_web/live/event_live/registrations/index.ex similarity index 95% rename from lib/haj_web/live/event_live/registrations.ex rename to lib/haj_web/live/event_live/registrations/index.ex index ac0dd69..61d9e35 100644 --- a/lib/haj_web/live/event_live/registrations.ex +++ b/lib/haj_web/live/event_live/registrations/index.ex @@ -1,4 +1,4 @@ -defmodule HajWeb.EventLive.Registrations do +defmodule HajWeb.EventLive.Registrations.Index do use HajWeb, :live_view on_mount {HajWeb.UserAuth, {:authorize, :event_registrations_read}} @@ -16,7 +16,8 @@ defmodule HajWeb.EventLive.Registrations do registrations = Events.list_event_registrations(id) most_popular = - Enum.flat_map(registrations, & &1.user.group_memberships) + Enum.filter(registrations, & &1.attending) + |> Enum.flat_map(& &1.user.group_memberships) |> Enum.map(& &1.show_group.group) |> Enum.frequencies() |> Enum.sort_by(fn {_, c} -> c end, &>=/2) diff --git a/lib/haj_web/live/event_live/registrations.html.heex b/lib/haj_web/live/event_live/registrations/index.html.heex similarity index 75% rename from lib/haj_web/live/event_live/registrations.html.heex rename to lib/haj_web/live/event_live/registrations/index.html.heex index e0ec4d6..129b314 100644 --- a/lib/haj_web/live/event_live/registrations.html.heex +++ b/lib/haj_web/live/event_live/registrations/index.html.heex @@ -14,7 +14,7 @@ <.input type="select" name="tab" - options={[{"Anmälda", :users}, {"Frågor", :questions}, {"Matpreferenser", :food}]} + options={[{"Svar", :users}, {"Frågor", :questions}, {"Matpreferenser", :food}]} value={@live_action} /> @@ -24,7 +24,7 @@
    -<.table :if={@live_action == :users} small id="registrations_table" rows={@registrations}> +<.table + :if={@live_action == :users} + small + id="registrations_table" + rows={@registrations} + row_click={fn reg -> JS.navigate(~p"/events/registrations/#{reg.id}") end} +> <:col :let={reg} label="Namn"> <%= reg.user.full_name %> @@ -81,7 +87,7 @@ <%= group.name %>
    -
    +
    Inga svar
    @@ -98,40 +104,38 @@ <%= class %>
    -
    +
    Inga svar
    - <%= if @event.form do %> -
    - <%= question.name %> -
    - <%= if @response_counts[question.id] do %> -
    -
    - <%= count %> -
    -
    - <%= answer %> -
    +
    + <%= question.name %> +
    + <%= if @response_counts[question.id] do %> +
    +
    + <%= count %>
    - <% else %> -
    - Inga svar +
    + <%= answer %>
    - <% end %> -
    +
    + <% else %> +
    + Inga svar +
    + <% end %>
    - <% end %> +
    - + Inga anmälda med matpreferenser @@ -139,7 +143,7 @@ <.stats_card :for={{food, count} <- @food_counts} title={food} value={count} /> -
    0} class="mt-4 rounded-lg border p-4"> +
    Övriga <.table diff --git a/lib/haj_web/live/event_live/registrations/show.ex b/lib/haj_web/live/event_live/registrations/show.ex new file mode 100644 index 0000000..a6d8bfc --- /dev/null +++ b/lib/haj_web/live/event_live/registrations/show.ex @@ -0,0 +1,113 @@ +defmodule HajWeb.EventLive.Registrations.Show do + use HajWeb, :live_view + + alias Haj.Events + alias Haj.Spex + + @impl Phoenix.LiveView + def mount(%{"id" => registration_id}, _session, socket) do + registration = Events.get_event_registration!(registration_id) + group_memberships = Spex.get_show_groups_for_user(registration.user_id) + + {:ok, + assign(socket, + registration: registration, + memberships: group_memberships, + page_title: "Anmälan" + )} + end + + @impl Phoenix.LiveView + def render(assigns) do + ~H""" +
    +
    +
    +

    Anmälan

    +

    + Till eventet <%= @registration.event.name %> +

    +
    +
    + +
    +
    + <.field name="Person" top> + <.user_card user={@registration.user} /> + + <.field name="Klass" top> + <%= @registration.user.class %> + + <.field name="Email"> + <%= @registration.user.email %> + + <.field name="Telefon"> + <%= @registration.user.phone %> + + <.field name="Tid för anmälan"> + <%= @registration.updated_at %> + + <.field name="Grupper"> +
    + <.link :for={show_group <- @memberships} navigate={~p"/group/#{show_group}"}> +
    + <%= show_group.group.name %> +
    + +
    + +
    +
    + +
    +
    +

    Formulärsvar

    +

    + Alla svar som användaren har fyllt i. +

    +
    +
    + +
    +
    + <.field large name="Kommer på eventet?" top> + <%= format_bool(@registration.attending) %> + + + <.field :for={qr <- @registration.response.question_responses} large name={qr.question.name}> + <%= qr.answer %> + +
    +
    +
    + """ + end + + attr(:large, :boolean, default: false) + attr(:top, :boolean, default: false) + attr(:name, :string) + slot(:inner_block) + + defp field(assigns) do + ~H""" +
    +
    <%= @name %>
    +
    + <%= render_slot(@inner_block) %> +
    +
    + """ + end + + defp format_bool(true), do: "Ja" + defp format_bool(false), do: "Nej" +end diff --git a/lib/haj_web/live/event_live/show.html.heex b/lib/haj_web/live/event_live/show.html.heex index bb11bcf..9b22b92 100644 --- a/lib/haj_web/live/event_live/show.html.heex +++ b/lib/haj_web/live/event_live/show.html.heex @@ -1,9 +1,13 @@
    + {"Picture - {"Picture
    <%!-- Main content --%> diff --git a/lib/haj_web/live/form_live/index.ex b/lib/haj_web/live/form_live/index.ex index beb323f..74ffec2 100644 --- a/lib/haj_web/live/form_live/index.ex +++ b/lib/haj_web/live/form_live/index.ex @@ -2,12 +2,44 @@ defmodule HajWeb.FormLive.Index do alias Haj.Forms use HajWeb, :live_view + @impl true def mount(%{"id" => id}, _session, socket) do changeset = Forms.get_form_changeset!(id, %{}) form = Forms.get_form!(id) {:ok, assign(socket, form: form) |> assign_form(changeset)} end + @impl true + def render(assigns) do + ~H""" +
    +
    + Formulär + Här kan du fylla i formuläret: <%= @form.name %> +
    + +
    +

    Beskrivning

    +

    <%= @form.description %>

    +
    + <.form for={@response_form} phx-submit="save" phx-change="validate"> +
    +
    + <.form_input question={question} field={@response_form[String.to_atom("#{question.id}")]} /> +
    + <%= question.description %> +
    +
    +
    +
    + <.button phx-disable-with="Sparar...">Spara +
    + +
    + """ + end + + @impl true def handle_event("validate", %{"form_response" => response}, socket) do response = flatten_response(response) @@ -45,4 +77,9 @@ defmodule HajWeb.FormLive.Index do defp assign_form(socket, %Ecto.Changeset{} = changeset) do assign(socket, :response_form, to_form(changeset, as: :form_response)) end + + defp styling_for_question(%{type: :multi_select}), do: "sm:col-span-4" + defp styling_for_question(%{type: :select}), do: "sm:col-span-3" + defp styling_for_question(%{type: :text_area}), do: "sm:col-span-6" + defp styling_for_question(%{type: :text}), do: "sm:col-span-4" end diff --git a/lib/haj_web/live/form_live/index.html.heex b/lib/haj_web/live/form_live/index.html.heex deleted file mode 100644 index 08fa408..0000000 --- a/lib/haj_web/live/form_live/index.html.heex +++ /dev/null @@ -1,9 +0,0 @@ -<.form for={@response_form} phx-submit="save" phx-change="validate"> - <.form_input - :for={question <- @form.questions} - question={question} - field={@response_form[String.to_atom("#{question.id}")]} - /> - - <.button phx-disable-with="Sparar...">Spara - diff --git a/lib/haj_web/live/merch_admin_live/form_component.ex b/lib/haj_web/live/merch_admin_live/form_component.ex index 5ed2459..f968406 100644 --- a/lib/haj_web/live/merch_admin_live/form_component.ex +++ b/lib/haj_web/live/merch_admin_live/form_component.ex @@ -192,7 +192,7 @@ defmodule HajWeb.MerchAdminLive.FormComponent do
    <%= if @image do %> - Imgproxy.resize(400, 400) |> to_string()} /> + <% else %>
    <% end %> diff --git a/lib/haj_web/live/merch_admin_live/index.html.heex b/lib/haj_web/live/merch_admin_live/index.html.heex index 77821c8..0598302 100644 --- a/lib/haj_web/live/merch_admin_live/index.html.heex +++ b/lib/haj_web/live/merch_admin_live/index.html.heex @@ -31,7 +31,7 @@
    <%= if merch_item.image do %> - Imgproxy.resize(400, 400) |> to_string()} /> + <% else %>
    <% end %> diff --git a/lib/haj_web/live/merch_live/index.ex b/lib/haj_web/live/merch_live/index.ex index abe4b6c..f8abfab 100644 --- a/lib/haj_web/live/merch_live/index.ex +++ b/lib/haj_web/live/merch_live/index.ex @@ -74,9 +74,9 @@ defmodule HajWeb.MerchLive.Index do > <%= if @item.image do %> Imgproxy.resize(800, 800) |> to_string()} + src={image_url(@item.image, 800, 800)} alt={@item.name} - class="absolute inset-0 h-full w-full object-cover brightness-50 bg-white duration-300 ease-in-out group-hover:scale-105" + class="absolute inset-0 h-full w-full bg-white object-cover brightness-50 duration-300 ease-in-out group-hover:scale-105" /> <% else %>
    diff --git a/lib/haj_web/live/merch_live/index.html.heex b/lib/haj_web/live/merch_live/index.html.heex index 8147ce4..d866c71 100644 --- a/lib/haj_web/live/merch_live/index.html.heex +++ b/lib/haj_web/live/merch_live/index.html.heex @@ -29,11 +29,7 @@
    <%= if @merch_item.image do %> Imgproxy.resize(1000, 1000, type: "fill-down") - |> to_string() - } + src={image_url(@merch_item.image, 1000, 1000, type: "fill-down")} alt={@merch_item.name} class="rounded-lg" /> diff --git a/lib/haj_web/live/settings_live/event/form_component.ex b/lib/haj_web/live/settings_live/event/form_component.ex index 18c3bde..bc2f22f 100644 --- a/lib/haj_web/live/settings_live/event/form_component.ex +++ b/lib/haj_web/live/settings_live/event/form_component.ex @@ -1,9 +1,8 @@ -require Logger - defmodule HajWeb.SettingsLive.Event.FormComponent do use HajWeb, :live_component alias Haj.Events + alias Haj.Events.Event @impl true def mount(socket) do @@ -32,7 +31,39 @@ defmodule HajWeb.SettingsLive.Event.FormComponent do <.input field={@form[:ticket_limit]} type="number" label="Biljettgräns" /> <.input field={@form[:event_date]} type="datetime-local" label="Datum" /> <.input field={@form[:purchase_deadline]} type="datetime-local" label="Köpdeadline" /> - <.input field={@form[:image]} type="text" label="Bild" /> + +
    + + Bild + +
    + <.live_file_input upload={@uploads.file} /> +
    + <%= for entry <- @uploads.file.entries do %> +
    +
    + <.live_img_preview entry={entry} /> +
    <%= entry.client_name %>
    +
    + + + + <%!-- Phoenix.Component.upload_errors/2 returns a list of error atoms --%> + <%= for err <- upload_errors(@uploads.file, entry) do %> +

    <%= error_to_string(err) %>

    + <% end %> +
    + <% end %> +
    + <.input field={@form[:has_tickets]} type="checkbox" @@ -112,6 +143,13 @@ defmodule HajWeb.SettingsLive.Event.FormComponent do {:ok, socket + |> assign(:uploaded_files, []) + |> allow_upload(:file, + accept: ~w(.jpg .jpeg), + max_file_size: 8_000_000, + max_entries: 1, + external: &presign_upload/2 + ) |> assign(assigns) |> assign_form(changeset)} end @@ -128,6 +166,8 @@ defmodule HajWeb.SettingsLive.Event.FormComponent do @impl true def handle_event("save", %{"event" => event_params}, socket) do + event_params = put_image_url(event_params, socket) + save_event(socket, socket.assigns.action, event_params) end @@ -136,12 +176,19 @@ defmodule HajWeb.SettingsLive.Event.FormComponent do delete_ticket_type(socket, ticket_type) end + @impl true + def handle_event("cancel-upload", %{"ref" => ref}, socket) do + {:noreply, cancel_upload(socket, :file, ref)} + end + defp assign_form(socket, %Ecto.Changeset{} = changeset) do assign(socket, :form, to_form(changeset)) end defp save_event(socket, :edit, event_params) do - case Events.update_event(socket.assigns.event, event_params, with_tickets: true) do + case Events.update_event(socket.assigns.event, event_params, &consume_images(socket, &1), + with_tickets: true + ) do {:ok, _event} -> {:noreply, socket @@ -154,7 +201,7 @@ defmodule HajWeb.SettingsLive.Event.FormComponent do end defp save_event(socket, :new, event_params) do - case Events.create_event(event_params, with_tickets: true) do + case Events.create_event(event_params, &consume_images(socket, &1), with_tickets: true) do {:ok, _event} -> {:noreply, socket @@ -178,4 +225,58 @@ defmodule HajWeb.SettingsLive.Event.FormComponent do {:noreply, assign_form(socket, changeset)} end end + + defp put_image_url(params, socket) do + {completed, []} = uploaded_entries(socket, :file) + + paths = + for entry <- completed do + "/images/events/#{entry.client_name}" + end + + case paths do + [] -> params + [path | _] -> Map.put(params, "image", path) + end + end + + defp presign_upload(entry, socket) do + uploads = socket.assigns.uploads + bucket = "metaspexet-haj" + key = "images/events/#{entry.client_name}" + + config = %{ + region: "eu-north-1", + access_key_id: System.fetch_env!("AWS_ACCESS_KEY_ID"), + secret_access_key: System.fetch_env!("AWS_SECRET_ACCESS_KEY") + } + + {:ok, fields} = + SimpleS3Upload.sign_form_upload(config, bucket, + key: key, + content_type: entry.client_type, + max_file_size: uploads[entry.upload_config].max_file_size, + expires_in: :timer.hours(1) + ) + + meta = %{ + uploader: "S3", + key: key, + url: Haj.S3.base_url(), + fields: fields + } + + {:ok, meta, socket} + end + + defp consume_images(socket, %Event{} = event) do + consume_uploaded_entries(socket, :file, fn _meta, _entry -> :ok end) + + {:ok, event} + end + + defp error_to_string(:too_large), do: "Too large, max size is 8MB" + defp error_to_string(:too_many_files), do: "You have selected too many files" + defp error_to_string(:not_accepted), do: "You have selected an unacceptable file type" + defp error_to_string(other), do: other end diff --git a/lib/haj_web/live/settings_live/form/index.ex b/lib/haj_web/live/settings_live/form/index.ex index e296997..6764d5f 100644 --- a/lib/haj_web/live/settings_live/form/index.ex +++ b/lib/haj_web/live/settings_live/form/index.ex @@ -4,12 +4,62 @@ defmodule HajWeb.SettingsLive.Form.Index do alias Haj.Forms alias Haj.Forms.Form - @impl true + @impl Phoenix.LiveView + def render(assigns) do + ~H""" + <.header> + Formulär + <:actions> + <.link patch={~p"/settings/forms/new"}> + <.button>Nytt formulär + + + + + <.table + id="forms" + rows={@streams.forms} + row_click={fn {_id, form} -> JS.navigate(~p"/settings/forms/#{form}/responses") end} + > + <:col :let={{_id, form}} label="Namn"><%= form.name %> + <:col :let={{_id, form}} label="Beskrivning"><%= form.description %> + <:action :let={{_id, form}}> + <.link patch={~p"/settings/forms/#{form}/edit"}>Redigera + + <:action :let={{id, form}}> + <.link + phx-click={JS.push("delete", value: %{id: form.id}) |> hide("##{id}")} + data-confirm="Är du säker, alla formulärsvar kommer försvinna!?" + > + Radera + + + + + <.modal + :if={@live_action in [:new, :edit]} + id="form-modal" + show + on_cancel={JS.navigate(~p"/settings/forms")} + > + <.live_component + module={HajWeb.SettingsLive.Form.FormComponent} + id={@form.id || :new} + title={@page_title} + action={@live_action} + form={@form} + patch={~p"/settings/forms"} + /> + + """ + end + + @impl Phoenix.LiveView def mount(_params, _session, socket) do {:ok, stream(socket, :forms, Forms.list_forms())} end - @impl true + @impl Phoenix.LiveView def handle_params(params, _url, socket) do {:noreply, apply_action(socket, socket.assigns.live_action, params)} end @@ -37,7 +87,7 @@ defmodule HajWeb.SettingsLive.Form.Index do {:noreply, stream_insert(socket, :forms, form)} end - @impl true + @impl Phoenix.LiveView def handle_event("delete", %{"id" => id}, socket) do form = Forms.get_form!(id) diff --git a/lib/haj_web/live/settings_live/form/index.html.heex b/lib/haj_web/live/settings_live/form/index.html.heex deleted file mode 100644 index df7b70e..0000000 --- a/lib/haj_web/live/settings_live/form/index.html.heex +++ /dev/null @@ -1,40 +0,0 @@ -<.header> - Formulär - <:actions> - <.link patch={~p"/settings/forms/new"}> - <.button>Nytt formulär - - - - -<.table id="forms" rows={@streams.forms}> - <:col :let={{_id, form}} label="Namn"><%= form.name %> - <:col :let={{_id, form}} label="Beskrivning"><%= form.description %> - <:action :let={{_id, form}}> - <.link patch={~p"/settings/forms/#{form}/edit"}>Redigera - - <:action :let={{id, form}}> - <.link - phx-click={JS.push("delete", value: %{id: form.id}) |> hide("##{id}")} - data-confirm="Är du säker, alla formulärsvar kommer försvinna!?" - > - Radera - - - - -<.modal - :if={@live_action in [:new, :edit]} - id="form-modal" - show - on_cancel={JS.navigate(~p"/settings/forms")} -> - <.live_component - module={HajWeb.SettingsLive.Form.FormComponent} - id={@form.id || :new} - title={@page_title} - action={@live_action} - form={@form} - patch={~p"/settings/forms"} - /> - diff --git a/lib/haj_web/live/settings_live/form/show.ex b/lib/haj_web/live/settings_live/form/show.ex new file mode 100644 index 0000000..5f5d47f --- /dev/null +++ b/lib/haj_web/live/settings_live/form/show.ex @@ -0,0 +1,120 @@ +defmodule HajWeb.SettingsLive.Form.Show do + use HajWeb, :live_view + + alias Haj.Forms + + @impl true + def mount(%{"id" => form_id}, _session, socket) do + form = Forms.get_form!(form_id) + responses = Forms.list_responses_for(form_id) + + {:ok, assign(socket, form: form) |> stream(:responses, responses)} + end + + @impl true + def handle_params(params, _url, socket) do + {:noreply, apply_action(socket, socket.assigns.live_action, params)} + end + + @impl true + def render(assigns) do + ~H""" + <.header> + Formulärsvar + <:subtitle>Listar alla formulärsvar till formuläret: <%= @form.name %> + + + <.table small id="food-users" rows={@streams.responses}> + <:col :let={{_id, response}} label="Namn"> + <%= response.user.full_name %> + + <:col :let={{_id, response}} label="Tid"> + <%= format_date(response.inserted_at) %> + + + <:action :let={{_id, response}}> + <.link patch={~p"/settings/forms/#{@form}/responses/#{response}"}> + Visa svar + + + + <:action :let={{id, response}}> + <.link + phx-click={JS.push("delete", value: %{id: response.id}) |> hide("##{id}")} + data-confirm="Är du säker?" + > + Radera + + + + + <.modal + :if={@live_action == :response} + id="form-modal" + show + on_cancel={JS.navigate(~p"/settings/forms/#{@form}/responses")} + > + <:title>Formulärsvar + +
    +
    + <.field name="Person"> + <.user_card user={@response.user} /> + + <.field name="Inskickad"> + <%= @response.updated_at %> + + + <.field :for={qr <- @response.question_responses} large name={qr.question.name}> + <%= qr.answer %> + +
    +
    + + """ + end + + defp apply_action(socket, :responses, %{"id" => id}) do + socket + |> assign(:page_title, "Alla formulärsvar") + |> assign(:form, Forms.get_form!(id)) + end + + defp apply_action(socket, :response, %{"id" => id, "response_id" => response_id}) do + response = + Forms.get_response!(response_id) + + id = String.to_integer(id) + + if response.form_id == id do + socket + |> assign(:page_title, "Formulärsvar") + |> assign(:form, Forms.get_form!(id)) + |> assign(:response, response) + else + socket + |> put_flash(:error, "Formulärsvar tillhör inte formuläret") + |> push_navigate(to: ~p"/settings/forms/#{id}/responses") + end + end + + attr :large, :boolean, default: false + attr :top, :boolean, default: false + attr :name, :string + slot :inner_block + + defp field(assigns) do + ~H""" +
    +
    <%= @name %>
    +
    + <%= render_slot(@inner_block) %> +
    +
    + """ + end +end diff --git a/lib/haj_web/live/settings_live/group/show.html.heex b/lib/haj_web/live/settings_live/group/show.html.heex index d73d8d1..868372e 100644 --- a/lib/haj_web/live/settings_live/group/show.html.heex +++ b/lib/haj_web/live/settings_live/group/show.html.heex @@ -6,10 +6,7 @@ <.link patch={~p"/settings/groups/#{@group}/show/edit"} phx-click={JS.push_focus()}> <.button>Redigera grupp - <.link - patch={~p"/settings/groups/#{@group}/show-groups/new"} - phx-click={JS.push_focus()} - > + <.link patch={~p"/settings/groups/#{@group}/show-groups/new"} phx-click={JS.push_focus()}> <.button>Lägg till till spex
    diff --git a/lib/haj_web/live/show_live/show.html.heex b/lib/haj_web/live/show_live/show.html.heex index 85e707c..1a0bf75 100644 --- a/lib/haj_web/live/show_live/show.html.heex +++ b/lib/haj_web/live/show_live/show.html.heex @@ -6,7 +6,7 @@ phx-no-submit autocomplete={:off} onkeydown="return event.key != 'Enter';" - class="flex flex-col gap-4 sm:flex-row sm:items-center" + class="flex flex-col gap-4 sm:flex-row sm:items-center" >
    <%= "Medlemmar i spexet #{@show.year.year}" %> diff --git a/lib/haj_web/live/song_live/edit/form_component.ex b/lib/haj_web/live/song_live/edit/form_component.ex index 40e93bd..dbbc086 100644 --- a/lib/haj_web/live/song_live/edit/form_component.ex +++ b/lib/haj_web/live/song_live/edit/form_component.ex @@ -104,7 +104,7 @@ defmodule HajWeb.SongLive.Edit.FormComponent do end def handle_event("save", %{"song" => song_params}, socket) do - song_params = put_image_url(song_params, socket) + song_params = put_song_url(song_params, socket) line_timings = parse_line_timings(song_params["line_timings"]) song_params = Map.put(song_params, "line_timings", line_timings) @@ -181,7 +181,7 @@ defmodule HajWeb.SongLive.Edit.FormComponent do {:ok, meta, socket} end - defp put_image_url(params, socket) do + defp put_song_url(params, socket) do {completed, []} = uploaded_entries(socket, :file) paths = for entry <- completed, do: "/archive/songs/#{entry.client_name}" diff --git a/lib/haj_web/router.ex b/lib/haj_web/router.ex index 7e448d4..ca70ada 100644 --- a/lib/haj_web/router.ex +++ b/lib/haj_web/router.ex @@ -112,10 +112,12 @@ defmodule HajWeb.Router do live "/events/:id", EventLive.Show, :index live "/events/:id/register", EventLive.Show, :register - live "/events/:id/registrations", EventLive.Registrations, :users - live "/events/:id/registrations/users", EventLive.Registrations, :users - live "/events/:id/registrations/questions", EventLive.Registrations, :questions - live "/events/:id/registrations/food", EventLive.Registrations, :food + live "/events/:id/registrations", EventLive.Registrations.Index, :users + live "/events/:id/registrations/users", EventLive.Registrations.Index, :users + live "/events/:id/registrations/questions", EventLive.Registrations.Index, :questions + live "/events/:id/registrations/food", EventLive.Registrations.Index, :food + + live "/events/registrations/:id", EventLive.Registrations.Show, :show live "/forms/:id", FormLive.Index, :index end @@ -191,6 +193,9 @@ defmodule HajWeb.Router do live "/forms/:id", SettingsLive.Form.Show, :show live "/forms/:id/show/edit", SettingsLive.Form.Show, :edit + + live "/forms/:id/responses", SettingsLive.Form.Show, :responses + live "/forms/:id/responses/:response_id", SettingsLive.Form.Show, :response end end end diff --git a/mix.exs b/mix.exs index 3fcab15..1189fc9 100644 --- a/mix.exs +++ b/mix.exs @@ -38,7 +38,7 @@ defmodule Haj.MixProject do {:ecto_sql, "~> 3.11"}, {:postgrex, ">= 0.0.0"}, {:phoenix_html, "~> 3.3"}, - {:phoenix_live_reload, "~> 1.2", only: :dev}, + {:phoenix_live_reload, "~> 1.5", only: :dev}, {:phoenix_live_view, "~> 0.20"}, {:floki, ">= 0.30.0", only: :test}, {:phoenix_live_dashboard, "~> 0.7"}, diff --git a/test/haj/archive_test.exs b/test/haj/archive_test.exs index 9f2c802..60a6982 100644 --- a/test/haj/archive_test.exs +++ b/test/haj/archive_test.exs @@ -21,7 +21,12 @@ defmodule Haj.ArchiveTest do end test "create_song/1 with valid data creates a song" do - valid_attrs = %{name: "some name", number: 42, original_name: "some original_name", text: "some text"} + valid_attrs = %{ + name: "some name", + number: 42, + original_name: "some original_name", + text: "some text" + } assert {:ok, %Song{} = song} = Archive.create_song(valid_attrs) assert song.name == "some name" @@ -36,7 +41,13 @@ defmodule Haj.ArchiveTest do test "update_song/2 with valid data updates the song" do song = song_fixture() - update_attrs = %{name: "some updated name", number: 43, original_name: "some updated original_name", text: "some updated text"} + + update_attrs = %{ + name: "some updated name", + number: 43, + original_name: "some updated original_name", + text: "some updated text" + } assert {:ok, %Song{} = song} = Archive.update_song(song, update_attrs) assert song.name == "some updated name" diff --git a/test/haj/events_test.exs b/test/haj/events_test.exs index 9f5a5a2..e0594af 100644 --- a/test/haj/events_test.exs +++ b/test/haj/events_test.exs @@ -8,7 +8,14 @@ defmodule Haj.EventsTest do import Haj.EventsFixtures - @invalid_attrs %{description: nil, event_date: nil, image: nil, name: nil, purchase_deadline: nil, ticket_limit: nil} + @invalid_attrs %{ + description: nil, + event_date: nil, + image: nil, + name: nil, + purchase_deadline: nil, + ticket_limit: nil + } test "list_events/0 returns all events" do event = event_fixture() @@ -21,7 +28,14 @@ defmodule Haj.EventsTest do end test "create_event/1 with valid data creates a event" do - valid_attrs = %{description: "some description", event_date: ~U[2022-11-21 19:51:00Z], image: "some image", name: "some name", purchase_deadline: ~U[2022-11-21 19:51:00Z], ticket_limit: 42} + valid_attrs = %{ + description: "some description", + event_date: ~U[2022-11-21 19:51:00Z], + image: "some image", + name: "some name", + purchase_deadline: ~U[2022-11-21 19:51:00Z], + ticket_limit: 42 + } assert {:ok, %Event{} = event} = Events.create_event(valid_attrs) assert event.description == "some description" @@ -38,7 +52,15 @@ defmodule Haj.EventsTest do test "update_event/2 with valid data updates the event" do event = event_fixture() - update_attrs = %{description: "some updated description", event_date: ~U[2022-11-22 19:51:00Z], image: "some updated image", name: "some updated name", purchase_deadline: ~U[2022-11-22 19:51:00Z], ticket_limit: 43} + + update_attrs = %{ + description: "some updated description", + event_date: ~U[2022-11-22 19:51:00Z], + image: "some updated image", + name: "some updated name", + purchase_deadline: ~U[2022-11-22 19:51:00Z], + ticket_limit: 43 + } assert {:ok, %Event{} = event} = Events.update_event(event, update_attrs) assert event.description == "some updated description" @@ -99,9 +121,16 @@ defmodule Haj.EventsTest do test "update_ticket_type/2 with valid data updates the ticket_type" do ticket_type = ticket_type_fixture() - update_attrs = %{description: "some updated description", name: "some updated name", price: 43} - assert {:ok, %TicketType{} = ticket_type} = Events.update_ticket_type(ticket_type, update_attrs) + update_attrs = %{ + description: "some updated description", + name: "some updated name", + price: 43 + } + + assert {:ok, %TicketType{} = ticket_type} = + Events.update_ticket_type(ticket_type, update_attrs) + assert ticket_type.description == "some updated description" assert ticket_type.name == "some updated name" assert ticket_type.price == 43 @@ -145,7 +174,8 @@ defmodule Haj.EventsTest do test "create_event_registration/1 with valid data creates a event_registration" do valid_attrs = %{} - assert {:ok, %EventRegistration{} = event_registration} = Events.create_event_registration(valid_attrs) + assert {:ok, %EventRegistration{} = event_registration} = + Events.create_event_registration(valid_attrs) end test "create_event_registration/1 with invalid data returns error changeset" do @@ -156,19 +186,26 @@ defmodule Haj.EventsTest do event_registration = event_registration_fixture() update_attrs = %{} - assert {:ok, %EventRegistration{} = event_registration} = Events.update_event_registration(event_registration, update_attrs) + assert {:ok, %EventRegistration{} = event_registration} = + Events.update_event_registration(event_registration, update_attrs) end test "update_event_registration/2 with invalid data returns error changeset" do event_registration = event_registration_fixture() - assert {:error, %Ecto.Changeset{}} = Events.update_event_registration(event_registration, @invalid_attrs) + + assert {:error, %Ecto.Changeset{}} = + Events.update_event_registration(event_registration, @invalid_attrs) + assert event_registration == Events.get_event_registration!(event_registration.id) end test "delete_event_registration/1 deletes the event_registration" do event_registration = event_registration_fixture() assert {:ok, %EventRegistration{}} = Events.delete_event_registration(event_registration) - assert_raise Ecto.NoResultsError, fn -> Events.get_event_registration!(event_registration.id) end + + assert_raise Ecto.NoResultsError, fn -> + Events.get_event_registration!(event_registration.id) + end end test "change_event_registration/1 returns a event_registration changeset" do diff --git a/test/haj/forms_test.exs b/test/haj/forms_test.exs index a705ec1..f19a97a 100644 --- a/test/haj/forms_test.exs +++ b/test/haj/forms_test.exs @@ -209,7 +209,9 @@ defmodule Haj.FormsTest do test "create_question_response/1 with valid data creates a question_response" do valid_attrs = %{answer: "some answer", multi_answer: ["option1", "option2"]} - assert {:ok, %QuestionResponse{} = question_response} = Forms.create_question_response(valid_attrs) + assert {:ok, %QuestionResponse{} = question_response} = + Forms.create_question_response(valid_attrs) + assert question_response.answer == "some answer" assert question_response.multi_answer == ["option1", "option2"] end @@ -222,21 +224,29 @@ defmodule Haj.FormsTest do question_response = question_response_fixture() update_attrs = %{answer: "some updated answer", multi_answer: ["option1"]} - assert {:ok, %QuestionResponse{} = question_response} = Forms.update_question_response(question_response, update_attrs) + assert {:ok, %QuestionResponse{} = question_response} = + Forms.update_question_response(question_response, update_attrs) + assert question_response.answer == "some updated answer" assert question_response.multi_answer == ["option1"] end test "update_question_response/2 with invalid data returns error changeset" do question_response = question_response_fixture() - assert {:error, %Ecto.Changeset{}} = Forms.update_question_response(question_response, @invalid_attrs) + + assert {:error, %Ecto.Changeset{}} = + Forms.update_question_response(question_response, @invalid_attrs) + assert question_response == Forms.get_question_response!(question_response.id) end test "delete_question_response/1 deletes the question_response" do question_response = question_response_fixture() assert {:ok, %QuestionResponse{}} = Forms.delete_question_response(question_response) - assert_raise Ecto.NoResultsError, fn -> Forms.get_question_response!(question_response.id) end + + assert_raise Ecto.NoResultsError, fn -> + Forms.get_question_response!(question_response.id) + end end test "change_question_response/1 returns a question_response changeset" do diff --git a/test/haj_web/live/event_live_test.exs b/test/haj_web/live/event_live_test.exs index 62a9c55..8c33a86 100644 --- a/test/haj_web/live/event_live_test.exs +++ b/test/haj_web/live/event_live_test.exs @@ -4,9 +4,30 @@ defmodule HajWeb.EventLiveTest do import Phoenix.LiveViewTest import Haj.EventsFixtures - @create_attrs %{description: "some description", event_date: "2022-11-21T19:51:00Z", image: "some image", name: "some name", purchase_deadline: "2022-11-21T19:51:00Z", ticket_limit: 42} - @update_attrs %{description: "some updated description", event_date: "2022-11-22T19:51:00Z", image: "some updated image", name: "some updated name", purchase_deadline: "2022-11-22T19:51:00Z", ticket_limit: 43} - @invalid_attrs %{description: nil, event_date: nil, image: nil, name: nil, purchase_deadline: nil, ticket_limit: nil} + @create_attrs %{ + description: "some description", + event_date: "2022-11-21T19:51:00Z", + image: "some image", + name: "some name", + purchase_deadline: "2022-11-21T19:51:00Z", + ticket_limit: 42 + } + @update_attrs %{ + description: "some updated description", + event_date: "2022-11-22T19:51:00Z", + image: "some updated image", + name: "some updated name", + purchase_deadline: "2022-11-22T19:51:00Z", + ticket_limit: 43 + } + @invalid_attrs %{ + description: nil, + event_date: nil, + image: nil, + name: nil, + purchase_deadline: nil, + ticket_limit: nil + } defp create_event(_) do event = event_fixture() diff --git a/test/support/fixtures/events_fixtures.ex b/test/support/fixtures/events_fixtures.ex index ab6b96d..0092ef4 100644 --- a/test/support/fixtures/events_fixtures.ex +++ b/test/support/fixtures/events_fixtures.ex @@ -45,9 +45,7 @@ defmodule Haj.EventsFixtures do def event_registration_fixture(attrs \\ %{}) do {:ok, event_registration} = attrs - |> Enum.into(%{ - - }) + |> Enum.into(%{}) |> Haj.Events.create_event_registration() event_registration From 4fcbfcb1b21eb678050fb7b18f3ac758175a0c29 Mon Sep 17 00:00:00 2001 From: Adrian Salamon Date: Sat, 19 Oct 2024 23:44:27 +0200 Subject: [PATCH 56/56] fix rendering of multiresponse --- lib/haj/forms/question_response.ex | 10 ++++++++++ .../live/event_live/registrations/show.ex | 2 +- lib/haj_web/live/settings_live/form/show.ex | 16 +++++++++++++++- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/lib/haj/forms/question_response.ex b/lib/haj/forms/question_response.ex index 1e0a780..4fddd57 100644 --- a/lib/haj/forms/question_response.ex +++ b/lib/haj/forms/question_response.ex @@ -19,3 +19,13 @@ defmodule Haj.Forms.QuestionResponse do |> validate_required([]) end end + +defimpl Phoenix.HTML.Safe, for: Haj.Forms.QuestionResponse do + def to_iodata(question_response) do + case {question_response.answer, question_response.multi_answer} do + {nil, nil} -> "" + {nil, multi_answer} -> Enum.join(multi_answer, ", ") |> Phoenix.HTML.Engine.html_escape() + {answer, nil} -> Phoenix.HTML.Engine.html_escape(answer) + end + end +end diff --git a/lib/haj_web/live/event_live/registrations/show.ex b/lib/haj_web/live/event_live/registrations/show.ex index a6d8bfc..27493d1 100644 --- a/lib/haj_web/live/event_live/registrations/show.ex +++ b/lib/haj_web/live/event_live/registrations/show.ex @@ -79,7 +79,7 @@ defmodule HajWeb.EventLive.Registrations.Show do <.field :for={qr <- @registration.response.question_responses} large name={qr.question.name}> - <%= qr.answer %> + <%= qr %>
    diff --git a/lib/haj_web/live/settings_live/form/show.ex b/lib/haj_web/live/settings_live/form/show.ex index 5f5d47f..04c5877 100644 --- a/lib/haj_web/live/settings_live/form/show.ex +++ b/lib/haj_web/live/settings_live/form/show.ex @@ -16,6 +16,20 @@ defmodule HajWeb.SettingsLive.Form.Show do {:noreply, apply_action(socket, socket.assigns.live_action, params)} end + @impl Phoenix.LiveView + def handle_event("delete", %{"id" => id}, socket) do + response = Forms.get_response!(id) + + case Forms.delete_response(response) do + {:ok, _} -> + {:noreply, stream_delete(socket, :responses, response)} + + {:error, _} -> + put_flash(socket, :error, "Kunde inte ta bort svaret") + {:noreply, socket} + end + end + @impl true def render(assigns) do ~H""" @@ -66,7 +80,7 @@ defmodule HajWeb.SettingsLive.Form.Show do <.field :for={qr <- @response.question_responses} large name={qr.question.name}> - <%= qr.answer %> + <%= qr %>