Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: barcode detector #1639

Open
wants to merge 43 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
604a5b9
feat: add lib and button
NaMax66 Mar 13, 2025
62ec9c2
feat: generate components
NaMax66 Mar 13, 2025
7802c69
fix: test for mobile
NaMax66 Mar 14, 2025
64a0382
feat: add button to mobile
NaMax66 Mar 14, 2025
c3f2aaf
fix type
NaMax66 Mar 14, 2025
aeb5732
fix: scanner
NaMax66 Mar 17, 2025
4fa2e6f
fix: refactor
NaMax66 Mar 17, 2025
bc8a2f4
fix: search
NaMax66 Mar 17, 2025
f3c54ef
fix: change barcode icon
NaMax66 Mar 18, 2025
4b73e0d
fix: change barcode icon desktop
NaMax66 Mar 18, 2025
e6f9aae
feat: add locales
NaMax66 Mar 18, 2025
33c3fcd
feat: add skeleton
NaMax66 Mar 18, 2025
e8b3b83
Merge remote-tracking branch 'origin/dev' into feat/vcst-2622-barcode…
NaMax66 Mar 19, 2025
8a3cd7d
fix: align with design
NaMax66 Mar 19, 2025
8254243
fix: add redirect search
NaMax66 Mar 19, 2025
4003201
feat: add external support
NaMax66 Mar 19, 2025
115ee12
fix: remove settings
NaMax66 Mar 19, 2025
dd07fc6
fix: remove src
NaMax66 Mar 19, 2025
a0b68ac
fix: bem
NaMax66 Mar 19, 2025
da74e51
fix: bem
NaMax66 Mar 20, 2025
9969abc
fix: review
NaMax66 Mar 20, 2025
e051cdb
fix: review
NaMax66 Mar 20, 2025
4653968
fix: append
NaMax66 Mar 20, 2025
d3b1394
fix: w-full
NaMax66 Mar 20, 2025
4136d10
Merge branch 'dev' into feat/vcst-2622-barcode-detector
NaMax66 Mar 21, 2025
8717250
feat: add file selection
NaMax66 Mar 21, 2025
178ed8c
feat: add files support
NaMax66 Mar 21, 2025
7f65d22
feat: fix input
NaMax66 Mar 21, 2025
9f932c3
feat: remove attrs
NaMax66 Mar 21, 2025
3a368a5
Merge remote-tracking branch 'origin/dev' into feat/vcst-2622-barcode…
NaMax66 Mar 24, 2025
943bcbb
feat: add on cancel
NaMax66 Mar 24, 2025
12ac9fb
feat: add error handle
NaMax66 Mar 24, 2025
0915a70
feat: remove host
NaMax66 Mar 24, 2025
06b4930
Merge remote-tracking branch 'origin/dev' into feat/vcst-2622-barcode…
NaMax66 Mar 25, 2025
fd74137
feat: install yarn
NaMax66 Mar 25, 2025
ee15d7f
Merge remote-tracking branch 'origin/dev' into feat/vcst-2622-barcode…
NaMax66 Mar 26, 2025
1aef256
chore: add locales
NaMax66 Mar 26, 2025
ee870bf
chore: add locales
NaMax66 Mar 26, 2025
7a660d8
fix: add locales
NaMax66 Mar 26, 2025
79261f3
fix: change buttons
NaMax66 Mar 26, 2025
0ddb824
feat: simplify
NaMax66 Mar 26, 2025
e0ab720
Merge remote-tracking branch 'origin/dev' into feat/vcst-2622-barcode…
NaMax66 Mar 27, 2025
58fb7d8
feat: review
NaMax66 Mar 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions client-app/router/routes/constants.ts
Original file line number Diff line number Diff line change
@@ -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;
2 changes: 1 addition & 1 deletion client-app/router/routes/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,15 @@
:placeholder="$t('shared.layout.header.mobile.search_bar.input_placeholder')"
class="mr-4 grow"
no-border
clearable
@keydown.enter="searchPhrase && $router.push(searchPageLink)"
@clear="reset"
/>
>
<template #append>
<button v-if="searchPhrase" type="button" class="vc-input__clear" @click.stop="reset">
<VcIcon name="delete-2" size="xs" />
</button>
<BarcodeScanner v-else @scanned-code="onBarcodeScanned" />
</template>
</VcInput>

