From a7553ce9a8cb0ef39ca7ade2905351568d8bd5ca Mon Sep 17 00:00:00 2001 From: Benjamin Sehl Date: Tue, 21 May 2024 23:17:04 -0400 Subject: [PATCH] Finish basic layout of homepage --- app/components/hydrogen/ProductCard.jsx | 6 +- app/components/hydrogen/new/Layout.tsx | 53 +++--- app/routes/_index.jsx | 156 ----------------- app/routes/_index/route.jsx | 43 +++++ app/routes/_index/sections/best-sellers.jsx | 26 +++ app/routes/_index/sections/collections.jsx | 163 ++++++++++++++++++ .../_index/sections/featured-products.jsx | 77 +++++++++ app/routes/_index/sections/hero.jsx | 33 ++++ app/routes/_index/sections/our-promise.jsx | 57 ++++++ app/routes/products.$handle/route.jsx | 4 +- .../sections/highlight-solution.jsx | 11 +- .../products.$handle/sections/recommended.jsx | 6 +- .../products.$handle/sections/reviews.jsx | 2 +- app/styles/app.css | 13 +- storefrontapi.generated.d.ts | 79 --------- tailwind.config.js | 3 - 16 files changed, 450 insertions(+), 282 deletions(-) delete mode 100644 app/routes/_index.jsx create mode 100644 app/routes/_index/route.jsx create mode 100644 app/routes/_index/sections/best-sellers.jsx create mode 100644 app/routes/_index/sections/collections.jsx create mode 100644 app/routes/_index/sections/featured-products.jsx create mode 100644 app/routes/_index/sections/hero.jsx create mode 100644 app/routes/_index/sections/our-promise.jsx diff --git a/app/components/hydrogen/ProductCard.jsx b/app/components/hydrogen/ProductCard.jsx index 221b96e..e0dc19c 100644 --- a/app/components/hydrogen/ProductCard.jsx +++ b/app/components/hydrogen/ProductCard.jsx @@ -1,10 +1,10 @@ -import clsx from 'clsx'; import {flattenConnection, Image} from '@shopify/hydrogen'; import {Text} from '@h2/Text'; import Link from '@h2/Link'; import {Button, AddToCartButton} from '@h2/Button'; import {Price, PriceCompareAt} from './Price'; +import {cx} from './new/cva.config'; export function ProductCard({product, className, loading, onClick, quickAdd}) { product = { @@ -42,13 +42,13 @@ export function ProductCard({product, className, loading, onClick, quickAdd}) { const {image} = firstVariant; return ( -
+
-
+
{image && ( ( ({as: Component = 'div', children, className = '', ...props}, ref) => { - const classes = cx(flex(props), className); + const classes = cx( + flex(props), + props.wrap ? 'flex-wrap' : 'flex-nowrap', + className, + ); return ( @@ -206,6 +207,13 @@ const grid = cva({ 3: 'grid-cols-3', 4: 'grid-cols-4', 5: 'grid-cols-5', + 6: 'grid-cols-6', + 7: 'grid-cols-7', + 8: 'grid-cols-8', + 9: 'grid-cols-9', + 10: 'grid-cols-10', + 11: 'grid-cols-11', + 12: 'grid-cols-12', }, rows: { 1: 'grid-rows-1', @@ -213,6 +221,13 @@ const grid = cva({ 3: 'grid-rows-3', 4: 'grid-rows-4', 5: 'grid-rows-5', + 6: 'grid-rows-6', + 7: 'grid-rows-7', + 8: 'grid-rows-8', + 9: 'grid-rows-9', + 10: 'grid-rows-10', + 11: 'grid-rows-11', + 12: 'grid-rows-12', }, gap: { 1: 'gap-1', @@ -321,10 +336,6 @@ const container = cva({ 'z-10', ], variants: { - fluid: { - true: 'max-w-none', - false: 'max-w-7xl', - }, resizeX: { hug: 'w-auto', fill: 'w-full', @@ -337,7 +348,6 @@ const container = cva({ }, }, defaultVariants: { - fluid: false, resizeX: 'hug', resizeY: 'hug', }, @@ -345,8 +355,11 @@ const container = cva({ export const Container = React.forwardRef( ({as: Component = 'div', children, className, ...props}, ref) => { - const classes = cx(container(props), className); - + const classes = cx( + container(props), + props.fluid ? 'max-w-none' : 'max-w-7xl', + className, + ); return ( {children} @@ -359,25 +372,17 @@ interface SectionProps { as?: React.ElementType; children?: React.ReactNode; className?: string; - padded?: boolean; // Controls whether the section has padding + padded?: boolean; } const section = cva({ base: ['w-full', 'relative', 'min-h-8'], - variants: { - padded: { - true: 'py-8', // Tailwind classes for padding (you can change as needed) - false: '', - }, - }, - defaultVariants: { - padded: true, - }, + variants: {}, }); export const Section = React.forwardRef( - ({as: Component = 'section', children, className, ...props}, ref) => { - const classes = cx(section(props), className); + ({as: Component = 'section', padded, children, className, ...props}, ref) => { + const classes = cx(section(props), padded && 'py-8', className); return ( diff --git a/app/routes/_index.jsx b/app/routes/_index.jsx deleted file mode 100644 index b40e48f..0000000 --- a/app/routes/_index.jsx +++ /dev/null @@ -1,156 +0,0 @@ -import {defer} from '@shopify/remix-oxygen'; -import {Await, useLoaderData, Link} from '@remix-run/react'; -import {Suspense} from 'react'; -import {Image, Money} from '@shopify/hydrogen'; - -/** - * @type {MetaFunction} - */ -export const meta = () => { - return [{title: 'Hydrogen | Home'}]; -}; - -/** - * @param {LoaderFunctionArgs} - */ -export async function loader({context}) { - const {storefront} = context; - const {collections} = await storefront.query(FEATURED_COLLECTION_QUERY); - const featuredCollection = collections.nodes[0]; - const recommendedProducts = storefront.query(RECOMMENDED_PRODUCTS_QUERY); - - return defer({featuredCollection, recommendedProducts}); -} - -export default function Homepage() { - /** @type {LoaderReturnData} */ - const data = useLoaderData(); - return ( -
- - -
- ); -} - -/** - * @param {{ - * collection: FeaturedCollectionFragment; - * }} - */ -function FeaturedCollection({collection}) { - if (!collection) return null; - const image = collection?.image; - return ( - - {image && ( -
- -
- )} -

{collection.title}

- - ); -} - -/** - * @param {{ - * products: Promise; - * }} - */ -function RecommendedProducts({products}) { - return ( -
-

Recommended Products

- Loading...
}> - - {({products}) => ( -
- {products.nodes.map((product) => ( - - -

{product.title}

- - - - - ))} -
- )} -
- -
-
- ); -} - -const FEATURED_COLLECTION_QUERY = `#graphql - fragment FeaturedCollection on Collection { - id - title - image { - id - url - altText - width - height - } - handle - } - query FeaturedCollection($country: CountryCode, $language: LanguageCode) - @inContext(country: $country, language: $language) { - collections(first: 1, sortKey: UPDATED_AT, reverse: true) { - nodes { - ...FeaturedCollection - } - } - } -`; - -const RECOMMENDED_PRODUCTS_QUERY = `#graphql - fragment RecommendedProduct on Product { - id - title - handle - priceRange { - minVariantPrice { - amount - currencyCode - } - } - images(first: 1) { - nodes { - id - url - altText - width - height - } - } - } - query RecommendedProducts ($country: CountryCode, $language: LanguageCode) - @inContext(country: $country, language: $language) { - products(first: 4, sortKey: UPDATED_AT, reverse: true) { - nodes { - ...RecommendedProduct - } - } - } -`; - -/** @typedef {import('@shopify/remix-oxygen').LoaderFunctionArgs} LoaderFunctionArgs */ -/** @template T @typedef {import('@remix-run/react').MetaFunction} MetaFunction */ -/** @typedef {import('storefrontapi.generated').FeaturedCollectionFragment} FeaturedCollectionFragment */ -/** @typedef {import('storefrontapi.generated').RecommendedProductsQuery} RecommendedProductsQuery */ -/** @typedef {import('@shopify/remix-oxygen').SerializeFrom} LoaderReturnData */ diff --git a/app/routes/_index/route.jsx b/app/routes/_index/route.jsx new file mode 100644 index 0000000..82c25c6 --- /dev/null +++ b/app/routes/_index/route.jsx @@ -0,0 +1,43 @@ +import {defer} from '@shopify/remix-oxygen'; +import {Await, useLoaderData, Link} from '@remix-run/react'; +import {Suspense} from 'react'; +import {Image, Money} from '@shopify/hydrogen'; +import Hero from './sections/hero'; +import BestSellers from './sections/best-sellers'; +import Collections from './sections/collections'; +import FeaturedProducts from './sections/featured-products'; +import OurPromise from './sections/our-promise'; + +/** + * @type {MetaFunction} + */ +export const meta = () => { + return [{title: 'Hydrogen | Home'}]; +}; + +/** + * @param {LoaderFunctionArgs} + */ +export async function loader({context: {storefront}}) { + return null; +} + +export default function Homepage() { + /** @type {LoaderReturnData} */ + const data = useLoaderData(); + return ( + <> + + + + + + + ); +} + +/** @typedef {import('@shopify/remix-oxygen').LoaderFunctionArgs} LoaderFunctionArgs */ +/** @template T @typedef {import('@remix-run/react').MetaFunction} MetaFunction */ +/** @typedef {import('storefrontapi.generated').FeaturedCollectionFragment} FeaturedCollectionFragment */ +/** @typedef {import('storefrontapi.generated').RecommendedProductsQuery} RecommendedProductsQuery */ +/** @typedef {import('@shopify/remix-oxygen').SerializeFrom} LoaderReturnData */ diff --git a/app/routes/_index/sections/best-sellers.jsx b/app/routes/_index/sections/best-sellers.jsx new file mode 100644 index 0000000..92eb3cf --- /dev/null +++ b/app/routes/_index/sections/best-sellers.jsx @@ -0,0 +1,26 @@ +import {ProductCard} from '@h2/ProductCard'; +import {Heading} from '@h2/Text'; +import {Container, Grid, Section} from '@h2/new/Layout'; + +export default function BestSellers() { + return ( +
+ + + Best + Sellers + + + + + + + + + + + + +
+ ); +} diff --git a/app/routes/_index/sections/collections.jsx b/app/routes/_index/sections/collections.jsx new file mode 100644 index 0000000..8dff9d7 --- /dev/null +++ b/app/routes/_index/sections/collections.jsx @@ -0,0 +1,163 @@ +import Link from '@h2/Link'; +import {Heading, Text} from '@h2/Text'; +import {Background, Container, Flex, Grid, Section} from '@h2/new/Layout'; + +export default function Collections() { + return ( + + + + Something +
+ + For Everyone + +
+
+
+ + + + + + Apparel + + + + + + + + Bags + + + + + + + + Tech + + + + + + + + Misc + + + + + + + + + +
+
+ ); +} + +function Globe({className}) { + return ( + + + + ); +} + +function ApparelBlob({className}) { + return ( + + + + ); +} + +function BagsBlob({className}) { + return ( + + + + ); +} +function TechBlob({className}) { + return ( + + + + ); +} +function MiscBlob({className}) { + return ( + + + + ); +} diff --git a/app/routes/_index/sections/featured-products.jsx b/app/routes/_index/sections/featured-products.jsx new file mode 100644 index 0000000..611c894 --- /dev/null +++ b/app/routes/_index/sections/featured-products.jsx @@ -0,0 +1,77 @@ +import {Button} from '@h2/Button'; +import {Heading, Text} from '@h2/Text'; +import {Flex, Grid, Section} from '@h2/new/Layout'; +import {Image} from '@shopify/hydrogen'; + +export default function FeaturedProducts() { + return ( +
+ + + + + Builders Tote + + Sofie Pavitt Face is an intentional edit of skincare essentials + designed for acne-prone skin. + + + + +
+ +
+
+ +
+ +
+ + + + Builders Tote + + Sofie Pavitt Face is an intentional edit of skincare essentials + designed for acne-prone skin. + + + + +
+
+ ); +} diff --git a/app/routes/_index/sections/hero.jsx b/app/routes/_index/sections/hero.jsx new file mode 100644 index 0000000..616f064 --- /dev/null +++ b/app/routes/_index/sections/hero.jsx @@ -0,0 +1,33 @@ +import {Button} from '@h2/Button'; +import {Heading, Text} from '@h2/Text'; +import {Background, Container, Flex, Section} from '@h2/new/Layout'; +import {Image} from '@shopify/hydrogen'; + +export default function Hero() { + return ( +
+ +
+
+ +
+
+ + + + + Summer 2024 + + + Building Essentials + + + + + +
+ ); +} diff --git a/app/routes/_index/sections/our-promise.jsx b/app/routes/_index/sections/our-promise.jsx new file mode 100644 index 0000000..f82f36c --- /dev/null +++ b/app/routes/_index/sections/our-promise.jsx @@ -0,0 +1,57 @@ +import {Heading, Text} from '@h2/Text'; +import {Container, Flex, Grid, Section} from '@h2/new/Layout'; + +export default function OurPromise() { + return ( +
+ + + Our +
+ Promise +
+
+ + + +
+ Guaranteed For Life + + Every one of our products comes with a limited lifetime warranty. + +
+ +
+ 100 days to try + + Try it out for 100 days. If you don’t love it, send it back. + +
+ +
+ Free shipping + + We’ll cover shipping on all orders to the contiguous US and + Canada. + +
+
+
+
+ ); +} diff --git a/app/routes/products.$handle/route.jsx b/app/routes/products.$handle/route.jsx index 223321d..704825e 100644 --- a/app/routes/products.$handle/route.jsx +++ b/app/routes/products.$handle/route.jsx @@ -68,7 +68,7 @@ export async function loader({params, request, context}) { export default function Product() { /** @type {LoaderReturnData} */ return ( -
+ <> @@ -76,7 +76,7 @@ export default function Product() { - + ); } diff --git a/app/routes/products.$handle/sections/highlight-solution.jsx b/app/routes/products.$handle/sections/highlight-solution.jsx index 28c77a4..ef12b31 100644 --- a/app/routes/products.$handle/sections/highlight-solution.jsx +++ b/app/routes/products.$handle/sections/highlight-solution.jsx @@ -21,6 +21,7 @@ export default function HighlightSolution() {
- +
+ +
diff --git a/app/routes/products.$handle/sections/recommended.jsx b/app/routes/products.$handle/sections/recommended.jsx index baa69c5..5acf29a 100644 --- a/app/routes/products.$handle/sections/recommended.jsx +++ b/app/routes/products.$handle/sections/recommended.jsx @@ -1,6 +1,6 @@ import {ProductCard} from '@h2/ProductCard'; import {Heading} from '@h2/Text'; -import {Container, Section} from '@h2/new/Layout'; +import {Container, Grid, Section} from '@h2/new/Layout'; const mockProducts = { nodes: new Array(12).fill(''), @@ -21,7 +21,7 @@ export default function Recommended({ also like -
+ {products.nodes.map((product) => ( ))} -
+ ); } diff --git a/app/routes/products.$handle/sections/reviews.jsx b/app/routes/products.$handle/sections/reviews.jsx index 77922d1..92b3f1a 100644 --- a/app/routes/products.$handle/sections/reviews.jsx +++ b/app/routes/products.$handle/sections/reviews.jsx @@ -45,7 +45,7 @@ const reviews = [ export default function Reviews({data = reviews}) { return (
- + Don’t take our word for it diff --git a/app/styles/app.css b/app/styles/app.css index 2da87d3..90c028b 100644 --- a/app/styles/app.css +++ b/app/styles/app.css @@ -28,6 +28,8 @@ --button-radius: 1rem; --card-radius: 0.625rem; + + --container-width: 80rem; } @layer base { @@ -42,19 +44,16 @@ @layer components { .swimlane { - /* @apply mx-auto px-4 md:px-8 lg:px-10 relative z-10 max-w-7xl w-auto h-auto py-16 */ - display: grid; width: 100%; scroll-snap-type: x mandatory; grid-auto-flow: column; justify-content: flex-start; - gap: 1rem; overflow-x: scroll; padding-bottom: 1rem; - padding-left: max(1rem, calc((100vw - 80rem + 2rem) / 2)); - padding-right: max(1rem, calc((100vw - 80rem + 2rem) / 2)); - scroll-padding-left: max(1rem, calc((100vw - 80rem + 2rem) / 2)); - scroll-padding-right: max(1rem, calc((100vw - 80rem + 2rem) / 2)); + padding-left: max(1rem, calc((100vw - var(--container-width) + 2rem) / 2)); + padding-right: max(1rem, calc((100vw - var(--container-width) + 2rem) / 2)); + scroll-padding-left: max(1rem, calc((100vw - var(--container-width) + 2rem) / 2)); + scroll-padding-right: max(1rem, calc((100vw - var(--container-width) + 2rem) / 2)); scrollbar-width: none; &::-webkit-scrollbar { display: none; diff --git a/storefrontapi.generated.d.ts b/storefrontapi.generated.d.ts index 5640531..c3ad008 100644 --- a/storefrontapi.generated.d.ts +++ b/storefrontapi.generated.d.ts @@ -324,77 +324,6 @@ export type SitemapQuery = { }; }; -export type FeaturedCollectionFragment = Pick< - StorefrontAPI.Collection, - 'id' | 'title' | 'handle' -> & { - image?: StorefrontAPI.Maybe< - Pick - >; -}; - -export type FeaturedCollectionQueryVariables = StorefrontAPI.Exact<{ - country?: StorefrontAPI.InputMaybe; - language?: StorefrontAPI.InputMaybe; -}>; - -export type FeaturedCollectionQuery = { - collections: { - nodes: Array< - Pick & { - image?: StorefrontAPI.Maybe< - Pick< - StorefrontAPI.Image, - 'id' | 'url' | 'altText' | 'width' | 'height' - > - >; - } - >; - }; -}; - -export type RecommendedProductFragment = Pick< - StorefrontAPI.Product, - 'id' | 'title' | 'handle' -> & { - priceRange: { - minVariantPrice: Pick; - }; - images: { - nodes: Array< - Pick - >; - }; -}; - -export type RecommendedProductsQueryVariables = StorefrontAPI.Exact<{ - country?: StorefrontAPI.InputMaybe; - language?: StorefrontAPI.InputMaybe; -}>; - -export type RecommendedProductsQuery = { - products: { - nodes: Array< - Pick & { - priceRange: { - minVariantPrice: Pick< - StorefrontAPI.MoneyV2, - 'amount' | 'currencyCode' - >; - }; - images: { - nodes: Array< - Pick< - StorefrontAPI.Image, - 'id' | 'url' | 'altText' | 'width' | 'height' - > - >; - }; - } - >; - }; -}; - export type PredictiveArticleFragment = {__typename: 'Article'} & Pick< StorefrontAPI.Article, 'id' | 'title' | 'handle' | 'trackingParameters' @@ -1208,14 +1137,6 @@ interface GeneratedQueryTypes { return: SitemapQuery; variables: SitemapQueryVariables; }; - '#graphql\n fragment FeaturedCollection on Collection {\n id\n title\n image {\n id\n url\n altText\n width\n height\n }\n handle\n }\n query FeaturedCollection($country: CountryCode, $language: LanguageCode)\n @inContext(country: $country, language: $language) {\n collections(first: 1, sortKey: UPDATED_AT, reverse: true) {\n nodes {\n ...FeaturedCollection\n }\n }\n }\n': { - return: FeaturedCollectionQuery; - variables: FeaturedCollectionQueryVariables; - }; - '#graphql\n fragment RecommendedProduct on Product {\n id\n title\n handle\n priceRange {\n minVariantPrice {\n amount\n currencyCode\n }\n }\n images(first: 1) {\n nodes {\n id\n url\n altText\n width\n height\n }\n }\n }\n query RecommendedProducts ($country: CountryCode, $language: LanguageCode)\n @inContext(country: $country, language: $language) {\n products(first: 4, sortKey: UPDATED_AT, reverse: true) {\n nodes {\n ...RecommendedProduct\n }\n }\n }\n': { - return: RecommendedProductsQuery; - variables: RecommendedProductsQueryVariables; - }; '#graphql\n fragment PredictiveArticle on Article {\n __typename\n id\n title\n handle\n image {\n url\n altText\n width\n height\n }\n trackingParameters\n }\n fragment PredictiveCollection on Collection {\n __typename\n id\n title\n handle\n image {\n url\n altText\n width\n height\n }\n trackingParameters\n }\n fragment PredictivePage on Page {\n __typename\n id\n title\n handle\n trackingParameters\n }\n fragment PredictiveProduct on Product {\n __typename\n id\n title\n handle\n trackingParameters\n variants(first: 1) {\n nodes {\n id\n image {\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n }\n }\n }\n fragment PredictiveQuery on SearchQuerySuggestion {\n __typename\n text\n styledText\n trackingParameters\n }\n query predictiveSearch(\n $country: CountryCode\n $language: LanguageCode\n $limit: Int!\n $limitScope: PredictiveSearchLimitScope!\n $searchTerm: String!\n $types: [PredictiveSearchType!]\n ) @inContext(country: $country, language: $language) {\n predictiveSearch(\n limit: $limit,\n limitScope: $limitScope,\n query: $searchTerm,\n types: $types,\n ) {\n articles {\n ...PredictiveArticle\n }\n collections {\n ...PredictiveCollection\n }\n pages {\n ...PredictivePage\n }\n products {\n ...PredictiveProduct\n }\n queries {\n ...PredictiveQuery\n }\n }\n }\n': { return: PredictiveSearchQuery; variables: PredictiveSearchQueryVariables; diff --git a/tailwind.config.js b/tailwind.config.js index a0cb973..5cea8b6 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -45,9 +45,6 @@ export default { 'calc(var(--screen-height, 100vh) - var(--height-nav))', 'screen-dynamic': 'var(--screen-height-dynamic, 100vh)', }, - width: { - mobileGallery: 'calc(100vw - 3rem)', - }, fontFamily: { sans: ['Helvetica Neue', 'ui-sans-serif', 'system-ui', 'sans-serif'], serif: ['"IBMPlexSerif"', 'Palatino', 'ui-serif'],