diff --git a/README.md b/README.md index 41c25654..a5189b15 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ _Pilot is an innovative Shopify theme, powered by Hydrogen, Remix, and Weaverse, ![demo](https://cdn.shopify.com/s/files/1/0693/8201/3220/files/Home.png?v=1695816170) ## What's included + - Remix - Hydrogen - Oxygen diff --git a/app/components/Checkbox.tsx b/app/components/Checkbox.tsx index 889ac44e..865cac07 100644 --- a/app/components/Checkbox.tsx +++ b/app/components/Checkbox.tsx @@ -1,8 +1,7 @@ -import * as React from 'react'; -import * as CheckboxPrimitive from '@radix-ui/react-checkbox'; -import {Check} from 'lucide-react'; -import { cn } from '~/lib/cn'; - +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 { @@ -12,24 +11,24 @@ interface CheckboxProps const Checkbox = React.forwardRef< React.ElementRef, CheckboxProps ->(({className, label, ...props}, ref) => ( -
- (({ className, label, ...props }, ref) => ( +
+ + - - - - - {label ? {label} : null} -
- )); + + +
+ {label ? {label} : null} +
+)); Checkbox.displayName = CheckboxPrimitive.Root.displayName; -export {Checkbox}; +export { Checkbox }; diff --git a/app/components/predictive-search/PredictiveSearch.tsx b/app/components/predictive-search/PredictiveSearch.tsx index 48f167bf..dc7138fb 100644 --- a/app/components/predictive-search/PredictiveSearch.tsx +++ b/app/components/predictive-search/PredictiveSearch.tsx @@ -1,6 +1,6 @@ -import { IconSearch, Input } from '~/modules'; -import {PredictiveSearchResults} from './PredictiveSearchResults'; -import {PredictiveSearchForm} from './SearchForm'; +import { IconSearch, Input } from "~/modules"; +import { PredictiveSearchResults } from "./PredictiveSearchResults"; +import { PredictiveSearchForm } from "./SearchForm"; interface PredictiveSearchProps { // Predictive search props @@ -8,14 +8,15 @@ interface PredictiveSearchProps { } export function PredictiveSearch(props: PredictiveSearchProps) { - let {isOpen} = props; + let { isOpen } = props; return (
- {({fetchResults, inputRef}) => ( + {({ fetchResults, inputRef }) => (
-
{isSuggestions ? 'Suggestions' : type}
+
+ {isSuggestions ? "Suggestions" : type} +
-
    - {items.map((item: NormalizedPredictiveSearchResultItem) => ( - - ))} -
+ {items?.length && ( +
    + {items.map((item: NormalizedPredictiveSearchResultItem) => ( + + ))} +
+ )}
); } @@ -50,20 +59,20 @@ export function PredictiveSearchResult({ */ function pluralToSingularSearchType( type: - | NormalizedPredictiveSearchResults[number]['type'] - | Array, + | NormalizedPredictiveSearchResults[number]["type"] + | Array, ) { const plural = { - articles: 'ARTICLE', - collections: 'COLLECTION', - pages: 'PAGE', - products: 'PRODUCT', - queries: 'QUERY', + articles: "ARTICLE", + collections: "COLLECTION", + pages: "PAGE", + products: "PRODUCT", + queries: "QUERY", }; - if (typeof type === 'string') { + if (typeof type === "string") { return plural[type]; } - return type.map((t) => plural[t]).join(','); + return type.map((t) => plural[t]).join(","); } diff --git a/app/components/predictive-search/PredictiveSearchResults.tsx b/app/components/predictive-search/PredictiveSearchResults.tsx index 08680071..9ed47db0 100644 --- a/app/components/predictive-search/PredictiveSearchResults.tsx +++ b/app/components/predictive-search/PredictiveSearchResults.tsx @@ -1,24 +1,23 @@ -import {Link} from '@remix-run/react'; -import {PredictiveSearchResult} from './PredictiveSearchResult'; -import {usePredictiveSearch} from './usePredictiveSearch'; +import { Link } from "@remix-run/react"; +import { PredictiveSearchResult } from "./PredictiveSearchResult"; +import { usePredictiveSearch } from "./usePredictiveSearch"; export function PredictiveSearchResults() { - const {results, totalResults, searchTerm, searchInputRef} = + const { results, totalResults, searchTerm, searchInputRef } = usePredictiveSearch(); - - let queries = results?.find((result) => result.type === 'queries'); - let articles = results?.find((result) => result.type === 'articles'); - let products = results?.find((result) => result.type === 'products'); + let queries = results?.find((result) => result.type === "queries"); + let articles = results?.find((result) => result.type === "articles"); + let products = results?.find((result) => result.type === "products"); function goToSearchResult(event: React.MouseEvent) { let type = event.currentTarget.dataset.type; if (!searchInputRef.current) return; - if (type === 'SearchQuerySuggestion') { + if (type === "SearchQuerySuggestion") { searchInputRef.current.value = event.currentTarget.innerText; // dispatch event onchange for the search searchInputRef.current.focus(); } else { searchInputRef.current.blur(); - searchInputRef.current.value = ''; + searchInputRef.current.value = ""; // close the aside window.location.href = event.currentTarget.href; } @@ -35,49 +34,40 @@ export function PredictiveSearchResults() {
- {queries && ( -
- -
- )} - {articles && ( -
- -
- )} -
- {products && ( -
+
- {/* view all results /search?q=term */} - {searchTerm.current && ( - -

View all products

- - )}
- )} +
+ +
+
+
+ + {/* view all results /search?q=term */} + {searchTerm.current && ( + +

View all products

+ + )} +
); @@ -92,8 +82,8 @@ function NoPredictiveSearchResults({ return null; } return ( -

+

No results found for {searchTerm.current}

); -} \ No newline at end of file +} diff --git a/app/components/predictive-search/ResultItem.tsx b/app/components/predictive-search/ResultItem.tsx index 99d68797..6d809f8b 100644 --- a/app/components/predictive-search/ResultItem.tsx +++ b/app/components/predictive-search/ResultItem.tsx @@ -1,6 +1,6 @@ -import {Link} from '@remix-run/react'; -import {Image, Money, Pagination} from '@shopify/hydrogen'; -import {SearchResultItemProps} from './types'; +import { Link } from "@remix-run/react"; +import { Image, Money, Pagination } from "@shopify/hydrogen"; +import { SearchResultItemProps } from "./types"; export function SearchResultItem({ goToSearchResult, @@ -8,12 +8,17 @@ export function SearchResultItem({ }: SearchResultItemProps) { return (
  • - - {item.__typename === 'Product' && ( + + {item.__typename === "Product" && (
    {item.image?.url && ( {item.image.altText {item.title} diff --git a/app/components/predictive-search/SearchForm.tsx b/app/components/predictive-search/SearchForm.tsx index f4217993..553e15f5 100644 --- a/app/components/predictive-search/SearchForm.tsx +++ b/app/components/predictive-search/SearchForm.tsx @@ -8,8 +8,8 @@ import { useEffect, useRef } from "react"; export function PredictiveSearchForm({ action, children, - className = 'predictive-search-form', - method = 'POST', + className = "predictive-search-form", + method = "POST", ...props }: SearchFromProps) { const params = useParams(); @@ -17,22 +17,22 @@ export function PredictiveSearchForm({ const inputRef = useRef(null); function fetchResults(event: React.ChangeEvent) { - const searchAction = action ?? '/api/predictive-search'; + const searchAction = action ?? "/api/predictive-search"; const localizedAction = params.locale ? `/${params.locale}${searchAction}` : searchAction; - const newSearchTerm = event.target.value || ''; + const newSearchTerm = event.target.value || ""; fetcher.submit( - {q: newSearchTerm, limit: '6'}, - {method, action: localizedAction}, + { q: newSearchTerm, limit: "6" }, + { method, action: localizedAction }, ); } // ensure the passed input has a type of search, because SearchResults // will select the element based on the input useEffect(() => { - inputRef?.current?.setAttribute('type', 'search'); - inputRef?.current?.focus() + inputRef?.current?.setAttribute("type", "search"); + inputRef?.current?.focus(); }, []); return ( @@ -42,13 +42,13 @@ export function PredictiveSearchForm({ onSubmit={(event) => { event.preventDefault(); event.stopPropagation(); - if (!inputRef?.current || inputRef.current.value === '') { + if (!inputRef?.current || inputRef.current.value === "") { return; } inputRef.current.blur(); }} > - {children({fetchResults, inputRef, fetcher})} + {children({ fetchResults, inputRef, fetcher })} ); -} \ No newline at end of file +} diff --git a/app/components/predictive-search/types.ts b/app/components/predictive-search/types.ts index 26be47f9..98d671f8 100644 --- a/app/components/predictive-search/types.ts +++ b/app/components/predictive-search/types.ts @@ -3,7 +3,7 @@ import type { PredictiveArticleFragment, PredictiveCollectionFragment, PredictiveProductFragment, -} from 'storefrontapi.generated'; +} from "storefrontapi.generated"; export type UseSearchReturn = NormalizedPredictiveSearch & { searchInputRef: React.MutableRefObject; @@ -11,12 +11,12 @@ export type UseSearchReturn = NormalizedPredictiveSearch & { }; type PredicticeSearchResultItemImage = - | PredictiveCollectionFragment['image'] - | PredictiveArticleFragment['image'] - | PredictiveProductFragment['featuredImage']; + | PredictiveCollectionFragment["image"] + | PredictiveArticleFragment["image"] + | PredictiveProductFragment["featuredImage"]; type PredictiveSearchResultItemPrice = - PredictiveProductFragment['variants']['nodes'][0]['price']; + PredictiveProductFragment["variants"]["nodes"][0]["price"]; export type NormalizedPredictiveSearch = { results: NormalizedPredictiveSearchResults; @@ -24,11 +24,11 @@ export type NormalizedPredictiveSearch = { }; export type NormalizedPredictiveSearchResults = Array< - | {type: 'queries'; items: Array} - | {type: 'products'; items: Array} - | {type: 'collections'; items: Array} - | {type: 'pages'; items: Array} - | {type: 'articles'; items: Array} + | { type: "queries"; items: Array } + | { type: "products"; items: Array } + | { type: "collections"; items: Array } + | { type: "pages"; items: Array } + | { type: "articles"; items: Array } >; export type NormalizedPredictiveSearchResultItem = { @@ -46,12 +46,15 @@ export type NormalizedPredictiveSearchResultItem = { export type SearchResultTypeProps = { goToSearchResult: (event: React.MouseEvent) => void; - items: NormalizedPredictiveSearchResultItem[]; - searchTerm: UseSearchReturn['searchTerm']; - type: NormalizedPredictiveSearchResults[number]['type']; + items?: NormalizedPredictiveSearchResultItem[]; + searchTerm: UseSearchReturn["searchTerm"]; + type: NormalizedPredictiveSearchResults[number]["type"]; }; -export type SearchResultItemProps = Pick & { +export type SearchResultItemProps = Pick< + SearchResultTypeProps, + "goToSearchResult" +> & { item: NormalizedPredictiveSearchResultItem; }; @@ -62,9 +65,9 @@ type ChildrenRenderProps = { }; export type SearchFromProps = { - action?: FormProps['action']; - method?: FormProps['method']; + action?: FormProps["action"]; + method?: FormProps["method"]; className?: string; children: (passedProps: ChildrenRenderProps) => React.ReactNode; [key: string]: unknown; -}; \ No newline at end of file +}; diff --git a/app/components/predictive-search/usePredictiveSearch.ts b/app/components/predictive-search/usePredictiveSearch.ts index e5316509..3e44b615 100644 --- a/app/components/predictive-search/usePredictiveSearch.ts +++ b/app/components/predictive-search/usePredictiveSearch.ts @@ -1,23 +1,27 @@ import { useFetchers } from "@remix-run/react"; import { useEffect, useRef } from "react"; -import { NormalizedPredictiveSearch, NormalizedPredictiveSearchResults, UseSearchReturn } from "./types"; +import { + NormalizedPredictiveSearch, + NormalizedPredictiveSearchResults, + UseSearchReturn, +} from "./types"; export const NO_PREDICTIVE_SEARCH_RESULTS: NormalizedPredictiveSearchResults = [ - {type: 'queries', items: []}, - {type: 'products', items: []}, - {type: 'collections', items: []}, - {type: 'pages', items: []}, - {type: 'articles', items: []}, + { type: "queries", items: [] }, + { type: "products", items: [] }, + { type: "collections", items: [] }, + { type: "pages", items: [] }, + { type: "articles", items: [] }, ]; export function usePredictiveSearch(): UseSearchReturn { const fetchers = useFetchers(); - const searchTerm = useRef(''); + const searchTerm = useRef(""); const searchInputRef = useRef(null); const searchFetcher = fetchers.find((fetcher) => fetcher.data?.searchResults); - if (searchFetcher?.state === 'loading') { - searchTerm.current = (searchFetcher.formData?.get('q') || '') as string; + if (searchFetcher?.state === "loading") { + searchTerm.current = (searchFetcher.formData?.get("q") || "") as string; } const search = (searchFetcher?.data?.searchResults || { @@ -31,5 +35,5 @@ export function usePredictiveSearch(): UseSearchReturn { searchInputRef.current = document.querySelector('input[type="search"]'); }, []); - return {...search, searchInputRef, searchTerm}; -} \ No newline at end of file + return { ...search, searchInputRef, searchTerm }; +} diff --git a/app/lib/cn.ts b/app/lib/cn.ts index 706b5919..ed8bf498 100644 --- a/app/lib/cn.ts +++ b/app/lib/cn.ts @@ -1,6 +1,6 @@ -import { clsx, type ClassValue } from 'clsx' -import { twMerge as merge } from 'tailwind-merge' +import { clsx, type ClassValue } from "clsx"; +import { twMerge as merge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { - return merge(clsx(inputs)) + return merge(clsx(inputs)); } diff --git a/app/lib/const.ts b/app/lib/const.ts index ebaff64f..8d332132 100644 --- a/app/lib/const.ts +++ b/app/lib/const.ts @@ -1,7 +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 const FILTER_URL_PREFIX = "filter."; export function getImageLoadingPriority( index: number, diff --git a/app/lib/filter.ts b/app/lib/filter.ts index 1bea526f..13319026 100644 --- a/app/lib/filter.ts +++ b/app/lib/filter.ts @@ -1,7 +1,5 @@ -import type { - ProductFilter, -} from '@shopify/hydrogen/storefront-api-types'; -import type {Location, useLocation} from '@remix-run/react'; +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 = { @@ -10,12 +8,12 @@ export type AppliedFilter = { }; export type SortParam = - | 'price-low-high' - | 'price-high-low' - | 'best-selling' - | 'newest' - | 'featured' - | 'relevance'; + | "price-low-high" + | "price-high-low" + | "best-selling" + | "newest" + | "featured" + | "relevance"; export function getAppliedFilterLink( filter: AppliedFilter, @@ -35,7 +33,7 @@ export function getSortLink( params: URLSearchParams, location: Location, ) { - params.set('sort', sort); + params.set("sort", sort); return `${location.pathname}?${params.toString()}`; } @@ -49,13 +47,12 @@ export function getFilterLink( return `${location.pathname}?${newParams.toString()}`; } - export function filterInputToParams( rawInput: string | ProductFilter, params: URLSearchParams, ) { const input = - typeof rawInput === 'string' + typeof rawInput === "string" ? (JSON.parse(rawInput) as ProductFilter) : rawInput; @@ -63,7 +60,7 @@ export function filterInputToParams( if (params.has(`${FILTER_URL_PREFIX}${key}`, JSON.stringify(value))) { return; } - if (key === 'price') { + if (key === "price") { // For price, we want to overwrite params.set(`${FILTER_URL_PREFIX}${key}`, JSON.stringify(value)); } else { @@ -72,4 +69,4 @@ export function filterInputToParams( }); return params; -} \ No newline at end of file +} diff --git a/app/modules/AddToCartButton.tsx b/app/modules/AddToCartButton.tsx index 285b43d1..6b391ea4 100644 --- a/app/modules/AddToCartButton.tsx +++ b/app/modules/AddToCartButton.tsx @@ -1,5 +1,7 @@ -import type { CartLineInput } from "@shopify/hydrogen/storefront-api-types"; -import type { ShopifyAddToCartPayload } from "@shopify/hydrogen"; +import type { + OptimisticCartLine, + ShopifyAddToCartPayload, +} from "@shopify/hydrogen"; import { AnalyticsEventName, CartForm, @@ -23,7 +25,7 @@ export function AddToCartButton({ ...props }: { children: React.ReactNode; - lines: CartLineInput[]; + lines: OptimisticCartLine[]; className?: string; variant?: "primary" | "secondary" | "inline"; width?: "auto" | "full"; diff --git a/app/modules/Cart.tsx b/app/modules/Cart.tsx index 1a7eeab1..8e00de8e 100644 --- a/app/modules/Cart.tsx +++ b/app/modules/Cart.tsx @@ -2,30 +2,31 @@ import clsx from "clsx"; import { useRef } from "react"; import useScroll from "react-use/esm/useScroll"; import { - flattenConnection, CartForm, + type CartReturn, Image, Money, - useOptimisticData, + OptimisticCart, OptimisticInput, - type CartReturn, + useOptimisticCart, + useOptimisticData, } from "@shopify/hydrogen"; import type { Cart as CartType, - CartCost, - CartLine, CartLineUpdateInput, } from "@shopify/hydrogen/storefront-api-types"; import { Button, + FeaturedProducts, Heading, IconRemove, - Text, Link, - FeaturedProducts, + Text, } from "~/modules"; import { getInputStyleClasses } from "~/lib/utils"; +import { CartApiQueryFragment } from "storefrontapi.generated"; +type CartLine = OptimisticCart["lines"]["nodes"][0]; type Layouts = "page" | "drawer"; @@ -38,12 +39,14 @@ export function Cart({ onClose?: () => void; cart: CartReturn | null; }) { - const linesCount = Boolean(cart?.lines?.edges?.length || 0); + let optimisticCart = useOptimisticCart(cart); + + const linesCount = Boolean(optimisticCart?.lines?.nodes?.length || 0); return ( <>