diff --git a/config/config.exs b/config/config.exs
index f161645..3f55d12 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -24,6 +24,12 @@ config :yearbook,
ecto_repos: [Yearbook.Repo],
generators: [timestamp_type: :utc_datetime, binary_id: true]
+# Waffle configuration
+config :waffle,
+ storage: Waffle.Storage.Local,
+ storage_dir_prefix: "priv",
+ asset_host: {:system, "ASSET_HOST"}
+
# Configures the endpoint
config :yearbook, YearbookWeb.Endpoint,
url: [host: "localhost"],
diff --git a/lib/yearbook/schema.ex b/lib/yearbook/schema.ex
new file mode 100644
index 0000000..14ebd4d
--- /dev/null
+++ b/lib/yearbook/schema.ex
@@ -0,0 +1,27 @@
+defmodule Yearbook.Schema do
+ @moduledoc """
+ Base schema module.
+ """
+ defmacro __using__(_) do
+ quote do
+ use Ecto.Schema
+ use Waffle.Ecto.Schema
+
+ import Ecto.Changeset
+
+ alias Yearbook.Uploaders
+
+ @primary_key {:id, :binary_id, autogenerate: true}
+ @foreign_key_type :binary_id
+
+ def validate_url(changeset, field) do
+ changeset
+ |> validate_format(
+ field,
+ ~r/^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&\/=]*)$/,
+ message: "must start with http:// or https:// and have a valid domain"
+ )
+ end
+ end
+ end
+end
diff --git a/lib/yearbook/uploader.ex b/lib/yearbook/uploader.ex
new file mode 100644
index 0000000..6af382e
--- /dev/null
+++ b/lib/yearbook/uploader.ex
@@ -0,0 +1,15 @@
+defmodule Yearbook.Uploader do
+ @moduledoc """
+ Base uploader module.
+ """
+ defmacro __using__(_) do
+ quote do
+ use Waffle.Definition
+ use Waffle.Ecto.Definition
+
+ def s3_object_headers(_version, {file, _scope}) do
+ [content_type: MIME.from_path(file.file_name)]
+ end
+ end
+ end
+end
diff --git a/lib/yearbook_web.ex b/lib/yearbook_web.ex
index 1c7e6a5..dad6485 100644
--- a/lib/yearbook_web.ex
+++ b/lib/yearbook_web.ex
@@ -64,6 +64,14 @@ defmodule YearbookWeb do
end
end
+ def component do
+ quote do
+ use Phoenix.Component
+
+ unquote(html_helpers())
+ end
+ end
+
def auth_view do
quote do
use Phoenix.LiveView,
@@ -120,6 +128,8 @@ defmodule YearbookWeb do
# Routes generation with the ~p sigil
unquote(verified_routes())
+
+ alias Yearbook.Uploaders
end
end
diff --git a/lib/yearbook_web/components/photo_upload_card.ex b/lib/yearbook_web/components/photo_upload_card.ex
new file mode 100644
index 0000000..d8765ae
--- /dev/null
+++ b/lib/yearbook_web/components/photo_upload_card.ex
@@ -0,0 +1,114 @@
+defmodule YearbookWeb.Components.PhotoUploadCard do
+ @moduledoc """
+ Photo upload card component.
+ """
+
+ use YearbookWeb, :live_component
+
+ import YearbookWeb.Components.PhotoUploader
+
+ @impl true
+ def render(assigns) do
+ ~H"""
+
+ <%= if @staged_photo_path do %>
+
+

+
Foto carregada com sucesso!
+
+ <% else %>
+
+ <% end %>
+
+ """
+ end
+
+ @impl true
+ def mount(socket) do
+ {:ok,
+ socket
+ |> assign(:staged_photo_path, nil)
+ |> allow_upload(:photo,
+ accept: ~w(.jpg .jpeg .png),
+ max_entries: 1,
+ max_file_size: 5_000_000
+ )}
+ end
+
+ def handle_event("validate", _params, socket) do
+ {:noreply, socket}
+ end
+
+ @impl true
+ def handle_event("save", %{}, socket) do
+ uploaded_files =
+ consume_uploaded_entries(socket, :photo, fn %{path: path}, entry ->
+ filename = "#{Ecto.UUID.generate()}-#{entry.client_name}"
+ upload_dir = Path.join([:code.priv_dir(:yearbook), "uploads"])
+ File.mkdir_p!(upload_dir)
+ dest = Path.join(upload_dir, filename)
+ File.cp!(path, dest)
+ {:ok, "/uploads/#{filename}"}
+ end)
+
+ case uploaded_files do
+ [file_path] ->
+ send(self(), {:photo_staged, file_path})
+
+ {:noreply, assign(socket, :staged_photo_path, file_path)}
+
+ [] ->
+ {:noreply, put_flash(socket, :error, "Ocorreu um erro ao processar a foto!")}
+ end
+ end
+
+ defp error_to_string(:too_large), do: "Foto demasiado grande (máx 5MB)!"
+ defp error_to_string(:not_accepted), do: "Formato inválido!"
+ defp error_to_string(:too_many_files), do: "Apenas uma foto permitida!"
+ defp error_to_string(_), do: "Erro desconhecido!"
+end
diff --git a/lib/yearbook_web/components/photo_uploader.ex b/lib/yearbook_web/components/photo_uploader.ex
new file mode 100644
index 0000000..999e1c6
--- /dev/null
+++ b/lib/yearbook_web/components/photo_uploader.ex
@@ -0,0 +1,82 @@
+defmodule YearbookWeb.Components.PhotoUploader do
+ @moduledoc """
+ Photo uploader component.
+ """
+ use YearbookWeb, :component
+
+ attr :upload, :any, required: true
+ attr :class, :string, default: ""
+ attr :image_class, :string, default: ""
+ attr :image, :string, default: nil
+ attr :icon, :string, default: "hero-photo"
+ attr :preview_disabled, :boolean, default: false
+ attr :rounded, :boolean, default: false
+ attr :capture, :string, default: nil
+ attr :accept, :string, default: nil
+
+ slot :placeholder, required: false, doc: "Slot for the placeholder content."
+
+ def photo_uploader(assigns) do
+ ~H"""
+ <.live_file_input upload={@upload} class="sr-only" capture={@capture} accept={@accept} />
+
+ """
+ end
+
+ defp image_file?(entry) do
+ entry.client_type in ["image/jpeg", "image/png", "image/gif", "image/heic", "image/webp"]
+ end
+end
diff --git a/lib/yearbook_web/endpoint.ex b/lib/yearbook_web/endpoint.ex
index 07a89a1..3cc8976 100644
--- a/lib/yearbook_web/endpoint.ex
+++ b/lib/yearbook_web/endpoint.ex
@@ -26,6 +26,14 @@ defmodule YearbookWeb.Endpoint do
gzip: not code_reloading?,
only: YearbookWeb.static_paths()
+ # Serve uploads from the "uploads" directory in development
+ if Mix.env() == :dev do
+ plug Plug.Static,
+ at: "/uploads",
+ from: Path.expand("./priv/uploads"),
+ gzip: false
+ end
+
# Code reloading can be explicitly enabled under the
# :code_reloader configuration of your endpoint.
if code_reloading? do
diff --git a/mix.exs b/mix.exs
index d0dad27..2528edb 100644
--- a/mix.exs
+++ b/mix.exs
@@ -53,6 +53,16 @@ defmodule Yearbook.MixProject do
{:postgrex, ">= 0.0.0"},
{:phoenix_html, "~> 4.1"},
+ # uploads
+ {:waffle_ecto, "~> 0.0.12"},
+ {:waffle, "~> 1.1.9"},
+ {:ex_aws, "~> 2.6.0"},
+ {:ex_aws_s3, "~> 2.5.8"},
+ {:hackney, "~> 1.25.0"},
+ {:httpoison, "~> 2.2.3"},
+ {:sweet_xml, "~> 0.7.5"},
+ {:zstream, "~> 0.6.7"},
+
# mailer
{:swoosh, "~> 1.16"},