Skip to content

Commit

Permalink
feat: registration flow (#447)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: João Lobo <[email protected]>
Co-authored-by: Nuno Miguel <[email protected]>
  • Loading branch information
3 people authored Jan 18, 2025
1 parent 1b79490 commit 44352d9
Show file tree
Hide file tree
Showing 41 changed files with 631 additions and 333 deletions.
4 changes: 3 additions & 1 deletion assets/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,16 @@ import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
import topbar from "../vendor/topbar"
import live_select from "live_select"
import { QrScanner, Wheel, Confetti, Countdown, Sorting } from "./hooks";
import { QrScanner, Wheel, Confetti, Countdown, Sorting, Redirect, CredentialScene } from "./hooks";

let Hooks = {
QrScanner: QrScanner,
Wheel: Wheel,
Confetti: Confetti,
Countdown: Countdown,
Sorting: Sorting,
Redirect: Redirect,
CredentialScene: CredentialScene,
...live_select
};

Expand Down
7 changes: 7 additions & 0 deletions assets/js/hooks/credential-scene.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import "../../vendor/credential-scene";

export const CredentialScene = {
mounted() {
window.initializeScene(this.el, this.el.dataset.attendee_name);
}
};
2 changes: 2 additions & 0 deletions assets/js/hooks/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ export { Wheel } from "./wheel.js";
export { Confetti } from "./confetti.js";
export { Sorting } from "./sorting.js";
export { Countdown } from "./countdown.js";
export { Redirect } from "./redirect.js";
export { CredentialScene } from "./credential-scene.js";
10 changes: 10 additions & 0 deletions assets/js/hooks/redirect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export const Redirect = {
mounted() {
this.handleEvent("redirect", data => {
console.log(data)
setTimeout(() => {
window.location.href = data.url;
}, data.time);
});
}
}
1 change: 1 addition & 0 deletions assets/vendor/credential-scene.js

Large diffs are not rendered by default.

23 changes: 18 additions & 5 deletions lib/safira/accounts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -201,16 +201,28 @@ defmodule Safira.Accounts do
## Examples
iex> register_attendee_user(%{field: value})
{:ok, %User{}}
{:ok, %{user: %User{}, attendee: %Attendee{}}}
iex> register_attendee_user(%{field: bad_value})
{:error, %Ecto.Changeset{}}
{:error, :struct, %Ecto.Changeset{}, %{}}
"""
def register_attendee_user(attrs) do
%User{}
|> User.registration_changeset(attrs |> Map.put(:type, :attendee))
|> Repo.insert()
Ecto.Multi.new()
|> Ecto.Multi.insert(
:user,
User.registration_changeset(%User{}, Map.delete(attrs, :attendee),
hash_password: true,
validate_email: true
)
)
|> Ecto.Multi.insert(
:attendee,
fn %{user: user} ->
Attendee.changeset(%Attendee{}, %{user_id: user.id})
end
)
|> Repo.transaction()
end

@doc """
Expand Down Expand Up @@ -260,6 +272,7 @@ defmodule Safira.Accounts do
"""
def change_user_registration(%User{} = user, attrs \\ %{}) do
User.registration_changeset(user, attrs, hash_password: false, validate_email: false)
|> User.password_confirmation_changeset(attrs)
end

## Settings
Expand Down
12 changes: 11 additions & 1 deletion lib/safira/accounts/user.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ defmodule Safira.Accounts.User do
alias Safira.Accounts.Staff

@required_fields ~w(name email handle password type)a
@optional_fields ~w(confirmed_at)a
@optional_fields ~w(confirmed_at allows_marketing)a

@derive {
Flop.Schema,
Expand Down Expand Up @@ -40,6 +40,7 @@ defmodule Safira.Accounts.User do
field :current_password, :string, virtual: true, redact: true
field :confirmed_at, :utc_datetime
field :type, Ecto.Enum, values: [:attendee, :staff], default: :attendee
field :allows_marketing, :boolean, default: false

has_one :attendee, Attendee, on_delete: :delete_all
has_one :staff, Staff, on_delete: :delete_all
Expand Down Expand Up @@ -77,6 +78,7 @@ defmodule Safira.Accounts.User do
|> validate_email(opts)
|> validate_handle()
|> validate_password(opts)
|> cast_assoc(:attendee, with: &Attendee.changeset/2)
end

defp validate_email(changeset, opts) do
Expand Down Expand Up @@ -105,6 +107,8 @@ defmodule Safira.Accounts.User do
|> validate_format(:handle, ~r/^[a-z0-9_]+$/,
message: "can only contain lowercase letters, numbers, and underscores"
)
|> unsafe_validate_unique(:handle, Safira.Repo)
|> unique_constraint(:handle)
end

defp maybe_hash_password(changeset, opts) do
Expand Down Expand Up @@ -168,6 +172,12 @@ defmodule Safira.Accounts.User do
|> validate_password(opts)
end

def password_confirmation_changeset(user, attrs) do
user
|> cast(attrs, [:password])
|> validate_confirmation(:password, message: "does not match password")
end

@doc """
Confirms the account by setting `confirmed_at`.
"""
Expand Down
2 changes: 1 addition & 1 deletion lib/safira_web.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ defmodule SafiraWeb do
those modules here.
"""

def static_paths, do: ~w(assets fonts images docs favicon.ico robots.txt)
def static_paths, do: ~w(assets docs fonts images models favicon.ico robots.txt)

def router do
quote do
Expand Down
51 changes: 43 additions & 8 deletions lib/safira_web/components/core_components.ex
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ defmodule SafiraWeb.CoreComponents do
attr :type, :string,
default: "text",
values: ~w(checkbox color date datetime-local email file month number password
range search select tel text textarea time url week)
range search select tel text textarea time url week handle)

