Skip to content

Commit

Permalink
Merge pull request #82 from buildheadless/collection
Browse files Browse the repository at this point in the history
Add collection template
  • Loading branch information
thomasKn authored Jan 12, 2024
2 parents 1cc35fe + 737b544 commit 1bf0f7f
Show file tree
Hide file tree
Showing 25 changed files with 863 additions and 333 deletions.
2 changes: 2 additions & 0 deletions templates/hydrogen-theme/app/components/CmsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {Suspense, useMemo} from 'react';

import type {FOOTERS_FRAGMENT} from '~/qroq/footers';
import type {
COLLECTION_SECTIONS_FRAGMENT,
PRODUCT_SECTIONS_FRAGMENT,
SECTIONS_FRAGMENT,
} from '~/qroq/sections';
Expand All @@ -14,6 +15,7 @@ import {useSettingsCssVars} from '~/hooks/useSettingsCssVars';
import {sections} from '~/lib/sectionRelsolver';

type CmsSectionsProps =
| NonNullable<InferType<typeof COLLECTION_SECTIONS_FRAGMENT>>[0]
| NonNullable<InferType<typeof FOOTERS_FRAGMENT>>
| NonNullable<InferType<typeof PRODUCT_SECTIONS_FRAGMENT>>[0]
| NonNullable<InferType<typeof SECTIONS_FRAGMENT>>[0];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,11 @@ export type SortParam =
type Props = {
appliedFilters?: AppliedFilter[];
children: React.ReactNode;
collections?: Array<{handle: string; title: string}>;
filters: Filter[];
};
export const FILTER_URL_PREFIX = 'filter.';

