diff --git a/client-app/router/routes/constants.ts b/client-app/router/routes/constants.ts index dc1157e846..f6c75cc2d0 100644 --- a/client-app/router/routes/constants.ts +++ b/client-app/router/routes/constants.ts @@ -1,11 +1,10 @@ -type RouteType = { - NAME: string; - PATH: string; -}; - -export const ROUTES: { [key: string]: RouteType } = { +export const ROUTES = { CATALOG: { NAME: "Catalog", PATH: "/catalog", }, + SEARCH: { + NAME: "Search", + PATH: "/search", + }, } as const; diff --git a/client-app/router/routes/main.ts b/client-app/router/routes/main.ts index 7f80b8f21e..af0d65168b 100644 --- a/client-app/router/routes/main.ts +++ b/client-app/router/routes/main.ts @@ -63,7 +63,7 @@ export const mainRoutes: RouteRecordRaw[] = [ }, }, { path: "/branch/:branchId", name: "BranchPage", component: Branch, props: true }, - { path: "/search", name: "Search", component: Search }, + { path: ROUTES.SEARCH.PATH, name: ROUTES.SEARCH.NAME, component: Search }, { path: "/bulk-order", name: "BulkOrder", component: BulkOrder }, { path: "/compare", name: "CompareProducts", component: CompareProducts }, { path: "/cart", name: "Cart", component: Cart }, diff --git a/client-app/shared/layout/components/header/_internal/mobile-header.vue b/client-app/shared/layout/components/header/_internal/mobile-header.vue index 714ae22e55..174a32bdb8 100644 --- a/client-app/shared/layout/components/header/_internal/mobile-header.vue +++ b/client-app/shared/layout/components/header/_internal/mobile-header.vue @@ -87,11 +87,15 @@ maxlength="64" :placeholder="$t('shared.layout.header.mobile.search_bar.input_placeholder')" class="mr-4 grow" + :clearable="!!searchPhrase" no-border - clearable - @keydown.enter="searchPhrase && $router.push(searchPageLink)" @clear="reset" - /> + @keydown.enter="searchPhrase && $router.push(searchPageLink)" + > + + + + @@ -137,6 +141,7 @@ import { useSearchBar } from "@/shared/layout/composables/useSearchBar"; import MobileMenu from "./mobile-menu/mobile-menu.vue"; import type { StyleValue } from "vue"; import type { RouteLocationRaw } from "vue-router"; +import BarcodeScanner from "@/shared/layout/components/search-bar/barcode-scanner.vue"; const router = useRouter(); const { customComponents } = useCustomMobileHeaderComponents(); @@ -158,7 +163,7 @@ const placeholderStyle = computed(() => ); const searchPageLink = computed(() => ({ - name: "Search", + name: ROUTES.SEARCH.NAME, query: { [QueryParamName.SearchPhrase]: searchPhrase.value.trim(), }, @@ -166,7 +171,14 @@ const searchPageLink = computed(() => ({ function reset() { searchPhrase.value = ""; - void router.push({ name: ROUTES.CATALOG.NAME }); + void router.push({ name: ROUTES.SEARCH.NAME }); +} + +function onBarcodeScanned(value: string) { + if (value) { + searchPhrase.value = value; + void router.push(searchPageLink.value); + } } syncRefs(mobileMenuVisible, useScrollLock(document.body)); diff --git a/client-app/shared/layout/components/search-bar/barcode-scanner-modal.vue b/client-app/shared/layout/components/search-bar/barcode-scanner-modal.vue new file mode 100644 index 0000000000..c4ed37cb2e --- /dev/null +++ b/client-app/shared/layout/components/search-bar/barcode-scanner-modal.vue @@ -0,0 +1,212 @@ + + + + {{ $t("shared.layout.search_bar.barcode_detector.description") }} + + + + + + + + + + + + + + + + + + {{ $t("shared.layout.search_bar.barcode_detector.cancel") }} + + {{ $t("shared.layout.search_bar.barcode_detector.browse") }} + + + + + + + diff --git a/client-app/shared/layout/components/search-bar/barcode-scanner.vue b/client-app/shared/layout/components/search-bar/barcode-scanner.vue new file mode 100644 index 0000000000..98cb4ade33 --- /dev/null +++ b/client-app/shared/layout/components/search-bar/barcode-scanner.vue @@ -0,0 +1,45 @@ + + + + + + + diff --git a/client-app/shared/layout/components/search-bar/search-bar.vue b/client-app/shared/layout/components/search-bar/search-bar.vue index 8019ff6b89..31cf72bf28 100644 --- a/client-app/shared/layout/components/search-bar/search-bar.vue +++ b/client-app/shared/layout/components/search-bar/search-bar.vue @@ -5,15 +5,17 @@ type="search" :maxlength="MAX_LENGTH" class="w-full" + :clearable="!!searchPhrase" :placeholder="$t('shared.layout.search_bar.enter_keyword_placeholder')" - clearable + @clear="reset" @keyup.enter="goToSearchResultsPage" @keyup.esc="hideSearchDropdown" @input="onSearchPhraseChanged" @focus="onSearchBarFocused" - @clear="reset" > + + { function getSearchRoute(phrase: string): RouteLocationRaw { return { - name: "Search", + name: ROUTES.SEARCH.NAME, query: { [QueryParamName.SearchPhrase]: phrase, }, @@ -291,7 +295,7 @@ function goToSearchResultsPage() { function reset() { searchPhrase.value = ""; hideSearchDropdown(); - void router.push({ name: ROUTES.CATALOG.NAME }); + void router.push({ name: ROUTES.SEARCH.NAME }); } const searchProductsDebounced = useDebounceFn(searchAndShowDropdownResults, SEARCH_BAR_DEBOUNCE_TIME); @@ -321,4 +325,11 @@ onMounted(() => { searchPhrase.value = searchPhraseInUrl.value; } }); + +const onBarcodeScanned = (value: string) => { + if (value) { + searchPhrase.value = value; + goToSearchResultsPage(); + } +}; diff --git a/client-app/ui-kit/components/molecules/dialog-content/vc-dialog-content.vue b/client-app/ui-kit/components/molecules/dialog-content/vc-dialog-content.vue index 9496aa387d..3c453066bc 100644 --- a/client-app/ui-kit/components/molecules/dialog-content/vc-dialog-content.vue +++ b/client-app/ui-kit/components/molecules/dialog-content/vc-dialog-content.vue @@ -16,14 +16,6 @@ &__container { @apply py-2 px-6; - - @at-root *:has(:not(.vc-dialog-footer)) > & { - @apply pb-4; - } - - @at-root *:has(:not(.vc-dialog-header)) > & { - @apply pt-4; - } } } diff --git a/client-app/ui-kit/icons/barcode.svg b/client-app/ui-kit/icons/barcode.svg new file mode 100644 index 0000000000..1caefdd34d --- /dev/null +++ b/client-app/ui-kit/icons/barcode.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/locales/de.json b/locales/de.json index 5b8515079c..2721dadd4b 100644 --- a/locales/de.json +++ b/locales/de.json @@ -377,7 +377,18 @@ "no_results": "Ihre {keyword} Abfrage hat keine Ergebnisse geliefert", "enter_keyword_placeholder": "Hier nach Produkten suchen", "search_button": "Suchen", - "view_all_results_button": "Alle {total} Ergebnisse anzeigen" + "view_all_results_button": "Alle {total} Ergebnisse anzeigen", + "barcode_detector": { + "title": "Barcode-Scan", + "description": "Um fortzufahren, aktivieren Sie bitte Ihre Kamera, um den Barcode auf dem Produkt zu scannen. Dies hilft uns, den Artikel schnell zu identifizieren und Ihnen die notwendigen Informationen bereitzustellen.", + "errors": { + "no_barcode_detected": "Keine Barcodes im Bild erkannt", + "wrong_format": "Ungültiges Dateiformat", + "no_file": "Keine Datei hochgeladen" + }, + "browse": "Dateien durchsuchen", + "cancel": "@:common.buttons.cancel" + } }, "language_selector": { "label": "Sprache:" @@ -1473,4 +1484,4 @@ "INVALID_SIZE": "Dateigröße überschreitet die erlaubte {0}.", "CANNOT_DELETE": "Fehler beim Löschen der Datei." } -} +} \ No newline at end of file diff --git a/locales/en.json b/locales/en.json index e6c1d68d2e..429ed5af13 100644 --- a/locales/en.json +++ b/locales/en.json @@ -377,7 +377,18 @@ "no_results": "Your {keyword} query did not return any results", "enter_keyword_placeholder": "Search for products here", "search_button": "Search", - "view_all_results_button": "View all {total} results" + "view_all_results_button": "View all {total} results", + "barcode_detector": { + "title": "Barcode scan", + "description": "To proceed, please enable your camera to scan the barcode on the product. This will help us quickly identify the item and provide you with the necessary information.", + "browse": "Browse files", + "cancel": "@:common.buttons.cancel", + "errors": { + "no_barcode_detected": "No barcodes detected in the image", + "wrong_format": "Unsupported file format", + "no_file": "No file uploaded" + } + } }, "language_selector": { "label": "Language:" @@ -1473,4 +1484,4 @@ "INVALID_SIZE": "File size exceeds the allowed {0}", "CANNOT_DELETE": "Failure while deleting file" } -} +} \ No newline at end of file diff --git a/locales/es.json b/locales/es.json index 82d6e0ca75..8d039a3b9d 100644 --- a/locales/es.json +++ b/locales/es.json @@ -377,7 +377,18 @@ "no_results": "Tu consulta {keyword} no devolvió ningún resultado", "enter_keyword_placeholder": "Buscar productos aquí", "search_button": "Buscar", - "view_all_results_button": "Ver todos los {total} resultados" + "view_all_results_button": "Ver todos los {total} resultados", + "barcode_detector": { + "title": "Escanear código de barras", + "description": "Para continuar, habilita tu cámara para escanear el código de barras del producto. Esto nos ayudará a identificar rápidamente el artículo y proporcionarte la información necesaria.", + "errors": { + "no_barcode_detected": "No se detectaron códigos de barras en la imagen", + "wrong_format": "Formato de archivo no admitido", + "no_file": "Ningún archivo subido" + }, + "browse": "Explorar archivos", + "cancel": "@:common.buttons.cancel" + } }, "language_selector": { "label": "Idioma:" @@ -1473,4 +1484,4 @@ "INVALID_SIZE": "File size exceeds the allowed {0}.", "CANNOT_DELETE": "Failure while deleting file." } -} +} \ No newline at end of file diff --git a/locales/fr.json b/locales/fr.json index c91013e300..abf723773a 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -377,7 +377,18 @@ "no_results": "Votre requête {keyword} n'a retourné aucun résultat", "enter_keyword_placeholder": "Recherchez des produits ici", "search_button": "Rechercher", - "view_all_results_button": "Voir tous les {total} résultats" + "view_all_results_button": "Voir tous les {total} résultats", + "barcode_detector": { + "title": "Scan du code-barres", + "description": "Pour continuer, veuillez activer votre caméra pour scanner le code-barres sur le produit. Cela nous aidera à identifier rapidement l'article et à vous fournir les informations nécessaires.", + "errors": { + "no_barcode_detected": "Aucun code-barres détecté dans l'image", + "wrong_format": "Format de fichier non pris en charge", + "no_file": "Aucun fichier n'a été téléchargé" + }, + "browse": "Parcourir les fichiers", + "cancel": "@:common.buttons.cancel" + } }, "language_selector": { "label": "Langue :" @@ -1473,4 +1484,4 @@ "INVALID_SIZE": "La taille du fichier dépasse la limite autorisée de {0}.", "CANNOT_DELETE": "Échec lors de la suppression du fichier." } -} +} \ No newline at end of file diff --git a/locales/it.json b/locales/it.json index 24685ff8b3..9849855091 100644 --- a/locales/it.json +++ b/locales/it.json @@ -377,7 +377,18 @@ "no_results": "La tua ricerca {keyword} non ha prodotto risultati", "enter_keyword_placeholder": "Cerca prodotti qui", "search_button": "Cerca", - "view_all_results_button": "Visualizza tutti i {total} risultati" + "view_all_results_button": "Visualizza tutti i {total} risultati", + "barcode_detector": { + "title": "Scansione codice a barre", + "description": "Per procedere, abilita la fotocamera per scansionare il codice a barre sul prodotto. Questo ci aiuterà a identificare rapidamente l'articolo e fornirti le informazioni necessarie.", + "errors": { + "no_barcode_detected": "Nessun codice a barre rilevato nell'immagine", + "wrong_format": "Formato file non supportato", + "no_file": "Nessun file caricato" + }, + "browse": "Sfoglia file", + "cancel": "@:common.buttons.cancel" + } }, "language_selector": { "label": "Lingua:" @@ -1473,4 +1484,4 @@ "INVALID_SIZE": "La dimensione del file supera il limite consentito di {0}.", "CANNOT_DELETE": "Errore durante l'eliminazione del file." } -} +} \ No newline at end of file diff --git a/locales/ja.json b/locales/ja.json index db2fa5c70b..d0215b742c 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -377,7 +377,18 @@ "no_results": "{keyword}の検索結果はありません", "enter_keyword_placeholder": "商品を検索", "search_button": "検索", - "view_all_results_button": "全{total}件を表示" + "view_all_results_button": "全{total}件を表示", + "barcode_detector": { + "title": "バーコードスキャン", + "description": "続けるには、カメラを有効にして製品のバーコードをスキャンしてください。これにより、商品を迅速に特定し、必要な情報を提供できます。", + "errors": { + "no_barcode_detected": "画像にバーコードが検出されませんでした", + "wrong_format": "対応していないファイル形式です", + "no_file": "ファイルはアップロードされていません" + }, + "browse": "ファイルを参照", + "cancel": "@:common.buttons.cancel" + } }, "language_selector": { "label": "言語:" @@ -1473,4 +1484,4 @@ "INVALID_SIZE": "ファイルサイズが許容サイズ{0}を超えています", "CANNOT_DELETE": "ファイルの削除中にエラーが発生しました" } -} +} \ No newline at end of file diff --git a/locales/pl.json b/locales/pl.json index 34e6df6485..d565b0c198 100644 --- a/locales/pl.json +++ b/locales/pl.json @@ -377,7 +377,18 @@ "no_results": "Twoje zapytanie {keyword} nie zwróciło żadnych wyników", "enter_keyword_placeholder": "Szukaj produktów tutaj", "search_button": "Szukaj", - "view_all_results_button": "Zobacz wszystkie {total} wyniki" + "view_all_results_button": "Zobacz wszystkie {total} wyniki", + "barcode_detector": { + "title": "Skanowanie kodu kreskowego", + "description": "Aby kontynuować, włącz kamerę, aby zeskanować kod kreskowy na produkcie. Pomoże nam to szybko zidentyfikować przedmiot i dostarczyć niezbędne informacje.", + "errors": { + "no_barcode_detected": "Nie wykryto kodów kreskowych na obrazie", + "wrong_format": "Nieobsługiwany format pliku", + "no_file": "Nie przesłano pliku" + }, + "browse": "Przeglądaj pliki", + "cancel": "@:common.buttons.cancel" + } }, "language_selector": { "label": "Język:" @@ -1473,4 +1484,4 @@ "INVALID_SIZE": "Rozmiar pliku przekracza dozwolone {0}.", "CANNOT_DELETE": "Błąd podczas usuwania pliku." } -} +} \ No newline at end of file diff --git a/locales/pt.json b/locales/pt.json index 00e16edb28..97eecce1bf 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -377,7 +377,18 @@ "no_results": "Sua pesquisa '{keyword}' não retornou resultados", "enter_keyword_placeholder": "Pesquise produtos aqui", "search_button": "Pesquisar", - "view_all_results_button": "Ver todos os {total} resultados" + "view_all_results_button": "Ver todos os {total} resultados", + "barcode_detector": { + "title": "Leitura de código de barras", + "description": "Para continuar, ative a câmera para ler o código de barras do produto. Isso nos ajudará a identificar o item rapidamente e fornecer as informações necessárias.", + "errors": { + "no_barcode_detected": "Nenhum código de barras detetado na imagem", + "wrong_format": "Formato de arquivo não suportado", + "no_file": "Nenhum ficheiro carregado" + }, + "browse": "Procurar ficheiros", + "cancel": "@:common.buttons.cancel" + } }, "language_selector": { "label": "Idioma:" @@ -1473,4 +1484,4 @@ "INVALID_SIZE": "O tamanho do arquivo excede o permitido {0}.", "CANNOT_DELETE": "Falha ao excluir o arquivo." } -} +} \ No newline at end of file diff --git a/locales/ru.json b/locales/ru.json index eb1077b784..0112a750ce 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -377,7 +377,18 @@ "no_results": "По вашему запросу {keyword} ничего не найдено", "enter_keyword_placeholder": "Поиск товаров", "search_button": "Искать", - "view_all_results_button": "Просмотреть все {total} результаты" + "view_all_results_button": "Просмотреть все результаты ({total})", + "barcode_detector": { + "title": "Сканирование штрихкода", + "description": "Чтобы продолжить, включите камеру для сканирования штрихкода на товаре. Это поможет нам быстро идентифицировать товар и предоставить вам необходимую информацию.", + "errors": { + "no_barcode_detected": "Штрихкоды на изображении не обнаружены", + "wrong_format": "Неподдерживаемый формат файла", + "no_file": "Файл не загружен" + }, + "browse": "Обзор файлов", + "cancel": "@:common.buttons.cancel" + } }, "language_selector": { "label": "Язык:" @@ -1473,4 +1484,4 @@ "INVALID_SIZE": "Размер файла превышает допустимый {0}.", "CANNOT_DELETE": "Ошибка при удалении файла." } -} +} \ No newline at end of file diff --git a/locales/zh.json b/locales/zh.json index 84c44b9c79..9de964295d 100644 --- a/locales/zh.json +++ b/locales/zh.json @@ -377,7 +377,18 @@ "no_results": "您的 {keyword} 查询未返回任何结果", "enter_keyword_placeholder": "在这里搜索产品", "search_button": "搜索", - "view_all_results_button": "查看所有 {total} 个结果" + "view_all_results_button": "查看所有 {total} 个结果", + "barcode_detector": { + "title": "条码扫描", + "description": "请启用相机扫描产品条形码以继续。 这将帮助我们快速识别商品并为您提供必要的信息。", + "errors": { + "no_barcode_detected": "图片中未检测到条形码", + "wrong_format": "不支持的文件格式", + "no_file": "未上传文件" + }, + "browse": "浏览文件", + "cancel": "@:common.buttons.cancel" + } }, "language_selector": { "label": "中文:" @@ -1473,4 +1484,4 @@ "INVALID_SIZE": "文件大小超过允许的 {0}。", "CANNOT_DELETE": "删除文件时出错。" } -} +} \ No newline at end of file diff --git a/package.json b/package.json index 5d24b7fbaf..28523bef38 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "@vueuse/core": "^10.11.0", "@vueuse/integrations": "^10.11.0", "axios": "^1.7.2", + "barcode-detector": "3.0.1", "dompurify": "^3.1.6", "firebase": "^11.0.1", "graphql": "^16.8.2", diff --git a/yarn.lock b/yarn.lock index 20e3251841..9d78c00eca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4902,6 +4902,13 @@ __metadata: languageName: node linkType: hard +"@types/emscripten@npm:^1.40.0": + version: 1.40.0 + resolution: "@types/emscripten@npm:1.40.0" + checksum: 10c0/2c809da43cb42396a78bc1bf1f8bb1eb23874b22425ccc0efd2dff80522318739fc38e845d98983948ca271fe1a551f68043094d20df14e745aff8db2123a0e5 + languageName: node + linkType: hard + "@types/estree@npm:1.0.6, @types/estree@npm:^1.0.0": version: 1.0.6 resolution: "@types/estree@npm:1.0.6" @@ -6437,6 +6444,15 @@ __metadata: languageName: node linkType: hard +"barcode-detector@npm:3.0.1": + version: 3.0.1 + resolution: "barcode-detector@npm:3.0.1" + dependencies: + zxing-wasm: "npm:^2.1.0" + checksum: 10c0/28d842b985ce5d6d21b684e6be5b3f437f627af90478f0530b92ebbe0d896a5d4bf70d9738b747bb6a3deee85d16897b6cece73891d3d8a9dc0707f22782a9c5 + languageName: node + linkType: hard + "base64-js@npm:^1.3.1": version: 1.5.1 resolution: "base64-js@npm:1.5.1" @@ -14801,7 +14817,7 @@ __metadata: languageName: node linkType: hard -"type-fest@npm:^4.8.3": +"type-fest@npm:^4.35.0, type-fest@npm:^4.8.3": version: 4.37.0 resolution: "type-fest@npm:4.37.0" checksum: 10c0/5bad189f66fbe3431e5d36befa08cab6010e56be68b7467530b7ef94c3cf81ef775a8ac3047c8bbda4dd3159929285870357498d7bc1df062714f9c5c3a84926 @@ -15200,6 +15216,7 @@ __metadata: "@vueuse/integrations": "npm:^10.11.0" autoprefixer: "npm:^10.4.19" axios: "npm:^1.7.2" + barcode-detector: "npm:3.0.1" dependency-cruiser: "npm:^16.3.3" dompurify: "npm:^3.1.6" eslint: "npm:^8.57.0" @@ -16255,3 +16272,15 @@ __metadata: checksum: 10c0/3d166fb661f1b7fdf8a0ef2222d9e574ab241e72141f2f1fda62a9250ca73aabf2eaf0d66046a3984cd24d1dd9bac231338c6271684d6b8caa6b66af7c45f275 languageName: node linkType: hard + +"zxing-wasm@npm:^2.1.0": + version: 2.1.0 + resolution: "zxing-wasm@npm:2.1.0" + dependencies: + "@types/emscripten": "npm:^1.40.0" + type-fest: "npm:^4.35.0" + peerDependencies: + "@types/emscripten": ">=1.39.6" + checksum: 10c0/1b77f9c13749d54e6942c17a06b83a8d929163b5609a04e8ba93008acac4626172e2bca6243005094ea6b78c1c5363d0a0933ac6a7df5c2ededf66d060310909 + languageName: node + linkType: hard
+ {{ $t("shared.layout.search_bar.barcode_detector.description") }} +