Ib challenge Add Discount page using superpowers #697
Conversation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rSettings Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Change sidebar link text to "My Discounts" and icon to currency-dollar in both Grand.Web and Theme.Modern navigation templates - Replace raw localization keys with hardcoded text in CustomerDiscounts view (title "My discounts", empty state "No discounts at the moment") - Add grandnode-startup.sh with start/restart/stop commands; restart builds from source and runs on port 80 for local development Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
BootstrapVue's bundled icon set does not include currency-dollar, causing it to render as blank. Use an inline SVG path instead. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| [CustomerGroupAuthorize(SystemCustomerGroupNames.Registered)] | ||
| public virtual async Task<IActionResult> CustomerDiscounts() | ||
| { | ||
| var storeId = _contextAccessor.StoreContext.CurrentStore.Id; |
There was a problem hiding this comment.
The CustomerDiscount action should not be available when HideDiscounts is set to true. Please add the appropriate conditions.
| @@ -0,0 +1,173 @@ | |||
| #!/usr/bin/env bash | |||
There was a problem hiding this comment.
Please delete this file.
There was a problem hiding this comment.
Pull request overview
Adds a new “My Discounts” area under the customer account section, with navigation support and an admin setting to hide/show the tab. It also introduces a root-level dev helper script for starting GrandNode2 via Docker or running from source.
Changes:
- Added a new customer account page (
CustomerDiscounts) and route (account/discounts) to display discounts. - Extended customer navigation + settings to support a “Discounts” tab with a “Hide discounts” toggle in Admin.
- Added
grandnode-startup.shto start/restart/stop a local dev environment with Docker/.NET.
Reviewed changes
Copilot reviewed 12 out of 13 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Web/Grand.Web/Views/Shared/Components/CustomerNavigation/Default.cshtml | Adds “My Discounts” link to the customer navigation menu. |
| src/Plugins/Theme.Modern/Views/Modern/Shared/Components/CustomerNavigation/Default.cshtml | Mirrors the new “My Discounts” navigation entry for the Modern theme. |
| src/Web/Grand.Web/Views/Discount/CustomerDiscounts.cshtml | New account page view rendering a discounts table / empty state. |
| src/Web/Grand.Web/Controllers/DiscountController.cs | New controller action to load discounts and return the view. |
| src/Web/Grand.Web/Endpoints/EndpointProvider.cs | Registers the new CustomerDiscounts route. |
| src/Web/Grand.Web/Models/Customer/CustomerNavigationModel.cs | Adds HideDiscounts flag + new AccountNavigationEnum.Discounts value. |
| src/Web/Grand.Web/Models/Catalog/CustomerDiscountModel.cs | Introduces a view model for displaying discount info on the new page. |
| src/Web/Grand.Web/Features/Handlers/Customers/GetNavigationHandler.cs | Populates HideDiscounts based on CustomerSettings.HideDiscountsTab. |
| src/Core/Grand.Domain/Customers/CustomerSettings.cs | Adds HideDiscountsTab setting to the domain model. |
| src/Web/Grand.Web.AdminShared/Models/Settings/CustomerSettingsModel.cs | Adds HideDiscountsTab to the admin settings model. |
| src/Web/Grand.Web.Admin/Areas/Admin/Views/Setting/Partials/Customer.TabCustomerSettings.cshtml | Adds a checkbox in Admin settings UI for hiding the Discounts tab. |
| grandnode-startup.sh | Adds a dev helper script to start Docker services and/or run from source. |
Comments suppressed due to low confidence (1)
src/Web/Grand.Web/Views/Discount/CustomerDiscounts.cshtml:71
- The empty-state message "No discounts at the moment" is hardcoded and not localized. Please switch it to a Loc[...] resource so it can be translated like the rest of the account UI.
else
{
<p class="text-muted">No discounts at the moment</p>
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| { | ||
| <li class="customer-discounts"> | ||
| <a href="@Url.RouteUrl("CustomerDiscounts")" class="@if (Model.SelectedTab == AccountNavigationEnum.Discounts) { <text>active</text> }else { <text>inactive</text> }"> | ||
| <svg viewBox="0 0 16 16" width="1em" height="1em" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="b-icon bi"><path d="M4 10.781c.148 1.667 1.513 2.85 3.591 3.003V15h1.043v-1.216c2.27-.179 3.678-1.438 3.678-3.3 0-1.59-.947-2.51-2.956-3.028l-.722-.187V3.467c1.122.11 1.879.714 2.07 1.616h1.47c-.166-1.6-1.54-2.748-3.54-2.875V1H7.591v1.233c-1.939.23-3.27 1.472-3.27 3.156 0 1.454.966 2.483 2.661 2.917l.61.162v4.031c-1.149-.17-1.94-.8-2.131-1.718H4zm3.391-3.836c-1.043-.263-1.6-.825-1.6-1.616 0-.944.704-1.641 1.8-1.828v3.495l-.2-.051zm1.591 1.872c1.287.323 1.852.859 1.852 1.769 0 1.097-.826 1.828-2.2 1.939V8.73l.348.086z"/></svg> <span>My Discounts</span> |
| { | ||
| <li class="customer-discounts"> | ||
| <a href="@Url.RouteUrl("CustomerDiscounts")" class="@if (Model.SelectedTab == AccountNavigationEnum.Discounts) { <text>active</text> }else { <text>inactive</text> }"> | ||
| <svg viewBox="0 0 16 16" width="1em" height="1em" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="b-icon bi"><path d="M4 10.781c.148 1.667 1.513 2.85 3.591 3.003V15h1.043v-1.216c2.27-.179 3.678-1.438 3.678-3.3 0-1.59-.947-2.51-2.956-3.028l-.722-.187V3.467c1.122.11 1.879.714 2.07 1.616h1.47c-.166-1.6-1.54-2.748-3.54-2.875V1H7.591v1.233c-1.939.23-3.27 1.472-3.27 3.156 0 1.454.966 2.483 2.661 2.917l.61.162v4.031c-1.149-.17-1.94-.8-2.131-1.718H4zm3.391-3.836c-1.043-.263-1.6-.825-1.6-1.616 0-.944.704-1.641 1.8-1.828v3.495l-.2-.051zm1.591 1.872c1.287.323 1.852.859 1.852 1.769 0 1.097-.826 1.828-2.2 1.939V8.73l.348.086z"/></svg> <span>My Discounts</span> |
| <section class="page account-page customer-discounts-page pl-lg-3 pt-lg-0 pt-3"> | ||
| <h1 class="h2 generalTitle">My discounts</h1> | ||
| @if (Model.Any()) |
| // GetActiveDiscountsByContext requires a non-null DiscountType; use GetDiscountsQuery to get all types | ||
| var allDiscounts = await _discountService.GetDiscountsQuery(discountType: null, storeId: storeId); | ||
|
|
||
| var now = DateTime.UtcNow; | ||
| var model = allDiscounts | ||
| .Where(d => d.IsEnabled | ||
| && (d.StartDateUtc == null || d.StartDateUtc <= now) | ||
| && (d.EndDateUtc == null || d.EndDateUtc >= now)) |
| [HttpGet] | ||
| [CustomerGroupAuthorize(SystemCustomerGroupNames.Registered)] | ||
| public virtual async Task<IActionResult> CustomerDiscounts() | ||
| { | ||
| var storeId = _contextAccessor.StoreContext.CurrentStore.Id; | ||
| // GetActiveDiscountsByContext requires a non-null DiscountType; use GetDiscountsQuery to get all types | ||
| var allDiscounts = await _discountService.GetDiscountsQuery(discountType: null, storeId: storeId); | ||
|
|
| [GrandResourceDisplayName("Admin.Settings.Customer.HideCoursesTab")] | ||
| public bool HideCoursesTab { get; set; } | ||
|
|
||
| [GrandResourceDisplayName("Admin.Settings.Customer.HideDiscountsTab")] | ||
| public bool HideDiscountsTab { get; set; } | ||
|
|
| <div class="form-group"> | ||
| <div class="col-6 col-md-6 col-sm-6"> | ||
| <admin-label asp-for="CustomerSettings.HideDiscountsTab" class="control-label"/> | ||
| </div> | ||
| <div class="col-6 col-md-6 col-sm-6"> | ||
| <label class="mt-checkbox mt-checkbox-outline control control-checkbox"> | ||
| <admin-input asp-for="CustomerSettings.HideDiscountsTab"/> | ||
| <div class="control__indicator"></div> | ||
| </label> | ||
| <span asp-validation-for="CustomerSettings.HideDiscountsTab"></span> | ||
| </div> |
| warn "Starting GrandNode2 container..." | ||
| if docker ps -a --format '{{.Names}}' | grep -q '^grandnode2$'; then | ||
| docker start grandnode2 > /dev/null | ||
| log "GrandNode2 container started (existing)" | ||
| else | ||
| docker run -d -p 80:8080 --name grandnode2 --link mongodb:mongo \ | ||
| -v grandnode_images:/app/wwwroot/assets/images \ | ||
| -v grandnode_appdata:/app/App_Data \ | ||
| grandnode/grandnode2 > /dev/null | ||
| log "GrandNode2 container created and started" |
| if docker ps -a --format '{{.Names}}' | grep -q '^mongodb$'; then | ||
| docker start mongodb > /dev/null | ||
| log "MongoDB container started (existing)" | ||
| else | ||
| docker run -d -p 127.0.0.1:27017:27017 --name mongodb mongo > /dev/null | ||
| log "MongoDB container created and started" | ||
| fi | ||
| } | ||
|
|
||
| stop_dev_process() { | ||
| if [ -f "$PID_FILE" ]; then | ||
| local pid | ||
| pid=$(cat "$PID_FILE") | ||
| if kill -0 "$pid" 2>/dev/null; then | ||
| warn "Stopping local dev server (pid $pid)..." | ||
| kill "$pid" 2>/dev/null || true | ||
| sleep 2 | ||
| kill -9 "$pid" 2>/dev/null || true | ||
| log "Dev server stopped" | ||
| fi | ||
| rm -f "$PID_FILE" | ||
| fi | ||
| } | ||
|
|
||
| # ── commands ───────────────────────────────────────────────────────────────── | ||
|
|
||
| cmd_start() { | ||
| start_mongodb | ||
|
|
||
| warn "Starting GrandNode2 container..." | ||
| if docker ps -a --format '{{.Names}}' | grep -q '^grandnode2$'; then | ||
| docker start grandnode2 > /dev/null | ||
| log "GrandNode2 container started (existing)" | ||
| else | ||
| docker run -d -p 80:8080 --name grandnode2 --link mongodb:mongo \ | ||
| -v grandnode_images:/app/wwwroot/assets/images \ | ||
| -v grandnode_appdata:/app/App_Data \ | ||
| grandnode/grandnode2 > /dev/null | ||
| log "GrandNode2 container created and started" |
| #!/usr/bin/env bash | ||
| set -euo pipefail | ||
|
|
||
| GREEN='\033[0;32m' | ||
| RED='\033[0;31m' | ||
| YELLOW='\033[1;33m' | ||
| NC='\033[0m' | ||
|
|
||
| log() { echo -e "${GREEN}[OK]${NC} $1"; } | ||
| warn() { echo -e "${YELLOW}[..] $1${NC}"; } | ||
| fail() { echo -e "${RED}[FAIL]${NC} $1"; exit 1; } | ||
|
|
||
| COMMAND="${1:-start}" | ||
| SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | ||
| PID_FILE="$SCRIPT_DIR/.grandnode-dev.pid" |
Resolves #issueNumber
Type: feature|bugfix|
Issue
Description of the issue this PR is solving, why it's happening, and how to reproduce it.
Solution
Summarize your solution to the problem. Please include short description.
Breaking changes
If you have a breaking changes, list them here, otherwise list none.
Testing