<VcButton :to="searchPhrase && searchPageLink" icon="search" />

Expand Down Expand Up @@ -137,6 +142,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();
Expand All @@ -158,17 +164,24 @@ const placeholderStyle = computed<StyleValue | undefined>(() =>
);

const searchPageLink = computed<RouteLocationRaw>(() => ({
name: "Search",
name: ROUTES.SEARCH.NAME,
query: {
[QueryParamName.SearchPhrase]: searchPhrase.value.trim(),
},
}));

function reset() {
searchPhrase.value = "";
void router.push({ name: ROUTES.CATALOG.NAME });
void router.push({ name: ROUTES.SEARCH.NAME });
}

const onBarcodeScanned = (value: string) => {
if (value) {
searchPhrase.value = value;
void router.push(searchPageLink.value);
}
};

syncRefs(mobileMenuVisible, useScrollLock(document.body));

whenever(searchBarVisible, () => (searchPhrase.value = searchPhraseInUrl.value ?? ""), { immediate: true });
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<template>
<VcModal
:title="$t('shared.layout.search_bar.barcode_detector.title')"
class="barcode-scanner-modal"
is-mobile-fullscreen
>
<p class="barcode-scanner-modal__description">
{{ $t("shared.layout.search_bar.barcode_detector.description") }}
</p>
<VcWidgetSkeleton v-show="loading">
<template #default-container>
<div class="p-1">
<div class="w-full! h-60"></div>
</div>
</template>
</VcWidgetSkeleton>
<video
v-show="!loading"
ref="videoElement"
class="barcode-scanner-modal__video"
playsinline
@canplaythrough="loading = false"
>
<track kind="captions" :label="$t('shared.layout.search_bar.barcode_detector.title')" default disabled src="" />
</video>
<template #actions="{ close }">
<VcButton class="barcode-scanner-modal__action" color="secondary" variant="outline" @click="close">
{{ $t("shared.catalog.branches_modal.cancel_button") }}
</VcButton>
</template>
</VcModal>
</template>

<script setup lang="ts">
import { useIntervalFn } from "@vueuse/core";
import { BarcodeDetector } from "barcode-detector/ponyfill";
import { ref, onMounted, onBeforeUnmount } from "vue";
import { Logger } from "@/core/utilities";

const emit = defineEmits<{
(e: "result", value: string[]): void;
}>();

const videoElement = ref<HTMLVideoElement | null>(null);
const videoStream = ref<MediaStream | null>(null);
let barcodeDetector: BarcodeDetector | null = null;

const loading = ref(true);

const SCAN_INTERVAL = 400;

const scan = async () => {
if (!barcodeDetector || !videoElement.value) {
return;
}
try {
const imageBitmap = await createImageBitmap(videoElement.value);

const barcodes = await barcodeDetector.detect(imageBitmap);
if (barcodes.length) {
emit(
"result",
barcodes.map((el) => el.rawValue),
);
}
} catch (error) {
Logger.error(scan.name, error);
}
};

const { resume: startScanner, pause: stopScanner } = useIntervalFn(scan, SCAN_INTERVAL);

async function startCamera() {
try {
if (!videoStream.value) {
videoStream.value = await navigator.mediaDevices.getUserMedia({
video: { facingMode: "environment" },
});
}

if (videoElement.value) {
videoElement.value.srcObject = videoStream.value;
await videoElement.value.play();
}
} catch (error) {
Logger.error(startCamera.name, error);
}
}

function stopCamera() {
if (videoStream.value) {
videoStream.value.getVideoTracks().every((track) => track.stop());
videoStream.value = null;
}

if (videoElement.value) {
videoElement.value.srcObject = null;
}
}

onMounted(async () => {
try {
await startCamera();
barcodeDetector = new BarcodeDetector();
startScanner();
} catch (error) {
Logger.error("BarcodeDetector initialization error:", error);
}
});

onBeforeUnmount(() => {
stopScanner();
barcodeDetector = null;
stopCamera();
});
</script>