attr :field, Phoenix.HTML.FormField,
doc: "a form field struct retrieved from the form, for example: @form[:email]"
Expand All @@ -295,6 +295,9 @@ defmodule SafiraWeb.CoreComponents do
include: ~w(accept autocomplete capture cols disabled form list max maxlength min minlength
multiple pattern placeholder readonly required rows size step)

attr :wrapper_class, :string, default: ""
attr :class, :string, default: ""

slot :inner_block

def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do
Expand All @@ -313,7 +316,7 @@ defmodule SafiraWeb.CoreComponents do
end)

~H"""
<div phx-feedback-for={@name}>
<div phx-feedback-for={@name} class={@wrapper_class}>
<label class="flex items-center gap-4 text-sm leading-6 text-zinc-600">
<input type="hidden" name={@name} value="false" />
<input
Expand All @@ -322,7 +325,7 @@ defmodule SafiraWeb.CoreComponents do
name={@name}
value="true"
checked={@checked}
class="rounded border-zinc-300 text-zinc-900 focus:ring-0"
class={"rounded border-zinc-300 text-accent focus:ring-0 #{@class}"}
{@rest}
/>
<%= @label %>
Expand All @@ -334,12 +337,12 @@ defmodule SafiraWeb.CoreComponents do

def input(%{type: "select"} = assigns) do
~H"""
<div phx-feedback-for={@name}>
<div phx-feedback-for={@name} class={@wrapper_class}>
<.label for={@id}><%= @label %></.label>
<select
id={@id}
name={@name}
class="mt-2 block w-full rounded-md border border-gray-300 bg-white shadow-sm focus:border-zinc-400 focus:ring-0 sm:text-sm"
class={"mt-2 block w-full rounded-md border border-gray-300 bg-white shadow-sm focus:border-zinc-400 focus:ring-0 sm:text-sm #{@class}"}
multiple={@multiple}
{@rest}
>
Expand All @@ -353,14 +356,15 @@ defmodule SafiraWeb.CoreComponents do

def input(%{type: "textarea"} = assigns) do
~H"""
<div phx-feedback-for={@name}>
<div phx-feedback-for={@name} class={@wrapper_class}>
<.label for={@id}><%= @label %></.label>
<textarea
id={@id}
name={@name}
class={[
"mt-2 block w-full rounded-lg text-zinc-900 focus:ring-0 sm:text-sm sm:leading-6",
"min-h-[6rem] phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400",
@class,
@errors == [] && "border-zinc-300 focus:border-zinc-400",
@errors != [] && "border-rose-400 focus:border-rose-400"
]}
Expand All @@ -371,10 +375,39 @@ defmodule SafiraWeb.CoreComponents do
"""
end

