diff --git a/app/components/hydrogen/new/Accordion.jsx b/app/components/hydrogen/new/Accordion.jsx index fed4567..43472cb 100644 --- a/app/components/hydrogen/new/Accordion.jsx +++ b/app/components/hydrogen/new/Accordion.jsx @@ -1,14 +1,19 @@ -import {useState} from 'react'; -import {Disclosure, DisclosureButton, DisclosurePanel} from '@headlessui/react'; -import {Text} from '@h2/Text'; +import React from 'react' +import { useState } from 'react' +import { + Disclosure, + DisclosureButton, + DisclosurePanel, +} from '@headlessui/react' +import { Text } from '/app/components/hydrogen/Text' -const Accordion = ({data}) => { - const [openPanel, setOpenPanel] = useState(null); +const Accordion = ({ data }) => { + const [openPanel, setOpenPanel] = useState(null) const handleToggle = (index) => { // Toggle functionality to open or close panels - setOpenPanel(openPanel === index ? null : index); - }; + setOpenPanel(openPanel === index ? null : index) + } return (
@@ -18,9 +23,9 @@ const Accordion = ({data}) => { open={openPanel === index} onChange={() => handleToggle(index)} > - {({open}) => ( + {({ open }) => ( <> - + {item.title} { × - {item.content} + + {item.content} + )} ))}
- ); -}; + ) +} -export default Accordion; +export default Accordion diff --git a/app/components/hydrogen/new/Layout.tsx b/app/components/hydrogen/new/Layout.jsx similarity index 51% rename from app/components/hydrogen/new/Layout.tsx rename to app/components/hydrogen/new/Layout.jsx index e5ecce5..6da3f7d 100644 --- a/app/components/hydrogen/new/Layout.tsx +++ b/app/components/hydrogen/new/Layout.jsx @@ -1,63 +1,7 @@ -import React from 'react'; +import React from 'react' // Import configured class names generator functions with predetermined styles. -import {cx, cva} from './cva.config'; - -/** - * Type definitions for the Flex component props enriched with Tailwind CSS class and CSS mapping details. - */ -interface FlexProps { - as?: React.ElementType; - asChild?: boolean; - children?: React.ReactNode; - className?: string; - /** - * Controls the flex-direction CSS property. - * - `'row'`: `flex-row` (CSS: `flex-direction: row`) - * - `'column'`: `flex-col` (CSS: `flex-direction: column`) - */ - direction?: 'row' | 'column'; - /** - * Controls the align-items CSS property. - * - `'start'`: `items-start` (CSS: `align-items: flex-start`) - * - `'center'`: `items-center` (CSS: `align-items: center`) - * - `'end'`: `items-end` (CSS: `align-items: flex-end`) - * - `'baseline'`: `items-baseline` (CSS: `align-items: baseline`) - * - `'stretch'`: `items-stretch` (CSS: `align-items: stretch`) - */ - align?: 'start' | 'center' | 'end' | 'baseline' | 'stretch'; - /** - * Controls the justify-content CSS property. - * - `'center'`: `justify-center` (CSS: `justify-content: center`) - * - `'start'`: `justify-start` (CSS: `justify-content: flex-start`) - * - `'end'`: `justify-end` (CSS: `justify-content: flex-end`) - * - `'between'`: `justify-between` (CSS: `justify-content: space-between`) - */ - justify?: 'center' | 'start' | 'end' | 'between'; - /** - * Controls the gap CSS property. - * - `1`: `gap-1` (CSS: `gap: 0.25rem`) - * - `2`: `gap-2` (CSS: `gap: 0.5rem`) - * - `3`: `gap-3` (CSS: `gap: 0.75rem`) - * - `4`: `gap-4` (CSS: `gap: 1rem`) - * - `5`: `gap-5` (CSS: `gap: 1.25rem`) - */ - gap?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8; - /** - * Controls the horizontal resizing behavior. - * - `'hug'`: `w-auto` (CSS: `width: auto`) - * - `'fill'`: `w-full` (CSS: `width: 100%`) - * - `'fixed'`: No additional class (CSS: `width: auto`) - */ - resizeX?: 'hug' | 'fill' | 'fixed'; - /** - * Controls the vertical resizing behavior. - * - `'hug'`: `h-auto` (CSS: `height: auto`) - * - `'fill'`: `h-full` (CSS: `height: 100%`) - * - `'fixed'`: No additional class (CSS: `height: auto`) - */ - resizeY?: 'hug' | 'fill' | 'fixed'; -} +import { cx, cva } from './cva.config' /** * Configuration for "cva" to generate a dynamic class string based on the provided flex configuration. @@ -114,7 +58,7 @@ const flex = cva({ resizeX: 'hug', resizeY: 'hug', }, -}); +}) /** * A highly customizable Flex component designed to facilitate the easier use of CSS flexbox layout @@ -129,28 +73,25 @@ const flex = cva({ * @param {'center' | 'start' | 'end' | 'between'} justify - Control the distribution of children along main axis. * @returns {React.ReactNode} The React component rendering the Flex container with applied styles. */ -export const Flex = React.forwardRef( - ({as: Component = 'div', children, className = '', ...props}, ref) => { - const classes = cx(flex(props), className); +export const Flex = React.forwardRef( + ( + { + as: Component = 'div', + children, + className = '', + ...props + }, + ref, + ) => { + const classes = cx(flex(props), className) return ( {children} - ); + ) }, -); - -interface GridProps { - as?: React.ElementType; - children?: React.ReactNode; - className?: string; - columns?: 1 | 2 | 3 | 4 | 5; - rows?: 1 | 2 | 3 | 4 | 5; - gap?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8; - resizeX?: 'hug' | 'fill' | 'fixed'; - resizeY?: 'hug' | 'fill' | 'fixed'; -} +) const grid = cva({ base: ['grid'], @@ -195,30 +136,28 @@ const grid = cva({ resizeX: 'hug', resizeY: 'hug', }, -}); +}) -export const Grid = React.forwardRef( - ({children, className, ...props}, ref) => { - const classes = cx(grid(props), className); +export const Grid = React.forwardRef( + ({ children, className, ...props }, ref) => { + const classes = cx(grid(props), className) return (
{children}
- ); + ) }, -); - -interface BackgroundProps { - as?: React.ElementType; - children?: React.ReactNode; - className?: string; - columns?: 1 | 2 | 3 | 4 | 5; - rows?: 1 | 2 | 3 | 4 | 5; -} +) const background = cva({ - base: ['grid', 'absolute', 'z-0', 'inset-0', 'pointer-events-none'], + base: [ + 'grid', + 'absolute', + 'z-0', + 'inset-0', + 'pointer-events-none', + ], variants: { columns: { 1: 'grid-cols-1', @@ -238,31 +177,28 @@ const background = cva({ defaultVariants: { columns: 2, }, -}); +}) -export const Background = React.forwardRef( - ({children, className, ...props}, ref) => { - const classes = cx(background(props), className); +export const Background = React.forwardRef( + ({ children, className, ...props }, ref) => { + const classes = cx(background(props), className) return (
{children}
- ); + ) }, -); - -interface ContainerProps { - as?: React.ElementType; - children?: React.ReactNode; - className?: string; - fluid?: boolean; // If true, the container will take the full width of the viewport - resizeX?: 'hug' | 'fill' | 'fixed'; // Controls the horizontal resizing behavior - resizeY?: 'hug' | 'fill' | 'fixed'; // Controls the vertical resizing behavior -} +) const container = cva({ - base: ['mx-auto', 'px-4', 'md:px-8', 'lg:px-10', 'w-full'], + base: [ + 'mx-auto', + 'px-4', + 'md:px-8', + 'lg:px-10', + 'w-full', + ], variants: { fluid: { true: 'max-w-none', @@ -284,26 +220,27 @@ const container = cva({ resizeX: 'hug', resizeY: 'hug', }, -}); +}) -export const Container = React.forwardRef( - ({as: Component = 'div', children, className, ...props}, ref) => { - const classes = cx(container(props), className); +export const Container = React.forwardRef( + ( + { + as: Component = 'div', + children, + className, + ...props + }, + ref, + ) => { + const classes = cx(container(props), className) return ( {children} - ); + ) }, -); - -interface SectionProps { - as?: React.ElementType; - children?: React.ReactNode; - className?: string; - padded?: boolean; // Controls whether the section has padding -} +) const section = cva({ base: ['w-full', 'relative', 'min-h-8'], @@ -316,16 +253,24 @@ const section = cva({ defaultVariants: { padded: true, }, -}); +}) -export const Section = React.forwardRef( - ({as: Component = 'section', children, className, ...props}, ref) => { - const classes = cx(section(props), className); +export const Section = React.forwardRef( + ( + { + as: Component = 'section', + children, + className, + ...props + }, + ref, + ) => { + const classes = cx(section(props), className) return ( {children} - ); + ) }, -); +) diff --git a/app/root.jsx b/app/root.jsx index af61bdb..781ec65 100644 --- a/app/root.jsx +++ b/app/root.jsx @@ -1,5 +1,5 @@ -import {useNonce} from '@shopify/hydrogen'; -import {defer} from '@shopify/remix-oxygen'; +import { useNonce } from '@shopify/hydrogen' +import { defer } from '@shopify/remix-oxygen' import { Meta, Outlet, @@ -11,33 +11,37 @@ import { ScrollRestoration, isRouteErrorResponse, Links, -} from '@remix-run/react'; +} from '@remix-run/react' -import appStyles from './styles/app.css?url'; +import appStyles from './styles/app.css' -import {Layout} from '~/components/Layout'; +import { Layout } from '~/components/Layout' /** * This is important to avoid re-fetching root queries on sub-navigations * @type {ShouldRevalidateFunction} */ -export const shouldRevalidate = ({formMethod, currentUrl, nextUrl}) => { +export const shouldRevalidate = ({ + formMethod, + currentUrl, + nextUrl, +}) => { // revalidate when a mutation is performed e.g add to cart, login... if (formMethod && formMethod !== 'GET') { - return true; + return true } // revalidate when manually revalidating via useRevalidator if (currentUrl.toString() === nextUrl.toString()) { - return true; + return true } - return false; -}; + return false +} export function links() { return [ - {rel: 'stylesheet', href: appStyles}, + { rel: 'stylesheet', href: appStyles }, { rel: 'preconnect', href: 'https://cdn.shopify.com', @@ -47,33 +51,36 @@ export function links() { href: 'https://shop.app', }, // {rel: 'icon', type: 'image/svg+xml', href: favicon}, - ]; + ] } /** * @return {LoaderReturnData} */ export const useRootLoaderData = () => { - const [root] = useMatches(); - return root?.data; -}; + const [root] = useMatches() + return root?.data +} /** * @param {LoaderFunctionArgs} */ -export async function loader({context}) { - const {storefront, session, cart} = context; - const customerAccessToken = await session.get('customerAccessToken'); - const publicStoreDomain = context.env.PUBLIC_STORE_DOMAIN; +export async function loader({ context }) { + const { storefront, session, cart } = context + const customerAccessToken = await session.get( + 'customerAccessToken', + ) + const publicStoreDomain = context.env.PUBLIC_STORE_DOMAIN // validate the customer access token is valid - const {isLoggedIn, headers} = await validateCustomerAccessToken( - session, - customerAccessToken, - ); + const { isLoggedIn, headers } = + await validateCustomerAccessToken( + session, + customerAccessToken, + ) // defer the cart query by not awaiting it - const cartPromise = cart.get(); + const cartPromise = cart.get() // defer the footer query (below the fold) const footerPromise = storefront.query(FOOTER_QUERY, { @@ -81,7 +88,7 @@ export async function loader({context}) { variables: { footerMenuHandle: 'footer', // Adjust to your footer menu handle }, - }); + }) // await the header query (above the fold) const headerPromise = storefront.query(HEADER_QUERY, { @@ -89,7 +96,7 @@ export async function loader({context}) { variables: { headerMenuHandle: 'main-menu', // Adjust to your header menu handle }, - }); + }) return defer( { @@ -99,12 +106,12 @@ export async function loader({context}) { isLoggedIn, publicStoreDomain, }, - {headers}, - ); + { headers }, + ) } export default function App() { - const data = useLoaderData(); + const data = useLoaderData() return ( @@ -112,52 +119,58 @@ export default function App() { - ); + ) } -function Root({children}) { - const nonce = useNonce(); +function Root({ children }) { + const nonce = useNonce() return ( - + - - + + - + {children} - ); + ) } export function ErrorBoundary() { - const error = useRouteError(); - const rootData = useRootLoaderData(); - const nonce = useNonce(); - let errorMessage = 'Unknown error'; - let errorStatus = 500; + const error = useRouteError() + const rootData = useRootLoaderData() + const nonce = useNonce() + let errorMessage = 'Unknown error' + let errorStatus = 500 if (isRouteErrorResponse(error)) { - errorMessage = error?.data?.message ?? error.data; - errorStatus = error.status; + errorMessage = error?.data?.message ?? error.data + errorStatus = error.status } else if (error instanceof Error) { - errorMessage = error.message; + errorMessage = error.message } return ( - + - - + + -
+

Oops

{errorStatus}

{errorMessage && ( @@ -172,7 +185,7 @@ export function ErrorBoundary() { - ); + ) } /** @@ -189,25 +202,33 @@ export function ErrorBoundary() { * @param {LoaderFunctionArgs['context']['session']} session * @param {CustomerAccessToken} [customerAccessToken] */ -async function validateCustomerAccessToken(session, customerAccessToken) { - let isLoggedIn = false; - const headers = new Headers(); - if (!customerAccessToken?.accessToken || !customerAccessToken?.expiresAt) { - return {isLoggedIn, headers}; +async function validateCustomerAccessToken( + session, + customerAccessToken, +) { + let isLoggedIn = false + const headers = new Headers() + if ( + !customerAccessToken?.accessToken || + !customerAccessToken?.expiresAt + ) { + return { isLoggedIn, headers } } - const expiresAt = new Date(customerAccessToken.expiresAt).getTime(); - const dateNow = Date.now(); - const customerAccessTokenExpired = expiresAt < dateNow; + const expiresAt = new Date( + customerAccessToken.expiresAt, + ).getTime() + const dateNow = Date.now() + const customerAccessTokenExpired = expiresAt < dateNow if (customerAccessTokenExpired) { - session.unset('customerAccessToken'); - headers.append('Set-Cookie', await session.commit()); + session.unset('customerAccessToken') + headers.append('Set-Cookie', await session.commit()) } else { - isLoggedIn = true; + isLoggedIn = true } - return {isLoggedIn, headers}; + return { isLoggedIn, headers } } const MENU_FRAGMENT = `#graphql @@ -234,7 +255,7 @@ const MENU_FRAGMENT = `#graphql ...ParentMenuItem } } -`; +` const HEADER_QUERY = `#graphql fragment Shop on Shop { @@ -265,7 +286,7 @@ const HEADER_QUERY = `#graphql } } ${MENU_FRAGMENT} -`; +` const FOOTER_QUERY = `#graphql query Footer( @@ -278,7 +299,7 @@ const FOOTER_QUERY = `#graphql } } ${MENU_FRAGMENT} -`; +` /** @typedef {import('@shopify/remix-oxygen').LoaderFunctionArgs} LoaderFunctionArgs */ /** @typedef {import('@remix-run/react').ShouldRevalidateFunction} ShouldRevalidateFunction */ diff --git a/app/routes/products.$handle/sections/hero.jsx b/app/routes/products.$handle/sections/hero.jsx index 630d559..be8d696 100644 --- a/app/routes/products.$handle/sections/hero.jsx +++ b/app/routes/products.$handle/sections/hero.jsx @@ -1,48 +1,63 @@ -import {Suspense} from 'react'; -import {Await, useLoaderData} from '@remix-run/react'; -import {Image, VariantSelector} from '@shopify/hydrogen'; -import {AddToCartButton} from '@h2/Button'; -import {Heading, Text} from '@h2/Text'; -import {Price, PriceCompareAt} from '@h2/Price'; -import {ShopPayButton} from '@h2/ShopPayButton'; -import {Background, Flex, Grid, Section} from '@h2/new/Layout'; -import Link from '@h2/Link'; -import Accordion from '@h2/new/Accordion'; +import React from 'react' +import { Suspense } from 'react' +import { Await, useLoaderData } from '@remix-run/react' +import { Image, VariantSelector } from '@shopify/hydrogen' +import { AddToCartButton } from '/app/components/hydrogen/Button' +import { + Heading, + Text, +} from '/app/components/hydrogen/Text' +import { + Price, + PriceCompareAt, +} from '/app/components/hydrogen/Price' +import { ShopPayButton } from '/app/components/hydrogen/ShopPayButton' +import { + Background, + Flex, + Grid, + Section, +} from '/app/components/hydrogen/new/Layout' +import Link from '/app/components/hydrogen/Link' +import Accordion from '/app/components/hydrogen/new/Accordion' export default function Hero() { - const {product} = useLoaderData(); - const {selectedVariant} = product; + const { product } = useLoaderData() + const { selectedVariant } = product return ( -
-
+
+
-
+
{selectedVariant?.image.altText
- +
-
-
+
+
- ); + ) } function ProductSummary() { - const {product, variants} = useLoaderData(); - const {selectedVariant, title, description} = product; + const { product, variants } = useLoaderData() + const { selectedVariant, title, description } = product const accordionData = [ { @@ -63,22 +78,33 @@ function ProductSummary() { content: 'Lorem, ipsum dolor sit amet consectetur adipisicing elit. Alias debitis illo unde itaque eius eos necessitatibus assumenda, quos nisi cum reprehenderit aut placeat modi corrupti repudiandae mollitia corporis et labore?', }, - ]; + ] return (
-
- {title} +
+ {title} -
-
+
+
- +
- {description} + + {description} +
{(data) => ( )}
-
+
{/* */} Receive it by{' '} - {businessDaysFromNow(7)} + + {businessDaysFromNow(7)} + @@ -130,25 +160,25 @@ function ProductSummary() {
- ); + ) } function businessDaysFromNow(days) { - let date = new Date(); - let addedDays = 0; + let date = new Date() + let addedDays = 0 while (addedDays < days) { - date.setDate(date.getDate() + 1); + date.setDate(date.getDate() + 1) // Check if the current day is a weekday (Monday-Friday) - const dayOfWeek = date.getDay(); + const dayOfWeek = date.getDay() if (dayOfWeek !== 0 && dayOfWeek !== 6) { // 0 = Sunday, 6 = Saturday - addedDays++; + addedDays++ } } - return formatDate(date); + return formatDate(date) } function formatDate(date) { @@ -160,7 +190,7 @@ function formatDate(date) { 'Thursday', 'Friday', 'Saturday', - ]; + ] const months = [ 'Jan', 'Feb', @@ -174,13 +204,13 @@ function formatDate(date) { 'Oct', 'Nov', 'Dec', - ]; + ] - let dayName = daysOfWeek[date.getDay()]; - let monthName = months[date.getMonth()]; - let day = date.getDate(); + let dayName = daysOfWeek[date.getDay()] + let monthName = months[date.getMonth()] + let day = date.getDate() - return `${dayName}, ${monthName} ${day}`; + return `${dayName}, ${monthName} ${day}` } /** * @param {{ @@ -189,38 +219,47 @@ function formatDate(date) { * variants: Array; * }} */ -function ProductForm({product, selectedVariant, variants}) { +function ProductForm({ + product, + selectedVariant, + variants, +}) { return ( -
+
- {({option}) => ( -
+ {({ option }) => ( +
{option.name}
-
- {option.values.map(({value, isAvailable, isActive, to}) => { - return ( - - {value} - - ); - })} +
+ {option.values.map( + ({ value, isAvailable, isActive, to }) => { + return ( + + {value} + + ) + }, + )}
)} @@ -230,11 +269,15 @@ function ProductForm({product, selectedVariant, variants}) { { - window.location.href = window.location.href + '#cart-aside'; + window.location.href = + window.location.href + '#cart-aside' }} lines={ selectedVariant @@ -247,9 +290,11 @@ function ProductForm({product, selectedVariant, variants}) { : [] } > - {selectedVariant?.availableForSale ? 'Add to cart' : 'Sold out'} + {selectedVariant?.availableForSale + ? 'Add to cart' + : 'Sold out'}
- ); + ) } diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..0d5eb68 --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "checkJs": true, + "jsx": "react-jsx", + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "Bundler", + "baseUrl": ".", + "paths": { + "@h2/*": ["app/components/hydrogen/*"], + "~/*": ["app/*"] + } + }, + "include": ["./**/*.d.ts", "./**/*.js", "./**/*.jsx"] + } + \ No newline at end of file diff --git a/server.js b/server.js index dd6ea94..bac9223 100644 --- a/server.js +++ b/server.js @@ -1,6 +1,6 @@ // @ts-ignore // Virtual entry point for the app -import * as remixBuild from 'virtual:remix/server-build'; +import * as remixBuild from 'virtual:remix/server-build' import { cartGetIdDefault, cartSetIdDefault, @@ -8,14 +8,14 @@ import { createStorefrontClient, storefrontRedirect, createCustomerAccountClient, -} from '@shopify/hydrogen'; +} from '@shopify/hydrogen' import { createRequestHandler, getStorefrontHeaders, -} from '@shopify/remix-oxygen'; -import {AppSession} from '~/lib/session'; -import {CART_QUERY_FRAGMENT} from '~/lib/fragments'; -import {getLoadContext} from 'server-context-getter'; +} from '@shopify/remix-oxygen' +import { AppSession } from '~/lib/session' +import { CART_QUERY_FRAGMENT } from '~/lib/fragments' +import { getLoadContext } from 'server-context-getter' /** * Export a fetch handler in module format. @@ -32,11 +32,16 @@ export default { * Open a cache instance in the worker and a custom session instance. */ if (!env?.SESSION_SECRET) { - throw new Error('SESSION_SECRET environment variable is not set'); + throw new Error( + 'SESSION_SECRET environment variable is not set', + ) } // this is pretty ugly but we need to pre-create the Remix Context to have access to the storefront later in this function - const loadContext = getLoadContext(env, executionContext)(request); + const loadContext = getLoadContext( + env, + executionContext, + )(request) /** * Create a Remix request handler and pass @@ -46,9 +51,9 @@ export default { build: remixBuild, mode: process.env.NODE_ENV, getLoadContext: () => loadContext, - }); + }) - const response = await handleRequest(request); + const response = await handleRequest(request) if (response.status === 404) { /** @@ -60,16 +65,18 @@ export default { request, response, storefront: loadContext.storefront, - }); + }) } - return response; + return response } catch (error) { // eslint-disable-next-line no-console - console.error(error); - return new Response('An unexpected error occurred', {status: 500}); + console.error(error) + return new Response('An unexpected error occurred', { + status: 500, + }) } }, -}; +} /** @typedef {import('@shopify/remix-oxygen').AppLoadContext} AppLoadContext */ diff --git a/utopia/storyboard.js b/utopia/storyboard.js index c117eec..fbe0cc5 100644 --- a/utopia/storyboard.js +++ b/utopia/storyboard.js @@ -6,9 +6,9 @@ import { Image } from '@shopify/hydrogen' const contextGetter = getLoadContext( { - PUBLIC_STORE_DOMAIN: 'praiseful-pear.myshopify.com', + PUBLIC_STORE_DOMAIN: '438c73-58.myshopify.com', PUBLIC_STOREFRONT_API_TOKEN: - '541564e540184b9648c529272ffa4b53', + 'f9ee7cc0a41218b2ce1a5b76ac72cfd5', }, { waitUntil: () => {},