<style lang="scss">
.barcode-scanner-modal {
&__description {
@apply mb-2.5;
}

&__video {
@apply aspect-[3/2] object-cover md:aspect-[2/1];
}

&__action {
@apply ml-auto w-full md:w-auto;
}
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<template>
<button class="barcode-scanner" type="button" icon="barcode" @click.stop="openBarcodeScanner">
<VcIcon name="barcode" size="sm" />
</button>
</template>

<script setup lang="ts">
import { useModal } from "@/shared/modal";
import BarcodeScannerModal from "./barcode-scanner-modal.vue";

interface IEmits {
(e: "scannedCode", code: string): void;
}
const emit = defineEmits<IEmits>();

const { openModal, closeModal } = useModal();

const onBarcodeValueReceived = (codes: string[]) => {
if (codes.length) {
emit("scannedCode", codes[0]);
closeModal();
}
};

const openBarcodeScanner = () => {
openModal({
component: BarcodeScannerModal,
props: {
onClose: closeModal,
onResult: onBarcodeValueReceived,
},
});
};
</script>

<style lang="scss">
.barcode-scanner {
@apply flex items-center p-3 text-[--base-color];
}
</style>
20 changes: 16 additions & 4 deletions client-app/shared/layout/components/search-bar/search-bar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@
:maxlength="MAX_LENGTH"
class="w-full"
:placeholder="$t('shared.layout.search_bar.enter_keyword_placeholder')"
clearable
@keyup.enter="goToSearchResultsPage"
@keyup.esc="hideSearchDropdown"
@input="onSearchPhraseChanged"
@focus="onSearchBarFocused"
@clear="reset"
>
<template #append>
<button v-if="searchPhrase" type="button" class="vc-input__clear" @click.stop="reset">
<VcIcon name="delete-2" size="xs" />
</button>
<BarcodeScanner v-else @scanned-code="onBarcodeScanned" />

<VcButton
:aria-label="$t('shared.layout.search_bar.search_button')"
icon="search"
Expand Down Expand Up @@ -146,10 +149,12 @@ import { getFilterExpressionForCategorySubtree, getFilterExpressionForZeroPrice
import { ROUTES } from "@/router/routes/constants";
import { useSearchBar } from "@/shared/layout/composables/useSearchBar";
import SearchBarProductCard from "./_internal/search-bar-product-card.vue";
import BarcodeScanner from "./barcode-scanner.vue";
import type { GetSearchResultsParamsType } from "@/core/api/graphql/catalog";
import type { Category } from "@/core/api/graphql/types";
import type { StyleValue } from "vue";
import type { RouteLocationRaw } from "vue-router";
import VcButton from "@/ui-kit/components/molecules/button/vc-button.vue";

const { themeContext } = useThemeContext();

Expand Down Expand Up @@ -272,7 +277,7 @@ async function searchAndShowDropdownResults(): Promise<void> {

function getSearchRoute(phrase: string): RouteLocationRaw {
return {
name: "Search",
name: ROUTES.SEARCH.NAME,
query: {
[QueryParamName.SearchPhrase]: phrase,
},
Expand All @@ -290,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);
Expand Down Expand Up @@ -320,4 +325,11 @@ onMounted(() => {
searchPhrase.value = searchPhraseInUrl.value;
}
});

const onBarcodeScanned = (value: string) => {
if (value) {
searchPhrase.value = value;
goToSearchResultsPage();
}
};
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
</style>
1 change: 1 addition & 0 deletions client-app/ui-kit/icons/barcode.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 6 additions & 2 deletions locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,11 @@
"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."
}
},
"language_selector": {
"label": "Sprache:"
Expand Down Expand Up @@ -1472,4 +1476,4 @@
"INVALID_SIZE": "Dateigröße überschreitet die erlaubte {0}.",
"CANNOT_DELETE": "Fehler beim Löschen der Datei."
}
}
}
8 changes: 6 additions & 2 deletions locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,11 @@
"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."
}
},
"language_selector": {
"label": "Language:"
Expand Down Expand Up @@ -1472,4 +1476,4 @@
"INVALID_SIZE": "File size exceeds the allowed {0}",
"CANNOT_DELETE": "Failure while deleting file"
}
}
}
8 changes: 6 additions & 2 deletions locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,11 @@
"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."
}
},
"language_selector": {
"label": "Idioma:"
Expand Down Expand Up @@ -1472,4 +1476,4 @@
"INVALID_SIZE": "File size exceeds the allowed {0}.",
"CANNOT_DELETE": "Failure while deleting file."
}
}
}
Loading
Loading