From 28105c38ef08b3850c74b98deca05b3dd06c6555 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20Fel=C3=ADcio?= <55213469+feliciofilipe@users.noreply.github.com> Date: Sun, 3 Sep 2023 01:47:43 +0100 Subject: [PATCH] feat: add boosts (#171) Co-authored-by: Rui Lopes <76881129+RuiL1904@users.noreply.github.com> --- lib/parzival/accounts.ex | 4 + lib/parzival/accounts/user.ex | 2 + lib/parzival/gamification.ex | 74 +++- lib/parzival/store.ex | 384 +++++++++++++++++- lib/parzival/store/boost.ex | 50 +++ lib/parzival/store/item.ex | 28 ++ lib/parzival/store/order.ex | 1 - lib/parzival/store/product.ex | 2 +- lib/parzival/uploaders/boost_image.ex | 45 ++ lib/parzival_web.ex | 13 + lib/parzival_web/components/boost.ex | 50 +++ lib/parzival_web/components/inventory.ex | 69 ++++ .../live/app/dashboard_live/index.ex | 11 +- .../gamification/leaderboard_live/index.ex | 2 +- .../live/app/gamification/task_live/show.ex | 21 + .../app/gamification/task_live/show.html.heex | 25 +- .../live/app/jobs/offer_live/index.html.heex | 6 +- .../live/app/store/boost_live/index.ex | 34 ++ .../live/app/store/boost_live/index.html.heex | 49 +++ .../live/app/store/boost_live/show.ex | 85 ++++ .../live/app/store/boost_live/show.html.heex | 69 ++++ .../app/store/product_live/index.html.heex | 22 +- .../live/app/store/product_live/show.ex | 22 +- .../live/app/vault/order_live/index.ex | 7 +- .../live/app/vault/order_live/index.html.heex | 15 +- .../task_live/form_component.html.heex | 100 ++--- .../live/backoffice/store/boost_live/edit.ex | 20 + .../store/boost_live/edit.html.heex | 1 + .../store/boost_live/form_component.ex | 63 +++ .../store/boost_live/form_component.html.heex | 115 ++++++ .../live/backoffice/store/boost_live/new.ex | 20 + .../backoffice/store/boost_live/new.html.heex | 1 + .../product_live/form_component.html.heex | 10 +- .../form_component.html.heex | 4 +- .../tools/faqs_live/form_component.html.heex | 4 +- lib/parzival_web/live/hooks.ex | 15 +- lib/parzival_web/router.ex | 6 + .../templates/layout/live.html.heex | 4 +- lib/parzival_web/templates/pdf/cv.html.heex | 12 +- lib/parzival_web/views/view_utils.ex | 7 + priv/fake/recruiters.txt | 1 + .../20220619010759_create_boosts.exs | 19 + .../20220620001713_create_items.exs | 19 + priv/repo/seeds/store.exs | 69 ++++ .../defaults/store/boost_image_original.png | Bin 0 -> 461150 bytes .../defaults/store/product_image_original.png | Bin 80186 -> 461150 bytes test/parzival/store_test.exs | 114 ++++++ test/support/fixtures/store_fixtures.ex | 41 +- 48 files changed, 1607 insertions(+), 128 deletions(-) create mode 100644 lib/parzival/store/boost.ex create mode 100644 lib/parzival/store/item.ex create mode 100644 lib/parzival/uploaders/boost_image.ex create mode 100644 lib/parzival_web/components/boost.ex create mode 100644 lib/parzival_web/components/inventory.ex create mode 100644 lib/parzival_web/live/app/store/boost_live/index.ex create mode 100644 lib/parzival_web/live/app/store/boost_live/index.html.heex create mode 100644 lib/parzival_web/live/app/store/boost_live/show.ex create mode 100644 lib/parzival_web/live/app/store/boost_live/show.html.heex create mode 100644 lib/parzival_web/live/backoffice/store/boost_live/edit.ex create mode 100644 lib/parzival_web/live/backoffice/store/boost_live/edit.html.heex create mode 100644 lib/parzival_web/live/backoffice/store/boost_live/form_component.ex create mode 100644 lib/parzival_web/live/backoffice/store/boost_live/form_component.html.heex create mode 100644 lib/parzival_web/live/backoffice/store/boost_live/new.ex create mode 100644 lib/parzival_web/live/backoffice/store/boost_live/new.html.heex create mode 100644 priv/repo/migrations/20220619010759_create_boosts.exs create mode 100644 priv/repo/migrations/20220620001713_create_items.exs create mode 100644 priv/static/images/defaults/store/boost_image_original.png diff --git a/lib/parzival/accounts.ex b/lib/parzival/accounts.ex index 29924192..4c78724e 100644 --- a/lib/parzival/accounts.ex +++ b/lib/parzival/accounts.ex @@ -213,6 +213,10 @@ defmodule Parzival.Accounts do User.changeset(user, attrs, generate_password: false) end + def load_user_fields(user, preloads \\ []) do + Repo.preload(user, preloads) + end + @doc """ Creates a user. ## Examples diff --git a/lib/parzival/accounts/user.ex b/lib/parzival/accounts/user.ex index 12b76baa..ef977976 100644 --- a/lib/parzival/accounts/user.ex +++ b/lib/parzival/accounts/user.ex @@ -11,6 +11,7 @@ defmodule Parzival.Accounts.User do alias Parzival.Companies.Connection alias Parzival.Gamification.Curriculum alias Parzival.Gamification.Mission + alias Parzival.Store.Item alias Parzival.Store.Order alias Parzival.Uploaders @@ -67,6 +68,7 @@ defmodule Parzival.Accounts.User do belongs_to :qrcode, QRCode has_many :orders, Order + has_many :inventory, Item many_to_many :missions, Mission, join_through: "missions_users" diff --git a/lib/parzival/gamification.ex b/lib/parzival/gamification.ex index e3e2fa74..c441e4ab 100644 --- a/lib/parzival/gamification.ex +++ b/lib/parzival/gamification.ex @@ -7,8 +7,10 @@ defmodule Parzival.Gamification do alias Ecto.Multi + alias Parzival.Accounts alias Parzival.Accounts.User alias Parzival.Companies + alias Parzival.Gamification alias Parzival.Gamification.Curriculum alias Parzival.Gamification.Curriculum.Education alias Parzival.Gamification.Curriculum.Experience @@ -16,6 +18,9 @@ defmodule Parzival.Gamification do alias Parzival.Gamification.Curriculum.Position alias Parzival.Gamification.Curriculum.Skill alias Parzival.Gamification.Curriculum.Volunteering + alias Parzival.Gamification.Mission.MissionUser + alias Parzival.Gamification.Mission.TaskUser + alias Parzival.Store @doc """ Returns the list of curriculums. @@ -438,6 +443,58 @@ defmodule Parzival.Gamification do Task.changeset(task, attrs) end + def skip_task(%User{} = user, %Task{} = task) do + Multi.new() + |> Multi.insert( + :task_user, + TaskUser.changeset(%TaskUser{}, %{ + user_id: user.id, + task_id: task.id + }) + ) + |> Multi.update( + :update_user, + User.task_completion_changeset(user, %{ + balance: user.balance + task.tokens, + exp: user.exp + task.exp + }) + ) + |> Multi.run(:mission, fn repo, _change -> + mission = Gamification.get_mission!(task.mission_id, tasks: [:users]) + + case Enum.all?(mission.tasks, fn task -> Enum.any?(task.users, &(&1.id == user.id)) end) do + true -> + %MissionUser{} + |> MissionUser.changeset(%{mission_id: mission.id, user_id: user.id}) + |> repo.insert() + + user + |> User.task_completion_changeset(%{ + balance: user.balance + mission.tokens * get_tokens_multiplier(user), + exp: user.exp + mission.exp + }) + |> repo.update() + + {:ok, mission} + + _ -> + {:ok, mission} + end + end) + |> Multi.delete( + :redeem_boost, + Store.get_skip_task_from_inventory(user.id) + ) + |> Repo.transaction() + |> case do + {:ok, transaction} -> + broadcast({:ok, transaction}, :updated) + + {:error, _transaction, changeset, _} -> + {:error, changeset} + end + end + alias Parzival.Gamification.Mission.Difficulty @doc """ @@ -907,7 +964,7 @@ defmodule Parzival.Gamification do TaskUser.changeset(%TaskUser{}, %{user_id: user.id, staff_id: staff.id, task_id: task.id}) ) |> Multi.run(:mission, fn repo, _change -> - mission = Parzival.Gamification.get_mission!(task.mission_id, tasks: [:users]) + mission = Gamification.get_mission!(task.mission_id, tasks: [:users]) case Enum.all?(mission.tasks, fn task -> Enum.any?(task.users, &(&1.id == user.id)) end) do true -> @@ -948,6 +1005,21 @@ defmodule Parzival.Gamification do end end + defp get_tokens_multiplier(%User{} = user) do + user = Accounts.load_user_fields(user, inventory: [:boost]) + + user.inventory + |> Enum.find_value( + 1.0, + fn item -> + if item.boost.type == :tokens && + Timex.diff(DateTime.utc_now(), item.boost.expires_at, :minutes) <= 60 do + item.boost.multiplier + end + end + ) + end + def is_task_completed?(task_id, user_id) do from(t in TaskUser, where: t.task_id == ^task_id and t.user_id == ^user_id) |> Repo.exists?() diff --git a/lib/parzival/store.ex b/lib/parzival/store.ex index ff392053..75595401 100644 --- a/lib/parzival/store.ex +++ b/lib/parzival/store.ex @@ -92,7 +92,7 @@ defmodule Parzival.Store do |> Product.changeset(attrs) |> Repo.update() |> after_save(after_save) - |> broadcast(:updated) + |> broadcast(:product_updated) end def update_product_image(%Product{} = product, attrs) do @@ -121,7 +121,7 @@ defmodule Parzival.Store do message: "This product cant be deleted, because users have bought it!" ) |> Repo.delete() - |> broadcast(:deleted) + |> broadcast(:product_deleted) end @doc """ @@ -251,7 +251,18 @@ defmodule Parzival.Store do Order.changeset(order, attrs) end - def purchase(user, product) do + @doc """ + Purchases a product. + + ## Examples + + iex> purchase_product(user, product) + {:ok, %Ecto.Changeset{}} + + iex> purchase_product(user, product) + {:error, %Ecto.Changeset{}} + """ + def purchase_product(user, product) do Multi.new() |> Multi.update( :update_balance, @@ -265,34 +276,385 @@ defmodule Parzival.Store do |> Repo.transaction() |> case do {:ok, transaction} -> - broadcast({:ok, transaction.update_stock}, :purchased) + broadcast({:ok, transaction.update_stock}, :product_purchased) {:error, _transaction, changeset, _} -> {:error, changeset} end end - def subscribe(topic) when topic in ["purchased", "updated", "deleted"] do + def subscribe(topic) + when topic in ["product_purchased", "product_updated", "product_deleted"] do Phoenix.PubSub.subscribe(Parzival.PubSub, topic) end defp broadcast({:error, _reason} = error, _event), do: error defp broadcast({:ok, %Product{} = product}, event) - when event in [:purchased] do - Phoenix.PubSub.broadcast!(Parzival.PubSub, "purchased", {event, product.stock}) + when event in [:product_purchased] do + Phoenix.PubSub.broadcast!(Parzival.PubSub, "product_purchased", {event, product.stock}) {:ok, product} end defp broadcast({:ok, %Product{} = product}, event) - when event in [:updated] do - Phoenix.PubSub.broadcast!(Parzival.PubSub, "updated", {event, product}) + when event in [:product_updated] do + Phoenix.PubSub.broadcast!(Parzival.PubSub, "product_updated", {event, product}) {:ok, product} end defp broadcast({:ok, %Product{} = product}, event) - when event in [:deleted] do - Phoenix.PubSub.broadcast!(Parzival.PubSub, "deleted", {event, product}) + when event in [:product_deleted] do + Phoenix.PubSub.broadcast!(Parzival.PubSub, "product_deleted", {event, product}) {:ok, product} end + + alias Parzival.Store.Boost + + @doc """ + Returns the list of boosts. + + ## Examples + + iex> list_boosts() + [%Boost{}, ...] + + """ + def list_boosts(params \\ %{}) + + def list_boosts(opts) when is_list(opts) do + Boost + |> apply_filters(opts) + |> Repo.all() + end + + def list_boosts(flop) do + Flop.validate_and_run(Boost, flop, for: Boost) + end + + def list_boosts(%{} = flop, opts) when is_list(opts) do + Boost + |> apply_filters(opts) + |> Flop.validate_and_run(flop, for: Boost) + end + + @doc """ + Gets a single boost. + + Raises `Ecto.NoResultsError` if the Boost does not exist. + + ## Examples + + iex> get_boost!(123) + %Boost{} + + iex> get_boost!(456) + ** (Ecto.NoResultsError) + + """ + def get_boost!(id), do: Repo.get!(Boost, id) + + @doc """ + Gets the :skip_task type boost. This function assumes the existance of a boost of type skip_task. + + Raises `Ecto.NoResultsError` if the Boost does not exist. + + ## Examples + + iex> get_skip_task_boost() + %Boost{} + + iex> get_skip_task_boost() + ** (Ecto.NoResultsError) + """ + def get_skip_task_boost do + Boost + |> where([b], b.type == :skip_task) + |> Repo.one() + end + + @doc """ + Creates a boost. + + ## Examples + + iex> create_boost(%{field: value}) + {:ok, %Boost{}} + + iex> create_boost(%{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def create_boost(attrs \\ %{}) do + %Boost{} + |> Boost.changeset(attrs) + |> Repo.insert() + end + + @doc """ + Updates a boost. + + ## Examples + + iex> update_boost(boost, %{field: new_value}) + {:ok, %Boost{}} + + iex> update_boost(boost, %{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def update_boost(%Boost{} = boost, attrs) do + boost + |> Boost.changeset(attrs) + |> Repo.update() + end + + @doc """ + Deletes a boost. + + ## Examples + + iex> delete_boost(boost) + {:ok, %Boost{}} + + iex> delete_boost(boost) + {:error, %Ecto.Changeset{}} + + """ + def delete_boost(%Boost{} = boost) do + Repo.delete(boost) + end + + @doc """ + Returns an `%Ecto.Changeset{}` for tracking boost changes. + + ## Examples + + iex> change_boost(boost) + %Ecto.Changeset{data: %Boost{}} + + """ + def change_boost(%Boost{} = boost, attrs \\ %{}) do + Boost.changeset(boost, attrs) + end + + alias Parzival.Store.Item + + @doc """ + Returns true if the user has an active boost of type exp or tokens, false otherwise. + + ## Examples + + iex> already_has_active_boost?(user_id) + true + + iex> already_has_active_boost?(user_id) + false + """ + def already_has_active_boost?(user_id) do + Item + |> where([i], i.user_id == ^user_id) + |> join(:inner, [i], b in Boost, on: i.boost_id == b.id) + |> where([i, b], b.type == :exp or b.type == :tokens) + |> where([i, b], fragment("now() - ? <= interval '60 minutes'", i.expires_at)) + |> Repo.exists?() + end + + @doc """ + Purchases a boost. + + ## Examples + + iex> purchase_boost(user, boost) + {:ok, %Ecto.Changeset{}} + + iex> purchase_boost(user, boost) + {:error, %Ecto.Changeset{}} + """ + def purchase_boost(user, boost) do + Multi.new() + |> Multi.update( + :update_balance, + User.balance_changeset(user, %{balance: user.balance - boost.price}) + ) + |> Multi.insert(:insert, %Item{user_id: user.id, boost_id: boost.id}) + |> Repo.transaction() + |> case do + {:ok, transaction} -> + {:ok, transaction} + + {:error, _transaction, changeset, _} -> + {:error, changeset} + end + end + + @doc """ + Returns the list of items. + + ## Examples + + iex> list_items() + [%Item{}, ...] + + """ + def list_items do + Item + |> Repo.all() + end + + def list_items(opts) when is_list(opts) do + Item + |> apply_filters(opts) + |> Repo.all() + end + + @doc """ + Returns the list of items present in the inventory. + + ## Examples + + iex> list_inventory() + [%Item{}, ...] + """ + def list_inventory(opts) when is_list(opts) do + from(i in Item, + where: is_nil(i.expires_at) or i.expires_at > ^NaiveDateTime.utc_now() + ) + |> apply_filters(opts) + |> Repo.all() + end + + @doc """ + Returns the item that represents the skip task boost in the user inventory. + + ## Examples + + iex> get_skip_task_from_inventory(user_id) + %Item{} + """ + def get_skip_task_from_inventory(user_id) do + Item + |> where(user_id: ^user_id) + |> join(:inner, [i], b in Boost, on: i.boost_id == b.id) + |> where([i, b], b.type == :skip_task) + |> Repo.one() + end + + @doc """ + Gets a single item. + + Raises `Ecto.NoResultsError` if the Item does not exist. + + ## Examples + + iex> get_item!(123) + %Item{} + + iex> get_item!(456) + ** (Ecto.NoResultsError) + + """ + def get_item!(id), do: Repo.get!(Item, id) + + @doc """ + Creates a item. + + ## Examples + + iex> create_item(%{field: value}) + {:ok, %Item{}} + + iex> create_item(%{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def create_item(attrs \\ %{}) do + %Item{} + |> Item.changeset(attrs) + |> Repo.insert() + end + + @doc """ + Updates a item. + + ## Examples + + iex> update_item(item, %{field: new_value}) + {:ok, %Item{}} + + iex> update_item(item, %{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def update_item(%Item{} = item, attrs) do + item + |> Item.changeset(attrs) + |> Repo.update() + end + + @doc """ + Deletes an item. + + ## Examples + + iex> delete_item(item) + {:ok, %Item{}} + + iex> delete_item(item) + {:error, %Ecto.Changeset{}} + + """ + def delete_item(%Item{} = item) do + Repo.delete(item) + end + + @doc """ + Returns an `%Ecto.Changeset{}` for tracking item changes. + + ## Examples + + iex> change_item(item) + %Ecto.Changeset{data: %Item{}} + + """ + def change_item(%Item{} = item, attrs \\ %{}) do + Item.changeset(item, attrs) + end + + @doc """ + Returns true if the user has a skip task boost in their inventory, false otherwise. + + ## Examples + + iex> has_skip_task?(user_id) + true + + iex> has_skip_task?(user_id) + false + """ + def has_skip_task?(user_id) do + Item + |> where([i], i.user_id == ^user_id) + |> join(:inner, [i], b in Boost, on: i.boost_id == b.id) + |> where([i, b], b.type == :skip_task) + |> Repo.exists?() + end + + @doc """ + Terminates a boost, by setting its `expires_at` to `nil`. It also deletes the item from the user inventory. + """ + def terminate_boost(item_id) do + item = get_item!(item_id) + + Ecto.Multi.new() + |> Ecto.Multi.update(:update_item, Item.changeset(item, %{expires_at: nil})) + |> Ecto.Multi.delete(:delete_item, item) + |> Repo.transaction() + |> case do + {:ok, transaction} -> + {:ok, transaction.update_item} + + {:error, _transaction, changeset, _} -> + {:error, changeset} + end + end end diff --git a/lib/parzival/store/boost.ex b/lib/parzival/store/boost.ex new file mode 100644 index 00000000..feaaf07e --- /dev/null +++ b/lib/parzival/store/boost.ex @@ -0,0 +1,50 @@ +defmodule Parzival.Store.Boost do + @moduledoc """ + A boost that can be purchased by a user. + """ + use Parzival.Schema + + alias Parzival.Store.Item + alias Parzival.Uploaders + + @types ~w(exp tokens skip_task)a + + @required_fields ~w(name description price type)a + @optional_fields ~w(multiplier)a + + @derive { + Flop.Schema, + filterable: [], + sortable: [:name], + compound_fields: [search: [:name]], + default_order_by: [:name], + default_order_directions: [:asc] + } + + schema "boosts" do + field :name, :string + field :description, :string + field :price, :integer + field :type, Ecto.Enum, values: @types + field :multiplier, :float + + field :image, Uploaders.BoostImage.Type + + has_many :item, Item + + timestamps() + end + + @doc false + def changeset(boost, attrs) do + boost + |> cast(attrs, @required_fields ++ @optional_fields) + |> cast_attachments(attrs, [:image]) + |> validate_required(@required_fields) + end + + def image_changeset(product, attrs) do + product + |> cast_attachments(attrs, [:image]) + end +end diff --git a/lib/parzival/store/item.ex b/lib/parzival/store/item.ex new file mode 100644 index 00000000..9ac7a07b --- /dev/null +++ b/lib/parzival/store/item.ex @@ -0,0 +1,28 @@ +defmodule Parzival.Store.Item do + @moduledoc """ + An item with a boost that belongs to a user. + """ + use Parzival.Schema + + alias Parzival.Accounts.User + alias Parzival.Store.Boost + + @required_fields ~w(user_id boost_id)a + @optional_fields ~w(expires_at)a + + schema "items" do + field :expires_at, :naive_datetime + + belongs_to :user, User + belongs_to :boost, Boost + + timestamps() + end + + @doc false + def changeset(item, attrs) do + item + |> cast(attrs, @required_fields ++ @optional_fields) + |> validate_required(@required_fields) + end +end diff --git a/lib/parzival/store/order.ex b/lib/parzival/store/order.ex index 0aa7a33f..cede73d6 100644 --- a/lib/parzival/store/order.ex +++ b/lib/parzival/store/order.ex @@ -26,7 +26,6 @@ defmodule Parzival.Store.Order do field :redeemed, :boolean, default: false belongs_to :user, User - belongs_to :product, Product timestamps() diff --git a/lib/parzival/store/product.ex b/lib/parzival/store/product.ex index dbd10661..9167ef70 100644 --- a/lib/parzival/store/product.ex +++ b/lib/parzival/store/product.ex @@ -29,7 +29,7 @@ defmodule Parzival.Store.Product do field :stock, :integer field :max_per_user, :integer - field :image, Uploaders.ProductImage.Type + field :image, Uploaders.BoostImage.Type has_many :orders, Order diff --git a/lib/parzival/uploaders/boost_image.ex b/lib/parzival/uploaders/boost_image.ex new file mode 100644 index 00000000..00fe191e --- /dev/null +++ b/lib/parzival/uploaders/boost_image.ex @@ -0,0 +1,45 @@ +defmodule Parzival.Uploaders.BoostImage do + @moduledoc """ + BoostImage is used for boost images. + """ + + use Waffle.Definition + use Waffle.Ecto.Definition + + alias Parzival.Store.Product + + @versions [:original, :medium, :thumb] + @extension_whitelist ~w(.jpg .jpeg .gif .png) + + def validate({file, _}) do + file.file_name + |> Path.extname() + |> String.downcase() + |> then(&Enum.member?(@extension_whitelist, &1)) + |> case do + true -> :ok + false -> {:error, "invalid file type"} + end + end + + def transform(:thumb, _) do + {:convert, "-strip -thumbnail 100x150^ -gravity center -extent 100x150 -format png", :png} + end + + def transform(:medium, _) do + {:convert, "-strip -thumbnail 400x600^ -gravity center -extent 400x600 -format png", :png} + end + + def filename(version, _) do + version + end + + def storage_dir(_version, {_file, %Product{} = scope}) do + "uploads/store/#{scope.id}" + end + + # Provide a default URL if there hasn't been a file uploaded + def default_url(version) do + "/images/defaults/store/boost_image_#{version}.png" + end +end diff --git a/lib/parzival_web.ex b/lib/parzival_web.ex index 22f4df52..60a1a3ed 100644 --- a/lib/parzival_web.ex +++ b/lib/parzival_web.ex @@ -48,6 +48,7 @@ defmodule ParzivalWeb do layout: {ParzivalWeb.LayoutView, "live.html"} unquote(view_helpers()) + unquote(flash_helper()) end end @@ -57,6 +58,7 @@ defmodule ParzivalWeb do layout: unquote(layout) unquote(view_helpers()) + unquote(flash_helper()) end end @@ -114,6 +116,17 @@ defmodule ParzivalWeb do end end + # Injects an `handle_info` clause into the every live view + defp flash_helper do + quote do + def handle_info({action, reason}, socket) when is_atom(action) do + {:noreply, + socket + |> put_flash(action, reason)} + end + end + end + @doc """ When used, dispatch to the appropriate controller/view/etc. """ diff --git a/lib/parzival_web/components/boost.ex b/lib/parzival_web/components/boost.ex new file mode 100644 index 00000000..44be55f4 --- /dev/null +++ b/lib/parzival_web/components/boost.ex @@ -0,0 +1,50 @@ +defmodule ParzivalWeb.Components.Boost do + @moduledoc false + use ParzivalWeb, :live_component + + alias Parzival.Store + alias Parzival.Uploaders + + @impl true + def render(assigns) do + ~H""" +
+ <%= if ! @item.expires_at do %> + <%= if @item.boost.type in [:exp, :tokens] do %> + + <% else %> + + <% end %> + <% else %> +
+ + +
+ <% end %> +
+ """ + end + + @impl true + def handle_event("activate-boost", %{"item_id" => item_id}, socket) do + user_id = socket.assigns.item.user_id + + if Store.already_has_active_boost?(user_id) do + send(self(), {:error, "You already have an active boost."}) + {:noreply, socket} + else + item = Store.get_item!(item_id) + Store.update_item(item, %{expires_at: Timex.shift(NaiveDateTime.utc_now(), minutes: 60)}) + + send(self(), {:info, "Boost activated!"}) + + {:noreply, + socket + |> assign( + inventory: Store.list_inventory(where: [user_id: item.user_id], preloads: [:boost]) + )} + end + end +end diff --git a/lib/parzival_web/components/inventory.ex b/lib/parzival_web/components/inventory.ex new file mode 100644 index 00000000..7a6febf9 --- /dev/null +++ b/lib/parzival_web/components/inventory.ex @@ -0,0 +1,69 @@ +defmodule ParzivalWeb.Components.Inventory do + @moduledoc false + use ParzivalWeb, :live_component + + alias Parzival.Store + + @impl true + def render(assigns) do + ~H""" + +

+ <%= "Inventory (#{length(@inventory)}/5)" %> +

+
+ <%= for i <- 1..5 do %> + <%= if i > length(@inventory) do %> +
+ <% else %> + <.live_component module={ParzivalWeb.Components.Boost} id={Enum.at(@inventory, i - 1).id} item={Enum.at(@inventory, i - 1)} flash={@flash} /> + <% end %> + <% end %> +
+ + <%= for item <- @inventory do %> + <%= if item.expires_at do %> +
+ <%= item.boost.name %> +
+ <%= relative_datetime_in_digital_clock_format(item.expires_at) %> + + + + + +
+
+ <% end %> + <% end %> + + """ + end + + @impl true + def handle_event("terminate-boost", %{"item_id" => item_id}, socket) do + case Store.terminate_boost(item_id) do + {:ok, item} -> + send(self(), {:info, "Boost terminated!"}) + + {:noreply, + socket + |> assign( + inventory: Store.list_inventory(where: [user_id: item.user_id], preloads: [:boost]) + )} + + {:error, _reason} -> + send( + self(), + {:error, + "There was an error while trying to terminate the boost. Please try again later."} + ) + + {:noreply, socket} + end + end +end diff --git a/lib/parzival_web/live/app/dashboard_live/index.ex b/lib/parzival_web/live/app/dashboard_live/index.ex index a725ddde..f34f9f84 100644 --- a/lib/parzival_web/live/app/dashboard_live/index.ex +++ b/lib/parzival_web/live/app/dashboard_live/index.ex @@ -21,7 +21,6 @@ defmodule ParzivalWeb.App.DashboardLive.Index do end @impl true - def handle_params(params, _url, socket) do user = Accounts.get_user!(socket.assigns.current_user.id, [:company, :curriculum]) @@ -57,6 +56,11 @@ defmodule ParzivalWeb.App.DashboardLive.Index do end end + @impl true + def handle_info({event, _post}, socket) when event in [:new_post] do + {:noreply, assign(socket, list_posts(socket.assigns.params))} + end + defp list_top_users(params) do params = params @@ -105,9 +109,4 @@ defmodule ParzivalWeb.App.DashboardLive.Index do defp list_announcements do Tools.list_announcements(preloads: [:author], limit: 2, order_by: [desc: :inserted_at]) end - - @impl true - def handle_info({event, _post}, socket) when event in [:new_post] do - {:noreply, assign(socket, list_posts(socket.assigns.params))} - end end diff --git a/lib/parzival_web/live/app/gamification/leaderboard_live/index.ex b/lib/parzival_web/live/app/gamification/leaderboard_live/index.ex index dd052ccb..e4515659 100644 --- a/lib/parzival_web/live/app/gamification/leaderboard_live/index.ex +++ b/lib/parzival_web/live/app/gamification/leaderboard_live/index.ex @@ -24,7 +24,7 @@ defmodule ParzivalWeb.App.LeaderboardLive.Index do @impl true def handle_params(params, _url, socket) do - user = Accounts.get_user!(socket.assigns.current_user.id, [:missions]) + user = Accounts.get_user!(socket.assigns.current_user.id, [:missions, inventory: [:boost]]) {:noreply, socket diff --git a/lib/parzival_web/live/app/gamification/task_live/show.ex b/lib/parzival_web/live/app/gamification/task_live/show.ex index 39358ccb..d0ddb6ff 100644 --- a/lib/parzival_web/live/app/gamification/task_live/show.ex +++ b/lib/parzival_web/live/app/gamification/task_live/show.ex @@ -6,6 +6,7 @@ defmodule ParzivalWeb.App.TaskLive.Show do alias Parzival.Accounts alias Parzival.Gamification + alias Parzival.Store @impl true def mount(_params, _session, socket) do @@ -24,12 +25,32 @@ defmodule ParzivalWeb.App.TaskLive.Show do :is_task_completed?, Gamification.is_task_completed?(task_id, socket.assigns.current_user.id) ) + |> assign( + :has_skip_task?, + Store.has_skip_task?(socket.assigns.current_user.id) + ) + |> assign( + :skip_task_boost, + Store.get_skip_task_boost() + ) |> assign(list_completed_tasks_users(params)) |> assign(:task, Gamification.get_task!(task_id)) |> assign(:qrcode, qrcode(socket, task_id)) |> apply_action(socket.assigns.live_action, params)} end + @impl true + def handle_event("skip-task", _, socket) do + task = socket.assigns.task + user = socket.assigns.current_user + + Gamification.skip_task(user, task) + + {:noreply, + socket + |> assign(inventory: Store.list_inventory(where: [user_id: user.id], preloads: [:boost]))} + end + defp apply_action(socket, :show, _params) do socket |> assign(:page_title, "Show Task") diff --git a/lib/parzival_web/live/app/gamification/task_live/show.html.heex b/lib/parzival_web/live/app/gamification/task_live/show.html.heex index 11073548..a24edd9b 100644 --- a/lib/parzival_web/live/app/gamification/task_live/show.html.heex +++ b/lib/parzival_web/live/app/gamification/task_live/show.html.heex @@ -14,14 +14,30 @@

Task

<%= if @current_user.role in [:attendee] do %> -
+
<%= if @is_task_completed? do %> -
+
Completed
<% else %> - <%= live_patch to: Routes.task_show_url(@socket, :redeem, @mission, @task), class: "button" do %> -
+
<%= live_redirect("< Back", to: Routes.mission_show_path(@socket, :show, @mission), class: "hover:underline inline-flex items-center justify-center whitespace-nowrap") %> diff --git a/lib/parzival_web/live/app/jobs/offer_live/index.html.heex b/lib/parzival_web/live/app/jobs/offer_live/index.html.heex index 3de8603e..65d29dfe 100644 --- a/lib/parzival_web/live/app/jobs/offer_live/index.html.heex +++ b/lib/parzival_web/live/app/jobs/offer_live/index.html.heex @@ -1,4 +1,4 @@ -
+

Jobs

@@ -7,10 +7,10 @@ <%= if @current_user.role in [:attendee] do %> <% end %> diff --git a/lib/parzival_web/live/app/store/boost_live/index.ex b/lib/parzival_web/live/app/store/boost_live/index.ex new file mode 100644 index 00000000..d9527ce0 --- /dev/null +++ b/lib/parzival_web/live/app/store/boost_live/index.ex @@ -0,0 +1,34 @@ +defmodule ParzivalWeb.App.BoostLive.Index do + @moduledoc false + use ParzivalWeb, :live_view + + import ParzivalWeb.Components.Pagination + + alias Parzival.Store + alias Parzival.Uploaders + + @impl true + def mount(_params, _session, socket) do + {:ok, socket} + end + + @impl true + def handle_params(params, _url, socket) do + {:noreply, + socket + |> assign(:current_page, :store) + |> assign(:page_title, "Store") + |> assign(:params, params) + |> assign(list_boosts(params))} + end + + defp list_boosts(params) do + case Store.list_boosts(params) do + {:ok, {boosts, meta}} -> + %{boosts: boosts, meta: meta} + + {:error, flop} -> + %{boosts: [], meta: flop} + end + end +end diff --git a/lib/parzival_web/live/app/store/boost_live/index.html.heex b/lib/parzival_web/live/app/store/boost_live/index.html.heex new file mode 100644 index 00000000..93819056 --- /dev/null +++ b/lib/parzival_web/live/app/store/boost_live/index.html.heex @@ -0,0 +1,49 @@ +
+
+

Store

+ +
+
+ +
+ <%= if @current_user.role in [:admin] do %> +
+ <%= live_patch("New", to: Routes.admin_boost_new_path(@socket, :new), class: "inline-flex items-center justify-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-secondary hover:bg-primary xl:w-full") %> +
+ <% end %> +
+
+
+
    + <%= for boost <- @boosts do %> + <%= live_patch to: Routes.boost_show_path(@socket, :show, boost) do %> +
  • +
    + +
    +
    +

    + + <%= boost.name %> +

    +

    + <%= boost.description %> +

    +

    + 💰 <%= boost.price %> +

    +
    +
  • + <% end %> + <% end %> +
+ <.pagination items={@boosts} meta={@meta} params={@params} class="flex justify-between items-center w-full" /> +
+
diff --git a/lib/parzival_web/live/app/store/boost_live/show.ex b/lib/parzival_web/live/app/store/boost_live/show.ex new file mode 100644 index 00000000..52030cc2 --- /dev/null +++ b/lib/parzival_web/live/app/store/boost_live/show.ex @@ -0,0 +1,85 @@ +defmodule ParzivalWeb.App.BoostLive.Show do + @moduledoc false + use ParzivalWeb, :live_view + + alias Parzival.Accounts + alias Parzival.Store + alias Parzival.Uploaders + + @impl true + def mount(%{"id" => id}, _session, socket) do + {:ok, assign(socket, :id, id)} + end + + @impl true + def handle_params(%{"id" => _id}, _, socket) do + {:noreply, reload(socket)} + end + + @impl true + def handle_event("delete", _payload, socket) do + case Store.delete_boost(socket.assigns.boost) do + {:ok, _boost} -> + {:noreply, + socket + |> put_flash(:success, "Boost deleted successfully!") + |> push_redirect(to: Routes.boost_index_path(socket, :index))} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, + socket + |> put_flash(:error, elem(changeset.errors[:error], 0)) + |> assign(:changeset, changeset)} + end + end + + @impl true + def handle_event("purchase", _payload, socket) do + boost = socket.assigns.boost + current_user = socket.assigns.current_user + + case Store.purchase_boost(current_user, boost) do + {:ok, _boost} -> + {:noreply, + socket + |> put_flash(:success, "Boost purchased successfully!") + |> reload()} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, assign(socket, :changeset, changeset)} + end + end + + @impl true + def handle_info({event, _changes}, socket) when event in [:boost_purchased, :boost_updated] do + {:noreply, reload(socket)} + end + + def handle_info({event, _changes}, socket) when event in [:boost_deleted] do + {:noreply, push_redirect(socket, to: Routes.product_index_path(socket, :index))} + end + + defp reload(socket) do + id = socket.assigns.id + + socket + |> assign(:current_page, :store) + |> assign( + inventory: + Store.list_inventory(where: [user_id: socket.assigns.current_user.id], preloads: [:boost]) + ) + |> assign(:page_title, page_title(socket.assigns.live_action)) + |> assign(:boost, Store.get_boost!(id)) + |> assign( + current_user: + Accounts.get_user!(socket.assigns.current_user.id, [ + :company, + :missions, + inventory: [:boost] + ]) + ) + end + + defp page_title(:show), do: "Show Boost" + defp page_title(:edit), do: "Edit Boost" +end diff --git a/lib/parzival_web/live/app/store/boost_live/show.html.heex b/lib/parzival_web/live/app/store/boost_live/show.html.heex new file mode 100644 index 00000000..d49d20ad --- /dev/null +++ b/lib/parzival_web/live/app/store/boost_live/show.html.heex @@ -0,0 +1,69 @@ +
+
+
+

Store

+ + <%= if @current_user.role in [:admin] do %> +
+ <%= live_patch to: Routes.admin_boost_edit_path(@socket, :edit, @boost), class: "button" do %> + + <% end %> + <%= link("Delete", to: "#", phx_click: "delete", data: [confirm: "Are you sure?"], class: "inline-flex items-center justify-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-secondary hover:bg-primary xl:w-full") %> +
+ <% end %> +
+
+ +
+
+ <%= live_redirect("< Back", to: Routes.boost_index_path(@socket, :index), class: "hover:underline inline-flex items-center justify-center whitespace-nowrap") %> +
+
+ +
+
+

+ <%= @boost.name %> +

+
+
+
+

+ 💰 <%= @boost.price %> +

+
+
+

+ <%= @boost.description %> +

+
+
+
+
+
+ +
+
+
+
+

boost options

+
+ <%= if @current_user.role == :attendee do %> + <%= if @current_user.balance >= @boost.price && length(@inventory) < 5 do %> + + <% else %> +
+ Purchase +
+ <% end %> + <% end %> +
+
+
+
+
+
diff --git a/lib/parzival_web/live/app/store/product_live/index.html.heex b/lib/parzival_web/live/app/store/product_live/index.html.heex index 7ac9aa91..dc0b61ee 100644 --- a/lib/parzival_web/live/app/store/product_live/index.html.heex +++ b/lib/parzival_web/live/app/store/product_live/index.html.heex @@ -1,9 +1,19 @@ -
-
-
-

Store

- - <%= if @current_user.role == :admin do %> +
+
+

Store

+ +
+
+ +
+ <%= if @current_user.role in [:admin] do %>
<%= live_patch("New", to: Routes.admin_product_new_path(@socket, :new), class: "inline-flex items-center justify-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-secondary hover:bg-primary xl:w-full") %>
diff --git a/lib/parzival_web/live/app/store/product_live/show.ex b/lib/parzival_web/live/app/store/product_live/show.ex index 4dad4c33..b284356c 100644 --- a/lib/parzival_web/live/app/store/product_live/show.ex +++ b/lib/parzival_web/live/app/store/product_live/show.ex @@ -9,9 +9,9 @@ defmodule ParzivalWeb.App.ProductLive.Show do @impl true def mount(%{"id" => id}, _session, socket) do if connected?(socket) do - Store.subscribe("purchased") - Store.subscribe("updated") - Store.subscribe("deleted") + Store.subscribe("product_purchased") + Store.subscribe("product_updated") + Store.subscribe("product_deleted") end {:ok, assign(socket, :id, id)} @@ -42,7 +42,7 @@ defmodule ParzivalWeb.App.ProductLive.Show do {:error, %Ecto.Changeset{} = changeset} -> {:noreply, socket - |> put_flash(:error, elem(changeset.errors[:orders], 0)) + |> put_flash(:error, elem(changeset.errors[:error], 0)) |> assign(:changeset, changeset)} end end @@ -52,7 +52,7 @@ defmodule ParzivalWeb.App.ProductLive.Show do product = socket.assigns.product current_user = socket.assigns.current_user - case Store.purchase(current_user, product) do + case Store.purchase_product(current_user, product) do {:ok, _product} -> {:noreply, socket @@ -65,11 +65,12 @@ defmodule ParzivalWeb.App.ProductLive.Show do end @impl true - def handle_info({event, _changes}, socket) when event in [:purchased, :updated] do + def handle_info({event, _changes}, socket) + when event in [:product_purchased, :product_updated] do {:noreply, reload(socket)} end - def handle_info({event, _changes}, socket) when event in [:deleted] do + def handle_info({event, _changes}, socket) when event in [:product_deleted] do {:noreply, push_redirect(socket, to: Routes.product_index_path(socket, :index))} end @@ -82,7 +83,12 @@ defmodule ParzivalWeb.App.ProductLive.Show do |> assign(:redeem_quantity, redeem_quantity(socket.assigns.current_user.id, id)) |> assign(:product, Store.get_product!(id)) |> assign( - current_user: Accounts.get_user!(socket.assigns.current_user.id, [:company, :missions]) + current_user: + Accounts.get_user!(socket.assigns.current_user.id, [ + :company, + :missions, + inventory: [:boost] + ]) ) end diff --git a/lib/parzival_web/live/app/vault/order_live/index.ex b/lib/parzival_web/live/app/vault/order_live/index.ex index 28a25926..11571b36 100644 --- a/lib/parzival_web/live/app/vault/order_live/index.ex +++ b/lib/parzival_web/live/app/vault/order_live/index.ex @@ -23,8 +23,13 @@ defmodule ParzivalWeb.App.OrderLive.Index do end defp list_products(params) do - case Store.list_products(params, limit: 2) do + case Store.list_products(params) do {:ok, {products, meta}} -> + products = + products + |> Enum.filter(fn product -> product.stock > 0 end) + |> Enum.take(3) + %{products: products, meta: meta} {:error, flop} -> diff --git a/lib/parzival_web/live/app/vault/order_live/index.html.heex b/lib/parzival_web/live/app/vault/order_live/index.html.heex index 5047a763..a0fdb411 100644 --- a/lib/parzival_web/live/app/vault/order_live/index.html.heex +++ b/lib/parzival_web/live/app/vault/order_live/index.html.heex @@ -17,7 +17,7 @@ <%= live_redirect to: Routes.order_show_path(@socket, :show, order) do %>
  • - Front side of mint cotton t-shirt with wavey lines pattern. +
    @@ -97,19 +97,6 @@
    - -
    -
    -
    -

    -

    -
    -
    -
    -

    -
      -
      -
      <% end %>
    • diff --git a/lib/parzival_web/live/backoffice/gamification/task_live/form_component.html.heex b/lib/parzival_web/live/backoffice/gamification/task_live/form_component.html.heex index 54b8668f..df1a1e24 100644 --- a/lib/parzival_web/live/backoffice/gamification/task_live/form_component.html.heex +++ b/lib/parzival_web/live/backoffice/gamification/task_live/form_component.html.heex @@ -17,62 +17,62 @@ <% end %>
      -
      +
      +
      +
      + <%= live_redirect("< Back", to: Routes.admin_scanner_index_path(@socket, :index), class: "hover:underline inline-flex items-center justify-center whitespace-nowrap") %> +
      +
      +

      + <%= @task.title %> +

      - <%= live_redirect("< Back", to: Routes.admin_scanner_index_path(@socket, :index), class: "hover:underline inline-flex items-center justify-center whitespace-nowrap") %> +

      + Description +

      + + <%= @task.description %> +
      -
      -

      - <%= @task.title %> -

      -
      -

      - Description -

      - - <%= @task.description %> - -
      -
      -

      - Rewards -

      - - 💰 <%= @task.tokens %> - - - <%= @task.exp %> EXP - -
      +
      +

      + Rewards +

      + + 💰 <%= @task.tokens %> + + + <%= @task.exp %> EXP +
      -
      -
      -

      - Redeeming task for user -

      -
      -
      - <%= live_redirect to: Routes.profile_show_path(@socket, :show, @attendee) do %> -
      -
      - - - <%= extract_initials(@attendee.name) %> - - -
      -
      - - <%= @attendee.name %> - - - Missions completed: <%= Enum.count(@attendee.missions) %> +
      +
      +
      +

      + Redeeming task for user +

      +
      +
      + <%= live_redirect to: Routes.profile_show_path(@socket, :show, @attendee) do %> +
      +
      + + + <%= extract_initials(@attendee.name) %> -
      +
      - <% end %> -
      +
      + + <%= @attendee.name %> + + + Missions completed: <%= Enum.count(@attendee.missions) %> + +
      +
      + <% end %>
      diff --git a/lib/parzival_web/live/backoffice/store/boost_live/edit.ex b/lib/parzival_web/live/backoffice/store/boost_live/edit.ex new file mode 100644 index 00000000..201a754b --- /dev/null +++ b/lib/parzival_web/live/backoffice/store/boost_live/edit.ex @@ -0,0 +1,20 @@ +defmodule ParzivalWeb.Backoffice.BoostLive.Edit do + @moduledoc false + use ParzivalWeb, :live_view + + alias Parzival.Store + + @impl true + def mount(_params, _session, socket) do + {:ok, socket} + end + + @impl true + def handle_params(%{"id" => id}, _, socket) do + {:noreply, + socket + |> assign(:current_page, :store) + |> assign(:page_title, "Edit Boost") + |> assign(:boost, Store.get_boost!(id))} + end +end diff --git a/lib/parzival_web/live/backoffice/store/boost_live/edit.html.heex b/lib/parzival_web/live/backoffice/store/boost_live/edit.html.heex new file mode 100644 index 00000000..ce60ab9d --- /dev/null +++ b/lib/parzival_web/live/backoffice/store/boost_live/edit.html.heex @@ -0,0 +1 @@ +<.live_component module={ParzivalWeb.Backoffice.BoostLive.FormComponent} id={@boost.id} current_user={@current_user} action={@live_action} boost={@boost} title={@page_title} return_to={Routes.boost_show_path(@socket, :show, @boost)} /> diff --git a/lib/parzival_web/live/backoffice/store/boost_live/form_component.ex b/lib/parzival_web/live/backoffice/store/boost_live/form_component.ex new file mode 100644 index 00000000..67b4ebec --- /dev/null +++ b/lib/parzival_web/live/backoffice/store/boost_live/form_component.ex @@ -0,0 +1,63 @@ +defmodule ParzivalWeb.Backoffice.BoostLive.FormComponent do + @moduledoc false + use ParzivalWeb, :live_component + + alias Parzival.Store + + @extensions_whitelist ~w(.jpg .jpeg .gif .png) + + @impl true + def mount(socket) do + {:ok, socket |> allow_upload(:image, accept: @extensions_whitelist, max_entries: 1)} + end + + @impl true + def update(%{boost: boost} = assigns, socket) do + changeset = Store.change_boost(boost) + + {:ok, + socket + |> assign(assigns) + |> assign(:changeset, changeset)} + end + + @impl true + def handle_event("validate", %{"boost" => boost_params}, socket) do + changeset = + socket.assigns.boost + |> Store.change_boost(boost_params) + |> Map.put(:action, :validate) + + {:noreply, assign(socket, :changeset, changeset)} + end + + def handle_event("save", %{"boost" => boost_params}, socket) do + save_boost(socket, socket.assigns.action, boost_params) + end + + defp save_boost(socket, :edit, boost_params) do + case Store.update_boost(socket.assigns.boost, boost_params) do + {:ok, _boost} -> + {:noreply, + socket + |> put_flash(:info, "Boost updated successfully") + |> push_redirect(to: socket.assigns.return_to)} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, assign(socket, :changeset, changeset)} + end + end + + defp save_boost(socket, :new, boost_params) do + case Store.create_boost(boost_params) do + {:ok, _boost} -> + {:noreply, + socket + |> put_flash(:info, "Boost created successfully") + |> push_redirect(to: socket.assigns.return_to)} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, assign(socket, changeset: changeset)} + end + end +end diff --git a/lib/parzival_web/live/backoffice/store/boost_live/form_component.html.heex b/lib/parzival_web/live/backoffice/store/boost_live/form_component.html.heex new file mode 100644 index 00000000..325d7a12 --- /dev/null +++ b/lib/parzival_web/live/backoffice/store/boost_live/form_component.html.heex @@ -0,0 +1,115 @@ +
      + <.form let={f} for={@changeset} id="boost-form" phx-target={@myself} phx-change="validate" phx-submit="save"> +
      +
      +
      +

      Boost

      + + <%= submit do %> +
      + Save +
      + <% end %> +
      +
      + +
      +
      + <%= live_redirect("< Back", to: @return_to, class: "hover:underline inline-flex items-center justify-center whitespace-nowrap") %> +
      +
      + +
      +
      + + <%= text_input(f, :name, phx_debounce: "blur", class: "mt-1 focus:ring-red-500 focus:border-red-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md") %> +
      +

      <%= error_tag(f, :name) %>

      + +
      + + <%= number_input(f, :price, phx_debounce: "blur", class: "mt-1 focus:ring-red-500 focus:border-red-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md") %> +
      +

      <%= error_tag(f, :price) %>

      + +
      + + <%= number_input(f, :multiplier, phx_debounce: "blur", class: "mt-1 focus:ring-red-500 focus:border-red-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md") %> +
      +

      <%= error_tag(f, :multiplier) %>

      + +
      + + <%= select(f, :type, [[key: "EXP", value: :exp], [key: "Tokens", value: :tokens], [key: "Skip task", value: :skip_task]], prompt: "Choose a type", selected: f.data.type, class: "mt-1 focus:ring-red-500 focus:border-red-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md") %> +
      +

      <%= error_tag(f, :type) %>

      + +
      + + <%= textarea(f, :description, rows: 3, class: "mt-1 focus:ring-red-500 focus:border-red-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md") %> +
      +

      <%= error_tag(f, :description) %>

      +
      + +
      +
      +
      +
      + <%= live_file_input(@uploads.image, class: "hidden") %> +
      +
      +
      +
      + +
      + +

      or drag and drop

      +
      +

      + PNG, JPG, GIF up to 10MB +

      +
      +
      +
      +
      +
      + <%= for entry <- @uploads.image.entries do %> + <%= for err <- upload_errors(@uploads.image, entry) do %> +

      <%= Phoenix.Naming.humanize(err) %>

      + <% end %> +
      +
      + <%= live_img_preview(entry) %> +
      +
      + <%= if String.length(entry.client_name) < 30 do + entry.client_name + else + String.slice(entry.client_name, 0..30) <> "... " + end %> +
      + +
      +
      +
      + <% end %> +
      +
      +
      +
      +
      +
      +
      +
      + +
      diff --git a/lib/parzival_web/live/backoffice/store/boost_live/new.ex b/lib/parzival_web/live/backoffice/store/boost_live/new.ex new file mode 100644 index 00000000..6c796e70 --- /dev/null +++ b/lib/parzival_web/live/backoffice/store/boost_live/new.ex @@ -0,0 +1,20 @@ +defmodule ParzivalWeb.Backoffice.BoostLive.New do + @moduledoc false + use ParzivalWeb, :live_view + + alias Parzival.Store.Boost + + @impl true + def mount(_params, _session, socket) do + {:ok, socket} + end + + @impl true + def handle_params(_params, _, socket) do + {:noreply, + socket + |> assign(:current_page, :store) + |> assign(:page_title, "New Boost") + |> assign(:boost, %Boost{})} + end +end diff --git a/lib/parzival_web/live/backoffice/store/boost_live/new.html.heex b/lib/parzival_web/live/backoffice/store/boost_live/new.html.heex new file mode 100644 index 00000000..71f84925 --- /dev/null +++ b/lib/parzival_web/live/backoffice/store/boost_live/new.html.heex @@ -0,0 +1 @@ +<.live_component module={ParzivalWeb.Backoffice.BoostLive.FormComponent} id={:new} title={@page_title} action={@live_action} boost={@boost} current_user={@current_user} return_to={Routes.boost_index_path(@socket, :index)} /> diff --git a/lib/parzival_web/live/backoffice/store/product_live/form_component.html.heex b/lib/parzival_web/live/backoffice/store/product_live/form_component.html.heex index 157bf9e1..a03ef03c 100644 --- a/lib/parzival_web/live/backoffice/store/product_live/form_component.html.heex +++ b/lib/parzival_web/live/backoffice/store/product_live/form_component.html.heex @@ -21,28 +21,28 @@
      - + <%= text_input(f, :name, phx_debounce: "blur", class: "mt-1 focus:ring-red-500 focus:border-red-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md") %>

      <%= error_tag(f, :name) %>

      - + <%= number_input(f, :price, phx_debounce: "blur", class: "mt-1 focus:ring-red-500 focus:border-red-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md") %>

      <%= error_tag(f, :price) %>

      - + <%= textarea(f, :description, rows: 3, class: "mt-1 focus:ring-red-500 focus:border-red-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md") %>

      <%= error_tag(f, :description) %>

      - + <%= number_input(f, :stock, rows: 3, class: "mt-1 focus:ring-red-500 focus:border-red-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md") %>

      <%= error_tag(f, :stock) %>

      - + <%= number_input(f, :max_per_user, rows: 3, class: "mt-1 focus:ring-red-500 focus:border-red-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md") %>

      <%= error_tag(f, :max_per_user) %>

      diff --git a/lib/parzival_web/live/backoffice/tools/announcement_live/form_component.html.heex b/lib/parzival_web/live/backoffice/tools/announcement_live/form_component.html.heex index 72c4f2d8..691c63e5 100644 --- a/lib/parzival_web/live/backoffice/tools/announcement_live/form_component.html.heex +++ b/lib/parzival_web/live/backoffice/tools/announcement_live/form_component.html.heex @@ -20,13 +20,13 @@ <%= live_redirect("< Back", to: @return_to, class: "hover:underline inline-flex items-center justify-center whitespace-nowrap") %>
      - + <%= text_input(f, :title, class: "mt-1 focus:ring-red-500 focus:border-red-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md") %>

      <%= error_tag(f, :title) %>

      - + <%= textarea(f, :text, rows: 12, class: "mt-1 focus:ring-red-500 focus:border-red-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md") %>

      <%= error_tag(f, :text) %>

      diff --git a/lib/parzival_web/live/backoffice/tools/faqs_live/form_component.html.heex b/lib/parzival_web/live/backoffice/tools/faqs_live/form_component.html.heex index 7ce1936a..db612d1e 100644 --- a/lib/parzival_web/live/backoffice/tools/faqs_live/form_component.html.heex +++ b/lib/parzival_web/live/backoffice/tools/faqs_live/form_component.html.heex @@ -22,12 +22,12 @@ <%= live_redirect("< Back", to: @return_to, class: "hover:underline inline-flex items-center justify-center whitespace-nowrap") %>
      - + <%= text_input(f, :question, rows: 3, class: "mt-1 focus:ring-red-500 focus:border-red-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md") %>

      <%= error_tag(f, :question) %>

      - + <%= textarea(f, :answer, rows: 3, class: "mt-1 focus:ring-red-500 focus:border-red-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md") %>

      <%= error_tag(f, :answer) %>

      diff --git a/lib/parzival_web/live/hooks.ex b/lib/parzival_web/live/hooks.ex index bcd4943e..6d402c48 100644 --- a/lib/parzival_web/live/hooks.ex +++ b/lib/parzival_web/live/hooks.ex @@ -5,16 +5,25 @@ defmodule ParzivalWeb.Hooks do import Phoenix.LiveView alias Parzival.Accounts + alias Parzival.Store def on_mount(:default, _params, _session, socket) do {:cont, assign(socket, :page_title, "JOIN")} end def on_mount(:current_user, _params, %{"user_token" => user_token}, socket) do - current_user = - Accounts.get_user_by_session_token(user_token, [:missions, company: [:connections]]) + current_user = Accounts.get_user_by_session_token(user_token) - {:cont, assign(socket, current_user: current_user)} + if is_nil(current_user) do + {:cont, socket} + else + {:cont, + socket + |> assign( + inventory: Store.list_inventory(where: [user_id: current_user.id], preloads: [:boost]) + ) + |> assign(current_user: Accounts.load_user_fields(current_user, [:company, :missions]))} + end end def on_mount(:current_user, _params, _session, socket) do diff --git a/lib/parzival_web/router.ex b/lib/parzival_web/router.ex index 3e2db742..56b8bf80 100644 --- a/lib/parzival_web/router.ex +++ b/lib/parzival_web/router.ex @@ -77,6 +77,9 @@ defmodule ParzivalWeb.Router do live "/store/", ProductLive.Index, :index live "/store/:id", ProductLive.Show, :show + live "/boosts/", BoostLive.Index, :index + live "/boosts/:id", BoostLive.Show, :show + live "/vault", OrderLive.Index, :index live "/announcements", AnnouncementLive.Index, :index @@ -138,6 +141,9 @@ defmodule ParzivalWeb.Router do live "/store/:id/edit", ProductLive.Edit, :edit live "/order/:id/redeem", OrderLive.Edit, :edit + live "/boosts/new", BoostLive.New, :new + live "/boosts/:id/edit", BoostLive.Edit, :edit + scope "/missions" do live "/new", MissionLive.New, :new live "/:id/edit", MissionLive.Edit, :edit diff --git a/lib/parzival_web/templates/layout/live.html.heex b/lib/parzival_web/templates/layout/live.html.heex index e28fda83..390acd1a 100644 --- a/lib/parzival_web/templates/layout/live.html.heex +++ b/lib/parzival_web/templates/layout/live.html.heex @@ -120,8 +120,10 @@
      <% end %> + <.live_component module={ParzivalWeb.Components.Inventory} id="inventory" inventory={@inventory} /> <% end %>
      +
      <%= if @current_page == :dashboard do %> - +