Skip to content

Commit da00062

Browse files
feat: landing page with dynamic filtering (#51)
* feat: landing page * feat: pagination * feat: Upload photo component * feat: entry form * fix: make photo uploader reusable * fix: check --------- Co-authored-by: a111430 <a111430@alunos.uminho.pt>
1 parent 6cf8412 commit da00062

26 files changed

Lines changed: 1037 additions & 49 deletions

assets/css/app.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
@theme {
1414
--color-primary: #ee7749;
15+
--color-app-bg: #F9F9F9;
1516
}
1617

1718

config/config.exs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ config :yearbook,
2424
ecto_repos: [Yearbook.Repo],
2525
generators: [timestamp_type: :utc_datetime, binary_id: true]
2626

27+
# Waffle configuration
28+
config :waffle,
29+
storage: Waffle.Storage.Local,
30+
storage_dir_prefix: "priv",
31+
asset_host: {:system, "ASSET_HOST"}
32+
2733
# Configures the endpoint
2834
config :yearbook, YearbookWeb.Endpoint,
2935
url: [host: "localhost"],

lib/yearbook/entries.ex

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,34 @@ defmodule Yearbook.Entries do
8686
}
8787
end
8888

89+
@doc """
90+
Returns a paginated map of accepted entries based on filters and page.
91+
"""
92+
def list_accepted_entries_pagination(params \\ %{}) do
93+
query = from e in Entry, where: e.status == :accepted
94+
95+
query =
96+
case Map.get(params, "masters") do
97+
"true" -> from e in query, where: e.masters == true
98+
"false" -> from e in query, where: e.masters == false
99+
_ -> query
100+
end
101+
102+
query =
103+
case Map.get(params, "year") do
104+
year_str when year_str in [nil, ""] ->
105+
query
106+
107+
year_str ->
108+
year_int = if is_binary(year_str), do: String.to_integer(year_str), else: year_str
109+
from e in query, where: e.year == ^year_int
110+
end
111+
112+
query = from e in query, order_by: [asc: e.name]
113+
114+
paginate(query, params)
115+
end
116+
89117
@doc """
90118
Gets a single entry.
91119

lib/yearbook/entries/entry.ex

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,29 @@ defmodule Yearbook.Entries.Entry do
66
use Ecto.Schema
77
import Ecto.Changeset
88

9-
@required_fields ~w(photo name text)a
10-
@optional_fields ~w(status)a
9+
@required_fields ~w(photo name text year)a
10+
@optional_fields ~w(status masters)a
1111
@primary_key {:id, :binary_id, autogenerate: true}
1212
@foreign_key_type :binary_id
1313
schema "entries" do
1414
field :photo, :string
1515
field :name, :string
1616
field :text, :string
1717
field :status, Ecto.Enum, values: [:accepted, :pending, :denied], default: :pending
18+
field :masters, :boolean, default: false
19+
field :year, :integer
1820

1921
timestamps(type: :utc_datetime)
2022
end
2123

2224
@doc false
2325
def changeset(entry, attrs) do
26+
current_year = Date.utc_today().year
27+
2428
entry
2529
|> cast(attrs, @required_fields ++ @optional_fields)
2630
|> validate_required(@required_fields)
31+
|> validate_number(:year, greater_than: 2020, less_than_or_equal_to: current_year)
32+
|> validate_length(:text, max: 200)
2733
end
2834
end

lib/yearbook/schema.ex

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
defmodule Yearbook.Schema do
2+
@moduledoc """
3+
Base schema module.
4+
"""
5+
defmacro __using__(_) do
6+
quote do
7+
use Ecto.Schema
8+
use Waffle.Ecto.Schema
9+
10+
import Ecto.Changeset
11+
12+
alias Yearbook.Uploaders
13+
14+
@primary_key {:id, :binary_id, autogenerate: true}
15+
@foreign_key_type :binary_id
16+
17+
def validate_url(changeset, field) do
18+
changeset
19+
|> validate_format(
20+
field,
21+
~r/^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&\/=]*)$/,
22+
message: "must start with http:// or https:// and have a valid domain"
23+
)
24+
end
25+
end
26+
end
27+
end

lib/yearbook/uploader.ex

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
defmodule Yearbook.Uploader do
2+
@moduledoc """
3+
Base uploader module.
4+
"""
5+
defmacro __using__(_) do
6+
quote do
7+
use Waffle.Definition
8+
use Waffle.Ecto.Definition
9+
10+
def s3_object_headers(_version, {file, _scope}) do
11+
[content_type: MIME.from_path(file.file_name)]
12+
end
13+
end
14+
end
15+
end

lib/yearbook_web.ex

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,14 @@ defmodule YearbookWeb do
6464
end
6565
end
6666

67+
def component do
68+
quote do
69+
use Phoenix.Component
70+
71+
unquote(html_helpers())
72+
end
73+
end
74+
6775
def auth_view do
6876
quote do
6977
use Phoenix.LiveView,
@@ -120,6 +128,8 @@ defmodule YearbookWeb do
120128

121129
# Routes generation with the ~p sigil
122130
unquote(verified_routes())
131+
132+
alias Yearbook.Uploaders
123133
end
124134
end
125135

lib/yearbook_web/components/core_components.ex

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -521,27 +521,41 @@ defmodule YearbookWeb.CoreComponents do
521521
phx-mounted={@show && show_modal(@id)}
522522
phx-remove={hide_modal(@id)}
523523
data-cancel={JS.exec(@on_cancel, "phx-remove") |> hide_modal(@id)}
524-
class="relative z-60 hidden"
524+
class="relative z-50 hidden"
525525
>
526526
<div
527527
id={"#{@id}-bg"}
528-
class="bg-black/60 backdrop-blur-sm fixed inset-0 transition-opacity"
529-
aria-hidden="true"
528+
class="fixed inset-0 bg-black/50 backdrop-blur-md transition-opacity"
530529
/>
531-
<div class="fixed inset-0 overflow-y-auto">
532-
<div class="flex min-h-full items-center justify-center p-4">
533-
<div
534-
id={"#{@id}-container"}
535-
phx-click-away={JS.exec("data-cancel", to: "##{@id}")}
536-
class="relative bg-white rounded-xl max-w-sm w-full shadow-2xl p-8 text-center transition"
530+
531+
<div class="fixed inset-0 flex min-h-full items-start justify-center overflow-y-auto px-4 py-6 sm:items-center sm:py-10">
532+
<div
533+
id={"#{@id}-container"}
534+
phx-click-away={JS.exec("data-cancel", to: "##{@id}")}
535+
class="
536+
relative w-full max-w-2xl
537+
bg-white rounded-3xl
538+
shadow-[0_20px_60px_rgba(0,0,0,0.15)]
539+
border border-gray-100
540+
p-6 sm:p-10
541+
542+
animate-in fade-in zoom-in-95 duration-200
543+
"
544+
>
545+
<button
546+
phx-click={JS.exec("data-cancel", to: "##{@id}")}
547+
class="
548+
absolute top-5 right-5
549+
w-9 h-9 flex items-center justify-center
550+
text-gray-500 hover:text-gray-700
551+
transition-all focus:outline-none
552+
cursor-pointer
553+
"
537554
>
538-
<button
539-
phx-click={JS.exec("data-cancel", to: "##{@id}")}
540-
class="absolute top-4 right-4 text-gray-400 hover:text-gray-600 transition-colors cursor-pointer"
541-
>
542-
<.icon name="hero-x-mark" class="size-5" />
543-
</button>
555+
<.icon name="hero-x-mark" class="size-5" />
556+
</button>
544557
558+
<div class="text-left">
545559
{render_slot(@inner_block)}
546560
</div>
547561
</div>

lib/yearbook_web/components/entry_card.ex

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,25 @@ defmodule YearbookWeb.Components.EntryCard do
1111

1212
def entry_card(assigns) do
1313
~H"""
14-
<div class="flex flex-col items-center gap-2 p-2 overflow-auto bg-white rounded-xl shadow-md w-full min-h-56 sm:min-h-72 lg:min-h-96">
15-
<div class="w-full shrink-0 overflow-hidden aspect-square sm:aspect-4/3">
14+
<div class="flex flex-col items-center gap-3 p-4 bg-white rounded-2xl border border-gray-100 w-full h-full transition-all">
15+
<div class="w-full aspect-square overflow-hidden rounded-xl border border-gray-50">
1616
<%= if @src do %>
17-
<img
18-
src={@src}
19-
alt={@name}
20-
class="w-full h-full object-cover rounded-md"
21-
/>
17+
<img src={@src} alt={@name} class="w-full h-full object-cover" />
2218
<% else %>
23-
<div class="flex items-center justify-center w-full h-full rounded-xl bg-gray-300 text-white font-bold text-2xl">
19+
<div class="flex items-center justify-center w-full h-full bg-gray-100 text-gray-400 font-bold text-xl">
2420
{get_initials(@name)}
2521
</div>
2622
<% end %>
2723
</div>
28-
<h1 class="text-center w-full wrap-break-word font-bold px-2 text-md sm:text-lg lg:text-xl">
29-
{@name}
30-
</h1>
31-
<p class="flex-1 place-items-center p-2 text-sm sm:text-md lg:text-lg">{@text}</p>
24+
25+
<div class="text-center w-full">
26+
<h1 class="font-bold text-gray-900 text-sm md:text-base leading-tight">
27+
{@name}
28+
</h1>
29+
<p class="italic text-[11px] md:text-xs text-gray-500 mt-2 line-clamp-3 leading-relaxed">
30+
"{@text}"
31+
</p>
32+
</div>
3233
</div>
3334
"""
3435
end

0 commit comments

Comments
 (0)