From 14c4cc7336a5088921a5a77a0cd342e850d7ab71 Mon Sep 17 00:00:00 2001 From: GuilhermePSF Date: Mon, 15 Dec 2025 11:40:06 +0000 Subject: [PATCH 1/9] feat: add user registration fields and migration --- lib/pearl/accounts/user.ex | 9 ++++++++- ...51122152450_add_registration_fields_to_users.exs | 13 +++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 priv/repo/migrations/20251122152450_add_registration_fields_to_users.exs diff --git a/lib/pearl/accounts/user.ex b/lib/pearl/accounts/user.ex index f9dba67..35fcc9c 100644 --- a/lib/pearl/accounts/user.ex +++ b/lib/pearl/accounts/user.ex @@ -44,7 +44,14 @@ defmodule Pearl.Accounts.User do field :type, Ecto.Enum, values: [:attendee, :staff, :company], default: :attendee field :allows_marketing, :boolean, default: false field :cv, Uploaders.CV.Type - + # The proper type does not exist yet + field :ticket_type, :string + field :notes, :string + field :university, :string + field :district, :string + field :dietary_restrictions, :string + + has_one :ticket, Pearl.Tickets.Ticket, on_delete: :delete_all has_one :attendee, Attendee, on_delete: :delete_all has_one :staff, Staff, on_delete: :delete_all, on_replace: :update has_one :company, Company, on_delete: :delete_all diff --git a/priv/repo/migrations/20251122152450_add_registration_fields_to_users.exs b/priv/repo/migrations/20251122152450_add_registration_fields_to_users.exs new file mode 100644 index 0000000..38d969d --- /dev/null +++ b/priv/repo/migrations/20251122152450_add_registration_fields_to_users.exs @@ -0,0 +1,13 @@ +defmodule Pearl.Repo.Migrations.AddRegistrationFieldsToUsers do + use Ecto.Migration + + def change do + alter table(:users) do + add :ticket_type, :string + add :notes, :string + add :university, :string + add :city, :string + add :dietary_restrictions, :string + end + end +end From 9514fd3cdb26678e1cb97c5b1eb6cbd692e24587 Mon Sep 17 00:00:00 2001 From: GuilhermePSF Date: Mon, 15 Dec 2025 11:40:11 +0000 Subject: [PATCH 2/9] ui: add flushed variant to input components --- lib/pearl_web/components/core_components.ex | 141 +++++++++----------- 1 file changed, 60 insertions(+), 81 deletions(-) diff --git a/lib/pearl_web/components/core_components.ex b/lib/pearl_web/components/core_components.ex index fa839ac..cf7d549 100644 --- a/lib/pearl_web/components/core_components.ex +++ b/lib/pearl_web/components/core_components.ex @@ -248,35 +248,16 @@ defmodule PearlWeb.CoreComponents do @doc """ Renders an input with label and error messages. - - A `Phoenix.HTML.FormField` may be passed as argument, - which is used to retrieve the input name, id, and values. - Otherwise all attributes may be passed explicitly. - - ## Types - - This function accepts all HTML input types, considering that: - - * You may also set `type="select"` to render a ` - - {Phoenix.HTML.Form.options_for_select(@options, @value)} - - <.error :for={msg <- @errors}>{msg} +
+ + <.error :for={msg <- @errors}>{msg} +
""" end @@ -358,74 +344,67 @@ defmodule PearlWeb.CoreComponents do ~H"""
<.label for={@id}>{@label} - - <.error :for={msg <- @errors}>{msg} +
+ + <.error :for={msg <- @errors}>{msg} +
""" end - def input(%{type: "handle"} = assigns) do + def input(assigns) do ~H"""
<.label for={@id}>{@label} -
- @ +
+ <.error :for={msg <- @errors}>{msg}
- <.error :for={msg <- @errors}>{msg}
""" end - # All other inputs text, datetime-local, url, password, etc. are handled here... - def input(assigns) do - ~H""" -
- <.label for={@id}>{@label} - - <.error :for={msg <- @errors}>{msg} -
- """ + defp input_class(:default) do + "mt-2 block w-full rounded-lg text-dark focus:ring-0 sm:text-sm sm:leading-6 bg-surface border-0" + end + + defp input_class(:flushed) do + "block w-full px-0 py-2 border-0 border-b border-dark-muted/30 focus:border-primary focus:ring-0 bg-transparent text-dark placeholder-dark-muted/60 focus:outline-none transition-colors sm:text-sm" + end + + defp input_border(:default, errors) do + cond do + errors == [] -> "border-transparent focus:border-primary" + true -> "border-danger-400 focus:border-danger-400" + end + end + + defp input_border(:flushed, errors) do + cond do + errors == [] -> "" + true -> "border-danger-500 focus:border-danger-500 text-danger-500 placeholder-danger-300" + end end @doc """ From 195b1db3a50d64043ceb0e6f679bdd6acf4fbef3 Mon Sep 17 00:00:00 2001 From: GuilhermePSF Date: Mon, 15 Dec 2025 11:40:14 +0000 Subject: [PATCH 3/9] feat: implement multi-step registration flow --- .../components/registration_components.ex | 91 ++++++ .../live/auth/user_registration_live.ex | 108 ++++++- .../auth/user_registration_live.html.heex | 297 +++++++++++++----- 3 files changed, 397 insertions(+), 99 deletions(-) create mode 100644 lib/pearl_web/components/registration_components.ex diff --git a/lib/pearl_web/components/registration_components.ex b/lib/pearl_web/components/registration_components.ex new file mode 100644 index 0000000..329f8e1 --- /dev/null +++ b/lib/pearl_web/components/registration_components.ex @@ -0,0 +1,91 @@ +defmodule PearlWeb.RegistrationComponents do + use PearlWeb, :component + + import PearlWeb.Landing.Components.Navbar + import PearlWeb.Landing.Components.Footer + + slot :sidebar, required: true + slot :header, required: false + slot :inner_block, required: true + + def registration_layout(assigns) do + ~H""" + <.navbar + pages={PearlWeb.Config.landing_pages()} + registrations_open?={Pearl.Event.registrations_open?()} + current_user={Map.get(assigns, :current_user)} + /> + +
+
+
+ {render_slot(@header)} +
+ +
+ + +
+ {render_slot(@inner_block)} +
+
+
+
+ + <.footer> + <:tip :if={ + Map.get(assigns, :current_page, nil) in [:home, :schedule, :speakers, :faqs] and + Pearl.Event.get_feature_flag("challenges_enabled") + }> + Have you checked out the + <.link class="underline" navigate={~p"/challenges"}>challenges + yet? <.link href="https://www.youtube.com/watch?v=xvFZjo5PgG0" target="_blank">🏆 + + + """ + end + + attr :current_step, :integer, required: true + + def step_bar(assigns) do + assigns = + assign(assigns, :steps, [ + {1, "Tipo de bilhete"}, + {2, "Dados pessoais"}, + {3, "Precauções"}, + {4, "Informações"}, + {5, "Verificação"} + ]) + + ~H""" +
+

Inscrição

+ +
+ <%= for {index, label} <- @steps do %> +
+ + {index} + + {label} +
+ <% end %> +
+
+ """ + end +end diff --git a/lib/pearl_web/live/auth/user_registration_live.ex b/lib/pearl_web/live/auth/user_registration_live.ex index 6dcadb6..21770ba 100644 --- a/lib/pearl_web/live/auth/user_registration_live.ex +++ b/lib/pearl_web/live/auth/user_registration_live.ex @@ -1,36 +1,114 @@ defmodule PearlWeb.UserRegistrationLive do - use PearlWeb, :landing_view + use PearlWeb, :live_view alias Pearl.Accounts alias Pearl.Accounts.User - import PearlWeb.Components.Button + import PearlWeb.RegistrationComponents + import PearlWeb.CoreComponents def mount(_params, _session, socket) do changeset = Accounts.change_user_registration(%User{}) - socket = - socket - |> assign(trigger_submit: false, errors: false) - |> assign_form(changeset) + default_ticket_id = :general + selected_ticket = get_ticket_config(default_ticket_id) - {:ok, socket, temporary_assigns: [form: nil]} + {:ok, + socket + |> assign(trigger_submit: false, check_errors: false) + |> assign_form(changeset) + |> assign(:step, 1) + |> assign(:total_price, selected_ticket.price) + |> assign(:selected_ticket, selected_ticket) + |> assign(:current_user, nil) + |> assign(:registrations_open?, true) + |> assign(:pages, [])} + end + + defp get_ticket_config(id) do + # Com os tickets o Enrico deve fazer isto mais seamless no futuro + tickets = %{ + general: %{ + id: :general, + name: "Passe Geral", + price: "XX,00", + # Ícones e Cores MAybe fazer isto mais "simples" era melhor + sidebar_icons: [ + %{icon: "hero-ticket", bg: "#FF5A87", rounded: "rounded-l-md"}, + %{icon: "hero-cake", bg: "#D9B568", rounded: ""}, + %{icon: "hero-sparkles", bg: "#A3C982", rounded: ""}, + %{icon: "hero-home", bg: "#8AB5C9", rounded: "rounded-r-md"}, + %{icon: "hero-star", bg: "#8FA3AD", rounded: "ml-1 rounded-[50%]"} + ], + # Lista de Benefícios Texto e Cor do certinho + benefits: [ + %{text: "Entrada nos 4 dias do evento", color: "text-[#FF5A87]"}, + %{text: "Acesso a todas as atividades", color: "text-[#FF5A87]"}, + %{text: "Coffee Break de manhã e de tarde", color: "text-[#D9B568]"}, + %{text: "Almoço e Jantar incluídos", color: "text-[#A3C982]"}, + %{text: "3 noites de alojamento", color: "text-[#8AB5C9]"}, + %{text: "Transporte alojamento-evento", color: "text-[#8AB5C9]"}, + %{text: "Pequeno almoço", color: "text-[#8AB5C9]"} + ] + } + + # depois adciona-se mais aqui + } + + Map.get(tickets, id, tickets.general) + end + + defp get_initials(name) do + case String.split(name || "", " ", trim: true) do + [] -> nil + [single] -> String.slice(single, 0, 1) + names -> "#{String.first(List.first(names))}#{String.first(List.last(names))}" + end + end + + def handle_event("next_step", params, socket) do + user_params = params["user"] || %{} + + changeset = + socket.assigns.form.source + |> User.registration_changeset(user_params) + + new_step = socket.assigns.step + 1 + {:noreply, socket |> assign(:step, new_step) |> assign_form(changeset)} + end + + def handle_event("prev_step", _, socket) do + new_step = max(1, socket.assigns.step - 1) + {:noreply, assign(socket, step: new_step)} end def handle_event("validate", %{"user" => user_params}, socket) do changeset = Accounts.change_user_registration(%User{}, user_params) - {:noreply, assign_form(socket, Map.put(changeset, :action, :validate))} end - defp assign_form(socket, %Ecto.Changeset{} = changeset) do - form = to_form(changeset, as: "user") + def handle_event("save", %{"user" => user_params}, socket) do + case Accounts.register_attendee_user(user_params) do + {:ok, user} -> + {:ok, _} = + Accounts.deliver_user_confirmation_instructions(user, &url(~p"/users/confirm/#{&1}")) - if changeset.valid? do - socket - |> assign(form: form, errors: false) - else - assign(socket, form: form, errors: true) + changeset = Accounts.change_user_registration(user) + {:noreply, socket |> assign(trigger_submit: true) |> assign_form(changeset)} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, socket |> assign(check_errors: true) |> assign_form(changeset)} end end + + def step_header(1), do: "Bilhete Geral" + def step_header(2), do: "Ótimo! Agora precisamos de alguns dados pessoais teus." + def step_header(3), do: "Precauções e Alergias" + def step_header(4), do: "Informações Adicionais" + def step_header(5), do: "Já temos tudo." + def step_header(_), do: "" + + defp assign_form(socket, %Ecto.Changeset{} = changeset) do + assign(socket, :form, to_form(changeset, as: "user")) + end end diff --git a/lib/pearl_web/live/auth/user_registration_live.html.heex b/lib/pearl_web/live/auth/user_registration_live.html.heex index c209525..457f0a0 100644 --- a/lib/pearl_web/live/auth/user_registration_live.html.heex +++ b/lib/pearl_web/live/auth/user_registration_live.html.heex @@ -1,91 +1,220 @@ -
-
- -
- <.header class="text-center px-4"> -

