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""" +
+ <%= "Inventory (#{length(@inventory)}/5)" %> +
++ <%= boost.description %> +
++ 💰 <%= boost.price %> +
++ 💰 <%= @boost.price %> +
++ <%= @boost.description %> +
++ Description +
+ + <%= @task.description %> +- Description -
- - <%= @task.description %> - -- Rewards -
- - 💰 <%= @task.tokens %> - - - <%= @task.exp %> EXP - -+ Rewards +
+ + 💰 <%= @task.tokens %> + + + <%= @task.exp %> EXP +<%= error_tag(f, :name) %>
+ +<%= error_tag(f, :price) %>
+ +<%= error_tag(f, :multiplier) %>
+ +<%= error_tag(f, :type) %>
+ +<%= error_tag(f, :description) %>
+or drag and drop
++ PNG, JPG, GIF up to 10MB +
+<%= Phoenix.Naming.humanize(err) %>
+ <% end %> +<%= error_tag(f, :name) %>
<%= error_tag(f, :price) %>
<%= error_tag(f, :description) %>
<%= error_tag(f, :stock) %>
<%= 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") %><%= error_tag(f, :title) %>
<%= 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") %><%= error_tag(f, :question) %>
<%= 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 @@}|0(fdaHgLsjC_L*Ac6wv8DaP0nitb;o1a>|n#DIhMD-7SZ{jp@l$1jz^ z3`XZnkCcUEY8ONwQCJ 4w{w8hc^3Py1K zPVUMdOGBP9llB}U#Sk+fDgWU{-Je vasWtj zT|d=zk&Ctc>sLwzS|+|+Gn&Oz@cExzyKjDm`+1nLa-g2J>VX_E?BUCzphHR;zjBBe zXUA|;p=mk4oLVX2y$71ko6qNZQt~=$S<_u%3>YFjlD}&yFn5_(T}zcx;zr!EtWqr% zT2`sV^ +G z^i+1*^mNxrQb0*9XZs4j0Y%V}l*i~-NTifrIYmo^*p8HtOTClU{BCkRNlWi5Bq98c zkk{wZ?Zw(+oye(+O45wgNtnbMH2ia*uMlv;JADYGCPyS;C5oskK9@dn1>|BkO4g}| zOGfqYWw9aU(XW?+;=`n|`1c8NTD4cu0?Cfhy{fgYs-kI(v+iYJRjO9SQxk=hA0NME z$~`jF1YvA)FQGag->MYiz2{}~Gz{8az~^Ll)&YLK^F8-9VCeB>c{tSRi#9b{l=IFh z?&!^{;9pbO=Oh8<5u~i-oE}GJ80CyQSNND0UH_;CbJ;ImoS$uBZ8i;MG5{$GcBEF* zEsZ$3n5iTh>B %l@7E+4{J9Etf%dP#Hl zaQTgsi#YmGY&C7xE ;8kT9{y+rGnGArw|Q|^1=o(t^0X< PM=q2m g#MT^Ps0O@ z1%24R#fw0oL@RJZNcqa!^Xd;%i_F-8F#Em*PkNp6Y^$783gp}}Zj9EYa8p_O>AeRh zCcbX$z}baDGok#ncAjOqu_OoAa&61;2e)dXW@{FtqTr{Jie=)IvF>5A57{p D8a@nq|HWUc6p&Bvo+M!to; ~B{=U!VT)`3 zUrH+SLlE)fmSf-Dc__fWkC{^TT#V8xY&PFLJattlS!t35D}BM5GbfR+8<{{Fi}h>0 z4v`9Pt-x^sMx*Yb1+5eg+HgNAusp0zE{Vofzijmf4(Ak%-#GXI`I%k4({2ncMc%-v zM11e?)Ss8Iv0T|sP9T6P4<^eY<~M6)CcQ}2>?ioE7g{XDkfa%IS^JU9-RZIwV@W_I z*Dnfru@wVm>@F=eeR|GfQYR3iF3)&RU+Cw!w4-|+5dvniV$xMYeuQJWo6eJll}waN zR=DK=n|-tW(#gwI5@W@fLd0Fi%>Ior78cwbafS@kxJ)eBW!5FfJgv)6(848#v;+)k zch(0^!nF>%AU_55h2{!gN+Wr33Y6tBDukvltR{`j4Q~3~v{fFQ;yx}pqxS~sUHHJq z+3!q&%kOOCf8;AK1$wknfzf*+s;PN6|1C=Ncc*Rk8jmCu?)7U$FM~3=Dk)^+WPavx zLEVro^q}Q#B+O$EYP`BK_0MjUJSj=5UmMK)=QGR%e@42vHzY%;a3!v@PeldSS@}P! zr4z-yUDPxTY{}B+$ XVm&O0@sFEZY z;mu(V9ABK0fh*pl0alceFrA@omkqLD2$>i@plqye!c%}E2g!kxq)cH+>(o}hSu<7J z$n3&vMJ=>&Uq7gp*STpEmmbc{3Z(6gcH@+ $%LH9(Ho?KKSF2=_xGS{m7w5t z(IkG+X{+8Do^U4HC|x9selG|6gUfb|1z`PW$)b|da}z}`pPbJWyvK F(hW29TI6JsXtq6|I?^W~HBtXrgWGOu%Jp xdBg$#lf^YZycBb`ZYbGZ=G&v zBUUDaT?(MZDhxN-hVB*959!dT6?LUg^=Stsr_ZZ=m#?S!9Q>=LJA2prl8li$Ru}+P zR=gRGA$MNE ZYE3W^n;9f2dpD8Z~grzekNKt9&6FuGk*JKEM8T3Gv1#c(#Dn zh#+NNKdLxMoTkK3L(Y*gW<#OnE%|a+dP)q&-#wrDMhEVRszV4erXDZ&M_jq$_ph`o zF>J<<$Kez|1ceK@AS37HE~2HWcrUdjJL1n0S3^Ar^f>yax@-8XW#(5Xfax#0y!R76 z(SCa 8&*uMjJntQ5(MVC`!y} z(^8C|3$*0jVcP42H!a+L7nJ +J z%?ruTWQqsv8H)k%=~l;NtWDw&g|Q`6^~GBQQ&(?(`1!1&oKWNlmp8-PIe!Bc0wA0X zKQYJq 7ng;E `Uytp;GmW9$4iwF?vqwyYPvq4pb_SNiuE~#L@O&0}rMf zB6}iC(W`P}XyKMMdW3tBtU=K+>MF7=oVa@b*{N4MD`-jo&Al9Bet>iT!?2{1L zucYvqQ=6og-IvM*CRfl;=@H+KyIAdHHm-O^-pOEqQ-XK5*5H=(SrNlbPfZUpQDg5p zMQ#pQPSk9+N~|vrVkn#pz?0hrd7C-plfZB77O&anq6P|(YyKTl6t=u}mz6u+&a)6V zNCc)o5*;t2ZMTR5y%DAuD+OT>N>84) Pv7l01`)(aPE)0{WTwSZ6rlh zb5XKmEDJm%F#HfR+E%BW8e o` zoqY_U@2PQE xyKo5g`_L3i1}o8?bcIF=XLoZi+SC9g&o3*Et)A28-vyRRmtKfx53ZqBBzY z0{G2}cdb|?r MBRe-Ya4i&p8j-&GttzJ1^)O$HQir@n4|Cw GkgAa1k`e))5vZ%{Bsk461R?fheztCBunUeiun2!;Duw4 zZQkP0?Wpfgjo`aPLEaP@c<$UmNMmtBqMziGQ+#SIk9&S;Kohg0=M?dGEv=L#JHiIC zfC3J83Px2MxAJk;hcHTjc|qf{#9OHGTd9P~`JkX)^FfiI$r9Vh3qL~Y5lv@tvS9=b z?-}J@Z%H !`HW;8=xC;-kd))hK(JH6;w zKyy}piZ`kKQhS%EdsHbNf9>mw*y@`pH+iFJ>o$ba*pLF~V^(}`C_0Y_VaIMAS6KX0 zs +kul|wF1Vh zGV=aE)3zr3+qOFeG}P}v{F;5JF`ALE;X@F+^!?O?ldJ*V#Eh{jVn5<&lKd* uX*$QQ V1yt;00InEd@(( |T}|?it5RF101I20kiNcagvN0JC`OThELi^YnN;_y zutCxB!2SuhPR6JGOI;MQ#~~0*Fd4$X&tkKx5fn>{S86kv|3zx6Q3SQFAa$untw2?g zkR%Hksz~i?)9lj=j<$oi`c(m?#i%8=xn*s50C^YLhBz!jH}2i|$MZ(lPb%?s$55?~ zJwKJDy18)Kfo;kmg&e+vn@(s5+R#Nu#tBIVCzioeSVoZkizGSHm?&B%%YH(mdFzR4 z6Kmp*s`-P%@*u1nLMQPyKnGRkaKIWBj3{8Iur?L6D)#S!3xH$C@EH-roOxNxA^P+H zuG%DkF(a)`sQXaq=01q5@KWwO+Hy*fLClHUlRH>%Z MLjf z1azl$m=em|f0qSW(cJ YRKoDwJBnHIC)WlqZPdZ2v_HsAHy ^s +q0|I-O_ zYU(Lt%?HI+H+^zHYvF%XIH~}~jSzonAC ymoqQ z6pWrb9ApAgA0(9`Y~2;(Z_YpX1K^XAW5&ITk+An$jxztj{|HaLOn&_@2*8RiAQY2` z80`>wmi~&)t2NJvnHew=Q~e)FW>{-& 9&YJ*^n;p2O|o%= z#Iod1{A$VRPzXG>97%r|eCdO=7XOKWB}>l!&3xvl1*v%(Mu3j2KN(m8GltrgR2b v;?Jo)Bh_k+t`>?o8Vz2maxOpjv|+Bf{v2$!_Mf)g@ej8$AOJHkg&7Ea zz%ZzDGUftag<2>S%Een_`!xR+rpU)$e_FR{9=P)FdCc@_2Rm?0HzivpBR5c9Q)L@4 zapOMKIj|n=pn#AgyCa%=|5BMHQLM`M9~sui 1EMKPyh3?G(1F=03?e!@h zp68TcU#`4!s9A-`hIjq;B0ivXmC|Yp(W{;m`+u;BZ(f0TTSaAz;~b7^Dp8=%udF@m zr=3z;G8d127Q2kr^>r`vSQ^;nJLGkxTf`8HW3Zisd|NQp1grdYu{;v*)*842;^`m| zFrGHqXHaErR381r;_W$ebE4SScmM2rx20Oj111;EHNnqdbHdc>)<~&5b1ZEu0Bijo zOG4x1>h4|fuNxG}tLgs$z`Z@92Uk?YMIgM>D=5Z%(9*pdygt|em6KdZB<)mkNQLFc zpDTmn*oLk+0 LqU&OQ{NEmu4GSue@~K`pzW#+C?V% za`|ISw0k1ibM!|gK|CiW-|S>kJU|d_xXaPA>RnxM96PXL{ML~flq_&Qb1P%;z^|92 z#{b2%w{g_e2MOGiQ37Bu1u!luR#0Ig-xF9HoXhP(;;tP`FH*KNluvx>3Z+NfT7tb| z+)wsY1)wEK=_XmtT7xScsL{*MC!|%AkFez-dp;$?ZOPq1(CaViX~;WfS5UqH`7EV; z8Qg|b{IqYF5?=XG71JZpkv{GTYK|2Hw9A?=XyTrwe9Ajb4>jD?_6;B+-VFXrsk+ti z4fWdP%6j`0 Me<>6gc%TZYvisnAV*zB%&bbIf{fYX3>eJ)eiW zY%PPmd;dbZY!BhZ9?G+hyb!4@8IKd+W&VA9dz;{{h$Np!%re$&moqkUgFWKNs|Z!M zs5{-l{6z{ADb;Zq0P$m`bv{7tvu|Z}nQSlWtLY3Z>|JRs306V}-$Iq9d?HUBl<(N< zaKv6q9jAJm!9gyXmx#V2rhsB$v$aJaf$y1KGk)vPu!fZiSB Kd+|#U#26v$%H{bYac=r%pzRmUOyzIvAVihn%jfDz^3?6!*AOTWD_rA zKL-g(HNA5n?*ZiMiCzN0=M U!_U#xu1r~_Sx|Rfa`%|1!J7_hWI7pWq_5d*47Z6s;&)I$-Hi;q>Or1xL9Mjk zWG1sDSnJxPkc6+}L;YR=?kz_E&{uSmhB5_y*q|1Sa&8}Av)O1u?fPp0lW*c_NA0#o z`a#nM`nlDZr+k_U6Yl`{av;aMPu?E$T1^_w*~%IeFLEEdjVRoUHaC@b?5B67@LHoD zjXvC-8@egbmJI!{&z;)tan89sJd4ZyJ^+cw>;OHv7T4OQi+No1Im4m~?-t-i1C)k8 z*uKqJ$yM|C-k}zuPGj)yfc@@Aj%VJgnyFy`*sXYIekd~GLHG3~lgR>tf>}3uYL6E- zl7ZcZb=qlrTo>(hf%8X6$DplNjo!!ve-6X3IT|X!SZ_G6t5+RQgK0ZaArt!{(MYpe z{QU>i!PVd&Wg}xY8Dx_a3ag$%7*+NS!=Oj0*RrHzWLat=8R=cBe#X)Q-UnD4r;9+a zr$wOTfyL)t){zyqw2?9Ic(ULTQ(1fDc{Jy?pWXtk`|3ujm^<`ZZ;#u;d?Y1E$oH2h z(MW4zL&x<$Z;jA<_2h$n^ZUGtg{Q@((nTFt!QNPY++;{!W|1O_)BMHX66dMDPxEv8 zf9yy{p9nTF4eDYvu)I$??L1(Ka2pBlCUyhQAOU(B>2P8Y)#jQ)(v;X${ot*b;1O`I zNYsF0;q`|YsTczy*$h`KZ=q3&Fun8Z3xWpY _Y;>X zQ^36fQ3J|_n3xy`W@L4>Ph1ziRsC5e+t-Zy@AhkI;TvYxX&^?H{maxsgB+AC!b@#r zKD!W{+=T};#l3IgD}?~^5;C507~R;v@+BW@vgfbO0xOn|a7XTOQ+nTxq3^mz9+c8H zx5a4p*G-eBivrRwM?`&XDNBVQj^6Z45xk#H(0UN4<~YyK8z{edk1Q1KEK)fxjri#i z6fbV}y_oqU;id8f;oL7rcZ{gJAmg>DS8AZD(1RWi0auTvw7IW95I)qdmmg@Sb_bJ< z%T0N)Ss-DT-GtO#w|5}7&b!P^k%G_s(}>qlQmGZmYvwIKuaVn^$Cl)F5+1?k?srRg z^u_oQIG@2rjGX_neOuRftel2Zq&1e^67NuNg5EnuM2usJZ y%f1l`;scNgs|^L5s-ArGdkRDIkeo B=M@)fXH5f|_F=JCfd=}$}7T+KA0eJs~BNO1S@<1^VVzEGJG>9{K&PmD?<{E)@ zxmu(Ij%Rbf9pg{G?(u7*#6gpxaHw^Azb*Y<<7Rxa#_>gApOd3&S`y@qPX@pT9p6Mb z<4~f47fN1_=XCAF3v>T`HbHj5)FdN0E8V8J$68C0>D@P;y_?AoU6z9BGB&(lhCtA# zAGnW 7z9U43C#BfxeHVe*WCZ}+-R$fU9-}a^gHz;Qr2EzS4%n4aCsL-Vh%Hh zBC7tPA8n*KW~>_&Wb4h3Ae#c7(FRcfldUM!{>HH(7};~(MnloYS}FWanK{0jl=+vt zn$AuNxPRcXmEV-vTV=eoB2Pha_a&4z!OMTjkuh{Mj~fxzA =TIqUn!X}2lL$XLkPxKvZu6u@tCh+|$Zs$YC z+oT19v9Xm!ezFnchhRv=$^kFC%Gt=r-~GMmeA{9a47-8B&+H!^n;aSbgWy-lZDx*N z 3faLy*_(gNrY;UKRg8b=KHu`?Yhw0P;^f|c>*n11`n(oWjfv+` zGreOvr$3Xn+*mb>P}C}*k)Tbp7g%$X0 P2u_<~tST)mA3A5$5xGL)K# zKS(t_3*vCBXslT7t#!Y81b`-z1C0D$-}cu+hiaU0?WdCW&52LE>}x)peoOmNo&hT( z`z?!~YoTu;;g4! SbcO@F#n0 z4*#+YOBXYI_Ij2}apwJBEM;zVqwbeRhf-j{LAb9)ZU}KD-og~gN9H S>jIedoFhO~E!84^CW6kO&CmSdt4C znDrqLr6SpfuH=K<=xAfff0ksZbN7wf{=%dDlkBnC{4G6K(Vxeorl%_MbV{mB-@n_q z`d*73vA<`fp@h&`DRzgo2T4xX*SF;%?0ws4!QOE1vc&tx;DubV;#Wkd$A(u-@C6@^ z-d2o2HByg_N?se(hMmQ_2pa2#wT$hQ70%rur;uRGJz>~uqoR}tcZkskqyo`Sa;0q1 zc=7)Bze!%kpBuC#gJ)+>HW!Yb%)H^DEyuKQ9nP+fCOS%4OSiL6$Ty$odOv$iC0pF< zx+FZH&w!A(4Hw 4F=bU&E0fm!iDI{(g_ zAg!!5y#So!#%OjXO~-M1hbX*Dv}WL6rh zyrAt}>MmFlBI&QqMh*(6>-+;UB1mhQmZxp#bddZ^fpX`P^Zbw7mamn{Iz=~yC))Tm z(RDkOB~Kl*$0d`;W{gxc9S?bb;j&e=l>7?bz&cwTMy$0e4H_gb`l*v^)*m(84SxD+ z+>$Ei?(uqTa?$pi>N4avf_<-=%~?_E3IjwS>-cihcNFw9KktLHH49XO(&y3GDjEo) z#`e96r3hoGuTo@ue*zf kLjen{o4pYthou zAo`^|EGjzJpNb=Csv9n`AEh~56~3_ ofbwOj5|4)x6Df4AF0NejFpPtvOob zF-k8}u#)QAw~gei5u01MNw8lCSrHzDEy8x5%Tm5s9KEcb{aI_M-J7yy)y!9zCr{Xj zMJMv``ew9}yJSGy8a9I;gS8k{B5e@f6-iIC-}tr>M^4-};UfHQz2R70a+95pH{{NN zr@U724EepwU}x=9*i?)F^H}cm?{jGH!-w~uM;3cM3sgHivQKk;CO7%enBr*lGt@J^ z+TGU(o=^}vy<|jSSy-7q>8M#l@J_Wje6-erF-}+>*W9Pz_n}Oo3#PUFRHQM@$8zpE zW~0Hy=`zvtG-Niv3H@aAWXwnI)i5lN_VND5#$e?@;(8U;a$&}YQZe#$*Tgv*1P+c3 zm{>Sqx?m^jWeatUB==0$WVb%DTrPSK@0y@oT{s*jPn`aOEDVjROwSlYSFLL)XWXNL zcM{p3yRGWAg 3(uHy_I`+>kNF$TyY#U%|msZN2bP z8Z6<(?juMgx#Mpc@AvJbP^HA!&1lev0uz02bJmv@^>Df^C3#<;(0H9DK?dsfoRe}d zEoRY<&+o4IRqHO5fr#w=%x-QqJu#V)M#uOa#a0p(GtGan<%)8J^xXc>*gG|wEe`L= zrQ5}lB57|ZwdMkiw8EO!4-SHLeMAa-0I2oGvo|=o>M@wo%F!;js)~I 5@ zbd<)sXZVv#y9I}=CP+k{i29T|Di+XVCRFO{9LCLdY?OY#UvEr1U2Q6>>1%Nqu-2-* z $dCwF(N*h(YnV#O)6|f!0#?l*_e$vc8vWr%Fa9>3y_wZI zAYU3W7D9^KTYH*Ed=$Mt0FztsJq)hQ9C)c*y77#+MfvFH2}&1>Jp1`D`?~v~T}089 zyT`Sr+7otMmQpq7RH}-E7&|^2d@7Cfftl&?g;;N6)v;rUOomR4Wr@3+(wbcDZ9JOV z%PR0KbI(E vC9-kbB$?oWf|8_p@3Q?1XjI zQKF>wZ0g&kVCNx||C|pvmUorIKOB~*c^Xqnc_jZFwFWvHhoQU)s0N=k3o-T$-DwFUK1KAoJM3lNKZ z5I 6NUCz12r9fhCj6$zP8*D*_Y z1Xhgvs^GTZX;Shj#ffh`QF#zWSjnhA+_Fg7EY}W`J6##HWXK3Rh-o~+=Rf6T@w7x+ zZMCsT*cj+s<-c{nHo|!}xOH|aP(RxeAi%_{S98&PspfPv&|K8je-Qee=WHRW?O;00 zV|Ysv>T 7ao+tVou*S;=j zrIG)|tR|)wDSBK#efrJ(Np`~~hl^f)hO^) GDxS&xpkdmpO2Z;G-|DG(yavFL@(B 0rVlgYw_g?67TA(MsqVnJNBq6 3CY3V z_T4_&a}29ln?6l M1#N*O1iiQNCzPI(6D?@k!u&ZGxvHa_%r!||p >8dKY>;DoQOsSSpBK%neNEKHrfs&dsRABO9|W;U@Kr zY)y7>-dShZ$=h!c$ue3hoqM~FRak=e>(lu*h7Y2~?2a00>YF3uszY13^{SMs`q=SO zPE>u?h60{J@2E??wHtm?BJdN`$-`2zhw~XHCCeFyooAal22Jd_lkE3*=F +_LS_nDam!noMnlpW3KlO&TUfgXrtKgbLIVRrjjXNmECDlmre{yVW2iU zV{7^>N@btrx8)G=_;kudKm56p%2s?ovk#5T=$+&^M+P@Bvfom3sZ>@=YN)nrT8>z` zwc?R3mm`-{a#GFRaa0M~sVY|6`u6GO<8>p71)28q6KlHGy7~{O6W1J xbX%Xi)MQf~N);N&0aYrF**-f7D4^6mL%E|2$ zF?8~OTujvy-LXa^eB28V F3UZwOH@bn^t +UehX7Tk&Uaon`1VJ%N?V#?W{E=r#&I{dqxB5mt$ zaVO#X4++>musie0C4bA*%1)Wc@@ep>9o@Q%f4%dKq>lPn>s&_VYRew@-|6sDz^71m z%9N~r%KoG$Zp>njVG9?jKhA0;-N&971$i0`l3= 1b?+iK%~YL3Qsc|!h#uJ+$1oftPY zH5PKW?PmSqc+q%qm#>O5-VN%p%C-OByP$2at&;57Yo7l4x4n95C)a%%s}+sU>JK`o zKMrR;@OTlFnU(UAL1r~lTaei!; VCSQ!05{y$xVsN zj$wQA0?Ivma@ohNL(5v@s2}gDw)~qh@qQ8#Tn$G0C0@7Wx#MCK$iWnm 7S54dkk(a!>2<&*|#~uQ5O&S4p>u~?xd0bjuR%5%88c} zq3ZLt%8UqHx`y>Bayy*a=N1aN6SHH8MZ}+evev@N&Dwb0uK1Ma-u-cYL2l`7VCE_t zi=cq7;X;l3{fw*UCaxG$R1p`_s#A`oG0ZCZed%N6+23N~FNo~LPY3_UKCvLrv>6vp zh=gH 0CMJ=XMws=6G*jA!A&%^ZxD{;-Wfj_`J{m$iIZ_8y7_xF1a;l7b`K;9v=z+8j8s5 zr=?O*6NQW)pB`^P $;y^0DA$t=@!;U(FU|f87}AAFrF`wz${the9t6wj zHI}yI&8+a}=~f!17TUFrwNo_q{Xk2W#LW~bow&4aE}Wt9c>gzJVDsb;-e;nM0b`~a z2G})_<@S$(gBe>7&K4 6V8N=j??U8qd9+zYfhf!@oAxc@#0Z1C{ hA!qxA6koXzLVIo-sO&5O+wga|om-N8$W&OL*l53*m9 zRwQn+4!L%o9nHyn4ow$tKCw*0Grzv@rt^=LlRlRSq4MMqd7K=uREDBn#G^C&BS_g# z%5CYYR)5QFkPgmX_G_S(1qi!22}&Q7OExZijfoeEBwYRP_@eu&kjwh&E=keoJh> ni-GXl;c~}*88fdso;AF$3Ao@8yV3?N$Vy?##`@wwNw&`wFOAnPHhQ7u z45C}=;z6yZu<;#xj0{3MBGVE@^m|bG;u<5hZ5t2jc15%qq5IUB5|Qw!Sc!I#P`snA zv-V`wCy&p6rs@XuV@qhLm)AgtW18|({T8Y}J7-0wFubFzxWrt~IZ%>39*E)Q?MYlq zEv(ybENE_kOAmV5y{krqvaJh`9$>G&p-yW*vT`b4k~b>yF|4d$#`8@dzIE3f)Qai5 zUQwwdb#xd`;h4pb`7dDVu(3~hYg}5%AZ%4`s6hQoA?(WEP0g!9Qc!mX^1O-hc{_QB zwBimzeJ9N0aGkt7rBSDQF^npmX9tJHuwk6b1Vf~BaDGJa>8fi+<_4)!df&NP(QWOa z kx2XWY7RxT`1KTI#mT1%HF zVI$DbHs>e%K~tYM<);&JeQJ2dmR%hjy1z2G$l;8Ynk4ikVPeQ`Ws*#$KXD^HL?}yg zmEXe(-j3BgmMoi__EE|Bp6 >4@9y!?~IWJXTBQEWir(~5%6E8e%OS$GEP59ndxa%bQ zf|L0CcGg @BIg!d(VB&y=Q&S=cEJNFJ`6H zcHEABCD+n SRGY zcM2-nLg|NB$q NBhua66v)4qe7z9t&~CAWzSsy!!O>MC2Luy-wNqQX!)EA>~P zT%{zXJg?42Z_jx#1!gglQ5Fwlqzq|)e|6v*h5ZRS&tPwVS1){jFcD_yV*S4;!?N>T z{Xh^MUA*A;QJUiNsLeJYY*6tRiJ0scjVg{Xkkoa{epOy(G jl1Gae?FfG79qQIeQv5RMOJfTd(UJ%?(J6> zQdM<9v?t&eIT!enLgnCb4QJhh^G6jK_h}h-YwWou4`ij&X_O}XavP~5%0#PV1Opj@ ziWt(@=wL8L)wsX?H7s~D-CN=JXI2y|;rAVDLrPa`9-eib`?p&Vci~uxuA6^SSz^}6 z$iUsMu+_Y_qhIpKH)wQ-g#M_ARL)57=y+V&1^vF=vakQdPCJwqnu@-@Jq%CAdnRO3 zyxal3$YiG072%FZgqaLg5Y?fo7O;ykh1Un}U49+D3l0_**r5+fA}s{tbsz*)V_zB$ z4zr=^&CPmUyP@Jc3Rcd{<+(PM!~W2-GP=8(5styxq`Tu9rOP`LH3%*qS_!#4**y9w zs`k$T6LfI0h&)4O%WVXn-Xk4ZpC8F|&HGW#E}pS4rI;8_x>FoJC7fm36|aOJBL4x% zyf`K5iPzudxdhZJ%~&m!qL_U15I!?q{kxVLhuyW|Z5Nq`qTlv5Nel+r$iaT0C)xqC z0xnO}mxATp2LwGbcI=lm*~3Dv_LeU?rEbpryQJg;Q3q>NMN)@1{v}<}iXAFv;UU)z zBVB3LqER`dI}ph~2KNg_!tQ77Cd4b% ?r!mgS|NO5Y!B)*+&bIw1(?O=XX$+L17mK6|E z4u4O9`YrCu2=(%ceY<^x>g{dNAzeLsRu?VOY_doGCFUHlYzDP?Jw!~9fCxUG#7HQ% z%Y>>Kqy*hAEm>s5$W)`FucAnp4%W*J29eS@RP^cUz-~ o;#P4Uh=Q&avfnTxNc5jh2 zmf+K _BDPNaQDw#tNp4fOl6gu-=c7UzmkumT726d7@ZlXcl^@4)d2 zKiBHd3e?lYqrEZ=m;);SFNG*H^Nw&bHBv2AuE{n)=C6$F=z-OSoNl(=KR(*1tma(Q zH_*bw=4*F3^!ow73n2FHB49A3=4MkAZZA1B4PQI!P%^*$gT|nRWv}@P*p!6#b|<;Q zsRE-OppRWFdXa{GOt262q)0$1; ?39OO%>zC|wSg+~}Uku *9(6UBMcGtWGr6KtGWFdqa~Yn z KIAhFSR5C0RCAe9-UkH7zq=0%})=Z zv<4fW>WNrk#K=|?j9y<@>|G`?PDK>K8Mag78&w+aWFhLK7Z^2);xIM1spV6p_Nz6k z@FkzPhP$gWBk+rBhPbYB2X~QOF>v3fX N; zlgH}(=TQY>?|2L8kSt!cV#+s)Y~v>s9TlH2Ul8hiMCrmr^;i%+C!fKHyxE^A9qY94 zzBZphMJa&)F5a$!@2OgVkFy>*iL(CkQ7H10tkgzq*hSF(VPiDATZoZ3S6UqW8lfsq zG{) CjrK-#g9hayH?77~Yrl?f%)PUPJMUE (<70#nZy2J_D5(W`d>O%)VI}7tJY5V_B(ad(eNx_a~=t0nv9)^>(lT zlT(MXoOwY9fxlC6cncIF{BU}(bogU1{83ft$7>iW)jMgp&hrXLA({ZLiT4^et2Oaw zd&qL7-%jU(X)XmST#P}wF0ZV}Cvp|QwyY%+qM>YMhHu!atJ@xS0nVR&8-;!It*`;n z;8y)mkN;Ic>ku0{Hs#rtNho2vx@!9_Df4D0$A%ikG-_M?KKoL)$NN-NI?-x^b-nst zK54L6y0%o{#rxR`?-;57xs>*u6rtBFGpNYIosqL~^iV>2%Qs6m^a6?Zi^`A2Glc(% z9H-QfIU~*egAHIdA$Q>HlSB+99f I=x9gKWsux#3f-#gsTfLR=+>sWI z4O6WH^$ECX3hx07b-ctW)Mq|oI}j$BBZawSv9 5BL+u#Vb9#CGccTmDKVqJdp70h;TUE{#ysPC~~YUCXb&L28< zclY#1NtTxSgV5aLQiX%F=r$bL&eay)ska*H_-0A$#r2Uk4Jq?sp7A#K{Q`;@qVOpA ze+7<}yM2@s5wM0>vNVrb2P%tT)g;}O&P!Qz=!21gX(Z-TD8s87H{M-DG_QBb`J72T zL9kwCd(S(*FALR(AR_xj_fnBGI1lSqr#PSTLAoZ}eg8=!_}}Q_-$;dt)kQVam`0{} zJna0-9HUbdy|_Ox_^p>h?bSl5zS=zw{&)*j0sSfA4J %E)>qs7T#l;Df%BX+D4+)7i7+1tAne3;0B8{ zk@KvoEyN {>IoZfVuaySW9aoku*MClc(~Pdm zx#I(=6^iiVBKFVNE5+;M^tHaOyLk=67eXVttT4OeB6{c6@0Q0!N=JJxB9%M6By`(D zV;B94aEzcOo2_;I+rx#U)E1dqC&2}|WYpk~_GDR_hempJ`r+GtJN7mf^&wt^hJssc zyUaKsa&TS`+-W;VdDO$wF4*;$!jrZs!{t(V_rQiJ;pTL7g-q#oAtm;IS=hU?kh8HS zTo(P#s4v6nMgyeM({4kawJ p8af)J+XX^1?Y#|2-W8rqoE4Bvfwhh(D0&}P5A-bS z`4%iO6s~dqa1uG~y30`N3hDb_nIR0Cx1OGHx6+ryp`4vpGHWH)LbOv7^LWqCj!8 z{mh0OXG_VB&$vU(k0)fzn}m(Oi_|WtMj9L%s8%jBIK<#hhN$#zuy%BpFVm8{UA~q= zU$lqh_`WP}s>a6c+zUTzl4%2Qc)J>$J8a(`WIc+ E#}HYc}mm8Y8d zyjz^U2zQ-^1Xb|S>1v*)amO&9Yu_;m?}>l7(8nsU oPp<)?`_e5-uPQS4L38JKdJ3dW+D3=> zzmfv`lm{h|Be*cms1FxcjNrC_OQ*OuH(#y*@4ZXbCUHKDjTWsCxyrcx&|6ZdWn(5s zXy6K}?=mm!{$i7r;dRfdCA(X!I1B+{ZSpjJf+7O4zIE|LqMz8s92$y$>dl{vdi%ba zT8FTeFfahCi}lN~Pa=WbLFt$`i7x!mJPoGL>cd$VPiyVb!T!|VR!020Q(+Fio}QxZ zSfpH^Qusx1U%EfiQnFcw-0_uF%N-i$*%M2KE?*JclG-?nFHTK}&VY9mf!?901r?~y zMF%!7sGhH!X1(R<=%S<@%k=u3;Mx95xun9;1O+?2kg4~2;C2o@Jhi{Mh8a|M|7E^` z$Q;G7&PGL!fleg>C{{IBxOQ4z&&w*qwtF44!rtSRU$vP|`Civqr#tWsT#iOK7Pu0C z9z4FRTIsks%9RhqLc`se{$z)o&%ygv?r)oL8hmGex!YuAX7Fq)`fO2a38<`qP;=e= zA~0H8lIV01 >}uCcmya)xy-WF?d*Q?NSnqlrjXZmLZbO(5AS0& z;{IdTQ9HNIeyhr7WItCo8~l5LT%i8`(h7R|i_@jowXZW|d5GVIi6$#KQ{ADyh*Y6P z+%`N$a^Y$IMf|p8IqnqpDzQ0TdAE+0wUhg`R(CCbSNNatamEgk{;(*cYVv;`QW)I# zhb;rPtpU8sBEuu4E0?Rpld9nT1FwK7DEATPvk@e~!jYI)G2*?od~y%aucER9(=&ok z>L@4(AVP!jgRxa-{Q;TJU&KChBSv@xrQQS-_R{?4*b4ZBNq)6se;si$_^|in7e#&B z19zarBZo~_v_SW=W?~bV%a4vixlDqGa>sB&5yEP>5WJ!#E)eAD(Ek)CgUFAIq%EP> zKbpWWlM8Ivk(2)2NUp?tfYT>q2=idQ_eB5~<4|mtwEN!6b;1J%zNz5yg033X3y!uE zHm~-RMo<*wCB^%m7Lh E6GNeg}Vxi9D&-o=h5eVu@lT@nAQWib!288gss zw!ly=AM=CLyEJ*XORV(Ubyf~y0!Y$eGV@G!dg;)oa9EOc2MYJyR}vPf&5U@@;C_9H zJ!1@#U)Mlftb}fK1x9$OXT7uI Uv3>_jXp|!P3F?L{1#!lwV5sY;Slq`B}C39Q+!t2`0OsyzUgaH z9z47FAr&h{?nc8aLiq3RO_C*Hz@>4ZjE7VZe@1o_R~Q``fOID1rtbw#4TQWcj1Lxm zD=x{L0Q$0C{1%UDHm6IGy9uM1{c+-IUB78OE#TUcd>|=nr fUl}EZ%{8eTBk> zGg1D&?<;ML(=&U_goB21!t{xEqxiMw)Ml99At;3>EKc4TkWMJO?~%b*@pg^q(O-LB z@mfW9-rl)cEG9{fo4GF+2T;G5KRl8G{P4{tyZaZ^<7ePXIcbsdFJulvUwvRo*24Cz z>@@*o*U(`A$K04n-Ts(Z(qGRJS^WfS4Xj;IvnkHpl^govFqpKYAn3jlWLEl^Ac6G| z>eax^Sr@FsPi;0UgRA3AtMGSgS!I={{F+tpBBpE*wEcXwsa4nnCb|{99dU~sO#1vHkH94j7~ITYdYv0KIzjdNZL7`$Ww5HIX0jRK?o6&TP7GWJ znHQI13MbO^J=afdoIm?zO40W%&l7};Qqvdafdpl|RSi5mpOr34N;f9thqHARk&e|s z$40R7@%xrn<^s> A=}k}mLWsjJBgN~{kg4EmaA%v z-l5|5_G;SC=7u`@^C63%!wL+2ghB$5?ZuCtHX*Bcxbw11`{f@WtBi4b72Y_2z$C1h zzt!j9x_nbDjJGv_P^W}NCe{G;+s=S>H}`kFEi10g^_e49;T6v`JTb9M3EpGr>tm7C z<;Y*Ue+ET)UHm;;rU6J&oP57yX$c-cc|4*nN RH!C$#y zblbR=^%v_LOD%delD}l(CUv;rc$s)#bR*w> nS7Yu!R4DiiS9vO}&iV(jJ zs~45^xk}%i+})IBsDB!;3#>`!zx&we438&`CSU(|om-}B+WCgX#gkQMQdL#BXb``N zWWFT%ZIrBFBM#%bXsr pDOcp zLX+F?<|(|0e?qCi?Q^E;mDfDo4)9Yv-q&Mw DjQXZOJxs9D^&RrlEI zWAbZl )Qe64jAwoJMVh`M}uDa$YIC^VH z3B7o(UMZAct*W$3kH_o$PO|FEmJ-*Z?kFbS4ifKGfq5s4-7HYDW%*{ulDiK6;kFwv zrg{h%W4{J`3%0k)JRFvO!bru3A>=^^9iIDKjrum}ruFXGL_C_88yglcR?ILA|JVLw zqSk%}FG%m=n;SIUqGkk$MPc$QFStgSa6y$uc)A?o3U|uH3Ih|lOQk6_st$qVH(mj6 zl%6q0jPG%Le1K=mGqVVsxc@@2Jo8hlz*ok;i-H#%7xrGGSuaNdq4h{~7GZuD-%$ zE{^+vIiHD=*wHF)f#5fi^l3ed@97ZZGh#rtcbIilFUH3y>{SPU2fQEI`t)J8=5}nP zvFlr~lS*!I8Qnr)C>=p}?o*BA9wB^uSk|t-!ypp(MTYF5hNvt<8e~Cz|KhZYxTOMP z5z6+@DyYT2PnBSVxuBwe$xr?(_zwGZM@a!1MeF;!$<1CN=)JrA+xXxf;-%0R8qG$} z^nbcv12DRxUS!Gnt-e{j23@k^INH;nRS)d%hFPL+pDWM->?-nh+6ZPBKL!gIN(5pp z9qcAU{SQckf1vIijx1*v2a`m|5*v>06WVOKO>{n(5hwK90NSncmR)BmxyiSVDMcQj z7LN5URg;OFlYbFZ9_W{vRW-)tRx`8@4a}Jn^b#yL7z (k F{H7 YMhMwqxkVlic z-*B`$edM 1BhkJVKP&NT+` zUOU4V1ZRR>a&fGLD?#6u#3g-9UdQnK_KkJTBSuF&^c*ItRlv1~OXFIQr(y~#RL08k zD$dA18v?b+T>j-vK=2lFOP^3EV h`1{zk6BE)uW{zd)_GVQk6&MHTIMMui(Ck!(egcRnLHrhof)Gi*8BR?C zzP-WQ*cRNt@rt#c;Gz|f^uOuz^P9BALpLcxqNRP2;>bMC$^Ud`FDu=$2qx8U^Ge|| zZ(ECWi!P#U6gBVVcq|i%JLAZb9;d3{tp0Ui7Wrg2 aA7R3`S<}5@I;O{le#63yMzJ;>&3T2)s5!!z3CV3cPtY2?RaGH zr6|2pF o 8hma z e6YS6a% zGYsuVU^_* 9YLzPWB$U-gh2Vu#ZQtRD^i0H&K()1pow6N zLIh-ROTXQnb(SaX5re73YrDZpF^VGt-I jO z4CM0xtj%s2am;C)+3xa3kP!P7&QA)sLjis|o>aQd JFLGRE8|#(=pDbv@*gf|M`p_H}UD=pf_xPfxzhF zUxtSx>SVll+dsp?|B5lCwp;nyqQ0vs_QMTc{naaAon=z@d>s59kcH!X!o1mRVmh^J zP4ELuigCoSlXnUM%Gm8rM;`plNrpq0BJvc26Tyx%b~%i)3nNv00c^Uiq;YAQS*Bwv z05;UR%+F4lTY;xsg79 >dvM)bueTZWKDC2~RR$jd?k~r{?X7e}mpwk1U{Bg0St=Lrww(VW^&LJ`3 z>t2P*p69lbnHXohy8rdx+L#t};2A*O?$t_WrSM*fvKPtHxwp<^Wh-ASPb4z@oh 0~hgdGrzM+-I %oSs;xaHT(La7_Dg*m*5Lb{0K4-xq7OViA`3o8Aih zb6gCsvc?ABLJV*o4zuSQ-gHdd+HyOCYtky#1>TROE!WBp+Pc>=&F&l6iQs8ejn%7+ zQOlm}6k4H1t!9e<+pH0MHd>U+HjLvYX@CAXh$8>pQ&MF;=u@SVz1+k@=7=ToP}J%o zUIf$YhkaQlEJ>y3EA5SjFCL%gkW6nqK>i`EivgC7hELkx5gl7hg#5h{&q7oA-;%rA z?_0Pg;(xRo4@+z9XuOO}p)Ccp&Z({3#~WY&ndFNI4m2Al!tFcwT&dH}SiC&F1#}iz z1UCxIoDccJGZGUZceu;W!eqB&3>Lv7`deNag+FZ7#1w*F13>|fQho!%$Y%lXSid*n z7$YDyABO>IkZ~`(Kt!_l#}!-rP7>b_9*UU}Wk$x0z{)q -zR0j2Ak|#|m*53%zyKn?M2;(3>Yy%9b!1COl7mRP zRx{$O07Hz#%0$|(G*!lQRF*(-9mN&9K$cs=aTDIFK8O
qVqV*HY*4E#|j# z(pEn-c0vQ)egB-p$Ago-v>skP;s}>-v8ZPyw|Au7ZtJ2(L{tIteJq%LYiDY=7P-`Q zmR@Vo{h2129M`BG?Ca*kB&5%N?B~4fG%v3vsFRp;GDx>qPlK|dw?MM=0<7LZSXym> z(B=*Orq6K23fPghTcmZQ+g=X(cam??m5og&y(l?PRAm;o$`jVsEjZg8R111oe?O3g z6zug6yWcT-RZ}_bQ*05H+sVGdSg|Hj8&Ai@70s?nK#Km#>`*GEw(&czLjKP&-)$?@ z|M;1Y%S0gNQEI!jXZhB5mjmmqiaa} 4~WWw%z9lgJr_3UkV z0s#@w>%SlfV2mLDpbEed&9{G0zk)q~8 TPW5UZ&u0&v%cvOwFb%lmnA7Nt3mf zYaNS{s!2-zq75V*L6pty_t<64DMt$&agI+;9$p7ugoCUH`sn)58ikUdxWD_TSnOEr zH(Nksa2p9~a{liA{|nlLZ#Bh8V8>Uw G74bSWrx2A*nWRx!zeu EINUSsXHoX`mC3SO=OO(uvl?+u)7)rvL4X`sQau39RWcivcGWNrjT&C69CF^ z{zOi6U-wpeHF& n%sp(Zlw#`i&T@MAp)jxl{jUa{(B7~el0$6NJ3=ke8nEceMIJg zr0AN?mkFk+8S1lJp$g>nk!7xYCXjvdF^RPWF4;hmpRK5Kf94ozdA p)!>E90%5 zi5@pnibV teb4M_ zeo4ET*2c~g%fX4ZCim%FW!5piQ>yBS=h%?e((CwOQ~&EdKFsf<(YMOM{jbX2=8?JL z`;bTcX9nmd8XF`k7pV-?Z{K(1bMa~+ulBLoA44icr$&NYs(8z~h<0UayKEqv;_^F9 z0YgM%c_(u_fJRJBA(cwvpx7XM-&3T1Hn-d7Aaqj sAahx%uqnS@7 zi5cmOSh6MsaPLU~wc>3+&-emGySqxnF6>b}`%G~p1dw@lxLY8>q-h`QG`|FqWRLD` z!uz)Qug%DHEH-k91y|u?Nch=RvjO`n?}s}#+{lT%r3Mlg)%NynegIAe4Og5DqBt3b z_gy$Nkn^W#lZC?hsE!Oyw_p);q^~@)n^>($XYSlD)b}S 5Hb 3*dbN)SSB zi+R xJQ-Gkg)6TBK#AkCz)r0vQB@X}_L;_!z6sHJg|c$-T}35iWkiSyCoR zBDEt?f3B22n1Lx^)di8DkO@k_B-7bP@phEP*J?bP^B0tEn>UNG%v&!B81+>BM*>IA z?8qIG*A(FeW0ua1bd(M3eT_yoj @H;X*WNda!mZR&Re5l&S{6K`xdP>C5p;jg7>Q4veN El? z*HI|@PVm{}j;C#DcH^7O!tYi8N+!gE{EnC1h!2jmIh)cP?RjD<+l}nXUz=~7O%>hK zQDvcDlKdN9lX0^=e3XG6nSYPT2X~3*E?1j`b!-!UskXx@iSlcUz~fa_=Nv`ES}{Z> z4Z sB~~kA`=UEQM*b58UL(-)ERNRt6YVzFS9Wsm7fM;Iec6}G#Ak$G z;78kU)21_U7bw3y%zO9?|NqQ^qeKlJ=<*qDe{~e>p)iz&`D^?@c+A~R=1WbpsGkW% z7vi-~?MEN?GWA(xMf{FmpvM@uaD4NiDFt|mSpn-eodC;&EjF)~_M$0)b)s=aR+207 zqwZQ(V4OJ%%}C=tM0w4BDTU&$(uN5D;D)AVo?XS(Zbje ~Ht{;2vL1d6aEmLYE3Pr9&O n^p}J7$RwnqMn$PAj!^EM7-L*2A~h%EyP!TtESasFfrI-4b6& zB#c(}&wSPAn(Y2AA`Z*{VlMd$0khV* SQ%MqO5VqG i(pxO@k3 z ZDoD|AVwPKMI#|0UbA`)pi)y zDdA-wIV6H*A84iP+A~o)wlxMUv)6KmmDOuyJ@ADj%ygF+6>^7H>W5!wZCdA7<*J2$ zM($4OvtIj-#3LTGJXYXs(^L9-2HeD-sgheFbIEni{l)^iuT^n9qy)2FcWLou4ZC?- z(|&}7= 3Ag%r*gDJ+17QW5p+jy$7kT z6!^W`h}?#js&NO#r49~{^`+BIuuc&hZ7Hr|gWvVPq~8=4O5`Zg8H7`mJ)t)myih zzs$i_yucS 5^b#{tG?lSh%6AAXBRWaj?}SeVh8eyK2!9XSSDXJInn z@QUKmSD@}tZ8wP8+8|R_NJg{t&J9@qH{8wlY}iA}6@Sw2-@$d7XXJ!bG6>h#qP6|T zB?9(XPNOV^NNSF4f9!io(%n^i;Le%f*f2zXidd=Noh|r9yi1k#zFb 34FaV{K9|ORh0%yQSQ<6x3x0MmW%^e6+5Y zw3m4^7PIvsRf81}BgEsQTtEL-Ljgs~_Yi*f9=Fm$6Au&OZR1q(t2&3C>PuwMfNAqp zaUwk>?P|vXf%Ru|A>rgVN~McEV^LWTZxYfNAZ~dzkc>_@xC^iL%jfn!A1oJNxFbwE zn5D|)Uc2yDgC%cI2?ia-7C5|$km-3N<&HI|Z`_d?o?adVWY456C=rgG`$msAW{ ~Cx6k2K!CpKDyM7TC&f|M=9Vh`) zIoEcGu8Nljx*C^`=6)7Y9k>o=MTDTJ40ee L}xWpLZdO0Dfw0+4R5ft!kZ~DV=|C4#Tp{oPzcB*N3Y%ob~Lwrsfm{d5> zZYgN_rJRyNMmc{l+d_Kv8~34Jrza7rlZ?Y}hKTmVaWR&vzPG{yAHc&8u5%6fT2 L%V_&!Uc0wgtqfq!m z0PWfD>Vy5 ANojUM1#^NzR_0^5cc2{)+X@QNMCcgcKBaU`Dd!3HR8 Id<-SYHgVc*ZlcQHDJ)keWsJ466L}EvU#}U(nNn1-R&4;emN$?peQw<2r2~y(m;u<0v*hlHg>Te>rQpy+@O< zGEECOmJjEGg2-JPTipPdJ $2uKM95HifT z(agc>R!?VM)*LX;?C$4xH@JB#d1G%BN5FhL%+YqaF&^a!wyw9=!#CMI1`uS{^g8aE zFK+gabvY8&sA$#`>gm%1OO2mUrGm4P&`R)bzQR`Q@89g3o!$BW+2lupunV5BwThqS zv$@u;ipnx-pp>KXaADcDhb X|G2_Qg*83@$k~J*o zGa>sYKl+^92Fo*fBN@U&U+MZ3=RB(+U qeKEQ^aY{@NEB1CcY6OL+LfGb7nxJ@-kKEu7Qt?e~ zy8lJ~F!ce}#w%WH*XR)wrPC)-x*$lMn_uNL%>4UO6Y{qf&m%sDOHwBp{1M93ILd)D z14eXti;Yx^v65z5fGD187IoK#W;b7HZ;<$_V*v^x;Ap=o=_in^z!AHTS8j}|S{H@> zJ3hL>C314HX~CP~hanfCg46XBH>Mx_bpLDDxiusLPMI9|?ePU2aZVQIPG6pvq;z2v z&w-r5)XM2Uirq%scf&<%I%O&>D4YfV9k-yYlEd1zv~0e&mYwG=1v8JEw|!5TC{yk4 z=A$M EB~Z5Q(c`Xs=hKfFd-M6GILrdbRvn@KCb$gn8j-9Bcp3kLcd+ z64<~&)^>TpF?zE#i?SgGo=-6UP0cZ?^AY~ lu)=;NXaGV zlpCByq2TDLvk*@}ll@kckl}3Gut=>;D;3b^vsPzWK}HcEKQz=8S;;4%+$MWmxkh%7 z)N+S2E}s#U_fL@hK^^NJjiCp2cxgkWXtkO>LBJ% dc?_Rm83?=NUE(zo99iTRo=~6-qW@5 zzQQ{xgwgVizgqD6btF{tM>ZX5O{qptvnm&FCs+_WEbvaMbJw_|zMc1n0-!RPF{r(6 z?!SMZD*MiD;uDq46Z#)6UJN_@VyD~k1I34U+d%L`-Nsy=E43zhqj?0#a=$$Pyj&eW z<*T0}^Jp9Q@DSTX&s}|5hLB}c_+hc&7);FLWE50MpTU@ZmOMW0#WoDYQHW^|H$Hq$ zO6{z1P%x$?Nvo^|S_26|ht8H1A2O2`!Wwm z@NCO#Y*l5cdX;m2=-i8gyLw+G@3x=4e}TtAry1&GbUiVNobSK{TJ<7|xP;LE#Ym+Q zgf(>d;ThuS*v$hzU(zRZmkh@*Gt=<<2S1jede>B=t1sQ`bcNj%-=wSvZ7bPW*02%J zUWnC?Z_9n~IdaTDq_gJe;~RRP6qe|@F(810?0OP&ojfNyK53bz5RuA+M5qb32sS-S zQYKR%cv =d zrn-GsARv&oc`fJ5*xj*|-{qoJx?j_Vx|_|S+?K=Lm2P6WQH2<(6SFsUDq>^!5*vF~ zlGRC_IxhGUSHoDm@h~Feb=^9w3~tpSP8=v@ZBg?48}`wn9QdO&ZE*i*weRoyBO}MA z`fC>@4G}9kQ)k^J@H_&5#=2H*a_L1i@gGfa-rJYJ*~CRn)iIHs1#G(*Yr`XEYnS^J z`G%*8k&( CQV2Z#DIFse$KD|FIEC2|7&6Yux`m5N`<`uwU-{ z2V@?92~eV&s7suPBY=2ur3yAmps{DaEy)Pd{vj9vti|}DTuJ^{-nd|L*O~}uSaTr@ z=ajk2c&WK4L=XCnLhb2KdEb0xMbNzhAV>7M(NWV}Fp{8GGo*pBlB7%3PVUxG^RTyE zwc*Gu%9qr!IHI@YY<8Sh=E^AKd+m4kI5lUTQ~mU;xBr7&QhA8j(8>l*snOk@ZH;o0 z!x?oaap{7AyF^?w6%I8<4Sgu0dVu|NlK6vABD^d$yod5V+&T(EDCVTt7q)eGEJF-D zAgp}ehbk@LJ0T(G^m|;0GuoRv%IQTW!~hqqX+C 7TS^z2zAds;c+)PbZbd;?PR2N#R};TmI@_vah-`VM^(*!*+cC@h zy*&YBQd48Iy#}~EzbD_^Vty)#&2Ps5YLze;3fBay{O>^rqWtH$UVswf5z*#M0mzml z(t+Aa;8@n@(?SH_;PKgF`zKlTa=6`_1zN_^?+wOkj`Cj;j0%c)h04{m87^F&n56eV zlCK$LtKY$KwTFJVp%Y);*jZfhkX&~PPtzvdt@t_6T<@k_9MBHyg UEC3j@j z&aV;2kOl_=z4EAPrGq6g%(5jFM|Re<*TZ~?{SwO?xCzf5K}oVw_*BvwrK1Zy>jfZB zvbS!jQ|6Y02{s;QNiJEqIZFdDg_DCUZs0zVL?nu9u)uNwe{QBWq5aWDa@`Nl71ibA z54`*Aad_4|WIdDK5T(4H4$<4`)*{vL!F9Gpdw7?e!jo0`fU*E1{m1iuLarkSC8Exu zzNb9N46J J?4i`QZ)~(yYN%Bop4!8vOU6z*1hh5sm`- zsK&61!Xxa~ZXaIORC#dDqpbAThY`>kQV-*eryA_aSmY!7iPSqYIo18yYT>;je@y0x z?_|4^<)7MQ3{|Xsx`N+g_uNm7#l-9nZZf>3%sEvPv(;$Nb9vT`ZE-mfwi%-;xkvwe zx6gy%J0)qjAv)i(!14dMVffpPMCSjUaAEP^aeKZcN9?jh)U)@yu*K*Ls$V_tXndo3 zzx^}L+}-crk+Nl-ork!UIMSK>6 6F*d4sSYP3>=bm!}k9f{FuJ4UJyOX pKN zRa*M$f+wF1c)XlTGMm_u>!sb$J7-J#f|Q^cI5g#^Q9v8%4$4ng%(q*5MPKKCSafe? zUV|Hqws_jjce`SpY}oa$w4=wW$nqMiZ3pO_t||a27inhfVJD63I1e^f>tUwJ4Kd-w z;<)dBrd0#Jp8>83Nv`Ian=%W!v3D!pPtAhoH`M{%++@zl1uM=@%%S)Biu?BO=z8}i zg+<{AhYsAiX%I_u%g+G+V4_2rR^Rzq9NzpY00B04eFQ_5)FZcc0zB%`5>`1jV@e}l zy52L0x9yAKkw9DM01ItP9CR{d^e=#IGn%&>m0A?Ba@*Cc%sm$E {tB}y{ohKXM zl8m`*hP*SJg=lRQtjf#m?D9PPL$&A^`uRTl?w14KIQ&4wd+wTA`hM=Tc53r zXS)q*xcnBk!M0bh8f|koz&p9$hmY_%bmtIf)nH)SFzdY!E|LCIC0X+Qay=-_qrM7Z zp5gC=B#88xC0vs<=d QlzpA~TO@4&Ps9o-gmSQNAcW?2 N)ATLyH4Ijs li;P5TFG53ytkJOM-Q6(I4vnTC zN#XYgREfZwr`QYu?f{(Lsynf3 !C0oK}~1lE`^1zFknaRWKfC z7+=>%nPK-XWjUwn5(wDSkC;qv1;JK!BFL_H$iU=>Cp}6?wy)vxq`QBM8%vuZBM61z z Y07y zOYy-ld>h={{#tfwoqdUq%X5~3EBX3eCC17*MqdLK_>&A=cw#R5Hstk=5?IxTy>_H* z&_>ZL!=HWb@?7njcqRb=*X20*vmPQn@ApyELgXto43|h%pjc3I;EAn&IJBJr@ORSB zVtPvqxaz6N%+VajX7 3^ayz;N 7+z`^8ZyY1EE3^CHdMYsPsj9tX};pE3VE)*=JD zJ=OuD0JxH4k2T| V(lm?(sE{SVEjM)p_!|J^ASdbiS}1ggtfYhoXJ zXzQQ&16M^z@i-$;!y>Z=b WG?8QDV5x+c & IK88c8V2z^y&AKCkP| ztO}EUj1!J&>^< 2g z-HnRa{hi 3s zTR?50d@z{AarE_a-)WKtFyYB5>Tt?8xNH- lTDYE4qfo;fORhhBM?e`HTCFtk;$F(vM-N?`Ma~G_0*3#_c%K!)jf| z|H3`dj7+lK!^+1~q<&TBWFN=ocNF!riOvdtb>`aPIYLds|2yVQ4jaaiG&Ba7_i2ex z7?h2O;tNFoJnmLdp4g5m#}aFmc6Fh#Ml3$fi6L|zMurtB$@7do6jl~ts@ON1={IrJ zL;QPj?jsC)CvO(vCqTTVuYDywT+u6k{C8L9R!So>n$%ZG<=J%;Lr&h^Rsk{dtrD9; zf)mGj=26L260>#GP>u*ulC=g~iHh%yax74LH%50r0HyEZimi%Il3CtuK*sl)|5C0D zlKxzp>e4vgZwI8~NVE)&S8kC9vI<-^;-Xh}`(5+>3P2_L#!)6seGToM?tRQXM;+Mc znz&*cykCeaIryvtTuuy(ycS(!Ed3oJ*zVqK-`Nfs#Iy+_zXaJ{xn;Mu4DcF7@l<4D z*KenC#~Ip#r%JN9Spgp3#;ay$dw31LO-kxfJO1H1TIdWG=4ARA12esNBg%qNZt8ay zBZ@ ^jT#gLubBS0I{yN|sbwqM7J*9MVm!l#Vq|jNt$4 z3&S*$bD3f8I~Wc}C )b8H`y` z7B^O{=B;3B*ZTbX;;o+g6j=vqn5tR!j)#^85dBv=f|&^7mD<2OwBMOeMtMrlmU)ZU z-m=K#n4WY=^=|d^tE *Z&(HcWqqr^ZBA4 z^K!yBw{<9J)8M#Z`AIQ*zfq7TNw_Xzx{UJaos){u3n02LbMR}*Lxr?p2@<6MM?4uO zv!qiNh;~q?R}}GzbgB8G{G)foyN`8xRRClkH)0B#Y@ k|mA~=-` Q5WoBPahbRS?e__%cODCQ?XePrD=fo~DN ze1=KZhKrWueQgWlvqV~6-JllNjHQc}XB9y9kvsTc@L&uWQ)40=RWDmTKBOCA#8>~& zr=HEhf-X<|cqn?7M^*U09!5nW#(U~eKu3OOFjz+q^zePkiPbRv2MM&@-Cco)>9rMb zKyB#eJO_9Qy?u~C;V)jY9?h&Ya>*;3b_b!rG?aN(BH@`{+bP%Y@IKY#z{%N7dNno$ zQ}DV8YOe(J#j$!#Mp3-^HuT+Gve8uo;OE&|tWcAt85_w^k=?)j6S&>xdgnbSQP31< z@rXO*D2R~5j-YY;!|@Y81nA-444-wrYFU_T-|iRe#Nc_F;>nE*eRBGt37vxz$L3bW z{n97IO4l@_^5%nh4d>m@NUdtdk3@~+r~0Ky56nV@lG-+8-<&RW^F+C_B6ZwgIX-@X znYB&Ecyf|1tx=-jF#@m=R^ `NZw# 6 zbl4FTV&o(mM&qVX+)-d~3rQHr4oO3{EL_6_?sDG}XDAoc*|yO{_W|Ix$$Z5{Jtpp$ zX_HdSwc+p)WXVRff;|3J_Yd82Ml>jzK0gRS2)iX_;&1r09TTzSf6o3rBhceg&ncee zf^ L9SdLA^qN7;qexf{Vmsf-dm;Ij`kVHr2mNZFitsc1j273AkY z{BHc`*&+=ql&q@Js=j{j*$EYP8$t>30!nzz4;y!h9a8T 79uS#v4M`;mI_>8sMpsKfbT-^yz7JddgDahFCZ zuaZo3rFe;bL%=Ty`G4C%v9H3DxluxAzngqV%s2riun+jsfIk~Azi0|yBPPg0S(>(2 z#ui#pFgQTO>Zfy^jZDj1t{l6aQ=X0hjP<8?K$QHkd4rt*?GZJXiI{NFq C( z80=3*e4jHRUFfP8#+3&@PZmPEIN~tY*!$h+$MZ0Lw9caZtJ%$Ho86iZg>A+zJ78=Y z?wtRH`{;18hjPc`q_>cc|3w?4ZxuZ^O1M>g*xjvWPY*aPY5_sNrvGSQ@u?b0^dJpb zs0sAjPj5xhP?Llki!@%pZrjFiyG+>xN^6EODS@jQoW~uXXO2hUbc8QLyB~+8gc9~$ zmA8UB2lhPC-fPa$M?_vN!aLn2GJ4$l@Rp!2j-dS5l?};RWBDn8*kw%rq4~q_p~7G> zdoI vCgmyknOGTaOq5ZJOJ+wfcSUD( zwvjUJQEkaM5m$Dq0XbS(r H z<`zl;xN%Ky{e2h|Z< dkB*scIxjN^8bQWTtkt&^9NfC(CL9ta-T| zLNW1<@noonWr5ZDe}R;*g&Xj%zF27WyXqDd(l~CEk}MS##{^)1qU7jDIrZ8VC-p%} z#ZJpHgUECKrA10L$aN!jjG@L)U17$l3F{I4eGnc_zqh!Fg&PT+8Dd!i$CZn#^~lrZ z6Fb!CCZ3PpO?k|0TgZKbsU<-cVf{JA>gB$14U<@}iu*h`$*eU7s~LWdEp?^eel(kQ zaV;`%vJ=sNUsW#-pyK!dLd1={asReagNC)ol11SEYyrjpsVQ`VGrt>1Xvl8}NF+)o zPG?g#^! }f1BYSt2b4paG3#_>1NPC98cji9#o4o zBS+xREJB@fE6Ki&0B9q))S_I6YGy=k1GHST(y&7M_b3IWEA4fB%8?vL>}50%8AkvT zf4hU24(Zl<7KI^FK4MB^Q%IkE?Vd79-BfEWjVo;5ihC;iJLV#Ts|D+HXN$p+d931@ zD4y^x?fSxD)6Il)3uIZzDVXKU@T$^hxA8(00df?w#0Q)JgN!Ywu<^lQS?!ClpOV>; zjbUsQ18c>npQHd8KN|Ua%X7lqM!3k?D=kj=&jh|u7_J|ArvFf*=-qzdGDO1Rs<-Ke z50l82x#B1?#D%mgEGgC-Z5CfTHe~#3N_q7~Pv5)^JOT)B@ut0ux^cAxcx8!AH%97I zK$~p&h G3yw;l8yl6N+B+NXd5` XGAza!TJF!k*C*x@Hkej^U!t3oZw5XKyem7@f&&XU7chgGXCr! z6BN~%;dkaKLs3vM#?R9AQNneH{jlCi OZIFIuIZ5#0GyneEZM z50`xJ=fz#ME8ai7M@L<=W(&REh+s=xto- 8N1UXo5GT&72%awJS&>jB$!0noL?%u~>33dfQs8IMr;-0t1hXRB^)+cRp z{j&xJA|!PieeQV#8l`)hkk`AnA^DTyKgUuWX4Ko`1;+jS6-uN*?uQccV@hk?q-5Y0 z#Q`6Vo9e9~!lzWc(dYhoQy=-w@@&YJ;#j_#vi#(y%nu5;W_CT)xX((mUuas~c`AO* zz!jli`?5E=nN0A+TH8&g`ZL{_NeE4__&rlM)7qa|S^ZP`a#->}Xcs;Cd7>!Z-^``e zrnPq6AVB8Ory9fit>FQ?mW1lx7QZ)Hkz}3A2VaN%hjrcLpHU!dCFYalKG_kKYQ7rh zGY$AMZN7Yv+T(QgNeQ6h{;bqfDqL_(H}2j7hCX~MjKcr?uqg25Tu`kPG*$?)8D)Gp zkbaFig~px#@%-w 8y=S|-^* z_<&&FXa5NvTVV zZHd=z0GaNqtP{f`M!_+R7Hr(5sS=-P+YPFq5jj?#PYI{65Z@oxbzy-L B4; zaO1MM-st?PvhO