add link picker and validation to navigation editor
All checks were successful
deploy / deploy (push) Successful in 1m26s
All checks were successful
deploy / deploy (push) Successful in 1m26s
- replace freeform inputs with grouped dropdown (pages, custom pages, collections, external URL) - add inline URL validation for external links - add inline feedback component instead of flash messages - add dismiss-on-interaction pattern (feedback clears on changes) - add no-JS fallback via NavigationController - add DirtyGuard hook to warn before navigating away with unsaved changes - add no-JS fallbacks for settings forms (from address, signing secret) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
81
lib/berrypod_web/controllers/navigation_controller.ex
Normal file
81
lib/berrypod_web/controllers/navigation_controller.ex
Normal file
@@ -0,0 +1,81 @@
|
||||
defmodule BerrypodWeb.NavigationController do
|
||||
@moduledoc """
|
||||
No-JS fallback for navigation form submission.
|
||||
|
||||
With JS enabled, the LiveView handles everything. Without JS,
|
||||
the form POSTs here and we redirect back to the LiveView page.
|
||||
"""
|
||||
use BerrypodWeb, :controller
|
||||
|
||||
alias Berrypod.Settings
|
||||
|
||||
def save(conn, %{"header_nav" => header_json, "footer_nav" => footer_json}) do
|
||||
with {:ok, header_items} <- Jason.decode(header_json),
|
||||
{:ok, footer_items} <- Jason.decode(footer_json) do
|
||||
all_items = header_items ++ footer_items
|
||||
errors = validate_nav_items(all_items)
|
||||
|
||||
if errors == [] do
|
||||
Settings.put_setting("header_nav", header_items, "json")
|
||||
Settings.put_setting("footer_nav", footer_items, "json")
|
||||
|
||||
conn
|
||||
|> put_flash(:info, "Navigation saved")
|
||||
|> redirect(to: ~p"/admin/navigation")
|
||||
else
|
||||
conn
|
||||
|> put_flash(:error, Enum.join(errors, ". "))
|
||||
|> redirect(to: ~p"/admin/navigation")
|
||||
end
|
||||
else
|
||||
{:error, _} ->
|
||||
conn
|
||||
|> put_flash(:error, "Invalid navigation data")
|
||||
|> redirect(to: ~p"/admin/navigation")
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_nav_items(items) do
|
||||
items
|
||||
|> Enum.flat_map(fn item ->
|
||||
cond do
|
||||
item["label"] == "" || item["label"] == nil ->
|
||||
["All links need a label"]
|
||||
|
||||
item["href"] == "" || item["href"] == nil ->
|
||||
["\"#{item["label"]}\" needs a destination"]
|
||||
|
||||
item["external"] == true || is_external_url?(item["href"]) ->
|
||||
if valid_url?(item["href"]) do
|
||||
[]
|
||||
else
|
||||
["\"#{item["label"]}\" has an invalid URL"]
|
||||
end
|
||||
|
||||
true ->
|
||||
[]
|
||||
end
|
||||
end)
|
||||
|> Enum.uniq()
|
||||
end
|
||||
|
||||
defp valid_url?(url) when is_binary(url) do
|
||||
case URI.parse(url) do
|
||||
%URI{scheme: scheme, host: host}
|
||||
when scheme in ["http", "https"] and is_binary(host) and host != "" ->
|
||||
true
|
||||
|
||||
_ ->
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
defp valid_url?(_), do: false
|
||||
|
||||
defp is_external_url?(nil), do: false
|
||||
defp is_external_url?(""), do: false
|
||||
|
||||
defp is_external_url?(href) do
|
||||
String.starts_with?(href, "http://") || String.starts_with?(href, "https://")
|
||||
end
|
||||
end
|
||||
42
lib/berrypod_web/controllers/settings_controller.ex
Normal file
42
lib/berrypod_web/controllers/settings_controller.ex
Normal file
@@ -0,0 +1,42 @@
|
||||
defmodule BerrypodWeb.SettingsController do
|
||||
@moduledoc """
|
||||
No-JS fallback for settings form submissions.
|
||||
|
||||
With JS enabled, the LiveView handles everything. Without JS,
|
||||
the forms POST here and we redirect back to the LiveView page.
|
||||
"""
|
||||
use BerrypodWeb, :controller
|
||||
|
||||
alias Berrypod.Settings
|
||||
alias Berrypod.Stripe.Setup, as: StripeSetup
|
||||
|
||||
def update_from_address(conn, %{"from_address" => address}) do
|
||||
address = String.trim(address)
|
||||
|
||||
if address != "" do
|
||||
Settings.put_setting("email_from_address", address)
|
||||
|
||||
conn
|
||||
|> put_flash(:info, "From address saved")
|
||||
|> redirect(to: ~p"/admin/settings")
|
||||
else
|
||||
conn
|
||||
|> put_flash(:error, "From address can't be blank")
|
||||
|> redirect(to: ~p"/admin/settings")
|
||||
end
|
||||
end
|
||||
|
||||
def update_signing_secret(conn, %{"webhook" => %{"signing_secret" => secret}}) do
|
||||
if secret == "" do
|
||||
conn
|
||||
|> put_flash(:error, "Please enter a signing secret")
|
||||
|> redirect(to: ~p"/admin/settings")
|
||||
else
|
||||
StripeSetup.save_signing_secret(secret)
|
||||
|
||||
conn
|
||||
|> put_flash(:info, "Signing secret saved")
|
||||
|> redirect(to: ~p"/admin/settings")
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user