add no-JS contact form and noscript banner
All checks were successful
deploy / deploy (push) Successful in 1m21s
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:
parent
af069c2bca
commit
ca01f43d70
61
lib/berrypod/contact_notifier.ex
Normal file
61
lib/berrypod/contact_notifier.ex
Normal 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
|
||||
@ -74,6 +74,11 @@
|
||||
</style>
|
||||
</head>
|
||||
<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
|
||||
class="themed shop-root"
|
||||
data-mood={@theme_settings.mood}
|
||||
|
||||
@ -100,29 +100,31 @@ defmodule BerrypodWeb.ShopComponents.Content do
|
||||
<div class="contact-form-spacer"></div>
|
||||
<% 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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<.shop_button type="submit" class="contact-form-submit">
|
||||
Send Message
|
||||
Send message
|
||||
</.shop_button>
|
||||
</form>
|
||||
</.shop_card>
|
||||
|
||||
27
lib/berrypod_web/controllers/contact_controller.ex
Normal file
27
lib/berrypod_web/controllers/contact_controller.ex
Normal 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
|
||||
@ -1,7 +1,7 @@
|
||||
defmodule BerrypodWeb.Shop.Contact do
|
||||
use BerrypodWeb, :live_view
|
||||
|
||||
alias Berrypod.Orders
|
||||
alias Berrypod.{ContactNotifier, Orders}
|
||||
alias Berrypod.Orders.OrderNotifier
|
||||
alias Berrypod.Pages
|
||||
alias BerrypodWeb.OrderLookupController
|
||||
@ -39,6 +39,23 @@ defmodule BerrypodWeb.Shop.Contact do
|
||||
{:noreply, assign(socket, :tracking_state, state)}
|
||||
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
|
||||
def handle_event("reset_tracking", _params, socket) do
|
||||
{:noreply, assign(socket, :tracking_state, :idle)}
|
||||
|
||||
@ -270,7 +270,8 @@ defmodule BerrypodWeb.Router do
|
||||
# Checkout (POST — creates Stripe session and redirects)
|
||||
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
|
||||
|
||||
# Cart form actions (no-JS fallbacks for LiveView cart events)
|
||||
|
||||
87
test/berrypod/contact_notifier_test.exs
Normal file
87
test/berrypod/contact_notifier_test.exs
Normal 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
|
||||
48
test/berrypod_web/controllers/contact_controller_test.exs
Normal file
48
test/berrypod_web/controllers/contact_controller_test.exs
Normal 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
|
||||
Loading…
Reference in New Issue
Block a user