add review submission flow (phase 3)
All checks were successful
deploy / deploy (push) Successful in 1m10s

- Add ReviewNotifier for verification emails
- Add review token generation and verification
- Add request_review_verification function
- Update reviews_section component with write-a-review form
- Create ReviewForm LiveView for submitting/editing reviews
- Add /reviews/new and /reviews/:id/edit routes
- Add review buttons to order detail page
- Update block_types to load real review data
- Tests for token and verification functions

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
jamey
2026-04-01 11:12:25 +01:00
parent 8dc17a6f4d
commit 32eb0c6758
12 changed files with 835 additions and 34 deletions

View File

@@ -6,7 +6,7 @@ defmodule BerrypodWeb.Shop.Pages.OrderDetail do
import Phoenix.Component, only: [assign: 3]
import Phoenix.LiveView, only: [push_navigate: 2]
alias Berrypod.{Orders, Pages}
alias Berrypod.{Orders, Pages, Reviews}
alias BerrypodWeb.R
alias Berrypod.Products
alias Berrypod.Products.ProductImage
@@ -44,7 +44,16 @@ defmodule BerrypodWeb.Shop.Pages.OrderDetail do
slug = if variant.product.visible, do: variant.product.slug, else: nil
{id, %{thumb: thumb, slug: slug}}
# Check if user has reviewed this product
existing_review = Reviews.get_review_by_email_and_product(email, variant.product_id)
{id,
%{
thumb: thumb,
slug: slug,
product_id: variant.product_id,
existing_review: existing_review
}}
end)
socket =

View File

@@ -6,7 +6,7 @@ defmodule BerrypodWeb.Shop.Pages.Product do
import Phoenix.Component, only: [assign: 2, assign: 3]
import Phoenix.LiveView, only: [connected?: 1, push_navigate: 2]
alias Berrypod.{Analytics, Cart, Pages}
alias Berrypod.{Analytics, Cart, Pages, Reviews}
alias BerrypodWeb.R
alias Berrypod.Images.Optimizer
alias Berrypod.Products
@@ -69,6 +69,12 @@ defmodule BerrypodWeb.Shop.Pages.Product do
|> assign(:variants, variants)
|> assign(:page, page)
|> assign(:product_discontinued, is_discontinued)
|> assign(:review_form, nil)
|> assign(:review_status, nil)
|> assign(:existing_review, nil)
# Check if user has an existing review for this product
socket = load_existing_review(socket)
# Block data loaders (related_products, reviews) run after product is assigned
extra = Pages.load_block_data(page.blocks, socket.assigns)
@@ -126,8 +132,56 @@ defmodule BerrypodWeb.Shop.Pages.Product do
end
end
# ── Review events ──────────────────────────────────────────────────────
def handle_event("request_review", %{"email" => email}, socket) do
product = socket.assigns.product
case Reviews.request_review_verification(email, product.id, product.title) do
{:ok, :sent} ->
{:noreply,
assign(
socket,
:review_status,
{:info, "Check your email for a link to leave your review."}
)}
{:error, :no_purchase} ->
{:noreply,
assign(
socket,
:review_status,
{:error, "We couldn't find a matching order for this product."}
)}
{:error, :already_reviewed} ->
{:noreply,
assign(socket, :review_status, {:error, "You've already reviewed this product."})}
{:error, _reason} ->
{:noreply,
assign(socket, :review_status, {:error, "Something went wrong. Please try again later."})}
end
end
def handle_event(_event, _params, _socket), do: :cont
# ── Review helpers ───────────────────────────────────────────────────
defp load_existing_review(socket) do
email = socket.assigns[:email_session]
product = socket.assigns.product
if email do
case Reviews.get_review_by_email_and_product(email, product.id) do
nil -> socket
review -> assign(socket, :existing_review, review)
end
else
socket
end
end
# ── Variant selection logic ──────────────────────────────────────────
defp apply_variant_params(params, socket) do