add no-JS contact form and noscript banner
All checks were successful
deploy / deploy (push) Successful in 1m21s

Wire up the contact form with action/method/name attrs so it works
without JavaScript. Add ContactNotifier, ContactController, and a
noscript info banner in the shop root layout.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jamey 2026-02-28 18:57:51 +00:00
parent af069c2bca
commit ca01f43d70
8 changed files with 256 additions and 8 deletions

View File

@ -0,0 +1,61 @@
defmodule Berrypod.ContactNotifier do
@moduledoc """
Sends contact form submissions to the shop owner.
"""
import Swoosh.Email
alias Berrypod.{Mailer, Settings}
require Logger
@doc """
Delivers a contact form message to the shop owner's email.
Expects a map with "name", "email", and "message" keys (string keys,
as received from form params). "subject" is optional.
"""
def deliver_contact_message(%{"name" => name, "email" => email, "message" => message} = params)
when is_binary(name) and name != "" and is_binary(email) and email != "" and
is_binary(message) and message != "" do
subject =
if params["subject"] in [nil, ""], do: "Contact form message", else: params["subject"]
shop_name = Settings.get_setting("shop_name", "Berrypod")
from_address = Settings.get_setting("email_from_address", "contact@example.com")
to_address = Settings.get_setting("contact_email") || from_address
body = """
==============================
New message from your #{shop_name} contact form
Name: #{name}
Email: #{email}
Subject: #{subject}
#{message}
==============================
"""
email_msg =
new()
|> to(to_address)
|> from({shop_name, from_address})
|> reply_to(email)
|> subject("[#{shop_name}] #{subject}")
|> text_body(body)
case Mailer.deliver(email_msg) do
{:ok, _metadata} = result ->
result
{:error, reason} = error ->
Logger.warning("Failed to send contact form email: #{inspect(reason)}")
error
end
end
def deliver_contact_message(_), do: {:error, :invalid_params}
end

View File

@ -74,6 +74,11 @@
</style> </style>
</head> </head>
<body> <body>
<noscript>
<div style="background: #fef3c7; color: #92400e; padding: 0.75rem 1rem; text-align: center; font-size: 0.875rem;">
This shop works without JavaScript, but some features work better with it enabled.
</div>
</noscript>
<div <div
class="themed shop-root" class="themed shop-root"
data-mood={@theme_settings.mood} data-mood={@theme_settings.mood}

View File

@ -100,29 +100,31 @@ defmodule BerrypodWeb.ShopComponents.Content do
<div class="contact-form-spacer"></div> <div class="contact-form-spacer"></div>
<% end %> <% end %>
<form class="contact-form"> <form action="/contact/send" method="post" phx-submit="send_contact" class="contact-form">
<input type="hidden" name="_csrf_token" value={Phoenix.Controller.get_csrf_token()} />
<div> <div>
<label class="contact-form-label">Name</label> <label class="contact-form-label">Name</label>
<.shop_input type="text" placeholder="Your name" /> <.shop_input type="text" name="name" placeholder="Your name" required />
</div> </div>
<div> <div>
<label class="contact-form-label">Email</label> <label class="contact-form-label">Email</label>
<.shop_input type="email" placeholder="your@email.com" /> <.shop_input type="email" name="email" placeholder="your@email.com" required />
</div> </div>
<div> <div>
<label class="contact-form-label">Subject</label> <label class="contact-form-label">Subject</label>
<.shop_input type="text" placeholder="How can I help?" /> <.shop_input type="text" name="subject" placeholder="How can I help?" />
</div> </div>
<div> <div>
<label class="contact-form-label">Message</label> <label class="contact-form-label">Message</label>
<.shop_textarea rows="5" placeholder="Your message..." /> <.shop_textarea name="message" rows="5" placeholder="Your message..." required />
</div> </div>
<.shop_button type="submit" class="contact-form-submit"> <.shop_button type="submit" class="contact-form-submit">
Send Message Send message
</.shop_button> </.shop_button>
</form> </form>
</.shop_card> </.shop_card>

View File

@ -0,0 +1,27 @@
defmodule BerrypodWeb.ContactController do
use BerrypodWeb, :controller
alias Berrypod.ContactNotifier
@doc """
Handles contact form submission (no-JS fallback).
"""
def create(conn, params) do
case ContactNotifier.deliver_contact_message(params) do
{:ok, _} ->
conn
|> put_flash(:info, "Message sent! We'll get back to you soon.")
|> redirect(to: ~p"/contact")
{:error, :invalid_params} ->
conn
|> put_flash(:error, "Please fill in all required fields.")
|> redirect(to: ~p"/contact")
{:error, _} ->
conn
|> put_flash(:error, "Sorry, something went wrong. Please try again.")
|> redirect(to: ~p"/contact")
end
end
end

View File

