diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000000000000000000000000000000000000..a362bcaa13b5f63e55981ec7fc15211ea46b2ff4 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,5 @@ +build +node_modules +bin +*.d.ts +dist diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000000000000000000000000000000000000..0d7f6656f40cdc19a279002fc741f279711f298e --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,16 @@ +/** + * @type {import("@types/eslint").Linter.BaseConfig} + */ +module.exports = { + extends: [ + '@remix-run/eslint-config', + 'plugin:hydrogen/recommended', + 'plugin:hydrogen/typescript', + ], + rules: { + '@typescript-eslint/ban-ts-comment': 'off', + '@typescript-eslint/naming-convention': 'off', + 'hydrogen/prefer-image-component': 'off', + 'no-case-declarations': 'off', + }, +}; diff --git a/.gitattributes b/.gitattributes index a6344aac8c09253b3b630fb776ae94478aa0275b..1003535dc3f0bc216b3ae86eb97ed459c7eb841f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text *.zip filter=lfs diff=lfs merge=lfs -text *.zst filter=lfs diff=lfs merge=lfs -text *tfevents* filter=lfs diff=lfs merge=lfs -text +app/assets/hero-banner.jpg filter=lfs diff=lfs merge=lfs -text +app/assets/product-placeholder.jpg filter=lfs diff=lfs merge=lfs -text diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000000000000000000000000000000000000..bbcb7f24a1cac75c9742b872ec89441a3f3a849e --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @netlify/ecosystem-pod-frameworks diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..0f6309e1f74f3fdc90d9f87f8c59e9357f9e40bc --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +node_modules +/.cache +/build +/dist +/public/build +/.mf +.env +.env.* +.shopify + +# Local Netlify folder +.netlify diff --git a/.graphqlrc.js b/.graphqlrc.js new file mode 100644 index 0000000000000000000000000000000000000000..f473343c5d21fb45c8253518d2eb99cf662b1b0b --- /dev/null +++ b/.graphqlrc.js @@ -0,0 +1,28 @@ +import { getSchema } from '@shopify/hydrogen-codegen'; + +/** + * GraphQL Config + * @see https://the-guild.dev/graphql/config/docs/user/usage + * @type {IGraphQLConfig} + */ +export default { + projects: { + default: { + schema: getSchema('storefront'), + documents: [ + './*.{ts,tsx,js,jsx}', + './app/**/*.{ts,tsx,js,jsx}', + '!./app/graphql/**/*.{ts,tsx,js,jsx}', + ], + }, + + customer: { + schema: getSchema('customer-account'), + documents: ['./app/graphql/customer-account/*.{ts,tsx,js,jsx}'], + }, + + // Add your own GraphQL projects here for CMS, Shopify Admin API, etc. + }, +}; + +/** @typedef {import('graphql-config').IGraphQLConfig} IGraphQLConfig */ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000000000000000000000000000000000..a0f131016122e938f45813ad60c1984f7c81dd13 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,143 @@ +# skeleton + +## 1.0.0 + +### Major Changes + +- The Storefront API 2023-10 now returns menu item URLs that include the `primaryDomainUrl`, instead of defaulting to the Shopify store ID URL (example.myshopify.com). The skeleton template requires changes to check for the `primaryDomainUrl`: by [@blittle](https://github.com/blittle) + + 1. Update the `HeaderMenu` component to accept a `primaryDomainUrl` and include + it in the internal url check + + ```diff + // app/components/Header.tsx + + + import type {HeaderQuery} from 'storefrontapi.generated'; + + export function HeaderMenu({ + menu, + + primaryDomainUrl, + viewport, + }: { + menu: HeaderProps['header']['menu']; + + primaryDomainUrl: HeaderQuery['shop']['primaryDomain']['url']; + viewport: Viewport; + }) { + + // ...code + + // if the url is internal, we strip the domain + const url = + item.url.includes('myshopify.com') || + item.url.includes(publicStoreDomain) || + + item.url.includes(primaryDomainUrl) + ? new URL(item.url).pathname + : item.url; + + // ...code + + } + ``` + + 2. Update the `FooterMenu` component to accept a `primaryDomainUrl` prop and include + it in the internal url check + + ```diff + // app/components/Footer.tsx + + - import type {FooterQuery} from 'storefrontapi.generated'; + + import type {FooterQuery, HeaderQuery} from 'storefrontapi.generated'; + + function FooterMenu({ + menu, + + primaryDomainUrl, + }: { + menu: FooterQuery['menu']; + + primaryDomainUrl: HeaderQuery['shop']['primaryDomain']['url']; + }) { + // code... + + // if the url is internal, we strip the domain + const url = + item.url.includes('myshopify.com') || + item.url.includes(publicStoreDomain) || + + item.url.includes(primaryDomainUrl) + ? new URL(item.url).pathname + : item.url; + + // ...code + + ); + } + ``` + + 3. Update the `Footer` component to accept a `shop` prop + + ```diff + export function Footer({ + menu, + + shop, + }: FooterQuery & {shop: HeaderQuery['shop']}) { + return ( + + ); + } + ``` + + 4. Update `Layout.tsx` to pass the `shop` prop + + ```diff + export function Layout({ + cart, + children = null, + footer, + header, + isLoggedIn, + }: LayoutProps) { + return ( + <> + + + +
+
{children}
+ + + - {(footer) =>
} + + {(footer) =>
} + + + + ); + } + ``` + +### Patch Changes + +- If you are calling `useMatches()` in different places of your app to access the data returned by the root loader, you may want to update it to the following pattern to enhance types: ([#1289](https://github.com/Shopify/hydrogen/pull/1289)) by [@frandiox](https://github.com/frandiox) + + ```ts + // root.tsx + + import {useMatches} from '@remix-run/react'; + import {type SerializeFrom} from '@netlify/remix-runtime'; + + export const useRootLoaderData = () => { + const [root] = useMatches(); + return root?.data as SerializeFrom; + }; + + export function loader(context) { + // ... + } + ``` + + This way, you can import `useRootLoaderData()` anywhere in your app and get the correct type for the data returned by the root loader. + +- Updated dependencies [[`81400439`](https://github.com/Shopify/hydrogen/commit/814004397c1d17ef0a53a425ed28a42cf67765cf), [`a6f397b6`](https://github.com/Shopify/hydrogen/commit/a6f397b64dc6a0d856cb7961731ee1f86bf80292), [`3464ec04`](https://github.com/Shopify/hydrogen/commit/3464ec04a084e1ceb30ee19874dc1b9171ce2b34), [`7fc088e2`](https://github.com/Shopify/hydrogen/commit/7fc088e21bea47840788cb7c60f873ce1f253128), [`867e0b03`](https://github.com/Shopify/hydrogen/commit/867e0b033fc9eb04b7250baea97d8fd49d26ccca), [`ad45656c`](https://github.com/Shopify/hydrogen/commit/ad45656c5f663cc1a60eab5daab4da1dfd0e6cc3), [`f24e3424`](https://github.com/Shopify/hydrogen/commit/f24e3424c8e2b363b181b71fcbd3e45f696fdd3f), [`66a48573`](https://github.com/Shopify/hydrogen/commit/66a4857387148b6a104df5783314c74aca8aada0), [`0ae7cbe2`](https://github.com/Shopify/hydrogen/commit/0ae7cbe280d8351126e11dc13f35d7277d9b2d86), [`8198c1be`](https://github.com/Shopify/hydrogen/commit/8198c1befdfafb39fbcc88d71f91d21eae252973), [`ad45656c`](https://github.com/Shopify/hydrogen/commit/ad45656c5f663cc1a60eab5daab4da1dfd0e6cc3)]: + - @shopify/hydrogen@2023.10.0 + - @shopify/remix-oxygen@2.0.0 + - @shopify/cli-hydrogen@6.0.0 diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..d5f50d281e60fa95497c0304b11f33bde9aa8783 --- /dev/null +++ b/README.md @@ -0,0 +1,60 @@ +# 🚀 Hydrogen Template: Skeleton (Enhanced) + +[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/AbdulElahOthmanGwaith/hydrogen-template) + +هذا المستودع عبارة عن قالب متطور لمتاجر **Shopify** باستخدام إطار عمل **Hydrogen** و **Remix**. تم تحديثه ليتوافق مع أحدث معايير الأداء والأمان لعام 2026. + +--- + +## 🌟 الميزات الرئيسية | Key Features + +- **Remix 2.x**: إطار عمل كامل للمواقع السريعة والمتجاوبة. +- **Hydrogen**: مكتبة Shopify المخصصة للتجارة الإلكترونية "Headless". +- **Vite Optimized**: إعدادات متقدمة لسرعة بناء وتطوير فائقة. +- **Netlify Ready**: مهيأ للنشر المباشر على Netlify مع دعم Edge Functions. +- **TypeScript**: دعم كامل للأنماط لضمان كود نظيف وخالٍ من الأخطاء. + +--- + +## 🛠️ البدء في العمل | Getting Started + +### المتطلبات | Prerequisites +- Node.js (إصدار 18 أو أحدث) +- Netlify CLI + +### التثبيت | Installation +```bash +# استنساخ المستودع +git clone https://github.com/AbdulElahOthmanGwaith/hydrogen-template.git + +# الدخول للمجلد +cd hydrogen-template + +# تثبيت التبعيات +npm install +``` + +### التشغيل | Development +```bash +npm run dev +``` + +--- + +## 📂 هيكل المشروع | Project Structure + +- `/app`: يحتوي على منطق التطبيق، المكونات، والمسارات (Routes). +- `/public`: الملفات الثابتة مثل الصور والأيقونات. +- `server.ts`: إعدادات الخادم المخصصة لـ Netlify Edge. + +--- + +## 🤝 المساهمة | Contributing + +نرحب بمساهماتكم! يرجى فتح "Issue" أو تقديم "Pull Request" لأي تحسينات تقترحونها. + +--- + +## 📄 الترخيص | License + +هذا المشروع مرخص تحت رخصة MIT. diff --git a/app/assets/favicon.svg b/app/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..f6c649733d683edee6cd253fbde202744ef72420 --- /dev/null +++ b/app/assets/favicon.svg @@ -0,0 +1,28 @@ + + + + + diff --git a/app/assets/hero-banner.jpg b/app/assets/hero-banner.jpg new file mode 100644 index 0000000000000000000000000000000000000000..823bf8ec16d84ffca7cd1f54f374e64767682063 --- /dev/null +++ b/app/assets/hero-banner.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d86ab3161972d089bfcb76d893bb8796b1d2942ecbd602689d09d6ff224145b2 +size 1919707 diff --git a/app/assets/product-placeholder.jpg b/app/assets/product-placeholder.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c52629fcd291767056e9564ac68f597ae6940d4f --- /dev/null +++ b/app/assets/product-placeholder.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6cf9993c892d272b64f38fbd93cd47ad9c814ad78ebf312065d489684e0c9cb8 +size 975960 diff --git a/app/components/AddToCartButton.tsx b/app/components/AddToCartButton.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c0d923f0786422544dc2a1e91f1b322e032cabb5 --- /dev/null +++ b/app/components/AddToCartButton.tsx @@ -0,0 +1,37 @@ +import {type FetcherWithComponents} from '@remix-run/react'; +import {CartForm, type OptimisticCartLineInput} from '@shopify/hydrogen'; + +export function AddToCartButton({ + analytics, + children, + disabled, + lines, + onClick, +}: { + analytics?: unknown; + children: React.ReactNode; + disabled?: boolean; + lines: Array; + onClick?: () => void; +}) { + return ( + + {(fetcher: FetcherWithComponents) => ( + <> + + + + )} + + ); +} diff --git a/app/components/Aside.tsx b/app/components/Aside.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c535668ba3f0c8332a7ef15d8e88bd4de6bb1c1d --- /dev/null +++ b/app/components/Aside.tsx @@ -0,0 +1,76 @@ +import {createContext, type ReactNode, useContext, useState} from 'react'; + +type AsideType = 'search' | 'cart' | 'mobile' | 'closed'; +type AsideContextValue = { + type: AsideType; + open: (mode: AsideType) => void; + close: () => void; +}; + +/** + * A side bar component with Overlay + * @example + * ```jsx + * + * ``` + */ +export function Aside({ + children, + heading, + type, +}: { + children?: React.ReactNode; + type: AsideType; + heading: React.ReactNode; +}) { + const {type: activeType, close} = useAside(); + const expanded = type === activeType; + + return ( +
+ +
+
{children}
+ + + ); +} + +const AsideContext = createContext(null); + +Aside.Provider = function AsideProvider({children}: {children: ReactNode}) { + const [type, setType] = useState('closed'); + + return ( + setType('closed'), + }} + > + {children} + + ); +}; + +export function useAside() { + const aside = useContext(AsideContext); + if (!aside) { + throw new Error('useAside must be used within an AsideProvider'); + } + return aside; +} diff --git a/app/components/CartLineItem.tsx b/app/components/CartLineItem.tsx new file mode 100644 index 0000000000000000000000000000000000000000..26102b612b2a5de210b1a1424a39a845b0c9d339 --- /dev/null +++ b/app/components/CartLineItem.tsx @@ -0,0 +1,153 @@ +import type {CartLineUpdateInput} from '@shopify/hydrogen/storefront-api-types'; +import type {CartLayout} from '~/components/CartMain'; +import {CartForm, Image, type OptimisticCartLine} from '@shopify/hydrogen'; +import {useVariantUrl} from '~/lib/variants'; +import {Link} from '@remix-run/react'; +import {ProductPrice} from './ProductPrice'; +import {useAside} from './Aside'; +import type {CartApiQueryFragment} from 'storefrontapi.generated'; + +type CartLine = OptimisticCartLine; + +/** + * A single line item in the cart. It displays the product image, title, price. + * It also provides controls to update the quantity or remove the line item. + */ +export function CartLineItem({ + layout, + line, +}: { + layout: CartLayout; + line: CartLine; +}) { + const {id, merchandise} = line; + const {product, title, image, selectedOptions} = merchandise; + const lineItemUrl = useVariantUrl(product.handle, selectedOptions); + const {close} = useAside(); + + return ( +
  • + {image && ( + {title} + )} + +
    + { + if (layout === 'aside') { + close(); + } + }} + > +

    + {product.title} +

    + + +
      + {selectedOptions.map((option) => ( +
    • + + {option.name}: {option.value} + +
    • + ))} +
    + +
    +
  • + ); +} + +/** + * Provides the controls to update the quantity of a line item in the cart. + * These controls are disabled when the line item is new, and the server + * hasn't yet responded that it was successfully added to the cart. + */ +function CartLineQuantity({line}: {line: CartLine}) { + if (!line || typeof line?.quantity === 'undefined') return null; + const {id: lineId, quantity, isOptimistic} = line; + const prevQuantity = Number(Math.max(0, quantity - 1).toFixed(0)); + const nextQuantity = Number((quantity + 1).toFixed(0)); + + return ( +
    + Quantity: {quantity}    + + + +   + + + +   + +
    + ); +} + +/** + * A button that removes a line item from the cart. It is disabled + * when the line item is new, and the server hasn't yet responded + * that it was successfully added to the cart. + */ +function CartLineRemoveButton({ + lineIds, + disabled, +}: { + lineIds: string[]; + disabled: boolean; +}) { + return ( + + + + ); +} + +function CartLineUpdateButton({ + children, + lines, +}: { + children: React.ReactNode; + lines: CartLineUpdateInput[]; +}) { + return ( + + {children} + + ); +} diff --git a/app/components/CartMain.tsx b/app/components/CartMain.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b42180988ad37bfbd72bf186a531a300ded26394 --- /dev/null +++ b/app/components/CartMain.tsx @@ -0,0 +1,68 @@ +import {useOptimisticCart} from '@shopify/hydrogen'; +import {Link} from '@remix-run/react'; +import type {CartApiQueryFragment} from 'storefrontapi.generated'; +import {useAside} from '~/components/Aside'; +import {CartLineItem} from '~/components/CartLineItem'; +import {CartSummary} from './CartSummary'; + +export type CartLayout = 'page' | 'aside'; + +export type CartMainProps = { + cart: CartApiQueryFragment | null; + layout: CartLayout; +}; + +/** + * The main cart component that displays the cart items and summary. + * It is used by both the /cart route and the cart aside dialog. + */ +export function CartMain({layout, cart: originalCart}: CartMainProps) { + // The useOptimisticCart hook applies pending actions to the cart + // so the user immediately sees feedback when they modify the cart. + const cart = useOptimisticCart(originalCart); + + const linesCount = Boolean(cart?.lines?.nodes?.length || 0); + const withDiscount = + cart && + Boolean(cart?.discountCodes?.filter((code) => code.applicable)?.length); + const className = `cart-main ${withDiscount ? 'with-discount' : ''}`; + const cartHasItems = (cart?.totalQuantity ?? 0) > 0; + + return ( +
    +
    + ); +} + +function CartEmpty({ + hidden = false, +}: { + hidden: boolean; + layout?: CartMainProps['layout']; +}) { + const {close} = useAside(); + return ( + + ); +} diff --git a/app/components/CartSummary.tsx b/app/components/CartSummary.tsx new file mode 100644 index 0000000000000000000000000000000000000000..cfd15a8979567ba2bff30867f1ad28e337da8b95 --- /dev/null +++ b/app/components/CartSummary.tsx @@ -0,0 +1,101 @@ +import type {CartApiQueryFragment} from 'storefrontapi.generated'; +import type {CartLayout} from '~/components/CartMain'; +import {CartForm, Money, type OptimisticCart} from '@shopify/hydrogen'; + +type CartSummaryProps = { + cart: OptimisticCart; + layout: CartLayout; +}; + +export function CartSummary({cart, layout}: CartSummaryProps) { + const className = + layout === 'page' ? 'cart-summary-page' : 'cart-summary-aside'; + + return ( +
    +

    Totals

    +
    +
    Subtotal
    +
    + {cart.cost?.subtotalAmount?.amount ? ( + + ) : ( + '-' + )} +
    +
    + + +
    + ); +} +function CartCheckoutActions({checkoutUrl}: {checkoutUrl?: string}) { + if (!checkoutUrl) return null; + + return ( + + ); +} + +function CartDiscounts({ + discountCodes, +}: { + discountCodes?: CartApiQueryFragment['discountCodes']; +}) { + const codes: string[] = + discountCodes + ?.filter((discount) => discount.applicable) + ?.map(({code}) => code) || []; + + return ( +
    + {/* Have existing discount, display it with a remove option */} + + + {/* Show an input to apply a discount */} + +
    + +   + +
    +
    +
    + ); +} + +function UpdateDiscountForm({ + discountCodes, + children, +}: { + discountCodes?: string[]; + children: React.ReactNode; +}) { + return ( + + {children} + + ); +} diff --git a/app/components/Footer.tsx b/app/components/Footer.tsx new file mode 100644 index 0000000000000000000000000000000000000000..79770cfd1d179aedd7d18beb2f88ca67a5be4e51 --- /dev/null +++ b/app/components/Footer.tsx @@ -0,0 +1,129 @@ +import {Suspense} from 'react'; +import {Await, NavLink} from '@remix-run/react'; +import type {FooterQuery, HeaderQuery} from 'storefrontapi.generated'; + +interface FooterProps { + footer: Promise; + header: HeaderQuery; + publicStoreDomain: string; +} + +export function Footer({ + footer: footerPromise, + header, + publicStoreDomain, +}: FooterProps) { + return ( + + + {(footer) => ( +
    + {footer?.menu && header.shop.primaryDomain?.url && ( + + )} +
    + )} +
    +
    + ); +} + +function FooterMenu({ + menu, + primaryDomainUrl, + publicStoreDomain, +}: { + menu: FooterQuery['menu']; + primaryDomainUrl: FooterProps['header']['shop']['primaryDomain']['url']; + publicStoreDomain: string; +}) { + return ( + + ); +} + +const FALLBACK_FOOTER_MENU = { + id: 'gid://shopify/Menu/199655620664', + items: [ + { + id: 'gid://shopify/MenuItem/461633060920', + resourceId: 'gid://shopify/ShopPolicy/23358046264', + tags: [], + title: 'Privacy Policy', + type: 'SHOP_POLICY', + url: '/policies/privacy-policy', + items: [], + }, + { + id: 'gid://shopify/MenuItem/461633093688', + resourceId: 'gid://shopify/ShopPolicy/23358013496', + tags: [], + title: 'Refund Policy', + type: 'SHOP_POLICY', + url: '/policies/refund-policy', + items: [], + }, + { + id: 'gid://shopify/MenuItem/461633126456', + resourceId: 'gid://shopify/ShopPolicy/23358111800', + tags: [], + title: 'Shipping Policy', + type: 'SHOP_POLICY', + url: '/policies/shipping-policy', + items: [], + }, + { + id: 'gid://shopify/MenuItem/461633159224', + resourceId: 'gid://shopify/ShopPolicy/23358079032', + tags: [], + title: 'Terms of Service', + type: 'SHOP_POLICY', + url: '/policies/terms-of-service', + items: [], + }, + ], +}; + +function activeLinkStyle({ + isActive, + isPending, +}: { + isActive: boolean; + isPending: boolean; +}) { + return { + fontWeight: isActive ? 'bold' : undefined, + color: isPending ? 'grey' : 'white', + }; +} diff --git a/app/components/Header.tsx b/app/components/Header.tsx new file mode 100644 index 0000000000000000000000000000000000000000..09fba1dc6fcd5d9306bdc4aabf7fdd9243afe414 --- /dev/null +++ b/app/components/Header.tsx @@ -0,0 +1,224 @@ +import {Suspense} from 'react'; +import {Await, NavLink} from '@remix-run/react'; +import {type CartViewPayload, useAnalytics} from '@shopify/hydrogen'; +import type {HeaderQuery, CartApiQueryFragment} from 'storefrontapi.generated'; +import {useAside} from '~/components/Aside'; + +interface HeaderProps { + header: HeaderQuery; + cart: Promise; + isLoggedIn: Promise; + publicStoreDomain: string; +} + +type Viewport = 'desktop' | 'mobile'; + +export function Header({ + header, + isLoggedIn, + cart, + publicStoreDomain, +}: HeaderProps) { + const {shop, menu} = header; + return ( +
    + + {shop.name} + + + +
    + ); +} + +export function HeaderMenu({ + menu, + primaryDomainUrl, + viewport, + publicStoreDomain, +}: { + menu: HeaderProps['header']['menu']; + primaryDomainUrl: HeaderProps['header']['shop']['primaryDomain']['url']; + viewport: Viewport; + publicStoreDomain: HeaderProps['publicStoreDomain']; +}) { + const className = `header-menu-${viewport}`; + const {close} = useAside(); + + return ( + + ); +} + +function HeaderCtas({ + isLoggedIn, + cart, +}: Pick) { + return ( + + ); +} + +function HeaderMenuMobileToggle() { + const {open} = useAside(); + return ( + + ); +} + +function SearchToggle() { + const {open} = useAside(); + return ( + + ); +} + +function CartBadge({count}: {count: number | null}) { + const {open} = useAside(); + const {publish, shop, cart, prevCart} = useAnalytics(); + + return ( + { + e.preventDefault(); + open('cart'); + publish('cart_viewed', { + cart, + prevCart, + shop, + url: window.location.href || '', + } as CartViewPayload); + }} + > + Cart {count === null ?   : count} + + ); +} + +function CartToggle({cart}: Pick) { + return ( + }> + + {(cart) => { + if (!cart) return ; + return ; + }} + + + ); +} + +const FALLBACK_HEADER_MENU = { + id: 'gid://shopify/Menu/199655587896', + items: [ + { + id: 'gid://shopify/MenuItem/461609500728', + resourceId: null, + tags: [], + title: 'Collections', + type: 'HTTP', + url: '/collections', + items: [], + }, + { + id: 'gid://shopify/MenuItem/461609533496', + resourceId: null, + tags: [], + title: 'Blog', + type: 'HTTP', + url: '/blogs/journal', + items: [], + }, + { + id: 'gid://shopify/MenuItem/461609566264', + resourceId: null, + tags: [], + title: 'Policies', + type: 'HTTP', + url: '/policies', + items: [], + }, + { + id: 'gid://shopify/MenuItem/461609599032', + resourceId: 'gid://shopify/Page/92591030328', + tags: [], + title: 'About', + type: 'PAGE', + url: '/pages/about', + items: [], + }, + ], +}; + +function activeLinkStyle({ + isActive, + isPending, +}: { + isActive: boolean; + isPending: boolean; +}) { + return { + fontWeight: isActive ? 'bold' : undefined, + color: isPending ? 'grey' : 'black', + }; +} diff --git a/app/components/PageLayout.tsx b/app/components/PageLayout.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c763690287b30181e00253bb7b09898b20f38052 --- /dev/null +++ b/app/components/PageLayout.tsx @@ -0,0 +1,172 @@ +import {Await, Link} from '@remix-run/react'; +import {Suspense} from 'react'; +import type { + CartApiQueryFragment, + FooterQuery, + HeaderQuery, +} from 'storefrontapi.generated'; +import {Aside} from '~/components/Aside'; +import {Footer} from '~/components/Footer'; +import {Header, HeaderMenu} from '~/components/Header'; +import {CartMain} from '~/components/CartMain'; +import { + SEARCH_ENDPOINT, + SearchFormPredictive, +} from '~/components/SearchFormPredictive'; +import {SearchResultsPredictive} from '~/components/SearchResultsPredictive'; + +interface PageLayoutProps { + cart: Promise; + footer: Promise; + header: HeaderQuery; + isLoggedIn: Promise; + publicStoreDomain: string; + children?: React.ReactNode; +} + +export function PageLayout({ + cart, + children = null, + footer, + header, + isLoggedIn, + publicStoreDomain, +}: PageLayoutProps) { + return ( + + + + + {header && ( +
    + )} +
    {children}
    +