Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: registration flow #447

Merged
merged 20 commits into from
Jan 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
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
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
Loading