@ -1,7 +1,7 @@
defmodule BerrypodWeb.Shop.Contact do defmodule BerrypodWeb.Shop.Contact do
use BerrypodWeb, :live_view use BerrypodWeb, :live_view
alias Berrypod.Orders alias Berrypod.{ContactNotifier, Orders}
alias Berrypod.Orders.OrderNotifier alias Berrypod.Orders.OrderNotifier
alias Berrypod.Pages alias Berrypod.Pages
alias BerrypodWeb.OrderLookupController alias BerrypodWeb.OrderLookupController
@ -39,6 +39,23 @@ defmodule BerrypodWeb.Shop.Contact do
{:noreply, assign(socket, :tracking_state, state)} {:noreply, assign(socket, :tracking_state, state)}
end end
@impl true
def handle_event("send_contact", params, socket) do
case ContactNotifier.deliver_contact_message(params) do
{:ok, _} ->
{:noreply,
socket
|> put_flash(:info, "Message sent! We'll get back to you soon.")
|> push_navigate(to: ~p"/contact")}
{:error, :invalid_params} ->
{:noreply, put_flash(socket, :error, "Please fill in all required fields.")}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Sorry, something went wrong. Please try again.")}
end
end
@impl true @impl true
def handle_event("reset_tracking", _params, socket) do def handle_event("reset_tracking", _params, socket) do
{:noreply, assign(socket, :tracking_state, :idle)} {:noreply, assign(socket, :tracking_state, :idle)}

View File

@ -270,7 +270,8 @@ defmodule BerrypodWeb.Router do
# Checkout (POST — creates Stripe session and redirects) # Checkout (POST — creates Stripe session and redirects)
post "/checkout", CheckoutController, :create post "/checkout", CheckoutController, :create
# Order lookup (no-JS fallback for contact page form) # Contact form + order lookup (no-JS fallbacks for contact page forms)
post "/contact/send", ContactController, :create
post "/contact/lookup", OrderLookupController, :lookup post "/contact/lookup", OrderLookupController, :lookup
# Cart form actions (no-JS fallbacks for LiveView cart events) # Cart form actions (no-JS fallbacks for LiveView cart events)

View File

@ -0,0 +1,87 @@
defmodule Berrypod.ContactNotifierTest do
use Berrypod.DataCase, async: true
import Swoosh.TestAssertions
alias Berrypod.ContactNotifier
alias Berrypod.Settings
describe "deliver_contact_message/1" do
test "sends email with valid params" do
assert {:ok, _} =
ContactNotifier.deliver_contact_message(%{
"name" => "Jo Bloggs",
"email" => "jo@example.com",
"subject" => "Question about prints",
"message" => "Do you ship to Mars?"
})
assert_email_sent(fn email ->
assert email.subject =~ "Question about prints"
assert email.text_body =~ "Jo Bloggs"
assert email.text_body =~ "jo@example.com"
assert email.text_body =~ "Do you ship to Mars?"
assert {"", "jo@example.com"} = email.reply_to
end)
end
test "sends to contact_email when set" do
Settings.put_setting("contact_email", "shop@example.com")
assert {:ok, _} =
ContactNotifier.deliver_contact_message(%{
"name" => "Test",
"email" => "test@example.com",
"message" => "Hello"
})
assert_email_sent(fn email ->
assert [{"", "shop@example.com"}] = email.to
end)
end
test "uses default subject when not provided" do
assert {:ok, _} =
ContactNotifier.deliver_contact_message(%{
"name" => "Test",
"email" => "test@example.com",
"message" => "Hello"
})
assert_email_sent(fn email ->
assert email.subject =~ "Contact form message"
end)
end
test "returns error for missing name" do
assert {:error, :invalid_params} =
ContactNotifier.deliver_contact_message(%{
"name" => "",
"email" => "test@example.com",
"message" => "Hello"
})
end
test "returns error for missing email" do
assert {:error, :invalid_params} =
ContactNotifier.deliver_contact_message(%{
"name" => "Test",
"email" => "",
"message" => "Hello"
})
end
test "returns error for missing message" do
assert {:error, :invalid_params} =
ContactNotifier.deliver_contact_message(%{
"name" => "Test",
"email" => "test@example.com",
"message" => ""
})
end
test "returns error for missing keys" do
assert {:error, :invalid_params} = ContactNotifier.deliver_contact_message(%{})
end
end
end

View File

@ -0,0 +1,48 @@
defmodule BerrypodWeb.ContactControllerTest do
use BerrypodWeb.ConnCase, async: false
import Berrypod.AccountsFixtures
import Swoosh.TestAssertions
setup do
user_fixture()
{:ok, _} = Berrypod.Settings.set_site_live(true)
# Clear confirmation email from user_fixture
Swoosh.TestAssertions.assert_email_sent()
:ok
end
describe "POST /contact/send" do
test "sends email and redirects with success flash", %{conn: conn} do
conn =
post(conn, ~p"/contact/send", %{
"name" => "Jo Bloggs",
"email" => "jo@example.com",
"subject" => "Question",
"message" => "Do you ship internationally?"
})
assert redirected_to(conn) == "/contact"
assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "Message sent"
assert_email_sent(fn email ->
assert email.subject =~ "Question"
assert email.text_body =~ "Do you ship internationally?"
end)
end
test "redirects with error flash when required fields missing", %{conn: conn} do
conn = post(conn, ~p"/contact/send", %{"name" => "", "email" => "", "message" => ""})
assert redirected_to(conn) == "/contact"
assert Phoenix.Flash.get(conn.assigns.flash, :error) =~ "required fields"
end
test "redirects with error flash when params empty", %{conn: conn} do
conn = post(conn, ~p"/contact/send", %{})
assert redirected_to(conn) == "/contact"
assert Phoenix.Flash.get(conn.assigns.flash, :error) =~ "required fields"
end
end
end