From 5b84146487af16514e55df39da5ae7813e36e6c6 Mon Sep 17 00:00:00 2001 From: ken Date: Sun, 2 Jun 2024 21:52:01 +0700 Subject: [PATCH 01/23] chore: update collection filter --- app/components/Checkbox.tsx | 35 ++ app/lib/const.ts | 1 + app/lib/filter.ts | 75 ++++ app/modules/Button.tsx | 3 +- app/modules/DrawerFilter.tsx | 345 ++++++++++++++++++ app/modules/Grid.tsx | 5 +- app/modules/Icon.tsx | 87 ++++- app/sections/collection-filters/index.tsx | 26 +- .../products-loaded-on-scroll.tsx | 5 +- package.json | 1 + 10 files changed, 568 insertions(+), 15 deletions(-) create mode 100644 app/components/Checkbox.tsx create mode 100644 app/lib/filter.ts create mode 100644 app/modules/DrawerFilter.tsx diff --git a/app/components/Checkbox.tsx b/app/components/Checkbox.tsx new file mode 100644 index 00000000..889ac44e --- /dev/null +++ b/app/components/Checkbox.tsx @@ -0,0 +1,35 @@ +import * as React from 'react'; +import * as CheckboxPrimitive from '@radix-ui/react-checkbox'; +import {Check} from 'lucide-react'; +import { cn } from '~/lib/cn'; + + +interface CheckboxProps + extends React.ComponentPropsWithoutRef { + label?: string; +} + +const Checkbox = React.forwardRef< + React.ElementRef, + CheckboxProps +>(({className, label, ...props}, ref) => ( +
+ + + + + + {label ? {label} : null} +
+ )); +Checkbox.displayName = CheckboxPrimitive.Root.displayName; + +export {Checkbox}; diff --git a/app/lib/const.ts b/app/lib/const.ts index 60d01e62..ebaff64f 100644 --- a/app/lib/const.ts +++ b/app/lib/const.ts @@ -1,6 +1,7 @@ export const PAGINATION_SIZE = 16; export const DEFAULT_GRID_IMG_LOAD_EAGER_COUNT = 4; export const ATTR_LOADING_EAGER = "eager"; +export const FILTER_URL_PREFIX = 'filter.'; export function getImageLoadingPriority( index: number, diff --git a/app/lib/filter.ts b/app/lib/filter.ts new file mode 100644 index 00000000..1bea526f --- /dev/null +++ b/app/lib/filter.ts @@ -0,0 +1,75 @@ +import type { + ProductFilter, +} from '@shopify/hydrogen/storefront-api-types'; +import type {Location, useLocation} from '@remix-run/react'; +import { FILTER_URL_PREFIX } from "./const"; + +export type AppliedFilter = { + label: string; + filter: ProductFilter; +}; + +export type SortParam = + | 'price-low-high' + | 'price-high-low' + | 'best-selling' + | 'newest' + | 'featured' + | 'relevance'; + +export function getAppliedFilterLink( + filter: AppliedFilter, + params: URLSearchParams, + location: Location, +) { + const paramsClone = new URLSearchParams(params); + Object.entries(filter.filter).forEach(([key, value]) => { + const fullKey = FILTER_URL_PREFIX + key; + paramsClone.delete(fullKey, JSON.stringify(value)); + }); + return `${location.pathname}?${paramsClone.toString()}`; +} + +export function getSortLink( + sort: SortParam, + params: URLSearchParams, + location: Location, +) { + params.set('sort', sort); + return `${location.pathname}?${params.toString()}`; +} + +export function getFilterLink( + rawInput: string | ProductFilter, + params: URLSearchParams, + location: ReturnType, +) { + const paramsClone = new URLSearchParams(params); + const newParams = filterInputToParams(rawInput, paramsClone); + return `${location.pathname}?${newParams.toString()}`; +} + + +export function filterInputToParams( + rawInput: string | ProductFilter, + params: URLSearchParams, +) { + const input = + typeof rawInput === 'string' + ? (JSON.parse(rawInput) as ProductFilter) + : rawInput; + + Object.entries(input).forEach(([key, value]) => { + if (params.has(`${FILTER_URL_PREFIX}${key}`, JSON.stringify(value))) { + return; + } + if (key === 'price') { + // For price, we want to overwrite + params.set(`${FILTER_URL_PREFIX}${key}`, JSON.stringify(value)); + } else { + params.append(`${FILTER_URL_PREFIX}${key}`, JSON.stringify(value)); + } + }); + + return params; +} \ No newline at end of file diff --git a/app/modules/Button.tsx b/app/modules/Button.tsx index 8cfc5995..6a5b225c 100644 --- a/app/modules/Button.tsx +++ b/app/modules/Button.tsx @@ -3,6 +3,7 @@ import { Link } from "@remix-run/react"; import clsx from "clsx"; import { missingClass } from "~/lib/utils"; +import { cn } from "~/lib/cn"; export const Button = forwardRef( ( @@ -41,7 +42,7 @@ export const Button = forwardRef( full: "w-full", }; - const styles = clsx( + const styles = cn( missingClass(className, "bg-") && variants[variant], missingClass(className, "w-") && widths[width], disabledClasses, diff --git a/app/modules/DrawerFilter.tsx b/app/modules/DrawerFilter.tsx new file mode 100644 index 00000000..587905f5 --- /dev/null +++ b/app/modules/DrawerFilter.tsx @@ -0,0 +1,345 @@ +import {Disclosure, Menu} from '@headlessui/react'; +import { + Link, + useLocation, + useNavigate, + useSearchParams, +} from '@remix-run/react'; +import type { + Filter, + ProductFilter, +} from '@shopify/hydrogen/storefront-api-types'; +import type {SyntheticEvent} from 'react'; +import {useMemo, useState} from 'react'; +import {useDebounce} from 'react-use'; +import {Drawer, useDrawer} from './Drawer'; +import { AppliedFilter, SortParam, filterInputToParams, getAppliedFilterLink, getFilterLink, getSortLink } from '~/lib/filter'; +import { Button } from './Button'; +import { Checkbox } from '~/components/Checkbox'; +import { FILTER_URL_PREFIX } from '~/lib/const'; +import { IconCaret, IconFilters, IconFourGrid, IconOneGrid, IconSliders, IconThreeGrid, IconTwoGrid } from './Icon'; +import { Input } from '.'; +import clsx from 'clsx'; + + +type DrawerFilterProps = { + productNumber?: number; + filters: Filter[]; + appliedFilters?: AppliedFilter[]; + collections?: Array<{handle: string; title: string}>; + showSearchSort?: boolean; + numberInRow?: number; + onLayoutChange: (number: number) => void; +}; + +export function DrawerFilter({ + filters, + numberInRow, + onLayoutChange, + appliedFilters = [], + productNumber = 0, + showSearchSort = false, +}: DrawerFilterProps) { + const {openDrawer, isOpen, closeDrawer} = useDrawer(); + return ( +
+
+
+ + +
onLayoutChange(4)} + > + +
+
onLayoutChange(3)} + > + +
+
+ + {productNumber} Products + +
+ + + +
+ +
+
+
+
+
+ ); +} + +function ListItemFilter({ + option, + appliedFilters, +}: { + option: Filter['values'][0]; + appliedFilters: AppliedFilter[]; +}) { + const navigate = useNavigate(); + const [params] = useSearchParams(); + const location = useLocation(); + let filter = appliedFilters.find( + (filter) => JSON.stringify(filter.filter) === option.input, + ); + let [checked, setChecked] = useState(!!filter); + + let handleCheckedChange = (checked: boolean) => { + setChecked(checked); + if (checked) { + const link = getFilterLink(option.input as string, params, location); + navigate(link); + } else if (filter) { + let link = getAppliedFilterLink(filter, params, location); + navigate(link); + } + }; + return ( +
+ +
+ ); +} + +export function FiltersDrawer({ + filters = [], + appliedFilters = [], +}: Omit) { + const [params] = useSearchParams(); + const filterMarkup = (filter: Filter, option: Filter['values'][0]) => { + switch (filter.type) { + case 'PRICE_RANGE': + const priceFilter = params.get(`${FILTER_URL_PREFIX}price`); + const price = priceFilter + ? (JSON.parse(priceFilter) as ProductFilter['price']) + : undefined; + const min = isNaN(Number(price?.min)) ? undefined : Number(price?.min); + const max = isNaN(Number(price?.max)) ? undefined : Number(price?.max); + return ; + + default: + return ( + + ); + } + }; + + return ( + + ); +} + + +const PRICE_RANGE_FILTER_DEBOUNCE = 500; + +function PriceRangeFilter({max, min}: {max?: number; min?: number}) { + const location = useLocation(); + const params = useMemo( + () => new URLSearchParams(location.search), + [location.search], + ); + const navigate = useNavigate(); + + const [minPrice, setMinPrice] = useState(min); + const [maxPrice, setMaxPrice] = useState(max); + + // useDebounce( + // () => { + // if (minPrice === undefined && maxPrice === undefined) { + // params.delete(`${FILTER_URL_PREFIX}price`); + // navigate(`${location.pathname}?${params.toString()}`); + // return; + // } + + // const price = { + // ...(minPrice === undefined ? {} : {min: minPrice}), + // ...(maxPrice === undefined ? {} : {max: maxPrice}), + // }; + // const newParams = filterInputToParams({price}, params); + // navigate(`${location.pathname}?${newParams.toString()}`); + // }, + // PRICE_RANGE_FILTER_DEBOUNCE, + // [minPrice, maxPrice], + // ); + + const onChangeMax = (event: SyntheticEvent) => { + const value = (event.target as HTMLInputElement).value; + const newMaxPrice = Number.isNaN(parseFloat(value)) + ? undefined + : parseFloat(value); + setMaxPrice(newMaxPrice); + }; + + const onChangeMin = (event: SyntheticEvent) => { + const value = (event.target as HTMLInputElement).value; + const newMinPrice = Number.isNaN(parseFloat(value)) + ? undefined + : parseFloat(value); + setMinPrice(newMinPrice); + }; + + return ( +
+ + +
+ ); +} + +export default function SortMenu({ + showSearchSort = false, +}: { + showSearchSort?: boolean; +}) { + const productShortItems: {label: string; key: SortParam}[] = [ + {label: 'Featured', key: 'featured'}, + { + label: 'Price: Low - High', + key: 'price-low-high', + }, + { + label: 'Price: High - Low', + key: 'price-high-low', + }, + { + label: 'Best Selling', + key: 'best-selling', + }, + { + label: 'Newest', + key: 'newest', + }, + ]; + + const searchSortItems: {label: string; key: SortParam}[] = [ + { + label: 'Price: Low - High', + key: 'price-low-high', + }, + { + label: 'Price: High - Low', + key: 'price-high-low', + }, + { + label: 'Relevance', + key: 'relevance', + }, + ]; + const items = showSearchSort ? searchSortItems : productShortItems; + const [params] = useSearchParams(); + const location = useLocation(); + const activeItem = + items.find((item) => item.key === params.get('sort')) || items[0]; + + return ( + + + Sort by + + + + {items.map((item) => ( + + {() => ( + +

+ {item.label} +

+ + )} +
+ ))} +
+
+ ); +} diff --git a/app/modules/Grid.tsx b/app/modules/Grid.tsx index 344073ac..710374ba 100644 --- a/app/modules/Grid.tsx +++ b/app/modules/Grid.tsx @@ -7,6 +7,7 @@ export function Grid({ gap = "default", items = 4, layout = "default", + numberInRow = 4, ...props }: { as?: React.ElementType; @@ -21,9 +22,7 @@ export function Grid({ default: `grid-cols-1 ${items === 2 && "md:grid-cols-2"} ${ items === 3 && "sm:grid-cols-3" } ${items > 3 && "md:grid-cols-3"} ${items >= 4 && "lg:grid-cols-4"}`, - products: `grid-cols-2 ${items >= 3 && "md:grid-cols-3"} ${ - items >= 4 && "lg:grid-cols-4" - }`, + products: `${numberInRow === 4 ? "lg:grid-cols-4 grid-cols-2" : "grid-cols-1 lg:grid-cols-3"}`, auto: "auto-cols-auto", blog: "grid-cols-1 md:grid-cols-2", }; diff --git a/app/modules/Icon.tsx b/app/modules/Icon.tsx index 1f08e169..8505b3fb 100644 --- a/app/modules/Icon.tsx +++ b/app/modules/Icon.tsx @@ -1,4 +1,5 @@ import clsx from "clsx"; +import { cn } from "~/lib/cn"; type IconProps = JSX.IntrinsicElements["svg"] & { direction?: "up" | "right" | "down" | "left"; @@ -18,7 +19,7 @@ function Icon({ {...props} fill={fill} stroke={stroke} - className={clsx("w-5 h-5", className)} + className={cn("w-5 h-5", className)} > {children} @@ -438,3 +439,87 @@ export function IconHalfFilledStar(props: IconProps) { ); } + +export function IconSliders(props: IconProps) { + return ( + + + + + ); +} +export function IconFourGrid(props: IconProps) { + return ( + + + + + + + + + + + + + + + + + + + ); +} +export function IconThreeGrid(props: IconProps) { + return ( + + + + + + + + + + + + + ); +} +export function IconTwoGrid(props: IconProps) { + return ( + + + + + + + ); + +}export function IconOneGrid(props: IconProps) { + return ( + + + + ); +} \ No newline at end of file diff --git a/app/sections/collection-filters/index.tsx b/app/sections/collection-filters/index.tsx index 41c7e712..8d97c54f 100644 --- a/app/sections/collection-filters/index.tsx +++ b/app/sections/collection-filters/index.tsx @@ -5,7 +5,7 @@ import type { HydrogenComponentProps, HydrogenComponentSchema, } from "@weaverse/hydrogen"; -import { forwardRef } from "react"; +import { forwardRef, useState } from "react"; import { useInView } from "react-intersection-observer"; import type { CollectionDetailsQuery } from "storefrontapi.generated"; @@ -13,6 +13,7 @@ import { Button, PageHeader, Section, SortFilter, Text } from "~/modules"; import type { AppliedFilter } from "~/modules/SortFilter"; import { ProductsLoadedOnScroll } from "./products-loaded-on-scroll"; +import { DrawerFilter } from "~/modules/DrawerFilter"; interface CollectionFiltersProps extends HydrogenComponentProps { showCollectionDescription: boolean; @@ -26,12 +27,18 @@ let CollectionFilters = forwardRef( props; let { ref, inView } = useInView(); + let [numberInRow, setNumberInRow] = useState(4); + let onLayoutChange = (number: number) => { + setNumberInRow(number); + } let { collection, collections, appliedFilters } = useLoaderData< CollectionDetailsQuery & { collections: Array<{ handle: string; title: string }>; appliedFilters: AppliedFilter[]; } >(); + let productNumber = collection?.products.nodes.length; + if (collection?.products && collections) { return ( @@ -47,12 +54,15 @@ let CollectionFilters = forwardRef( )} +
- {({ nodes, @@ -64,7 +74,7 @@ let CollectionFilters = forwardRef( state, }) => ( <> -
+
( )} -
); diff --git a/app/sections/collection-filters/products-loaded-on-scroll.tsx b/app/sections/collection-filters/products-loaded-on-scroll.tsx index 73bfe52f..c78165e7 100644 --- a/app/sections/collection-filters/products-loaded-on-scroll.tsx +++ b/app/sections/collection-filters/products-loaded-on-scroll.tsx @@ -6,6 +6,7 @@ import { getImageLoadingPriority } from "~/lib/const"; type ProductsLoadedOnScrollProps = { nodes: any; + numberInRow: number; inView: boolean; nextPageUrl: string; hasNextPage: boolean; @@ -13,7 +14,7 @@ type ProductsLoadedOnScrollProps = { }; export function ProductsLoadedOnScroll(props: ProductsLoadedOnScrollProps) { - let { nodes, inView, nextPageUrl, hasNextPage, state } = props; + let { nodes, inView, nextPageUrl, hasNextPage, state, numberInRow = 4 } = props; let navigate = useNavigate(); useEffect(() => { @@ -27,7 +28,7 @@ export function ProductsLoadedOnScroll(props: ProductsLoadedOnScrollProps) { }, [inView, navigate, state, nextPageUrl, hasNextPage]); return ( - + {nodes.map((product: any, i: number) => ( Date: Tue, 4 Jun 2024 15:58:44 +0700 Subject: [PATCH 02/23] chore: add mega menu --- app/modules/Layout.tsx | 22 +--- app/modules/MegaMenu.tsx | 233 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 235 insertions(+), 20 deletions(-) create mode 100644 app/modules/MegaMenu.tsx diff --git a/app/modules/Layout.tsx b/app/modules/Layout.tsx index 6cf99e8f..ebb14592 100644 --- a/app/modules/Layout.tsx +++ b/app/modules/Layout.tsx @@ -37,6 +37,7 @@ import { useRootLoaderData } from "~/root"; import { Logo } from "./Logo"; import { PredictiveSearch } from "~/components/predictive-search/PredictiveSearch"; +import { MegaMenu } from "./MegaMenu"; type LayoutProps = { children: React.ReactNode; @@ -261,27 +262,8 @@ function DesktopHeader({ "w-full px-6 md:px-8 lg:px-12 py-4", )} > -
- -
+
diff --git a/app/modules/MegaMenu.tsx b/app/modules/MegaMenu.tsx new file mode 100644 index 00000000..46f7ad4b --- /dev/null +++ b/app/modules/MegaMenu.tsx @@ -0,0 +1,233 @@ +import { Link } from "@remix-run/react" +import { Image } from "@shopify/hydrogen"; + +interface Item { + title: string; + to: string; +} +interface SingleMenuItem { + title: string; + items: Item[]; +} + +interface MenuItemProps { + title: string; + items: SingleMenuItem[]; +} + + +export function MegaMenu() { + let items = [ + { + title: "Best Sellers", + items: [ + { + title: "Black Friday", + to: "/black-friday", + }, + { + title: "History Month", + to: "/history-month", + }, + { + title: "Outlets", + to: "/outlets", + }, + ] + }, + { + title: "SHIRTS & TEES", + items: [ + { + title: "New Arrivals", + to: "/new-arrivals", + }, + { + title: "Tops", + to: "/tops", + }, + { + title: "Jackets", + to: "/jackets", + }, + { + title: "Denims", + to: "/denims", + }, + { + title: "Pants", + to: "/pants", + }, + ] + }, + { + title: "PANTS & JEANS", + items: [ + { + title: "New Arrivals", + to: "/new-arrivals", + }, + { + title: "Scarfs", + to: "/scarfs", + }, + { + title: "Hats", + to: "/hats", + }, + { + title: "Jewelries", + to: "/jewelries", + } + ] + }, + { + title: "Accessories", + items: [ + { + title: "Bags", + to: "/bags", + }, + { + title: "Earrings", + to: "/earrings", + }, + { + title: "Hats", + to: "/hats", + }, + { + title: "Socks", + to: "/socks", + }, + { + title: "Belts", + to: "/belts", + } + ] + } + ] + return ( + + ) +} + +function MenuItem(props: MenuItemProps) { + let {title, items} = props; + return ( +
+ +
+
+
+ { + items.map((item) => ( +
+
{item.title}
+
    + {item.items.map((subItem, ind) => ( +
  • + + {subItem.title} + +
  • + ))} +
+
+ )) + } +
+
+
+ +
+
+ +
+
+
+
+ +
+ ) +} + +function PopupItem({title}: {title: string}) { + let items = { + title: "Pilot", + items: [ + { + title: "Journal", + to: "/journal", + }, + { + title: "Shipping & returns", + to: "/shipping-returns", + }, + { + title: "About us", + to: "/about-us", + } + ] + } + return ( +
+ +
+
+
+
Pilot
+
    + { + items.items.map((subItem, ind) => ( +
  • + + {subItem.title} + +
  • + )) + } +
+
+ +
+
+ +
+ ) +} \ No newline at end of file From ee8af4cc06f7853a5bda9ceecdf8d0a71e0708b7 Mon Sep 17 00:00:00 2001 From: ken Date: Wed, 5 Jun 2024 16:45:22 +0700 Subject: [PATCH 03/23] chore: add quick add modal --- app/data/fragments.ts | 2 +- app/lib/products.ts | 29 +++ app/modules/Modal.tsx | 23 +- app/modules/ProductCard.tsx | 100 ++++++--- app/modules/QuickView.tsx | 202 ++++++++++++++++++ app/modules/product-form/product-media.tsx | 2 +- app/routes/api.query.$param.ts | 32 +++ .../products-loaded-on-scroll.tsx | 1 + app/weaverse/schema.server.ts | 89 ++++++++ storefrontapi.generated.d.ts | 14 +- 10 files changed, 453 insertions(+), 41 deletions(-) create mode 100644 app/lib/products.ts create mode 100644 app/modules/QuickView.tsx create mode 100644 app/routes/api.query.$param.ts diff --git a/app/data/fragments.ts b/app/data/fragments.ts index a237d941..9a22da94 100644 --- a/app/data/fragments.ts +++ b/app/data/fragments.ts @@ -44,7 +44,7 @@ export const PRODUCT_CARD_FRAGMENT = `#graphql publishedAt handle vendor - variants(first: 1) { + variants(first: 10) { nodes { id availableForSale diff --git a/app/lib/products.ts b/app/lib/products.ts new file mode 100644 index 00000000..22e7d5c0 --- /dev/null +++ b/app/lib/products.ts @@ -0,0 +1,29 @@ +import { Storefront } from "@shopify/hydrogen"; +import { ProductQuery } from "storefrontapi.generated"; +import { PRODUCT_QUERY, VARIANTS_QUERY } from "~/data/queries"; + +export let getProductData = async (storefront: Storefront, productHandle: string) => { + let { product, shop } = await storefront.query(PRODUCT_QUERY, { + variables: { + handle: productHandle, + selectedOptions: [], + language: storefront.i18n.language, + country: storefront.i18n.country, + }, + }); + let variants = await storefront.query(VARIANTS_QUERY, { + variables: { + handle: productHandle, + language: storefront.i18n.language, + country: storefront.i18n.country, + }, + }); + return { + product, + variants, + storeDomain: shop.primaryDomain.url, + shop + }; +} + +export type ProductData = Awaited>; diff --git a/app/modules/Modal.tsx b/app/modules/Modal.tsx index 48154ae7..5b5ced87 100644 --- a/app/modules/Modal.tsx +++ b/app/modules/Modal.tsx @@ -1,12 +1,25 @@ -import { IconClose, Link } from "~/modules"; +import { useEffect } from "react"; +import { Button, IconClose, Link } from "~/modules"; export function Modal({ children, cancelLink, + onClose, + }: { children: React.ReactNode; cancelLink: string; + onClose?: () => void; + }) { + useEffect(() => { + if (!document.body.classList.contains("overflow-hidden")) { + document.body.classList.add("overflow-hidden") + } + return () => { + document.body.classList.remove("overflow-hidden") + } + },[]) return (
{ e.stopPropagation(); @@ -30,12 +43,18 @@ export function Modal({ tabIndex={0} >
+ {cancelLink ? ( + ) : ( + + ) }
{children}
diff --git a/app/modules/ProductCard.tsx b/app/modules/ProductCard.tsx index 490276d0..68e6fbdf 100644 --- a/app/modules/ProductCard.tsx +++ b/app/modules/ProductCard.tsx @@ -7,6 +7,7 @@ import type { ProductCardFragment } from "storefrontapi.generated"; import { Text, Link, AddToCartButton, Button } from "~/modules"; import { isDiscounted, isNewArrival } from "~/lib/utils"; import { getProductPlaceholder } from "~/lib/placeholders"; +import { QuickViewTrigger } from "./QuickView"; export function ProductCard({ product, @@ -29,8 +30,9 @@ export function ProductCard({ ? (product as Product) : getProductPlaceholder(); if (!cardProduct?.variants?.nodes?.length) return null; - - const firstVariant = flattenConnection(cardProduct.variants)[0]; + + const variants = flattenConnection(cardProduct.variants); + const firstVariant = variants[0]; if (!firstVariant) return null; const { image, price, compareAtPrice } = firstVariant; @@ -55,25 +57,26 @@ export function ProductCard({ return (
- { - return isTransitioning ? "vt-product-image" : ""; - }} - >
-
+
{image && ( - {image.altText + { + return isTransitioning ? "vt-product-image" : ""; + }} + > + {image.altText + )} {cardLabel} + { + quickAdd && variants.length > 1 && + ( + + )} + {quickAdd && + variants.length === 1 && + firstVariant.availableForSale && ( +
+ + + Add to Cart + + +
+ )}
- - {product.title} - {firstVariant.sku && ({firstVariant.sku})} - + + { + return isTransitioning ? "vt-product-image" : ""; + }} + > + {product.title} + {firstVariant.sku && ({firstVariant.sku})} + +
@@ -104,7 +145,6 @@ export function ProductCard({
- {quickAdd && firstVariant.availableForSale && ( )} {quickAdd && !firstVariant.availableForSale && ( - diff --git a/app/modules/QuickView.tsx b/app/modules/QuickView.tsx new file mode 100644 index 00000000..ec52864a --- /dev/null +++ b/app/modules/QuickView.tsx @@ -0,0 +1,202 @@ +import {useFetcher} from '@remix-run/react'; +import {Jsonify} from '@remix-run/server-runtime/dist/jsonify'; +import {Money, ShopPayButton} from '@shopify/hydrogen'; +import {useThemeSettings} from '@weaverse/hydrogen'; +import {useEffect, useState} from 'react'; +import {getExcerpt} from '~/lib/utils'; +import {ProductDetail} from '~/sections/product-information/product-detail'; +import {AddToCartButton} from './AddToCartButton'; +import {ProductMedia} from './product-form/product-media'; +import {Quantity} from './product-form/quantity'; +import {ProductVariants} from './product-form/variants'; +import { ProductData } from '~/lib/products'; +import { Button } from './Button'; +import { Modal } from './Modal'; + + +export function QuickView(props: {data: Jsonify}) { + const {data} = props; + + let themeSettings = useThemeSettings(); + let swatches = themeSettings?.swatches || { + configs: [], + swatches: { + imageSwatches: [], + colorSwatches: [], + }, + }; + let {product, variants: _variants, storeDomain, shop} = data || {}; + + let [selectedVariant, setSelectedVariant] = useState( + product?.selectedVariant, + ); + + let variants = _variants?.product?.variants; + let [quantity, setQuantity] = useState(1); + let { + addToCartText, + soldOutText, + unavailableText, + showShippingPolicy, + showRefundPolicy, + hideUnavailableOptions, + showThumbnails, + numberOfThumbnails, + spacing, + } = themeSettings; + let handleSelectedVariantChange = (variant: any) => { + setSelectedVariant(variant); + }; + console.log("🚀 ~ QuickView ~ selectedVariant:", selectedVariant) + useEffect(() => { + if (variants?.nodes?.length) { + if (!selectedVariant) { + setSelectedVariant(variants?.nodes?.[0]); + } else if (selectedVariant?.id !== product?.selectedVariant?.id) { + setSelectedVariant(product?.selectedVariant); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [product?.id]); + + const {shippingPolicy, refundPolicy} = shop; + + const {title, vendor, descriptionHtml} = product; + let atcText = selectedVariant?.availableForSale + ? addToCartText + : selectedVariant?.quantityAvailable === -1 + ? unavailableText + : soldOutText; + return ( +
+
+ +
+
+
+

+ {title} +

+
+

+ {selectedVariant && selectedVariant.compareAtPrice && ( + + )} + + {selectedVariant ? ( + + ) : null} +

+ +
+ + + {atcText} + + {selectedVariant?.availableForSale && ( + + )} +

+

+ {showShippingPolicy && shippingPolicy?.body && ( + + )} + {showRefundPolicy && refundPolicy?.body && ( + + )} +
+
+
+
+ ); +} + +export function QuickViewTrigger(props: {productHandle: string}) { + let [quickAddOpen, setQuickAddOpen] = useState(false); + const {productHandle} = props; + let {load, data, state} = useFetcher(); + console.log("🚀 ~ QuickViewTrigger ~ data:", data, productHandle) + useEffect(() => { + if (quickAddOpen && !data && state !== 'loading') { + load(`/api/query/products?handle=${productHandle}`); + } + }, [quickAddOpen, data, load, state]); + return ( + <> +
+ +
+ {quickAddOpen && data && ( + setQuickAddOpen(false)}> + + + )} + + ); +} diff --git a/app/modules/product-form/product-media.tsx b/app/modules/product-form/product-media.tsx index dc481461..0ab94037 100644 --- a/app/modules/product-form/product-media.tsx +++ b/app/modules/product-form/product-media.tsx @@ -74,7 +74,7 @@ export function ProductMedia(props: ProductMediaProps) { // thumbnailInstance.current?.update(thumbnailOptions); let selectedInd = media.findIndex((med) => { if (med.__typename !== "MediaImage") return false; - return med.image?.url === selectedVariant.image.url; + return med.image?.url === selectedVariant?.image.url; }); moveToIdx(selectedInd); // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/app/routes/api.query.$param.ts b/app/routes/api.query.$param.ts new file mode 100644 index 00000000..efa1591c --- /dev/null +++ b/app/routes/api.query.$param.ts @@ -0,0 +1,32 @@ +import { LoaderFunction, json } from '@remix-run/server-runtime'; +import { getProductData } from "~/lib/products"; + +function getRequestQueries>(request: Request) { + let url = new URL(request.url); + return Array.from(url.searchParams.entries()).reduce((q, [k, v]) => { + q[k] = v; + return q; + }, {}) as T; +} + +export let loader: LoaderFunction = async ({request, params, context}) => { + try { + let queries = getRequestQueries(request); + switch (params.param) { + case 'products': { + let handle = queries.handle; + if (!handle) return json(null, {status: 404}); + let productData = await getProductData( + context.storefront, + String(handle), + ); + return json(productData); + } + default: + return json(null, {status: 404}); + } + } catch (error) { + console.error(error); + return json({error: 'An error occurred'}, {status: 500}); + } +}; \ No newline at end of file diff --git a/app/sections/collection-filters/products-loaded-on-scroll.tsx b/app/sections/collection-filters/products-loaded-on-scroll.tsx index c78165e7..a4629646 100644 --- a/app/sections/collection-filters/products-loaded-on-scroll.tsx +++ b/app/sections/collection-filters/products-loaded-on-scroll.tsx @@ -31,6 +31,7 @@ export function ProductsLoadedOnScroll(props: ProductsLoadedOnScrollProps) { {nodes.map((product: any, i: number) => ( Date: Wed, 5 Jun 2024 17:05:01 +0700 Subject: [PATCH 04/23] chore: remove unused codes --- app/lib/products.ts | 53 ++++++++------- app/modules/ProductCard.tsx | 126 ++++++++++++++++++------------------ app/modules/QuickView.tsx | 54 ++++++++-------- 3 files changed, 117 insertions(+), 116 deletions(-) diff --git a/app/lib/products.ts b/app/lib/products.ts index 22e7d5c0..9e62da0e 100644 --- a/app/lib/products.ts +++ b/app/lib/products.ts @@ -1,29 +1,32 @@ -import { Storefront } from "@shopify/hydrogen"; -import { ProductQuery } from "storefrontapi.generated"; +import type { Storefront } from "@shopify/hydrogen"; +import type { ProductQuery } from "storefrontapi.generated"; import { PRODUCT_QUERY, VARIANTS_QUERY } from "~/data/queries"; -export let getProductData = async (storefront: Storefront, productHandle: string) => { - let { product, shop } = await storefront.query(PRODUCT_QUERY, { - variables: { - handle: productHandle, - selectedOptions: [], - language: storefront.i18n.language, - country: storefront.i18n.country, - }, - }); - let variants = await storefront.query(VARIANTS_QUERY, { - variables: { - handle: productHandle, - language: storefront.i18n.language, - country: storefront.i18n.country, - }, - }); - return { - product, - variants, - storeDomain: shop.primaryDomain.url, - shop - }; -} +export let getProductData = async ( + storefront: Storefront, + productHandle: string, +) => { + let { product, shop } = await storefront.query(PRODUCT_QUERY, { + variables: { + handle: productHandle, + selectedOptions: [], + language: storefront.i18n.language, + country: storefront.i18n.country, + }, + }); + let variants = await storefront.query(VARIANTS_QUERY, { + variables: { + handle: productHandle, + language: storefront.i18n.language, + country: storefront.i18n.country, + }, + }); + return { + product, + variants, + storeDomain: shop.primaryDomain.url, + shop, + }; +}; export type ProductData = Awaited>; diff --git a/app/modules/ProductCard.tsx b/app/modules/ProductCard.tsx index 68e6fbdf..779600c8 100644 --- a/app/modules/ProductCard.tsx +++ b/app/modules/ProductCard.tsx @@ -2,7 +2,6 @@ import clsx from "clsx"; import type { ShopifyAnalyticsProduct } from "@shopify/hydrogen"; import { flattenConnection, Image, Money, useMoney } from "@shopify/hydrogen"; import type { MoneyV2, Product } from "@shopify/hydrogen/storefront-api-types"; - import type { ProductCardFragment } from "storefrontapi.generated"; import { Text, Link, AddToCartButton, Button } from "~/modules"; import { isDiscounted, isNewArrival } from "~/lib/utils"; @@ -30,7 +29,7 @@ export function ProductCard({ ? (product as Product) : getProductPlaceholder(); if (!cardProduct?.variants?.nodes?.length) return null; - + const variants = flattenConnection(cardProduct.variants); const firstVariant = variants[0]; @@ -57,38 +56,36 @@ export function ProductCard({ return (
-
-
- {image && ( - { - return isTransitioning ? "vt-product-image" : ""; - }} - > - {image.altText - - )} - +
+ {image && ( + { + return isTransitioning ? "vt-product-image" : ""; + }} > - {cardLabel} - - { - quickAdd && variants.length > 1 && - ( - + {image.altText + + )} + + {cardLabel} + + {quickAdd && variants.length > 1 && ( + )} {quickAdd && variants.length === 1 && @@ -108,43 +105,46 @@ export function ProductCard({ totalValue: parseFloat(productAnalytics.price), }} > - + Add to Cart
)} -
-
- - { - return isTransitioning ? "vt-product-image" : ""; - }} - > - {product.title} - {firstVariant.sku && ({firstVariant.sku})} - - -
- - - {isDiscounted(price as MoneyV2, compareAtPrice as MoneyV2) && ( - - )} - -
+
+
+ + { + return isTransitioning ? "vt-product-image" : ""; + }} + > + {product.title} + {firstVariant.sku && ({firstVariant.sku})} + + +
+ + + {isDiscounted(price as MoneyV2, compareAtPrice as MoneyV2) && ( + + )} +
+
{quickAdd && firstVariant.availableForSale && ( }) { - const {data} = props; +export function QuickView(props: { data: Jsonify }) { + const { data } = props; let themeSettings = useThemeSettings(); let swatches = themeSettings?.swatches || { @@ -25,7 +24,7 @@ export function QuickView(props: {data: Jsonify}) { colorSwatches: [], }, }; - let {product, variants: _variants, storeDomain, shop} = data || {}; + let { product, variants: _variants, storeDomain, shop } = data || {}; let [selectedVariant, setSelectedVariant] = useState( product?.selectedVariant, @@ -47,7 +46,7 @@ export function QuickView(props: {data: Jsonify}) { let handleSelectedVariantChange = (variant: any) => { setSelectedVariant(variant); }; - console.log("🚀 ~ QuickView ~ selectedVariant:", selectedVariant) + console.log("🚀 ~ QuickView ~ selectedVariant:", selectedVariant); useEffect(() => { if (variants?.nodes?.length) { if (!selectedVariant) { @@ -59,9 +58,9 @@ export function QuickView(props: {data: Jsonify}) { // eslint-disable-next-line react-hooks/exhaustive-deps }, [product?.id]); - const {shippingPolicy, refundPolicy} = shop; + const { shippingPolicy, refundPolicy } = shop; - const {title, vendor, descriptionHtml} = product; + const { title, vendor, descriptionHtml } = product; let atcText = selectedVariant?.availableForSale ? addToCartText : selectedVariant?.quantityAvailable === -1 @@ -167,13 +166,12 @@ export function QuickView(props: {data: Jsonify}) { ); } -export function QuickViewTrigger(props: {productHandle: string}) { +export function QuickViewTrigger(props: { productHandle: string }) { let [quickAddOpen, setQuickAddOpen] = useState(false); - const {productHandle} = props; - let {load, data, state} = useFetcher(); - console.log("🚀 ~ QuickViewTrigger ~ data:", data, productHandle) + const { productHandle } = props; + let { load, data, state } = useFetcher(); useEffect(() => { - if (quickAddOpen && !data && state !== 'loading') { + if (quickAddOpen && !data && state !== "loading") { load(`/api/query/products?handle=${productHandle}`); } }, [quickAddOpen, data, load, state]); @@ -186,14 +184,14 @@ export function QuickViewTrigger(props: {productHandle: string}) { e.stopPropagation(); setQuickAddOpen(true); }} - loading={state === 'loading'} + loading={state === "loading"} className="w-full" > Select options
{quickAddOpen && data && ( - setQuickAddOpen(false)}> + setQuickAddOpen(false)}> )} From 0aee33209e35c56a9ae43f6890a5b2c89eae137f Mon Sep 17 00:00:00 2001 From: hta218 Date: Wed, 5 Jun 2024 17:32:35 +0700 Subject: [PATCH 05/23] Update Ali Reviews section preset data --- app/sections/ali-reviews/index.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/sections/ali-reviews/index.tsx b/app/sections/ali-reviews/index.tsx index 60c8af33..9f5be43b 100644 --- a/app/sections/ali-reviews/index.tsx +++ b/app/sections/ali-reviews/index.tsx @@ -69,7 +69,11 @@ export let schema: HydrogenComponentSchema = { presets: { children: [ { type: "heading", content: "Reviews" }, - { type: "paragraph", content: "Ali Reviews" }, + { + type: "paragraph", + content: + "This section demonstrates how to integrate with third-party apps using their public APIs. Reviews are fetched from Ali Reviews API.", + }, { type: "ali-reviews--list", showAvgRating: true, From 0fd2d8f94eb0965ba6ea4294471ca0392dcc4418 Mon Sep 17 00:00:00 2001 From: hta218 Date: Wed, 5 Jun 2024 21:07:47 +0700 Subject: [PATCH 06/23] Update lock file --- package-lock.json | 227 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 226 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 3f33fcf4..c9ebac70 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@fontsource/poppins": "^5.0.14", "@graphql-codegen/cli": "^5.0.2", "@headlessui/react": "2.0.3", + "@radix-ui/react-checkbox": "^1.0.4", "@remix-run/react": "2.9.2", "@remix-run/server-runtime": "2.9.2", "@shopify/cli": "3.60.1", @@ -5381,6 +5382,230 @@ "node": ">=12" } }, + "node_modules/@radix-ui/primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz", + "integrity": "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.0.4.tgz", + "integrity": "sha512-CBuGQa52aAYnADZVt/KBQzXrwx6TqnlwtcIPGtVt5JkkzQwMOLJjPukimhfKEr4GQNd43C+djUh5Ikopj8pSLg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-use-previous": "1.0.1", + "@radix-ui/react-use-size": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", + "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz", + "integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.1.tgz", + "integrity": "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", + "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", + "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", + "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz", + "integrity": "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", + "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.0.1.tgz", + "integrity": "sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.0.1.tgz", + "integrity": "sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@react-aria/focus": { "version": "3.17.1", "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.17.1.tgz", @@ -7409,7 +7634,7 @@ "version": "18.3.0", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", - "dev": true, + "devOptional": true, "dependencies": { "@types/react": "*" } From 4d1a788e12f3f4a9224686b766953773482ab1b0 Mon Sep 17 00:00:00 2001 From: hta218 Date: Thu, 6 Jun 2024 11:06:56 +0700 Subject: [PATCH 07/23] Refactor review bar and review item components --- app/sections/ali-reviews/review-bar.tsx | 2 +- app/sections/ali-reviews/review-item.tsx | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/app/sections/ali-reviews/review-bar.tsx b/app/sections/ali-reviews/review-bar.tsx index f8741407..3f6714ac 100644 --- a/app/sections/ali-reviews/review-bar.tsx +++ b/app/sections/ali-reviews/review-bar.tsx @@ -17,7 +17,7 @@ export function ReviewBar(review: { style={{ width: `${review.avg * 100}%` }} />
-
+
{review.count} reviews
diff --git a/app/sections/ali-reviews/review-item.tsx b/app/sections/ali-reviews/review-item.tsx index dc66f33e..325c4382 100644 --- a/app/sections/ali-reviews/review-item.tsx +++ b/app/sections/ali-reviews/review-item.tsx @@ -119,7 +119,7 @@ export function ReviewItem(props: ReviewItemProps) {
))}
- setPreviewMedia(null)} /> @@ -128,13 +128,11 @@ export function ReviewItem(props: ReviewItemProps) { ); } -function PreviewMedia({ - media, - closePreview, -}: { +function ReviewMediaPreview(props: { media: ReviewMedia | null; closePreview: () => void; }) { + let { media, closePreview } = props; if (media) { return (
From 504feb5c74986f55ae501d604fb4211affee9de2 Mon Sep 17 00:00:00 2001 From: hta218 Date: Thu, 6 Jun 2024 11:07:07 +0700 Subject: [PATCH 08/23] Update testimonials section with new presets data and styles --- app/sections/testimonials/index.tsx | 68 ++++++++++++++++++++++++++++- app/sections/testimonials/item.tsx | 14 +++--- app/sections/testimonials/items.tsx | 26 ----------- 3 files changed, 73 insertions(+), 35 deletions(-) diff --git a/app/sections/testimonials/index.tsx b/app/sections/testimonials/index.tsx index 11092b66..e700779c 100644 --- a/app/sections/testimonials/index.tsx +++ b/app/sections/testimonials/index.tsx @@ -27,7 +27,7 @@ export let schema: HydrogenComponentSchema = { children: [ { type: "heading", - content: "See what our customers are saying", + content: "Testimonials", }, { type: "paragraph", @@ -36,6 +36,72 @@ export let schema: HydrogenComponentSchema = { }, { type: "testimonials-items", + children: [ + { + type: "testimonial--item", + authorName: "Glen P.", + authorTitle: "Founder, eCom Graduates", + authorImage: + "https://cdn.shopify.com/s/files/1/0838/0052/3057/files/glen_p.webp?v=1711343796", + heading: "Shopify Headless Game Changer", + content: + "I run a Shopify development agency and this is the kind of tool I’ve been looking for. Clients do not understand why headless is rather expensive to build but having a tool/option like this is a game changer. ", + }, + { + type: "testimonial--item", + authorName: "Tom H.", + authorTitle: "Owner, On The Road UK", + authorImage: + "https://cdn.shopify.com/s/files/1/0838/0052/3057/files/tom_h.webp?v=1711343959", + heading: "Intuitive Tool with Big Plus", + content: + "I love how intuitive the tool is. It looks very promising for my potential clients, and being able to easily use meta objects with this is a big plus.", + }, + { + type: "testimonial--item", + authorName: "Kenneth G.", + authorTitle: "Frontend Developer, DevInside Agency", + authorImage: + "https://cdn.shopify.com/s/files/1/0838/0052/3057/files/Kenneth_g.webp?v=1711359007", + heading: "Hydrogen Editor Mirrors Shopify", + content: + "We already love the Shopify theme editor, so having something similar for Hydrogen is so cool because now we can get hydrogen storefront setup similar to a liquid store.", + hideOnMobile: true, + }, + { + type: "testimonial--item", + authorName: "Leonardo G.", + authorTitle: "Solo developer", + authorImage: + "https://cdn.shopify.com/s/files/1/0838/0052/3057/files/leo_1.webp?v=1711359106", + heading: "Hydrogen Shift Eases for Solo Dev", + content: + "As a solo dev with a small Shopify shop, this is something interesting to hear about. I’m migrating from a GatsbyJS headless to Hydrogen solution, and Weaverse makes it a lot easier because I want to avoid hydrogen-react with NextJS!", + hideOnMobile: true, + }, + { + type: "testimonial--item", + authorName: "Micky M.", + authorTitle: "Owner, Joylery Silver", + authorImage: + "https://cdn.shopify.com/s/files/1/0838/0052/3057/files/micky_m.webp?v=1711359054", + heading: "Weaverse Makes Headless Accessible", + content: + "We struggled with site speed and as an ex-developer, I wanted to go headless but with only one in-house developer, it seemed impossible. Weaverse really made going headless a lot more accessible.", + hideOnMobile: true, + }, + { + type: "testimonial--item", + authorName: "John D.", + authorTitle: "CEO, Tech Solutions", + authorImage: + "https://cdn.shopify.com/s/files/1/0838/0052/3057/files/glen_p.webp?v=1711343796", + heading: "Incredible Tool for Development", + content: + "As a tech company CEO, this tool has revolutionized how we approach development. It's intuitive, efficient, and has made our processes significantly more streamlined.", + hideOnMobile: true, + }, + ], }, ], }, diff --git a/app/sections/testimonials/item.tsx b/app/sections/testimonials/item.tsx index 3989438f..fc9941fe 100644 --- a/app/sections/testimonials/item.tsx +++ b/app/sections/testimonials/item.tsx @@ -34,11 +34,11 @@ let TestimonialItem = forwardRef( {...rest} className={clsx(hideOnMobile && "hidden sm:block")} > -
-
+
+

{heading}

@@ -55,11 +55,9 @@ let TestimonialItem = forwardRef( width={36} sizes="auto" /> -

-
{authorName}
-
- {authorTitle} -
+
+
{authorName}
+
{authorTitle}
diff --git a/app/sections/testimonials/items.tsx b/app/sections/testimonials/items.tsx index 871a0e06..6d54707d 100644 --- a/app/sections/testimonials/items.tsx +++ b/app/sections/testimonials/items.tsx @@ -71,30 +71,4 @@ export let schema: HydrogenComponentSchema = { }, ], toolbar: ["general-settings", ["duplicate", "delete"]], - presets: { - children: [ - { - type: "testimonial--item", - }, - { - type: "testimonial--item", - hideOnMobile: true, - }, - { - type: "testimonial--item", - hideOnMobile: true, - }, - { - type: "testimonial--item", - }, - { - type: "testimonial--item", - hideOnMobile: true, - }, - { - type: "testimonial--item", - hideOnMobile: true, - }, - ], - }, }; From 9f1d6cc54ffe778a80e184d249b9e46844a563f7 Mon Sep 17 00:00:00 2001 From: hta218 Date: Thu, 6 Jun 2024 14:54:28 +0700 Subject: [PATCH 09/23] Refactor VideoEmbed component and update VideoEmbedItem name --- app/sections/video-embed/index.tsx | 5 +---- app/sections/video-embed/video.tsx | 34 ++++++++++++++++-------------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/app/sections/video-embed/index.tsx b/app/sections/video-embed/index.tsx index e6293f14..d3b348c6 100644 --- a/app/sections/video-embed/index.tsx +++ b/app/sections/video-embed/index.tsx @@ -3,10 +3,7 @@ import { forwardRef } from "react"; import type { SectionProps } from "~/components/Section"; import { Section, sectionInspector } from "~/components/Section"; -type VideoEmbedProps = SectionProps & { - heading: string; - description: string; -}; +type VideoEmbedProps = SectionProps; let VideoEmbed = forwardRef((props, ref) => { let { children, ...rest } = props; diff --git a/app/sections/video-embed/video.tsx b/app/sections/video-embed/video.tsx index b6d24d03..ab44949e 100644 --- a/app/sections/video-embed/video.tsx +++ b/app/sections/video-embed/video.tsx @@ -8,23 +8,25 @@ interface VideoItemProps extends HydrogenComponentProps { videoUrl: string; } -let VideoItem = forwardRef((props, ref) => { - let { videoUrl, ...rest } = props; - return ( -