diff --git a/.github/workflows/analyse.yml b/.github/workflows/analyse.yml index 81aa152df..75f0f9fbd 100644 --- a/.github/workflows/analyse.yml +++ b/.github/workflows/analyse.yml @@ -16,8 +16,6 @@ jobs: php: [8.2, 8.3, 8.4] stability: [prefer-stable] include: - - laravel: 11.* - testbench: 9.* - laravel: 12.* testbench: 10.* diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index a813a6ca6..21fefca96 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -16,11 +16,9 @@ jobs: fail-fast: false matrix: php: [8.2, 8.3, 8.4] - laravel: [11.*, 12.*] + laravel: [12.*] stability: [prefer-lowest, prefer-stable] include: - - laravel: 11.* - testbench: ~9.2 - laravel: 12.* testbench: 10.* diff --git a/composer.json b/composer.json index 15841a866..88c5415b1 100644 --- a/composer.json +++ b/composer.json @@ -17,18 +17,20 @@ "prefer-stable": true, "require": { "php": "^8.2", - "rapidez/laravel-multi-cache": "^2.0", "blade-ui-kit/blade-heroicons": "^2.6", - "illuminate/database": "^11.0|^12.0", - "illuminate/events": "^11.0|^12.0", - "illuminate/queue": "^11.0|^12.0", - "illuminate/support": "^11.0|^12.0", + "http-interop/http-factory-guzzle": "^1.2", + "illuminate/database": "^12.0", + "illuminate/events": "^12.0", + "illuminate/queue": "^12.0", + "illuminate/support": "^12.0", "justbetter/laravel-http3earlyhints": "^1.4", + "laravel/scout": "^10.14", "lcobucci/clock": "^2.0|^3.2", "lcobucci/jwt": "^4.0|^5.3", - "mailerlite/laravel-elasticsearch": "^11.2", + "matchish/laravel-scout-elasticsearch": "^7.11", "rapidez/blade-components": "^1.8", "rapidez/blade-directives": "^1.1", + "illuminate/cache": "^12.9", "tormjens/eventy": "^0.8" }, "require-dev": { @@ -52,12 +54,16 @@ } }, "config": { - "sort-packages": true + "sort-packages": true, + "allow-plugins": { + "php-http/discovery": true + } }, "extra": { "laravel": { "providers": [ - "Rapidez\\Core\\RapidezServiceProvider" + "Rapidez\\Core\\RapidezServiceProvider", + "Matchish\\ScoutElasticSearch\\ElasticSearchServiceProvider" ], "aliases": { "Rapidez": "Rapidez\\Core\\Facades\\Rapidez" diff --git a/config/rapidez/frontend.php b/config/rapidez/frontend.php index 30122854f..ff773dccd 100644 --- a/config/rapidez/frontend.php +++ b/config/rapidez/frontend.php @@ -5,7 +5,6 @@ 'exposed' => [ 'store', 'es_url', - 'es_prefix', 'media_url', 'magento_url', 'notifications', @@ -21,38 +20,31 @@ ], // The checkout steps which are used to name the steps - // in the url and in the progressbar on steps. You can - // add different steps for different stores. Keep + // in the url and in the progressbar on steps. Keep // them lowercase and do not include any spaces. 'checkout_steps' => [ - // 'default' => ['onestep'], - 'default' => ['login', 'credentials', 'payment'], + 'login', 'credentials', 'payment', + // 'onestep', ], 'autocomplete' => [ // Attach additional indexes to the autocomplete // Uses the views in rapidez::layouts.partials.header.autocomplete 'additionals' => [ - 'categories' => ['name^3', 'description'], + 'history' => [], + 'categories' => [], // For example: // 'blogs' => [ - // 'fields' => ['title^3', 'description'], // Required // 'size' => 3, // Optional; Overrides the default `size` as defined below - // 'stores' => ['my_second_store'], // Optional; Define this only if you want to specify which stores use this index - // 'sort' => ['date' => 'desc'], // Optional; See: https://www.elastic.co/guide/en/elasticsearch/reference/7.17/sort-search-results.html) // ], ], - 'debounce' => 500, - 'size' => 3, + 'size' => 3, ], - // Link store codes to theme folders // The structure is `'store_code' => 'folder_path'` - 'themes' => [ - 'default' => resource_path('themes/default'), - ], + 'theme' => resource_path('themes/default'), // The fully qualified class names of the widgets. 'widgets' => [ diff --git a/config/rapidez/indexer.php b/config/rapidez/indexer.php deleted file mode 100644 index 517991e66..000000000 --- a/config/rapidez/indexer.php +++ /dev/null @@ -1,22 +0,0 @@ - [2, 3, 4], - - // Additional searchable attributes with the search weight. - 'searchable' => [ - // 'attribute' => 4.0, - ], - - // From Magento only "Yes/No, Dropdown, Multiple Select and Price" attribute types - // can be configured as filter. If you'd like to have a filter for an attribute - // with, for example, the type of "Text", you can specify the attribute_code here. - 'additional_filters' => [ - // eav_attribute attribute_code. e.g. brand - ], -]; diff --git a/config/rapidez/searchkit.php b/config/rapidez/searchkit.php new file mode 100644 index 000000000..8f329d6d2 --- /dev/null +++ b/config/rapidez/searchkit.php @@ -0,0 +1,65 @@ + [ + 'name', + ], + + // Additional attributes that are used to search the results. + // This will be merged with the searchable + // attributes configured in Magento. + 'search_attributes' => [ + // ['field' => 'attribute_code', 'weight' => 4.0], + ], + + // Attributes that are returned in the search result response. + // Don't want to keep track of this? An empty array will + // return all attributes, but that's not recommended! + 'result_attributes' => [ + 'entity_id', + 'name', + 'sku', + 'price', + 'special_price', + 'image', + 'images', + 'url', + 'thumbnail', + 'in_stock', + 'children', + 'super_*', + 'reviews_count', + 'reviews_score', + ], + + // Extra attributes that should be a range slider. Only supports numeric values. + 'range_attributes' => [ + // 'attribute_code' + ], + + // Additional attributes that are used to create facets. + // From Magento only "Yes/No, Dropdown, Multiple Select and Price" attribute types + // can be configured as filter. If you'd like to have a filter for an attribute + // with, for example, the type of "Text", you can specify the attribute_code here. + 'facet_attributes' => [ + // ['attribute' => 'brand', 'field' => 'brand.keyword', 'type' => 'string'], + ], + + // Attributes that are used to create filters. + // Required so that SearchKit can keep track of the type and field of each attribute. + 'filter_attributes' => [ + ['attribute' => 'entity_id', 'field' => 'entity_id', 'type' => 'numeric'], + ['attribute' => 'sku', 'field' => 'sku.keyword', 'type' => 'string'], + ['attribute' => 'category_ids', 'field' => 'category_ids', 'type' => 'numeric'], + ['attribute' => 'visibility', 'field' => 'visibility', 'type' => 'numeric'], + ], + + // Additional sorting options to be added to the product listings + // Given directions can only be an array of 'asc' and/or 'desc' + // Order shown here will be the order shown in the dropdown (including the order of the given directions!) + 'sorting' => [ + 'created_at' => ['desc'], + ], +]; diff --git a/config/rapidez/system.php b/config/rapidez/system.php index d5179d98c..09db30437 100644 --- a/config/rapidez/system.php +++ b/config/rapidez/system.php @@ -7,9 +7,6 @@ // Elasticsearch url. 'es_url' => env('ELASTICSEARCH_URL', 'http://localhost:9200'), - // Elasticsearch prefix. - 'es_prefix' => env('ELASTICSEARCH_PREFIX', 'rapidez'), - // Get Magento url from Database 'magento_url_from_db' => env('GET_MAGENTO_URL_FROM_DATABASE', false), diff --git a/lang/en/frontend.php b/lang/en/frontend.php index 892320988..1edd10a1c 100644 --- a/lang/en/frontend.php +++ b/lang/en/frontend.php @@ -28,20 +28,23 @@ 'yes' => 'Yes', ], - 'asc' => 'asc', - 'desc' => 'desc', + 'asc' => 'ascending', + 'desc' => 'descending', 'relevance' => 'Relevance', - 'newest' => 'Newest', 'all' => 'All', + 'sorting' => [ + 'created_at' => [ + 'asc' => 'Oldest', + 'desc' => 'Newest', + ], + 'name' => [ + 'asc' => 'Name A-Z', + 'desc' => 'Name Z-A', + ], + ], + 'search' => [ 'title' => 'Search for', ], - - // 'sorting' => [ - // 'attribute' => [ - // 'asc' => 'Attribute asc', - // 'desc' => 'Attrribute desc', - // ], - // ], ]; diff --git a/lang/nl.json b/lang/nl.json index 713d5ec02..8f0cf6929 100644 --- a/lang/nl.json +++ b/lang/nl.json @@ -7,14 +7,14 @@ "Adding": "Aan het toevoegen", "Addition": "Toevoeging", "All rights reserved.": "Alle rechten voorbehouden.", - "Apply coupon code": "Kortingscode toepassen", "Apply": "Toepassen", + "Apply coupon code": "Kortingscode toepassen", "Billing address": "Factuuradres", "Cart": "Winkelwagen", "Categories": "Categorieën", "Category": "Categorie", - "Checkout": "Afrekenen", "Check the spelling of your search term": "De spelling van je zoekterm te controleren", + "Checkout": "Afrekenen", "City": "Stad", "Close": "Sluiten", "Company": "Bedrijf", @@ -23,17 +23,21 @@ "Country": "Land", "Create an account": "Account aanmaken", "Credentials": "Gegevens", + "Decrease": "Verlagen", "Description": "Beschrijving", "Email": "Email", "Fax": "Fax", "Filters": "Filters", + "First": "Eerste", "Firstname": "Voornaam", "Forgot your password?": "Wachtwoord vergeten?", "Go to home": "Naar de homepagina", "Have you tried:": "Heb je gedacht aan:", "Home": "Homepagina", "Housenumber": "Huisnummer", + "Increase": "Verhogen", "Items per page": "Producten per pagina", + "Last": "Laatste", "Lastname": "Achternaam", "Less options": "Minder opties", "Loading": "Aan het laden", @@ -45,18 +49,19 @@ "Middlename": "Tussenvoegsel", "More options": "Meer opties", "My billing and shipping address are the same": "Mijn factuur- en verzendadres zijn hetzelfde", - "Please enter a shipping address first": "Voer eerst een verzendadres in", "New address": "Nieuw adres", "Next": "Volgende", "No": "Nee", "No results found for :searchterm": "Geen resultaten voor :searchterm", "Order placed succesfully": "Bestelling succesvol geplaatst", "Orders": "Bestellingen", + "Out of stock": "Niet op voorraad", "page": "pagina", "Password": "Wachtwoord", - "Payment method": "Betaalmethode", "Payment": "Betalen", + "Payment method": "Betaalmethode", "Place order": "Bestelling plaatsen", + "Please enter a shipping address first": "Voer eerst een verzendadres in", "Please log in": "Log hier in", "Postcode": "Postcode", "Prefix": "Aanhef", @@ -65,28 +70,26 @@ "products": "producten", "Products": "Producten", "Quantity": "Hoeveelheid", - "Increase": "Verhogen", - "Decrease": "Verlagen", "Region": "Regio", "Related products": "Gerelateerde producten", + "Relevance": "Relevantie", "Remove": "Verwijderen", "Repeat password": "Wachtwoord herhalen", "Reset filters": "Filters resetten", - "Search for": "Zoeken naar", "Search": "Zoeken", + "Search for": "Zoeken naar", "Select": "Selecteer", + "Shipping": "Verzending", "Shipping & billing address": "Verzend- en factuuradres", "Shipping address": "Verzendadres", "Shipping method": "Verzendmethode", - "Shipping": "Verzending", "Show cart": "Bekijk winkelwagen", - "This product is out of stock, remove it to continue your order.": "Dit product is niet op voorraad, verwijder het om verder te gaan met je bestelling.", - "Out of stock": "Niet op voorraad", "Show results": "Bekijk resultaten", "Sign up for our newsletter to stay up to date.": "Meld je aan voor onze nieuwsbrief om op de hoogte te blijven.", "Sorry! No image": "Sorry! Geen afbeelding", "Sorry! This product is currently out of stock.": "Sorry! Dit product is momenteel niet op voorraad.", - "Sorry! We did not find any products.": "Sorry! We hebben geen producten gevonden.", + "No products found.": "Geen producten gevonden.", + "Here are some suggestions:": "Hier zijn enkele suggesties:", "Specifications": "Specificaties", "Street": "Straat", "Subscribe": "Inschrijven", @@ -97,21 +100,31 @@ "Tax ID": "Btw-id", "Telephone": "Telefoonnummer", "Thank you for subscribing!": "Bedankt voor het inschrijven!", + "This product is out of stock, remove it to continue your order.": "Dit product is niet op voorraad, verwijder het om verder te gaan met je bestelling.", "This product will be backordered": "Dit product zal worden nageleverd", "This website uses cookies": "Deze website maakt gebruik van cookies", "Total": "Totaal", "Update": "Update", "Use other search terms": "Andere zoektermen te gebruiken", + "Previous Searches": "Vorige zoekopdrachten", "View all products": "Bekijk alle producten", - "What are you looking for?": "Waar ben je naar op zoek?", "Want product news and updates?": "Wil je productnieuws en updates?", "We care about the protection of your data. Read our": "Wij geven om de bescherming van uw gegevens. Lees onze", "We found other products you might like!": "We hebben andere producten gevonden die je misschien leuk vindt!", "We will get to work for you right away": "Wij gaan meteen aan de slag", "We will send a confirmation of your order to": "Wij zullen een confirmatie sturen naar", + "What are you looking for?": "Waar ben je naar op zoek?", "Wishlist": "Wensenlijst", "Yes": "Ja", "You don't have anything in your cart.": "Je hebt geen producten in je winkelwagen.", "You have filtered for:": "Je hebt gefilterd op:", - "Your order is currently:": "Je bestelling is op dit moment:" + "Your order is currently:": "Je bestelling is op dit moment:", + "Search within the results": "Zoeken binnen resultaten", + "Selected filters": "Geselecteerde filters", + "First Page": "Eerste pagina", + "Previous Page": "Vorige pagina", + "Page": "Pagina", + "Next Page": "Volgende pagina", + "Last Page": "Laatste pagina", + "Clear the search query": "De zoekopdracht wissen" } diff --git a/lang/nl/frontend.php b/lang/nl/frontend.php index 206e5cbb1..27e88e3a4 100644 --- a/lang/nl/frontend.php +++ b/lang/nl/frontend.php @@ -26,21 +26,25 @@ 'asc' => 'oplopend', 'desc' => 'aflopend', 'relevance' => 'Relevantie', - 'newest' => 'Nieuwste', 'all' => 'Alles', + 'price' => 'Prijs', 'search' => [ 'title' => 'Zoeken naar', ], 'sorting' => [ - 'price' => [ - 'asc' => 'Prijs oplopend', - 'desc' => 'Prijs aflopend', + 'created_at' => [ + 'asc' => 'Oudste', + 'desc' => 'Nieuwste', ], 'name' => [ 'asc' => 'Naam A-Z', 'desc' => 'Naam Z-A', ], ], + + 'search' => [ + 'title' => 'Zoeken naar', + ], ]; diff --git a/package.json b/package.json index b1a8920f3..a719816b7 100644 --- a/package.json +++ b/package.json @@ -7,9 +7,9 @@ "prod": "vite build" }, "devDependencies": { - "@appbaseio/reactivesearch-vue": "https://gitpkg.vercel.app/api/pkg?url=rapidez/reactivesearch/packages/vue&commit=v1.34.0-vue&scripts.postinstall=yarn%20install%20--ignore-scripts%20%26%26%20yarn%20run%20build-es&scripts.build-es=nps%20build.es", "@babel/core": "^7.23.9", "@hotwired/turbo": "^8.0.2", + "@searchkit/instantsearch-client": "^4.14.1", "@tailwindcss/forms": "^0.5.3", "@tailwindcss/typography": "^0.5.9", "@vitejs/plugin-vue2": "^2.2.0", @@ -20,6 +20,7 @@ "graphql": "^16.8.1", "graphql-combine-query": "indykoning/graphql-combine-query#feature/add-allowed-duplicates", "graphql-tag": "^2.12.6", + "instantsearch.js": "^4.75.7", "laravel-vite-plugin": "^1.0.5", "postcss": "^8.4.29", "postcss-import": "^16.1.0", @@ -33,6 +34,7 @@ "vue": "^2.7", "vue-clickaway": "^2.2.2", "vue-cookies": "^1.8.2", + "vue-instantsearch": "^4.19.13", "vue-template-compiler": "^2.7.14", "vue-turbolinks": "^2.2.2", "vue2-teleport": "^1.1.4" diff --git a/resources/css/app.css b/resources/css/app.css index f27d883dc..3ab39769e 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -1,8 +1,3 @@ -@import 'components/vue-slider'; -@import 'components/price-slider'; -@import 'components/pagination'; -@import 'components/autocomplete'; - @tailwind base; @tailwind components; @tailwind utilities; @@ -19,7 +14,8 @@ -webkit-tap-highlight-color: transparant; } -listing { +listing, +autocomplete { /* Reset browser added styling causing unexpected behavior */ display: contents; font-family: unset; @@ -38,3 +34,19 @@ listing { .arrows-hidden[type='number'] { -moz-appearance: textfield; } + +mark { + @apply bg-transparent font-bold; +} + +input[type='search']::-webkit-search-decoration, +input[type='search']::-webkit-search-cancel-button, +input[type='search']::-webkit-search-results-button, +input[type='search']::-webkit-search-results-decoration { + display: none; +} + +.custom-select { + background-position: right center; + field-sizing: content; +} diff --git a/resources/css/components/autocomplete.css b/resources/css/components/autocomplete.css deleted file mode 100644 index 1ce3ace8a..000000000 --- a/resources/css/components/autocomplete.css +++ /dev/null @@ -1,17 +0,0 @@ -.search-input + div div[groupposition='right'] { - @apply size-6 right-16 cursor-pointer z-50 !-translate-y-1/2 !top-1/2 !absolute; - - .cancel-icon { - @apply hidden; - } -} - -.search-input + div div[groupposition='right'] div[isclearicon='true']::before { - @apply size-6; - content: ''; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none'%3E%3Cpath d='M4.20986 4.38708L4.29286 4.29308C4.46505 4.1209 4.69415 4.01747 4.93718 4.00219C5.18021 3.98691 5.42046 4.06083 5.61286 4.21008L5.70686 4.29308L11.9999 10.5851L18.2929 4.29308C18.3851 4.19757 18.4955 4.12139 18.6175 4.06898C18.7395 4.01657 18.8707 3.98898 19.0035 3.98783C19.1362 3.98668 19.2679 4.01198 19.3908 4.06226C19.5137 4.11254 19.6254 4.18679 19.7193 4.28069C19.8131 4.37458 19.8874 4.48623 19.9377 4.60913C19.988 4.73202 20.0133 4.8637 20.0121 4.99648C20.011 5.12926 19.9834 5.26048 19.931 5.38249C19.8786 5.50449 19.8024 5.61483 19.7069 5.70708L13.4149 12.0001L19.7069 18.2931C19.879 18.4653 19.9825 18.6944 19.9978 18.9374C20.013 19.1804 19.9391 19.4207 19.7899 19.6131L19.7069 19.7071C19.5347 19.8793 19.3056 19.9827 19.0625 19.998C18.8195 20.0133 18.5793 19.9393 18.3869 19.7901L18.2929 19.7071L11.9999 13.4151L5.70686 19.7071C5.51826 19.8892 5.26566 19.99 5.00346 19.9878C4.74126 19.9855 4.49045 19.8803 4.30504 19.6949C4.11963 19.5095 4.01447 19.2587 4.01219 18.9965C4.00991 18.7343 4.1107 18.4817 4.29286 18.2931L10.5849 12.0001L4.29286 5.70708C4.12068 5.53489 4.01725 5.30579 4.00197 5.06276C3.98669 4.81974 4.06061 4.57949 4.20986 4.38708Z' fill='%232E4A56'/%3E%3C/svg%3E"); -} - -mark { - @apply bg-transparent text-inherit font-bold; -} diff --git a/resources/css/components/pagination.css b/resources/css/components/pagination.css deleted file mode 100644 index 1027377b5..000000000 --- a/resources/css/components/pagination.css +++ /dev/null @@ -1,37 +0,0 @@ -.pagination .pagination-button:first-child:nth-last-child(3), -.pagination .pagination-button:first-child:nth-last-child(3) ~ .pagination-button { - @apply hidden; -} - -.pagination { - @apply flex flex-wrap justify-center gap-x-2 !m-0 !my-6 max-md:gap-y-4; -} - -.pagination-button { - @apply !font-semibold !font-sans !border !border-border !rounded !bg-white !text !shadow; -} - -.pagination-button.active { - @apply !border !border-none !bg-primary !text-white !shadow-none; -} - -.pagination-button:first-child { - @apply !mr-auto max-md:w-full max-md:order-last; -} - -.pagination-button:last-child { - @apply !ml-auto max-md:w-full max-md:-order-1; -} - -.pagination-button:not(:first-child):not(:last-child) { - @apply !m-0 !size-14 max-md:flex-1 max-md:w-auto; -} - -.pagination-button:first-child, -.pagination-button:last-child { - @apply h-14 px-6 max-md:!m-0; -} - -.pagination-button[disabled] { - @apply opacity-60; -} diff --git a/resources/css/components/price-slider.css b/resources/css/components/price-slider.css deleted file mode 100644 index 1452735bc..000000000 --- a/resources/css/components/price-slider.css +++ /dev/null @@ -1,44 +0,0 @@ -div.vue-slider { - @apply mt-5 mb-3.5 max-w-sm; -} - -div.vue-slider-process, -div.vue-slider-rail { - @apply h-2 bg-primary !important; -} - -div.vue-slider-rail { - @apply bg-emphasis !important; -} - -div.vue-slider .vue-slider-dot { - @apply size-6 !important; -} - -div.vue-slider-dot-tooltip-inner { - @apply bg-white text border border-default px-1.5 !important; -} - -span.vue-slider-dot-tooltip-text { - @apply font-medium font-sans text; -} - -div.vue-slider-dot-tooltip::before { - @apply hidden; -} - -div.vue-slider-dot .vue-slider-dot-handle { - @apply border border-border shadow; -} - -div.vue-slider-tooltip-wrap.vue-slider-tooltip-top { - @apply font-sans; -} - -.price-input { - @apply space-x-3; -} - -.vue-slider-component.vue-slider-horizontal { - @apply mt-5; -} diff --git a/resources/css/components/vue-slider.css b/resources/css/components/vue-slider.css deleted file mode 100644 index 53a7b0acd..000000000 --- a/resources/css/components/vue-slider.css +++ /dev/null @@ -1,11 +0,0 @@ -.vue-slider { - @apply mx-2; -} - -.vue-slider-dot-tooltip-inner { - @apply border-primary bg-primary !important; -} - -.vue-slider-process { - @apply bg-primary !important; -} diff --git a/resources/js/callbacks.js b/resources/js/callbacks.js index 374572c8b..f2012f8ea 100644 --- a/resources/js/callbacks.js +++ b/resources/js/callbacks.js @@ -3,16 +3,18 @@ import { fillFromGraphqlResponse as updateOrder } from './stores/useOrder' import { runAfterPlaceOrderHandlers, runBeforePaymentMethodHandlers, runBeforePlaceOrderHandlers } from './stores/usePaymentHandlers' import { refresh as refreshUser, token } from './stores/useUser' -Vue.prototype.scrollToElement = (selector) => { +Vue.prototype.scrollToElement = (selector, offset = 0) => { let el = window.document.querySelector(selector) - window.scrollTo({ - top: el.offsetTop, - behavior: 'smooth', - }) + if (el) { + window.scrollTo({ + top: el.offsetTop - offset, + behavior: 'smooth', + }) + } } Vue.prototype.getCheckoutStep = (stepName) => { - return (config.checkout_steps[config.store_code] ?? config.checkout_steps['default'])?.indexOf(stepName) + return config.checkout_steps?.indexOf(stepName) } Vue.prototype.submitPartials = async function (form, sequential = false) { diff --git a/resources/js/components/Elements/RangeSlider.vue b/resources/js/components/Elements/RangeSlider.vue new file mode 100644 index 000000000..a2f3a8d7c --- /dev/null +++ b/resources/js/components/Elements/RangeSlider.vue @@ -0,0 +1,85 @@ + diff --git a/resources/js/components/Listing/Filters/CategoryFilter.vue b/resources/js/components/Listing/Filters/CategoryFilter.vue deleted file mode 100644 index 4561a17f5..000000000 --- a/resources/js/components/Listing/Filters/CategoryFilter.vue +++ /dev/null @@ -1,155 +0,0 @@ - diff --git a/resources/js/components/Listing/Filters/CategoryFilterCategory.vue b/resources/js/components/Listing/Filters/CategoryFilterCategory.vue deleted file mode 100644 index e772015f0..000000000 --- a/resources/js/components/Listing/Filters/CategoryFilterCategory.vue +++ /dev/null @@ -1,45 +0,0 @@ - - - diff --git a/resources/js/components/Listing/Filters/SelectedFiltersValues.vue b/resources/js/components/Listing/Filters/SelectedFiltersValues.vue deleted file mode 100644 index 5c683151e..000000000 --- a/resources/js/components/Listing/Filters/SelectedFiltersValues.vue +++ /dev/null @@ -1,56 +0,0 @@ - diff --git a/resources/js/components/Listing/Listing.vue b/resources/js/components/Listing/Listing.vue index 12a9e296d..8ba9f3718 100644 --- a/resources/js/components/Listing/Listing.vue +++ b/resources/js/components/Listing/Listing.vue @@ -1,129 +1,174 @@ diff --git a/resources/js/components/Listing/SearchSuggestions.vue b/resources/js/components/Listing/SearchSuggestions.vue new file mode 100644 index 000000000..5fda00957 --- /dev/null +++ b/resources/js/components/Listing/SearchSuggestions.vue @@ -0,0 +1,82 @@ + diff --git a/resources/js/components/Product/AddToCart.vue b/resources/js/components/Product/AddToCart.vue index 1d3e768d6..f592fde04 100644 --- a/resources/js/components/Product/AddToCart.vue +++ b/resources/js/components/Product/AddToCart.vue @@ -3,6 +3,21 @@ import { GraphQLError } from '../../fetch' import { mask, refreshMask } from '../../stores/useMask' export default { + inject: { + instantSearchInstance: { + from: '$_ais_instantSearchInstance', + default: () => { + return { + helper: { + state: { + disjunctiveFacetsRefinements: {}, + }, + }, + } + }, + }, + }, + props: { product: { type: Object, @@ -50,6 +65,7 @@ export default { this.$nextTick(() => { this.setDefaultOptions() this.setDefaultCustomSelectedOptions() + this.setOptionsFromRefinements() }) }, @@ -150,8 +166,8 @@ export default { }, getOptions: function (superAttributeCode) { - if (this.$root.swatches.hasOwnProperty(superAttributeCode)) { - let swatchOptions = this.$root.swatches[superAttributeCode].options + if (window.config.swatches.hasOwnProperty(superAttributeCode)) { + let swatchOptions = window.config.swatches[superAttributeCode].options let values = {} Object.entries(this.product['super_' + superAttributeCode]).forEach(([key, val]) => { @@ -255,6 +271,15 @@ export default { Vue.set(this.customSelectedOptions, option.option_id, value) }) }, + async setOptionsFromRefinements() { + Object.entries(this.refinementOptions).forEach(([key, availableValue]) => { + if (!availableValue.length) { + return + } + + Vue.set(this.options, key, availableValue[0]) + }) + }, }, computed: { @@ -410,9 +435,40 @@ export default { }) return valuesPerAttribute }, + + superRefinements() { + // Note that `disjunctiveFacetsRefinements` is only one of 5 total sets of refinements that gets exposed by the state + // It looks like all super attributes will end up in there, so right now it's the only one we check + let disjunctiveFacetsRefinements = this.instantSearchInstance.helper.state.disjunctiveFacetsRefinements + return Object.fromEntries( + Object.entries(disjunctiveFacetsRefinements) + .filter(([key, value]) => key.startsWith('super_') && value.length > 0) + .map(([key, value]) => [key.replace('super_', ''), value]), + ) + }, + + refinementOptions() { + // Options per super attribute that match the current refinements. + return Object.fromEntries( + Object.entries(this.product.super_attributes).map(([index, attribute]) => { + return [ + index, + (Object.entries(this.superRefinements).find(([key, value]) => key === attribute.code)?.[1] || []).filter((val) => + this.enabledOptions[index].includes(val * 1), + ), + ] // Filter out disabled options + }), + ) + }, }, watch: { + superRefinements: { + handler(refinements) { + this.setOptionsFromRefinements() + }, + deep: true, + }, customOptions: { handler() { this.calculatePrices() diff --git a/resources/js/components/Recursion.vue b/resources/js/components/Recursion.vue new file mode 100644 index 000000000..2859d202b --- /dev/null +++ b/resources/js/components/Recursion.vue @@ -0,0 +1,38 @@ + + + diff --git a/resources/js/components/Search/AisStatsAnalytics.vue b/resources/js/components/Search/AisStatsAnalytics.vue new file mode 100644 index 000000000..15d21c2fb --- /dev/null +++ b/resources/js/components/Search/AisStatsAnalytics.vue @@ -0,0 +1,43 @@ + + diff --git a/resources/js/components/Search/Autocomplete.vue b/resources/js/components/Search/Autocomplete.vue index 09566f2eb..34a11ea1d 100644 --- a/resources/js/components/Search/Autocomplete.vue +++ b/resources/js/components/Search/Autocomplete.vue @@ -1,126 +1,142 @@ diff --git a/resources/js/fetch.js b/resources/js/fetch.js index 00488c7ff..ac56ec264 100644 --- a/resources/js/fetch.js +++ b/resources/js/fetch.js @@ -38,6 +38,7 @@ export const rapidezAPI = (window.rapidezAPI = async (method, endpoint, data = { method: method.toUpperCase(), headers: Object.assign( { + Accept: 'application/json', Store: window.config.store_code, Authorization: token.value ? `Bearer ${token.value}` : null, 'Content-Type': 'application/json', diff --git a/resources/js/filters.js b/resources/js/filters.js index 077e90e2b..0ec51eb33 100644 --- a/resources/js/filters.js +++ b/resources/js/filters.js @@ -8,10 +8,11 @@ window.truncate = function (value, limit) { Vue.filter('truncate', window.truncate) -window.price = function (value) { +window.price = function (value, extra = {}) { return new Intl.NumberFormat(config.locale.replace('_', '-'), { style: 'currency', currency: config.currency, + ...extra, }).format(value) } diff --git a/resources/js/instantsearch.js b/resources/js/instantsearch.js new file mode 100644 index 000000000..6d8499533 --- /dev/null +++ b/resources/js/instantsearch.js @@ -0,0 +1,48 @@ +import { addQuery } from './stores/useSearchHistory' + +// Shared between Autocomplete and Listing +Vue.component('ais-instant-search', () => import('vue-instantsearch/vue2/es/src/components/InstantSearch')) +Vue.component('ais-hits', () => import('vue-instantsearch/vue2/es/src/components/Hits.js')) +Vue.component('ais-configure', () => import('vue-instantsearch/vue2/es/src/components/Configure.js')) +Vue.component('ais-highlight', () => import('vue-instantsearch/vue2/es/src/components/Highlight.vue.js')) +Vue.component('ais-search-box', () => import('vue-instantsearch/vue2/es/src/components/SearchBox.vue.js')) +Vue.component('ais-state-results', () => import('vue-instantsearch/vue2/es/src/components/StateResults.vue.js')) + +// Used by Autocomplete +Vue.component('ais-index', () => import('vue-instantsearch/vue2/es/src/components/Index.js')) + +// Used by Listing +Vue.component('ais-refinement-list', () => import('vue-instantsearch/vue2/es/src/components/RefinementList.vue.js')) +Vue.component('ais-hierarchical-menu', () => import('vue-instantsearch/vue2/es/src/components/HierarchicalMenu.vue.js')) +Vue.component('ais-range-input', () => import('vue-instantsearch/vue2/es/src/components/RangeInput.vue.js')) +Vue.component('ais-current-refinements', () => import('vue-instantsearch/vue2/es/src/components/CurrentRefinements.vue.js')) +Vue.component('ais-clear-refinements', () => import('vue-instantsearch/vue2/es/src/components/ClearRefinements.vue.js')) +Vue.component('ais-hits-per-page', () => import('vue-instantsearch/vue2/es/src/components/HitsPerPage.vue.js')) +Vue.component('ais-sort-by', () => import('vue-instantsearch/vue2/es/src/components/SortBy.vue.js')) +Vue.component('ais-pagination', () => import('vue-instantsearch/vue2/es/src/components/Pagination.vue.js')) +Vue.component('ais-stats', () => import('vue-instantsearch/vue2/es/src/components/Stats.vue.js')) +Vue.component('ais-stats-analytics', () => import('./components/Search/AisStatsAnalytics.vue')) + +document.addEventListener('insights-event:viewedObjectIDs', (event) => { + setTimeout(() => { + if (event?.detail?.insightsEvent?.eventType !== 'search') { + return + } + + let url = new URL(window.location.href) + if ( + url.pathname !== '/search' || + !url.searchParams.has('q') || + url.searchParams.get('q') !== event.detail.insightsEvent.payload.query + ) { + // Do not track autocomplete + return + } + + if (event.detail.insightsEvent.payload.nbHits < 1) { + return + } + + addQuery(event.detail.insightsEvent.payload.query, { hits: event.detail.insightsEvent.payload.nbHits }) + }) +}) diff --git a/resources/js/mixins.js b/resources/js/mixins.js index 3c41d22d3..46bb7eb4c 100644 --- a/resources/js/mixins.js +++ b/resources/js/mixins.js @@ -15,5 +15,31 @@ Vue.mixin({ return await window.magentoAPI(method, 'guest-carts/' + mask.value + '/' + endpoint, data) } }, + + attributeLabel(attributeCode) { + return Object.values(window.config.attributes)?.find((attribute) => attribute.code === attributeCode)?.name + }, + }, + + computed: { + currencySymbolLocation() { + return new Intl.NumberFormat(config.locale.replace('_', '-'), { + style: 'currency', + currency: config.currency, + }).formatToParts(1)?.[0]?.type === 'currency' + ? 'left' + : 'right' + }, + + currencySymbol() { + return new Intl.NumberFormat(config.locale.replace('_', '-'), { + style: 'currency', + currency: config.currency, + maximumFractionDigits: 0, + }) + .format(0) + .replace(/\d/g, '') + .trim() + }, }, }) diff --git a/resources/js/package.js b/resources/js/package.js index d045be31a..4a0807e7e 100644 --- a/resources/js/package.js +++ b/resources/js/package.js @@ -12,8 +12,6 @@ import useOrder from './stores/useOrder.js' import useCart from './stores/useCart' import useUser from './stores/useUser' import useMask from './stores/useMask' -import { swatches, clear as clearSwatches } from './stores/useSwatches' -import { clear as clearAttributes } from './stores/useAttributes.js' import './vue' import './fetch' import './filters' @@ -21,6 +19,7 @@ import './mixins' import './cookies' import './callbacks' import './vue-components' +import './instantsearch' import { fetchCount } from './stores/useFetches.js' ;(() => import('./turbolinks'))() @@ -43,15 +42,6 @@ if (import.meta.env.VITE_DEBUG === 'true') { }) } -document.addEventListener('vue:loaded', () => { - const lastStoreCode = useLocalStorage('last_store_code', window.config.store_code) - if (lastStoreCode.value !== window.config.store_code) { - clearAttributes() - clearSwatches() - lastStoreCode.value = window.config.store_code - } -}) - let booting = false function init() { if (booting || document.body.contains(window.app.$el)) { @@ -101,14 +91,13 @@ function init() { config: window.config, loadingCount: fetchCount, loading: false, - loadAutocomplete: false, + autocompleteFacadeQuery: '', csrfToken: document.querySelector('[name=csrf-token]')?.content, cart: useCart(), order: useOrder(), user: useUser(), mask: useMask(), showTax: window.config.show_tax, - swatches: swatches, scrollLock: useScrollLock(document.body), }, methods: { @@ -140,6 +129,18 @@ function init() { return `/storage/${store}/resizes/${size}/magento${url}` }, + + categoryPositions(categoryId) { + return { + function_score: { + script_score: { + script: { + source: `Integer.parseInt(doc['positions.${categoryId}'].empty ? '0' : doc['positions.${categoryId}'].value)`, + }, + }, + }, + } + }, }, computed: { // Wrap the local storage in getter and setter functions so you do not have to interact using .value @@ -158,10 +159,6 @@ function init() { canOrder() { return this.cart.items.every((item) => item.is_available) }, - - queryParams() { - return new URLSearchParams(window.location.search) - }, }, watch: { loadingCount: function (count) { diff --git a/resources/js/stores/useAttributes.js b/resources/js/stores/useAttributes.js deleted file mode 100644 index e16ef9b6a..000000000 --- a/resources/js/stores/useAttributes.js +++ /dev/null @@ -1,45 +0,0 @@ -// TODO: In this file there is a lot of duplication compared to useSwatches. Can we improve that? -import { computedAsync, useLocalStorage } from '@vueuse/core' -import { computed } from 'vue' - -export const attributesStorage = useLocalStorage('attributes', {}) -let isRefreshing = false - -export const refresh = async function () { - if (isRefreshing) { - console.debug('Refresh canceled, request already in progress...') - return - } - - try { - isRefreshing = true - attributesStorage.value = (await window.rapidezAPI('get', 'attributes')) || {} - isRefreshing = false - } catch (error) { - isRefreshing = false - console.error(error) - Notify(window.config.translations.errors.wrong, 'error') - } -} - -export const clear = async function () { - attributesStorage.value = {} -} - -export const attributes = computedAsync( - async () => { - if (Object.keys(attributesStorage.value).length === 0) { - await refresh() - } - - return attributesStorage - }, - attributesStorage.value, - { lazy: true, shallow: false }, -) - -window.attributeLabel = computed((attributeCode) => { - return Object.values(attributes.value)?.find((attribute) => attribute.code === attributeCode)?.name -}) - -export default () => attributes diff --git a/resources/js/stores/useInstantsearchMiddlewares.js b/resources/js/stores/useInstantsearchMiddlewares.js new file mode 100644 index 000000000..9bb2fef86 --- /dev/null +++ b/resources/js/stores/useInstantsearchMiddlewares.js @@ -0,0 +1,9 @@ +export let instantsearchMiddlewares = [] + +export async function addInstantsearchMiddleware(middlewareFn) { + instantsearchMiddlewares.push(middlewareFn) +} + +export async function removeInstantsearchMiddleware(middlewareFn) { + instantsearchMiddlewares = instantsearchMiddlewares.filter((registeredMiddleware) => registeredMiddleware !== middlewareFn) +} diff --git a/resources/js/stores/useSearchHistory.js b/resources/js/stores/useSearchHistory.js new file mode 100644 index 000000000..f6ac505d4 --- /dev/null +++ b/resources/js/stores/useSearchHistory.js @@ -0,0 +1,30 @@ +import { useLocalStorage } from '@vueuse/core' + +/** + * @typedef {Object} SearchHistory + * { + * [query: string]: { + * count: number, + * lastSearched: string, + * hits: number, + * } + */ +export const searchHistory = useLocalStorage('search_history', {}) + +export const addQuery = (query, metadata = {}) => { + query = query.toLowerCase() + Vue.set(searchHistory.value, query, { + ...searchHistory.value[query], + count: (searchHistory.value[query]?.count || 0) + 1, + lastSearched: new Date().toISOString(), + ...metadata, + }) +} + +export const removeQuery = (query) => { + if (searchHistory.value[query]) { + delete searchHistory.value[query] + } +} + +export default () => searchHistory diff --git a/resources/js/stores/useSwatches.js b/resources/js/stores/useSwatches.js deleted file mode 100644 index 42772dedc..000000000 --- a/resources/js/stores/useSwatches.js +++ /dev/null @@ -1,43 +0,0 @@ -// TODO: In this file there is a lot of duplication compared to useAttributes. Can we improve that? -import { computedAsync, useLocalStorage } from '@vueuse/core' - -export const swatchesStorage = useLocalStorage('swatches', {}) -let isRefreshing = false -let hasFetched = false - -export const refresh = async function () { - if (isRefreshing) { - console.debug('Refresh canceled, request already in progress...') - return - } - - try { - isRefreshing = true - swatchesStorage.value = (await window.rapidezAPI('get', 'swatches')) || {} - isRefreshing = false - hasFetched = true - } catch (error) { - isRefreshing = false - console.error(error) - Notify(window.config.translations.errors.wrong, 'error') - } -} - -export const clear = async function () { - swatchesStorage.value = {} - hasFetched = false -} - -export const swatches = computedAsync( - async () => { - if (!hasFetched && Object.keys(swatchesStorage.value).length === 0) { - await refresh() - } - - return swatchesStorage - }, - swatchesStorage.value, - { lazy: true, shallow: false }, -) - -export default () => swatches diff --git a/resources/js/turbolinks.js b/resources/js/turbolinks.js index 01bb2931e..f6feb0ae9 100644 --- a/resources/js/turbolinks.js +++ b/resources/js/turbolinks.js @@ -1,3 +1,10 @@ import * as Turbo from '@hotwired/turbo' Turbo.config.drive.progressBarDelay = 5 + +document.addEventListener('turbo:before-visit', function (e) { + if (typeof history.state.turbo === 'undefined') { + // Trigger turbo to add the state. + Turbo?.navigator?.history?.replace(window.location) + } +}) diff --git a/resources/js/vue-components.js b/resources/js/vue-components.js index 4d530eb5c..0060db546 100644 --- a/resources/js/vue-components.js +++ b/resources/js/vue-components.js @@ -21,6 +21,8 @@ import graphql from './components/Graphql.vue' Vue.component('graphql', graphql) import graphqlMutation from './components/GraphqlMutation.vue' Vue.component('graphql-mutation', graphqlMutation) +import recursion from './components/Recursion.vue' +Vue.component('recursion', recursion) import notifications from './components/Notifications/Notifications.vue' Vue.component('notifications', notifications) @@ -35,10 +37,29 @@ Vue.component('images', images) import quantitySelect from './components/Product/QuantitySelect.vue' Vue.component('quantity-select', quantitySelect) -Vue.component('autocomplete', () => import('./components/Search/Autocomplete.vue')) +Vue.component('autocomplete', () => ({ + // https://v2.vuejs.org/v2/guide/components-dynamic-async.html#Async-Components + component: new Promise(function (resolve, reject) { + document.addEventListener('loadAutoComplete', () => import('./components/Search/Autocomplete.vue').then(resolve)) + }), + // https://v2.vuejs.org/v2/guide/components-dynamic-async.html#Handling-Loading-State + loading: { + data: () => ({ + loaded: false, + searchClient: null, + searchHistory: {}, + }), + + render() { + return this.$scopedSlots.default(this) + }, + }, + delay: 0, +})) Vue.component('checkout-login', () => import('./components/Checkout/CheckoutLogin.vue')) Vue.component('login', () => import('./components/User/Login.vue')) Vue.component('listing', () => import('./components/Listing/Listing.vue')) +Vue.component('search-suggestions', () => import('./components/Listing/SearchSuggestions.vue')) Vue.component('checkout-success', () => import('./components/Checkout/CheckoutSuccess.vue')) Vue.component('popup', () => import('./components/Popup.vue')) -Vue.component('selected-filters-values', () => import('./components/Listing/Filters/SelectedFiltersValues.vue')) +Vue.component('range-slider', () => import('./components/Elements/RangeSlider.vue')) diff --git a/resources/views/cart/item.blade.php b/resources/views/cart/item.blade.php index 0596a6a22..963cb2f2f 100644 --- a/resources/views/cart/item.blade.php +++ b/resources/views/cart/item.blade.php @@ -35,7 +35,7 @@ class="mx-auto"
@include('rapidez::cart.item.remove') diff --git a/resources/views/category/overview.blade.php b/resources/views/category/overview.blade.php index 95e9985a1..738084fed 100644 --- a/resources/views/category/overview.blade.php +++ b/resources/views/category/overview.blade.php @@ -8,16 +8,23 @@ @section('content')
@include('rapidez::category.partials.breadcrumbs') -

{{ $category->name }}

@if ($category->is_anchor) - + + +

{{ $category->name }}

+
+
@else -
-
+
+
+

{{ $category->name }}

@widget('sidebar.main', 'anchor_categories', 'catalog_category_view_type_layered', $category->entity_id)
-
+
@widget('content.top', 'anchor_categories', 'catalog_category_view_type_layered', $category->entity_id)
diff --git a/resources/views/components/autocomplete/input.blade.php b/resources/views/components/autocomplete/input.blade.php new file mode 100644 index 000000000..892202d1a --- /dev/null +++ b/resources/views/components/autocomplete/input.blade.php @@ -0,0 +1,30 @@ +
+ merge([ + 'id' => 'autocomplete-input', + 'type' => 'search', + 'name' => 'q', + 'autocomplete' => 'off', + 'autocorrect' => 'off', + 'autocapitalize' => 'none', + 'spellcheck' => 'false', + 'placeholder' => __('What are you looking for?'), + 'class' => 'h-12 peer', + ]) }} + /> + + + + + diff --git a/resources/views/components/autocomplete/magnifying-glass.blade.php b/resources/views/components/autocomplete/magnifying-glass.blade.php index 749f8ac0a..27f390da7 100644 --- a/resources/views/components/autocomplete/magnifying-glass.blade.php +++ b/resources/views/components/autocomplete/magnifying-glass.blade.php @@ -1,4 +1,4 @@ {{-- This is the search icon in the autocomplete --}} -
twMerge('transition-colors duration-300 rounded-lg bg-muted absolute right-1 inset-y-1 w-11 pointer-events-none flex items-center justify-center text z-header-autocomplete-button') }}> +
twMerge('transition-colors duration-300 rounded-lg bg-primary text-primary-text absolute right-1 inset-y-1 w-11 pointer-events-none flex items-center justify-center z-header-autocomplete-button') }}>
diff --git a/resources/views/components/filter/heading.blade.php b/resources/views/components/filter/heading.blade.php index 5da92c527..1cbb65024 100644 --- a/resources/views/components/filter/heading.blade.php +++ b/resources/views/components/filter/heading.blade.php @@ -1,20 +1,31 @@ @slots(['title']) +{{-- +TODO: Can't we use
with for this? +Or maybe is JS easier so we don't need an input? +--}} -