Event Registration

- <:subtitle> - {gettext("Already registered?")} - <.link navigate={~p"/users/log_in"} class="font-semibold text-accent hover:underline"> - Log in - - {gettext("to your account now")} - - +<.registration_layout> + <:header> + <.step_bar current_step={@step} /> + - <.form - for={@form} - id="registration_form" - phx-change="validate" - phx-trigger-action={@trigger_submit} - action={~p"/users/register"} - class="mx-auto w-90 flex flex-col px-6 sm:px-0 sm:grid grid-cols-2 gap-4 py-10" - > - <.input - field={@form[:name]} - type="text" - label="Name" - placeholder="John Doe" - autocomplete="off" - required - /> - <.input - field={@form[:handle]} - type="handle" - label="Username" - autocomplete="off" - required - placeholder="johndoe" - /> - <.input - field={@form[:email]} - type="email" - label="Email" - required - wrapper_class="col-span-2" - placeholder="john.doe@cesium.pt" - /> - <.input field={@form[:password]} type="password" label="Password" required /> - <.input - field={@form[:password_confirmation]} - type="password" - label="Confirm Password" - required - /> + <:sidebar> +
+

+ {step_header(@step)} +

- <.label class="col-span-2 pt-4"> -
-
- <.input name="consent" label="" type="checkbox" value={false} required /> + <%= if @step == 1 do %> +
+ <%= for icon_config <- @selected_ticket.sidebar_icons do %> +
+ <.icon name={icon_config.icon} class="text-white w-6 h-6" /> +
+ <% end %>
- -
- {gettext("I have read and agree to the ")} - <.link - href={~p"/docs/privacy_policy.pdf"} - target="_blank" - class="font-semibold text-accent hover:underline" - > - {gettext("Privacy Policy.")} - + <% else %> +
+
+ <% name_value = Phoenix.HTML.Form.input_value(@form, :name) %> + <% initials = get_initials(name_value) %> + <% has_initials = initials != nil %> + +
+ <.icon name="hero-user" class="text-white size-20" /> +
+ +
+ + {initials} + +
+
-
- - <.label class="col-span-2 pb-8"> -
-
- <.input label="" type="checkbox" field={@form[:allows_marketing]} /> -
-
- {gettext("I want to receive marketing emails promoting the event.")} + <% end %> + +
+
+ +

