From bbc2f08215bbe8882a2c4b7c42b49a7637920ee5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?V=C3=ADctor=20Mart=C3=ADnez?=
<49537445+JasterV@users.noreply.github.com>
Date: Sun, 1 Feb 2026 19:30:58 +0100
Subject: [PATCH] feat: build an admin dashboard (#11)
* refactor: update button colors
* feat: add dashboard panel for admins only & update header styles
---
assets/css/app.css | 5 +
lib/spazio_solazzo/accounts/user.ex | 21 ++
.../components/admin_components.ex | 38 +++
.../components/core_components.ex | 23 +-
.../components/landing_components.ex | 14 +-
lib/spazio_solazzo_web/components/layouts.ex | 313 +++++++++---------
.../live/admin/dashboard_live.ex | 21 ++
.../live/admin/dashboard_live.html.heex | 28 ++
.../live/booking/asset_booking_live.html.heex | 10 +-
.../booking/booking_form_live_component.ex | 4 +-
.../live/page_live.html.heex | 4 +-
.../live/user/profile_live.html.heex | 11 +-
lib/spazio_solazzo_web/live_user_auth.ex | 10 +
lib/spazio_solazzo_web/router.ex | 7 +
.../20260118203753_add_role_to_users.exs | 21 ++
.../repo/users/20260118203753.json | 94 ++++++
16 files changed, 451 insertions(+), 173 deletions(-)
create mode 100644 lib/spazio_solazzo_web/components/admin_components.ex
create mode 100644 lib/spazio_solazzo_web/live/admin/dashboard_live.ex
create mode 100644 lib/spazio_solazzo_web/live/admin/dashboard_live.html.heex
create mode 100644 priv/repo/migrations/20260118203753_add_role_to_users.exs
create mode 100644 priv/resource_snapshots/repo/users/20260118203753.json
diff --git a/assets/css/app.css b/assets/css/app.css
index 465a4af..34f98a1 100644
--- a/assets/css/app.css
+++ b/assets/css/app.css
@@ -220,3 +220,8 @@ h6 {
.hover\:text-plum:hover {
color: var(--color-primary);
}
+
+/* Fix label-text color in dark mode */
+.label-text {
+ color: var(--color-base-content);
+}
diff --git a/lib/spazio_solazzo/accounts/user.ex b/lib/spazio_solazzo/accounts/user.ex
index 27af03c..aecf91b 100644
--- a/lib/spazio_solazzo/accounts/user.ex
+++ b/lib/spazio_solazzo/accounts/user.ex
@@ -135,6 +135,16 @@ defmodule SpazioSolazzo.Accounts.User do
end
end
+ # Make sure it is loaded in all read actions
+ preparations do
+ prepare build(load: [:is_admin])
+ end
+
+ # Make sure it is loaded in all write actions
+ changes do
+ change load(:is_admin)
+ end
+
attributes do
uuid_primary_key :id
@@ -152,6 +162,17 @@ defmodule SpazioSolazzo.Accounts.User do
allow_nil? true
public? true
end
+
+ attribute :role, :atom do
+ allow_nil? false
+ public? false
+ constraints one_of: [:customer, :admin]
+ default :customer
+ end
+ end
+
+ calculations do
+ calculate :is_admin, :boolean, expr(role == :admin)
end
identities do
diff --git a/lib/spazio_solazzo_web/components/admin_components.ex b/lib/spazio_solazzo_web/components/admin_components.ex
new file mode 100644
index 0000000..44a392c
--- /dev/null
+++ b/lib/spazio_solazzo_web/components/admin_components.ex
@@ -0,0 +1,38 @@
+defmodule SpazioSolazzoWeb.AdminComponents do
+ @moduledoc """
+ Reusable components for admin pages (dashboard & tools).
+ """
+ use Phoenix.Component
+
+ import SpazioSolazzoWeb.CoreComponents, only: [icon: 1, button: 1]
+ import Phoenix.Component
+
+ use Phoenix.VerifiedRoutes,
+ endpoint: SpazioSolazzoWeb.Endpoint,
+ router: SpazioSolazzoWeb.Router,
+ statics: SpazioSolazzoWeb.static_paths()
+
+ @doc """
+ Cards displayed for each tool available to admins
+ """
+ attr :id, :string, doc: "optional id for the tool"
+ attr :title, :string
+ attr :icon, :string, doc: "Icon used to represent the type of tool"
+ attr :description, :string
+
+ def tool_card(assigns) do
+ ~H"""
+
+
+
+ <.icon name={@icon} class="size-6 text-secondary" /> {@title}
+
+
{@description}
+
+ <.button class="btn btn-primary btn-sm">Open
+
+
+
+ """
+ end
+end
diff --git a/lib/spazio_solazzo_web/components/core_components.ex b/lib/spazio_solazzo_web/components/core_components.ex
index 984cf1d..5bdea1d 100644
--- a/lib/spazio_solazzo_web/components/core_components.ex
+++ b/lib/spazio_solazzo_web/components/core_components.ex
@@ -31,6 +31,26 @@ defmodule SpazioSolazzoWeb.CoreComponents do
alias Phoenix.LiveView.JS
+ @doc """
+ Renders a back-to navigation link
+ """
+
+ attr :navigate, :string
+ attr :value, :string
+ attr :rest, :global, doc: "the arbitrary HTML attributes to add to the flash container"
+
+ def back_to_link(assigns) do
+ ~H"""
+ <.link
+ class="mb-6 text-sm font-medium text-neutral hover:text-secondary transition-colors inline-flex items-center gap-2"
+ navigate={@navigate}
+ {@rest}
+ >
+ <.icon name="hero-arrow-left" class="w-5 h-5" />{@value}
+
+ """
+ end
+
@doc """
Renders flash notices.
@@ -223,7 +243,8 @@ defmodule SpazioSolazzoWeb.CoreComponents do
checked={@checked}
class={@class || "checkbox checkbox-sm"}
{@rest}
- />{@label}
+ />
+ {@label}
<.error :for={msg <- @errors}>{msg}
diff --git a/lib/spazio_solazzo_web/components/landing_components.ex b/lib/spazio_solazzo_web/components/landing_components.ex
index 1371817..4a132eb 100644
--- a/lib/spazio_solazzo_web/components/landing_components.ex
+++ b/lib/spazio_solazzo_web/components/landing_components.ex
@@ -4,7 +4,7 @@ defmodule SpazioSolazzoWeb.LandingComponents do
"""
use Phoenix.Component
- import SpazioSolazzoWeb.CoreComponents, only: [icon: 1]
+ import SpazioSolazzoWeb.CoreComponents, only: [icon: 1, back_to_link: 1]
import Phoenix.Component
use Phoenix.VerifiedRoutes,
@@ -131,14 +131,10 @@ defmodule SpazioSolazzoWeb.LandingComponents do
~H"""
-
- <.link
- navigate={~p"/"}
- class="hover:text-primary transition-colors flex items-center gap-1"
- >
- <.icon name="hero-arrow-left" class="w-4 h-4" /> Back to Home
-
-
+ <.back_to_link
+ navigate={~p"/"}
+ value="Back to Home"
+ />
diff --git a/lib/spazio_solazzo_web/components/layouts.ex b/lib/spazio_solazzo_web/components/layouts.ex
index 3513ccf..75961cd 100644
--- a/lib/spazio_solazzo_web/components/layouts.ex
+++ b/lib/spazio_solazzo_web/components/layouts.ex
@@ -38,8 +38,85 @@ defmodule SpazioSolazzoWeb.Layouts do
slot :inner_block, required: true
def app(assigns) do
+ current_year = Date.utc_today().year
+
+ assigns = assign(assigns, :current_year, current_year)
+
~H"""
- <.app_header current_user={@current_user} />
+
+
+ <.link navigate="/" class="flex items-center gap-3 hover:opacity-80 transition-opacity">
+
+
+
+
+
+
+
+ <.theme_toggle />
+
+ <%= if @current_user do %>
+ <%!-- Desktop Menu --%>
+
+ <.link
+ navigate={~p"/profile"}
+ class="btn btn-circle btn-outline text-secondary hover:bg-info/10"
+ aria-label="Profile"
+ >
+ <.icon name="hero-user" class="size-6" />
+
+
+ <% else %>
+ <.link navigate={~p"/sign-in"} id="sign-in-link" class="btn btn-secondary btn-sm">
+ Sign In
+
+ <% end %>
+
+ <%!-- Mobile menu --%>
+ <%= if @current_user do %>
+
+
+ <.menu_svg />
+
+
+ <%= if @current_user && @current_user.is_admin do %>
+
+ <.link navigate={~p"/admin/dashboard"}>
+ <.icon name="hero-squares-2x2" class="size-4" /> Dashboard
+
+
+ <% end %>
+
+
+ <.link navigate={~p"/profile"}>
+ <.icon name="hero-user" class="size-4" /> Profile
+
+
+
+ <.link href={~p"/sign-out"} id="mobile-sign-out-link" class="text-error">
+ <.icon name="hero-arrow-right-on-rectangle" class="size-4" /> Sign Out
+
+
+
+
+ <% end %>
+
+
{render_slot(@inner_block)}
@@ -47,156 +124,6 @@ defmodule SpazioSolazzoWeb.Layouts do
<.flash_group flash={@flash} />
- <.footer />
- """
- end
-
- @doc """
- Shows the flash group with standard titles and content.
-
- ## Examples
-
- <.flash_group flash={@flash} />
- """
- attr :flash, :map, required: true, doc: "the map of flash messages"
- attr :id, :string, default: "flash-group", doc: "the optional id of flash container"
-
- def flash_group(assigns) do
- ~H"""
-
- <.flash kind={:info} flash={@flash} />
- <.flash kind={:error} flash={@flash} />
-
- <.flash
- id="client-error"
- kind={:error}
- title={gettext("We can't find the internet")}
- phx-disconnected={show(".phx-client-error #client-error") |> JS.remove_attribute("hidden")}
- phx-connected={hide("#client-error") |> JS.set_attribute({"hidden", ""})}
- hidden
- >
- {gettext("Attempting to reconnect")}
- <.icon name="hero-arrow-path" class="ml-1 size-3 motion-safe:animate-spin" />
-
-
- <.flash
- id="server-error"
- kind={:error}
- title={gettext("Something went wrong!")}
- phx-disconnected={show(".phx-server-error #server-error") |> JS.remove_attribute("hidden")}
- phx-connected={hide("#server-error") |> JS.set_attribute({"hidden", ""})}
- hidden
- >
- {gettext("Attempting to reconnect")}
- <.icon name="hero-arrow-path" class="ml-1 size-3 motion-safe:animate-spin" />
-
-
- """
- end
-
- @doc """
- Provides dark vs light theme toggle based on themes defined in app.css.
-
- See in root.html.heex which applies the theme before page load.
- """
- def theme_toggle(assigns) do
- ~H"""
-
- <.icon name="hero-sun" class="size-5 [[data-theme=dark]_&]:hidden" />
- <.icon name="hero-moon" class="size-5 hidden [[data-theme=dark]_&]:block" />
-
- """
- end
-
- attr :current_user, :map,
- default: nil,
- doc: "the current authenticated user"
-
- defp app_header(assigns) do
- ~H"""
-
- """
- end
-
- defp footer(assigns) do
- current_year = Date.utc_today().year
-
- assigns = assign(assigns, :current_year, current_year)
-
- ~H"""