Skip to content

Commit

Permalink
feat(pcomparator): add reverse search price (#77)
Browse files Browse the repository at this point in the history
* feat(pcomparator): add reverse search price

* improve pending state
  • Loading branch information
Clement-Muth authored Nov 10, 2024
1 parent 37577a4 commit b7ad0ab
Show file tree
Hide file tree
Showing 15 changed files with 318 additions and 70 deletions.
66 changes: 64 additions & 2 deletions pcomparator/public/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion pcomparator/src/app/[locale]/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 3 additions & 1 deletion pcomparator/src/app/api/v1/prices/search/documentation.ts
Original file line number Diff line number Diff line change
@@ -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);

Expand Down Expand Up @@ -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()
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion pcomparator/src/app/api/v1/prices/search/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { HttpStatus } from "~/types/httpError";
const SearchRepository = new PrismaSearchRepository();

export const GET = withAuthentication(
errorHandler(async (request, ctx): Promise<NextResponse> => {
errorHandler(async (request): Promise<NextResponse> => {
const { searchParams } = new URL(request.url);
const search = searchParams.get("q") as string;

Expand Down
24 changes: 24 additions & 0 deletions pcomparator/src/applications/Searchbar/Api/searchByBarcode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"use server";
import { z } from "zod";

const ParamsSchema = z.object({
barcode: z.string()
});

export const searchByBarcode = async (params: z.infer<typeof ParamsSchema>): 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;
}
};
Original file line number Diff line number Diff line change
@@ -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 (
<Modal
isOpen={isOpen}
onOpenChange={onOpenChange}
size="4xl"
data-testid="modal-barcode-scanner"
className="h-[80dvh]"
>
<ModalContent>
<BarcodeScanner
onCapture={(barcode) => 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"
]
}}
/>
</ModalContent>
</Modal>
);
};
Original file line number Diff line number Diff line change
@@ -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<Awaited<ReturnType<typeof search>> | null>(null);

return (
<>
{!searchResult ? (
<BarcodeScannerModal
isOpen={isOpen}
onOpenChange={onOpenChange}
onBarcodeDetected={(barcode) => {
!pending &&
startTransition(async () => {
const { name } = await searchByBarcode({ barcode: barcode.barcode });
const formData = new FormData();

formData.append("search", name);
setSearchResult(await search(null, formData));
});
}}
/>
) : null}
<Button
startContent={<ScanBarcode />}
onPress={onOpen}
radius="full"
variant="faded"
className="p-7 w-18 h-18 -mt-8 border-none shadow-medium"
isIconOnly
/>
{searchResult && (
<SearchResultModal
isOpen={isOpen}
onClose={() => {
onClose();
setSearchResult(null);
}}
onOpenChange={onOpenChange}
prices={searchResult?.prices as any}
search={searchResult?.search}
/>
)}
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Trans } from "@lingui/macro";
import {
Button,
Card,
CardBody,
Image,
Modal,
ModalBody,
ModalContent,
ModalFooter,
ModalHeader
} from "@nextui-org/react";
import type { Price } from "~/applications/Prices/Domain/Entities/Price";
import type { Product } from "~/applications/Prices/Domain/Entities/Product";
import type { Store } from "~/applications/Prices/Domain/Entities/Store";
import { getCurrencySymbol } from "~/applications/Prices/Domain/ValueObjects/Currency";

interface SearchResultProps {
isOpen: boolean;
onOpenChange: () => void;
onClose: () => void;
search: string;
prices: (Price & { product: Product; store: Store })[];
}

export const SearchResultModal = ({ isOpen, onClose, onOpenChange, prices, search }: SearchResultProps) => (
<Modal isOpen={isOpen} onClose={onClose} onOpenChange={onOpenChange}>
<ModalContent>
<ModalHeader>
<p>
<Trans>Prices for {search}</Trans>
</p>
</ModalHeader>
<ModalBody>
{prices.map((price, i) => (
<Card key={price.product.name + i}>
<CardBody className="flex !flex-row gap-4">
<Image src={price.priceProofImage!} width={100} height={100} className="object-cover" />
<div className="flex flex-col justify-between">
<div>
<p className="font-bold text-lg">{price.product.name}</p>
<p className="text-small">{price.store.name} – 6km</p>
</div>
<p>
{getCurrencySymbol(price.currency)} {price.amount}
</p>
</div>
</CardBody>
</Card>
))}
</ModalBody>
<ModalFooter>
<Button size="sm" onPress={onClose}>
<Trans>Close</Trans>
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
59 changes: 11 additions & 48 deletions pcomparator/src/applications/Searchbar/Ui/Searchbar.tsx
Original file line number Diff line number Diff line change
@@ -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();
Expand Down Expand Up @@ -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();
Expand All @@ -74,38 +62,13 @@ export const Searchbar = ({ startContent }: SearchbarProps) => {
</div>

{state?.success && (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent>
<ModalHeader>
<p>
<Trans>Prices for {state.search}</Trans>
</p>
</ModalHeader>
<ModalBody>
{state.prices?.map((price, i) => (
<Card key={price.product.name + i}>
<CardBody className="flex !flex-row gap-4">
<Image src={price.priceProofImage} width={100} height={100} className="object-cover" />
<div className="flex flex-col justify-between">
<div>
<p className="font-bold text-lg">{price.product.name}</p>
<p className="text-small">{price.store.name} – 6km</p>
</div>
<p>
{getCurrencySymbol(price.currency)} {price.amount}
</p>
</div>
</CardBody>
</Card>
))}
</ModalBody>
<ModalFooter>
<Button size="sm">
<Trans>Close</Trans>
</Button>
</ModalFooter>
</ModalContent>
</Modal>
<SearchResultModal
isOpen={isOpen}
onClose={onClose}
onOpenChange={onOpenChange}
prices={state.prices as any}
search={state.search}
/>
)}
</form>
);
Expand Down
Loading

0 comments on commit b7ad0ab

Please sign in to comment.