Preço atual:

+

+ {@total_price} +

+
+

+ INCL. IVA +

- -
- <.action_button title="Register" subtitle="" class="mx-12" disabled={@errors} />
- -
+ + +
+ <.form + for={@form} + id="registration_form" + phx-change="validate" + phx-submit={if @step < 5, do: "next_step", else: "save"} + class="flex-1 flex flex-col justify-between min-h-[500px]" + > +
+ <%= case @step do %> + <% 1 -> %> +
+

Benefícios deste bilhete:

+
    + <%= for benefit <- @selected_ticket.benefits do %> +
  • + <.icon name="hero-check" class={"w-5 h-5 #{benefit.color}"} /> + {benefit.text} +
  • + <% end %> +
+
+ +
+ +
+

+ ESPACO PARA OS TICKETS (╯°□°)╯︵ ┻━┻ +

+
+ <% 2 -> %> + <.input + field={@form[:name]} + label="Nome" + placeholder="Nome completo" + variant={:flushed} + wrapper_class="grid grid-cols-1 sm:grid-cols-[200px_1fr] gap-25 items-baseline" + required + /> + +
+ <.label for={@form[:email].id} class="text-dark">Email +
+ <.input + field={@form[:email]} + type="email" + placeholder="Endereço de email" + variant={:flushed} + required + label="" + /> +

