fix: translation of login page
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
086ecdcb1b
commit
99a8d64344
18 changed files with 487 additions and 200 deletions
|
|
@ -3,52 +3,57 @@ defmodule MvWeb.AuthOverrides do
|
|||
UI customizations for AshAuthentication Phoenix components.
|
||||
|
||||
## Overrides
|
||||
- `SignIn` - Restricts form width to prevent full-width display
|
||||
- `Banner` - Replaces default logo with "Mitgliederverwaltung" text
|
||||
- `HorizontalRule` - Translates "or" text to German
|
||||
- `SignIn` - Restricts form width and hides the library banner (title is rendered in SignInLive)
|
||||
- `Banner` - Replaces default logo with text for reset/confirm pages
|
||||
- `Flash` - Hides library flash (we use flash_group in root layout)
|
||||
|
||||
## Documentation
|
||||
For complete reference on available overrides, see:
|
||||
https://hexdocs.pm/ash_authentication_phoenix/ui-overrides.html
|
||||
"""
|
||||
use AshAuthentication.Phoenix.Overrides
|
||||
use Gettext, backend: MvWeb.Gettext
|
||||
|
||||
# configure your UI overrides here
|
||||
|
||||
# First argument to `override` is the component name you are overriding.
|
||||
# The body contains any number of configurations you wish to override
|
||||
# Below are some examples
|
||||
|
||||
# For a complete reference, see https://hexdocs.pm/ash_authentication_phoenix/ui-overrides.html
|
||||
|
||||
# override AshAuthentication.Phoenix.Components.Banner do
|
||||
# set :image_url, "https://media.giphy.com/media/g7GKcSzwQfugw/giphy.gif"
|
||||
# set :text_class, "bg-red-500"
|
||||
# end
|
||||
|
||||
# Avoid full-width for the Sign In Form
|
||||
# Avoid full-width for the Sign In Form.
|
||||
# Banner is hidden because SignInLive renders its own locale-aware title.
|
||||
override AshAuthentication.Phoenix.Components.SignIn do
|
||||
set :root_class, "md:min-w-md"
|
||||
set :show_banner, false
|
||||
end
|
||||
|
||||
# Replace banner logo with text (no image in light or dark so link has discernible text)
|
||||
# Replace banner logo with text for reset/confirm pages (no image so link has discernible text).
|
||||
override AshAuthentication.Phoenix.Components.Banner do
|
||||
set :text, "Mitgliederverwaltung"
|
||||
set :image_url, nil
|
||||
set :dark_image_url, nil
|
||||
end
|
||||
|
||||
# Translate the "or" in the horizontal rule (between password form and SSO).
|
||||
# Uses auth domain so it respects the current locale (e.g. "oder" in German).
|
||||
override AshAuthentication.Phoenix.Components.HorizontalRule do
|
||||
set :text, dgettext("auth", "or")
|
||||
end
|
||||
|
||||
# Hide AshAuthentication's Flash component since we use flash_group in root layout
|
||||
# This prevents duplicate flash messages
|
||||
# Hide AshAuthentication's Flash component since we use flash_group in root layout.
|
||||
# This prevents duplicate flash messages.
|
||||
override AshAuthentication.Phoenix.Components.Flash do
|
||||
set :message_class_info, "hidden"
|
||||
set :message_class_error, "hidden"
|
||||
end
|
||||
end
|
||||
|
||||
defmodule MvWeb.AuthOverridesDE do
|
||||
@moduledoc """
|
||||
German locale-specific overrides for AshAuthentication Phoenix components.
|
||||
|
||||
Prepended to the overrides list in SignInLive when the locale is "de".
|
||||
Provides runtime-static German text for components that do not use
|
||||
the `_gettext` mechanism (e.g. HorizontalRule renders its text directly),
|
||||
and for submit buttons whose disable_text bypasses the POT extraction pipeline.
|
||||
"""
|
||||
use AshAuthentication.Phoenix.Overrides
|
||||
|
||||
# HorizontalRule renders text without `_gettext`, so we need a static German string.
|
||||
override AshAuthentication.Phoenix.Components.HorizontalRule do
|
||||
set :text, "oder"
|
||||
end
|
||||
|
||||
# Registering ... disable-text is passed through _gettext but "Registering ..."
|
||||
# has no dgettext source reference, so we supply the German string directly.
|
||||
override AshAuthentication.Phoenix.Components.Password.RegisterForm do
|
||||
set :disable_button_text, "Registrieren..."
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -13,6 +13,54 @@ defmodule MvWeb.Layouts do
|
|||
|
||||
embed_templates "layouts/*"
|
||||
|
||||
@doc """
|
||||
Renders the public (unauthenticated) page layout: header with logo + "Mitgliederverwaltung" left,
|
||||
club name centered, language selector right; plus main content and flash group. Use for sign-in, join, and join-confirm pages so they
|
||||
share the same chrome without the sidebar or authenticated layout logic.
|
||||
"""
|
||||
attr :flash, :map, required: true, doc: "the map of flash messages"
|
||||
slot :inner_block, required: true
|
||||
|
||||
def public_page(assigns) do
|
||||
club_name =
|
||||
case Mv.Membership.get_settings() do
|
||||
{:ok, s} -> s.club_name || "Mitgliederverwaltung"
|
||||
_ -> "Mitgliederverwaltung"
|
||||
end
|
||||
|
||||
assigns = assign(assigns, :club_name, club_name)
|
||||
|
||||
~H"""
|
||||
<header class="relative flex items-center justify-between p-4 border-b border-base-300 bg-base-100">
|
||||
<div class="flex items-center gap-3 shrink-0 min-w-0 max-w-[45%]">
|
||||
<img src={~p"/images/mila.svg"} alt="Mila Logo" class="size-8 shrink-0" />
|
||||
<span class="text-lg font-bold truncate">Mitgliederverwaltung</span>
|
||||
</div>
|
||||
<span class="absolute left-1/2 -translate-x-1/2 text-lg font-bold text-center max-w-[50%] truncate">
|
||||
{@club_name}
|
||||
</span>
|
||||
<form method="post" action={~p"/set_locale"} class="shrink-0">
|
||||
<input type="hidden" name="_csrf_token" value={Plug.CSRFProtection.get_csrf_token()} />
|
||||
<select
|
||||
name="locale"
|
||||
onchange="this.form.submit()"
|
||||
class="select select-sm focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
|
||||
aria-label={gettext("Select language")}
|
||||
>
|
||||
<option value="de" selected={Gettext.get_locale(MvWeb.Gettext) == "de"}>Deutsch</option>
|
||||
<option value="en" selected={Gettext.get_locale(MvWeb.Gettext) == "en"}>English</option>
|
||||
</select>
|
||||
</form>
|
||||
</header>
|
||||
<main class="px-4 py-8 sm:px-6">
|
||||
<div class="mx-auto max-full space-y-4">
|
||||
{render_slot(@inner_block)}
|
||||
</div>
|
||||
</main>
|
||||
<.flash_group flash={@flash} />
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders the app layout. Can be used with or without a current_user.
|
||||
When current_user is present, it will show the navigation bar.
|
||||
|
|
@ -99,10 +147,13 @@ defmodule MvWeb.Layouts do
|
|||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<!-- Unauthenticated: simple header (logo, club name, language selector; same classes as sidebar header) -->
|
||||
<header class="flex items-center gap-3 p-4 border-b border-base-300 bg-base-100">
|
||||
<img src={~p"/images/mila.svg"} alt="Mila Logo" class="size-8 shrink-0" />
|
||||
<span class="menu-label text-lg font-bold truncate flex-1">
|
||||
<!-- Unauthenticated: Option 3 header (logo + app name left, club name center, language selector right) -->
|
||||
<header class="relative flex items-center justify-between p-4 border-b border-base-300 bg-base-100">
|
||||
<div class="flex items-center gap-3 shrink-0 min-w-0 max-w-[45%]">
|
||||
<img src={~p"/images/mila.svg"} alt="Mila Logo" class="size-8 shrink-0" />
|
||||
<span class="menu-label text-lg font-bold truncate">Mitgliederverwaltung</span>
|
||||
</div>
|
||||
<span class="absolute left-1/2 -translate-x-1/2 text-lg font-bold text-center max-w-[50%] truncate">
|
||||
{@club_name}
|
||||
</span>
|
||||
<form method="post" action={~p"/set_locale"} class="shrink-0">
|
||||
|
|
@ -113,8 +164,8 @@ defmodule MvWeb.Layouts do
|
|||
class="select select-sm focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
|
||||
aria-label={gettext("Select language")}
|
||||
>
|
||||
<option value="de" selected={Gettext.get_locale() == "de"}>Deutsch</option>
|
||||
<option value="en" selected={Gettext.get_locale() == "en"}>English</option>
|
||||
<option value="de" selected={Gettext.get_locale(MvWeb.Gettext) == "de"}>Deutsch</option>
|
||||
<option value="en" selected={Gettext.get_locale(MvWeb.Gettext) == "en"}>English</option>
|
||||
</select>
|
||||
</form>
|
||||
</header>
|
||||
|
|
|
|||
|
|
@ -260,8 +260,8 @@ defmodule MvWeb.Layouts.Sidebar do
|
|||
class="select select-sm w-full focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
|
||||
aria-label={gettext("Select language")}
|
||||
>
|
||||
<option value="de" selected={Gettext.get_locale() == "de"}>Deutsch</option>
|
||||
<option value="en" selected={Gettext.get_locale() == "en"}>English</option>
|
||||
<option value="de" selected={Gettext.get_locale(MvWeb.Gettext) == "de"}>Deutsch</option>
|
||||
<option value="en" selected={Gettext.get_locale(MvWeb.Gettext) == "en"}>English</option>
|
||||
</select>
|
||||
</form>
|
||||
<!-- Theme Toggle (immer sichtbar) -->
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@ defmodule MvWeb.JoinConfirmController do
|
|||
@moduledoc """
|
||||
Handles GET /confirm_join/:token for the public join flow (double opt-in).
|
||||
|
||||
Calls a configurable callback (default Mv.Membership) so tests can stub the
|
||||
dependency. Public route; no authentication required.
|
||||
Renders a full HTML page with public header and hero layout (success, expired,
|
||||
or invalid). Calls a configurable callback (default Mv.Membership) so tests can
|
||||
stub the dependency. Public route; no authentication required.
|
||||
"""
|
||||
use MvWeb, :controller
|
||||
|
||||
|
|
@ -26,20 +27,36 @@ defmodule MvWeb.JoinConfirmController do
|
|||
|
||||
defp success_response(conn) do
|
||||
conn
|
||||
|> put_resp_content_type("text/html")
|
||||
|> send_resp(200, gettext("Thank you, we have received your request."))
|
||||
|> assign_confirm_assigns(:success)
|
||||
|> put_view(MvWeb.JoinConfirmHTML)
|
||||
|> render("confirm.html")
|
||||
end
|
||||
|
||||
defp expired_response(conn) do
|
||||
conn
|
||||
|> put_resp_content_type("text/html")
|
||||
|> send_resp(200, gettext("This link has expired. Please submit the form again."))
|
||||
|> assign_confirm_assigns(:expired)
|
||||
|> put_view(MvWeb.JoinConfirmHTML)
|
||||
|> render("confirm.html")
|
||||
end
|
||||
|
||||
defp invalid_response(conn) do
|
||||
conn
|
||||
|> put_resp_content_type("text/html")
|
||||
|> put_status(404)
|
||||
|> send_resp(404, gettext("Invalid or expired link."))
|
||||
|> assign_confirm_assigns(:invalid)
|
||||
|> put_view(MvWeb.JoinConfirmHTML)
|
||||
|> render("confirm.html")
|
||||
end
|
||||
|
||||
defp assign_confirm_assigns(conn, result) do
|
||||
club_name =
|
||||
case Mv.Membership.get_settings() do
|
||||
{:ok, settings} -> settings.club_name || "Mitgliederverwaltung"
|
||||
_ -> "Mitgliederverwaltung"
|
||||
end
|
||||
|
||||
conn
|
||||
|> assign(:result, result)
|
||||
|> assign(:club_name, club_name)
|
||||
|> assign(:csrf_token, Plug.CSRFProtection.get_csrf_token())
|
||||
end
|
||||
end
|
||||
|
|
|
|||
9
lib/mv_web/controllers/join_confirm_html.ex
Normal file
9
lib/mv_web/controllers/join_confirm_html.ex
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
defmodule MvWeb.JoinConfirmHTML do
|
||||
@moduledoc """
|
||||
Renders join confirmation result pages (success, expired, invalid) with
|
||||
public header and hero layout. Used by JoinConfirmController.
|
||||
"""
|
||||
use MvWeb, :html
|
||||
|
||||
embed_templates "join_confirm_html/*"
|
||||
end
|
||||
65
lib/mv_web/controllers/join_confirm_html/confirm.html.heex
Normal file
65
lib/mv_web/controllers/join_confirm_html/confirm.html.heex
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
<%!-- Public header (same structure as Layouts.app unauthenticated branch) --%>
|
||||
<header class="flex items-center gap-3 p-4 border-b border-base-300 bg-base-100">
|
||||
<img src={~p"/images/mila.svg"} alt="Mila Logo" class="size-8 shrink-0" />
|
||||
<span class="menu-label text-lg font-bold truncate flex-1">
|
||||
{@club_name}
|
||||
</span>
|
||||
<form method="post" action={~p"/set_locale"} class="shrink-0">
|
||||
<input type="hidden" name="_csrf_token" value={@csrf_token} />
|
||||
<select
|
||||
name="locale"
|
||||
onchange="this.form.submit()"
|
||||
class="select select-sm focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
|
||||
aria-label={gettext("Select language")}
|
||||
>
|
||||
<option value="de" selected={Gettext.get_locale() == "de"}>Deutsch</option>
|
||||
<option value="en" selected={Gettext.get_locale() == "en"}>English</option>
|
||||
</select>
|
||||
</form>
|
||||
</header>
|
||||
|
||||
<main class="px-4 py-8 sm:px-6">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<div class="hero min-h-[60vh] bg-base-200 rounded-lg">
|
||||
<div class="hero-content flex-col items-start text-left">
|
||||
<div class="max-w-md">
|
||||
<%= case @result do %>
|
||||
<% :success -> %>
|
||||
<h1 class="text-3xl font-bold">
|
||||
{gettext("Thank you")}
|
||||
</h1>
|
||||
<p class="py-4 text-base-content/80">
|
||||
{gettext("Thank you, we have received your request.")}
|
||||
</p>
|
||||
<p class="text-sm text-base-content/70">
|
||||
{gettext("You will receive an email once your application has been reviewed.")}
|
||||
</p>
|
||||
<a href={~p"/join"} class="btn btn-primary mt-4">
|
||||
{gettext("Back to join form")}
|
||||
</a>
|
||||
<% :expired -> %>
|
||||
<h1 class="text-3xl font-bold">
|
||||
{gettext("Link expired")}
|
||||
</h1>
|
||||
<p class="py-4 text-base-content/80">
|
||||
{gettext("This link has expired. Please submit the form again.")}
|
||||
</p>
|
||||
<a href={~p"/join"} class="btn btn-primary mt-4">
|
||||
{gettext("Submit new request")}
|
||||
</a>
|
||||
<% :invalid -> %>
|
||||
<h1 class="text-3xl font-bold text-error">
|
||||
{gettext("Invalid or expired link")}
|
||||
</h1>
|
||||
<p class="py-4 text-base-content/80">
|
||||
{gettext("Invalid or expired link.")}
|
||||
</p>
|
||||
<a href={~p"/join"} class="btn btn-primary mt-4">
|
||||
{gettext("Go to join form")}
|
||||
</a>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
|
@ -1,28 +1,42 @@
|
|||
defmodule MvWeb.SignInLive do
|
||||
@moduledoc """
|
||||
Custom sign-in page with language selector and conditional Single Sign-On button.
|
||||
Custom sign-in page with public header and hero layout (same as Join/Join Confirm).
|
||||
|
||||
- Renders a language selector (same pattern as LinkOidcAccountLive).
|
||||
- Wraps the default AshAuthentication SignIn component in a container with
|
||||
`data-oidc-configured` so that CSS can hide the SSO button when OIDC is not configured.
|
||||
Uses Layouts.public_page (no sidebar, no app-layout hooks). Wraps the AshAuthentication
|
||||
SignIn component in a hero section. Container has data-oidc-configured so CSS can hide
|
||||
the SSO button when OIDC is not configured.
|
||||
|
||||
Keeps `use Phoenix.LiveView` (not MvWeb :live_view) so AshAuthentication's sign_in_route
|
||||
live_session on_mount chain is not mixed with LiveHelpers hooks.
|
||||
|
||||
## Locale overrides
|
||||
`MvWeb.AuthOverridesDE` is prepended to the overrides list when the locale is "de",
|
||||
providing static German strings for components that do not use `_gettext` internally
|
||||
(e.g. HorizontalRule renders its `:text` override directly).
|
||||
"""
|
||||
use Phoenix.LiveView
|
||||
use Gettext, backend: MvWeb.Gettext
|
||||
|
||||
alias AshAuthentication.Phoenix.Components
|
||||
alias Mv.Config
|
||||
alias MvWeb.{AuthOverridesDE, Layouts}
|
||||
|
||||
@impl true
|
||||
def mount(_params, session, socket) do
|
||||
overrides =
|
||||
session
|
||||
|> Map.get("overrides", [AshAuthentication.Phoenix.Overrides.Default])
|
||||
|
||||
# Locale: same fallback as LiveUserAuth so config :default_locale (e.g. "en" in test) is respected
|
||||
locale =
|
||||
session["locale"] || Application.get_env(:mv, :default_locale, "de")
|
||||
locale = session["locale"] || Application.get_env(:mv, :default_locale, "de")
|
||||
|
||||
# Set both backend-specific and global locale so Gettext.get_locale/0 and
|
||||
# Gettext.get_locale/1 both return the correct value (important for the
|
||||
# language-selector `selected` attribute in Layouts.public_page).
|
||||
Gettext.put_locale(MvWeb.Gettext, locale)
|
||||
Gettext.put_locale(locale)
|
||||
|
||||
# Prepend DE-specific overrides when locale is German so that components
|
||||
# without _gettext support (e.g. HorizontalRule) still render in German.
|
||||
base_overrides = Map.get(session, "overrides", [AshAuthentication.Phoenix.Overrides.Default])
|
||||
locale_overrides = if locale == "de", do: [AuthOverridesDE], else: []
|
||||
overrides = locale_overrides ++ base_overrides
|
||||
|
||||
socket =
|
||||
socket
|
||||
|
|
@ -36,10 +50,9 @@ defmodule MvWeb.SignInLive do
|
|||
|> assign(:context, session["context"] || %{})
|
||||
|> assign(:auth_routes_prefix, session["auth_routes_prefix"])
|
||||
|> assign(:gettext_fn, session["gettext_fn"])
|
||||
|> assign(:live_action, :sign_in)
|
||||
|> assign_new(:live_action, fn -> :sign_in end)
|
||||
|> assign(:oidc_configured, Config.oidc_configured?())
|
||||
|> assign(:oidc_only, Config.oidc_only?())
|
||||
|> assign(:root_class, "grid h-screen place-items-center bg-base-100")
|
||||
|> assign(:sign_in_id, "sign-in")
|
||||
|> assign(:locale, locale)
|
||||
|
||||
|
|
@ -54,50 +67,43 @@ defmodule MvWeb.SignInLive do
|
|||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<main
|
||||
id="sign-in-page"
|
||||
role="main"
|
||||
class={@root_class}
|
||||
data-oidc-configured={to_string(@oidc_configured)}
|
||||
data-oidc-only={to_string(@oidc_only)}
|
||||
data-locale={@locale}
|
||||
>
|
||||
<h1 class="sr-only">{dgettext("auth", "Sign in")}</h1>
|
||||
<%!-- Language selector --%>
|
||||
<nav
|
||||
aria-label={dgettext("auth", "Language selection")}
|
||||
class="absolute top-4 right-4 flex justify-end z-10"
|
||||
>
|
||||
<form method="post" action="/set_locale" class="text-sm">
|
||||
<input type="hidden" name="_csrf_token" value={Plug.CSRFProtection.get_csrf_token()} />
|
||||
<select
|
||||
name="locale"
|
||||
onchange="this.form.submit()"
|
||||
class="select select-sm select-bordered bg-base-100"
|
||||
aria-label={dgettext("auth", "Select language")}
|
||||
>
|
||||
<option value="de" selected={@locale == "de"}>Deutsch</option>
|
||||
<option value="en" selected={@locale == "en"}>English</option>
|
||||
</select>
|
||||
</form>
|
||||
</nav>
|
||||
|
||||
<.live_component
|
||||
module={Components.SignIn}
|
||||
otp_app={@otp_app}
|
||||
live_action={@live_action}
|
||||
path={@path}
|
||||
auth_routes_prefix={@auth_routes_prefix}
|
||||
resources={@resources}
|
||||
reset_path={@reset_path}
|
||||
register_path={@register_path}
|
||||
id={@sign_in_id}
|
||||
overrides={@overrides}
|
||||
current_tenant={@current_tenant}
|
||||
context={@context}
|
||||
gettext_fn={@gettext_fn}
|
||||
/>
|
||||
</main>
|
||||
<Layouts.public_page flash={@flash}>
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<div
|
||||
class="hero min-h-[60vh] bg-base-200 rounded-lg"
|
||||
id="sign-in-page"
|
||||
role="main"
|
||||
data-oidc-configured={to_string(@oidc_configured)}
|
||||
data-oidc-only={to_string(@oidc_only)}
|
||||
data-locale={@locale}
|
||||
>
|
||||
<div class="hero-content flex-col items-start text-left">
|
||||
<div class="w-full max-w-md">
|
||||
<h1 class="text-xl font-semibold leading-8">
|
||||
{if @live_action == :register,
|
||||
do: dgettext("auth", "Register"),
|
||||
else: dgettext("auth", "Sign in")}
|
||||
</h1>
|
||||
<.live_component
|
||||
module={Components.SignIn}
|
||||
otp_app={@otp_app}
|
||||
live_action={@live_action}
|
||||
path={@path}
|
||||
auth_routes_prefix={@auth_routes_prefix}
|
||||
resources={@resources}
|
||||
reset_path={@reset_path}
|
||||
register_path={@register_path}
|
||||
id={@sign_in_id}
|
||||
overrides={@overrides}
|
||||
current_tenant={@current_tenant}
|
||||
context={@context}
|
||||
gettext_fn={@gettext_fn}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layouts.public_page>
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -33,91 +33,97 @@ defmodule MvWeb.JoinLive do
|
|||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<Layouts.app flash={@flash} current_user={@current_user}>
|
||||
<div class="max-w-xl mx-auto mt-8 space-y-6">
|
||||
<.header>
|
||||
{gettext("Become a member")}
|
||||
</.header>
|
||||
<Layouts.public_page flash={@flash}>
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<div class="hero min-h-[60vh] bg-base-200 rounded-lg">
|
||||
<div class="hero-content flex-col items-start text-left">
|
||||
<div class="max-w-xl w-full space-y-6">
|
||||
<.header>
|
||||
{gettext("Become a member")}
|
||||
</.header>
|
||||
|
||||
<p class="text-base-content/80">
|
||||
{gettext("Please enter your details for the membership application here.")}
|
||||
</p>
|
||||
<p class="text-base-content/80">
|
||||
{gettext("Please enter your details for the membership application here.")}
|
||||
</p>
|
||||
|
||||
<%= if @submitted do %>
|
||||
<div data-testid="join-success-message" class="alert alert-success">
|
||||
<p class="font-medium">
|
||||
{gettext(
|
||||
"We have saved your details. To complete your request, please click the link we sent to your email."
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<% else %>
|
||||
<.form
|
||||
for={@form}
|
||||
id="join-form"
|
||||
phx-submit="submit"
|
||||
class="space-y-4"
|
||||
>
|
||||
<%= if @rate_limit_error do %>
|
||||
<div class="alert alert-error">
|
||||
{@rate_limit_error}
|
||||
</div>
|
||||
<% end %>
|
||||
<%= if @submitted do %>
|
||||
<div data-testid="join-success-message" class="alert alert-success">
|
||||
<p class="font-medium">
|
||||
{gettext(
|
||||
"We have saved your details. To complete your request, please click the link we sent to your email."
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<% else %>
|
||||
<.form
|
||||
for={@form}
|
||||
id="join-form"
|
||||
phx-submit="submit"
|
||||
class="space-y-4"
|
||||
>
|
||||
<%= if @rate_limit_error do %>
|
||||
<div class="alert alert-error">
|
||||
{@rate_limit_error}
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= for field <- @join_fields do %>
|
||||
<div>
|
||||
<label for={"join-field-#{field.id}"} class="label">
|
||||
<span class="label-text">{field.label}{if field.required, do: " *"}</span>
|
||||
</label>
|
||||
<input
|
||||
type={input_type(field.id)}
|
||||
name={field.id}
|
||||
id={"join-field-#{field.id}"}
|
||||
value={@form.params[field.id]}
|
||||
required={field.required}
|
||||
class="input input-bordered w-full"
|
||||
/>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= for field <- @join_fields do %>
|
||||
<div>
|
||||
<label for={"join-field-#{field.id}"} class="label">
|
||||
<span class="label-text">{field.label}{if field.required, do: " *"}</span>
|
||||
</label>
|
||||
<input
|
||||
type={input_type(field.id)}
|
||||
name={field.id}
|
||||
id={"join-field-#{field.id}"}
|
||||
value={@form.params[field.id]}
|
||||
required={field.required}
|
||||
class="input input-bordered w-full"
|
||||
/>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%!--
|
||||
<%!--
|
||||
Honeypot (best practice): legit field name "website", type="text", no inline CSS,
|
||||
hidden via class in app.css (off-screen + 1px). tabindex=-1, autocomplete=off,
|
||||
aria-hidden so screen readers skip. If filled → silent failure (same success UI).
|
||||
--%>
|
||||
<div class="join-form-helper" aria-hidden="true">
|
||||
<label for="join-website" class="sr-only">{gettext("Website")}</label>
|
||||
<input
|
||||
type="text"
|
||||
name={@honeypot_field}
|
||||
id="join-website"
|
||||
tabindex="-1"
|
||||
autocomplete="off"
|
||||
class="join-form-helper-input"
|
||||
/>
|
||||
<div class="join-form-helper" aria-hidden="true">
|
||||
<label for="join-website" class="sr-only">{gettext("Website")}</label>
|
||||
<input
|
||||
type="text"
|
||||
name={@honeypot_field}
|
||||
id="join-website"
|
||||
tabindex="-1"
|
||||
autocomplete="off"
|
||||
class="join-form-helper-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-base-content/70">
|
||||
{gettext(
|
||||
"By submitting your application you will receive an email with a confirmation link. Once you have confirmed your email address, your application will be reviewed."
|
||||
)}
|
||||
</p>
|
||||
|
||||
<p class="text-xs text-base-content/60">
|
||||
{gettext(
|
||||
"Your details are only used to process your membership application and to contact you. To prevent abuse we also process technical data (e.g. IP address) only as necessary."
|
||||
)}
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
{gettext("Submit request")}
|
||||
</button>
|
||||
</div>
|
||||
</.form>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-base-content/70">
|
||||
{gettext(
|
||||
"By submitting your application you will receive an email with a confirmation link. Once you have confirmed your email address, your application will be reviewed."
|
||||
)}
|
||||
</p>
|
||||
|
||||
<p class="text-xs text-base-content/60">
|
||||
{gettext(
|
||||
"Your details are only used to process your membership application and to contact you. To prevent abuse we also process technical data (e.g. IP address) only as necessary."
|
||||
)}
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
{gettext("Submit request")}
|
||||
</button>
|
||||
</div>
|
||||
</.form>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layouts.app>
|
||||
</Layouts.public_page>
|
||||
"""
|
||||
end
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue