Skip to content

Commit ebec53a

Browse files
authored
feat: redesign auth UI with dark Frontman branding and client-side account settings (#357)
1 parent d582d48 commit ebec53a

22 files changed

Lines changed: 693 additions & 311 deletions

File tree

.changeset/redesign-auth-ui.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@frontman/client": patch
3+
---
4+
5+
Redesign authentication UI with dark Frontman branding. The server-side login page now features a dark theme with the Frontman logo and GitHub/Google OAuth buttons only (no email/password forms). Registration routes redirect to login. The root URL redirects to the sign-in page in dev and to frontman.sh in production. The client-side settings modal General tab now shows the logged-in user's email, avatar, and a sign-out button. The sign-out flow preserves a `return_to` URL so users are redirected back to the client app after re-authenticating.

apps/frontman_server/assets/js/app.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ window.addEventListener("phx:page-loading-stop", _info => topbar.hide())
4040
// connect if there are any LiveViews on the page
4141
liveSocket.connect()
4242

43+
// Auto-submit forms marked with data-auto-submit (used by the logout interstitial)
44+
document.querySelectorAll("form[data-auto-submit]").forEach(form => {
45+
form.requestSubmit()
46+
})
47+
4348
// expose liveSocket on window for web console debug logs and latency simulation:
4449
// >> liveSocket.enableDebug()
4550
// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session

apps/frontman_server/lib/frontman_server_web/components/core_components.ex

Lines changed: 66 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,13 @@ defmodule FrontmanServerWeb.CoreComponents do
4141
<.flash kind={:info} flash={@flash} />
4242
<.flash kind={:info} phx-mounted={show("#flash")}>Welcome Back!</.flash>
4343
"""
44-
attr :id, :string, doc: "the optional id of flash container"
45-
attr :flash, :map, default: %{}, doc: "the map of flash messages to display"
46-
attr :title, :string, default: nil
47-
attr :kind, :atom, values: [:info, :error], doc: "used for styling and flash lookup"
48-
attr :rest, :global, doc: "the arbitrary HTML attributes to add to the flash container"
44+
attr(:id, :string, doc: "the optional id of flash container")
45+
attr(:flash, :map, default: %{}, doc: "the map of flash messages to display")
46+
attr(:title, :string, default: nil)
47+
attr(:kind, :atom, values: [:info, :error], doc: "used for styling and flash lookup")
48+
attr(:rest, :global, doc: "the arbitrary HTML attributes to add to the flash container")
4949

50-
slot :inner_block, doc: "the optional inner block that renders the flash message"
50+
slot(:inner_block, doc: "the optional inner block that renders the flash message")
5151

5252
def flash(assigns) do
5353
assigns = assign_new(assigns, :id, fn -> "flash-#{assigns.kind}" end)
@@ -90,10 +90,10 @@ defmodule FrontmanServerWeb.CoreComponents do
9090
<.button phx-click="go" variant="primary">Send!</.button>
9191
<.button navigate={~p"/"}>Home</.button>
9292
"""
93-
attr :rest, :global, include: ~w(href navigate patch method download name value disabled)
94-
attr :class, :string
95-
attr :variant, :string, values: ~w(primary)
96-
slot :inner_block, required: true
93+
attr(:rest, :global, include: ~w(href navigate patch method download name value disabled))
94+
attr(:class, :string)
95+
attr(:variant, :string, values: ~w(primary))
96+
slot(:inner_block, required: true)
9797

9898
def button(%{rest: rest} = assigns) do
9999
variants = %{"primary" => "btn-primary", nil => "btn-primary btn-soft"}
@@ -144,30 +144,33 @@ defmodule FrontmanServerWeb.CoreComponents do
144144
<.input field={@form[:email]} type="email" />
145145
<.input name="my-input" errors={["oh no!"]} />
146146
"""
147-
attr :id, :any, default: nil
148-
attr :name, :any
149-
attr :label, :string, default: nil
150-
attr :value, :any
147+
attr(:id, :any, default: nil)
148+
attr(:name, :any)
149+
attr(:label, :string, default: nil)
150+
attr(:value, :any)
151151

152-
attr :type, :string,
152+
attr(:type, :string,
153153
default: "text",
154154
values: ~w(checkbox color date datetime-local email file month number password
155155
search select tel text textarea time url week)
156+
)
156157

157-
attr :field, Phoenix.HTML.FormField,
158+
attr(:field, Phoenix.HTML.FormField,
158159
doc: "a form field struct retrieved from the form, for example: @form[:email]"
160+
)
159161

160-
attr :errors, :list, default: []
161-
attr :checked, :boolean, doc: "the checked flag for checkbox inputs"
162-
attr :prompt, :string, default: nil, doc: "the prompt for select inputs"
163-
attr :options, :list, doc: "the options to pass to Phoenix.HTML.Form.options_for_select/2"
164-
attr :multiple, :boolean, default: false, doc: "the multiple flag for select inputs"
165-
attr :class, :string, default: nil, doc: "the input class to use over defaults"
166-
attr :error_class, :string, default: nil, doc: "the input error class to use over defaults"
162+
attr(:errors, :list, default: [])
163+
attr(:checked, :boolean, doc: "the checked flag for checkbox inputs")
164+
attr(:prompt, :string, default: nil, doc: "the prompt for select inputs")
165+
attr(:options, :list, doc: "the options to pass to Phoenix.HTML.Form.options_for_select/2")
166+
attr(:multiple, :boolean, default: false, doc: "the multiple flag for select inputs")
167+
attr(:class, :string, default: nil, doc: "the input class to use over defaults")
168+
attr(:error_class, :string, default: nil, doc: "the input error class to use over defaults")
167169

168-
attr :rest, :global,
170+
attr(:rest, :global,
169171
include: ~w(accept autocomplete capture cols disabled form list max maxlength min minlength
170172
multiple pattern placeholder readonly required rows size step)
173+
)
171174

172175
def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do
173176
errors = if Phoenix.Component.used_input?(field), do: field.errors, else: []
@@ -284,9 +287,9 @@ defmodule FrontmanServerWeb.CoreComponents do
284287
@doc """
285288
Renders a header with title.
286289
"""
287-
slot :inner_block, required: true
288-
slot :subtitle
289-
slot :actions
290+
slot(:inner_block, required: true)
291+
slot(:subtitle)
292+
slot(:actions)
290293

291294
def header(assigns) do
292295
~H"""
@@ -314,20 +317,21 @@ defmodule FrontmanServerWeb.CoreComponents do
314317
<:col :let={user} label="username">{user.username}</:col>
315318
</.table>
316319
"""
317-
attr :id, :string, required: true
318-
attr :rows, :list, required: true
319-
attr :row_id, :any, default: nil, doc: "the function for generating the row id"
320-
attr :row_click, :any, default: nil, doc: "the function for handling phx-click on each row"
320+
attr(:id, :string, required: true)
321+
attr(:rows, :list, required: true)
322+
attr(:row_id, :any, default: nil, doc: "the function for generating the row id")
323+
attr(:row_click, :any, default: nil, doc: "the function for handling phx-click on each row")
321324

322-
attr :row_item, :any,
325+
attr(:row_item, :any,
323326
default: &Function.identity/1,
324327
doc: "the function for mapping each row before calling the :col and :action slots"
328+
)
325329

326330
slot :col, required: true do
327-
attr :label, :string
331+
attr(:label, :string)
328332
end
329333

330-
slot :action, doc: "the slot for showing user actions in the last table column"
334+
slot(:action, doc: "the slot for showing user actions in the last table column")
331335

332336
def table(assigns) do
333337
assigns =
@@ -378,7 +382,7 @@ defmodule FrontmanServerWeb.CoreComponents do
378382
</.list>
379383
"""
380384
slot :item, required: true do
381-
attr :title, :string, required: true
385+
attr(:title, :string, required: true)
382386
end
383387

384388
def list(assigns) do
@@ -412,8 +416,8 @@ defmodule FrontmanServerWeb.CoreComponents do
412416
<.icon name="hero-x-mark" />
413417
<.icon name="hero-arrow-path" class="ml-1 size-3 motion-safe:animate-spin" />
414418
"""
415-
attr :name, :string, required: true
416-
attr :class, :string, default: "size-4"
419+
attr(:name, :string, required: true)
420+
attr(:class, :string, default: "size-4")
417421

418422
def icon(%{name: "hero-" <> _} = assigns) do
419423
~H"""
@@ -475,35 +479,49 @@ defmodule FrontmanServerWeb.CoreComponents do
475479
@doc """
476480
Renders a connected account row with connect/disconnect actions.
477481
"""
478-
attr :provider, :string, required: true
479-
attr :label, :string, required: true
480-
attr :identities, :list, required: true
482+
attr(:provider, :string, required: true)
483+
attr(:label, :string, required: true)
484+
attr(:identities, :list, required: true)
481485

482-
slot :inner_block, required: true, doc: "the icon slot"
486+
slot(:inner_block, required: true, doc: "the icon slot")
483487

484488
def connected_account(assigns) do
485489
identity = Enum.find(assigns.identities, &(&1.provider == assigns.provider))
486490
assigns = assign(assigns, :identity, identity)
487491

488492
~H"""
489-
<div class="flex items-center justify-between p-4 border border-base-300 rounded-lg">
493+
<div class="flex items-center justify-between px-5 py-4 border border-white/[0.08] bg-white/[0.02] rounded-lg">
490494
<div class="flex items-center gap-3">
491-
{render_slot(@inner_block)}
495+
<span class="text-white/70">{render_slot(@inner_block)}</span>
492496
<div>
493-
<p class="font-medium">{@label}</p>
494-
<p :if={@identity} class="text-sm text-base-content/70">{@identity.provider_email}</p>
495-
<p :if={!@identity} class="text-sm text-base-content/70">Not connected</p>
497+
<p class="text-sm font-medium text-white/90">{@label}</p>
498+
<p :if={@identity} class="text-xs text-white/50">{@identity.provider_email}</p>
499+
<p :if={!@identity} class="text-xs text-white/40">Not connected</p>
496500
</div>
497501
</div>
498502
<.link
499503
:if={@identity}
500504
href={~p"/auth/#{@provider}/unlink"}
501505
method="delete"
502-
class="btn btn-outline btn-sm"
506+
class={[
507+
"rounded-lg border border-white/[0.12] bg-white/[0.04]",
508+
"px-3 py-1.5 text-xs font-medium text-white/70",
509+
"transition-all duration-150",
510+
"hover:bg-red-500/10 hover:border-red-500/30 hover:text-red-400"
511+
]}
503512
>
504513
Disconnect
505514
</.link>
506-
<.link :if={!@identity} href={~p"/auth/#{@provider}/link"} class="btn btn-outline btn-sm">
515+
<.link
516+
:if={!@identity}
517+
href={~p"/auth/#{@provider}/link"}
518+
class={[
519+
"rounded-lg border border-white/[0.12] bg-white/[0.04]",
520+
"px-3 py-1.5 text-xs font-medium text-white/70",
521+
"transition-all duration-150",
522+
"hover:bg-white/[0.08] hover:border-white/[0.18] hover:text-white"
523+
]}
524+
>
507525
Connect
508526
</.link>
509527
</div>

apps/frontman_server/lib/frontman_server_web/components/layouts.ex

Lines changed: 28 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ defmodule FrontmanServerWeb.Layouts do
99
# The default root.html.heex file contains the HTML
1010
# skeleton of your application, namely HTML headers
1111
# and other static content.
12-
embed_templates "layouts/*"
12+
embed_templates("layouts/*")
1313

1414
@doc """
1515
Renders your app layout.
@@ -25,48 +25,39 @@ defmodule FrontmanServerWeb.Layouts do
2525
</Layouts.app>
2626
2727
"""
28-
attr :flash, :map, required: true, doc: "the map of flash messages"
28+
attr(:flash, :map, required: true, doc: "the map of flash messages")
2929

30-
attr :current_scope, :map,
30+
attr(:current_scope, :map,
3131
default: nil,
3232
doc: "the current [scope](https://hexdocs.pm/phoenix/scopes.html)"
33+
)
3334

34-
slot :inner_block, required: true
35+
slot(:inner_block, required: true)
3536

3637
def app(assigns) do
3738
~H"""
38-
<header class="navbar px-4 sm:px-6 lg:px-8">
39-
<div class="flex-1">
40-
<a href="/" class="flex-1 flex w-fit items-center gap-2">
41-
<img src={~p"/images/logo.svg"} width="36" />
42-
<span class="text-sm font-semibold">v{Application.spec(:phoenix, :vsn)}</span>
39+
<div class="min-h-screen flex flex-col bg-[#0a0a0a]">
40+
<header class="flex items-center justify-between px-6 py-4 border-b border-white/[0.08]">
41+
<a href="/" class="flex items-center gap-2.5 group">
42+
<img src={~p"/images/frontman-logo.svg"} width="28" height="28" alt="Frontman" />
43+
<span class="text-[15px] font-semibold text-white/90 tracking-tight group-hover:text-white transition-colors">
44+
Frontman
45+
</span>
4346
</a>
44-
</div>
45-
<div class="flex-none">
46-
<ul class="flex flex-column px-1 space-x-4 items-center">
47-
<li>
48-
<a href="https://phoenixframework.org/" class="btn btn-ghost">Website</a>
49-
</li>
50-
<li>
51-
<a href="https://github.com/phoenixframework/phoenix" class="btn btn-ghost">GitHub</a>
52-
</li>
53-
<li>
54-
<.theme_toggle />
55-
</li>
56-
<li>
57-
<a href="https://hexdocs.pm/phoenix/overview.html" class="btn btn-primary">
58-
Get Started <span aria-hidden="true">&rarr;</span>
59-
</a>
60-
</li>
61-
</ul>
62-
</div>
63-
</header>
64-
65-
<main class="px-4 py-20 sm:px-6 lg:px-8">
66-
<div class="mx-auto max-w-2xl space-y-4">
67-
{render_slot(@inner_block)}
68-
</div>
69-
</main>
47+
</header>
48+
49+
<main class="flex-1 flex items-center justify-center px-4 py-12">
50+
<div class="w-full max-w-sm space-y-6">
51+
{render_slot(@inner_block)}
52+
</div>
53+
</main>
54+
55+
<footer class="py-6 text-center">
56+
<p class="text-xs text-white/30">
57+
&copy; {DateTime.utc_now().year} Frontman. All rights reserved.
58+
</p>
59+
</footer>
60+
</div>
7061
7162
<.flash_group flash={@flash} />
7263
"""
@@ -79,8 +70,8 @@ defmodule FrontmanServerWeb.Layouts do
7970
8071
<.flash_group flash={@flash} />
8172
"""
82-
attr :flash, :map, required: true, doc: "the map of flash messages"
83-
attr :id, :string, default: "flash-group", doc: "the optional id of flash container"
73+
attr(:flash, :map, required: true, doc: "the map of flash messages")
74+
attr(:id, :string, default: "flash-group", doc: "the optional id of flash container")
8475

8576
def flash_group(assigns) do
8677
~H"""
@@ -114,41 +105,4 @@ defmodule FrontmanServerWeb.Layouts do
114105
</div>
115106
"""
116107
end
117-
118-
@doc """
119-
Provides dark vs light theme toggle based on themes defined in app.css.
120-
121-
See <head> in root.html.heex which applies the theme before page load.
122-
"""
123-
def theme_toggle(assigns) do
124-
~H"""
125-
<div class="card relative flex flex-row items-center border-2 border-base-300 bg-base-300 rounded-full">
126-
<div class="absolute w-1/3 h-full rounded-full border-1 border-base-200 bg-base-100 brightness-200 left-0 [[data-theme=light]_&]:left-1/3 [[data-theme=dark]_&]:left-2/3 transition-[left]" />
127-
128-
<button
129-
class="flex p-2 cursor-pointer w-1/3"
130-
phx-click={JS.dispatch("phx:set-theme")}
131-
data-phx-theme="system"
132-
>
133-
<.icon name="hero-computer-desktop-micro" class="size-4 opacity-75 hover:opacity-100" />
134-
</button>
135-
136-
<button
137-
class="flex p-2 cursor-pointer w-1/3"
138-
phx-click={JS.dispatch("phx:set-theme")}
139-
data-phx-theme="light"
140-
>
141-
<.icon name="hero-sun-micro" class="size-4 opacity-75 hover:opacity-100" />
142-
</button>
143-
144-
<button
145-
class="flex p-2 cursor-pointer w-1/3"
146-
phx-click={JS.dispatch("phx:set-theme")}
147-
data-phx-theme="dark"
148-
>
149-
<.icon name="hero-moon-micro" class="size-4 opacity-75 hover:opacity-100" />
150-
</button>
151-
</div>
152-
"""
153-
end
154108
end

0 commit comments

Comments
 (0)