+ <.icon name="hero-information-circle-mini" class="w-5 h-5" /> + Vamos verificar o teu email num passo seguinte. +

+
+
+ +
+ +
+ + + +
+
+ +
+ +
+
+ <.input + field={@form[:phone_prefix]} + type="select" + options={[{"PT (+351)", "+351"}, {"ES (+34)", "+34"}]} + variant={:flushed} + label="" + /> +
+
+ <.input + field={@form[:phone_number]} + placeholder="Número de telefone" + variant={:flushed} + label="" + /> +
+
+
+ + <.input + field={@form[:country]} + type="select" + label="País" + options={[{"Portugal", "PT"}, {"Brasil", "BR"}]} + placeholder="Selecionar país" + variant={:flushed} + wrapper_class="grid grid-cols-1 sm:grid-cols-[200px_1fr] gap-25 items-baseline" + required + /> + + <.input + field={@form[:city]} + type="select" + label="Cidade" + options={[{"Lisboa", "Lisboa"}, {"Porto", "Porto"}, {"Braga", "Braga"}]} + placeholder="Selecionar distrito" + variant={:flushed} + wrapper_class="grid grid-cols-1 sm:grid-cols-[200px_1fr] gap-25 items-baseline" + required + /> + + <.input + field={@form[:nif]} + label="NIF" + placeholder="Número de Identificação Fiscal" + variant={:flushed} + wrapper_class="grid grid-cols-1 sm:grid-cols-[200px_1fr] gap-25 items-baseline" + /> + <% 3 -> %> + (╯°□°)╯︵ ┻━┻" + <% 4 -> %> + (╯°□°)╯︵ ┻━┻" + <% 5 -> %> + (╯°□°)╯︵ ┻━┻" + <% _ -> %> +
+ Passo {@step} desconhecido (╯°□°)╯︵ ┻━┻ +
+ <% end %> +
+ +
+ + + +
+ +
+ From 341aca702ca23681443207abe2417c99ce9ce723 Mon Sep 17 00:00:00 2001 From: GuilhermePSF Date: Mon, 15 Dec 2025 11:57:37 +0000 Subject: [PATCH 4/9] fix: display dynamic user name in step 3 header --- .../live/auth/user_registration_live.ex | 25 ++++++++++++++----- .../auth/user_registration_live.html.heex | 4 +-- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/lib/pearl_web/live/auth/user_registration_live.ex b/lib/pearl_web/live/auth/user_registration_live.ex index 21770ba..be88eab 100644 --- a/lib/pearl_web/live/auth/user_registration_live.ex +++ b/lib/pearl_web/live/auth/user_registration_live.ex @@ -101,13 +101,26 @@ defmodule PearlWeb.UserRegistrationLive do end end - def step_header(1), do: "Bilhete Geral" - def step_header(2), do: "Ótimo! Agora precisamos de alguns dados pessoais teus." - def step_header(3), do: "Precauções e Alergias" - def step_header(4), do: "Informações Adicionais" - def step_header(5), do: "Já temos tudo." - def step_header(_), do: "" + def step_header(step, form \\ nil) + def step_header(1, _), do: "Bem-vindo ao ENEI, para começar, escolhe o tipo de bilhete que desejas adquirir!" + def step_header(2, _), do: "Ótimo! Agora precisamos de alguns dados pessoais teus." + + def step_header(3, form) do + first_name = + (Phoenix.HTML.Form.input_value(form, :name) || "") + |> String.split(" ", trim: true) + |> List.first() + |> Kernel.||("Participante") + + "Olá, #{first_name}! Se tiveres alguma incapacidade (motora, visual, auditiva) ou alergia alimentar, pedimos que nos informes." + end + + def step_header(4, _), do: "Ok! Para finalizar, precisamos de saber mais alguns detalhes, e também temos algumas curiosidades." + + def step_header(5, _), do: "Já temos tudo. Confirma se está tudo certo e verifica o teu email com o código que enviamos. A seguir, serás redirecionado para o pagamento." + + def step_header(_, _), do: "" defp assign_form(socket, %Ecto.Changeset{} = changeset) do assign(socket, :form, to_form(changeset, as: "user")) end diff --git a/lib/pearl_web/live/auth/user_registration_live.html.heex b/lib/pearl_web/live/auth/user_registration_live.html.heex index 457f0a0..ce45b43 100644 --- a/lib/pearl_web/live/auth/user_registration_live.html.heex +++ b/lib/pearl_web/live/auth/user_registration_live.html.heex @@ -5,8 +5,8 @@ <:sidebar>
-

- {step_header(@step)} +

+ {step_header(@step, @form)}

