Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add collection template #82

Merged
merged 1 commit into from
Jan 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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