diff --git a/pcomparator/public/openapi.yaml b/pcomparator/public/openapi.yaml index ef0831e..c9674a1 100644 --- a/pcomparator/public/openapi.yaml +++ b/pcomparator/public/openapi.yaml @@ -207,10 +207,10 @@ paths: example: "" createdAt: type: string - example: 2024-11-10T20:35:03.737Z + example: 2024-11-10T22:37:23.376Z updatedAt: type: string - example: 2024-11-10T20:35:03.737Z + example: 2024-11-10T22:37:23.376Z required: - id - barcode @@ -669,12 +669,74 @@ paths: type: - string - "null" + product: + type: object + properties: + id: + type: string + format: uuid + barcode: + type: string + minLength: 1 + name: + type: string + minLength: 1 + description: + type: + - string + - "null" + minLength: 1 + categoryId: + type: + - string + - "null" + format: uuid + brandId: + type: + - string + - "null" + format: uuid + nutritionScore: + type: "null" + createdAt: + type: string + updatedAt: + type: string + required: + - id + - barcode + - name + - createdAt + - updatedAt + store: + type: object + properties: + id: + type: string + format: uuid + name: + type: string + minLength: 1 + location: + type: string + minLength: 1 + websiteUrl: + type: + - string + - "null" + format: uri + required: + - id + - name + - location required: - id - productId - storeId - amount - currency + - product + - store "400": description: The request is malformed or contains invalid parameters. Please check the data provided. diff --git a/pcomparator/src/app/[locale]/globals.css b/pcomparator/src/app/[locale]/globals.css index eada263..1178265 100644 --- a/pcomparator/src/app/[locale]/globals.css +++ b/pcomparator/src/app/[locale]/globals.css @@ -4,7 +4,7 @@ div[data-overlay-container="true"] { @apply flex flex-col flex-1; - @apply min-h-screen w-full bg-gradient-to-br dark:from-[#1f121b] dark:via-[#0c1820] dark:via-80% dark:to-[#081917] from-indigo-50 via-white to-primary-200; + @apply min-h-dvh w-full bg-gradient-to-br dark:from-[#1f121b] dark:via-[#0c1820] dark:via-80% dark:to-[#081917] from-indigo-50 via-white to-primary-200; } .PhoneInput { diff --git a/pcomparator/src/app/api/v1/prices/search/documentation.ts b/pcomparator/src/app/api/v1/prices/search/documentation.ts index 80d9ceb..1cf7c85 100644 --- a/pcomparator/src/app/api/v1/prices/search/documentation.ts +++ b/pcomparator/src/app/api/v1/prices/search/documentation.ts @@ -1,6 +1,8 @@ import { z } from "zod"; import { type ZodOpenApiPathsObject, extendZodWithOpenApi } from "zod-openapi"; import { PriceSchema } from "~/applications/Prices/Domain/Entities/Price"; +import { ProductSchema } from "~/applications/Prices/Domain/Entities/Product"; +import { StoreSchema } from "~/applications/Prices/Domain/Entities/Store"; extendZodWithOpenApi(z); @@ -33,7 +35,7 @@ export const paths: ZodOpenApiPathsObject = { description: "The burger was created successfully.", content: { "application/json": { - schema: PriceSchema.array() + schema: PriceSchema.extend({ product: ProductSchema, store: StoreSchema }).array() } } } diff --git a/pcomparator/src/app/api/v1/prices/search/route.ts b/pcomparator/src/app/api/v1/prices/search/route.ts index 75b0742..ed1bdda 100644 --- a/pcomparator/src/app/api/v1/prices/search/route.ts +++ b/pcomparator/src/app/api/v1/prices/search/route.ts @@ -7,7 +7,7 @@ import { HttpStatus } from "~/types/httpError"; const SearchRepository = new PrismaSearchRepository(); export const GET = withAuthentication( - errorHandler(async (request, ctx): Promise => { + errorHandler(async (request): Promise => { const { searchParams } = new URL(request.url); const search = searchParams.get("q") as string; diff --git a/pcomparator/src/applications/Searchbar/Api/searchByBarcode.ts b/pcomparator/src/applications/Searchbar/Api/searchByBarcode.ts new file mode 100644 index 0000000..8d84ba0 --- /dev/null +++ b/pcomparator/src/applications/Searchbar/Api/searchByBarcode.ts @@ -0,0 +1,24 @@ +"use server"; +import { z } from "zod"; + +const ParamsSchema = z.object({ + barcode: z.string() +}); + +export const searchByBarcode = async (params: z.infer): Promise<{ name: string }> => { + try { + const payload = ParamsSchema.parse(params); + + const { + product: { product_name } + } = await ( + await fetch(`https://world.openfoodfacts.net/api/v2/product/${payload.barcode}`, { method: "get" }) + ).json(); + + return { name: product_name }; + // return { success: true, prices: res, search: payload.data.search }; + } catch (error) { + console.error(error); + throw error; + } +}; diff --git a/pcomparator/src/applications/Searchbar/Ui/SearchBarcode/BarcodeScannerModal.tsx b/pcomparator/src/applications/Searchbar/Ui/SearchBarcode/BarcodeScannerModal.tsx new file mode 100644 index 0000000..8c799a5 --- /dev/null +++ b/pcomparator/src/applications/Searchbar/Ui/SearchBarcode/BarcodeScannerModal.tsx @@ -0,0 +1,47 @@ +import { Modal, ModalContent } from "@nextui-org/react"; +import { BarcodeScanner } from "react-barcode-scanner"; +import type { Barcode } from "~/applications/Prices/Domain/ValueObjects/Barcode"; + +interface BarcodeScannerModalProps { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; + onBarcodeDetected: (barcode: Barcode) => void; +} + +export const BarcodeScannerModal = ({ + isOpen, + onOpenChange, + onBarcodeDetected +}: BarcodeScannerModalProps) => { + return ( + + + onBarcodeDetected({ barcode: barcode.rawValue, format: barcode.format })} + options={{ + formats: [ + "codabar", + "upc_a", + "code_128", + "code_39", + "code_93", + "data_matrix", + "ean_13", + "ean_8", + "itf", + "pdf417", + "qr_code", + "upc_e" + ] + }} + /> + + + ); +}; diff --git a/pcomparator/src/applications/Searchbar/Ui/SearchBarcode/SearchBarcode.tsx b/pcomparator/src/applications/Searchbar/Ui/SearchBarcode/SearchBarcode.tsx new file mode 100644 index 0000000..69fbbd8 --- /dev/null +++ b/pcomparator/src/applications/Searchbar/Ui/SearchBarcode/SearchBarcode.tsx @@ -0,0 +1,56 @@ +"use client"; + +import { Button, useDisclosure } from "@nextui-org/react"; +import { ScanBarcode } from "lucide-react"; +import { useState, useTransition } from "react"; +import { search } from "~/applications/Searchbar/Api/search"; +import { searchByBarcode } from "~/applications/Searchbar/Api/searchByBarcode"; +import { BarcodeScannerModal } from "~/applications/Searchbar/Ui/SearchBarcode/BarcodeScannerModal"; +import { SearchResultModal } from "~/applications/Searchbar/Ui/SearchResult/SearchResultModal"; + +export const SearchBarcode = () => { + const [pending, startTransition] = useTransition(); + const { onOpen, onClose, isOpen, onOpenChange } = useDisclosure(); + const [searchResult, setSearchResult] = useState> | null>(null); + + return ( + <> + {!searchResult ? ( + { + !pending && + startTransition(async () => { + const { name } = await searchByBarcode({ barcode: barcode.barcode }); + const formData = new FormData(); + + formData.append("search", name); + setSearchResult(await search(null, formData)); + }); + }} + /> + ) : null} + + + + +); diff --git a/pcomparator/src/applications/Searchbar/Ui/Searchbar.tsx b/pcomparator/src/applications/Searchbar/Ui/Searchbar.tsx index 7e9a03b..e2123af 100644 --- a/pcomparator/src/applications/Searchbar/Ui/Searchbar.tsx +++ b/pcomparator/src/applications/Searchbar/Ui/Searchbar.tsx @@ -1,26 +1,14 @@ "use client"; -import { Trans, t } from "@lingui/macro"; +import { t } from "@lingui/macro"; import { useLingui } from "@lingui/react"; -import { - Button, - Card, - CardBody, - Image, - Modal, - ModalBody, - ModalContent, - ModalFooter, - ModalHeader, - useDisclosure -} from "@nextui-org/react"; +import { useDisclosure } from "@nextui-org/react"; import clsx from "clsx"; import { Send } from "lucide-react"; import { type ReactNode, useActionState, useEffect } from "react"; import { useFormStatus } from "react-dom"; -import {} from "react-hook-form"; -import { getCurrencySymbol } from "~/applications/Prices/Domain/ValueObjects/Currency"; import { search } from "~/applications/Searchbar/Api/search"; +import { SearchResultModal } from "~/applications/Searchbar/Ui/SearchResult/SearchResultModal"; const SubmitButton = () => { const { pending } = useFormStatus(); @@ -48,7 +36,7 @@ interface SearchbarProps { export const Searchbar = ({ startContent }: SearchbarProps) => { const [state, formAction] = useActionState(search, null); const { i18n } = useLingui(); - const { isOpen, onOpen, onOpenChange } = useDisclosure(); + const { isOpen, onOpen, onClose, onOpenChange } = useDisclosure(); useEffect(() => { if (state?.success) onOpen(); @@ -74,38 +62,13 @@ export const Searchbar = ({ startContent }: SearchbarProps) => { {state?.success && ( - - - -

- Prices for {state.search} -

-
- - {state.prices?.map((price, i) => ( - - - -
-
-

{price.product.name}

-

{price.store.name} – 6km

-
-

- {getCurrencySymbol(price.currency)} {price.amount} -

-
-
-
- ))} -
- - - -
-
+ )} ); diff --git a/pcomparator/src/components/Tabbar/Tabbar.stories.tsx b/pcomparator/src/components/Tabbar/Tabbar.stories.tsx index d76147e..f30e69e 100644 --- a/pcomparator/src/components/Tabbar/Tabbar.stories.tsx +++ b/pcomparator/src/components/Tabbar/Tabbar.stories.tsx @@ -1,4 +1,6 @@ +import { Button } from "@nextui-org/react"; import type { Meta, StoryObj } from "@storybook/react"; +import { ScanBarcode } from "lucide-react"; import type { FieldValues, RegisterOptions } from "react-hook-form"; import { Tabbar, type TabbarProps } from "~/components/Tabbar/Tabbar"; @@ -9,7 +11,9 @@ export default { type FileObjProps = TabbarProps & RegisterOptions; -const defaultProps: FileObjProps = {}; +const defaultProps: FileObjProps = { + mainButton: