diff --git a/apps/web/app/api/feed/sync/route.ts b/apps/web/app/api/feed/sync/route.ts index efa300d..ea393e6 100644 --- a/apps/web/app/api/feed/sync/route.ts +++ b/apps/web/app/api/feed/sync/route.ts @@ -4,6 +4,7 @@ import { storefrontClient } from "clients/storefrontClient" import { env } from "env.mjs" import type { FailedAttemptError } from "p-retry" import { compareHmac } from "utils/compare-hmac" +import { enrichProduct } from "utils/enrich-product" type SupportedTopic = "products/update" | "products/delete" | "products/create" | "collections/update" | "collections/delete" | "collections/create" @@ -80,11 +81,15 @@ async function handleProductTopics(topic: SupportedTopic, { id }: Record diff --git a/apps/web/app/products/[slug]/draft/page.tsx b/apps/web/app/products/[slug]/draft/page.tsx index da8bc96..2c34456 100644 --- a/apps/web/app/products/[slug]/draft/page.tsx +++ b/apps/web/app/products/[slug]/draft/page.tsx @@ -43,14 +43,14 @@ export async function generateStaticParams() { async function ProductView({ slug }: { slug: string }) { const product = await getDraftAwareProduct(slug) - const { color, size } = getOptionsFromUrl(slug) - const hasInvalidOptions = !hasValidOption(product?.variants, "color", color) || !hasValidOption(product?.variants, "size", size) + const { color } = getOptionsFromUrl(slug) + const hasInvalidOptions = !hasValidOption(product?.variants, "color", color) if (!product || hasInvalidOptions) { return notFound() } - const combination = getCombination(product as CommerceProduct, color, size) + const combination = getCombination(product as CommerceProduct, color) const lastCollection = product?.collections?.findLast(Boolean) const hasOnlyOneVariant = product.variants.length <= 1 diff --git a/apps/web/app/products/[slug]/page.tsx b/apps/web/app/products/[slug]/page.tsx index 5cf5a7b..9de0dbc 100644 --- a/apps/web/app/products/[slug]/page.tsx +++ b/apps/web/app/products/[slug]/page.tsx @@ -41,17 +41,19 @@ export default async function Product({ params: { slug } }: ProductProps) { } async function ProductView({ slug }: { slug: string }) { - const product = await getProduct(removeOptionsFromUrl(slug)) - const { reviews, total: totalReviews } = await getProductReviews(removeOptionsFromUrl(slug), { limit: 4 }) + const [product, { reviews, total: totalReviews }] = await Promise.all([ + await getProduct(removeOptionsFromUrl(slug)), + await getProductReviews(removeOptionsFromUrl(slug), { limit: 4 }), + ]) - const { color, size } = getOptionsFromUrl(slug) - const hasInvalidOptions = !hasValidOption(product?.variants, "color", color) || !hasValidOption(product?.variants, "size", size) + const { color } = getOptionsFromUrl(slug) + const hasInvalidOptions = !hasValidOption(product?.variants, "color", color) if (!product || hasInvalidOptions) { return notFound() } - const combination = getCombination(product, color, size) + const combination = getCombination(product, color) const lastCollection = product?.collections?.findLast(Boolean) const hasOnlyOneVariant = product.variants.length <= 1 diff --git a/apps/web/components/Pagination/Pagination.tsx b/apps/web/components/Pagination/Pagination.tsx index 1ab5bfa..97bf9de 100644 --- a/apps/web/components/Pagination/Pagination.tsx +++ b/apps/web/components/Pagination/Pagination.tsx @@ -37,7 +37,7 @@ const PaginationLink = ({ className, isActive, children, href, disabled, "aria-l buttonVariants({ variant: "ghost", }), - "hidden size-9 items-center justify-center rounded-full border border-black bg-white px-0 py-0 text-[16px] text-slate-800 transition-colors hover:bg-black hover:text-white md:flex", + "size-9 items-center justify-center rounded-full border border-black bg-white px-0 py-0 text-[16px] text-slate-800 transition-colors hover:bg-black hover:text-white md:flex", { "bg-black font-bold text-white": isActive }, { "pointer-events-none cursor-not-allowed opacity-50": disabled }, className diff --git a/apps/web/constants/index.ts b/apps/web/constants/index.ts index d8c4219..20b431f 100644 --- a/apps/web/constants/index.ts +++ b/apps/web/constants/index.ts @@ -11,4 +11,7 @@ export const BUCKETS = { HOME: ["a", "b"], } as const -export const facetParams = ["q", "minPrice", "maxPrice", "sortBy", "categories", "vendors", "tags", "colors", "sizes", "rating"] +export const facetParams = ["q", "minPrice", "maxPrice", "sortBy", "categories", "vendors", "tags", "colors", "sizes", "rating"] as const +export const HIERARCHICAL_ATRIBUTES = ["hierarchicalCategories.lvl0", "hierarchicalCategories.lvl1", "hierarchicalCategories.lvl2"] as const +export const HIERARCHICAL_SEPARATOR = " > " +export const HITS_PER_PAGE = 24 diff --git a/apps/web/env.mjs b/apps/web/env.mjs index cdb1bf3..aa6a119 100644 --- a/apps/web/env.mjs +++ b/apps/web/env.mjs @@ -8,6 +8,7 @@ export const env = createEnv({ SHOPIFY_STORE_DOMAIN: z.string(), SHOPIFY_ADMIN_ACCESS_TOKEN: z.string().optional(), SHOPIFY_APP_API_SECRET_KEY: z.string().optional(), + SHOPIFY_HIERARCHICAL_NAV_HANDLE: z.string().optional(), MEILISEARCH_PRODUCTS_INDEX: z.string(), MEILISEARCH_CATEGORIES_INDEX: z.string(), MEILISEARCH_ADMIN_KEY: z.string().optional(), @@ -36,6 +37,7 @@ export const env = createEnv({ SHOPIFY_ADMIN_ACCESS_TOKEN: process.env.SHOPIFY_ADMIN_ACCESS_TOKEN || "demo", SHOPIFY_APP_API_SECRET_KEY: process.env.SHOPIFY_APP_API_SECRET_KEY || "demo", SHOPIFY_STORE_DOMAIN: process.env.SHOPIFY_STORE_DOMAIN || "demo", + SHOPIFY_HIERARCHICAL_NAV_HANDLE: process.env.SHOPIFY_HIERARCHICAL_NAV_HANDLE, MEILISEARCH_PRODUCTS_INDEX: process.env.MEILISEARCH_PRODUCTS_INDEX || "products", MEILISEARCH_CATEGORIES_INDEX: process.env.MEILISEARCH_CATEGORIES_INDEX || "categories", MEILISEARCH_ADMIN_KEY: process.env.MEILISEARCH_ADMIN_KEY || "demo", diff --git a/apps/web/utils/enrich-product.ts b/apps/web/utils/enrich-product.ts new file mode 100644 index 0000000..77ef975 --- /dev/null +++ b/apps/web/utils/enrich-product.ts @@ -0,0 +1,89 @@ +import { PlatformImage, PlatformMenu, PlatformProduct } from "@enterprise-commerce/core/platform/types" +import { replicate } from "clients/replicate" + +/* + * Enrich product by attaching hierarchical categories to it + * Takes in all tags, and tries to find hierarchy against it + * Currently just shopify focused + */ + +export const enrichProduct = async (product: PlatformProduct, collections: PlatformMenu["items"]) => { + const categoryMap = buildCategoryMap(collections) + const images = await generateProductAltTags(product) + const hierarchicalCategories = generateHierarchicalCategories(product.tags, categoryMap) + + return { + ...product, + images: images.filter(Boolean), + hierarchicalCategories, + } +} + +function buildCategoryMap(categories: PlatformMenu["items"]) { + const categoryMap = new Map() + + function traverse(items: PlatformMenu["items"], path: string[]) { + for (const item of items) { + const newPath = [...path, item.resource!.handle] + categoryMap.set(item.resource!.handle, newPath) + if (item.items && item.items.length > 0) { + traverse(item.items, newPath) + } + } + } + + traverse(categories, []) + + return categoryMap +} + +function generateHierarchicalCategories(tags: PlatformProduct["tags"], categoryMap: Map) { + const hierarchicalCategories: Record<"lvl0" | "lvl1" | "lvl2", string[]> = { lvl0: [], lvl1: [], lvl2: [] } + + if (!tags.length || !categoryMap.size) return hierarchicalCategories + + tags.forEach((tag) => { + const path = categoryMap.get(tag) + if (path) { + if (path.length > 0 && !hierarchicalCategories.lvl0.includes(path[0])) { + hierarchicalCategories.lvl0.push(path[0]) + } + if (path.length > 1) { + const lvl1Path = path.slice(0, 2).join(" > ") + if (!hierarchicalCategories.lvl1.includes(lvl1Path)) { + hierarchicalCategories.lvl1.push(lvl1Path) + } + } + if (path.length > 2) { + const lvl2Path = path.slice(0, 3).join(" > ") + if (!hierarchicalCategories.lvl2.includes(lvl2Path)) { + hierarchicalCategories.lvl2.push(lvl2Path) + } + } + } + }) + + return hierarchicalCategories +} + +async function generateProductAltTags(product: PlatformProduct): Promise<(PlatformImage | undefined)[]> { + try { + const altTagAwareImages = await Promise.all(product?.images?.slice(0, 1).map(mapper).filter(Boolean)) + return [...altTagAwareImages, ...product?.images?.slice(1)?.filter(Boolean)] || [] + } catch (e) { + console.error(e) + return product.images // graceful exit + } + + async function mapper(image: PlatformImage) { + if (!replicate) return + const output = (await replicate.run("salesforce/blip:2e1dddc8621f72155f24cf2e0adbde548458d3cab9f00c0139eea840d0ac4746", { + input: { + task: "image_captioning", + image: image.url, + }, + })) as unknown as string + + return { ...image, altText: output?.replace("Caption:", "") || "" } + } +} diff --git a/apps/web/utils/productOptionsUtils.ts b/apps/web/utils/productOptionsUtils.ts index 61dbd63..abfb0f4 100644 --- a/apps/web/utils/productOptionsUtils.ts +++ b/apps/web/utils/productOptionsUtils.ts @@ -7,11 +7,10 @@ export interface Combination { quantityAvailable?: number | null | undefined price: PlatformVariant["price"] | undefined title: string - size?: string color?: string } -type Option = keyof Pick +type Option = keyof Pick export function getAllCombinations(variants: PlatformVariant[]): Combination[] { return variants?.map((variant) => ({ @@ -24,15 +23,12 @@ export function getAllCombinations(variants: PlatformVariant[]): Combination[] { })) } -export function getCombination(product: CommerceProduct, color: string | null, size: string | null) { +export function getCombination(product: CommerceProduct, color: string | null) { const hasOnlyOneVariant = product.variants.length <= 1 const defaultColor = product.flatOptions?.["Color"]?.find(Boolean)?.toLowerCase() ?? undefined - const defaultSize = product.flatOptions?.["Size"]?.find(Boolean)?.toLowerCase() ?? undefined - return hasOnlyOneVariant - ? product.variants.find(Boolean) - : getAllCombinations(product.variants).find((combination) => combination.size === (size ?? defaultSize) && combination.color === (color ?? defaultColor)) + return hasOnlyOneVariant ? product.variants.find(Boolean) : getAllCombinations(product.variants).find((combination) => combination.color === (color ?? defaultColor)) } export function hasValidOption(variants: PlatformVariant[] | null | undefined, optionName: Option, optionValue: string | null): boolean { @@ -43,37 +39,31 @@ export function hasValidOption(variants: PlatformVariant[] | null | undefined, o return !optionValue || combinations.includes(optionValue) } -export function createOptionfulUrl(originalUrl: string, size: string | null | undefined, color: string | null | undefined) { +export function createOptionfulUrl(originalUrl: string, color: string | null | undefined) { let urlWithoutParams = removeOptionsFromUrl(originalUrl) - const newSizeParam = size ? `-size_${size}` : "" const newColorParam = color ? `-color_${color}` : "" - return `${urlWithoutParams}${newSizeParam}${newColorParam}` + return `${urlWithoutParams}${newColorParam}` } export function removeOptionsFromUrl(pathname: string) { - const sizePattern = /-size_([0-9a-zA-Z\s]+)/ const colorPattern = /-color_([0-9a-zA-Z\s]+)/ - return decodeURIComponent(pathname).replace(sizePattern, "").replace(colorPattern, "") + return decodeURIComponent(pathname).replace(colorPattern, "") } export function getOptionsFromUrl(pathname: string) { const result: Record = { - size: null, color: null, } - const sizePattern = /-size_([0-9a-zA-Z\s]+)/ const colorPattern = /-color_([0-9a-zA-Z\s]+)/ const decodedPathname = decodeURIComponent(pathname) - const sizeMatch = decodedPathname.match(sizePattern) const colorMatch = decodedPathname.match(colorPattern) - if (sizeMatch) result.size = sizeMatch[1].toLowerCase() if (colorMatch) result.color = colorMatch[1].toLowerCase() return result diff --git a/apps/web/utils/useHierarchicalMenu.ts b/apps/web/utils/useHierarchicalMenu.ts new file mode 100644 index 0000000..a94a30f --- /dev/null +++ b/apps/web/utils/useHierarchicalMenu.ts @@ -0,0 +1,60 @@ +import type { CategoriesDistribution } from "meilisearch" +import { useParams, useSearchParams } from "next/navigation" + +interface HierarchicalMenuOptions { + attributes: readonly string[] + distribution: Record + separator: string +} + +type HierarchicalMenuItem = Record + +export const useHierarchicalMenu = ({ attributes, distribution, separator }: HierarchicalMenuOptions) => { + const { slug } = useParams() + const searchParams = useSearchParams() + const normalizedSlug = Array.isArray(slug) ? slug.join(separator) : slug || searchParams.get("categories")?.split(",").join(separator) || "" + + const initialPath = findInitialPath(separator, normalizedSlug, attributes, distribution) || [] + + const getCategoryChildren = (path: string[]) => { + const level = path.length + const key = attributes[level] + const prefix = path.join(separator) + + return Object.keys(distribution[key] || {}) + .filter((item) => item.startsWith(prefix)) + .reduce((acc: HierarchicalMenuItem, item) => { + const category = item.split(separator)[level] + if (category && !acc[category]) { + acc[category] = distribution[key][item] + } + return acc + }, {}) + } + + const items: HierarchicalMenuItem = initialPath.length === 0 ? distribution[attributes[0]] : getCategoryChildren(initialPath) + const parent = initialPath.length > 0 ? initialPath.slice(0, -1).pop() : null + const current = initialPath.length > 0 ? initialPath.pop() : null + + return { + items, + current, + parent, + } +} + +const findInitialPath = (separator: string, activeCategory: string, attributes: readonly string[], distribution: Record): string[] => { + if (activeCategory === "") { + return [] + } + + for (let level = 0; level < attributes.length; level++) { + const key = attributes[level] + const categoriesAtLevel = Object.keys(distribution[key] || {}) + const path = categoriesAtLevel.find((item) => item.endsWith(activeCategory)) + if (path) { + return path.split(separator) + } + } + return [] +} diff --git a/apps/web/views/Category/CategoryView.tsx b/apps/web/views/Category/CategoryView.tsx index 67a02d9..d2f2eb3 100644 --- a/apps/web/views/Category/CategoryView.tsx +++ b/apps/web/views/Category/CategoryView.tsx @@ -18,7 +18,6 @@ export async function CategoryView({ params, searchParams = {} }: CategoryViewPr } /> diff --git a/apps/web/views/Homepage/CategoriesSection.tsx b/apps/web/views/Homepage/CategoriesSection.tsx index b61273b..5ab15ad 100644 --- a/apps/web/views/Homepage/CategoriesSection.tsx +++ b/apps/web/views/Homepage/CategoriesSection.tsx @@ -2,6 +2,7 @@ import { meilisearch } from "clients/meilisearch" import { Skeleton } from "components/Skeleton/Skeleton" import { env } from "env.mjs" import { unstable_cache } from "next/cache" +import Image from "next/image" import Link from "next/link" import { getDemoCategories, isDemoMode } from "utils/demoUtils" @@ -17,9 +18,9 @@ export async function CategoriesSection() {
{categories.map((singleCategory, index) => ( - -
- + +
+

{singleCategory.title}

diff --git a/apps/web/views/Listing/CategoryFacet.tsx b/apps/web/views/Listing/CategoryFacet.tsx index 586f1bb..4c90f68 100644 --- a/apps/web/views/Listing/CategoryFacet.tsx +++ b/apps/web/views/Listing/CategoryFacet.tsx @@ -1,36 +1,87 @@ +import { ArrowIcon } from "components/Icons/ArrowIcon" +import { HIERARCHICAL_ATRIBUTES, HIERARCHICAL_SEPARATOR } from "constants/index" +import type { CategoriesDistribution } from "meilisearch" +import { usePathname } from "next/navigation" import { cn } from "utils/cn" import { slugToName } from "utils/slug-name" +import { useHierarchicalMenu } from "utils/useHierarchicalMenu" interface CategoryFacetProps { title: string - distribution: Record | undefined + distribution: Record isChecked: (value: string) => boolean onCheckedChange: (checked: boolean, value: string) => void + onBackClick: (currentCategory: string | null, parentSlug: string | null) => void } -export function CategoryFacet({ distribution, isChecked, onCheckedChange }: CategoryFacetProps) { - const distributionsEntries = Object.entries(distribution || {}) - const hasNoResults = distributionsEntries.length === 0 +export function CategoryFacet({ distribution, isChecked, onCheckedChange, onBackClick }: CategoryFacetProps) { + const { items, current } = useHierarchicalMenu({ + attributes: HIERARCHICAL_ATRIBUTES, + distribution: distribution, + separator: HIERARCHICAL_SEPARATOR, + }) + + const distributionsEntries = Object.entries(items || {}) function handleClick(value: string) { onCheckedChange(!isChecked(value), value) } return ( -
- {hasNoResults ? null : ( -
- {distributionsEntries.map(([value, count], index) => ( - - ))} -
- )} +
+ + {!!current &&

{slugToName(current)}

} +
+ {distributionsEntries.map(([value, count], index) => ( + + ))} +
+ {distributionsEntries.length === 0 && !current &&

No categories found

} +
) } + +const BackButton = ({ distribution, onBackClick }: { distribution: CategoryFacetProps["distribution"]; onBackClick: CategoryFacetProps["onBackClick"] }) => { + const pathname = usePathname() + const { parent, current } = useHierarchicalMenu({ + attributes: HIERARCHICAL_ATRIBUTES, + distribution: distribution, + separator: HIERARCHICAL_SEPARATOR, + }) + + const parentCategory = { + label: !!parent ? slugToName(parent) : null, + value: parent || null, + } + const currentCategory = { + label: !!current ? slugToName(current) : null, + value: current || null, + } + + if (pathname === "/search") { + return ( + <> + {!parentCategory.value && !currentCategory.value ? ( + All Categories: + ) : ( + + )} + + ) + } + + return ( + parentCategory.value && ( + + ) + ) +} diff --git a/apps/web/views/Listing/Controls.tsx b/apps/web/views/Listing/Controls.tsx new file mode 100644 index 0000000..98e3263 --- /dev/null +++ b/apps/web/views/Listing/Controls.tsx @@ -0,0 +1,27 @@ +import { Suspense } from "react" +import { FacetsMobile } from "./FacetsMobile" +import { Sorter } from "./Sorter" +import { CategoriesDistribution } from "meilisearch" + +type ControlsProps = { + facetDistribution: Record + totalHits: number + disabledFacets?: string[] +} + +export const Controls = ({ disabledFacets, facetDistribution, totalHits }: ControlsProps) => { + return ( +
+ + {/* has to be wrapped w. suspense, nuqs is using useSearchParams in useQueryState + * https://github.com/47ng/nuqs/issues/496 + */} +
+ {totalHits} results found +
+ + + +
+ ) +} diff --git a/apps/web/views/Listing/FacetsContent.tsx b/apps/web/views/Listing/FacetsContent.tsx index 301cd3f..fe005bf 100644 --- a/apps/web/views/Listing/FacetsContent.tsx +++ b/apps/web/views/Listing/FacetsContent.tsx @@ -14,6 +14,8 @@ import { CategoryFacet } from "./CategoryFacet" import { PriceFacet } from "./PriceFacet" import { Sorter } from "./Sorter" import { RatingFacet } from "./RatingFacet" +import { HIERARCHICAL_ATRIBUTES } from "constants/index" +import { usePathname, useRouter } from "next/navigation" interface FacetsContentProps { facetDistribution: Record | undefined @@ -22,10 +24,14 @@ interface FacetsContentProps { } export function FacetsContent({ facetDistribution, className, disabledFacets }: FacetsContentProps) { - const collections = facetDistribution?.["collections.handle"] - const tags = facetDistribution?.["tags"] + const router = useRouter() + const pathname = usePathname() + + const collections: Record = HIERARCHICAL_ATRIBUTES.reduce((acc, key) => { + acc[key] = facetDistribution?.[key] || {} + return acc + }, {}) const vendors = facetDistribution?.["vendor"] - const sizes = facetDistribution?.["flatOptions.Size"] const colors = facetDistribution?.["flatOptions.Color"] const { set: setLastSelected, selected: lastSelected } = useFilterTransitionStore((s) => s) @@ -52,18 +58,14 @@ export function FacetsContent({ facetDistribution, className, disabledFacets }: history: "push", clearOnDefault: true, }) - const [selectedTags, setSelectedTags] = useQueryState("tags", { ...parseAsArrayOf(parseAsString), defaultValue: [], shallow: false, history: "push", clearOnDefault: true }) const [selectedColors, setSelectedColors] = useQueryState("colors", { ...parseAsArrayOf(parseAsString), defaultValue: [], shallow: false, history: "push", clearOnDefault: true }) - const [selectedSizes, setSelectedSizes] = useQueryState("sizes", { ...parseAsArrayOf(parseAsString), defaultValue: [], shallow: false, history: "push", clearOnDefault: true }) const [_, setPage] = useQueryState("page", { ...parseAsInteger, defaultValue: 1, shallow: false, history: "push", clearOnDefault: true }) const [minPrice, setMinPrice] = useQueryState("minPrice", { ...parseAsInteger, shallow: false, defaultValue: 0, clearOnDefault: true }) const [maxPrice, setMaxPrice] = useQueryState("maxPrice", { ...parseAsInteger, shallow: false, defaultValue: 0, clearOnDefault: true }) - const filtersCount = [selectedCategories, selectedVendors, selectedTags, selectedColors, selectedSizes, minPrice, maxPrice, selectedRating].filter((v) => - Array.isArray(v) ? v.length !== 0 : !!v - ).length + const filtersCount = [selectedCategories, selectedVendors, selectedColors, minPrice, maxPrice, selectedRating].filter((v) => (Array.isArray(v) ? v.length !== 0 : !!v)).length const roundedRatings = Object.entries(facetDistribution?.["avgRating"] || {}).reduce( (acc, [key, value]) => { @@ -87,12 +89,11 @@ export function FacetsContent({ facetDistribution, className, disabledFacets }: function resetAllFilters() { setSelectedCategories(null) setSelectedVendors(null) - setSelectedTags(null) setSelectedColors(null) - setSelectedSizes(null) setMinPrice(null) setMaxPrice(null) setSelectedRating(null) + setPage(1) } return ( @@ -100,17 +101,34 @@ export function FacetsContent({ facetDistribution, className, disabledFacets }: - {!disabledFacets?.includes("category") ? ( + {!disabledFacets?.includes("categories") && ( selectedCategories.includes(category)} + onBackClick={(currentCategory, parentSlug) => { + if (pathname === "/search") { + setSelectedCategories((prev) => { + if (!currentCategory) return [] + const index = prev.indexOf(currentCategory) + return prev.slice(0, index) + }) + + return + } + + router.push(`/category/${parentSlug}`) + }} onCheckedChange={(checked, category) => { - setSelectedCategories((prev) => (checked ? [...prev, category] : prev.filter((cat) => cat !== category))) - setPage(1) + if (pathname === "/search") { + setSelectedCategories((prev) => (checked ? [...prev, category] : prev.filter((cat) => cat !== category))) + return + } + + router.push(`/category/${category}`) }} /> - ) : null} + )}
@@ -118,21 +136,7 @@ export function FacetsContent({ facetDistribution, className, disabledFacets }:
- {!disabledFacets?.includes("tags") ? ( - selectedTags.includes(tag)} - onCheckedChange={(checked, tag) => { - setSelectedTags((prev) => (checked ? [...prev, tag] : prev.filter((cat) => cat !== tag))) - setLastSelected("tags") - setPage(1) - }} - /> - ) : null} - - {!disabledFacets?.includes("vendors") ? ( + {!disabledFacets?.includes("vendors") && ( - ) : null} - - {!disabledFacets?.includes("sizes") ? ( - selectedSizes.includes(size)} - onCheckedChange={(checked, size) => { - setSelectedSizes((prev) => (checked ? [...prev, size] : prev.filter((cat) => cat !== size))) - setLastSelected("sizes") - setPage(1) - }} - /> - ) : null} + )} - {!disabledFacets?.includes("colors") ? ( + {!disabledFacets?.includes("colors") && ( - ) : null} + )} - {!disabledFacets?.includes("avgRating") ? ( + {!disabledFacets?.includes("avgRating") && ( - ) : null} + )} Price Range @@ -205,11 +195,11 @@ export function FacetsContent({ facetDistribution, className, disabledFacets }: - {!!filtersCount ? ( + {!!filtersCount && (
resetAllFilters()}> Reset all filters {filtersCount}
- ) : null} + )}
) } diff --git a/apps/web/views/Listing/FacetsMobile.tsx b/apps/web/views/Listing/FacetsMobile.tsx index dff5963..f1d1332 100644 --- a/apps/web/views/Listing/FacetsMobile.tsx +++ b/apps/web/views/Listing/FacetsMobile.tsx @@ -1,5 +1,6 @@ "use client" +import { Button } from "components/Button/Button" import { Placeholder } from "components/GenericModal/GenericModal" import { FiltersIcon } from "components/Icons/FiltersIcon" import { CategoriesDistribution } from "meilisearch" @@ -20,9 +21,10 @@ export function FacetsMobile({ className, facetDistribution, disabledFacets }: F return (
-
openModal("facets-mobile")}> - -
+ {!!modals["facets-mobile"] && ( closeModal("facets-mobile")}> diff --git a/apps/web/views/Listing/HitsSection.tsx b/apps/web/views/Listing/HitsSection.tsx index a1a8f52..83469cb 100644 --- a/apps/web/views/Listing/HitsSection.tsx +++ b/apps/web/views/Listing/HitsSection.tsx @@ -10,7 +10,7 @@ export async function HitsSection({ hits }: HitsSectionProps) { return

No results for this query

} return ( -
+
{hits.map((singleResult, idx) => ( ))} diff --git a/apps/web/views/Listing/PaginationSection.tsx b/apps/web/views/Listing/PaginationSection.tsx index cf0dde7..e4c29d5 100644 --- a/apps/web/views/Listing/PaginationSection.tsx +++ b/apps/web/views/Listing/PaginationSection.tsx @@ -1,5 +1,4 @@ import { Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious } from "components/Pagination/Pagination" -import { cn } from "utils/cn" const PAGE_OFFSET = 2 @@ -42,10 +41,10 @@ export function PaginationSection({ queryParams, totalPages }: PaginationSection {pages.map((singlePage, idx) => ( {singlePage} diff --git a/apps/web/views/Listing/composeFilters.test.ts b/apps/web/views/Listing/composeFilters.test.ts index ab68617..876ede6 100644 --- a/apps/web/views/Listing/composeFilters.test.ts +++ b/apps/web/views/Listing/composeFilters.test.ts @@ -1,52 +1,46 @@ +import { HIERARCHICAL_SEPARATOR } from "constants/index" import { FilterBuilder } from "../../utils/filterBuilder" import { composeFilters } from "./composeFilters" describe("composeFilters", () => { test("should add a category filter when categories are present", () => { - const parsedSearchParams = { categories: ["electronics"], vendors: [], tags: [], colors: [], sizes: [], minPrice: null, maxPrice: null } - const filter = composeFilters(new FilterBuilder(), parsedSearchParams) + const parsedSearchParams = { categories: ["electronics"], vendors: [], colors: [], sizes: [], minPrice: null, maxPrice: null } + const filter = composeFilters(new FilterBuilder(), parsedSearchParams, HIERARCHICAL_SEPARATOR) expect(filter.build()).toStrictEqual(`(collections.handle IN ["electronics"])`) }) test("should add a vendor filter when vendors are present", () => { - const parsedSearchParams = { categories: [], vendors: ["Apple"], tags: [], colors: [], sizes: [], minPrice: null, maxPrice: null } - const filter = composeFilters(new FilterBuilder(), parsedSearchParams) + const parsedSearchParams = { categories: [], vendors: ["Apple"], colors: [], sizes: [], minPrice: null, maxPrice: null } + const filter = composeFilters(new FilterBuilder(), parsedSearchParams, HIERARCHICAL_SEPARATOR) expect(filter.build()).toStrictEqual(`(vendor IN ["Apple"])`) }) - test("should add a tags filter when tags are present", () => { - const parsedSearchParams = { categories: [], vendors: [], tags: ["Smartphone"], colors: [], sizes: [], minPrice: null, maxPrice: null } - const filter = composeFilters(new FilterBuilder(), parsedSearchParams) - - expect(filter.build()).toStrictEqual(`(tags IN ["Smartphone"])`) - }) - test("should add a color filter when colors are present", () => { - const parsedSearchParams = { categories: [], vendors: [], tags: [], colors: ["Black"], sizes: [], minPrice: null, maxPrice: null } - const filter = composeFilters(new FilterBuilder(), parsedSearchParams) + const parsedSearchParams = { categories: [], vendors: [], colors: ["Black"], sizes: [], minPrice: null, maxPrice: null } + const filter = composeFilters(new FilterBuilder(), parsedSearchParams, HIERARCHICAL_SEPARATOR) expect(filter.build()).toStrictEqual(`(flatOptions.Color IN ["Black"])`) }) test("should add a size filter when sizes are present", () => { - const parsedSearchParams = { categories: [], vendors: [], tags: [], colors: [], sizes: ["M"], minPrice: null, maxPrice: null } - const filter = composeFilters(new FilterBuilder(), parsedSearchParams) + const parsedSearchParams = { categories: [], vendors: [], colors: [], sizes: ["M"], minPrice: null, maxPrice: null } + const filter = composeFilters(new FilterBuilder(), parsedSearchParams, HIERARCHICAL_SEPARATOR) expect(filter.build()).toStrictEqual(`(flatOptions.Size IN ["M"])`) }) test("should add a minPrice filter when minPrice is specified", () => { - const parsedSearchParams = { categories: [], vendors: [], tags: [], colors: [], sizes: [], minPrice: 100, maxPrice: null } - const filter = composeFilters(new FilterBuilder(), parsedSearchParams) + const parsedSearchParams = { categories: [], vendors: [], colors: [], sizes: [], minPrice: 100, maxPrice: null } + const filter = composeFilters(new FilterBuilder(), parsedSearchParams, HIERARCHICAL_SEPARATOR) expect(filter.build()).toStrictEqual(`minPrice >= 100`) }) test("should add a maxPrice filter when maxPrice is specified", () => { - const parsedSearchParams = { categories: [], vendors: [], tags: [], colors: [], sizes: [], minPrice: null, maxPrice: 500 } - const filter = composeFilters(new FilterBuilder(), parsedSearchParams) + const parsedSearchParams = { categories: [], vendors: [], colors: [], sizes: [], minPrice: null, maxPrice: 500 } + const filter = composeFilters(new FilterBuilder(), parsedSearchParams, HIERARCHICAL_SEPARATOR) expect(filter.build()).toStrictEqual(`minPrice <= 500`) }) @@ -55,18 +49,17 @@ describe("composeFilters", () => { const parsedSearchParams = { categories: ["electronics"], vendors: ["Apple"], - tags: ["Smartphone"], colors: ["Black"], sizes: ["M"], minPrice: 100, maxPrice: 500, } - const filter = composeFilters(new FilterBuilder(), parsedSearchParams) + const filter = composeFilters(new FilterBuilder(), parsedSearchParams, HIERARCHICAL_SEPARATOR) const builtFilter = filter.build() expect(builtFilter).toStrictEqual( - '(collections.handle IN ["electronics"]) AND (vendor IN ["Apple"]) AND (tags IN ["Smartphone"]) AND (flatOptions.Color IN ["Black"]) AND (flatOptions.Size IN ["M"]) AND minPrice >= 100 AND minPrice <= 500' + '(collections.handle IN ["electronics"]) AND (vendor IN ["Apple"]) AND (flatOptions.Color IN ["Black"]) AND (flatOptions.Size IN ["M"]) AND minPrice >= 100 AND minPrice <= 500' ) }) }) diff --git a/apps/web/views/Listing/composeFilters.ts b/apps/web/views/Listing/composeFilters.ts index c06cc7c..0e28167 100644 --- a/apps/web/views/Listing/composeFilters.ts +++ b/apps/web/views/Listing/composeFilters.ts @@ -5,34 +5,24 @@ interface MakeFilterProps { maxPrice: number | null categories: string[] vendors: string[] - tags: string[] colors: string[] - sizes: string[] - rating: number | null + rating?: number | null } -export function composeFilters(filter: FilterBuilder, parsedSearchParams: MakeFilterProps) { +export function composeFilters(filter: FilterBuilder, parsedSearchParams: MakeFilterProps, separator: string) { const filterConditions = [ { predicate: parsedSearchParams.categories.length > 0, - action: () => filter.and().group((sub) => sub.in("collections.handle", parsedSearchParams.categories)), + action: () => filter.and().group((sub) => sub.in(`hierarchicalCategories.lvl${parsedSearchParams.categories.length - 1}`, [parsedSearchParams.categories.join(separator)])), }, { predicate: parsedSearchParams.vendors.length > 0, action: () => filter.and().group((sub) => sub.in("vendor", parsedSearchParams.vendors)), }, - { - predicate: parsedSearchParams.tags.length > 0, - action: () => filter.and().group((sub) => sub.in("tags", parsedSearchParams.tags)), - }, { predicate: parsedSearchParams.colors.length > 0, action: () => filter.and().group((sub) => sub.in("flatOptions.Color", parsedSearchParams.colors)), }, - { - predicate: parsedSearchParams.sizes.length > 0, - action: () => filter.and().group((sub) => sub.in("flatOptions.Size", parsedSearchParams.sizes)), - }, { predicate: !!parsedSearchParams.minPrice, action: () => filter.and().where("minPrice", ComparisonOperators.GreaterThanOrEqual, parsedSearchParams.minPrice!), diff --git a/apps/web/views/Product/DetailsSection.tsx b/apps/web/views/Product/DetailsSection.tsx index e89dd53..78e1c87 100644 --- a/apps/web/views/Product/DetailsSection.tsx +++ b/apps/web/views/Product/DetailsSection.tsx @@ -18,8 +18,8 @@ export function DetailsSection({ product, slug }: { product: CommerceProduct; sl rootMargin: "0px", }) - const { color, size } = getOptionsFromUrl(slug) - const combination = getCombination(product, color, size) + const { color } = getOptionsFromUrl(slug) + const combination = getCombination(product, color) if (!hasLoaded.current && entry?.isIntersecting) { hasLoaded.current = true diff --git a/apps/web/views/Product/VariantsSection.tsx b/apps/web/views/Product/VariantsSection.tsx index 078e089..5ff6ebe 100644 --- a/apps/web/views/Product/VariantsSection.tsx +++ b/apps/web/views/Product/VariantsSection.tsx @@ -27,7 +27,7 @@ export function VariantsSection({ variants, className, handle, combination }: Va diff --git a/apps/web/views/Search/SearchView.tsx b/apps/web/views/Search/SearchView.tsx index b385c39..9479ed1 100644 --- a/apps/web/views/Search/SearchView.tsx +++ b/apps/web/views/Search/SearchView.tsx @@ -7,14 +7,14 @@ import { meilisearch } from "clients/meilisearch" import { ComparisonOperators, FilterBuilder } from "utils/filterBuilder" import { composeFilters } from "views/Listing/composeFilters" import { FacetsDesktop } from "views/Listing/FacetsDesktop" -import { FacetsMobile } from "views/Listing/FacetsMobile" import { HitsSection } from "views/Listing/HitsSection" import { PaginationSection } from "views/Listing/PaginationSection" import { SearchFacet } from "views/Listing/SearchFacet" -import { Sorter } from "views/Listing/Sorter" import { getDemoProducts, isDemoMode } from "utils/demoUtils" import { env } from "env.mjs" import { CommerceProduct, SearchParamsType } from "types" +import { HIERARCHICAL_SEPARATOR, HITS_PER_PAGE } from "constants/index" +import { Controls } from "views/Listing/Controls" interface SearchViewProps { searchParams: SearchParamsType @@ -32,9 +32,7 @@ export const searchParamsCache = createSearchParamsCache({ sortBy: parseAsString.withDefault(""), categories: parseAsArrayOf(parseAsString).withDefault([]), vendors: parseAsArrayOf(parseAsString).withDefault([]), - tags: parseAsArrayOf(parseAsString).withDefault([]), colors: parseAsArrayOf(parseAsString).withDefault([]), - sizes: parseAsArrayOf(parseAsString).withDefault([]), rating: parseAsInteger, }) @@ -47,33 +45,20 @@ export async function SearchView({ searchParams, disabledFacets, intro, collecti filterBuilder.where("collections.handle", ComparisonOperators.Equal, collection.handle) } - const { facetDistribution, hits, totalPages } = await searchProducts(q, sortBy, page, composeFilters(filterBuilder, rest).build()) + const { facetDistribution, hits, totalPages, totalHits } = await searchProducts(q, sortBy, page, composeFilters(filterBuilder, rest, HIERARCHICAL_SEPARATOR).build()) return (
{intro} -
- -
-
-
-
- -
- - - - {/* has to be wrapped w. suspense, nuqs is using useSearchParams in useQueryState - * https://github.com/47ng/nuqs/issues/496 - */} - - - -
- - - -
+
+ +
+ + + + + +
@@ -92,8 +77,11 @@ const searchProducts = unstable_cache( const results = await index?.search(query, { sort: sortBy ? [sortBy] : undefined, - hitsPerPage: 24, - facets: ["collections.handle", "collections.title", "tags", "vendor", "variants.availableForSale", "flatOptions.Size", "flatOptions.Color", "minPrice", "avgRating"], + limit: HITS_PER_PAGE, + hitsPerPage: HITS_PER_PAGE, + facets: ["collections.handle", "collections.title", "vendor", "variants.availableForSale", "flatOptions.Color", "minPrice", "avgRating"].concat( + !!env.SHOPIFY_HIERARCHICAL_NAV_HANDLE ? [`hierarchicalCategories.lvl0`, `hierarchicalCategories.lvl1`, `hierarchicalCategories.lvl2`] : [] + ), filter, page, attributesToRetrieve: ["id", "handle", "title", "priceRange", "featuredImage", "minPrice", "variants", "images", "avgRating", "totalReviews"], diff --git a/packages/core/platform/shopify/fragments/menu.ts b/packages/core/platform/shopify/fragments/menu.ts new file mode 100644 index 0000000..221a605 --- /dev/null +++ b/packages/core/platform/shopify/fragments/menu.ts @@ -0,0 +1,28 @@ +const menuItemFragment = `#graphql +fragment NavigationItemFields on MenuItem { + title + id + resource { + __typename + ... on Collection { + handle + } + } +} +` + +const createMenuItemFragment = (depth: number): string => { + if (depth <= 0) { + return `#graphql + ...NavigationItemFields + ` + } + return `#graphql + ...NavigationItemFields + items { + ${createMenuItemFragment(depth - 1)} + } + ` +} + +export { menuItemFragment, createMenuItemFragment } diff --git a/packages/core/platform/shopify/fragments/product.ts b/packages/core/platform/shopify/fragments/product.ts index 42a6910..8b7eec6 100644 --- a/packages/core/platform/shopify/fragments/product.ts +++ b/packages/core/platform/shopify/fragments/product.ts @@ -24,17 +24,10 @@ const productFragment = `#graphql currencyCode } } - collections(first: 15) { + collections(first: 250) { nodes { handle - title - description - updatedAt id - descriptionHtml - image { - ...singleImage - } } } variants(first: 250) { diff --git a/packages/core/platform/shopify/index.ts b/packages/core/platform/shopify/index.ts index 4775d9f..2658fee 100644 --- a/packages/core/platform/shopify/index.ts +++ b/packages/core/platform/shopify/index.ts @@ -9,7 +9,7 @@ import { normalizeCart, normalizeCollection, normalizeProduct } from "./normaliz import { getCartQuery } from "./queries/cart.storefront" import { getCollectionByIdQuery, getCollectionQuery, getCollectionsQuery } from "./queries/collection.storefront" import { getCustomerQuery } from "./queries/customer.storefront" -import { getMenuQuery } from "./queries/menu.storefront" +import { getMenuQuery, type MenuQuery } from "./queries/menu.storefront" import { getPageQuery, getPagesQuery } from "./queries/page.storefront" import { getLatestProductFeedQuery } from "./queries/product-feed.admin" import { getAdminProductQuery, getProductStatusQuery } from "./queries/product.admin" @@ -31,7 +31,6 @@ import type { CreateCartMutation, CreateCustomerMutation, DeleteCartItemsMutation, - MenuQuery, PagesQuery, ProductsByHandleQuery, SingleCartQuery, @@ -80,7 +79,7 @@ export function createShopifyClient({ storefrontAccessToken, adminAccessToken, s // To prevent prettier from wrapping pretty one liners and making them unreadable // prettier-ignore return { - getMenu: async (handle?: string) => getMenu(client!, handle), + getMenu: async (handle?: string, depth?: number) => getMenu(client!, handle, depth), getProduct: async (id: string) => getProduct(client!, id), getProductByHandle: async (handle: string) => getProductByHandle(client!, handle), subscribeWebhook: async (topic: `${WebhookSubscriptionTopic}`, callbackUrl: string) => subscribeWebhook(adminClient, topic, callbackUrl), @@ -102,16 +101,23 @@ export function createShopifyClient({ storefrontAccessToken, adminAccessToken, s createUser: async (input: PlatformUserCreateInput) => createUser(client!, input), getUser: async (accessToken: string) => getUser(client!, accessToken), updateUser: async (accessToken: string, input: Omit) => updateUser(client!, accessToken, input), - createUserAccessToken: async (input: Pick) => createUserAccessToken(client!, input) + createUserAccessToken: async (input: Pick) => createUserAccessToken(client!, input), + getHierarchicalCollections: async (handle: string, depth?: number) => getHierarchicalCollections(client!, handle, depth), } } -async function getMenu(client: StorefrontApiClient, handle: string = "main-menu"): Promise { - const response = await client.request(getMenuQuery, { variables: { handle } }) - const mappedItems = response.data?.menu?.items?.map((item) => ({ - title: item.title, - url: item.url, - })) +async function getMenu(client: StorefrontApiClient, handle: string = "main-menu", depth = 3): Promise { + const query = getMenuQuery(depth) + const response = await client.request(query, { variables: { handle } }) + const mappedItems = response.data?.menu?.items + + return { items: mappedItems || [] } +} + +async function getHierarchicalCollections(client: StorefrontApiClient, handle: string, depth = 3): Promise { + const query = getMenuQuery(depth) + const response = await client.request(query, { variables: { handle } }) + const mappedItems = response.data?.menu.items.filter((item) => item.resource?.__typename === "Collection") return { items: mappedItems || [], diff --git a/packages/core/platform/shopify/queries/menu.storefront.ts b/packages/core/platform/shopify/queries/menu.storefront.ts index 79d8333..6899db5 100644 --- a/packages/core/platform/shopify/queries/menu.storefront.ts +++ b/packages/core/platform/shopify/queries/menu.storefront.ts @@ -1,10 +1,20 @@ -export const getMenuQuery = `#graphql +import { PlatformMenu } from "../../types" +import { createMenuItemFragment, menuItemFragment } from "../fragments/menu" +/* Not using auto generated types here, as I'm either too bad using codegen-cli or it just does not work for such code */ + +export type MenuQuery = { + menu: { + items: PlatformMenu["items"] + } +} + +export const getMenuQuery = (depth: number) => `#graphql query Menu($handle: String!) { menu(handle: $handle) { items { - title - url + ${createMenuItemFragment(depth)} } } } +${menuItemFragment} ` diff --git a/packages/core/platform/shopify/types/storefront.generated.d.ts b/packages/core/platform/shopify/types/storefront.generated.d.ts index 56f31cd..8f473dc 100644 --- a/packages/core/platform/shopify/types/storefront.generated.d.ts +++ b/packages/core/platform/shopify/types/storefront.generated.d.ts @@ -11,10 +11,7 @@ export type SingleCartFragment = ( Pick & { price: Pick, selectedOptions: Array>, product: ( Pick - & { options: Array>, priceRange: { maxVariantPrice: Pick, minVariantPrice: Pick }, collections: { nodes: Array<( - Pick - & { image?: StorefrontTypes.Maybe> } - )> }, variants: { edges: Array<{ node: ( + & { options: Array>, priceRange: { maxVariantPrice: Pick, minVariantPrice: Pick }, collections: { nodes: Array> }, variants: { edges: Array<{ node: ( Pick & { selectedOptions: Array>, price: Pick } ) }> }, featuredImage?: StorefrontTypes.Maybe>, images: { edges: Array<{ node: Pick }> }, seo: Pick } @@ -26,10 +23,7 @@ export type SingleCartFragment = ( Pick & { price: Pick, selectedOptions: Array>, product: ( Pick - & { options: Array>, priceRange: { maxVariantPrice: Pick, minVariantPrice: Pick }, collections: { nodes: Array<( - Pick - & { image?: StorefrontTypes.Maybe> } - )> }, variants: { edges: Array<{ node: ( + & { options: Array>, priceRange: { maxVariantPrice: Pick, minVariantPrice: Pick }, collections: { nodes: Array> }, variants: { edges: Array<{ node: ( Pick & { selectedOptions: Array>, price: Pick } ) }> }, featuredImage?: StorefrontTypes.Maybe>, images: { edges: Array<{ node: Pick }> }, seo: Pick } @@ -54,10 +48,7 @@ export type SinglePageFragment = ( export type SingleProductFragment = ( Pick - & { options: Array>, priceRange: { maxVariantPrice: Pick, minVariantPrice: Pick }, collections: { nodes: Array<( - Pick - & { image?: StorefrontTypes.Maybe> } - )> }, variants: { edges: Array<{ node: ( + & { options: Array>, priceRange: { maxVariantPrice: Pick, minVariantPrice: Pick }, collections: { nodes: Array> }, variants: { edges: Array<{ node: ( Pick & { selectedOptions: Array>, price: Pick } ) }> }, featuredImage?: StorefrontTypes.Maybe>, images: { edges: Array<{ node: Pick }> }, seo: Pick } @@ -79,10 +70,7 @@ export type CreateCartItemMutation = { cartLinesAdd?: StorefrontTypes.Maybe<{ ca Pick & { price: Pick, selectedOptions: Array>, product: ( Pick - & { options: Array>, priceRange: { maxVariantPrice: Pick, minVariantPrice: Pick }, collections: { nodes: Array<( - Pick - & { image?: StorefrontTypes.Maybe> } - )> }, variants: { edges: Array<{ node: ( + & { options: Array>, priceRange: { maxVariantPrice: Pick, minVariantPrice: Pick }, collections: { nodes: Array> }, variants: { edges: Array<{ node: ( Pick & { selectedOptions: Array>, price: Pick } ) }> }, featuredImage?: StorefrontTypes.Maybe>, images: { edges: Array<{ node: Pick }> }, seo: Pick } @@ -94,10 +82,7 @@ export type CreateCartItemMutation = { cartLinesAdd?: StorefrontTypes.Maybe<{ ca Pick & { price: Pick, selectedOptions: Array>, product: ( Pick - & { options: Array>, priceRange: { maxVariantPrice: Pick, minVariantPrice: Pick }, collections: { nodes: Array<( - Pick - & { image?: StorefrontTypes.Maybe> } - )> }, variants: { edges: Array<{ node: ( + & { options: Array>, priceRange: { maxVariantPrice: Pick, minVariantPrice: Pick }, collections: { nodes: Array> }, variants: { edges: Array<{ node: ( Pick & { selectedOptions: Array>, price: Pick } ) }> }, featuredImage?: StorefrontTypes.Maybe>, images: { edges: Array<{ node: Pick }> }, seo: Pick } @@ -119,10 +104,7 @@ export type CreateCartMutation = { cartCreate?: StorefrontTypes.Maybe<{ cart?: S Pick & { price: Pick, selectedOptions: Array>, product: ( Pick - & { options: Array>, priceRange: { maxVariantPrice: Pick, minVariantPrice: Pick }, collections: { nodes: Array<( - Pick - & { image?: StorefrontTypes.Maybe> } - )> }, variants: { edges: Array<{ node: ( + & { options: Array>, priceRange: { maxVariantPrice: Pick, minVariantPrice: Pick }, collections: { nodes: Array> }, variants: { edges: Array<{ node: ( Pick & { selectedOptions: Array>, price: Pick } ) }> }, featuredImage?: StorefrontTypes.Maybe>, images: { edges: Array<{ node: Pick }> }, seo: Pick } @@ -134,10 +116,7 @@ export type CreateCartMutation = { cartCreate?: StorefrontTypes.Maybe<{ cart?: S Pick & { price: Pick, selectedOptions: Array>, product: ( Pick - & { options: Array>, priceRange: { maxVariantPrice: Pick, minVariantPrice: Pick }, collections: { nodes: Array<( - Pick - & { image?: StorefrontTypes.Maybe> } - )> }, variants: { edges: Array<{ node: ( + & { options: Array>, priceRange: { maxVariantPrice: Pick, minVariantPrice: Pick }, collections: { nodes: Array> }, variants: { edges: Array<{ node: ( Pick & { selectedOptions: Array>, price: Pick } ) }> }, featuredImage?: StorefrontTypes.Maybe>, images: { edges: Array<{ node: Pick }> }, seo: Pick } @@ -160,10 +139,7 @@ export type UpdateCartItemsMutation = { cartLinesUpdate?: StorefrontTypes.Maybe< Pick & { price: Pick, selectedOptions: Array>, product: ( Pick - & { options: Array>, priceRange: { maxVariantPrice: Pick, minVariantPrice: Pick }, collections: { nodes: Array<( - Pick - & { image?: StorefrontTypes.Maybe> } - )> }, variants: { edges: Array<{ node: ( + & { options: Array>, priceRange: { maxVariantPrice: Pick, minVariantPrice: Pick }, collections: { nodes: Array> }, variants: { edges: Array<{ node: ( Pick & { selectedOptions: Array>, price: Pick } ) }> }, featuredImage?: StorefrontTypes.Maybe>, images: { edges: Array<{ node: Pick }> }, seo: Pick } @@ -175,10 +151,7 @@ export type UpdateCartItemsMutation = { cartLinesUpdate?: StorefrontTypes.Maybe< Pick & { price: Pick, selectedOptions: Array>, product: ( Pick - & { options: Array>, priceRange: { maxVariantPrice: Pick, minVariantPrice: Pick }, collections: { nodes: Array<( - Pick - & { image?: StorefrontTypes.Maybe> } - )> }, variants: { edges: Array<{ node: ( + & { options: Array>, priceRange: { maxVariantPrice: Pick, minVariantPrice: Pick }, collections: { nodes: Array> }, variants: { edges: Array<{ node: ( Pick & { selectedOptions: Array>, price: Pick } ) }> }, featuredImage?: StorefrontTypes.Maybe>, images: { edges: Array<{ node: Pick }> }, seo: Pick } @@ -201,10 +174,7 @@ export type DeleteCartItemsMutation = { cartLinesRemove?: StorefrontTypes.Maybe< Pick & { price: Pick, selectedOptions: Array>, product: ( Pick - & { options: Array>, priceRange: { maxVariantPrice: Pick, minVariantPrice: Pick }, collections: { nodes: Array<( - Pick - & { image?: StorefrontTypes.Maybe> } - )> }, variants: { edges: Array<{ node: ( + & { options: Array>, priceRange: { maxVariantPrice: Pick, minVariantPrice: Pick }, collections: { nodes: Array> }, variants: { edges: Array<{ node: ( Pick & { selectedOptions: Array>, price: Pick } ) }> }, featuredImage?: StorefrontTypes.Maybe>, images: { edges: Array<{ node: Pick }> }, seo: Pick } @@ -216,10 +186,7 @@ export type DeleteCartItemsMutation = { cartLinesRemove?: StorefrontTypes.Maybe< Pick & { price: Pick, selectedOptions: Array>, product: ( Pick - & { options: Array>, priceRange: { maxVariantPrice: Pick, minVariantPrice: Pick }, collections: { nodes: Array<( - Pick - & { image?: StorefrontTypes.Maybe> } - )> }, variants: { edges: Array<{ node: ( + & { options: Array>, priceRange: { maxVariantPrice: Pick, minVariantPrice: Pick }, collections: { nodes: Array> }, variants: { edges: Array<{ node: ( Pick & { selectedOptions: Array>, price: Pick } ) }> }, featuredImage?: StorefrontTypes.Maybe>, images: { edges: Array<{ node: Pick }> }, seo: Pick } @@ -271,10 +238,7 @@ export type SingleCartQuery = { cart?: StorefrontTypes.Maybe<( Pick & { price: Pick, selectedOptions: Array>, product: ( Pick - & { options: Array>, priceRange: { maxVariantPrice: Pick, minVariantPrice: Pick }, collections: { nodes: Array<( - Pick - & { image?: StorefrontTypes.Maybe> } - )> }, variants: { edges: Array<{ node: ( + & { options: Array>, priceRange: { maxVariantPrice: Pick, minVariantPrice: Pick }, collections: { nodes: Array> }, variants: { edges: Array<{ node: ( Pick & { selectedOptions: Array>, price: Pick } ) }> }, featuredImage?: StorefrontTypes.Maybe>, images: { edges: Array<{ node: Pick }> }, seo: Pick } @@ -286,10 +250,7 @@ export type SingleCartQuery = { cart?: StorefrontTypes.Maybe<( Pick & { price: Pick, selectedOptions: Array>, product: ( Pick - & { options: Array>, priceRange: { maxVariantPrice: Pick, minVariantPrice: Pick }, collections: { nodes: Array<( - Pick - & { image?: StorefrontTypes.Maybe> } - )> }, variants: { edges: Array<{ node: ( + & { options: Array>, priceRange: { maxVariantPrice: Pick, minVariantPrice: Pick }, collections: { nodes: Array> }, variants: { edges: Array<{ node: ( Pick & { selectedOptions: Array>, price: Pick } ) }> }, featuredImage?: StorefrontTypes.Maybe>, images: { edges: Array<{ node: Pick }> }, seo: Pick } @@ -335,13 +296,6 @@ export type SingleCustomerQueryVariables = StorefrontTypes.Exact<{ export type SingleCustomerQuery = { customer?: StorefrontTypes.Maybe> }; -export type MenuQueryVariables = StorefrontTypes.Exact<{ - handle: StorefrontTypes.Scalars['String']['input']; -}>; - - -export type MenuQuery = { menu?: StorefrontTypes.Maybe<{ items: Array> }> }; - export type SinglePageQueryVariables = StorefrontTypes.Exact<{ handle: StorefrontTypes.Scalars['String']['input']; }>; @@ -367,10 +321,7 @@ export type SingleProductQueryVariables = StorefrontTypes.Exact<{ export type SingleProductQuery = { product?: StorefrontTypes.Maybe<( Pick - & { options: Array>, priceRange: { maxVariantPrice: Pick, minVariantPrice: Pick }, collections: { nodes: Array<( - Pick - & { image?: StorefrontTypes.Maybe> } - )> }, variants: { edges: Array<{ node: ( + & { options: Array>, priceRange: { maxVariantPrice: Pick, minVariantPrice: Pick }, collections: { nodes: Array> }, variants: { edges: Array<{ node: ( Pick & { selectedOptions: Array>, price: Pick } ) }> }, featuredImage?: StorefrontTypes.Maybe>, images: { edges: Array<{ node: Pick }> }, seo: Pick } @@ -383,10 +334,7 @@ export type ProductsByHandleQueryVariables = StorefrontTypes.Exact<{ export type ProductsByHandleQuery = { products: { edges: Array<{ node: ( Pick - & { options: Array>, priceRange: { maxVariantPrice: Pick, minVariantPrice: Pick }, collections: { nodes: Array<( - Pick - & { image?: StorefrontTypes.Maybe> } - )> }, variants: { edges: Array<{ node: ( + & { options: Array>, priceRange: { maxVariantPrice: Pick, minVariantPrice: Pick }, collections: { nodes: Array> }, variants: { edges: Array<{ node: ( Pick & { selectedOptions: Array>, price: Pick } ) }> }, featuredImage?: StorefrontTypes.Maybe>, images: { edges: Array<{ node: Pick }> }, seo: Pick } @@ -403,10 +351,7 @@ export type ProductsQueryVariables = StorefrontTypes.Exact<{ export type ProductsQuery = { products: { edges: Array<{ node: ( Pick - & { options: Array>, priceRange: { maxVariantPrice: Pick, minVariantPrice: Pick }, collections: { nodes: Array<( - Pick - & { image?: StorefrontTypes.Maybe> } - )> }, variants: { edges: Array<{ node: ( + & { options: Array>, priceRange: { maxVariantPrice: Pick, minVariantPrice: Pick }, collections: { nodes: Array> }, variants: { edges: Array<{ node: ( Pick & { selectedOptions: Array>, price: Pick } ) }> }, featuredImage?: StorefrontTypes.Maybe>, images: { edges: Array<{ node: Pick }> }, seo: Pick } @@ -419,35 +364,31 @@ export type ProductRecommendationsQueryVariables = StorefrontTypes.Exact<{ export type ProductRecommendationsQuery = { productRecommendations?: StorefrontTypes.Maybe - & { options: Array>, priceRange: { maxVariantPrice: Pick, minVariantPrice: Pick }, collections: { nodes: Array<( - Pick - & { image?: StorefrontTypes.Maybe> } - )> }, variants: { edges: Array<{ node: ( + & { options: Array>, priceRange: { maxVariantPrice: Pick, minVariantPrice: Pick }, collections: { nodes: Array> }, variants: { edges: Array<{ node: ( Pick & { selectedOptions: Array>, price: Pick } ) }> }, featuredImage?: StorefrontTypes.Maybe>, images: { edges: Array<{ node: Pick }> }, seo: Pick } )>> }; interface GeneratedQueryTypes { - "#graphql\n query SingleCart($cartId: ID!) {\n cart(id: $cartId) {\n ...singleCart\n }\n }\n #graphql \n fragment singleCart on Cart {\n id\n checkoutUrl\n cost {\n subtotalAmount {\n amount\n currencyCode\n }\n totalAmount {\n amount\n currencyCode\n }\n totalTaxAmount {\n amount\n currencyCode\n }\n }\n lines(first: 100) {\n edges {\n node {\n id\n quantity\n cost {\n totalAmount {\n amount\n currencyCode\n }\n }\n merchandise {\n ... on ProductVariant {\n id\n title\n price {\n amount\n currencyCode\n }\n quantityAvailable\n selectedOptions {\n name\n value\n }\n product {\n ...singleProduct\n }\n }\n }\n }\n }\n }\n totalQuantity\n }\n #graphql\n fragment singleProduct on Product {\n id\n handle\n title\n description\n descriptionHtml\n vendor\n options {\n id\n name\n values\n }\n priceRange {\n maxVariantPrice {\n amount\n currencyCode\n }\n minVariantPrice {\n amount\n currencyCode\n }\n }\n collections(first: 15) {\n nodes {\n handle\n title\n description\n updatedAt\n id\n descriptionHtml\n image {\n ...singleImage\n }\n }\n }\n variants(first: 250) {\n edges {\n node {\n id\n title\n quantityAvailable\n availableForSale\n selectedOptions {\n name\n value\n }\n price {\n amount\n currencyCode\n }\n }\n }\n }\n featuredImage {\n ...singleImage\n }\n images(first: 20) {\n edges {\n node {\n ...singleImage\n }\n }\n }\n seo {\n ...seo\n }\n tags\n updatedAt\n createdAt\n }\n #graphql\n fragment singleImage on Image {\n url\n altText\n width\n height\n }\n\n #graphql\n fragment seo on SEO {\n description\n title\n }\n\n\n\n": {return: SingleCartQuery, variables: SingleCartQueryVariables}, + "#graphql\n query SingleCart($cartId: ID!) {\n cart(id: $cartId) {\n ...singleCart\n }\n }\n #graphql \n fragment singleCart on Cart {\n id\n checkoutUrl\n cost {\n subtotalAmount {\n amount\n currencyCode\n }\n totalAmount {\n amount\n currencyCode\n }\n totalTaxAmount {\n amount\n currencyCode\n }\n }\n lines(first: 100) {\n edges {\n node {\n id\n quantity\n cost {\n totalAmount {\n amount\n currencyCode\n }\n }\n merchandise {\n ... on ProductVariant {\n id\n title\n price {\n amount\n currencyCode\n }\n quantityAvailable\n selectedOptions {\n name\n value\n }\n product {\n ...singleProduct\n }\n }\n }\n }\n }\n }\n totalQuantity\n }\n #graphql\n fragment singleProduct on Product {\n id\n handle\n title\n description\n descriptionHtml\n vendor\n options {\n id\n name\n values\n }\n priceRange {\n maxVariantPrice {\n amount\n currencyCode\n }\n minVariantPrice {\n amount\n currencyCode\n }\n }\n collections(first: 250) {\n nodes {\n handle\n id\n }\n }\n variants(first: 250) {\n edges {\n node {\n id\n title\n quantityAvailable\n availableForSale\n selectedOptions {\n name\n value\n }\n price {\n amount\n currencyCode\n }\n }\n }\n }\n featuredImage {\n ...singleImage\n }\n images(first: 20) {\n edges {\n node {\n ...singleImage\n }\n }\n }\n seo {\n ...seo\n }\n tags\n updatedAt\n createdAt\n }\n #graphql\n fragment singleImage on Image {\n url\n altText\n width\n height\n }\n\n #graphql\n fragment seo on SEO {\n description\n title\n }\n\n\n\n": {return: SingleCartQuery, variables: SingleCartQueryVariables}, "#graphql\n query SingleCollectionById($id: ID!) {\n collection(id: $id) {\n ...singleCollection\n }\n }\n #graphql\n fragment singleCollection on Collection {\n handle\n image {\n ...singleImage\n }\n title\n descriptionHtml\n id\n description\n seo {\n ...seo\n }\n updatedAt\n }\n #graphql\n fragment seo on SEO {\n description\n title\n }\n\n #graphql\n fragment singleImage on Image {\n url\n altText\n width\n height\n }\n\n\n": {return: SingleCollectionByIdQuery, variables: SingleCollectionByIdQueryVariables}, "#graphql\n query SingleCollection($handle: String!) {\n collection(handle: $handle) {\n ...singleCollection\n }\n }\n #graphql\n fragment singleCollection on Collection {\n handle\n image {\n ...singleImage\n }\n title\n descriptionHtml\n id\n description\n seo {\n ...seo\n }\n updatedAt\n }\n #graphql\n fragment seo on SEO {\n description\n title\n }\n\n #graphql\n fragment singleImage on Image {\n url\n altText\n width\n height\n }\n\n\n": {return: SingleCollectionQuery, variables: SingleCollectionQueryVariables}, "#graphql\n query Collections($limit: Int = 250) {\n collections(first: $limit, sortKey: TITLE) {\n edges {\n node {\n ...singleCollection\n }\n }\n }\n }\n #graphql\n fragment singleCollection on Collection {\n handle\n image {\n ...singleImage\n }\n title\n descriptionHtml\n id\n description\n seo {\n ...seo\n }\n updatedAt\n }\n #graphql\n fragment seo on SEO {\n description\n title\n }\n\n #graphql\n fragment singleImage on Image {\n url\n altText\n width\n height\n }\n\n\n": {return: CollectionsQuery, variables: CollectionsQueryVariables}, "#graphql\n query SingleCustomer($customerAccessToken: String!) {\n customer(customerAccessToken: $customerAccessToken) {\n ...singleCustomer\n }\n }\n #graphql \n fragment singleCustomer on Customer {\n acceptsMarketing\n createdAt\n updatedAt\n displayName\n email\n firstName\n lastName\n id\n phone\n tags\n }\n\n": {return: SingleCustomerQuery, variables: SingleCustomerQueryVariables}, - "#graphql\n query Menu($handle: String!) {\n menu(handle: $handle) {\n items {\n title\n url\n }\n }\n }\n": {return: MenuQuery, variables: MenuQueryVariables}, "#graphql\n query SinglePage($handle: String!) {\n page(handle: $handle) {\n ...singlePage\n }\n }\n #graphql\n fragment singlePage on Page {\n ... on Page {\n id\n title\n handle\n body\n bodySummary\n seo {\n ...seo\n }\n createdAt\n updatedAt\n }\n }\n #graphql\n fragment seo on SEO {\n description\n title\n }\n\n\n": {return: SinglePageQuery, variables: SinglePageQueryVariables}, "#graphql\n query Pages {\n pages(first: 100) {\n edges {\n node {\n ...singlePage\n }\n }\n }\n }\n #graphql\n fragment singlePage on Page {\n ... on Page {\n id\n title\n handle\n body\n bodySummary\n seo {\n ...seo\n }\n createdAt\n updatedAt\n }\n }\n #graphql\n fragment seo on SEO {\n description\n title\n }\n\n\n": {return: PagesQuery, variables: PagesQueryVariables}, - "#graphql\n query SingleProduct($id: ID!) {\n product(id: $id) {\n ...singleProduct\n }\n }\n #graphql\n fragment singleProduct on Product {\n id\n handle\n title\n description\n descriptionHtml\n vendor\n options {\n id\n name\n values\n }\n priceRange {\n maxVariantPrice {\n amount\n currencyCode\n }\n minVariantPrice {\n amount\n currencyCode\n }\n }\n collections(first: 15) {\n nodes {\n handle\n title\n description\n updatedAt\n id\n descriptionHtml\n image {\n ...singleImage\n }\n }\n }\n variants(first: 250) {\n edges {\n node {\n id\n title\n quantityAvailable\n availableForSale\n selectedOptions {\n name\n value\n }\n price {\n amount\n currencyCode\n }\n }\n }\n }\n featuredImage {\n ...singleImage\n }\n images(first: 20) {\n edges {\n node {\n ...singleImage\n }\n }\n }\n seo {\n ...seo\n }\n tags\n updatedAt\n createdAt\n }\n #graphql\n fragment singleImage on Image {\n url\n altText\n width\n height\n }\n\n #graphql\n fragment seo on SEO {\n description\n title\n }\n\n\n": {return: SingleProductQuery, variables: SingleProductQueryVariables}, - "#graphql\n query ProductsByHandle($query: String!) {\n products(first: 1, query: $query) {\n edges {\n node {\n ...singleProduct\n }\n }\n }\n }\n #graphql\n fragment singleProduct on Product {\n id\n handle\n title\n description\n descriptionHtml\n vendor\n options {\n id\n name\n values\n }\n priceRange {\n maxVariantPrice {\n amount\n currencyCode\n }\n minVariantPrice {\n amount\n currencyCode\n }\n }\n collections(first: 15) {\n nodes {\n handle\n title\n description\n updatedAt\n id\n descriptionHtml\n image {\n ...singleImage\n }\n }\n }\n variants(first: 250) {\n edges {\n node {\n id\n title\n quantityAvailable\n availableForSale\n selectedOptions {\n name\n value\n }\n price {\n amount\n currencyCode\n }\n }\n }\n }\n featuredImage {\n ...singleImage\n }\n images(first: 20) {\n edges {\n node {\n ...singleImage\n }\n }\n }\n seo {\n ...seo\n }\n tags\n updatedAt\n createdAt\n }\n #graphql\n fragment singleImage on Image {\n url\n altText\n width\n height\n }\n\n #graphql\n fragment seo on SEO {\n description\n title\n }\n\n\n": {return: ProductsByHandleQuery, variables: ProductsByHandleQueryVariables}, - "#graphql\n query Products($sortKey: ProductSortKeys, $reverse: Boolean, $query: String, $numProducts: Int!, $cursor: String) {\n products(sortKey: $sortKey, reverse: $reverse, query: $query, first: $numProducts, after: $cursor ) {\n edges {\n node {\n ...singleProduct\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n #graphql\n fragment singleProduct on Product {\n id\n handle\n title\n description\n descriptionHtml\n vendor\n options {\n id\n name\n values\n }\n priceRange {\n maxVariantPrice {\n amount\n currencyCode\n }\n minVariantPrice {\n amount\n currencyCode\n }\n }\n collections(first: 15) {\n nodes {\n handle\n title\n description\n updatedAt\n id\n descriptionHtml\n image {\n ...singleImage\n }\n }\n }\n variants(first: 250) {\n edges {\n node {\n id\n title\n quantityAvailable\n availableForSale\n selectedOptions {\n name\n value\n }\n price {\n amount\n currencyCode\n }\n }\n }\n }\n featuredImage {\n ...singleImage\n }\n images(first: 20) {\n edges {\n node {\n ...singleImage\n }\n }\n }\n seo {\n ...seo\n }\n tags\n updatedAt\n createdAt\n }\n #graphql\n fragment singleImage on Image {\n url\n altText\n width\n height\n }\n\n #graphql\n fragment seo on SEO {\n description\n title\n }\n\n\n": {return: ProductsQuery, variables: ProductsQueryVariables}, - "#graphql\n query ProductRecommendations($productId: ID!) {\n productRecommendations(productId: $productId) {\n ...singleProduct\n }\n }\n #graphql\n fragment singleProduct on Product {\n id\n handle\n title\n description\n descriptionHtml\n vendor\n options {\n id\n name\n values\n }\n priceRange {\n maxVariantPrice {\n amount\n currencyCode\n }\n minVariantPrice {\n amount\n currencyCode\n }\n }\n collections(first: 15) {\n nodes {\n handle\n title\n description\n updatedAt\n id\n descriptionHtml\n image {\n ...singleImage\n }\n }\n }\n variants(first: 250) {\n edges {\n node {\n id\n title\n quantityAvailable\n availableForSale\n selectedOptions {\n name\n value\n }\n price {\n amount\n currencyCode\n }\n }\n }\n }\n featuredImage {\n ...singleImage\n }\n images(first: 20) {\n edges {\n node {\n ...singleImage\n }\n }\n }\n seo {\n ...seo\n }\n tags\n updatedAt\n createdAt\n }\n #graphql\n fragment singleImage on Image {\n url\n altText\n width\n height\n }\n\n #graphql\n fragment seo on SEO {\n description\n title\n }\n\n\n": {return: ProductRecommendationsQuery, variables: ProductRecommendationsQueryVariables}, + "#graphql\n query SingleProduct($id: ID!) {\n product(id: $id) {\n ...singleProduct\n }\n }\n #graphql\n fragment singleProduct on Product {\n id\n handle\n title\n description\n descriptionHtml\n vendor\n options {\n id\n name\n values\n }\n priceRange {\n maxVariantPrice {\n amount\n currencyCode\n }\n minVariantPrice {\n amount\n currencyCode\n }\n }\n collections(first: 250) {\n nodes {\n handle\n id\n }\n }\n variants(first: 250) {\n edges {\n node {\n id\n title\n quantityAvailable\n availableForSale\n selectedOptions {\n name\n value\n }\n price {\n amount\n currencyCode\n }\n }\n }\n }\n featuredImage {\n ...singleImage\n }\n images(first: 20) {\n edges {\n node {\n ...singleImage\n }\n }\n }\n seo {\n ...seo\n }\n tags\n updatedAt\n createdAt\n }\n #graphql\n fragment singleImage on Image {\n url\n altText\n width\n height\n }\n\n #graphql\n fragment seo on SEO {\n description\n title\n }\n\n\n": {return: SingleProductQuery, variables: SingleProductQueryVariables}, + "#graphql\n query ProductsByHandle($query: String!) {\n products(first: 1, query: $query) {\n edges {\n node {\n ...singleProduct\n }\n }\n }\n }\n #graphql\n fragment singleProduct on Product {\n id\n handle\n title\n description\n descriptionHtml\n vendor\n options {\n id\n name\n values\n }\n priceRange {\n maxVariantPrice {\n amount\n currencyCode\n }\n minVariantPrice {\n amount\n currencyCode\n }\n }\n collections(first: 250) {\n nodes {\n handle\n id\n }\n }\n variants(first: 250) {\n edges {\n node {\n id\n title\n quantityAvailable\n availableForSale\n selectedOptions {\n name\n value\n }\n price {\n amount\n currencyCode\n }\n }\n }\n }\n featuredImage {\n ...singleImage\n }\n images(first: 20) {\n edges {\n node {\n ...singleImage\n }\n }\n }\n seo {\n ...seo\n }\n tags\n updatedAt\n createdAt\n }\n #graphql\n fragment singleImage on Image {\n url\n altText\n width\n height\n }\n\n #graphql\n fragment seo on SEO {\n description\n title\n }\n\n\n": {return: ProductsByHandleQuery, variables: ProductsByHandleQueryVariables}, + "#graphql\n query Products($sortKey: ProductSortKeys, $reverse: Boolean, $query: String, $numProducts: Int!, $cursor: String) {\n products(sortKey: $sortKey, reverse: $reverse, query: $query, first: $numProducts, after: $cursor ) {\n edges {\n node {\n ...singleProduct\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n #graphql\n fragment singleProduct on Product {\n id\n handle\n title\n description\n descriptionHtml\n vendor\n options {\n id\n name\n values\n }\n priceRange {\n maxVariantPrice {\n amount\n currencyCode\n }\n minVariantPrice {\n amount\n currencyCode\n }\n }\n collections(first: 250) {\n nodes {\n handle\n id\n }\n }\n variants(first: 250) {\n edges {\n node {\n id\n title\n quantityAvailable\n availableForSale\n selectedOptions {\n name\n value\n }\n price {\n amount\n currencyCode\n }\n }\n }\n }\n featuredImage {\n ...singleImage\n }\n images(first: 20) {\n edges {\n node {\n ...singleImage\n }\n }\n }\n seo {\n ...seo\n }\n tags\n updatedAt\n createdAt\n }\n #graphql\n fragment singleImage on Image {\n url\n altText\n width\n height\n }\n\n #graphql\n fragment seo on SEO {\n description\n title\n }\n\n\n": {return: ProductsQuery, variables: ProductsQueryVariables}, + "#graphql\n query ProductRecommendations($productId: ID!) {\n productRecommendations(productId: $productId) {\n ...singleProduct\n }\n }\n #graphql\n fragment singleProduct on Product {\n id\n handle\n title\n description\n descriptionHtml\n vendor\n options {\n id\n name\n values\n }\n priceRange {\n maxVariantPrice {\n amount\n currencyCode\n }\n minVariantPrice {\n amount\n currencyCode\n }\n }\n collections(first: 250) {\n nodes {\n handle\n id\n }\n }\n variants(first: 250) {\n edges {\n node {\n id\n title\n quantityAvailable\n availableForSale\n selectedOptions {\n name\n value\n }\n price {\n amount\n currencyCode\n }\n }\n }\n }\n featuredImage {\n ...singleImage\n }\n images(first: 20) {\n edges {\n node {\n ...singleImage\n }\n }\n }\n seo {\n ...seo\n }\n tags\n updatedAt\n createdAt\n }\n #graphql\n fragment singleImage on Image {\n url\n altText\n width\n height\n }\n\n #graphql\n fragment seo on SEO {\n description\n title\n }\n\n\n": {return: ProductRecommendationsQuery, variables: ProductRecommendationsQueryVariables}, } interface GeneratedMutationTypes { - "#graphql\n mutation CreateCartItem($cartId: ID!, $items: [CartLineInput!]!) {\n cartLinesAdd(cartId: $cartId, lines: $items) {\n cart {\n ...singleCart\n }\n }\n }\n #graphql \n fragment singleCart on Cart {\n id\n checkoutUrl\n cost {\n subtotalAmount {\n amount\n currencyCode\n }\n totalAmount {\n amount\n currencyCode\n }\n totalTaxAmount {\n amount\n currencyCode\n }\n }\n lines(first: 100) {\n edges {\n node {\n id\n quantity\n cost {\n totalAmount {\n amount\n currencyCode\n }\n }\n merchandise {\n ... on ProductVariant {\n id\n title\n price {\n amount\n currencyCode\n }\n quantityAvailable\n selectedOptions {\n name\n value\n }\n product {\n ...singleProduct\n }\n }\n }\n }\n }\n }\n totalQuantity\n }\n #graphql\n fragment singleProduct on Product {\n id\n handle\n title\n description\n descriptionHtml\n vendor\n options {\n id\n name\n values\n }\n priceRange {\n maxVariantPrice {\n amount\n currencyCode\n }\n minVariantPrice {\n amount\n currencyCode\n }\n }\n collections(first: 15) {\n nodes {\n handle\n title\n description\n updatedAt\n id\n descriptionHtml\n image {\n ...singleImage\n }\n }\n }\n variants(first: 250) {\n edges {\n node {\n id\n title\n quantityAvailable\n availableForSale\n selectedOptions {\n name\n value\n }\n price {\n amount\n currencyCode\n }\n }\n }\n }\n featuredImage {\n ...singleImage\n }\n images(first: 20) {\n edges {\n node {\n ...singleImage\n }\n }\n }\n seo {\n ...seo\n }\n tags\n updatedAt\n createdAt\n }\n #graphql\n fragment singleImage on Image {\n url\n altText\n width\n height\n }\n\n #graphql\n fragment seo on SEO {\n description\n title\n }\n\n\n\n": {return: CreateCartItemMutation, variables: CreateCartItemMutationVariables}, - "#graphql\n mutation CreateCart($items: [CartLineInput!]) {\n cartCreate(input: { lines: $items }) {\n cart {\n ...singleCart\n }\n }\n }\n #graphql \n fragment singleCart on Cart {\n id\n checkoutUrl\n cost {\n subtotalAmount {\n amount\n currencyCode\n }\n totalAmount {\n amount\n currencyCode\n }\n totalTaxAmount {\n amount\n currencyCode\n }\n }\n lines(first: 100) {\n edges {\n node {\n id\n quantity\n cost {\n totalAmount {\n amount\n currencyCode\n }\n }\n merchandise {\n ... on ProductVariant {\n id\n title\n price {\n amount\n currencyCode\n }\n quantityAvailable\n selectedOptions {\n name\n value\n }\n product {\n ...singleProduct\n }\n }\n }\n }\n }\n }\n totalQuantity\n }\n #graphql\n fragment singleProduct on Product {\n id\n handle\n title\n description\n descriptionHtml\n vendor\n options {\n id\n name\n values\n }\n priceRange {\n maxVariantPrice {\n amount\n currencyCode\n }\n minVariantPrice {\n amount\n currencyCode\n }\n }\n collections(first: 15) {\n nodes {\n handle\n title\n description\n updatedAt\n id\n descriptionHtml\n image {\n ...singleImage\n }\n }\n }\n variants(first: 250) {\n edges {\n node {\n id\n title\n quantityAvailable\n availableForSale\n selectedOptions {\n name\n value\n }\n price {\n amount\n currencyCode\n }\n }\n }\n }\n featuredImage {\n ...singleImage\n }\n images(first: 20) {\n edges {\n node {\n ...singleImage\n }\n }\n }\n seo {\n ...seo\n }\n tags\n updatedAt\n createdAt\n }\n #graphql\n fragment singleImage on Image {\n url\n altText\n width\n height\n }\n\n #graphql\n fragment seo on SEO {\n description\n title\n }\n\n\n\n": {return: CreateCartMutation, variables: CreateCartMutationVariables}, - "#graphql\n mutation UpdateCartItems($cartId: ID!, $items: [CartLineUpdateInput!]!) {\n cartLinesUpdate(cartId: $cartId, lines: $items) {\n cart {\n ...singleCart\n }\n }\n }\n #graphql \n fragment singleCart on Cart {\n id\n checkoutUrl\n cost {\n subtotalAmount {\n amount\n currencyCode\n }\n totalAmount {\n amount\n currencyCode\n }\n totalTaxAmount {\n amount\n currencyCode\n }\n }\n lines(first: 100) {\n edges {\n node {\n id\n quantity\n cost {\n totalAmount {\n amount\n currencyCode\n }\n }\n merchandise {\n ... on ProductVariant {\n id\n title\n price {\n amount\n currencyCode\n }\n quantityAvailable\n selectedOptions {\n name\n value\n }\n product {\n ...singleProduct\n }\n }\n }\n }\n }\n }\n totalQuantity\n }\n #graphql\n fragment singleProduct on Product {\n id\n handle\n title\n description\n descriptionHtml\n vendor\n options {\n id\n name\n values\n }\n priceRange {\n maxVariantPrice {\n amount\n currencyCode\n }\n minVariantPrice {\n amount\n currencyCode\n }\n }\n collections(first: 15) {\n nodes {\n handle\n title\n description\n updatedAt\n id\n descriptionHtml\n image {\n ...singleImage\n }\n }\n }\n variants(first: 250) {\n edges {\n node {\n id\n title\n quantityAvailable\n availableForSale\n selectedOptions {\n name\n value\n }\n price {\n amount\n currencyCode\n }\n }\n }\n }\n featuredImage {\n ...singleImage\n }\n images(first: 20) {\n edges {\n node {\n ...singleImage\n }\n }\n }\n seo {\n ...seo\n }\n tags\n updatedAt\n createdAt\n }\n #graphql\n fragment singleImage on Image {\n url\n altText\n width\n height\n }\n\n #graphql\n fragment seo on SEO {\n description\n title\n }\n\n\n\n": {return: UpdateCartItemsMutation, variables: UpdateCartItemsMutationVariables}, - "#graphql\n mutation DeleteCartItems($cartId: ID!, $itemIds: [ID!]!) {\n cartLinesRemove(cartId: $cartId, lineIds: $itemIds) {\n cart {\n ...singleCart\n }\n }\n }\n #graphql \n fragment singleCart on Cart {\n id\n checkoutUrl\n cost {\n subtotalAmount {\n amount\n currencyCode\n }\n totalAmount {\n amount\n currencyCode\n }\n totalTaxAmount {\n amount\n currencyCode\n }\n }\n lines(first: 100) {\n edges {\n node {\n id\n quantity\n cost {\n totalAmount {\n amount\n currencyCode\n }\n }\n merchandise {\n ... on ProductVariant {\n id\n title\n price {\n amount\n currencyCode\n }\n quantityAvailable\n selectedOptions {\n name\n value\n }\n product {\n ...singleProduct\n }\n }\n }\n }\n }\n }\n totalQuantity\n }\n #graphql\n fragment singleProduct on Product {\n id\n handle\n title\n description\n descriptionHtml\n vendor\n options {\n id\n name\n values\n }\n priceRange {\n maxVariantPrice {\n amount\n currencyCode\n }\n minVariantPrice {\n amount\n currencyCode\n }\n }\n collections(first: 15) {\n nodes {\n handle\n title\n description\n updatedAt\n id\n descriptionHtml\n image {\n ...singleImage\n }\n }\n }\n variants(first: 250) {\n edges {\n node {\n id\n title\n quantityAvailable\n availableForSale\n selectedOptions {\n name\n value\n }\n price {\n amount\n currencyCode\n }\n }\n }\n }\n featuredImage {\n ...singleImage\n }\n images(first: 20) {\n edges {\n node {\n ...singleImage\n }\n }\n }\n seo {\n ...seo\n }\n tags\n updatedAt\n createdAt\n }\n #graphql\n fragment singleImage on Image {\n url\n altText\n width\n height\n }\n\n #graphql\n fragment seo on SEO {\n description\n title\n }\n\n\n\n": {return: DeleteCartItemsMutation, variables: DeleteCartItemsMutationVariables}, + "#graphql\n mutation CreateCartItem($cartId: ID!, $items: [CartLineInput!]!) {\n cartLinesAdd(cartId: $cartId, lines: $items) {\n cart {\n ...singleCart\n }\n }\n }\n #graphql \n fragment singleCart on Cart {\n id\n checkoutUrl\n cost {\n subtotalAmount {\n amount\n currencyCode\n }\n totalAmount {\n amount\n currencyCode\n }\n totalTaxAmount {\n amount\n currencyCode\n }\n }\n lines(first: 100) {\n edges {\n node {\n id\n quantity\n cost {\n totalAmount {\n amount\n currencyCode\n }\n }\n merchandise {\n ... on ProductVariant {\n id\n title\n price {\n amount\n currencyCode\n }\n quantityAvailable\n selectedOptions {\n name\n value\n }\n product {\n ...singleProduct\n }\n }\n }\n }\n }\n }\n totalQuantity\n }\n #graphql\n fragment singleProduct on Product {\n id\n handle\n title\n description\n descriptionHtml\n vendor\n options {\n id\n name\n values\n }\n priceRange {\n maxVariantPrice {\n amount\n currencyCode\n }\n minVariantPrice {\n amount\n currencyCode\n }\n }\n collections(first: 250) {\n nodes {\n handle\n id\n }\n }\n variants(first: 250) {\n edges {\n node {\n id\n title\n quantityAvailable\n availableForSale\n selectedOptions {\n name\n value\n }\n price {\n amount\n currencyCode\n }\n }\n }\n }\n featuredImage {\n ...singleImage\n }\n images(first: 20) {\n edges {\n node {\n ...singleImage\n }\n }\n }\n seo {\n ...seo\n }\n tags\n updatedAt\n createdAt\n }\n #graphql\n fragment singleImage on Image {\n url\n altText\n width\n height\n }\n\n #graphql\n fragment seo on SEO {\n description\n title\n }\n\n\n\n": {return: CreateCartItemMutation, variables: CreateCartItemMutationVariables}, + "#graphql\n mutation CreateCart($items: [CartLineInput!]) {\n cartCreate(input: { lines: $items }) {\n cart {\n ...singleCart\n }\n }\n }\n #graphql \n fragment singleCart on Cart {\n id\n checkoutUrl\n cost {\n subtotalAmount {\n amount\n currencyCode\n }\n totalAmount {\n amount\n currencyCode\n }\n totalTaxAmount {\n amount\n currencyCode\n }\n }\n lines(first: 100) {\n edges {\n node {\n id\n quantity\n cost {\n totalAmount {\n amount\n currencyCode\n }\n }\n merchandise {\n ... on ProductVariant {\n id\n title\n price {\n amount\n currencyCode\n }\n quantityAvailable\n selectedOptions {\n name\n value\n }\n product {\n ...singleProduct\n }\n }\n }\n }\n }\n }\n totalQuantity\n }\n #graphql\n fragment singleProduct on Product {\n id\n handle\n title\n description\n descriptionHtml\n vendor\n options {\n id\n name\n values\n }\n priceRange {\n maxVariantPrice {\n amount\n currencyCode\n }\n minVariantPrice {\n amount\n currencyCode\n }\n }\n collections(first: 250) {\n nodes {\n handle\n id\n }\n }\n variants(first: 250) {\n edges {\n node {\n id\n title\n quantityAvailable\n availableForSale\n selectedOptions {\n name\n value\n }\n price {\n amount\n currencyCode\n }\n }\n }\n }\n featuredImage {\n ...singleImage\n }\n images(first: 20) {\n edges {\n node {\n ...singleImage\n }\n }\n }\n seo {\n ...seo\n }\n tags\n updatedAt\n createdAt\n }\n #graphql\n fragment singleImage on Image {\n url\n altText\n width\n height\n }\n\n #graphql\n fragment seo on SEO {\n description\n title\n }\n\n\n\n": {return: CreateCartMutation, variables: CreateCartMutationVariables}, + "#graphql\n mutation UpdateCartItems($cartId: ID!, $items: [CartLineUpdateInput!]!) {\n cartLinesUpdate(cartId: $cartId, lines: $items) {\n cart {\n ...singleCart\n }\n }\n }\n #graphql \n fragment singleCart on Cart {\n id\n checkoutUrl\n cost {\n subtotalAmount {\n amount\n currencyCode\n }\n totalAmount {\n amount\n currencyCode\n }\n totalTaxAmount {\n amount\n currencyCode\n }\n }\n lines(first: 100) {\n edges {\n node {\n id\n quantity\n cost {\n totalAmount {\n amount\n currencyCode\n }\n }\n merchandise {\n ... on ProductVariant {\n id\n title\n price {\n amount\n currencyCode\n }\n quantityAvailable\n selectedOptions {\n name\n value\n }\n product {\n ...singleProduct\n }\n }\n }\n }\n }\n }\n totalQuantity\n }\n #graphql\n fragment singleProduct on Product {\n id\n handle\n title\n description\n descriptionHtml\n vendor\n options {\n id\n name\n values\n }\n priceRange {\n maxVariantPrice {\n amount\n currencyCode\n }\n minVariantPrice {\n amount\n currencyCode\n }\n }\n collections(first: 250) {\n nodes {\n handle\n id\n }\n }\n variants(first: 250) {\n edges {\n node {\n id\n title\n quantityAvailable\n availableForSale\n selectedOptions {\n name\n value\n }\n price {\n amount\n currencyCode\n }\n }\n }\n }\n featuredImage {\n ...singleImage\n }\n images(first: 20) {\n edges {\n node {\n ...singleImage\n }\n }\n }\n seo {\n ...seo\n }\n tags\n updatedAt\n createdAt\n }\n #graphql\n fragment singleImage on Image {\n url\n altText\n width\n height\n }\n\n #graphql\n fragment seo on SEO {\n description\n title\n }\n\n\n\n": {return: UpdateCartItemsMutation, variables: UpdateCartItemsMutationVariables}, + "#graphql\n mutation DeleteCartItems($cartId: ID!, $itemIds: [ID!]!) {\n cartLinesRemove(cartId: $cartId, lineIds: $itemIds) {\n cart {\n ...singleCart\n }\n }\n }\n #graphql \n fragment singleCart on Cart {\n id\n checkoutUrl\n cost {\n subtotalAmount {\n amount\n currencyCode\n }\n totalAmount {\n amount\n currencyCode\n }\n totalTaxAmount {\n amount\n currencyCode\n }\n }\n lines(first: 100) {\n edges {\n node {\n id\n quantity\n cost {\n totalAmount {\n amount\n currencyCode\n }\n }\n merchandise {\n ... on ProductVariant {\n id\n title\n price {\n amount\n currencyCode\n }\n quantityAvailable\n selectedOptions {\n name\n value\n }\n product {\n ...singleProduct\n }\n }\n }\n }\n }\n }\n totalQuantity\n }\n #graphql\n fragment singleProduct on Product {\n id\n handle\n title\n description\n descriptionHtml\n vendor\n options {\n id\n name\n values\n }\n priceRange {\n maxVariantPrice {\n amount\n currencyCode\n }\n minVariantPrice {\n amount\n currencyCode\n }\n }\n collections(first: 250) {\n nodes {\n handle\n id\n }\n }\n variants(first: 250) {\n edges {\n node {\n id\n title\n quantityAvailable\n availableForSale\n selectedOptions {\n name\n value\n }\n price {\n amount\n currencyCode\n }\n }\n }\n }\n featuredImage {\n ...singleImage\n }\n images(first: 20) {\n edges {\n node {\n ...singleImage\n }\n }\n }\n seo {\n ...seo\n }\n tags\n updatedAt\n createdAt\n }\n #graphql\n fragment singleImage on Image {\n url\n altText\n width\n height\n }\n\n #graphql\n fragment seo on SEO {\n description\n title\n }\n\n\n\n": {return: DeleteCartItemsMutation, variables: DeleteCartItemsMutationVariables}, "#graphql\n mutation CreateCustomer($input: CustomerCreateInput!) {\n customerCreate(input: $input) {\n customerUserErrors {\n code\n field\n message\n }\n customer {\n id\n }\n }\n }\n": {return: CreateCustomerMutation, variables: CreateCustomerMutationVariables}, "#graphql\n mutation UpdateCustomer($customer: CustomerUpdateInput!, $customerAccessToken: String!) {\n customerUpdate(customer: $customer, customerAccessToken: $customerAccessToken) {\n customerUserErrors {\n code\n field\n message\n }\n customer {\n id\n }\n }\n }\n": {return: UpdateCustomerMutation, variables: UpdateCustomerMutationVariables}, "#graphql\n mutation ActivateCustomer($id: ID!, $input: CustomerActivateInput!) {\n customerActivate(id: $id, input: $input) {\n customerUserErrors {\n code\n field\n message\n }\n customer {\n id\n }\n }\n }\n": {return: ActivateCustomerMutation, variables: ActivateCustomerMutationVariables}, diff --git a/packages/core/platform/types.ts b/packages/core/platform/types.ts index 38b9ebf..e29dfc2 100644 --- a/packages/core/platform/types.ts +++ b/packages/core/platform/types.ts @@ -1,5 +1,7 @@ +import { MenuItem } from "./shopify/types/storefront.types" + export interface PlatformMenu { - items: { title: string; url: string }[] + items: Array> } export interface PlatformProduct {