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

@@ -963,24 +963,37 @@ defmodule BerrypodWeb.ShopComponents.Content do
- `author` - Reviewer name
- `date` - Relative date string (e.g., "2 weeks ago")
- `verified` - Boolean, if true shows "Verified purchase" badge
* `average_rating` - Optional. Average rating to show in header. Defaults to 5.
* `average_rating` - Optional. Average rating to show in header. Defaults to nil.
* `total_count` - Optional. Total number of reviews. Defaults to length of reviews list.
* `open` - Optional. Whether section is expanded by default. Defaults to true.
* `product` - Optional. The product struct for review submission.
* `email_session` - Optional. Email from session cookie.
* `existing_review` - Optional. User's existing review if any.
* `review_status` - Optional. Status message tuple {type, message}.
## Examples
<.reviews_section reviews={@product.reviews} average_rating={4.8} total_count={24} />
"""
attr :reviews, :list, required: true
attr :average_rating, :integer, default: 5
attr :average_rating, :any, default: nil
attr :total_count, :integer, default: nil
attr :open, :boolean, default: true
attr :product, :any, default: nil
attr :email_session, :string, default: nil
attr :existing_review, :any, default: nil
attr :review_form, :any, default: nil
attr :review_status, :any, default: nil
def reviews_section(assigns) do
assigns =
assign_new(assigns, :display_count, fn ->
assigns
|> assign_new(:display_count, fn ->
assigns.total_count || length(assigns.reviews)
end)
|> assign_new(:has_reviews, fn ->
assigns.reviews != [] and assigns.total_count != 0
end)
~H"""
<details open={@open} class="pdp-reviews">
@@ -989,10 +1002,12 @@ defmodule BerrypodWeb.ShopComponents.Content do
<h2 class="reviews-heading">
Customer reviews
</h2>
<div class="reviews-rating-group">
<.star_rating rating={@average_rating} />
<span class="reviews-count">({@display_count})</span>
</div>
<%= if @has_reviews do %>
<div class="reviews-rating-group">
<.star_rating rating={@average_rating} />
<span class="reviews-count">({@display_count})</span>
</div>
<% end %>
</div>
<svg
class="reviews-chevron"
@@ -1007,20 +1022,110 @@ defmodule BerrypodWeb.ShopComponents.Content do
</summary>
<div class="reviews-body">
<div class="reviews-list">
<%= for review <- @reviews do %>
<.review_card review={review} />
<% end %>
</div>
<.review_request_form
:if={@product && !@existing_review}
product={@product}
email_session={@email_session}
review_status={@review_status}
/>
<.shop_button_outline class="reviews-load-more">
Load more reviews
</.shop_button_outline>
<.existing_review_notice :if={@existing_review} review={@existing_review} product={@product} />
<%= if @has_reviews do %>
<div class="reviews-list">
<%= for review <- @reviews do %>
<.review_card review={review} />
<% end %>
</div>
<.shop_button_outline :if={length(@reviews) < @display_count} class="reviews-load-more">
Load more reviews
</.shop_button_outline>
<% else %>
<p class="reviews-empty">No reviews yet. Be the first to share your experience!</p>
<% end %>
</div>
</details>
"""
end
@doc """
Renders the review request form for verified purchasers.
"""
attr :product, :any, required: true
attr :email_session, :string, default: nil
attr :review_status, :any, default: nil
def review_request_form(assigns) do
~H"""
<div class="review-request-form">
<h3 class="review-request-title">Write a review</h3>
<%= if @review_status do %>
<.review_status_message status={@review_status} />
<% end %>
<form phx-submit="request_review" class="review-email-form">
<p class="review-request-hint">
Enter the email you used to purchase this product. We'll send you a link to leave your review.
</p>
<div class="review-email-row">
<input
type="email"
name="email"
value={@email_session}
placeholder="your@email.com"
required
class="review-email-input"
/>
<button type="submit" class="review-email-submit">
Continue
</button>
</div>
</form>
</div>
"""
end
@doc """
Shows a notice that the user has already reviewed this product.
"""
attr :review, :any, required: true
attr :product, :any, required: true
def existing_review_notice(assigns) do
~H"""
<div class="existing-review-notice">
<p>
You've already reviewed this product.
<.link
href={"/reviews/#{@review.id}/edit?product=#{@product.slug}"}
class="existing-review-edit-link"
>
Edit your review
</.link>
</p>
</div>
"""
end
@doc """
Renders a status message for review operations.
"""
attr :status, :any, required: true
def review_status_message(assigns) do
{type, message} = assigns.status
assigns = assign(assigns, :type, type)
assigns = assign(assigns, :message, message)
~H"""
<div class={"review-status review-status-#{@type}"}>
{@message}
</div>
"""
end
@doc """
Renders a single review card.