Skip to content

Commit

Permalink
Merge pull request #1 from deco-sites/feature/28720/autocomplete
Browse files Browse the repository at this point in the history
feature/28720/autocomplete
  • Loading branch information
gsbenevides2 authored Oct 24, 2024
2 parents 61de9ec + b464913 commit c62c2cb
Show file tree
Hide file tree
Showing 18 changed files with 617 additions and 163 deletions.
17 changes: 16 additions & 1 deletion .deco/blocks/Header.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,22 @@
"__resolveType": "vtex/loaders/intelligentSearch/suggestions.ts",
"count": 4
}
}
},
"banner": "https://deco-sites-assets.s3.sa-east-1.amazonaws.com/alphabeto/9752e7c5-94e0-4e29-ab58-1ae939d55ed9/banner-header-autocomplete.png",
"bannerAlt": "Uma garota e um Garoto",
"bannerMobile": "https://deco-sites-assets.s3.sa-east-1.amazonaws.com/alphabeto/4e940e54-504c-4a0c-b2e5-e7dac2337909/banner-header-autocomplete-mobile.png",
"topSearch": {
"data": {
"__resolveType": "vtex/loaders/intelligentSearch/topsearches.ts"
},
"__resolveType": "resolved"
},
"mostSellerTerms": [
"Vestidos Estampados",
"Conjuntinhos",
"Camisetas",
"Bonecas"
]
},
"alerts": [
"<p>Get 10% off today: <strong>NEW10</strong></p>"
Expand Down
4 changes: 1 addition & 3 deletions .deco/blocks/site.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,7 @@
}
]
},
"freeShippingBarSettings": {
"target": 0
}
"freeShippingBarSettings": {}
}
],
"routes": [
Expand Down
8 changes: 4 additions & 4 deletions components/header/HeaderDesktop.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { HTMLWidget, ImageWidget } from "apps/admin/widgets.ts";
import Image from "apps/website/components/Image.tsx";
import { SearchbarProps } from "../search/Searchbar/Form.tsx";
import { SearchBarComponentProps } from "../search/Searchbar/Form.tsx";
import Bag from "./Bag.tsx";
import { Items } from "./Menu.types.ts";
import NavItem from "./NavItem.tsx";
import { Offers } from "./Offers.tsx";
import { Search } from "./Search.tsx";
import { SearchDesktop } from "./Search.tsx";
import { SignInDesktop } from "./SignIn.tsx";
import { Wishlist } from "./Wishlist.tsx";

Expand All @@ -27,7 +27,7 @@ export interface SectionProps {
* @title Searchbar
* @description Searchbar configuration
*/
searchbar: SearchbarProps;
searchbar: SearchBarComponentProps;
/** @title Logo */
logo: Logo;
/**
Expand All @@ -54,7 +54,7 @@ export const Desktop = ({ navItems, logo, searchbar, loading }: SectionProps) =>

<div class="flex items-center gap-x-5 desk-small:gap-x-3">
<Offers />
<Search searchbar={searchbar} loading={loading} />
<SearchDesktop searchbar={searchbar} loading={loading} />

<div class="flex gap-4">
<Wishlist />
Expand Down
25 changes: 3 additions & 22 deletions components/header/HeaderMobile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,15 @@ import Image from "apps/website/components/Image.tsx";
import Bag from "../../components/header/Bag.tsx";
import Drawer from "../../components/ui/Drawer.tsx";
import { Props } from "../../sections/Header/Header.tsx";
import { SearchMobile } from "./Search.tsx";

import { NAVBAR_HEIGHT_MOBILE, SEARCHBAR_DRAWER_ID, SIDEMENU_DRAWER_ID } from "../../constants.ts";
import { NAVBAR_HEIGHT_MOBILE, SIDEMENU_DRAWER_ID } from "../../constants.ts";
import { IconMenuDrawerOpen } from "../Icons/IconMenuDrawerOpen.tsx";
import { IconSearch } from "../Icons/IconSearch.tsx";
import Searchbar from "../search/Searchbar/Form.tsx";
import { MenuMobile } from "./MenuMobile.tsx";

export function Mobile({ logo, searchbar, loading, navItems, links }: Props) {
return (
<>
<Drawer
id={SEARCHBAR_DRAWER_ID}
aside={
<Drawer.Aside title="Search" drawer={SEARCHBAR_DRAWER_ID} class="max-w-[calc(100vw_-_20px)]">
<div class="overflow-y-auto">
{loading === "lazy" ? (
<div class="h-full w-full flex items-center justify-center">
<span class="loading loading-spinner" />
</div>
) : (
<Searchbar {...searchbar} />
)}
</div>
</Drawer.Aside>
}
/>
<Drawer
id={SIDEMENU_DRAWER_ID}
class="w-full"
Expand All @@ -54,9 +37,7 @@ export function Mobile({ logo, searchbar, loading, navItems, links }: Props) {
</a>
)}
<div className="self-center flex justify-end items-center gap-5">
<label for={SEARCHBAR_DRAWER_ID} class="btn btn-square btn-sm btn-ghost w-fit" aria-label="search icon button">
<IconSearch />
</label>
<SearchMobile searchbar={searchbar} loading={loading} />
<Bag />
</div>
</div>
Expand Down
2 changes: 2 additions & 0 deletions components/header/Menu.types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ImageWidget as Image } from "apps/admin/widgets.ts";

/** @title {{item}} */
export interface Item {
item: string;
href: string;
Expand All @@ -11,6 +12,7 @@ export interface Submenu {
seeAll?: boolean;
}

/** @title {{menuItem}} */
export interface Items {
menuItem: string;
href: string;
Expand Down
1 change: 0 additions & 1 deletion components/header/MenuMobileDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ interface Props {
}

export function MenuMobileDetails({ submenu }: Props) {
console.log(submenu);
return (
<div class="px-6">
{submenu?.map((item, index) => (
Expand Down
53 changes: 34 additions & 19 deletions components/header/Search.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,50 @@
import { SEARCHBAR_POPUP_ID } from "../../constants.ts";
import { IconSearch } from "../Icons/IconSearch.tsx";
import Searchbar, { SearchbarProps } from "../search/Searchbar/Form.tsx";
import Searchbar, { SearchBarComponentProps } from "../search/Searchbar/Form.tsx";
import Modal from "../ui/Modal.tsx";

interface SearchProps {
searchbar: SearchbarProps;
searchbar: SearchBarComponentProps;
loading?: "eager" | "lazy";
}

export function Search({ searchbar, loading }: SearchProps) {
export function SearchDesktop({ searchbar, loading }: SearchProps) {
return (
<>
<Modal id={SEARCHBAR_POPUP_ID}>
<Modal id={SEARCHBAR_POPUP_ID} className="!bg-transparent">
<div class="absolute top-[30px] bg-base-100 w-full">
{loading === "lazy"
? (
<div class="flex justify-center items-center">
<span class="loading loading-spinner" />
</div>
)
: <Searchbar {...searchbar} />}
{loading === "lazy" ? (
<div class="flex justify-center items-center">
<span class="loading loading-spinner" />
</div>
) : (
<Searchbar {...searchbar} />
)}
</div>
</Modal>
<label
for={SEARCHBAR_POPUP_ID}
class="w-[243px] desk-small:w-[150px] bg-primary-content h-10 cursor-pointer rounded-lg flex items-center px-2 justify-between gap-2"
aria-label="search icon button"
>
<span class="text-base-400 truncate text-xs">
{searchbar.placeholder}
</span>
<label for={SEARCHBAR_POPUP_ID} class="w-[243px] desk-small:w-[150px] bg-primary-content h-10 cursor-pointer rounded-lg flex items-center px-2 justify-between gap-2" aria-label="search icon button">
<span class="text-base-400 truncate text-xs">{searchbar.placeholder}</span>
<IconSearch />
</label>
</>
);
}

export function SearchMobile({ searchbar, loading }: SearchProps) {
return (
<>
<Modal id={SEARCHBAR_POPUP_ID} className="!bg-transparent">
<div class="absolute top-0 bg-base-100 w-full">
{loading === "lazy" ? (
<div class="flex justify-center items-center">
<span class="loading loading-spinner" />
</div>
) : (
<Searchbar {...searchbar} />
)}
</div>
</Modal>
<label for={SEARCHBAR_POPUP_ID} class="btn btn-square btn-sm btn-ghost w-fit" aria-label="search icon button">
<IconSearch />
</label>
</>
Expand Down
54 changes: 47 additions & 7 deletions components/search/Searchbar/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,21 @@
*/
import { asResolved, type Resolved } from "@deco/deco";
import { useScript } from "@deco/deco/hooks";
import { ImageWidget } from "apps/admin/widgets.ts";
import { Suggestion } from "apps/commerce/types.ts";
import { SEARCHBAR_INPUT_FORM_ID, SEARCHBAR_POPUP_ID } from "../../../constants.ts";
import { clx } from "../../../sdk/clx.ts";
import { useId } from "../../../sdk/useId.ts";
import { useComponent } from "../../../sections/Component.tsx";
import { IconSearch } from "../../Icons/IconSearch.tsx";
import Icon from "../../ui/Icon.tsx";
import { Props as SuggestionProps } from "./Suggestions.tsx";
import TopSearchs from "./TopSearchs.tsx";
// When user clicks on the search button, navigate it to
export const ACTION = "/s";
// Querystring param used when navigating the user
export const NAME = "q";

export interface SearchbarProps {
/**
* @title Placeholder
Expand All @@ -30,9 +34,39 @@ export interface SearchbarProps {
placeholder?: string;
/** @description Loader to run when suggesting new elements */
loader: Resolved<Suggestion | null>;

/**
* @title Banner
* @description Banner image to display on the search bar results
*/
banner?: ImageWidget;
/**
* @title Banner Mobile
* @description Banner image to display on the search bar results for mobile screens
*/
bannerMobile?: ImageWidget;
/**
* @title Banner Alternative Text
* @description Banner image alternative text to people with disabilities
*/
bannerAlt?: string;
/**
* @title Top Search
* @description Loader to run when most searched items are requested
*/
topSearch: Resolved<Suggestion>;
/**
* @title Most Seller Terms
* @description List of most searched terms
*/
mostSellerTerms: string[];
}

export interface SearchBarComponentProps extends Omit<SearchbarProps, "topSearch"> {
topSearch: Suggestion;
}

const script = (formId: string, name: string, popupId: string) => {
console.log("OI");
const form = document.getElementById(formId) as HTMLFormElement | null;
const input = form?.elements.namedItem(name) as HTMLInputElement | null;
form?.addEventListener("submit", () => {
Expand All @@ -58,11 +92,12 @@ const script = (formId: string, name: string, popupId: string) => {
});
};
const Suggestions = import.meta.resolve("./Suggestions.tsx");
export default function Searchbar({ placeholder, loader }: SearchbarProps) {

export default function Searchbar({ placeholder, loader, banner, bannerAlt, bannerMobile, topSearch, mostSellerTerms }: SearchBarComponentProps) {
const slot = useId();
return (
<div class="w-full grid gap-[25px] container" style={{ gridTemplateRows: "min-content auto" }}>
<form id={SEARCHBAR_INPUT_FORM_ID} action={ACTION} class="join bg-primary-content mt-5 rounded-lg">
<div className={clx("flex flex-col gap-[22px] px-5 overflow-y-auto max-h-dvh", "desk:gap-8 desk:max-w-[95rem] desk:w-full desk:mx-auto desk:px-10")}>
<form id={SEARCHBAR_INPUT_FORM_ID} action={ACTION} class="join bg-[#f5f4f1] mt-5 rounded-lg">
<button type="submit" class="join-item no-animation w-10 flex justify-center items-center" aria-label="Search" for={SEARCHBAR_INPUT_FORM_ID} tabIndex={-1}>
<span class="loading text-primary loading-spinner loading-xs hidden [.htmx-request_&]:inline" />
<span class="inline [.htmx-request_&]:hidden">
Expand All @@ -81,19 +116,24 @@ export default function Searchbar({ placeholder, loader }: SearchbarProps) {
loader &&
useComponent<SuggestionProps>(Suggestions, {
loader: asResolved(loader),
banner,
bannerAlt,
bannerMobile,
})
}
hx-trigger={`input changed delay:300ms, ${NAME}`}
hx-indicator={`#${SEARCHBAR_INPUT_FORM_ID}`}
hx-swap="innerHTML"
/>
<label type="button" class="join-item btn btn-ghost btn-square hidden sm:inline-flex no-animation" for={SEARCHBAR_POPUP_ID} aria-label="Toggle searchbar">
<Icon id="close" />
<label type="button" class="join-item btn btn-ghost btn-square sm:inline-flex no-animation text-[#676767] min-h-10 h-10 w-10" for={SEARCHBAR_POPUP_ID} aria-label="Toggle searchbar">
<Icon id="close-search" />
</label>
</form>

{/* Suggestions slot */}
<div id={slot} />
<div id={slot}>
<TopSearchs suggestion={topSearch} mostSellerTerms={mostSellerTerms} />
</div>

{/* Send search events as the user types */}
<script
Expand Down
82 changes: 82 additions & 0 deletions components/search/Searchbar/ProductCardSearch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import type { Product } from "apps/commerce/types.ts";
import { mapProductToAnalyticsItem } from "apps/commerce/utils/productToAnalyticsItem.ts";
import Image from "apps/website/components/Image.tsx";
import { clx } from "../../../sdk/clx.ts";
import { formatPrice } from "../../../sdk/format.ts";
import { relative } from "../../../sdk/url.ts";
import { useOffer } from "../../../sdk/useOffer.ts";
import { useSendEvent } from "../../../sdk/useSendEvent.ts";

interface Props {
product: Product;
/** Preload card image */
preload?: boolean;

/** @description used for analytics event */
itemListName?: string;

/** @description index of the product card in the list */
index?: number;

class?: string;
}

const WIDTH = 73;
const HEIGHT = 102;
const ASPECT_RATIO = `${WIDTH} / ${HEIGHT}`;

function ProductCardSearch({ product, preload, itemListName, index, class: _class }: Props) {
const { url, image: images, offers, isVariantOf } = product;

const title = isVariantOf?.name ?? product.name;
const [front, back] = images ?? [];

const { listPrice, price, availability } = useOffer(offers);
const inStock = availability === "https://schema.org/InStock";

const relativeUrl = relative(url);

const item = mapProductToAnalyticsItem({ product, price, listPrice, index });

{
/* Add click event to dataLayer */
}
const event = useSendEvent({
on: "click",
event: {
name: "select_item" as const,
params: {
item_list_name: itemListName,
items: [item],
},
},
});

return (
<div {...event} class={clx("card card-side text-sm gap-1", _class)}>
<figure class={clx("relative", "rounded border border-transparent", "w-[73px] min-w-[73px] h-[102px]")} style={{ aspectRatio: ASPECT_RATIO }}>
{/* Product Images */}
<a href={relativeUrl} aria-label="view product" class={clx("absolute top-0 left-0", "grid grid-cols-1 grid-rows-1", "w-full", !inStock && "opacity-70")}>
<Image src={front.url!} alt={front.alternateName} width={WIDTH} height={HEIGHT} style={{ aspectRatio: ASPECT_RATIO }} class={clx("object-cover", "rounded w-full", "col-span-full row-span-full")} sizes="(max-width: 640px) 50vw, 20vw" preload={preload} loading={preload ? "eager" : "lazy"} decoding="async" />
<Image src={back?.url ?? front.url!} alt={back?.alternateName ?? front.alternateName} width={WIDTH} height={HEIGHT} style={{ aspectRatio: ASPECT_RATIO }} class={clx("object-cover", "rounded w-full", "col-span-full row-span-full", "transition-opacity opacity-0 lg:group-hover:opacity-100")} sizes="(max-width: 640px) 50vw, 20vw" loading="lazy" decoding="async" />
</a>
</figure>

<a href={relativeUrl} className="">
<span class="font-bold text-base-content lg:text-accent text-xs leading-[18px]">{title}</span>

<div class="flex items-center gap-[5px] pt-4">
{Boolean(listPrice) && (
<>
<span class="line-through text-xs leading-[14.4px] font-semibold text-[#c5c5c5]">{formatPrice(listPrice, offers?.priceCurrency)}</span>
<span class="text-sm leading-[14.4px] font-semibold text-primary"></span>
</>
)}
<span class="text-sm leading-[16.8px] font-bold text-primary">{formatPrice(price, offers?.priceCurrency)}</span>
</div>
</a>
</div>
);
}

export default ProductCardSearch;
Loading

0 comments on commit c62c2cb

Please sign in to comment.