Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
27 changes: 27 additions & 0 deletions lib/yearbook/schema.ex
Original file line number Diff line number Diff line change
@@ -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
15 changes: 15 additions & 0 deletions lib/yearbook/uploader.ex
Original file line number Diff line number Diff line change
@@ -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
10 changes: 10 additions & 0 deletions lib/yearbook_web.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -120,6 +128,8 @@ defmodule YearbookWeb do

# Routes generation with the ~p sigil
unquote(verified_routes())

alias Yearbook.Uploaders
end
end

Expand Down
114 changes: 114 additions & 0 deletions lib/yearbook_web/components/photo_upload_card.ex
Original file line number Diff line number Diff line change
@@ -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"""
<div class="bg-white p-6 rounded-2xl shadow-sm border border-gray-200 w-full max-w-md mx-auto">
<%= if @staged_photo_path do %>
<div class="flex flex-col gap-6 text-center">
<img
src={@staged_photo_path}
class="w-48 h-48 object-cover rounded-xl mx-auto border-2 border-primary"
/>
<p class="text-sm font-bold text-primary">Foto carregada com sucesso!</p>
</div>
<% else %>
<form
id="photo-upload-form"
phx-target={@myself}
phx-change="validate"
phx-submit="save"
class="flex flex-col gap-4"
>
<.photo_uploader
class="h-48 w-48 mx-auto rounded-xl border-primary/30! hover:border-primary! hover:bg-primary/5! transition-all! overflow-hidden cursor-pointer"
upload={@uploads.photo}
image_class="w-full h-full object-cover"
>
<:placeholder>
<div class="flex flex-col gap-2 items-center justify-center h-full text-primary/50 hover:text-primary transition-colors p-4">
<.icon name="hero-arrow-up-tray" class="w-8 h-8" />
<p class="text-sm font-medium text-gray-600">Escolha uma foto</p>
<p class="text-[10px] text-gray-400 uppercase tracking-widest mt-1">
PNG · JPG · JPEG · max 5 MB
</p>
</div>
</:placeholder>
</.photo_uploader>

<%= for err <- upload_errors(@uploads.photo) do %>
<p class="text-xs text-red-500 text-center font-bold tracking-wider">
{error_to_string(err)}
</p>
<% end %>
<%= for entry <- @uploads.photo.entries, err <- upload_errors(@uploads.photo, entry) do %>
<p class="text-xs text-red-500 text-center font-bold tracking-wider">
{error_to_string(err)}
</p>
<% end %>

<button
type="submit"
class="w-full py-3.5 rounded-full bg-primary text-white font-bold tracking-widest text-xs hover:bg-primary/85 active:scale-[0.98] transition-all duration-200 disabled:opacity-40 disabled:cursor-not-allowed"
disabled={@uploads.photo.entries == [] or upload_errors(@uploads.photo) != []}
phx-disable-with="A CARREGAR..."
>
UPLOAD
</button>
</form>
<% end %>
</div>
"""
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
Comment thread
matheusm18 marked this conversation as resolved.
82 changes: 82 additions & 0 deletions lib/yearbook_web/components/photo_uploader.ex
Original file line number Diff line number Diff line change
@@ -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} />
<label for={@upload.ref} class="block">
<section
phx-drop-target={@upload.ref}
class={[
"transition-colors hover:cursor-pointer border-2 border-dashed",
@rounded && "rounded-full overflow-hidden",
@class
]}
>
<%= if @upload.entries == [] do %>
<article class="h-full">
<figure class="h-full flex items-center justify-center">
<%= if @image do %>
<img class={[@rounded && "p-0", not @rounded && "p-4", @image_class]} src={@image} />
<% else %>
<%= if @placeholder do %>
{render_slot(@placeholder)}
<% else %>
<div class="select-none flex flex-col gap-2 items-center">
<.icon name={@icon} class="w-12 h-12" />
<p class="px-4 text-center">{gettext("Upload a file or drag and drop.")}</p>
</div>
<% end %>
<% end %>
</figure>
</article>
<% end %>
<%= if !@preview_disabled do %>
<%= for entry <- @upload.entries do %>
<article class="h-full">
<figure class="h-full flex items-center justify-center">
<%= if image_file?(entry) do %>
<.live_img_preview
class={[@rounded && "p-0", not @rounded && "p-4", @image_class]}
entry={entry}
/>
<% else %>
<div class="select-none flex flex-col gap-2 items-center">
<.icon name="hero-document" class="w-12 h-12" />
<p class="px-4 text-center">{entry.client_name}</p>
</div>
<% end %>
</figure>
<%= for err <- upload_errors(@upload, entry) do %>
<p class="alert alert-danger">{Phoenix.Naming.humanize(err)}</p>
<% end %>
</article>
<% end %>
<% end %>
<%= for err <- upload_errors(@upload) do %>
<p class="alert alert-danger">{Phoenix.Naming.humanize(err)}</p>
<% end %>
</section>
</label>
"""
end

defp image_file?(entry) do
entry.client_type in ["image/jpeg", "image/png", "image/gif", "image/heic", "image/webp"]
end
end
Comment thread
matheusm18 marked this conversation as resolved.
8 changes: 8 additions & 0 deletions lib/yearbook_web/endpoint.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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"},

Expand Down
Loading