def input(%{type: "handle"} = assigns) do
~H"""
<div phx-feedback-for={@name} class={@wrapper_class}>
<.label for={@id}><%= @label %></.label>
<div class={[
"mt-2 flex bg-white w-full rounded-lg text-zinc-900 focus:ring-0 sm:text-sm sm:leading-6",
"phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400 select-none",
@class,
@errors == [] && "border-zinc-300 focus:border-zinc-400",
@errors != [] && "border-rose-400 focus:border-rose-400"
]}>
<span class="pl-3 self-center text-zinc-600 border-r pr-2 mr-3 border-zinc-400/50">@</span>
<input
type={@type}
name={@name}
id={@id}
value={Phoenix.HTML.Form.normalize_value(@type, @value)}
class={[
"block w-full border-none focus:outline-none focus:ring-transparent mr-4 text-zinc-900 sm:text-sm sm:leading-6 border-0 ring-0 py-[0.5rem]",
@class
]}
{@rest}
/>
</div>
<.error :for={msg <- @errors}><%= msg %></.error>
</div>
"""
end

# All other inputs text, datetime-local, url, password, etc. are handled here...
def input(assigns) do
~H"""
<div phx-feedback-for={@name}>
<div phx-feedback-for={@name} class={@wrapper_class}>
<.label for={@id}><%= @label %></.label>
<input
type={@type}
Expand All @@ -384,6 +417,7 @@ defmodule SafiraWeb.CoreComponents do
class={[
"mt-2 block w-full rounded-lg text-zinc-900 focus:ring-0 sm:text-sm sm:leading-6",
"phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400",
@class,
@errors == [] && "border-zinc-300 focus:border-zinc-400",
@errors != [] && "border-rose-400 focus:border-rose-400"
]}
Expand All @@ -398,11 +432,12 @@ defmodule SafiraWeb.CoreComponents do
Renders a label.
"""
attr :for, :string, default: nil
attr :class, :string, default: ""
slot :inner_block, required: true

