Skip to content

feat: [Testimonials] PageTypes: TestimonialsHolder, ReviewPage (#118)#250

Open
jsirish wants to merge 1 commit intomainfrom
autopipe/issue-118
Open

feat: [Testimonials] PageTypes: TestimonialsHolder, ReviewPage (#118)#250
jsirish wants to merge 1 commit intomainfrom
autopipe/issue-118

Conversation

@jsirish
Copy link
Copy Markdown
Member

@jsirish jsirish commented Apr 3, 2026

Overview

Create Silverstripe PageTypes for testimonials frontend with grid display and review submission.

Changes

  • TestimonialsHolder: Displays featured testimonials
    • Grid layout with rating summary (average stars, count)
    • Featured testimonials with author info and photos
    • Submit testimonial button
    • Settings: testimonials per page, show submit button
  • ReviewPage: Product/service reviews with submission
    • Rating distribution bars (1-5 stars)
    • Sort by newest or highest rated
    • Pagination
    • Settings: reviews per page, allow anonymous, require approval
  • Templates:
    • TestimonialsHolder.ss: grid with rating summary
    • ReviewPage.ss: reviews list with distribution, sorting, pagination
    • TestimonialCard.ss: quote, stars, author photo
    • ReviewCard.ss: stars, title, content, verified badge
  • testimonials_pages.yml: Director routing

Routes

  • /testimonials/ - Featured testimonials
  • /testimonials/submit - Submit testimonial
  • /reviews/ - Reviews list
  • /reviews/submit - Write review

Fixes #118
Relates to #115

- TestimonialsHolder: PageType displaying featured testimonials
  - getFeaturedTestimonials(): approved featured testimonials sorted by SortOrder
  - getTestimonials(): all approved testimonials
  - getAverageRating(): calculates average star rating
  - getTestimonialCount(): total approved count
  - Settings: TestimonialsPerPage, ShowSubmitButton
  - submit action for testimonial submission form
- ReviewPage: PageType for product/service reviews
  - getReviews(): paginated approved reviews with sort by date/rating
  - getAverageRating(): calculates average rating
  - getRatingDistribution(): breakdown by star count (1-5)
  - getReviewCount(): total approved reviews
  - getFiveStarPercentage(): percentage of 5-star reviews
  - Settings: ReviewsPerPage, AllowAnonymous, RequireApproval
  - sort action for rating/date sorting
- TestimonialsHolder.ss: grid layout with rating summary, featured testimonials
- ReviewPage.ss: reviews list with rating distribution bars, sort options, pagination
- TestimonialCard.ss: reusable card with quote, star rating, author photo/info
- ReviewCard.ss: review card with stars, title, content, reviewer info, verified badge
- testimonials_pages.yml: Director routing

Fixes #118
Relates to #115
Copilot AI review requested due to automatic review settings April 3, 2026 16:40
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds frontend PageTypes and templates for a testimonials/reviews feature in the SilverStripe site, aiming to provide grid/list displays with rating summaries and submission entry points.

Changes:

  • Introduces two new PageTypes (TestimonialsHolder, ReviewPage) with CMS settings and basic aggregation helpers (counts/averages).
  • Adds new SS templates for listing testimonials/reviews and reusable card includes.
  • Adds a Director routing config intended to expose /testimonials/* and /reviews/* routes.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
app/templates/Layout/TestimonialsHolder.ss New testimonials grid page layout with rating summary and featured testimonial cards
app/templates/Layout/ReviewPage.ss New reviews list layout with rating summary, distribution bars, sorting links, and pagination
app/templates/Includes/TestimonialCard.ss Reusable testimonial card include (quote, stars, author info)
app/templates/Includes/ReviewCard.ss Reusable review card include (stars, title/content, reviewer info, verified badge)
app/src/Testimonials/Pages/TestimonialsHolder.php New TestimonialsHolder PageType + controller action stub for “submit”
app/src/Testimonials/Pages/ReviewPage.php New ReviewPage PageType + controller logic stubs for sorting/submit and rating aggregation helpers
app/_config/testimonials_pages.yml Adds Director routing rules for testimonials/reviews actions

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +26 to +34
<% loop $AverageRating %>
<% if $Pos <= $Up.Rating %>
<i class="fas fa-star text-warning"></i>
<% else_if $Pos <= $Up.Rating + 0.5 %>
<i class="fas fa-star-half-alt text-warning"></i>
<% else %>
<i class="far fa-star text-warning"></i>
<% end_if %>
<% end_loop %>
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$AverageRating is returned as a float (see getAverageRating()), but the template treats it as an iterable (<% loop $AverageRating %>) and then references $Up.Rating (which doesn’t exist on the page). This star rendering block won’t work as written; consider looping a fixed 1..5 range (or a dedicated Stars list from PHP) and comparing against $Up.AverageRating.

Suggested change
<% loop $AverageRating %>
<% if $Pos <= $Up.Rating %>
<i class="fas fa-star text-warning"></i>
<% else_if $Pos <= $Up.Rating + 0.5 %>
<i class="fas fa-star-half-alt text-warning"></i>
<% else %>
<i class="far fa-star text-warning"></i>
<% end_if %>
<% end_loop %>
<% if $AverageRating >= 1 %>
<i class="fas fa-star text-warning"></i>
<% else_if $AverageRating >= 0.5 %>
<i class="fas fa-star-half-alt text-warning"></i>
<% else %>
<i class="far fa-star text-warning"></i>
<% end_if %>
<% if $AverageRating >= 2 %>
<i class="fas fa-star text-warning"></i>
<% else_if $AverageRating >= 1.5 %>
<i class="fas fa-star-half-alt text-warning"></i>
<% else %>
<i class="far fa-star text-warning"></i>
<% end_if %>
<% if $AverageRating >= 3 %>
<i class="fas fa-star text-warning"></i>
<% else_if $AverageRating >= 2.5 %>
<i class="fas fa-star-half-alt text-warning"></i>
<% else %>
<i class="far fa-star text-warning"></i>
<% end_if %>
<% if $AverageRating >= 4 %>
<i class="fas fa-star text-warning"></i>
<% else_if $AverageRating >= 3.5 %>
<i class="fas fa-star-half-alt text-warning"></i>
<% else %>
<i class="far fa-star text-warning"></i>
<% end_if %>
<% if $AverageRating >= 5 %>
<i class="fas fa-star text-warning"></i>
<% else_if $AverageRating >= 4.5 %>
<i class="fas fa-star-half-alt text-warning"></i>
<% else %>
<i class="far fa-star text-warning"></i>
<% end_if %>

Copilot uses AI. Check for mistakes.
Comment on lines +25 to +33
<% loop $AverageRating %>
<% if $Pos <= $Up.Rating %>
<i class="fas fa-star text-warning"></i>
<% else_if $Pos <= $Up.Rating + 0.5 %>
<i class="fas fa-star-half-alt text-warning"></i>
<% else %>
<i class="far fa-star text-warning"></i>
<% end_if %>
<% end_loop %>
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as TestimonialsHolder: $AverageRating is a float but is used as a loop, and the logic references $Up.Rating instead of the average rating value. This will prevent the rating stars from rendering correctly; loop a 1..5 range (or provide a helper list) and compare against $Up.AverageRating.

Suggested change
<% loop $AverageRating %>
<% if $Pos <= $Up.Rating %>
<i class="fas fa-star text-warning"></i>
<% else_if $Pos <= $Up.Rating + 0.5 %>
<i class="fas fa-star-half-alt text-warning"></i>
<% else %>
<i class="far fa-star text-warning"></i>
<% end_if %>
<% end_loop %>
<% if $AverageRating >= 1 %>
<i class="fas fa-star text-warning"></i>
<% else_if $AverageRating >= 0.5 %>
<i class="fas fa-star-half-alt text-warning"></i>
<% else %>
<i class="far fa-star text-warning"></i>
<% end_if %>
<% if $AverageRating >= 2 %>
<i class="fas fa-star text-warning"></i>
<% else_if $AverageRating >= 1.5 %>
<i class="fas fa-star-half-alt text-warning"></i>
<% else %>
<i class="far fa-star text-warning"></i>
<% end_if %>
<% if $AverageRating >= 3 %>
<i class="fas fa-star text-warning"></i>
<% else_if $AverageRating >= 2.5 %>
<i class="fas fa-star-half-alt text-warning"></i>
<% else %>
<i class="far fa-star text-warning"></i>
<% end_if %>
<% if $AverageRating >= 4 %>
<i class="fas fa-star text-warning"></i>
<% else_if $AverageRating >= 3.5 %>
<i class="fas fa-star-half-alt text-warning"></i>
<% else %>
<i class="far fa-star text-warning"></i>
<% end_if %>
<% if $AverageRating >= 5 %>
<i class="fas fa-star text-warning"></i>
<% else_if $AverageRating >= 4.5 %>
<i class="fas fa-star-half-alt text-warning"></i>
<% else %>
<i class="far fa-star text-warning"></i>
<% end_if %>

Copilot uses AI. Check for mistakes.
Comment on lines +46 to +49
<div class="progress" style="height: 20px;">
<div class="progress-bar bg-warning"
style="width: ${Value / $Up.ReviewCount * 100}%;"></div>
</div>
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SilverStripe templates don’t support inline arithmetic like ${Value / $Up.ReviewCount * 100}. As a result, the progress bar width will not render correctly. Compute the percentage in PHP (e.g., include a Percentage field in the distribution items) or expose a helper method/filter for this value.

Suggested change
<div class="progress" style="height: 20px;">
<div class="progress-bar bg-warning"
style="width: ${Value / $Up.ReviewCount * 100}%;"></div>
</div>
<progress class="w-100" style="height: 20px;" value="$Value" max="$Up.ReviewCount">$Value</progress>

Copilot uses AI. Check for mistakes.
Comment on lines +3 to +6
Name: testimonials_pages

---

Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This SilverStripe YAML config file is missing the leading --- document header before Name:. Without it, Name: won’t be treated as the config header and the config may not be loaded as intended. Align the header structure with other files in app/_config (e.g., routes.yml).

Suggested change
Name: testimonials_pages
---
---
Name: testimonials_pages

Copilot uses AI. Check for mistakes.
SilverStripe\Control\Director:
rules:
'testimonials//$Action': 'App\Testimonials\Pages\TestimonialsHolderController'
'reviews//$Action': 'App\Testimonials\Pages\ReviewPageController'
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Director rules map directly to *PageController classes. Instantiating a PageController via a Director rule typically won’t have an associated SiteTree record, so $Link, $Title, etc. may not resolve and actions may error. If you need fixed routes, map to the Page class (as in routes.yml’s contact: ContactPage) or use a dedicated controller that finds/loads the correct page record.

Suggested change
'reviews//$Action': 'App\Testimonials\Pages\ReviewPageController'
'reviews//$Action': 'App\Testimonials\Pages\ReviewPage'

Copilot uses AI. Check for mistakes.
Comment on lines +91 to +101
$distribution = [];

for ($i = 1; $i <= 5; $i++) {
$count = Review::get()
->filter([
'Status' => 'Approved',
'Rating' => $i,
])
->count();

$distribution[$i] = $count;
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getRatingDistribution() runs 5 separate count() queries (one per star value). This adds avoidable DB load on every page view. Consider computing the distribution in a single aggregate query (GROUP BY Rating) and then normalising to 1..5 in PHP.

Suggested change
$distribution = [];
for ($i = 1; $i <= 5; $i++) {
$count = Review::get()
->filter([
'Status' => 'Approved',
'Rating' => $i,
])
->count();
$distribution[$i] = $count;
$distribution = [
1 => 0,
2 => 0,
3 => 0,
4 => 0,
5 => 0,
];
$ratings = Review::get()
->filter('Status', 'Approved')
->column('Rating');
foreach ($ratings as $rating) {
$rating = (int) $rating;
if ($rating >= 1 && $rating <= 5) {
$distribution[$rating]++;
}

Copilot uses AI. Check for mistakes.

public function submit()
{
return $this->renderWith(['SubmitReviewPage', 'Page']);
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

renderWith(['SubmitReviewPage', 'Page']) references a SubmitReviewPage template, but there is no matching template file in app/templates (none found). This action will fall back to Page and won’t show a submission form/page as implied. Add the missing template (and any form logic), or adjust to an existing template/action pattern.

Suggested change
return $this->renderWith(['SubmitReviewPage', 'Page']);
return $this->customise([])->renderWith([$this->dataRecord->ClassName, 'Page']);

Copilot uses AI. Check for mistakes.

public function submit()
{
return $this->renderWith(['SubmitTestimonialPage', 'Page']);
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

renderWith(['SubmitTestimonialPage', 'Page']) references a SubmitTestimonialPage template, but there is no matching template file in app/templates (none found). This action will fall back to Page and won’t show a submission form/page as implied. Add the missing template (and any form logic), or adjust to an existing template/action pattern.

Suggested change
return $this->renderWith(['SubmitTestimonialPage', 'Page']);
return $this->renderWith([$this->dataRecord->ClassName, 'Page']);

Copilot uses AI. Check for mistakes.

use App\Testimonials\Models\Testimonial;
use Page;
use SilverStripe\Forms\DropdownField;
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused imports: DropdownField is imported but never used in this class. Removing unused use statements will keep the file tidy and avoid confusion about intended CMS fields.

Suggested change
use SilverStripe\Forms\DropdownField;

Copilot uses AI. Check for mistakes.
Comment on lines +8 to +11
use SilverStripe\Forms\DropdownField;
use SilverStripe\Forms\NumericField;
use SilverStripe\Forms\FieldList;
use SilverStripe\ORM\PaginatedList;
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused imports: DropdownField (and SilverStripe\Forms\DropdownField specifically) is imported but never used in this class. Removing unused use statements will keep the file tidy and avoid confusion about intended CMS fields.

Suggested change
use SilverStripe\Forms\DropdownField;
use SilverStripe\Forms\NumericField;
use SilverStripe\Forms\FieldList;
use SilverStripe\ORM\PaginatedList;
use SilverStripe\Forms\NumericField;
use SilverStripe\Forms\FieldList;

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Testimonials] PageTypes: TestimonialsHolder, ReviewPage

2 participants