diff --git a/.deco/blocks/Header.json b/.deco/blocks/Header.json index bbb0ae7..a670c0a 100644 --- a/.deco/blocks/Header.json +++ b/.deco/blocks/Header.json @@ -18,16 +18,9 @@ }, "__resolveType": "resolved" }, - "mostSellerTerms": [ - "Vestidos Estampados", - "Conjuntinhos", - "Camisetas", - "Bonecas" - ] + "mostSellerTerms": ["Vestidos Estampados", "Conjuntinhos", "Camisetas", "Bonecas"] }, - "alerts": [ - "

Get 10% off today: NEW10

" - ], + "alerts": ["

Get 10% off today: NEW10

"], "logo": { "src": "https://deco-sites-assets.s3.sa-east-1.amazonaws.com/alphabeto/e30a75c7-a480-4ecb-828e-e76c39805c71/svgviewer-output-(21).svg", "alt": "Logo Alphabeto", @@ -35,6 +28,29 @@ "height": 42 }, "loading": "eager", + "links": [ + { + "href": "/", + "title": "Fale com as fadinhas" + }, + { + "title": "Troca fácil", + "href": "/" + }, + { + "title": "Seja um franqueado", + "href": "/" + }, + { + "title": "Seja um revendedor", + "href": " /" + } + ], + "benefits": { + "alerts": [], + "benefits": ["

Frete fixo de R$ 9,90

", "

Item 2

"], + "interval": 5 + }, "navItems": [ { "submenu": [], @@ -166,30 +182,9 @@ "menuItem": "Bazar" } ], - "links": [ - { - "href": "/", - "title": "Fale com as fadinhas" - }, - { - "title": "Troca fácil", - "href": "/" - }, - { - "title": "Seja um franqueado", - "href": "/" - }, - { - "title": "Seja um revendedor", - "href": " /" - } - ], - "benefits": { - "alerts": [], - "benefits": [ - "

Frete fixo de R$ 9,90

", - "

Item 2

" - ], - "interval": 5 + "googleMapsApiKey": { + "__resolveType": "website/loaders/secret.ts", + "name": "GOOGLE_MAPS_API_KEY", + "encrypted": "6187760b6f56eccaa52720e22e451805632cc03d6aa6896a5dded87ccdaafc77c37cda5bfbd5ef05c358635ab0ba12fb" } -} \ No newline at end of file +} diff --git a/.deco/blocks/pages-home-c4bcbfb771e9.json b/.deco/blocks/pages-home-c4bcbfb771e9.json index 97d013a..1eb3984 100644 --- a/.deco/blocks/pages-home-c4bcbfb771e9.json +++ b/.deco/blocks/pages-home-c4bcbfb771e9.json @@ -47,20 +47,18 @@ "preload": true }, { - "__resolveType": "website/sections/Rendering/Lazy.tsx", - "section": { - "__resolveType": "site/sections/Product/ProductShelf.tsx", - "products": { - "__resolveType": "vtex/loaders/workflow/products.ts", - "page": 3, - "pagesize": 1, - "props": { - "sort": "OrderByPriceDESC", - "term": "vestido", - "count": 7 - } + "__resolveType": "site/sections/Product/ProductShelf.tsx", + "products": { + "__resolveType": "vtex/loaders/intelligentSearch/productList.ts", + "page": 3, + "pagesize": 1, + "props": { + "sort": "price:desc", + "collection": "773", + "count": 16 } - } + }, + "title": "Nossos Queridinhos" }, { "__resolveType": "website/sections/Rendering/Lazy.tsx", diff --git a/actions/minicart/submit.ts b/actions/minicart/submit.ts index 61acd45..7134ab1 100644 --- a/actions/minicart/submit.ts +++ b/actions/minicart/submit.ts @@ -18,20 +18,22 @@ const actions: Record = { nuvemshop: nuvemshop as CartSubmitActions, }; -interface CartForm { +interface CartFormData { items: number[]; coupon: string | null; action: string | null; platformCart: unknown; addToCart: unknown; sellerCode: string | null; + cep: string | null; } export interface CartSubmitActions { - addToCart?: (props: CartForm, req: Request, ctx: AC) => Promise; - setQuantity?: (props: CartForm, req: Request, ctx: AC) => Promise; - setCoupon?: (props: CartForm, req: Request, ctx: AC) => Promise; - setSellerCode?: (props: CartForm, req: Request, ctx: AC) => Promise; + addToCart?: (props: CartFormData, req: Request, ctx: AC) => Promise; + setQuantity?: (props: CartFormData, req: Request, ctx: AC) => Promise; + setCoupon?: (props: CartFormData, req: Request, ctx: AC) => Promise; + setSellerCode?: (props: CartFormData, req: Request, ctx: AC) => Promise; + setShipping?: (props: CartFormData, req: Request, ctx: AC) => Promise; } const safeParse = (payload: string | null) => { @@ -42,15 +44,20 @@ const safeParse = (payload: string | null) => { } }; +interface FormData { + entries: () => IterableIterator<[string, string]>; +} + // Reconstruct the cart state from the received form data const cartFrom = (form: FormData) => { - const cart: CartForm = { + const cart: CartFormData = { items: [], coupon: null, platformCart: null, action: null, addToCart: null, sellerCode: null, + cep: null, }; for (const [name, value] of form.entries()) { @@ -67,22 +74,35 @@ const cartFrom = (form: FormData) => { cart.items[Number(it)] = Number(value); } else if (name === "add-to-cart") { cart.addToCart = safeParse(decodeURIComponent(value.toString())); + } else if (name === "cep") { + cart.cep = value.toString(); } } return cart; }; +const formActionsToCartActions: Record = { + "set-coupon": "setCoupon", + "add-to-cart": "addToCart", + "set-seller-code": "setSellerCode", + "set-quantity": "setQuantity", + "set-shipping": "setShipping", +}; + async function action(_props: unknown, req: Request, ctx: AppContext): Promise { - const { setQuantity, setCoupon, addToCart, setSellerCode } = actions[usePlatform()]; + const platformActions = actions[usePlatform()] as CartSubmitActions; - const form = cartFrom(await req.formData()); + const form = cartFrom((await req.formData()) as unknown as FormData); + const action = form.action as string | null; - const handler = form.action === "set-coupon" ? setCoupon : form.action === "add-to-cart" ? addToCart : form.action === "set-seller-code" ? setSellerCode : setQuantity; + const decoActionName = action === null ? "setQuantity" : action in formActionsToCartActions ? formActionsToCartActions[action] : "setQuantity"; + const handler = platformActions[decoActionName]; if (!handler) { throw new Error(`Unsupported action on platform ${usePlatform()}`); } + return await handler(form, req, ctx); } diff --git a/apps/site.ts b/apps/site.ts index 2bd2e62..63ed5d0 100644 --- a/apps/site.ts +++ b/apps/site.ts @@ -1,3 +1,5 @@ +import { type App as A, type AppContext as AC } from "@deco/deco"; +import { type Section } from "@deco/deco/blocks"; import commerce from "apps/commerce/mod.ts"; import { color as linx } from "apps/linx/mod.ts"; import { color as nuvemshop } from "apps/nuvemshop/mod.ts"; @@ -8,8 +10,6 @@ import { color as wake } from "apps/wake/mod.ts"; import { Props as WebsiteProps } from "apps/website/mod.ts"; import { rgb24 } from "std/fmt/colors.ts"; import manifest, { Manifest } from "../manifest.gen.ts"; -import { type Section } from "@deco/deco/blocks"; -import { type App as A, type AppContext as AC } from "@deco/deco"; export interface Props extends WebsiteProps { /** * @title Active Commerce Platform @@ -19,15 +19,8 @@ export interface Props extends WebsiteProps { platform: Platform; theme?: Section; } -export type Platform = - | "vtex" - | "vnda" - | "shopify" - | "wake" - | "linx" - | "nuvemshop" - | "custom"; -export let _platform: Platform = "custom"; +export type Platform = "vtex" | "vnda" | "shopify" | "wake" | "linx" | "nuvemshop" | "custom"; +export let _platform: Platform = "vtex"; export type App = ReturnType; // @ts-ignore somehow deno task check breaks, I have no idea why export type AppContext = AC; @@ -58,25 +51,17 @@ let firstRun = true; * @category Tool * @logo https://ozksgdmyrqcxcwhnbepg.supabase.co/storage/v1/object/public/assets/1/0ac02239-61e6-4289-8a36-e78c0975bcc8 */ -export default function Site({ ...state }: Props): A, -]> { +export default function Site({ ...state }: Props): A]> { _platform = state.platform || "custom"; // Prevent console.logging twice if (firstRun) { firstRun = false; - console.info( - ` ${rgb24("Storefront", color("deco"))} | ${ - rgb24(_platform, color(_platform)) - } \n`, - ); + console.info(` ${rgb24("Storefront", color("deco"))} | ${rgb24(_platform, color(_platform))} \n`); } return { state, manifest, - dependencies: [ - commerce(state), - ], + dependencies: [commerce(state)], }; } export { onBeforeResolveProps, Preview } from "apps/website/mod.ts"; diff --git a/components/Session.tsx b/components/Session.tsx index 570cce0..88980b5 100644 --- a/components/Session.tsx +++ b/components/Session.tsx @@ -1,17 +1,22 @@ import { Head } from "$fresh/runtime.ts"; import { useScript } from "@deco/deco/hooks"; import { type Person } from "apps/commerce/types.ts"; +import * as Htmx from "npm:htmx.org"; +import { MINICART_DRAWER_ID } from "site/constants.ts"; import { type AppContext } from "../apps/site.ts"; -import { MINICART_DRAWER_ID } from "../constants.ts"; import { useComponent } from "../sections/Component.tsx"; import { type Item } from "./minicart/Item.tsx"; import CartProvider, { type Minicart, MinicartSettings } from "./minicart/Minicart.tsx"; -import Drawer from "./ui/Drawer.tsx"; +import Drawer from "./ui/Drawer/index.tsx"; import UserProvider from "./user/Provider.tsx"; import WishlistProvider, { type Wishlist } from "./wishlist/Provider.tsx"; + declare global { + const htmx: typeof Htmx; + interface Window { STOREFRONT: SDK; + htmx: typeof Htmx; } } export interface Cart { diff --git a/components/header/GeolocationOffers/index.tsx b/components/header/GeolocationOffers/index.tsx new file mode 100644 index 0000000..dd0ceb3 --- /dev/null +++ b/components/header/GeolocationOffers/index.tsx @@ -0,0 +1,237 @@ +import { useScript } from "@deco/deco/hooks"; +import { TypedResponse } from "apps/utils/http.ts"; +import { AppContext } from "apps/vtex/mod.ts"; +import { getCookies } from "std/http/cookie.ts"; +import { useComponent } from "../../../sections/Component.tsx"; +import { IconOffersLocal } from "../../Icons/IconOffersLocal.tsx"; +import Button, { ButtonLabel, ButtonType } from "../../ui/Button.tsx"; +import Icon from "../../ui/Icon.tsx"; +import Input from "../../ui/Input.tsx"; +import Modal from "../../ui/Modal.tsx"; + +export const ids = { + GEOLOCATION_OFFERS_WRAPPER_ID: "geolocation-offers-wrapper", + GEOLOCATION_OFFERS_MODAL_ID: "geolocation-offers-modal", + GEOLOCATION_CEP_INPUT_ID: "modal-gelocation-offers-cep", + GEOLOCATION_USE_LOCATION_BUTTON_ID: "modal-gelocation-offers-use-location", + GEOLOCATION_OFFERS_FORM_ID: "modal-gelocation-offers-form", + GEOLOCATION_OFFERS_MODAL_CONTENT_ID: "modal-gelocation-offers-content", + GEOLOCATION_OFFERS_MODAL_LABEL_OPEN_ID: "modal-gelocation-offers-label-open", + GEOLOCATION_OFFERS_MODAL_REPLACE_WRAPPER: "modal-geolocation-replace-wrapper", + GEOLOCATION_OFFERS_FORM_BUTTON_ID: "modal-gelocation-offers-button", +}; + +interface GoogleGeoPosition { + results: { + address_components: { + long_name: string; + types: string[]; + }[]; + }[]; +} + +const loadScript = ({ GEOLOCATION_CEP_INPUT_ID, GEOLOCATION_USE_LOCATION_BUTTON_ID }: typeof ids, googleMapsApiKey: string) => { + const maskCEP = () => { + const cepInput = document.getElementById(GEOLOCATION_CEP_INPUT_ID) as HTMLInputElement | null; + if (!cepInput) return; + const mask = (value: string) => value.replace(/\D/g, "").replace(/(\d{5})(\d{3})?/, "$1-$2"); + cepInput.addEventListener("input", (event) => { + const target = event.target as HTMLInputElement; + const value = target.value; + target.value = mask(value); + }); + }; + const applyGetLocation = () => { + const button = document.getElementById(GEOLOCATION_USE_LOCATION_BUTTON_ID) as HTMLButtonElement | null; + const cepInput = document.getElementById(GEOLOCATION_CEP_INPUT_ID) as HTMLInputElement | null; + if (!googleMapsApiKey) return; + if (!cepInput) return; + if (!button) return; + + const fetchLocation = () => { + navigator.geolocation.getCurrentPosition( + (position) => { + const { latitude, longitude } = position.coords; + fetch("https://maps.google.com/maps/api/geocode/json?latlng=" + latitude + "," + longitude + "&sensor=false&key=" + googleMapsApiKey) + .then((response) => response.json()) + .then((data: GoogleGeoPosition) => { + const address = data.results[0].address_components; + const cep = address.find((component) => component.types.includes("postal_code")); + if (cep) { + cepInput.value = cep.long_name; + } else { + alert("Não foi possível obter o CEP da sua localização"); + } + }) + .catch(() => { + alert("Não foi possível obter a sua localização"); + }); + }, + (error) => { + if (error.code === error.PERMISSION_DENIED) { + alert("Você negou a permissão de localização"); + } else { + alert("Não foi possível obter a sua localização"); + } + } + ); + }; + + button.addEventListener("click", fetchLocation); + }; + maskCEP(); + applyGetLocation(); +}; + +interface Props { + googleMapsApiKey: string; + cep?: string; +} + +const numberOnly = (value: string) => value.replace(/\D/g, ""); + +export const loader = async (props: Props, req: Request, ctx: AppContext) => { + let cep = props.cep; + const isPost = req.method.toLowerCase() === "post"; + if (isPost) { + const formData = await req.formData(); + const formCEP = formData.get("cep")?.toString(); + if (formCEP?.length) cep = formCEP; + } + + if (cep && cep.length === 9 && isPost) { + const vtexClient = await ctx.invoke.vtex.loaders.config(); + interface VtexPostalCodeResponse { + country: string; + postalCode: string; + city: string; + } + const postalCodeResponse = (await vtexClient.vcs["GET /api/checkout/pub/postal-code/:countryCode/:postalCode"]({ + countryCode: "BRA", + postalCode: numberOnly(cep), + })) as TypedResponse; + const { country, postalCode, city } = await postalCodeResponse.json(); + + const cookies = getCookies(req.headers); + const segmentCookie = cookies["vtex_segment"]; + const sessionCookie = cookies["vtex_session"]; + if (!segmentCookie || !sessionCookie) { + const headers = new Headers(); + headers.append("content-type", "application/json"); + const sessionResponse = await fetch(`https://alphabeto.myvtex.com/api/sessions`, { + method: "POST", + body: JSON.stringify({ + public: { + city: { + value: city, + }, + postalCode: { + value: postalCode, + }, + country: { + value: country, + }, + }, + }), + headers, + }); + interface VtexSessionResponse { + sessionToken: string; + segmentToken: string; + } + const sessionData = (await sessionResponse.json()) as VtexSessionResponse; + ctx.response.headers.set("Set-Cookie", `vtex_session=${sessionData.sessionToken}; vtex_segment=${sessionData.segmentToken}; Path=/;`); + } else { + const headers = new Headers(); + headers.append("content-type", "application/json"); + headers.append("Cookie", `vtex_session=${sessionCookie}; vtex_segment=${segmentCookie}`); + await fetch(`https://alphabeto.myvtex.com/api/sessions`, { + method: "PUT", + body: JSON.stringify({ + public: { + city: { + value: city, + }, + postalCode: { + value: postalCode, + }, + country: { + value: country, + }, + }, + }), + headers, + }); + } + + return { + ...props, + cep, + }; + } +}; + +export default function GeolocationOffers(props: Props) { + return ( +
+ +
+
+ +

+ Quer ofertas? +

+

+ Coloque o seu CEP que achamos os melhores preços e prazos de entrega perto de você. +

+
(import.meta.url, { + ...props, + })} + hx-target={`#${ids.GEOLOCATION_OFFERS_MODAL_REPLACE_WRAPPER}`} + hx-swap="outterHTML" + hx-disabled-elt={`this, #${ids.GEOLOCATION_OFFERS_FORM_BUTTON_ID}`} + hx-indicator={`#${ids.GEOLOCATION_OFFERS_MODAL_CONTENT_ID}`} + > + + + +
+ + cancelar + + +
+
+
+
+
+ +