def label(assigns) do
~H"""
<label for={@for} class="block text-sm font-semibold leading-6 dark:text-white">
<label for={@for} class={"block text-sm font-semibold leading-6 dark:text-white #{@class}"}>
<%= render_slot(@inner_block) %>
</label>
"""
Expand Down
40 changes: 22 additions & 18 deletions lib/safira_web/components/layouts/app.html.heex
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<div class="relative h-screen flex overflow-hidden">
<%= if Map.get(assigns, :event_started, true) do %>
<body class="bg-primary">
<div class="relative h-screen flex overflow-hidden">
<.sidebar
:if={Map.get(assigns, :event_started, true)}
current_user={@current_user}
pages={SafiraWeb.Config.app_pages()}
current_page={Map.get(assigns, :current_page, nil)}
Expand All @@ -16,22 +17,25 @@
link_active_class="bg-light text-primary"
link_inactive_class="hover:bg-primary-500/10 text-light"
/>
<% end %>
<div class="flex flex-col flex-1 overflow-hidden">
<div class="bg-primary flex justify-end lg:hidden px-4 sm:px-6 py-2">
<button
class="sidebar-toggle flex items-center justify-center w-16 dark:text-light text-dark"
aria-expanded="false"
phx-click={show_mobile_sidebar()}
<div class="flex flex-col flex-1 overflow-hidden">
<div
:if={Map.get(assigns, :event_started, true)}
class="flex justify-end lg:hidden px-4 sm:px-6 py-2"
>
<.icon class="w-8 h-8" name="hero-bars-3" />
</button>
</div>
<main class="text-light bg-primary flex-1 relative z-0 overflow-y-auto focus:outline-none">
<div class="px-6 sm:px-6 lg:px-8 py-8">
<.flash_group flash={@flash} />
<%= @inner_content %>
<button
class="sidebar-toggle flex items-center justify-center w-16 text-light"
aria-expanded="false"
phx-click={show_mobile_sidebar()}
>
<.icon class="w-8 h-8" name="hero-bars-3" />
</button>
</div>
</main>
<main class="text-light flex-1 relative z-0 overflow-y-auto focus:outline-none font-iregular">
<div class="px-6 sm:px-6 lg:px-8 py-8">
<.flash_group flash={@flash} />
<%= @inner_content %>
</div>
</main>
</div>
</div>
</div>
</body>
66 changes: 34 additions & 32 deletions lib/safira_web/components/layouts/backoffice.html.heex
Original file line number Diff line number Diff line change
@@ -1,34 +1,36 @@
<div class="relative h-screen flex overflow-hidden">
<.sidebar
current_user={@current_user}
pages={SafiraWeb.Config.backoffice_pages(@current_user)}
current_page={Map.get(assigns, :current_page, nil)}
logo_images={%{light: "/images/safira-light.svg", dark: "/images/safira-dark.svg"}}
background="bg-light dark:bg-dark"
border="border-lightShade dark:border-darkShade"
logo_padding="px-8 pt-8 pb-4"
user_dropdown_name_color="text-dark dark:text-light"
user_dropdown_handle_color="text-darkMuted dark:text-lightMuted"
user_dropdown_icon_color="text-lightShade dark:text-darkShade"
link_class="px-3 dark:hover:bg-darkShade group flex items-center py-2 text-sm font-medium rounded-md transition-colors"
link_active_class="bg-dark text-light hover:bg-darkShade dark:bg-darkShade"
link_inactive_class="text-dark hover:bg-lightShade/40 dark:text-light"
/>
<div class="flex flex-col flex-1 overflow-hidden">
<div class="bg-light dark:bg-dark flex justify-end lg:hidden px-4 sm:px-6 py-2">
<button
class="sidebar-toggle flex items-center justify-center w-16 dark:text-light text-dark"
aria-expanded="false"
phx-click={show_mobile_sidebar()}
>
<.icon class="w-8 h-8" name="hero-bars-3" />
</button>
</div>
<main class="bg-light dark:bg-dark text-dark dark:text-light flex-1 relative z-0 overflow-y-auto focus:outline-none">
<div class="px-4 sm:px-6 lg:px-8 py-8">
<.flash_group flash={@flash} />
<%= @inner_content %>
<body>
<div class="relative h-screen flex overflow-hidden">
<.sidebar
current_user={@current_user}
pages={SafiraWeb.Config.backoffice_pages(@current_user)}
current_page={Map.get(assigns, :current_page, nil)}
logo_images={%{light: "/images/safira-light.svg", dark: "/images/safira-dark.svg"}}
background="bg-light dark:bg-dark"
border="border-lightShade dark:border-darkShade"
logo_padding="px-8 pt-8 pb-4"
user_dropdown_name_color="text-dark dark:text-light"
user_dropdown_handle_color="text-darkMuted dark:text-lightMuted"
user_dropdown_icon_color="text-lightShade dark:text-darkShade"
link_class="px-3 dark:hover:bg-darkShade group flex items-center py-2 text-sm font-medium rounded-md transition-colors"
link_active_class="bg-dark text-light hover:bg-darkShade dark:bg-darkShade"
link_inactive_class="text-dark hover:bg-lightShade/40 dark:text-light"
/>
<div class="flex flex-col flex-1 overflow-hidden">
<div class="bg-light dark:bg-dark flex justify-end lg:hidden px-4 sm:px-6 py-2">
<button
class="sidebar-toggle flex items-center justify-center w-16 dark:text-light text-dark"
aria-expanded="false"
phx-click={show_mobile_sidebar()}
>
<.icon class="w-8 h-8" name="hero-bars-3" />
</button>
</div>
</main>
<main class="bg-light dark:bg-dark text-dark dark:text-light flex-1 relative z-0 overflow-y-auto focus:outline-none">
<div class="px-4 sm:px-6 lg:px-8 py-8">
<.flash_group flash={@flash} />
<%= @inner_content %>
</div>
</main>
</div>
</div>
</div>
</body>
Loading

0 comments on commit 44352d9

Please sign in to comment.