export function SortFilter({
appliedFilters = [],
children,
collections = [],
filters,
}: Props) {
export function SortFilter({appliedFilters = [], children, filters}: Props) {
const [isOpen, setIsOpen] = useState(false);
return (
<>
Expand All @@ -71,7 +65,7 @@ export function SortFilter({
</button>
<SortMenu />
</div>
<div className="flex flex-col flex-wrap md:flex-row">
<div className="flex flex-row flex-wrap">
<div
className={`transition-all duration-200 ${
isOpen
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ export const cardClassName = cx('overflow-hidden rounded-lg border');

export function ProductCard(props: {
className?: string;
columns?: null | number;
columns?: {
desktop?: null | number;
mobile?: null | number;
};
product?: ProductCardFragment;
skeleton?: {
cardsNumber?: number;
Expand All @@ -20,8 +23,8 @@ export function ProductCard(props: {
: null;
const sizes = cx([
'(min-width: 1024px)',
columns ? `${100 / columns}vw,` : '33vw,',
'100vw',
columns?.desktop ? `${100 / columns.desktop}vw,` : '33vw,',
columns?.mobile ? `${100 / columns.mobile}vw` : '100vw',
]);

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,36 +6,51 @@ import {cx} from 'class-variance-authority';
import {ProductCard} from './ProductCard';

export function ProductCardGrid(props: {
columns?: null | number;
columns?: {
desktop?: null | number;
mobile?: null | number;
};
products?: ProductCardFragment[];
skeleton?: {
cardsNumber?: number;
};
}) {
const {products, skeleton} = props;
const columnsVar = {
'--columns': props.columns ?? 3,
'--columns': props.columns?.desktop ?? 3,
'--mobileColumns': props.columns?.mobile ?? 1,
} as CSSProperties;

return (
<ul
className={cx([
'grid gap-6',
'grid-cols-[repeat(var(--mobileColumns),_minmax(0,_1fr))]',
'sm:grid-cols-2',
'lg:grid-cols-[repeat(var(--columns),_minmax(0,_1fr))]',
])}
style={columnsVar}
>
{!skeleton && products && products.length > 0
? products.map((product) => (
<li key={product.id}>
<ProductCard columns={props.columns} product={product} />
<ProductCard
columns={{
desktop: props.columns?.desktop,
mobile: props.columns?.mobile,
}}
product={product}
/>
</li>
))
: skeleton
? [...Array(skeleton.cardsNumber ?? 3)].map((_, i) => (
<li key={i}>
<ProductCard
columns={props.columns}
columns={{
desktop: props.columns?.desktop,
mobile: props.columns?.mobile,
}}
skeleton={{
cardsNumber: skeleton.cardsNumber,
}}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type {TypeFromSelection} from 'groqd';

import {useLoaderData} from '@remix-run/react';
import {Image} from '@shopify/hydrogen';

import type {SectionDefaultProps} from '~/lib/type';
import type {COLLECTION_BANNER_SECTION_FRAGMENT} from '~/qroq/sections';
import type {loader} from '~/routes/($locale).collections.$collectionHandle';

type CollectionBannerSectionProps = TypeFromSelection<
typeof COLLECTION_BANNER_SECTION_FRAGMENT
>;

export function CollectionBannerSection(
props: SectionDefaultProps & {data: CollectionBannerSectionProps},
) {
const loaderData = useLoaderData<typeof loader>();
const collection = loaderData.collection;

return collection ? (
<section>
{/* Todo => add settings for banner height */}
{/* Todo => add setting to add overlay */}
{/* Todo => add settings for text and content alignment */}
<div className="relative h-80 w-full overflow-hidden">
{props.data.showImage && collection.image && (
<Image
aspectRatio="16/9"
className="h-auto"
crop="center"
data={collection.image}
loading="eager"
sizes="100vw"
/>
)}
<div className="absolute inset-0">
<div className="flex h-full items-center justify-center">
<div className="flex flex-col gap-1 text-center">
<h1>{collection.title}</h1>
{props.data.showDescription && <p>{collection.description}</p>}
</div>
</div>
</div>
</div>
</section>
) : null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import type {Filter} from '@shopify/hydrogen/storefront-api-types';
import type {TypeFromSelection} from 'groqd';
import type {
CollectionProductGridQuery,
ProductCardFragment,
} from 'storefrontapi.generated';

import {
Await,
useLoaderData,
useNavigate,
useSearchParams,
} from '@remix-run/react';
import {Pagination, flattenConnection} from '@shopify/hydrogen';
import {Suspense, useEffect} from 'react';

import type {SectionDefaultProps} from '~/lib/type';
import type {COLLECTION_PRODUCT_GRID_SECTION_FRAGMENT} from '~/qroq/sections';
import type {loader} from '~/routes/($locale).collections.$collectionHandle';

import {useLocale} from '~/hooks/useLocale';
import {getAppliedFilters} from '~/lib/shopifyCollection';

import {SortFilter} from '../collection/SortFilter';
import {ProductCardGrid} from '../product/ProductCardGrid';

type CollectionProductGridSectionProps = TypeFromSelection<
typeof COLLECTION_PRODUCT_GRID_SECTION_FRAGMENT
>;

export type ShopifyCollection = CollectionProductGridQuery['collection'];

export function CollectionProductGridSection(
props: SectionDefaultProps & {data: CollectionProductGridSectionProps},
) {
const locale = useLocale();
const [searchParams] = useSearchParams();
const loaderData = useLoaderData<typeof loader>();
const collectionProductGridPromise = loaderData?.collectionProductGridPromise;
const columns = props.data.desktopColumns;
const mobileColumns = props.data.mobileColumns;

// Todo => Add skeleton and errorElement
return (
<Suspense fallback="loading...">
<Await
errorElement={<div>Error</div>}
resolve={collectionProductGridPromise}
>
{(result) => {
const collection = result?.collection as ShopifyCollection;

if (!collection) {
return null;
}

const appliedFilters = getAppliedFilters({
collection,
locale,
searchParams,
});

return (
<div className="container">
<SortFilter
appliedFilters={appliedFilters}
filters={collection?.products.filters as Filter[]}
>
<Pagination connection={collection?.products}>
{({
NextLink,
PreviousLink,
hasNextPage,
isLoading,
nextPageUrl,
nodes,
state,
}) => (
<>
<div className="mb-6 flex items-center justify-center">
<PreviousLink>
{isLoading ? 'Loading...' : 'Load previous'}
</PreviousLink>
</div>
<ProductsLoadedOnScroll
columns={{
desktop: columns,
mobile: mobileColumns,
}}
hasNextPage={hasNextPage}
inView={true}
nextPageUrl={nextPageUrl}
nodes={nodes}
state={state}
/>
<div className="mt-6 flex items-center justify-center">
<NextLink>
{isLoading ? 'Loading...' : 'Load more products'}
</NextLink>
</div>
</>
)}
</Pagination>
</SortFilter>
</div>
);
}}
</Await>
</Suspense>
);
}

function ProductsLoadedOnScroll({
columns,
hasNextPage,
inView,
nextPageUrl,
nodes,
state,
}: {
columns?: {
desktop?: null | number;
mobile?: null | number;
};
hasNextPage: boolean;
inView: boolean;
nextPageUrl: string;
nodes: ProductCardFragment[];
state: unknown;
}) {
const navigate = useNavigate();

useEffect(() => {
if (inView && hasNextPage) {
navigate(nextPageUrl, {
preventScrollReset: true,
replace: true,
state,
});
}
}, [inView, navigate, state, nextPageUrl, hasNextPage]);

return (
<ProductCardGrid
columns={{
desktop: columns?.desktop,
mobile: columns?.mobile,
}}
products={nodes}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ export function FeaturedCollectionSection(
>
{(products) => (
<ProductCardGrid
columns={props.data.desktopColumns}
columns={{
desktop: props.data.desktopColumns,
}}
products={products}
/>
)}
Expand All @@ -52,7 +54,9 @@ function Skeleton(props: {cardsNumber: number; columns: number}) {
return (
<div aria-hidden className="animate-pulse">
<ProductCardGrid
columns={props.columns}
columns={{
desktop: props.columns,
}}
skeleton={{
cardsNumber: props.cardsNumber,
}}
Expand Down
Loading

0 comments on commit 1bf0f7f

Please sign in to comment.