<%= if @step == 1 do %> From f31df25c8e68beaa74687cb2f454ef97a8d06246 Mon Sep 17 00:00:00 2001 From: GuilhermePSF Date: Mon, 15 Dec 2025 12:32:27 +0000 Subject: [PATCH 5/9] refactor: add content to selects --- lib/pearl/catalog.ex | 103 ++++++++++++++++++ .../live/auth/user_registration_live.ex | 49 ++++++++- .../auth/user_registration_live.html.heex | 82 +++++++++++--- 3 files changed, 214 insertions(+), 20 deletions(-) create mode 100644 lib/pearl/catalog.ex diff --git a/lib/pearl/catalog.ex b/lib/pearl/catalog.ex new file mode 100644 index 0000000..f1dc604 --- /dev/null +++ b/lib/pearl/catalog.ex @@ -0,0 +1,103 @@ +defmodule Pearl.Catalog do + @moduledoc """ + Listas para preencher selects de formulários. + """ + + def universities do + [ + # Universidades Públicas + "Universidade de Lisboa", + "Universidade do Porto", + "Universidade de Coimbra", + "Universidade Nova de Lisboa", + "Universidade do Minho", + "Universidade de Aveiro", + "Universidade da Beira Interior", + "Universidade de Évora", + "Universidade do Algarve", + "Universidade de Trás-os-Montes e Alto Douro", + "Universidade da Madeira", + "Universidade dos Açores", + "Universidade Aberta", + "ISCTE - Instituto Universitário de Lisboa", + + # Institutos Politécnicos + "Instituto Politécnico de Lisboa", + "Instituto Politécnico do Porto", + "Instituto Politécnico de Coimbra", + "Instituto Politécnico de Leiria", + "Instituto Politécnico de Setúbal", + "Instituto Politécnico de Viseu", + "Instituto Politécnico de Santarém", + "Instituto Politécnico de Viana do Castelo", + "Instituto Politécnico de Castelo Branco", + "Instituto Politécnico de Beja", + "Instituto Politécnico de Bragança", + "Instituto Politécnico da Guarda", + "Instituto Politécnico de Portalegre", + "Instituto Politécnico de Tomar", + "Instituto Politécnico do Cávado e do Ave", + + # Privadas + "Universidade Católica Portuguesa", + "Universidade Lusófona", + "Universidade Lusíada", + "Universidade Fernando Pessoa", + "Universidade Europeia", + "Universidade Autónoma de Lisboa", + "Universidade Portucalense", + "Universidade Atlântica", + + # Outra + "Outra / Externo" + ] + end + + def cities do + # Lista simplificada dos principais concelhos/cidades para não ser gigante + [ + "Lisboa", + "Porto", + "Vila Nova de Gaia", + "Amadora", + "Braga", + "Funchal", + "Coimbra", + "Setúbal", + "Almada", + "Agualva-Cacém", + "Queluz", + "Rio Tinto", + "Barreiro", + "Aveiro", + "Viseu", + "Odivelas", + "Leiria", + "Guimarães", + "Faro", + "Matosinhos", + "Loures", + "Póvoa de Varzim", + "Maia", + "Évora", + "Portimão", + "Viana do Castelo", + "Castelo Branco", + "Covilhã", + "Guarda", + "Vila Real", + "Ponta Delgada", + "Santarém", + "Figueira da Foz", + "Caldas da Rainha", + "Torres Vedras", + "Vila Franca de Xira", + "Valongo", + "Gondomar", + "Vila do Conde", + "Barcelos", + "Outra" + ] + |> Enum.sort() + end +end diff --git a/lib/pearl_web/live/auth/user_registration_live.ex b/lib/pearl_web/live/auth/user_registration_live.ex index be88eab..818ca74 100644 --- a/lib/pearl_web/live/auth/user_registration_live.ex +++ b/lib/pearl_web/live/auth/user_registration_live.ex @@ -13,6 +13,17 @@ defmodule PearlWeb.UserRegistrationLive do default_ticket_id = :general selected_ticket = get_ticket_config(default_ticket_id) + months = [ + {"Janeiro", 1}, {"Fevereiro", 2}, {"Março", 3}, {"Abril", 4}, + {"Maio", 5}, {"Junho", 6}, {"Julho", 7}, {"Agosto", 8}, + {"Setembro", 9}, {"Outubro", 10}, {"Novembro", 11}, {"Dezembro", 12} + ] + + current_year = Date.utc_today().year + years = (current_year)..(current_year - 100) + + days = 1..31 + {:ok, socket |> assign(trigger_submit: false, check_errors: false) @@ -22,7 +33,12 @@ defmodule PearlWeb.UserRegistrationLive do |> assign(:selected_ticket, selected_ticket) |> assign(:current_user, nil) |> assign(:registrations_open?, true) - |> assign(:pages, [])} + |> assign(:universities, Pearl.Catalog.universities()) + |> assign(:cities, Pearl.Catalog.cities()) + |> assign(:pages, []) + |> assign(:months, months) + |> assign(:years, years) + |> assign(:days, days)} end defp get_ticket_config(id) do @@ -84,7 +100,11 @@ defmodule PearlWeb.UserRegistrationLive do def handle_event("validate", %{"user" => user_params}, socket) do changeset = Accounts.change_user_registration(%User{}, user_params) - {:noreply, assign_form(socket, Map.put(changeset, :action, :validate))} + days_range = calculate_days_range(user_params["birth_date"]) + {:noreply, + socket + |> assign(:days, days_range) + |> assign_form(Map.put(changeset, :action, :validate))} end def handle_event("save", %{"user" => user_params}, socket) do @@ -121,7 +141,32 @@ defmodule PearlWeb.UserRegistrationLive do def step_header(5, _), do: "Já temos tudo. Confirma se está tudo certo e verifica o teu email com o código que enviamos. A seguir, serás redirecionado para o pagamento." def step_header(_, _), do: "" + defp assign_form(socket, %Ecto.Changeset{} = changeset) do assign(socket, :form, to_form(changeset, as: "user")) end + + defp calculate_days_range(%{"year" => y, "month" => m}) when y != "" and m != "" do + year = String.to_integer(y) + month = String.to_integer(m) + + case Date.new(year, month, 1) do + + {:ok, date} -> 1..Date.days_in_month(date) + + _ -> 1..31 + end + end + + defp calculate_days_range(%{"month" => m}) when m != "" do + month = String.to_integer(m) + + case Date.new(Date.utc_today().year, month, 1) do + {:ok, date} -> 1..Date.days_in_month(date) + + _ -> 1..31 + end + end + + defp calculate_days_range(_), do: 1..31 end diff --git a/lib/pearl_web/live/auth/user_registration_live.html.heex b/lib/pearl_web/live/auth/user_registration_live.html.heex index ce45b43..046de76 100644 --- a/lib/pearl_web/live/auth/user_registration_live.html.heex +++ b/lib/pearl_web/live/auth/user_registration_live.html.heex @@ -6,7 +6,7 @@ <:sidebar>

- {step_header(@step, @form)} + {step_header(@step, @form)}

<%= if @step == 1 do %> @@ -26,11 +26,11 @@ <% name_value = Phoenix.HTML.Form.input_value(@form, :name) %> <% initials = get_initials(name_value) %> <% has_initials = initials != nil %> - +
<.icon name="hero-user" class="text-white size-20" />
- +
{initials} @@ -118,17 +118,63 @@ + + <% field = @form[:birth_date] %> + <% value = Phoenix.HTML.Form.input_value(@form, :birth_date) %> +
- - - +
+ +
+ +
+ +
+ +
+ +
+ <.error :for={msg <- field.errors}>{msg}
@@ -155,11 +201,11 @@
<.input - field={@form[:country]} + field={@form[:university]} type="select" - label="País" - options={[{"Portugal", "PT"}, {"Brasil", "BR"}]} - placeholder="Selecionar país" + label="Universidade" + options={@universities} + prompt="Selecionar instituição" variant={:flushed} wrapper_class="grid grid-cols-1 sm:grid-cols-[200px_1fr] gap-25 items-baseline" required @@ -169,8 +215,8 @@ field={@form[:city]} type="select" label="Cidade" - options={[{"Lisboa", "Lisboa"}, {"Porto", "Porto"}, {"Braga", "Braga"}]} - placeholder="Selecionar distrito" + options={@cities} + prompt="Selecionar cidade" variant={:flushed} wrapper_class="grid grid-cols-1 sm:grid-cols-[200px_1fr] gap-25 items-baseline" required From b181c08a0fcff81834c2cd9ee2d5504ef41d0435 Mon Sep 17 00:00:00 2001 From: GuilhermePSF Date: Mon, 15 Dec 2025 12:45:36 +0000 Subject: [PATCH 6/9] fix: comment line to pass build --- lib/pearl/accounts/user.ex | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/pearl/accounts/user.ex b/lib/pearl/accounts/user.ex index 35fcc9c..b83a2ff 100644 --- a/lib/pearl/accounts/user.ex +++ b/lib/pearl/accounts/user.ex @@ -44,14 +44,12 @@ defmodule Pearl.Accounts.User do field :type, Ecto.Enum, values: [:attendee, :staff, :company], default: :attendee field :allows_marketing, :boolean, default: false field :cv, Uploaders.CV.Type - # The proper type does not exist yet - field :ticket_type, :string field :notes, :string field :university, :string field :district, :string field :dietary_restrictions, :string - has_one :ticket, Pearl.Tickets.Ticket, on_delete: :delete_all + # has_one :ticket, Pearl.Tickets.Ticket, on_delete: :delete_all has_one :attendee, Attendee, on_delete: :delete_all has_one :staff, Staff, on_delete: :delete_all, on_replace: :update has_one :company, Company, on_delete: :delete_all From 230a8549885cfa3e5623faca897b5c8bb210b4b1 Mon Sep 17 00:00:00 2001 From: GuilhermePSF Date: Mon, 15 Dec 2025 13:06:27 +0000 Subject: [PATCH 7/9] chore: change tests to pass build --- lib/pearl/accounts/user.ex | 2 +- .../live/auth/user_registration_live.ex | 37 +++++++++++++------ ...52450_add_registration_fields_to_users.exs | 2 +- test/pearl_web/live/user_login_live_test.exs | 2 +- .../live/user_registration_live_test.exs | 25 +++++++------ 5 files changed, 42 insertions(+), 26 deletions(-) diff --git a/lib/pearl/accounts/user.ex b/lib/pearl/accounts/user.ex index b83a2ff..ed359bb 100644 --- a/lib/pearl/accounts/user.ex +++ b/lib/pearl/accounts/user.ex @@ -46,7 +46,7 @@ defmodule Pearl.Accounts.User do field :cv, Uploaders.CV.Type field :notes, :string field :university, :string - field :district, :string + field :city, :string field :dietary_restrictions, :string # has_one :ticket, Pearl.Tickets.Ticket, on_delete: :delete_all diff --git a/lib/pearl_web/live/auth/user_registration_live.ex b/lib/pearl_web/live/auth/user_registration_live.ex index 818ca74..b09087a 100644 --- a/lib/pearl_web/live/auth/user_registration_live.ex +++ b/lib/pearl_web/live/auth/user_registration_live.ex @@ -14,13 +14,22 @@ defmodule PearlWeb.UserRegistrationLive do selected_ticket = get_ticket_config(default_ticket_id) months = [ - {"Janeiro", 1}, {"Fevereiro", 2}, {"Março", 3}, {"Abril", 4}, - {"Maio", 5}, {"Junho", 6}, {"Julho", 7}, {"Agosto", 8}, - {"Setembro", 9}, {"Outubro", 10}, {"Novembro", 11}, {"Dezembro", 12} + {"Janeiro", 1}, + {"Fevereiro", 2}, + {"Março", 3}, + {"Abril", 4}, + {"Maio", 5}, + {"Junho", 6}, + {"Julho", 7}, + {"Agosto", 8}, + {"Setembro", 9}, + {"Outubro", 10}, + {"Novembro", 11}, + {"Dezembro", 12} ] current_year = Date.utc_today().year - years = (current_year)..(current_year - 100) + years = current_year..(current_year - 100) days = 1..31 @@ -67,7 +76,6 @@ defmodule PearlWeb.UserRegistrationLive do %{text: "Pequeno almoço", color: "text-[#8AB5C9]"} ] } - # depois adciona-se mais aqui } @@ -100,7 +108,9 @@ defmodule PearlWeb.UserRegistrationLive do def handle_event("validate", %{"user" => user_params}, socket) do changeset = Accounts.change_user_registration(%User{}, user_params) + days_range = calculate_days_range(user_params["birth_date"]) + {:noreply, socket |> assign(:days, days_range) @@ -123,7 +133,9 @@ defmodule PearlWeb.UserRegistrationLive do def step_header(step, form \\ nil) - def step_header(1, _), do: "Bem-vindo ao ENEI, para começar, escolhe o tipo de bilhete que desejas adquirir!" + def step_header(1, _), + do: "Bem-vindo ao ENEI, para começar, escolhe o tipo de bilhete que desejas adquirir!" + def step_header(2, _), do: "Ótimo! Agora precisamos de alguns dados pessoais teus." def step_header(3, form) do @@ -136,9 +148,13 @@ defmodule PearlWeb.UserRegistrationLive do "Olá, #{first_name}! Se tiveres alguma incapacidade (motora, visual, auditiva) ou alergia alimentar, pedimos que nos informes." end - def step_header(4, _), do: "Ok! Para finalizar, precisamos de saber mais alguns detalhes, e também temos algumas curiosidades." + def step_header(4, _), + do: + "Ok! Para finalizar, precisamos de saber mais alguns detalhes, e também temos algumas curiosidades." - def step_header(5, _), do: "Já temos tudo. Confirma se está tudo certo e verifica o teu email com o código que enviamos. A seguir, serás redirecionado para o pagamento." + def step_header(5, _), + do: + "Já temos tudo. Confirma se está tudo certo e verifica o teu email com o código que enviamos. A seguir, serás redirecionado para o pagamento." def step_header(_, _), do: "" @@ -151,9 +167,7 @@ defmodule PearlWeb.UserRegistrationLive do month = String.to_integer(m) case Date.new(year, month, 1) do - {:ok, date} -> 1..Date.days_in_month(date) - _ -> 1..31 end end @@ -161,9 +175,8 @@ defmodule PearlWeb.UserRegistrationLive do defp calculate_days_range(%{"month" => m}) when m != "" do month = String.to_integer(m) - case Date.new(Date.utc_today().year, month, 1) do + case Date.new(2024, month, 1) do {:ok, date} -> 1..Date.days_in_month(date) - _ -> 1..31 end end diff --git a/priv/repo/migrations/20251122152450_add_registration_fields_to_users.exs b/priv/repo/migrations/20251122152450_add_registration_fields_to_users.exs index 38d969d..7cced6a 100644 --- a/priv/repo/migrations/20251122152450_add_registration_fields_to_users.exs +++ b/priv/repo/migrations/20251122152450_add_registration_fields_to_users.exs @@ -3,7 +3,7 @@ defmodule Pearl.Repo.Migrations.AddRegistrationFieldsToUsers do def change do alter table(:users) do - add :ticket_type, :string + # add :ticket_type, :string TICKET POR IMPLEMENTAR add :notes, :string add :university, :string add :city, :string diff --git a/test/pearl_web/live/user_login_live_test.exs b/test/pearl_web/live/user_login_live_test.exs index b0e81b2..2666bc0 100644 --- a/test/pearl_web/live/user_login_live_test.exs +++ b/test/pearl_web/live/user_login_live_test.exs @@ -63,7 +63,7 @@ defmodule PearlWeb.UserLoginLiveTest do |> render_click() |> follow_redirect(conn, ~p"/users/register") - assert login_html =~ "Register" + assert login_html =~ "inscrição" end test "redirects to forgot password page when the Forgot Password button is clicked", %{ diff --git a/test/pearl_web/live/user_registration_live_test.exs b/test/pearl_web/live/user_registration_live_test.exs index 8a9d9e6..5232791 100644 --- a/test/pearl_web/live/user_registration_live_test.exs +++ b/test/pearl_web/live/user_registration_live_test.exs @@ -15,7 +15,7 @@ defmodule PearlWeb.UserRegistrationLiveTest do test "renders registration page", %{conn: conn} do {:ok, _lv, html} = live(conn, ~p"/users/register") - assert html =~ "Register" + assert html =~ "Inscrição" assert html =~ "Log in" end @@ -29,18 +29,21 @@ defmodule PearlWeb.UserRegistrationLiveTest do assert {:ok, _conn} = result end - test "renders errors for invalid data", %{conn: conn} do - {:ok, lv, _html} = live(conn, ~p"/users/register") + test "Registration page renders errors for invalid data", %{conn: conn} do + {:ok, lv, _html} = live(conn, ~p"/users/register") - result = - lv - |> element("#registration_form") - |> render_change(user: %{"email" => "with spaces", "password" => "too short"}) + lv + |> form("#registration_form", user: %{}) + |> render_submit() - assert result =~ "Register" - assert result =~ "must have the @ sign and no spaces" - assert result =~ "should be at least 12 character" - end + result = + lv + |> element("#registration_form") + |> render_change(user: %{"email" => "with spaces"}) + + assert result =~ "Inscrição" + assert result =~ "must have the @ sign and no spaces" + end end describe "registration navigation" do From d3eef54d404a1668ede0cb9f9cef8da002e23d11 Mon Sep 17 00:00:00 2001 From: GuilhermePSF Date: Mon, 15 Dec 2025 13:08:59 +0000 Subject: [PATCH 8/9] chore: format --- .../live/user_registration_live_test.exs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/test/pearl_web/live/user_registration_live_test.exs b/test/pearl_web/live/user_registration_live_test.exs index 5232791..4ca97ea 100644 --- a/test/pearl_web/live/user_registration_live_test.exs +++ b/test/pearl_web/live/user_registration_live_test.exs @@ -30,20 +30,20 @@ defmodule PearlWeb.UserRegistrationLiveTest do end test "Registration page renders errors for invalid data", %{conn: conn} do - {:ok, lv, _html} = live(conn, ~p"/users/register") - - lv - |> form("#registration_form", user: %{}) - |> render_submit() + {:ok, lv, _html} = live(conn, ~p"/users/register") - result = lv - |> element("#registration_form") - |> render_change(user: %{"email" => "with spaces"}) + |> form("#registration_form", user: %{}) + |> render_submit() - assert result =~ "Inscrição" - assert result =~ "must have the @ sign and no spaces" - end + result = + lv + |> element("#registration_form") + |> render_change(user: %{"email" => "with spaces"}) + + assert result =~ "Inscrição" + assert result =~ "must have the @ sign and no spaces" + end end describe "registration navigation" do From d2c75f48d93ac896a75390651e358d92f7fcf349 Mon Sep 17 00:00:00 2001 From: GuilhermePSF Date: Mon, 15 Dec 2025 13:23:20 +0000 Subject: [PATCH 9/9] =?UTF-8?q?chore:=20fix=20lint=20(=E2=95=AF=C2=B0?= =?UTF-8?q?=E2=96=A1=C2=B0)=E2=95=AF=EF=B8=B5=20=E2=94=BB=E2=94=81?= =?UTF-8?q?=E2=94=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/pearl_web/components/core_components.ex | 14 ++++++++------ .../components/registration_components.ex | 7 +++++++ lib/pearl_web/live/auth/user_registration_live.ex | 3 ++- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/lib/pearl_web/components/core_components.ex b/lib/pearl_web/components/core_components.ex index cf7d549..b366fe3 100644 --- a/lib/pearl_web/components/core_components.ex +++ b/lib/pearl_web/components/core_components.ex @@ -394,16 +394,18 @@ defmodule PearlWeb.CoreComponents do end defp input_border(:default, errors) do - cond do - errors == [] -> "border-transparent focus:border-primary" - true -> "border-danger-400 focus:border-danger-400" + if errors == [] do + "border-transparent focus:border-primary" + else + "border-danger-400 focus:border-danger-400" end end defp input_border(:flushed, errors) do - cond do - errors == [] -> "" - true -> "border-danger-500 focus:border-danger-500 text-danger-500 placeholder-danger-300" + if errors == [] do + "" + else + "border-danger-500 focus:border-danger-500 text-danger-500 placeholder-danger-300" end end diff --git a/lib/pearl_web/components/registration_components.ex b/lib/pearl_web/components/registration_components.ex index 329f8e1..f104696 100644 --- a/lib/pearl_web/components/registration_components.ex +++ b/lib/pearl_web/components/registration_components.ex @@ -1,4 +1,11 @@ defmodule PearlWeb.RegistrationComponents do + @moduledoc """ + Provides UI components specific to the user registration flow. + + This module includes the main layout for the registration page (`registration_layout`), + the progress stepper (`step_bar`), and other visual elements used exclusively + during the attendee sign-up flow. + """ use PearlWeb, :component import PearlWeb.Landing.Components.Navbar diff --git a/lib/pearl_web/live/auth/user_registration_live.ex b/lib/pearl_web/live/auth/user_registration_live.ex index b09087a..5e537fd 100644 --- a/lib/pearl_web/live/auth/user_registration_live.ex +++ b/lib/pearl_web/live/auth/user_registration_live.ex @@ -3,6 +3,7 @@ defmodule PearlWeb.UserRegistrationLive do alias Pearl.Accounts alias Pearl.Accounts.User + alias Phoenix.HTML.Form import PearlWeb.RegistrationComponents import PearlWeb.CoreComponents @@ -140,7 +141,7 @@ defmodule PearlWeb.UserRegistrationLive do def step_header(3, form) do first_name = - (Phoenix.HTML.Form.input_value(form, :name) || "") + (Form.input_value(form, :name) || "") |> String.split(" ", trim: true) |> List.first() |> Kernel.||("Participante")