diff --git a/.gitattributes b/.gitattributes index 1ff0c4230..cf9ac3f2d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,6 +2,7 @@ # Set default behavior to automatically normalize line endings. ############################################################################### * text=auto +*.verified.txt text eol=lf working-tree-encoding=UTF-8 ############################################################################### # Set default behavior for command prompt diff. diff --git a/.sonarcloud.properties b/.sonarcloud.properties index 225fc6f34..65eb9825f 100644 --- a/.sonarcloud.properties +++ b/.sonarcloud.properties @@ -1,4 +1,4 @@ sonar.coverage.exclusions="**" ` sonar.cpd.exclusions="**" sonar.language=cs -sonar.exclusions=**/*.js,**/*.css,**/*.html,**/*.ts \ No newline at end of file +sonar.exclusions=**/*.js,**/*.css,**/*.chtml,**/*.ts \ No newline at end of file diff --git a/Directory.Packages.props b/Directory.Packages.props index a9f87a784..ad4c653a6 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,46 +3,47 @@ true - + - + - - - + + + + - - - + + + - - + + - - + + - - - - - - + + + + + + - + - - - + + + - - - + + + @@ -52,32 +53,32 @@ - + - - + + - - + + - - - - - - - - - - - + + + + + + + + + + + - - + + \ No newline at end of file diff --git a/README.md b/README.md index ffc05e555..dfdaf8e3e 100644 --- a/README.md +++ b/README.md @@ -34,12 +34,10 @@ Achieve superior performance, unlimited scalability, and comprehensive customiza
![Tests on Linux, MacOS and Windows](https://github.com/grandnode/grandnode2/actions/workflows/aspnetcore.yml/badge.svg) -[![Build Status](https://dev.azure.com/grandnode/grandnode2/_apis/build/status/grandnode.grandnode2?branchName=main)](https://dev.azure.com/grandnode/grandnode2/_build/latest?definitionId=8&branchName=main) +[![Build Status](https://dev.azure.com/grandnode/grandnode2/_apis/build/status/grandnode.grandnode2?branchName=develop)](https://dev.azure.com/grandnode/grandnode2/_build/latest?definitionId=8&branchName=develop) [![Docker Image CI](https://github.com/grandnode/grandnode2/actions/workflows/docker-image.yml/badge.svg)](https://github.com/grandnode/grandnode2/actions/workflows/docker-image.yml) ![License](https://img.shields.io/github/license/grandnode/grandnode2) [![CodeQL Advanced](https://github.com/grandnode/grandnode2/actions/workflows/codeql.yml/badge.svg)](https://github.com/grandnode/grandnode2/actions/workflows/codeql.yml) -[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=grandnode_grandnode2&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=grandnode_grandnode2) -
@@ -51,6 +49,8 @@ Achieve superior performance, unlimited scalability, and comprehensive customiza * [Getting Started](#getting-started) * [Prerequisites](#prerequisites) * [Installation](#installation) + * [Building from source](#building-from-source) + * [Running locally](#running-locally) * [Online demo](#online-demo) * [Roadmap](#roadmap) * [Contributing](#contributing) @@ -94,8 +94,9 @@ GrandNode was designed to solve the most important business challenges from the GrandNode 2 leverages the latest technologies to deliver a high-performance e-commerce solution: -- **ASP.NET Core 9.0** - Modern, cross-platform framework +- **ASP.NET Core 10.0** - Modern, cross-platform framework - **MongoDB 4.0+** - NoSQL database for unlimited scalability +- **Vue 3 + Bootstrap 5** - Storefront UI, bundled with Vite - **Docker Support** - Easy deployment and containerization - **REST API** - Comprehensive API for integrations - **Cloud-Ready** - Optimized for cloud hosting environments @@ -106,9 +107,17 @@ GrandNode 2 leverages the latest technologies to deliver a high-performance e-co To get a local copy up and running follow these simple steps. -### Prerequisites (develop version) +### Prerequisites -GrandNode requires .NET Core 9.0, MongoDB 4.0+, and OS-specific dependency tools. +| Tool | Version | Needed for | +| --- | --- | --- | +| [.NET SDK](https://dotnet.microsoft.com/download) | **10.0.100** or newer | building and running everything. The version is pinned in `global.json` with `rollForward: latestFeature`, so any 10.0.x SDK works | +| [MongoDB](https://www.mongodb.com/try/download/community) | **4.0+** | the database. A local server, a Docker container or a MongoDB Atlas cluster all work | +| [Node.js](https://nodejs.org/) + npm | **20 LTS** or newer | only when you change the storefront frontend sources. The build output is committed, so you can run the shop without Node | +| IDE | any with .NET 10 support - Visual Studio, JetBrains Rider, VS Code | optional | + +Only the SDK and MongoDB are required to get the shop running - see +[Building from source](#building-from-source) for when Node.js comes into play. ### Installation @@ -124,13 +133,18 @@ If you want to download the latest stable version of GrandNode please use the fo docker pull grandnode/grandnode2:x.xx ``` -* Open locally with VS2022+ (v17.12.0) or above +* Open locally in an IDE -Run the project in the Visual Studio 2022+, extract the source code package downloaded from Releases tab to a folder. Enter the extracted folder and double-click the GrandNode.sln solution file. Select the Plugins project, rebuild it, then select the GrandNode.Web project. +Extract the source code package downloaded from the Releases tab to a folder (or +clone the repository), and open `GrandNode.sln`. Build the whole solution - that +compiles the modules and plugins into the web project's output as well - then set +`Grand.Web` as the startup project and run it. See +[Building from source](#building-from-source) for the command line equivalent and +for the frontend build. * Host on Linux server -Before you start - please install, configure the nginx server, .NET Core 9.0+ and MongoDB 4.0+ +Before you start - please install, configure the nginx server, the .NET 10 SDK and MongoDB 4.0+ ```bash mkdir ~/source cd ~/source @@ -140,9 +154,13 @@ git clone - b x.xx https://github.com/grandnode/grandnode2.git cd ~/source/grandnode dotnet restore GrandNode.sln ``` -Now it's time to rebuild all of our plugins and publish application (command is pretty long because we've combined all commands in a single line, to ease up your work): +Now it's time to rebuild all modules and plugins and publish the application. Each +module and plugin copies itself into the web project's output, so they have to be +built *before* the publish step: ```bash -sudo dotnet build src/Plugins/Authentication.Facebook && sudo dotnet build src/Plugins/Authentication.Google && sudo dotnet build src/Plugins/DiscountRules.Standard && sudo dotnet build src/Plugins/ExchangeRate.McExchange && sudo dotnet build src/Plugins/Payments.BrainTree && sudo dotnet build src/Plugins/Payments.CashOnDelivery && sudo dotnet build src/Plugins/Payments.StripeCheckout && sudo dotnet build src/Plugins/Shipping.ByWeight && sudo dotnet build src/Plugins/Shipping.FixedRateShipping && sudo dotnet build src/Plugins/Shipping.ShippingPoint && sudo dotnet build src/Plugins/Tax.CountryStateZip && sudo dotnet build src/Plugins/Tax.FixedRate && sudo dotnet build src/Plugins/Widgets.FacebookPixel && sudo dotnet build src/Plugins/Widgets.GoogleAnalytics && sudo dotnet build src/Plugins/Widgets.Slider && sudo dotnet build src/Plugins/Theme.Modern && sudo dotnet publish src/Web/Grand.Web -c Release -o /var/webapps/grandnode +for module in src/Modules/*; do dotnet build "$module" -c Release; done +for plugin in src/Plugins/*; do dotnet build "$plugin" -c Release; done +dotnet publish src/Web/Grand.Web -c Release -o /var/webapps/grandnode ``` Optional: Create the service file, to automatically restart your application. ```bash @@ -172,6 +190,90 @@ sudo systemctl start grandnode.service ``` Feel free to visit our [detailed guide about GrandNode installation.](https://grandnode.com/how-to-install-grandnode-on-linux-ubuntu-1604/?utm_source=github&utm_medium=link&utm_campaign=readme) +### Building from source + +#### Backend + +```bash +dotnet restore GrandNode.sln +dotnet build GrandNode.sln +``` + +`GrandNode.sln` contains the whole application: the core libraries, the web +project, the modules under `src/Modules` (installer, migrations, REST API, +scheduled tasks) and the plugins under `src/Plugins`. Building the solution +builds them all and copies each module and plugin into +`src/Web/Grand.Web/Modules` / `Plugins`, so a plain `dotnet build` is enough. + +Two things worth knowing: + +* **Plugins that ship views compile those views into the plugin DLL.** After + editing a `.cshtml` file in, for example, `src/Plugins/Theme.Modern`, rebuild + that plugin (`dotnet build src/Plugins/Theme.Modern`) - the running site will + not pick the change up otherwise. Razor runtime compilation covers only + `Grand.Web`'s own views. Stop the site before rebuilding a plugin, or the + build fails on a locked DLL. +* Building a plugin on its own is fine and is what the Docker image does; you + only need the full solution build after changing shared code. + +#### Frontend + +The storefront UI (Vue 3, Bootstrap 5) lives in `src/Web/Grand.Web/vueapp` and is +the only npm project in the repository: + +```bash +cd src/Web/Grand.Web/vueapp +npm install +npm run build +``` + +That writes into `src/Web/Grand.Web/wwwroot/bundles`: + +| output | contents | +| --- | --- | +| `app.runtime.bundle.js` | Vue 3, the compatibility layer, the per-page view-models and the shared DOM behaviours | +| `libs.css` | Bootstrap, Bootstrap Icons, animate.css, Pikaday | +| `style.min.css`, `style.rtl.min.css` | the theme stylesheets from `wwwroot/theme/css`, concatenated in cascade order and minified | + +**This output is committed to the repository**, which is why neither the CI +workflows nor the Dockerfile install Node - they build the .NET solution against +the bundles already in the tree. The flip side is that when you change anything +under `vueapp/src` or `wwwroot/theme/css` you have to run `npm run build` and +commit the regenerated bundles together with the source change, otherwise your +change simply will not be on the page. + +Other scripts: `npm run dev` (watch build), `npm run lint` (ESLint over +`vueapp/src`), `npm run audit:prod`. More detail in +[`vueapp/README.md`](src/Web/Grand.Web/vueapp/README.md). + +### Running locally + +```bash +dotnet run --project src/Web/Grand.Web +``` + +`Grand.Web` does not reference the plugins - they install themselves into its +output directory when *they* are built. So build the solution once +(`dotnet build GrandNode.sln`) before the first run; after that you can start the +web project alone. + +The Kestrel profile listens on and +; the Visual Studio IIS Express profile uses +. + +> **Set `ASPNETCORE_ENVIRONMENT=Development`.** Both launch profiles already do. +> If you start the application without it, the static web assets manifest is not +> consulted, every file served from `_content/...` returns 404 and the admin +> panel loads with no CSS and no JavaScript at all. It looks like a broken +> install; it is only the missing environment variable. + +On the first run the application redirects to `/install`, where you enter the +MongoDB connection string (for example `mongodb://localhost/grandnode`) and the +administrator account, and choose whether to load the sample data. The installer +writes the connection string to `src/Web/Grand.Web/App_Data/Settings.cfg`, which +is not tracked by git - delete that file to run the installer again against a +fresh database. + ### Online demo #### Frontend #### [https://demo.grandnode.com/](https://demo.grandnode.com/?utm_source=github&utm_medium=link&utm_campaign=readme) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 09d6c0757..3f961fa9c 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -1,3 +1,13 @@ +trigger: + branches: + include: + - develop + - feature/* +pr: + branches: + include: + - develop + resources: containers: - container: mongo @@ -13,8 +23,31 @@ pool: variables: buildConfiguration: 'Release' - + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: '1' + NUGET_PACKAGES: '$(Agent.TempDirectory)/.nuget/packages' + NUGET_HTTP_CACHE_PATH: '$(Agent.TempDirectory)/.nuget/http-cache' + NUGET_PLUGINS_CACHE_PATH: '$(Agent.TempDirectory)/.nuget/plugins-cache' + DOTNET_CLI_HOME: '$(Agent.TempDirectory)' + DOTNET_NOLOGO: '1' + DOTNET_CLI_TELEMETRY_OPTOUT: '1' + VSTEST_CONNECTION_TIMEOUT: '120' + VSTEST_RESULTS_DIRECTORY: '$(Agent.TempDirectory)/TestResults' + steps: +- checkout: self + clean: true + fetchDepth: 1 + +- bash: | + sudo rm -rf /usr/local/lib/android + sudo rm -rf /usr/share/swift + sudo rm -rf /opt/hostedtoolcache/CodeQL + sudo rm -rf /usr/local/share/powershell + sudo apt-get clean + + df -h + displayName: Free disk space + - task: UseDotNet@2 displayName: 'Install .NET Core SDK 10' inputs: @@ -39,14 +72,14 @@ steps: inputs: command: 'build' projects: '**/Grand.Web.csproj' - arguments: '--configuration $(buildConfiguration)' + arguments: '--configuration $(buildConfiguration) --no-restore' displayName: 'dotnet build $(buildConfiguration)' - task: DotNetCoreCLI@2 displayName: 'dotnet test' inputs: command: 'test' - arguments: '--configuration $(buildConfiguration) --collect:"XPlat Code Coverage" -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=cobertura' + arguments: '--configuration $(buildConfiguration) --no-restore --collect:"XPlat Code Coverage" -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=cobertura --results-directory "$(VSTEST_RESULTS_DIRECTORY)"' publishTestResults: true projects: '**/Tests/**/*.csproj' @@ -54,13 +87,13 @@ steps: displayName: 'Publish code coverage report' inputs: codeCoverageTool: 'Cobertura' - summaryFileLocation: '$(Agent.TempDirectory)/**/coverage.cobertura.xml' + summaryFileLocation: '$(VSTEST_RESULTS_DIRECTORY)/**/coverage.cobertura.xml' - task: DotNetCoreCLI@2 inputs: command: publish publishWebProjects: True - arguments: '--configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)' + arguments: '--configuration $(buildConfiguration) --no-restore --output $(Build.ArtifactStagingDirectory)' zipAfterPublish: True - task: PublishPipelineArtifact@1 diff --git a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/AddCustomerReservationCommandHandler.cs b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/AddCustomerReservationCommandHandler.cs index 6d0eb9e4b..512be8213 100644 --- a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/AddCustomerReservationCommandHandler.cs +++ b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/AddCustomerReservationCommandHandler.cs @@ -1,4 +1,5 @@ using Grand.Business.Core.Commands.Checkout.Orders; +using Grand.Business.Core.Extensions; using Grand.Business.Core.Interfaces.Catalog.Products; using Grand.Domain.Catalog; using MediatR; @@ -27,55 +28,17 @@ public async Task Handle(AddCustomerReservationCommand request, Cancellati if (match != null) reservations.Remove(match); } - IGrouping groupToBook = null; - - var grouped = reservations.GroupBy(x => x.Resource); - foreach (var group in grouped) - { - var groupCanBeBooked = true; - if (request.Product.IncBothDate && request.Product.IntervalUnitId == IntervalUnit.Day) - for (var iterator = request.RentalStartDate.Value; - iterator <= request.RentalEndDate.Value; - iterator += new TimeSpan(24, 0, 0)) - { - if (group.Select(x => x.Date).Contains(iterator)) continue; - groupCanBeBooked = false; - break; - } - else - for (var iterator = request.RentalStartDate.Value; - iterator < request.RentalEndDate.Value; - iterator += new TimeSpan(24, 0, 0)) - { - if (group.Select(x => x.Date).Contains(iterator)) continue; - groupCanBeBooked = false; - break; - } - - if (!groupCanBeBooked) continue; - groupToBook = group; - break; - } + var groupToBook = reservations.FindGroupToBook(request.Product, request.RentalStartDate.Value, + request.RentalEndDate.Value); if (groupToBook != null) - { - if (request.Product.IncBothDate && request.Product.IntervalUnitId == IntervalUnit.Day) - foreach (var item in groupToBook.Where(x => - x.Date >= request.RentalStartDate && x.Date <= request.RentalEndDate)) - await _productReservationService.InsertCustomerReservationsHelper(new CustomerReservationsHelper { - CustomerId = request.Customer.Id, - ReservationId = item.Id, - ShoppingCartItemId = request.ShoppingCartItem.Id - }); - else - foreach (var item in groupToBook.Where(x => - x.Date >= request.RentalStartDate && x.Date < request.RentalEndDate)) - await _productReservationService.InsertCustomerReservationsHelper(new CustomerReservationsHelper { - CustomerId = request.Customer.Id, - ReservationId = item.Id, - ShoppingCartItemId = request.ShoppingCartItem.Id - }); - } + foreach (var item in groupToBook.InRentalPeriod(request.Product, request.RentalStartDate.Value, + request.RentalEndDate.Value)) + await _productReservationService.InsertCustomerReservationsHelper(new CustomerReservationsHelper { + CustomerId = request.Customer.Id, + ReservationId = item.Id, + ShoppingCartItemId = request.ShoppingCartItem.Id + }); } if (!string.IsNullOrEmpty(request.ReservationId)) diff --git a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/PlaceOrderCommandHandler.cs b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/PlaceOrderCommandHandler.cs index dad11a8ab..55c8236a6 100644 --- a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/PlaceOrderCommandHandler.cs +++ b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/PlaceOrderCommandHandler.cs @@ -758,43 +758,13 @@ protected virtual async Task UpdateProductReservation(Order order, PlaceOrderCon if (!sc.RentalStartDateUtc.HasValue || !sc.RentalEndDateUtc.HasValue) continue; var reservations = await _productReservationService.GetProductReservationsByProductId(product.Id, true, null); - var grouped = reservations.GroupBy(x => x.Resource); - - IGrouping groupToBook = null; - foreach (var group in grouped) - { - var groupCanBeBooked = true; - if (product.IncBothDate && product.IntervalUnitId == IntervalUnit.Day) - for (var iterator = sc.RentalStartDateUtc.Value; - iterator <= sc.RentalEndDateUtc.Value; - iterator += new TimeSpan(24, 0, 0)) - { - if (group.Select(x => x.Date).Contains(iterator)) continue; - groupCanBeBooked = false; - break; - } - else - for (var iterator = sc.RentalStartDateUtc.Value; - iterator < sc.RentalEndDateUtc.Value; - iterator += new TimeSpan(24, 0, 0)) - { - if (group.Select(x => x.Date).Contains(iterator)) continue; - groupCanBeBooked = false; - break; - } - - if (!groupCanBeBooked) continue; - groupToBook = group; - break; - } + var groupToBook = reservations.FindGroupToBook(product, sc.RentalStartDateUtc.Value, + sc.RentalEndDateUtc.Value); if (groupToBook == null) throw new Exception("ShoppingCart.Reservation.NoFreeReservationsInThisPeriod"); - var temp = groupToBook.AsQueryable(); - if (product.IncBothDate && product.IntervalUnitId == IntervalUnit.Day) - temp = temp.Where(x => x.Date >= sc.RentalStartDateUtc && x.Date <= sc.RentalEndDateUtc); - else - temp = temp.Where(x => x.Date >= sc.RentalStartDateUtc && x.Date < sc.RentalEndDateUtc); + var temp = groupToBook.InRentalPeriod(product, sc.RentalStartDateUtc.Value, sc.RentalEndDateUtc.Value) + .ToList(); foreach (var item in temp) { diff --git a/src/Business/Grand.Business.Checkout/Validators/ShoppingCartReservationProductValidator.cs b/src/Business/Grand.Business.Checkout/Validators/ShoppingCartReservationProductValidator.cs index 9a2e92d27..4169b68dd 100644 --- a/src/Business/Grand.Business.Checkout/Validators/ShoppingCartReservationProductValidator.cs +++ b/src/Business/Grand.Business.Checkout/Validators/ShoppingCartReservationProductValidator.cs @@ -1,4 +1,5 @@ using FluentValidation; +using Grand.Business.Core.Extensions; using Grand.Business.Core.Interfaces.Catalog.Products; using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Domain.Catalog; @@ -29,7 +30,6 @@ public ShoppingCartReservationProductValidator(ITranslationService translationSe if (value.ShoppingCartItem.RentalStartDateUtc.HasValue && value.ShoppingCartItem.RentalEndDateUtc.HasValue) { - var canBeBook = false; var reservations = await productReservationService.GetProductReservationsByProductId(value.Product.Id, true, null); var reserved = await productReservationService.GetCustomerReservationsHelpers(value.Customer.Id); @@ -39,33 +39,9 @@ public ShoppingCartReservationProductValidator(ITranslationService translationSe if (match != null) reservations.Remove(match); } - var grouped = reservations.GroupBy(x => x.Resource); - foreach (var group in grouped) - { - var groupCanBeBooked = true; - if (value.Product.IncBothDate && value.Product.IntervalUnitId == IntervalUnit.Day) - for (var iterator = value.ShoppingCartItem.RentalStartDateUtc.Value; - iterator <= value.ShoppingCartItem.RentalEndDateUtc.Value; - iterator += new TimeSpan(24, 0, 0)) - { - if (group.Select(x => x.Date).Contains(iterator)) continue; - groupCanBeBooked = false; - break; - } - else - for (var iterator = value.ShoppingCartItem.RentalStartDateUtc.Value; - iterator < value.ShoppingCartItem.RentalEndDateUtc.Value; - iterator += new TimeSpan(24, 0, 0)) - { - if (group.Select(x => x.Date).Contains(iterator)) continue; - groupCanBeBooked = false; - break; - } - - if (!groupCanBeBooked) continue; - canBeBook = true; - break; - } + var canBeBook = reservations.FindGroupToBook(value.Product, + value.ShoppingCartItem.RentalStartDateUtc.Value, + value.ShoppingCartItem.RentalEndDateUtc.Value) != null; if (!canBeBook) { diff --git a/src/Business/Grand.Business.Core/Extensions/ProductReservationExtensions.cs b/src/Business/Grand.Business.Core/Extensions/ProductReservationExtensions.cs new file mode 100644 index 000000000..cfe1f13c5 --- /dev/null +++ b/src/Business/Grand.Business.Core/Extensions/ProductReservationExtensions.cs @@ -0,0 +1,47 @@ +using Grand.Domain.Catalog; + +namespace Grand.Business.Core.Extensions; + +public static class ProductReservationExtensions +{ + /// + /// Finds the first resource whose reservations cover the whole rental period, or null when none is free + /// + public static IGrouping FindGroupToBook( + this IEnumerable reservations, Product product, DateTime rentalStartDate, + DateTime rentalEndDate) + { + foreach (var group in reservations.GroupBy(x => x.Resource)) + { + var freeDays = group.Select(x => x.Date.Date).ToHashSet(); + if (BookedDays(product, rentalStartDate, rentalEndDate).All(day => freeDays.Contains(day))) + return group; + } + + return null; + } + + /// + /// Gets the reservations of a resource that fall within the rental period + /// + public static IEnumerable InRentalPeriod(this IEnumerable reservations, + Product product, DateTime rentalStartDate, DateTime rentalEndDate) + { + var bookedDays = BookedDays(product, rentalStartDate, rentalEndDate).ToHashSet(); + return reservations.Where(x => bookedDays.Contains(x.Date.Date)); + } + + /// + /// Gets the days that a rental period covers. Reservations of day products are generated at the store's local + /// midnight and read back as UTC, so the time of day of a stored reservation is an arbitrary offset, while the + /// dates posted by the datepicker are always midnight - only the date part may be compared. + /// + private static IEnumerable BookedDays(Product product, DateTime rentalStartDate, DateTime rentalEndDate) + { + var includeEndDate = product.IncBothDate && product.IntervalUnitId == IntervalUnit.Day; + for (var iterator = rentalStartDate.Date; + includeEndDate ? iterator <= rentalEndDate.Date : iterator < rentalEndDate.Date; + iterator = iterator.AddDays(1)) + yield return iterator; + } +} diff --git a/src/Modules/Grand.Module.Api/Grand.Module.Api.csproj b/src/Modules/Grand.Module.Api/Grand.Module.Api.csproj index 765ced4aa..5bc94f45a 100644 --- a/src/Modules/Grand.Module.Api/Grand.Module.Api.csproj +++ b/src/Modules/Grand.Module.Api/Grand.Module.Api.csproj @@ -43,6 +43,7 @@ + diff --git a/src/Modules/Grand.Module.Migration/Migrations/2.4/MigrationUpdateResourceString.cs b/src/Modules/Grand.Module.Migration/Migrations/2.4/MigrationUpdateResourceString.cs new file mode 100644 index 000000000..2813d0371 --- /dev/null +++ b/src/Modules/Grand.Module.Migration/Migrations/2.4/MigrationUpdateResourceString.cs @@ -0,0 +1,21 @@ +using Grand.Infrastructure.Migrations; + +namespace Grand.Module.Migration.Migrations._2._4; + +public class MigrationUpdateResourceString : IMigration +{ + public int Priority => 0; + public DbVersion Version => new(2, 4); + public Guid Identity => new("6C0F2E4B-9A17-4D65-B3C8-51E0A9D77B42"); + public string Name => "Update resource string for english language 2.4"; + + /// + /// Upgrade process + /// + /// + /// + public bool UpgradeProcess(IServiceProvider serviceProvider) + { + return serviceProvider.ImportLanguageResourcesFromXml("App_Data/Resources/Upgrade/en_240.xml"); + } +} diff --git a/src/Plugins/Authentication.Facebook/Authentication.Facebook.csproj b/src/Plugins/Authentication.Facebook/Authentication.Facebook.csproj index 3089521e3..d58e2f3ec 100644 --- a/src/Plugins/Authentication.Facebook/Authentication.Facebook.csproj +++ b/src/Plugins/Authentication.Facebook/Authentication.Facebook.csproj @@ -21,7 +21,7 @@ - + diff --git a/src/Plugins/Authentication.Google/Authentication.Google.csproj b/src/Plugins/Authentication.Google/Authentication.Google.csproj index 07847958b..cf6d6ec07 100644 --- a/src/Plugins/Authentication.Google/Authentication.Google.csproj +++ b/src/Plugins/Authentication.Google/Authentication.Google.csproj @@ -20,7 +20,7 @@ - + diff --git a/src/Plugins/Theme.Modern/Content/css/animate-subset.css b/src/Plugins/Theme.Modern/Content/css/animate-subset.css new file mode 100644 index 000000000..51214add7 --- /dev/null +++ b/src/Plugins/Theme.Modern/Content/css/animate-subset.css @@ -0,0 +1,58 @@ +/* + The three animate.css classes this theme actually uses. + + animate.css itself is 95 kB of keyframes for ~90 animations and used to be + imported into the shared storefront bundle, so every page of both themes paid + for it while only Theme.Modern used it - and only here: + + - Partials/ProductBoxTemplate marks the spec table animate__animated and names + the animation in data-animation / data-delay; + - Content/script/npslider-home.js reads those attributes and adds the classes + as a slide becomes active. + + Because the class names come from data attributes, a theme that wants a + different animation has to add its keyframes here as well. +*/ + +:root { + --animate-duration: 1s; + --animate-delay: 1s; +} + +.animate__animated { + -webkit-animation-duration: var(--animate-duration); + animation-duration: var(--animate-duration); + -webkit-animation-fill-mode: both; + animation-fill-mode: both; +} + +.animate__delay-1s { + -webkit-animation-delay: var(--animate-delay); + animation-delay: var(--animate-delay); +} + +@-webkit-keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +.animate__fadeIn { + -webkit-animation-name: fadeIn; + animation-name: fadeIn; +} + +@media (prefers-reduced-motion: reduce) { + .animate__animated { + -webkit-animation-duration: 1ms !important; + animation-duration: 1ms !important; + -webkit-animation-delay: 1ms !important; + animation-delay: 1ms !important; + -webkit-animation-iteration-count: 1 !important; + animation-iteration-count: 1 !important; + } +} diff --git a/src/Plugins/Theme.Modern/Content/css/blog-news/blog-news.css b/src/Plugins/Theme.Modern/Content/css/blog-news/blog-news.css index 0d5a58075..5ab8144db 100644 --- a/src/Plugins/Theme.Modern/Content/css/blog-news/blog-news.css +++ b/src/Plugins/Theme.Modern/Content/css/blog-news/blog-news.css @@ -76,11 +76,11 @@ padding: 10px; border-left: 1px solid #d6d6d6; border-bottom: 1px solid #d6d6d6; - background-color: #fff; + background-color: var(--bs-body-bg); line-height: 1; } .card.post-news .post-date .day { - color: #000; + color: var(--bs-body-color); font-size: 24px; font-weight: 700; } diff --git a/src/Plugins/Theme.Modern/Content/css/cart/cart.css b/src/Plugins/Theme.Modern/Content/css/cart/cart.css index 24747946d..91e5132af 100644 --- a/src/Plugins/Theme.Modern/Content/css/cart/cart.css +++ b/src/Plugins/Theme.Modern/Content/css/cart/cart.css @@ -8,10 +8,10 @@ .shopping-cart { margin: 0 0 15px 0; - color: #1c1b1b; + color: var(--bs-body-color); } .shopping-cart.table thead th { - color: #1c1b1b; + color: var(--bs-body-color); font-size: 16px; font-weight: 700; text-transform: uppercase; @@ -23,7 +23,7 @@ border: 1px solid rgba(0,0,0,.1); } .shopping-cart.table td img { - background-color: #eaf0fa; + background-color: var(--bs-secondary-bg); } .shopping-cart.table .product-name { font-size: 14px; @@ -36,7 +36,7 @@ .shopping-cart .prod-total { font-size: 18px; font-weight: 700; - color: #1c1b1b; + color: var(--bs-body-color); } .shopping-cart .prod-total { color: #25b232; @@ -61,7 +61,7 @@ border-radius: 0; } .totals-inner table, .totals-inner .table-hover tbody tr:hover { - color: #1c1b1b; + color: var(--bs-body-color); } .checkout-buttons { display: inline-flex; @@ -88,7 +88,7 @@ -webkit-transition: all .2s ease-in-out; transition: all .2s ease-in-out; } -.coupon-button.not-collapsed::after { +.coupon-button:not(.collapsed)::after { -webkit-transform: rotate(180deg); transform: rotate(180deg); } @@ -120,7 +120,7 @@ [dark-theme="true"] .opc-header .card-header .btn-info { color: #fff; } -.opc-header .card-header .btn-info.not-collapsed { +.opc-header .card-header .btn-info:not(.collapsed) { color:#25b232; font-size: 16px; font-weight: 700; @@ -144,10 +144,10 @@ [dark-theme="true"] .opc-header .card-header .btn-info::after { background: #fff; } -[dark-theme="true"] .opc-header .card-header .btn-info.not-collapsed::before, -[dark-theme="true"] .opc-header .card-header .btn-info.not-collapsed::after, -.opc-header .card-header .btn-info.not-collapsed::before, -.opc-header .card-header .btn-info.not-collapsed::after { +[dark-theme="true"] .opc-header .card-header .btn-info:not(.collapsed)::before, +[dark-theme="true"] .opc-header .card-header .btn-info:not(.collapsed)::after, +.opc-header .card-header .btn-info:not(.collapsed)::before, +.opc-header .card-header .btn-info:not(.collapsed)::after { background:#25b232; } .opc-header .card-header:last-child .btn-info::before, diff --git a/src/Plugins/Theme.Modern/Content/css/catalog/catalog.css b/src/Plugins/Theme.Modern/Content/css/catalog/catalog.css index e91306c6a..baff5a6ec 100644 --- a/src/Plugins/Theme.Modern/Content/css/catalog/catalog.css +++ b/src/Plugins/Theme.Modern/Content/css/catalog/catalog.css @@ -3,6 +3,7 @@ .breadcrumb { margin: 0; padding: 15px 0 0 0; + align-items: center; } /* category list */ @@ -28,7 +29,7 @@ .category-item .title { font-size: 22px; font-weight: 400; - color: #000; + color: var(--bs-body-color); font-family: 'Poppins', sans-serif; margin: 5px 0 0 0; white-space: pre-wrap; @@ -89,7 +90,7 @@ overflow-x: hidden; overflow-y: auto; max-height: calc(100% - 54px); - background-color: #fff; + background-color: var(--bs-body-bg); margin: 15px 0 0 0; border: 1px solid rgba(0,0,0,.1); padding: 0 15px 15px 15px; @@ -148,7 +149,7 @@ margin: 0 0 15px 0; padding: 10px; border: 0; - background: #eaf0fa; + background: var(--bs-secondary-bg); border-radius: .25rem; } .generalLeftSide .available-items ul { @@ -160,10 +161,10 @@ text-transform: uppercase; } .generalLeftSide .available-items ul .btn { - color: #1c1b1b; + color: var(--bs-body-color); font-size: 14px; font-weight: 700; - background-color: #fff; + background-color: var(--bs-body-bg); } .generalLeftSide .available-items ul .btn.color-container { background-color: transparent; @@ -191,7 +192,7 @@ } .generalLeftSide li a { position: relative; - color: #1c1b1b; + color: var(--bs-body-color); text-decoration: none; font-size: 14px; font-weight: 400; @@ -320,7 +321,7 @@ a.left-side-toggler:hover { align-items: center; justify-content: space-between; padding: 5%; - background: #eaf0fa; + background: var(--bs-secondary-bg); overflow: hidden; } .parallax-banner .content { @@ -470,7 +471,7 @@ a.left-side-toggler:hover { text-transform: uppercase; font-weight: 700; font-family: Poppins, sans-serif; - color: #000; + color: var(--bs-body-color); background-color: transparent; border: 1px solid rgba(0,0,0,.1); border-bottom: 0; @@ -529,11 +530,12 @@ a.left-side-toggler:hover { } .sort-container .col-form-label { position: absolute; - top: -10px; + top: -20px; left: 4px; padding: 0 4px; - background-color: #eaf0fa; + background-color: var(--bs-secondary-bg); font-size: 12px; + line-height: 18px; font-weight: 400; white-space: nowrap; } @@ -585,7 +587,7 @@ a.left-side-toggler:hover { /* selectors */ .catalog-selectors { - background: #eaf0fa; + background: var(--bs-secondary-bg); padding: 10px; } .grid-icon { @@ -687,7 +689,7 @@ a.left-side-toggler:hover { position: relative; height: 100%; overflow: hidden; - background-color: #eaf0fa; + background-color: var(--bs-secondary-bg); } .block-recently-viewed-products .card a.picture::before { content: ""; @@ -702,7 +704,7 @@ a.left-side-toggler:hover { left: 0; max-width: 100%; margin: auto; - background-color: #eaf0fa; + background-color: var(--bs-secondary-bg); } /* new cols */ diff --git a/src/Plugins/Theme.Modern/Content/css/common/common.css b/src/Plugins/Theme.Modern/Content/css/common/common.css index fac160317..36530014b 100644 --- a/src/Plugins/Theme.Modern/Content/css/common/common.css +++ b/src/Plugins/Theme.Modern/Content/css/common/common.css @@ -1,3 +1,17 @@ +/* Bootstrap 5 renamed .sr-only to .visually-hidden; this theme's views + * (carried over from Bootstrap 4) still use the old class name. */ +.sr-only { + position: absolute !important; + width: 1px !important; + height: 1px !important; + padding: 0 !important; + margin: -1px !important; + overflow: hidden !important; + clip: rect(0, 0, 0, 0) !important; + white-space: nowrap !important; + border: 0 !important; +} + .store-selector-container { position: sticky; position: -webkit-sticky; @@ -28,11 +42,11 @@ .store-selector-container .inside-container .btn-secondary.dropdown-toggle, .store-selector-container .inside-container .btn-secondary.dropdown-toggle:hover, .store-selector-container .inside-container .btn-secondary.dropdown-toggle:focus { - color: #000; + color: var(--bs-body-color); font-size: 16px; font-weight: 700; padding: 1rem 2rem; - background-color: #fff; + background-color: var(--bs-body-bg); border-color: #fff; border-radius: .25rem; } @@ -62,7 +76,7 @@ left: 0; width: 28px; height: 2px; - background: #fff; + background: var(--bs-body-bg); } .store-selector-container .inside-container .close-add .icon::before { transform: rotate(45deg); @@ -289,12 +303,12 @@ flex-direction: column; margin: 15px 0 0 0; padding: 15px; - background-color: #fff; + background-color: var(--bs-body-bg); border-radius: 4px; box-shadow: 0 14px 28px rgba(0, 0, 0, .25), 0 10px 10px rgba(0, 0, 0, .22); } .c-picker .content input { - color: #000; + color: var(--bs-body-color); font-size: 16px; font-weight: 500; margin: 0 0 15px 0; @@ -431,7 +445,7 @@ /* commons */ a { - color: #000; + color: var(--bs-body-color); } a:hover { color: #25b232; @@ -454,7 +468,7 @@ a:hover { padding: 0 4px; font-size: 12px; font-weight: 400; - background-color: #fff; + background-color: var(--bs-body-bg); z-index: 1; } .field-validation-error { @@ -480,9 +494,7 @@ a:hover { } .navbar-brand img { width: auto; - height: auto; - max-width: 150px; - max-height: 50px; + height: auto; backface-visibility: hidden; } .cookie-bar-notification { @@ -516,7 +528,13 @@ body:not(.modal-open) { .modal.fade + .modal-backdrop { opacity: .5; } -[v-cloak] .page-loader-container::before { +/* + Keyed off .axios-request, which app.js toggles around XHR, and no longer off + [v-cloak] on #app: that hid every server-rendered page behind an opaque sheet + until the deferred bundle had run. Markup only Vue can fill carries v-cloak + itself and is hidden by the scoped rule below. +*/ +.page-loader-container.axios-request::before { content: ''; position: fixed; top: 0; @@ -524,15 +542,16 @@ body:not(.modal-open) { width: 100%; height: 100%; background: #fff; + opacity: 0.5; overflow: hidden; z-index: 1041; } -[v-cloak] .page-loader, -[v-cloak] .page-loader:before, -[v-cloak] .page-loader:after { +.axios-request .page-loader, +.axios-request .page-loader:before, +.axios-request .page-loader:after { border-radius: 50%; } -[v-cloak] .page-loader { +.axios-request .page-loader { color: #25b232; margin: 22.5px auto; position: fixed; @@ -546,12 +565,12 @@ body:not(.modal-open) { transform: translateZ(0); z-index: 1041; } -[v-cloak] .page-loader:before, -[v-cloak] .page-loader:after { +.axios-request .page-loader:before, +.axios-request .page-loader:after { position: absolute; content: ''; } -[v-cloak] .page-loader:before { +.axios-request .page-loader:before { width: 27.5px; height: 55px; background: #25b232; @@ -564,7 +583,7 @@ body:not(.modal-open) { -webkit-animation: load2 2s infinite ease 1.5s; animation: load2 2s infinite ease 1.5s; } -[v-cloak] .page-loader:after { +.axios-request .page-loader:after { width: 27.5px; height: 55px; background: #25b232; @@ -577,6 +596,14 @@ body:not(.modal-open) { -webkit-animation: load2 2s infinite ease; animation: load2 2s infinite ease; } + +[v-cloak] { + visibility: hidden; +} + +validation-provider .field-validation-error { + visibility: hidden; +} @-webkit-keyframes load2 { 0% { -webkit-transform: rotate(0deg); @@ -659,6 +686,18 @@ body:not(.modal-open) { padding-right: 0; padding-left: 0; } +.form-row { + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + margin-right: -7.5px; + margin-left: -7.5px; +} +.form-row>.col, .form-row>[class*=col-] { + padding-right: 7.5px; + padding-left: 7.5px; +} /* checkbox & radio */ @@ -705,13 +744,13 @@ button.close:hover { margin: 0 0 15px 0; } .quick-view-overview .actual-price { - color: #000; + color: var(--bs-body-color); font-size: 28px; font-weight: 900; line-height: 1; } .quick-view-overview .bid { - color: #000; + color: var(--bs-body-color); font-size: 22px; } .quick-view-overview .old-price { @@ -751,7 +790,7 @@ button.close:hover { .quick-view .spec-container tr td { /*display: inline-flex;*/ /*border: 0; - color: #000;*/ + color: var(--bs-body-color);*/ } .quick-view .spec-container .spec-name { font-size: 16px; @@ -800,15 +839,15 @@ button.close:hover { font-size: 18px; font-weight: 700; border: 0; - background-color: #fff; + background-color: var(--bs-body-bg); border: 2px solid #25b232; - color: #000; + color: var(--bs-body-color); } .quick-view-overview .accordion .btn.collapse-button .b-icon { -webkit-transition: all .2s ease-in-out; transition: all .2s ease-in-out; } -.quick-view-overview .accordion .btn.collapse-button.not-collapsed .b-icon { +.quick-view-overview .accordion .btn.collapse-button:not(.collapsed) .b-icon { -webkit-transform: rotate( 180deg ); transform: rotate( 180deg ); } @@ -817,7 +856,7 @@ button.close:hover { } .quick-view .product-gallery img { max-width: 100%; - background-color: #eaf0fa; + background-color: var(--bs-secondary-bg); border: 1px solid #ddd; } .quick-view .product-gallery { @@ -854,7 +893,7 @@ button.close:hover { align-items: center; justify-content: center; height: 60px; - background: #fff; + background: var(--bs-body-bg); } .quick-view .qty-controls div { display: flex; @@ -1074,21 +1113,21 @@ button.close:hover { } /* card */ .card { - background-color: #fff; + background-color: var(--bs-body-bg); border: 1px solid rgba(0,0,0,.1); } .card-header { font-size: 18px; font-weight: 700; - background-color: #eaf0fa; + background-color: var(--bs-secondary-bg); border-bottom: 1px solid rgba(0,0,0,.1); } /* forms */ .form-control { - color: #1c1b1b; - background-color: #fff; + color: var(--bs-body-color); + background-color: var(--bs-body-bg); border: 1px solid rgba(0,0,0,.2); border-radius: 0; } @@ -1099,7 +1138,7 @@ button.close:hover { } .form-control:disabled, .form-control[readonly], .form-control:hover:disabled, .form-control[readonly]:hover { color: #aaa; - background-color: #fff; + background-color: var(--bs-body-bg); border-color: #333; } .input-group-append .btn, .input-group-prepend .btn { @@ -1109,11 +1148,22 @@ button.close:hover { .custom-select { font-size: 14px; font-weight: 500; - color: #1c1b1b; + color: var(--bs-body-color); min-width: 75px; min-height: 40px; - background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23000' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px; - border: 2px solid #1c1b1b; + /* the background shorthand resets background-color, which left the control and its + option list transparent - the browser then painted the popup white and the + themed text became unreadable in dark mode */ + background: var(--bs-body-bg) url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23000' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px; + border: 2px solid var(--bs-border-color); +} +/* the arrow is baked into a data URI, so it cannot inherit currentColor */ +[data-bs-theme="dark"] .custom-select { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23dee2e6' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e"); +} +.custom-select option { + color: var(--bs-body-color); + background-color: var(--bs-body-bg); } /* page */ @@ -1407,7 +1457,7 @@ button.close:hover { height: 195px; margin: 0 0 15px 0; padding: 15px 0 0 0; - background-color: #fff; + background-color: var(--bs-body-bg); border-bottom: 1px solid rgba(0,0,0,.1); direction: rtl; z-index: 1034; @@ -1476,7 +1526,7 @@ ul.Menu.-vertical { padding: 0 15px 0 15px; } .Menu > li.category > a { - color: #000; + color: var(--bs-body-color); font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"; font-size: 14px; font-weight: 700; @@ -1573,7 +1623,7 @@ ul.Menu.-vertical { visibility: visible; } .Menu li.-hasSubmenu > ul.gallery > li.category > a { - color: #000; + color: var(--bs-body-color); font-size: 16px; font-weight: 900; text-transform: uppercase; @@ -1722,7 +1772,7 @@ ul.Menu.-vertical { } /* Background */ .Menu ul, .Menu.-floating { - background-color: #fff; + background-color: var(--bs-body-bg); } .Menu ul.opacity { background-color: rgba(28, 27, 27, .95); @@ -1763,7 +1813,7 @@ ul.Menu.-vertical { display: none; } .Menu li.-hasSubmenu ul li a { - color: #000; + color: var(--bs-body-color); font-size: 14px; font-weight: 400; display: flex; @@ -1816,7 +1866,7 @@ ul.Menu.-vertical { box-shadow: none; } .btn-outline-secondary { - color: #1c1b1b; + color: var(--bs-body-color); border-color: rgba(0,0,0,.2); } .btn-outline-secondary:hover { @@ -1827,8 +1877,8 @@ ul.Menu.-vertical { opacity: 0.85; } .btn-outline-info { - color: #000; - background-color: #fff; + color: var(--bs-body-color); + background-color: var(--bs-body-bg); border-color: #25b232; } .btn-outline-info:hover, .btn-outline-info .focus, .btn-outline-info :focus, .btn-outline-info :not(:disabled):not(.disabled).active, .btn-outline-info :not(:disabled):not(.disabled):active, .show > .btn-outline-info .dropdown-toggle { @@ -1852,7 +1902,7 @@ ul.Menu.-vertical { .generalTitle, .page-page .page-title > h1 { position: relative; font-family: 'Poppins', sans-serif; - background-color: #fff; + background-color: var(--bs-body-bg); display: table; font-size: 38px; font-weight: 700; @@ -1865,7 +1915,7 @@ ul.Menu.-vertical { h5.generalTitle { position: relative; font-family: 'Poppins', sans-serif; - background-color: #fff; + background-color: var(--bs-body-bg); display: table; font-size: 22px; font-weight: 700; @@ -1876,7 +1926,7 @@ h5.generalTitle { } .generalTitle > span { - background-color: #fff; + background-color: var(--bs-body-bg); display: table; font-size: 27px; font-weight: 700; @@ -1896,7 +1946,7 @@ h5.generalTitle { } .generalTitle div { padding: 0 10px 0 0; - background-color: #fff; + background-color: var(--bs-body-bg); display: table; font-size: 38px; font-weight: 700; @@ -1972,7 +2022,7 @@ dl, ol, ul { z-index: 1050; } #ModalAddToCart img { - background-color: #eaf0fa; + background-color: var(--bs-secondary-bg); } #ModalAddToCart .card { border: 1px solid #25b232; @@ -2018,7 +2068,7 @@ input:checked ~ .color-container .color::before { width: 45px; height: 45px; font-size: 22px; - background-color: #fff; + background-color: var(--bs-body-bg); border: 1px solid rgba(0,0,0,.2); border-radius: 100%; box-shadow: 0px 12px 28px rgba(0,123,255,.25), 0 -4px 14px rgba(0,123,255,.25); @@ -2095,7 +2145,7 @@ input:checked ~ .color-container .color::before { #voiceSearchButton { position: relative; - color: #1c1b1b; + color: var(--bs-body-color); margin: 0 3px; border-radius: 0; box-shadow: none; @@ -2141,8 +2191,8 @@ input:checked ~ .color-container .color::before { #small-searchterms { margin: 0 -1px 0 0; height: 50px; - color: #000; - background-color: #fff; + color: var(--bs-body-color); + background-color: var(--bs-body-bg); border: 2px solid #d7d7d7d7; } #small-searchterms:hover, #small-searchterms:hover + .input-group-append .btn, #small-searchterms + .input-group-append .btn:hover { @@ -2157,18 +2207,18 @@ input:checked ~ .color-container .color::before { padding: 0 20px; font-size: 22px; border: 2px solid #d7d7d7; - color: #000; - background-color: #fff; + color: var(--bs-body-color); + background-color: var(--bs-body-bg); } .search-box-select { max-width: 100px; height: 50px; - background-color: #fff; + background-color: var(--bs-body-bg); border: 2px solid #d7d7d7d7; } .search-box-select + .input-group-append { margin-left: -2px; - background-color: #fff; + background-color: var(--bs-body-bg); } .search-button { border: 2px solid #d7d7d7d7; @@ -2182,18 +2232,18 @@ input:checked ~ .color-container .color::before { overflow-x: hidden; overflow-y: auto; padding: 15px; - background: #fff; + background: var(--bs-body-bg); border: 1px solid rgba(0,0,0,.1); border-top: 0; z-index: 3; } .advanced-search-results .title { - color: #000; + color: var(--bs-body-color); font-size: 16px; font-weight: 700; } .advanced-search-results .list h6 a { - color: #000; + color: var(--bs-body-color); font-size: 14px; font-weight: 500; } @@ -2215,7 +2265,7 @@ input:checked ~ .color-container .color::before { margin: 0 0 10px 0; } .advanced-search-results .products .list .media .media-aside { - background-color: #eaf0fa; + background-color: var(--bs-secondary-bg); } .advanced-search-results .products .list .media:not(:last-child) { margin: 0 0 10px 0; @@ -2231,7 +2281,7 @@ input:checked ~ .color-container .color::before { } .advanced-search-results .price { width: 100%; - color: #000; + color: var(--bs-body-color); font-size: 16px; font-weight: 700; line-height: 1; @@ -2264,7 +2314,7 @@ input:checked ~ .color-container .color::before { padding: 7.5px; border: 0; text-align: left; - background-color: #fff; + background-color: var(--bs-body-bg); } .product-box .attr-container { position: absolute; @@ -2286,7 +2336,7 @@ input:checked ~ .color-container .color::before { width: auto; height: auto; padding: 0; - background-color: #fff; + background-color: var(--bs-body-bg); border: 0; } .attr-container .image-container { @@ -2311,7 +2361,7 @@ input:checked ~ .color-container .color::before { left: 0; visibility: hidden; opacity: 0; - background: #fff; + background: var(--bs-body-bg); border: 1px solid transparent; border-bottom: 0; border-radius: 0; @@ -2599,7 +2649,7 @@ input:checked ~ .color-container .color::before { .product-box .picture-container { position: relative; overflow: hidden; - background-color: #eaf0fa; + background-color: var(--bs-secondary-bg); } .product-box .picture-container .img-container { display: block; @@ -2621,7 +2671,7 @@ input:checked ~ .color-container .color::before { width: auto; height: auto; margin: auto; - background-color: #eaf0fa; + background-color: var(--bs-secondary-bg); -webkit-transition: all .4s ease-in-out; transition: all .4s ease-in-out; } @@ -2716,8 +2766,8 @@ input:checked ~ .color-container .color::before { border: 1px solid #ccc; border-radius: 100%; padding: 0; - color: #000; - background-color: #fff; + color: var(--bs-body-color); + background-color: var(--bs-body-bg); } .product-box .other-buttons .btn.btn-outline-secondary .icons, .product-box .qvb-container .btn .icons { font-size: 16px; @@ -2735,7 +2785,7 @@ input:checked ~ .color-container .color::before { display: flex; justify-content: flex-start; width: 100%; - color: #1c1b1b; + color: var(--bs-body-color); margin: 0; font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"; font-size: 15px; @@ -2795,7 +2845,7 @@ input:checked ~ .color-container .color::before { } .product-box .actual-price { margin: 0 5px 0 0; - color: #1c1b1b; + color: var(--bs-body-color); font-size: 20px; font-weight: 700; line-height: 1; @@ -2835,7 +2885,7 @@ input:checked ~ .color-container .color::before { right: -1px; bottom: 0; left: 0; - background-color: #fff; + background-color: var(--bs-body-bg); border: 1px solid transparent; border-top: 0; border-radius: 0; @@ -2847,7 +2897,7 @@ input:checked ~ .color-container .color::before { border-color: rgba(0,0,0,.1); } .product-box .attr-name { - color: #000; + color: var(--bs-body-color); font-size: 14px; font-weight: 700; padding: 5px; @@ -3046,13 +3096,13 @@ input:checked ~ .color-container .color::before { .rating .b-rating + a { display: none; - color: #000; + color: var(--bs-body-color); position: relative; font-size: 12px; font-weight: 500; } .b-rating .b-rating-value { - color: #000; + color: var(--bs-body-color); font-size: 12px; display: none; } @@ -3098,7 +3148,7 @@ input:checked ~ .color-container .color::before { background-color: transparent; } .breadcrumb-item.active { - color: #000; + color: var(--bs-body-color); } /* newsletter */ @@ -3408,7 +3458,7 @@ transform: rotate( height: auto; margin: 0; padding: 15px 0; - background-color: #fff; + background-color: var(--bs-body-bg); border: 1px solid rgba(0,0,0,.1); } .top-menu-container .menu-button.show + ul.Menu.-vertical { @@ -3545,7 +3595,7 @@ transform: rotate( padding: 0; left: 0; right: 0; - background-color: #fff; + background-color: var(--bs-body-bg); } .generalTitle span { padding: 0 15px 0 0; @@ -3745,7 +3795,7 @@ transform: rotate( left: 0; margin: 1px 0 0 0; padding: 15px 10px; - background-color: #fff; + background-color: var(--bs-body-bg); -webkit-transform: translateX(-100%); transform: translateX(-100%); -webkit-transition: transform .4s cubic-bezier(0.645, 0.045, 0.355, 1.000); @@ -3777,7 +3827,7 @@ transform: rotate( bottom: 125px; } .Menu .other-links li > a { - color: #000; + color: var(--bs-body-color); font-weight: 700; } } diff --git a/src/Plugins/Theme.Modern/Content/css/customer/customer.css b/src/Plugins/Theme.Modern/Content/css/customer/customer.css index 9b7974836..5d60ebcdb 100644 --- a/src/Plugins/Theme.Modern/Content/css/customer/customer.css +++ b/src/Plugins/Theme.Modern/Content/css/customer/customer.css @@ -11,7 +11,8 @@ color: #25b232; border-color: #25b232; } -.block-account-navigation ul li svg { +.block-account-navigation ul li svg, +.block-account-navigation ul li .b-icon { font-size: 28px; margin: 0 10px 0 0; } @@ -127,21 +128,33 @@ /* status */ +/* Bootstrap's .badge sets a white foreground, which was unreadable on these pale + tints - white on #d4edda is about 1.2:1. The subtle/emphasis variable pairs carry + their own accessible foreground and flip with data-bs-theme, so the separate dark + override this used to need is gone. + The class comes from the order status *name*, which is editable in admin, so the + base rule has to stand on its own for a renamed or custom status. */ table .status.badge { font-size: 16px; padding: 6px 8px; + color: var(--bs-secondary-color); + background-color: var(--bs-secondary-bg); } table .status.pending { - background-color: #d1ecf1; + color: var(--bs-info-text-emphasis); + background-color: var(--bs-info-bg-subtle); } table .status.processing { - background-color: #fff3cd; + color: var(--bs-warning-text-emphasis); + background-color: var(--bs-warning-bg-subtle); } table .status.complete { - background-color: #d4edda; + color: var(--bs-success-text-emphasis); + background-color: var(--bs-success-bg-subtle); } table .status.cancelled { - background-color: #f8d7da; + color: var(--bs-danger-text-emphasis); + background-color: var(--bs-danger-bg-subtle); } /* pagination */ diff --git a/src/Plugins/Theme.Modern/Content/css/header/header.css b/src/Plugins/Theme.Modern/Content/css/header/header.css index 3958dfd0c..5e04b4d75 100644 --- a/src/Plugins/Theme.Modern/Content/css/header/header.css +++ b/src/Plugins/Theme.Modern/Content/css/header/header.css @@ -4,7 +4,7 @@ .header-nav { padding: 0 20px 0 0; width: 100%; - background-color: #fff; + background-color: var(--bs-body-bg); z-index: 1040; } .header-nav-container::before { @@ -29,7 +29,7 @@ display: flex; justify-content: center; align-items: center; - color: #000; + color: var(--bs-body-color); cursor: pointer; } .navbar-nav.top-header-items .nav-link:hover { @@ -40,7 +40,7 @@ display: flex; justify-content: center; align-items: center; - color: #000; + color: var(--bs-body-color); cursor: pointer; height: 56px; padding: 0 15px 0 0; @@ -88,6 +88,9 @@ background: #1c1b1b; color: #fff; } +.b-sidebar > .b-sidebar-header .btn-close { + filter: invert(1); +} .b-sidebar-header strong { font-weight: 300; } @@ -137,7 +140,7 @@ width: 100%; } .swiper-info .top-header-info strong { - color: #000; + color: var(--bs-body-color); } .swiper-info .top-header-info span { color: #25b232; @@ -162,8 +165,8 @@ .navbar-nav.selectors .btn.dropdown-toggle { margin: 0 0 3px 0; padding: 3px 6px; - background-color: #fff; - color: #000; + background-color: var(--bs-body-bg); + color: var(--bs-body-color); border: 1px solid rgba(0,0,0,.2); font-size: 12px; font-weight: 400; @@ -175,67 +178,102 @@ padding-left: 0; } -.sidebar-cart .item { +#sidebar-cart .item, +#sidebar-wishlist .item, +#sidebar-compare .item { position: relative; width: 100%; margin: 0 0 10px 0; padding: 10px; border: 1px solid rgba(0,0,0,.1); - background-color: #fff; -} -.sidebar-cart .item img { - background-color: #eaf0fa; -} -.sidebar-cart .item:last-child, -.sidebar-cart .item:last-child:first-child { + background-color: var(--bs-body-bg); +} +#sidebar-cart .item img, +#sidebar-wishlist .item img, +#sidebar-compare .item img { + background-color: var(--bs-secondary-bg); + margin-right: 12px; +} +#sidebar-cart .item:last-child, +#sidebar-wishlist .item:last-child, +#sidebar-compare .item:last-child, +#sidebar-cart .item:last-child:first-child, +#sidebar-wishlist .item:last-child:first-child, +#sidebar-compare .item:last-child:first-child { margin: 0; } -.sidebar-cart .item .attributes { +#sidebar-cart .item .attributes, +#sidebar-wishlist .item .attributes, +#sidebar-compare .item .attributes { font-size: 12px; - color: #000; - background: #eaf0fa; + color: var(--bs-body-color); + background: var(--bs-secondary-bg); display: table; margin: 5px 0; } -.sidebar-cart .item .deletecartitem { +#sidebar-cart .item .deletecartitem, +#sidebar-wishlist .item .deletecartitem, +#sidebar-compare .item .deletecartitem { position: absolute; top: 5px; right: 5px; cursor: pointer; } -.sidebar-cart .item .col a { - color: #000; +#sidebar-cart .item .col a, +#sidebar-wishlist .item .col a, +#sidebar-compare .item .col a { + color: var(--bs-body-color); font-family: Poppins, sans-serif; font-size: 14px; font-weight: 400; text-decoration: none; } -.sidebar-cart .item .col .price, .sidebar-cart .item .col .quantity { +#sidebar-cart .item .col .price, +#sidebar-wishlist .item .col .price, +#sidebar-compare .item .col .price, +#sidebar-cart .item .col .quantity, +#sidebar-wishlist .item .col .quantity, +#sidebar-compare .item .col .quantity { font-size: 14px; font-weight: 500; } -.sidebar-cart .item .col .price strong, .sidebar-cart .item .col .quantity strong { +#sidebar-cart .item .col .price strong, +#sidebar-wishlist .item .col .price strong, +#sidebar-compare .item .col .price strong, +#sidebar-cart .item .col .quantity strong, +#sidebar-wishlist .item .col .quantity strong, +#sidebar-compare .item .col .quantity strong { margin: 0 0 0 5px; font-size: 18px; font-weight: 700; } -.sidebar-cart .b-sidebar-footer .h5 { +#sidebar-cart .b-sidebar-footer .h5, +#sidebar-wishlist .b-sidebar-footer .h5, +#sidebar-compare .b-sidebar-footer .h5 { font-weight: 300; } -.sidebar-cart .b-sidebar-footer .h4 { +#sidebar-cart .b-sidebar-footer .h4, +#sidebar-wishlist .b-sidebar-footer .h4, +#sidebar-compare .b-sidebar-footer .h4 { font-weight: 700; } -.sidebar-cart .sub-tot-top { +#sidebar-cart .sub-tot-top, +#sidebar-wishlist .sub-tot-top, +#sidebar-compare .sub-tot-top { font-weight: 300; } -.sidebar-cart .sub-tot-bottom { +#sidebar-cart .sub-tot-bottom, +#sidebar-wishlist .sub-tot-bottom, +#sidebar-compare .sub-tot-bottom { font-family: 'Poppins', sans-serif; } .header-nav .bi-cart-check { width: 40px; margin: 6px 0 0 0; } -.sidebar-cart .cart-button.btn-secondary { +#sidebar-cart .cart-button.btn-secondary, +#sidebar-wishlist .cart-button.btn-secondary, +#sidebar-compare .cart-button.btn-secondary { padding: 12px; font-size: 16px; font-weight: 700; @@ -243,10 +281,14 @@ border-color: #111; border-width: 2px; } -.sidebar-cart .cart-button.btn-secondary:hover { +#sidebar-cart .cart-button.btn-secondary:hover, +#sidebar-wishlist .cart-button.btn-secondary:hover, +#sidebar-compare .cart-button.btn-secondary:hover { background-color: #000; } -.sidebar-cart .no-items-icon { +#sidebar-cart .no-items-icon, +#sidebar-wishlist .no-items-icon, +#sidebar-compare .no-items-icon { font-size: 80px; } @@ -281,7 +323,7 @@ } @media (min-width: 576px) { .b-sidebar { - width: 530px; + --bs-offcanvas-width: 530px; } } @media (max-width: 1399px) { @@ -312,54 +354,51 @@ border-bottom: 0; } .header-nav { - position: fixed; - top: unset; - bottom: 0; - left: 0; - right: 0; - width: 100%; - padding: 10px; - display: inline-flex; - justify-content: space-around; - box-shadow: 0 -3px 6px rgba(0,0,0,.16), 0 -3px 6px rgba(0,0,0,.23); - z-index: 1034; - } - .header-nav > .navbar { - width: 100%; - height: 58px; + display: none; } .navbar-brand img { max-width: 85px; max-height: 45px; } - .header-nav-container::before { - margin-bottom: 70px; - } } @media (max-width: 575px) { .navbar-nav.top-header-items .nav-link { font-size: 22px; } - .sidebar-cart .item img { +#sidebar-cart .item img, +#sidebar-wishlist .item img, +#sidebar-compare .item img { max-width: 75px; } - .sidebar-cart .item .col a { +#sidebar-cart .item .col a, +#sidebar-wishlist .item .col a, +#sidebar-compare .item .col a { display: table; max-width: calc(100% - 15px); font-size: 14px; line-height: 1; } - .sidebar-cart .item .price, - .sidebar-cart .item .quantity { +#sidebar-cart .item .price, +#sidebar-wishlist .item .price, +#sidebar-compare .item .price, +#sidebar-cart .item .quantity, +#sidebar-wishlist .item .quantity, +#sidebar-compare .item .quantity { font-size: 14px; } - .sidebar-cart .item .attributes { +#sidebar-cart .item .attributes, +#sidebar-wishlist .item .attributes, +#sidebar-compare .item .attributes { font-size: 12px; } - .sidebar-cart .b-sidebar-footer .h4 { +#sidebar-cart .b-sidebar-footer .h4, +#sidebar-wishlist .b-sidebar-footer .h4, +#sidebar-compare .b-sidebar-footer .h4 { font-size: 20px; } - .sidebar-cart .b-sidebar-footer .h5 { +#sidebar-cart .b-sidebar-footer .h5, +#sidebar-wishlist .b-sidebar-footer .h5, +#sidebar-compare .b-sidebar-footer .h5 { font-size: 16px; } .shopping-item .subtotal { diff --git a/src/Plugins/Theme.Modern/Content/css/home/home.css b/src/Plugins/Theme.Modern/Content/css/home/home.css index 6cb9b4feb..71d5ba4ae 100644 --- a/src/Plugins/Theme.Modern/Content/css/home/home.css +++ b/src/Plugins/Theme.Modern/Content/css/home/home.css @@ -3,7 +3,7 @@ .generalTitle { margin: 15px 0; } -[v-cloak] .products-tabs { +.products-tabs[v-cloak] { display: none; } .home-page .home-page-section { @@ -16,7 +16,7 @@ position: relative; height: calc(100vh - 195px); min-height: 550px; - background-color: #eaf0fa; + background-color: var(--bs-secondary-bg); -webkit-transition: all .2s ease-in-out; transition: all .2s ease-in-out; } @@ -131,7 +131,7 @@ z-index: 3; } .product-box-slide .content .title a { - color: #1c1b1b; + color: var(--bs-body-color); font-size: 48px; line-height: 1; font-weight: 900; @@ -336,7 +336,7 @@ margin: 0 -1px 1px 0; padding: 16px 26px; font-family: 'Poppins', sans-serif; - color: #000; + color: var(--bs-body-color); background-color: transparent; font-size: 14px; font-weight: 700; @@ -385,7 +385,7 @@ .brands-container { padding: 0 10%; margin: 0 -15px; - background-color: #fff; + background-color: var(--bs-body-bg); } .brands-container .swiper-wrapper { align-items: center; @@ -419,7 +419,7 @@ align-items: center; justify-content: space-between; padding: 10%; - background: #eaf0fa; + background: var(--bs-secondary-bg); overflow: hidden; } .parallax-banner .content { @@ -508,7 +508,7 @@ position: relative; top: -118px; padding: 15px 7.5px; - background: #fff; + background: var(--bs-body-bg); z-index: 15; } .home-page-banner + * { diff --git a/src/Plugins/Theme.Modern/Content/css/product/product.css b/src/Plugins/Theme.Modern/Content/css/product/product.css index e94221e74..f6aca6522 100644 --- a/src/Plugins/Theme.Modern/Content/css/product/product.css +++ b/src/Plugins/Theme.Modern/Content/css/product/product.css @@ -25,7 +25,7 @@ .gallery-top { height: 100%; width: 75%; - background-color: #eaf0fa; + background-color: var(--bs-secondary-bg); } .gallery-thumbs { width: 25%; @@ -37,7 +37,7 @@ width: 100%; height: 25%; opacity: 0.4; - background-color: #eaf0fa; + background-color: var(--bs-secondary-bg); border: 1px solid rgba(0,0,0,.1); box-shadow: none; cursor: pointer; @@ -66,7 +66,7 @@ z-index: 1101; } .medium-zoom-image { - background-color: #eaf0fa; + background-color: var(--bs-secondary-bg); } .zoom-button { position: absolute; @@ -120,7 +120,7 @@ font-size: 14px; } .product-details-page .overview .label { - color: #000; + color: var(--bs-body-color); font-size: 14px; font-weight: 500; } @@ -134,12 +134,12 @@ /* spec */ .spec-container .spec-name { - color: #000; + color: var(--bs-body-color); font-size: 16px; font-weight: 700; } .spec-container .spec-value { - color: #000; + color: var(--bs-body-color); font-size: 16px; font-weight: 500; } @@ -150,7 +150,7 @@ margin: 0 0 15px 0; padding: 15px; border: 0; - background-color: #eaf0fa; + background-color: var(--bs-secondary-bg); border-radius: .25rem; } .attributes table { @@ -166,12 +166,12 @@ padding: 10px; } .attributes .text-prompt { - color: #000; + color: var(--bs-body-color); font-size: 16px; font-weight: 700; } .attributes .custom-control-description { - color: #000; + color: var(--bs-body-color); font-size: 14px; font-weight: 400; } @@ -233,7 +233,7 @@ align-items: center; justify-content: center; height: 60px; - background: #eaf0fa; + background: var(--bs-secondary-bg); } .product-details-page .qty-controls div { display: flex; @@ -270,11 +270,11 @@ .product-add-to-cart .qty-input, .product-add-to-cart .qty-dropdown { max-width: 65px; height: 60px; - color: #000; + color: var(--bs-body-color); font-size: 18px; font-weight: 700; - background-color: #eaf0fa; - border-color: rgba(0,0,0,.2); + background-color: var(--bs-secondary-bg); + border-color: var(--bs-border-color); } .product-details-page .add-to-cart-button.btn-info { display: flex; @@ -355,7 +355,7 @@ text-transform: uppercase; font-weight: 700; font-family: Poppins, sans-serif; - color: #000; + color: var(--bs-body-color); background-color: transparent; border: 1px solid rgba(0,0,0,.1); border-bottom: 0; diff --git a/src/Plugins/Theme.Modern/Content/script/app.js b/src/Plugins/Theme.Modern/Content/script/app.js index 82a12dae0..9f9bf3c4b 100644 --- a/src/Plugins/Theme.Modern/Content/script/app.js +++ b/src/Plugins/Theme.Modern/Content/script/app.js @@ -1,6 +1,5 @@ "use strict"; -Vue.use(VueAwesomeSwiper) var vm = new Vue({ el: '#app', data: function () { @@ -8,7 +7,10 @@ var vm = new Vue({ show: false, fluid: false, hover: false, - darkMode: false, + // resolved in by Partials/ColorScheme (explicit choice, else the OS + // preference). Set here and not in mounted() so the watcher does not fire on + // the initial value - that would persist a choice the visitor never made. + darkMode: typeof window.grandColorSchemeIsDark === 'function' && window.grandColorSchemeIsDark(), active: false, NextDropdownVisible: false, value: 5, @@ -60,6 +62,29 @@ var vm = new Vue({ slideToClickedSlide: true, }, }, + // Product image gallery (see Pictures.cshtml, which sets + // window.productGallery before this vm is created). loop mode + // needs at least loopedSlides real slides to work; with fewer, + // Swiper's loop math breaks and navigation silently does nothing. + swiperOptionTop: { + loop: !!window.productGallery && window.productGallery.pictureCount > 4, + loopedSlides: 4, + spaceBetween: 10, + navigation: { + nextEl: '#ppslider .swiper-button-next', + prevEl: '#ppslider .swiper-button-prev' + }, + }, + swiperOptionThumbs: { + direction: 'vertical', + loop: !!window.productGallery && window.productGallery.pictureCount > 4, + loopedSlides: 4, + spaceBetween: 10, + centeredSlides: true, + slidesPerView: 'auto', + touchRatio: 0.2, + slideToClickedSlide: true, + }, } }, props: { @@ -75,9 +100,8 @@ var vm = new Vue({ if (localStorage.fluid == "true") this.fluid = "fluid"; if (localStorage.fluid == "fluid") this.fluid = "fluid"; if (localStorage.fluid == "") this.fluid = "false"; - if (localStorage.darkMode == "true") this.darkMode = true; this.TopScroll(); - //this.wishindicator = parseInt(this.$refs.wishlistQty.innerText); + this.wishindicator = this.readWishlistQty(); this.updateCompareProductsQty(); this.backToTop(); if (991 < window.innerWidth) { @@ -86,6 +110,15 @@ var vm = new Vue({ window.addEventListener('DOMContentLoaded', () => { vm.$forceUpdate(); }); + if (window.productGallery) { + setTimeout(function () { + if (!vm.$refs.swiperTop || !vm.$refs.swiperThumbs) return; + const swiperTop = vm.$refs.swiperTop.$swiper + const swiperThumbs = vm.$refs.swiperThumbs.$swiper + swiperTop.controller.control = swiperThumbs + swiperThumbs.controller.control = swiperTop + }, 1000) + } }, created: function () { if (location.pathname !== "/") { @@ -97,13 +130,18 @@ var vm = new Vue({ localStorage.fluid = newName; }, darkMode: function (newValue) { - localStorage.darkMode = newValue; + // fires only when the visitor uses the switch, which is exactly when the + // choice should become explicit and stop following the OS + window.grandSetColorScheme(newValue); }, PopupQuickViewVueModal: function () { vm.getLinkedProductsQV(vm.PopupQuickViewVueModal.Id); } }, methods: { + slideToThumb(index) { + this.$refs.swiperTop.$swiper.slideTo(index) + }, openMenu(el, mainMenu) { var menu = document.getElementById(mainMenu); if (menu.classList.contains('show')) { @@ -361,6 +399,21 @@ var vm = new Vue({ }); return false; }, + /* + * Seeds the wishlist counter from the server-rendered header. + * + * The element only exists when the wishlist is enabled for the current + * customer, so reading the ref unguarded threw and aborted the rest of + * mounted() - which is why the call had been commented out. The number comes + * from data-qty rather than the element text, which is a localized template + * a store is free to decorate ("(0)", "0 items") and parseInt cannot read. + */ + readWishlistQty: function () { + const el = this.$refs.wishlistQty; + if (!el) return undefined; + const qty = parseInt(el.dataset.qty, 10); + return isNaN(qty) ? undefined : qty; + }, updateCompareProductsQty: function () { const cookie = AxiosCart.getCookie('Grand.CompareProduct'); if (cookie !== '') { @@ -435,9 +488,6 @@ var vm = new Vue({ } this.updateCompareProductsQty(); }, - showModalOutOfStock: function () { - this.$refs['out-of-stock'].show() - }, productImage: function (event) { var Imagesrc = event.target.parentElement.getAttribute('data-href'); function collectionHas(a, b) { @@ -626,8 +676,12 @@ var vm = new Vue({ }); }, warehouse_change_handler(id, url) { + //scoped to the modal: the product page behind it uses the same + //element id, and getElementById would return that one instead + var select = document.querySelector('#ModalQuickView #WarehouseId'); + if (!select) return; var data = new FormData(); - data.append('warehouseId', document.getElementById('WarehouseId').value); + data.append('warehouseId', select.value); data.append('productId', id); axios({ url: url, @@ -637,6 +691,12 @@ var vm = new Vue({ if (response.data.stockAvailability) { vm.PopupQuickViewVueModal.StockAvailability = response.data.stockAvailability; } + //an attribute product prices per warehouse too, so let the + //attribute handler refresh price and availability together + if (vm.PopupQuickViewVueModal.ProductAttributes + && vm.PopupQuickViewVueModal.ProductAttributes.length > 0) { + vm.attrchange(id, true); + } }) }, formatDate(date) { diff --git a/src/Plugins/Theme.Modern/Content/script/catalog.js b/src/Plugins/Theme.Modern/Content/script/catalog.js index fe05192b8..e100187e3 100644 --- a/src/Plugins/Theme.Modern/Content/script/catalog.js +++ b/src/Plugins/Theme.Modern/Content/script/catalog.js @@ -1,5 +1,4 @@ -Vue.use(VueAwesomeSwiper) -var subcatslider = new Vue({ +var subcatslider = new Vue({ data() { return { swiperOptions: { @@ -73,3 +72,19 @@ function closeLeftSide() { }, 400); } +function updateFiltersToggleVisibility() { + var toggle = document.getElementById('mobile-filters-toggle'); + var leftSide = document.querySelector('.generalLeftSide'); + if (!toggle || !leftSide) return; + // block-category-navigation is d-lg-block/d-none, so it only counts as + // real content above the lg breakpoint - modal-close is always present + // and isn't filter content, so it's excluded from the check. + var hasContent = Array.prototype.some.call(leftSide.children, function (el) { + if (el.classList.contains('modal-close')) return false; + return getComputedStyle(el).display !== 'none'; + }); + toggle.style.display = hasContent ? '' : 'none'; +} +document.addEventListener("DOMContentLoaded", updateFiltersToggleVisibility); +window.addEventListener("resize", updateFiltersToggleVisibility); + diff --git a/src/Plugins/Theme.Modern/Content/script/home.js b/src/Plugins/Theme.Modern/Content/script/home.js index 4075d31ef..aaecf1076 100644 --- a/src/Plugins/Theme.Modern/Content/script/home.js +++ b/src/Plugins/Theme.Modern/Content/script/home.js @@ -1,5 +1,4 @@ -Vue.use(VueAwesomeSwiper) -var hpi = new Vue({ +var hpi = new Vue({ props: { HomePageItems: { type: Array, diff --git a/src/Plugins/Theme.Modern/Content/script/npslider-home.js b/src/Plugins/Theme.Modern/Content/script/npslider-home.js index 63f8dd5f9..db304b6b2 100644 --- a/src/Plugins/Theme.Modern/Content/script/npslider-home.js +++ b/src/Plugins/Theme.Modern/Content/script/npslider-home.js @@ -1,5 +1,4 @@ -Vue.use(VueAwesomeSwiper) -var hpnslider = new Vue({ +var hpnslider = new Vue({ data() { return { Model: null, diff --git a/src/Plugins/Theme.Modern/Content/script/swiper-vue3.js b/src/Plugins/Theme.Modern/Content/script/swiper-vue3.js new file mode 100644 index 000000000..71f941b22 --- /dev/null +++ b/src/Plugins/Theme.Modern/Content/script/swiper-vue3.js @@ -0,0 +1,50 @@ +"use strict"; + +/* Vue 2 -> Vue 3: replaces vue-awesome-swiper (which registered and + * via Vue.use, an API the LegacyVue compat shim does not have). + * / are registered here as thin wrappers around the + * (framework-agnostic) Swiper core library loaded just before this script. */ +Vue.component('swiper-slide', { + template: '
' +}) +Vue.component('swiper', { + props: { + options: { + type: Object, + default: () => ({}) + } + }, + data: function () { + return { _swiper: null } + }, + computed: { + $swiper: function () { + return this._swiper; + } + }, + mounted: function () { + this.reparentControls(); + this.$nextTick(() => { + this._swiper = new Swiper(this.$refs.container, this.options); + }); + }, + updated: function () { + this.reparentControls(); + if (this._swiper) this._swiper.update(); + }, + beforeUnmount: function () { + if (this._swiper) this._swiper.destroy(true, true); + }, + methods: { + reparentControls: function () { + var container = this.$refs.container; + if (!container) return; + var wrapper = container.querySelector('.swiper-wrapper'); + if (!wrapper) return; + wrapper.querySelectorAll(':scope > .swiper-button-next, :scope > .swiper-button-prev, :scope > .swiper-pagination, :scope > .swiper-scrollbar').forEach(function (el) { + container.appendChild(el); + }); + } + }, + template: '
' +}) diff --git a/src/Plugins/Theme.Modern/Content/swiper/vue-awesome-swiper.min.js b/src/Plugins/Theme.Modern/Content/swiper/vue-awesome-swiper.min.js deleted file mode 100644 index 194afdbda..000000000 --- a/src/Plugins/Theme.Modern/Content/swiper/vue-awesome-swiper.min.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Minified by jsDelivr using Terser v3.14.1. - * Original file: /npm/vue-awesome-swiper@4.1.1/dist/vue-awesome-swiper.js - * - * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files - */ -!function (e, t) { "object" == typeof exports && "undefined" != typeof module ? t(exports, require("swiper"), require("vue")) : "function" == typeof define && define.amd ? define(["exports", "swiper", "vue"], t) : t((e = e || self).VueAwesomeSwiper = {}, e.Swiper, e.Vue) }(this, function (e, t, n) { "use strict"; var i; t = t && Object.prototype.hasOwnProperty.call(t, "default") ? t.default : t, n = n && Object.prototype.hasOwnProperty.call(n, "default") ? n.default : n, function (e) { e.SwiperComponent = "Swiper", e.SwiperSlideComponent = "SwiperSlide", e.SwiperDirective = "swiper", e.SwiperInstance = "$swiper" }(i || (i = {})); var o, r, l = Object.freeze({ containerClass: "swiper-container", wrapperClass: "swiper-wrapper", slideClass: "swiper-slide" }); !function (e) { e.Ready = "ready", e.ClickSlide = "clickSlide" }(o || (o = {})), function (e) { e.AutoUpdate = "autoUpdate", e.AutoDestroy = "autoDestroy", e.DeleteInstanceOnDestroy = "deleteInstanceOnDestroy", e.CleanupStylesOnDestroy = "cleanupStylesOnDestroy" }(r || (r = {})); var a = ["init", "beforeDestroy", "slideChange", "slideChangeTransitionStart", "slideChangeTransitionEnd", "slideNextTransitionStart", "slideNextTransitionEnd", "slidePrevTransitionStart", "slidePrevTransitionEnd", "transitionStart", "transitionEnd", "touchStart", "touchMove", "touchMoveOpposite", "sliderMove", "touchEnd", "click", "tap", "doubleTap", "imagesReady", "progress", "reachBeginning", "reachEnd", "fromEdge", "setTranslate", "setTransition", "resize", "observerUpdate", "beforeLoopFix", "loopFix"]; function s() { for (var e = 0, t = 0, n = arguments.length; t < n; t++)e += arguments[t].length; var i = Array(e), o = 0; for (t = 0; t < n; t++)for (var r = arguments[t], l = 0, a = r.length; l < a; l++, o++)i[o] = r[l]; return i } var d, u = function (e) { return e.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/\s+/g, "-").toLowerCase() }, p = function (e, t, n) { var i, r, l; if (e && !e.destroyed) { var a = (null === (i = t.composedPath) || void 0 === i ? void 0 : i.call(t)) || t.path; if ((null == t ? void 0 : t.target) && a) { var s = Array.from(e.slides), d = Array.from(a); if (s.includes(t.target) || d.some(function (e) { return s.includes(e) })) { var p = e.clickedIndex, c = Number(null === (l = null === (r = e.clickedSlide) || void 0 === r ? void 0 : r.dataset) || void 0 === l ? void 0 : l.swiperSlideIndex), v = Number.isInteger(c) ? c : null; n(o.ClickSlide, p, v), n(u(o.ClickSlide), p, v) } } } }, c = function (e, t) { a.forEach(function (n) { e.on(n, function () { for (var e = arguments, i = [], o = 0; o < arguments.length; o++)i[o] = e[o]; t.apply(void 0, s([n], i)); var r = u(n); r !== n && t.apply(void 0, s([r], i)) }) }) }, v = "instanceName"; function f(e, t) { var n = function (e, t) { var n, i, o, r, l = null === (i = null === (n = e.data) || void 0 === n ? void 0 : n.attrs) || void 0 === i ? void 0 : i[t]; return void 0 !== l ? l : null === (r = null === (o = e.data) || void 0 === o ? void 0 : o.attrs) || void 0 === r ? void 0 : r[u(t)] }, a = function (e, t, o) { return t.arg || n(o, v) || e.id || i.SwiperInstance }, s = function (e, t, n) { var i = a(e, t, n); return n.context[i] || null }, d = function (e) { return e.value || t }, f = function (e) { return [!0, void 0, null, ""].includes(e) }, h = function (e) { var t, n, i = (null === (t = e.data) || void 0 === t ? void 0 : t.on) || (null === (n = e.componentOptions) || void 0 === n ? void 0 : n.listeners); return function (e) { for (var t, n = arguments, o = [], r = 1; r < arguments.length; r++)o[r - 1] = n[r]; var l = null === (t = i) || void 0 === t ? void 0 : t[e]; l && l.fns.apply(l, o) } }; return { bind: function (e, t, n) { -1 === e.className.indexOf(l.containerClass) && (e.className += (e.className ? " " : "") + l.containerClass), e.addEventListener("click", function (i) { var o = h(n), r = s(e, t, n); p(r, i, o) }) }, inserted: function (t, n, i) { var r = i.context, l = d(n), s = a(t, n, i), u = h(i), p = r, v = null == p ? void 0 : p[s]; v && !v.destroyed || (v = new e(t, l), p[s] = v, c(v, u), u(o.Ready, v)) }, componentUpdated: function (e, t, i) { var o, l, a, u, p, c, v, h, w, S, y, m, C = n(i, r.AutoUpdate); if (f(C)) { var g = s(e, t, i); if (g) { var I = d(t).loop; I && (null === (l = null === (o = g) || void 0 === o ? void 0 : o.loopDestroy) || void 0 === l || l.call(o)), null === (a = null == g ? void 0 : g.update) || void 0 === a || a.call(g), null === (p = null === (u = g.navigation) || void 0 === u ? void 0 : u.update) || void 0 === p || p.call(u), null === (v = null === (c = g.pagination) || void 0 === c ? void 0 : c.render) || void 0 === v || v.call(c), null === (w = null === (h = g.pagination) || void 0 === h ? void 0 : h.update) || void 0 === w || w.call(h), I && (null === (y = null === (S = g) || void 0 === S ? void 0 : S.loopCreate) || void 0 === y || y.call(S), null === (m = null == g ? void 0 : g.update) || void 0 === m || m.call(g)) } } }, unbind: function (e, t, i) { var o, l = n(i, r.AutoDestroy); if (f(l)) { var a = s(e, t, i); a && a.initialized && (null === (o = null == a ? void 0 : a.destroy) || void 0 === o || o.call(a, f(n(i, r.DeleteInstanceOnDestroy)), f(n(i, r.CleanupStylesOnDestroy)))) } } } } function h(e) { var t; return n.extend({ name: i.SwiperComponent, props: (t = { defaultOptions: { type: Object, required: !1, default: function () { return {} } }, options: { type: Object, required: !1 } }, t[r.AutoUpdate] = { type: Boolean, default: !0 }, t[r.AutoDestroy] = { type: Boolean, default: !0 }, t[r.DeleteInstanceOnDestroy] = { type: Boolean, required: !1, default: !0 }, t[r.CleanupStylesOnDestroy] = { type: Boolean, required: !1, default: !0 }, t), data: function () { var e; return (e = {})[i.SwiperInstance] = null, e }, computed: { swiperInstance: { cache: !1, set: function (e) { this[i.SwiperInstance] = e }, get: function () { return this[i.SwiperInstance] } }, swiperOptions: function () { return this.options || this.defaultOptions }, wrapperClass: function () { return this.swiperOptions.wrapperClass || l.wrapperClass } }, methods: { handleSwiperClick: function (e) { p(this.swiperInstance, e, this.$emit.bind(this)) }, autoReLoopSwiper: function () { var e, t; if (this.swiperInstance && this.swiperOptions.loop) { var n = this.swiperInstance; null === (e = null == n ? void 0 : n.loopDestroy) || void 0 === e || e.call(n), null === (t = null == n ? void 0 : n.loopCreate) || void 0 === t || t.call(n) } }, updateSwiper: function () { var e, t, n, i, o, l, a, s; this[r.AutoUpdate] && this.swiperInstance && (this.autoReLoopSwiper(), null === (t = null === (e = this.swiperInstance) || void 0 === e ? void 0 : e.update) || void 0 === t || t.call(e), null === (i = null === (n = this.swiperInstance.navigation) || void 0 === n ? void 0 : n.update) || void 0 === i || i.call(n), null === (l = null === (o = this.swiperInstance.pagination) || void 0 === o ? void 0 : o.render) || void 0 === l || l.call(o), null === (s = null === (a = this.swiperInstance.pagination) || void 0 === a ? void 0 : a.update) || void 0 === s || s.call(a)) }, destroySwiper: function () { var e, t; this[r.AutoDestroy] && this.swiperInstance && this.swiperInstance.initialized && (null === (t = null === (e = this.swiperInstance) || void 0 === e ? void 0 : e.destroy) || void 0 === t || t.call(e, this[r.DeleteInstanceOnDestroy], this[r.CleanupStylesOnDestroy])) }, initSwiper: function () { this.swiperInstance = new e(this.$el, this.swiperOptions), c(this.swiperInstance, this.$emit.bind(this)), this.$emit(o.Ready, this.swiperInstance) } }, mounted: function () { this.swiperInstance || this.initSwiper() }, activated: function () { this.updateSwiper() }, updated: function () { this.updateSwiper() }, beforeDestroy: function () { this.$nextTick(this.destroySwiper) }, render: function (e) { return e("div", { staticClass: l.containerClass, on: { click: this.handleSwiperClick } }, [this.$slots[d.ParallaxBg], e("div", { class: this.wrapperClass }, this.$slots.default), this.$slots[d.Pagination], this.$slots[d.PrevButton], this.$slots[d.NextButton], this.$slots[d.Scrollbar]]) } }) } !function (e) { e.ParallaxBg = "parallax-bg", e.Pagination = "pagination", e.Scrollbar = "scrollbar", e.PrevButton = "button-prev", e.NextButton = "button-next" }(d || (d = {})); var w = n.extend({ name: i.SwiperSlideComponent, computed: { slideClass: function () { var e, t; return (null === (t = null === (e = this.$parent) || void 0 === e ? void 0 : e.swiperOptions) || void 0 === t ? void 0 : t.slideClass) || l.slideClass } }, methods: { update: function () { var e, t = this.$parent; t[r.AutoUpdate] && (null === (e = null == t ? void 0 : t.swiperInstance) || void 0 === e || e.update()) } }, mounted: function () { this.update() }, updated: function () { this.update() }, render: function (e) { return e("div", { class: this.slideClass }, this.$slots.default) } }), S = function (e) { var t = function (n, o) { if (!t.installed) { var r = h(e); o && (r.options.props.defaultOptions.default = function () { return o }), n.component(i.SwiperComponent, r), n.component(i.SwiperSlideComponent, w), n.directive(i.SwiperDirective, f(e, o)), t.installed = !0 } }; return t }; var y = function (e) { var t; return (t = { version: "4.1.1", install: S(e), directive: f(e) })[i.SwiperComponent] = h(e), t[i.SwiperSlideComponent] = w, t }(t), m = y.version, C = y.install, g = y.directive, I = y.Swiper, O = y.SwiperSlide; e.Swiper = I, e.SwiperSlide = O, e.default = y, e.directive = g, e.install = C, e.version = m, Object.defineProperty(e, "__esModule", { value: !0 }) }); -//# sourceMappingURL=/sm/20d7110b3463446defd51923206a17b7ff00b136457c620768cd77a8c5a24bfc.map \ No newline at end of file diff --git a/src/Plugins/Theme.Modern/Views/Modern/Account/AddressAdd.cshtml b/src/Plugins/Theme.Modern/Views/Modern/Account/AddressAdd.cshtml index e879c3049..a39550844 100644 --- a/src/Plugins/Theme.Modern/Views/Modern/Account/AddressAdd.cshtml +++ b/src/Plugins/Theme.Modern/Views/Modern/Account/AddressAdd.cshtml @@ -15,7 +15,7 @@
-