diff --git a/.env.example b/.env.example index e6aa9ecf..31a4eaf3 100644 --- a/.env.example +++ b/.env.example @@ -15,5 +15,5 @@ WEAVERSE_PROJECT_ID="your-project-id" # Additional services #PUBLIC_GOOGLE_GTM_ID=G-R1KFYYKE48 -#JUDGEME_PUBLIC_TOKEN="your-judgeme-public-token" +#JUDGEME_PRIVATE_API_TOKEN="your-judgeme-private-api-token" diff --git a/README.md b/README.md index a5189b15..6676bc66 100644 --- a/README.md +++ b/README.md @@ -213,7 +213,6 @@ Export a `schema` object from the file to define the component's schema with def export let schema: HydrogenComponentSchema = { type: 'video', title: 'Video', - toolbar: ['general-settings', ['duplicate', 'delete']], inspector: [ { group: 'Video', diff --git a/app/components/Button.tsx b/app/components/Button.tsx index bcc4d201..a3beed17 100644 --- a/app/components/Button.tsx +++ b/app/components/Button.tsx @@ -38,25 +38,29 @@ let variants = cva( }, ); +export interface ButtonStyleProps { + buttonStyle: "inherit" | "custom"; + backgroundColor: string; + textColor: string; + borderColor: string; + backgroundColorHover: string; + textColorHover: string; + borderColorHover: string; +} + export interface ButtonProps extends VariantProps, Omit< HTMLAttributes, "children" | "type" >, - Partial { + Partial, + Partial { as?: keyof HTMLElementTagNameMap; className?: string; text: string; link?: string; openInNewTab?: boolean; - buttonStyle?: "inherit" | "custom"; - backgroundColor?: string; - textColor?: string; - borderColor?: string; - backgroundColorHover?: string; - textColorHover?: string; - borderColorHover?: string; } let Button = forwardRef((props, ref) => { @@ -158,10 +162,6 @@ export let buttonContentInputs: InspectorGroup["inputs"] = [ }, ]; export let buttonStylesInputs: InspectorGroup["inputs"] = [ - { - type: "heading", - label: "Button styles", - }, { type: "select", name: "buttonStyle", @@ -222,6 +222,10 @@ export let buttonStylesInputs: InspectorGroup["inputs"] = [ export let buttonInputs: InspectorGroup["inputs"] = [ ...buttonContentInputs, + { + type: "heading", + label: "Button styles", + }, ...buttonStylesInputs, ]; @@ -234,5 +238,4 @@ export let schema: HydrogenComponentSchema = { inputs: buttonInputs, }, ], - toolbar: ["general-settings", ["duplicate", "delete"]], }; diff --git a/app/components/Heading.tsx b/app/components/Heading.tsx index 51f9ca23..f0705aff 100644 --- a/app/components/Heading.tsx +++ b/app/components/Heading.tsx @@ -220,5 +220,4 @@ export let schema: HydrogenComponentSchema = { inputs: headingInputs, }, ], - toolbar: ["general-settings", ["duplicate", "delete"]], }; diff --git a/app/components/Paragraph.tsx b/app/components/Paragraph.tsx index e0a6e633..fb88bc32 100644 --- a/app/components/Paragraph.tsx +++ b/app/components/Paragraph.tsx @@ -128,5 +128,4 @@ export let schema: HydrogenComponentSchema = { ], }, ], - toolbar: ["general-settings", ["duplicate", "delete"]], }; diff --git a/app/components/SubHeading.tsx b/app/components/SubHeading.tsx index 659dbed5..e03a31ce 100644 --- a/app/components/SubHeading.tsx +++ b/app/components/SubHeading.tsx @@ -144,5 +144,4 @@ export let schema: HydrogenComponentSchema = { ], }, ], - toolbar: ["general-settings", ["duplicate", "delete"]], }; diff --git a/app/data/queries.ts b/app/data/queries.ts index e1201b77..2c1f3ce2 100644 --- a/app/data/queries.ts +++ b/app/data/queries.ts @@ -42,29 +42,6 @@ export let HOMEPAGE_FEATURED_PRODUCTS_QUERY = `#graphql ${PRODUCT_CARD_FRAGMENT} `; -// @see: https://shopify.dev/api/storefront/current/queries/collections -export let FEATURED_COLLECTIONS_QUERY = `#graphql - query homepageFeaturedCollections($country: CountryCode, $language: LanguageCode) - @inContext(country: $country, language: $language) { - collections( - first: 4, - sortKey: UPDATED_AT - ) { - nodes { - id - title - handle - image { - altText - width - height - url - } - } - } - } -` as const; - export let PRODUCT_INFO_QUERY = `#graphql query ProductInfo( $country: CountryCode @@ -462,20 +439,6 @@ export let VARIANTS_QUERY = `#graphql } ${PRODUCT_VARIANT_FRAGMENT} ` as const; -export let CUSTOMER_CREATE = - `#graphql mutation customerCreate($input: CustomerCreateInput!) { - customerCreate(input: $input) { - customer { - id - email - } - customerUserErrors { - field - message - code - } - } -}` as const; export const METAOBJECTS_QUERY = `#graphql query MetaObjects ($type: String!, $first: Int) diff --git a/app/lib/judgeme.ts b/app/lib/judgeme.ts index 50690ca7..a69a4fcb 100644 --- a/app/lib/judgeme.ts +++ b/app/lib/judgeme.ts @@ -34,7 +34,7 @@ export let getJudgemeReviews = async ( ) => { if (!api_token) { return { - error: "Missing JUDGEME_PUBLIC_TOKEN", + error: "Missing JUDGEME_PRIVATE_API_TOKEN", }; } let internalId = await getInternalIdByHandle(api_token, shop_domain, handle); diff --git a/app/lib/menu.ts b/app/lib/menu.ts new file mode 100644 index 00000000..55833b52 --- /dev/null +++ b/app/lib/menu.ts @@ -0,0 +1,6 @@ +export function getMaxDepth(item: { items: any[] }): number { + if (item.items?.length > 0) { + return Math.max(...item.items.map(getMaxDepth)) + 1; + } + return 1; + } \ No newline at end of file diff --git a/app/lib/type.ts b/app/lib/type.ts index 65c0b262..aa0d409c 100644 --- a/app/lib/type.ts +++ b/app/lib/type.ts @@ -25,3 +25,19 @@ export type I18nLocale = Locale & { export type Storefront = HydrogenStorefront; export type Alignment = "left" | "center" | "right"; + +export interface SingleMenuItem { + id: string; + title: string; + items: SingleMenuItem[]; + to: string; + resource?: { + image?: { + altText: string; + height: number; + id: string; + url: string; + width: number; + }; + } +} \ No newline at end of file diff --git a/app/lib/utils.ts b/app/lib/utils.ts index b57c125b..b5ded23f 100644 --- a/app/lib/utils.ts +++ b/app/lib/utils.ts @@ -1,7 +1,6 @@ import { useLocation } from "@remix-run/react"; import type { FulfillmentStatus } from "@shopify/hydrogen/customer-account-api-types"; import type { MoneyV2 } from "@shopify/hydrogen/storefront-api-types"; -import type { WeaverseImage } from "@weaverse/hydrogen"; import type { LinkHTMLAttributes } from "react"; import type { ChildMenuItemFragment, @@ -335,7 +334,11 @@ export function removeFalsy( } export function getImageAspectRatio( - image: Partial, + image: { + width?: number | null; + height?: number | null; + [key: string]: any; + }, aspectRatio: string, ) { let aspRt: string | undefined; diff --git a/app/modules/Drawer.tsx b/app/modules/Drawer.tsx index cf164ac0..83a4d3b8 100644 --- a/app/modules/Drawer.tsx +++ b/app/modules/Drawer.tsx @@ -1,6 +1,7 @@ import { Dialog, Transition } from "@headlessui/react"; +import { useLocation } from "@remix-run/react"; import clsx from "clsx"; -import { Fragment, useState } from "react"; +import { Fragment, useEffect, useState } from "react"; import { IconCaretLeft } from "~/components/Icons"; import { cn } from "~/lib/cn"; import { Heading, IconClose } from "~/modules"; @@ -141,6 +142,12 @@ Drawer.Title = Dialog.Title; export function useDrawer(openDefault = false) { const [isOpen, setIsOpen] = useState(openDefault); + let { pathname } = useLocation(); + useEffect(() => { + if (isOpen) { + closeDrawer(); + } + }, [pathname]); function openDrawer() { setIsOpen(true); diff --git a/app/modules/FeaturedCollections.tsx b/app/modules/FeaturedCollections.tsx deleted file mode 100644 index 3c0ce9fe..00000000 --- a/app/modules/FeaturedCollections.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { Image } from "@shopify/hydrogen"; - -import type { HomepageFeaturedCollectionsQuery } from "storefrontapi.generated"; -import { Heading, Section, Grid, Link } from "~/modules"; - -type FeaturedCollectionsProps = HomepageFeaturedCollectionsQuery & { - title?: string; - [key: string]: any; -}; - -export function FeaturedCollections({ - collections, - title = "Collections", - count, - ...props -}: FeaturedCollectionsProps) { - const haveCollections = collections?.nodes?.length > 0; - if (!haveCollections) return null; - - const collectionsWithImage = collections.nodes - .filter((item) => item.image) - .slice(0, count); - - return ( -
- - {collectionsWithImage.map((collection) => { - return ( - -
-
- {collection?.image && ( - {`Image - )} -
- {collection.title} -
- - ); - })} -
-
- ); -} diff --git a/app/modules/Layout.tsx b/app/modules/Layout.tsx index 4e8c36bb..76a024d5 100644 --- a/app/modules/Layout.tsx +++ b/app/modules/Layout.tsx @@ -1,41 +1,21 @@ import { Disclosure } from "@headlessui/react"; -import { Await, Form, useLocation, useParams } from "@remix-run/react"; -import { CartForm, useAnalytics } from "@shopify/hydrogen"; -import { Suspense, useEffect, useMemo } from "react"; -import useWindowScroll from "react-use/esm/useWindowScroll"; -import clsx from "clsx"; +import { Suspense } from "react"; import { type LayoutQuery } from "storefrontapi.generated"; import { - Cart, - CartLoading, + useIsHomePath, + type ChildEnhancedMenuItem, + type EnhancedMenu, +} from "~/lib/utils"; +import { CountrySelector, - Drawer, Heading, - IconAccount, - IconBag, IconCaret, - IconLogin, - IconMenu, - IconSearch, Link, - Section, - Text, - useDrawer, + Section } from "~/modules"; -import { useCartFetchers } from "~/hooks/useCartFetchers"; -import { useIsHydrated } from "~/hooks/useIsHydrated"; -import { - type ChildEnhancedMenuItem, - type EnhancedMenu, - useIsHomePath, -} from "~/lib/utils"; -import { useRootLoaderData } from "~/root"; +import { Header } from "./header"; -import { Logo } from "./Logo"; -import { PredictiveSearch } from "~/components/predictive-search/PredictiveSearch"; -import { DesktopMenu } from "./menu/DesktopMenu"; -import { MobileMenu } from "./menu/MobileMenu"; type LayoutProps = { children: React.ReactNode; @@ -67,296 +47,6 @@ export function Layout({ children, layout }: LayoutProps) { ); } -function Header({ title, menu }: { title: string; menu?: EnhancedMenu }) { - const isHome = useIsHomePath(); - - const { - isOpen: isCartOpen, - openDrawer: openCart, - closeDrawer: closeCart, - } = useDrawer(); - - const { - isOpen: isMenuOpen, - openDrawer: openMenu, - closeDrawer: closeMenu, - } = useDrawer(); - - const addToCartFetchers = useCartFetchers(CartForm.ACTIONS.LinesAdd); - - // toggle cart drawer when adding to cart - useEffect(() => { - if (isCartOpen || !addToCartFetchers.length) return; - openCart(); - }, [addToCartFetchers, isCartOpen, openCart]); - - return ( - <> - - {menu && ( - - )} - - - - ); -} - -function CartDrawer({ - isOpen, - onClose, -}: { - isOpen: boolean; - onClose: () => void; -}) { - const rootData = useRootLoaderData(); - - return ( - -
- }> - - {(cart) => } - - -
-
- ); -} - -export function MenuDrawer({ - isOpen, - onClose, - menu, -}: { - isOpen: boolean; - onClose: () => void; - menu: EnhancedMenu; -}) { - return ( - - - - ); -} - -function MobileHeader({ - title, - isHome, - openCart, - openMenu, -}: { - title: string; - isHome: boolean; - openCart: () => void; - openMenu: () => void; -}) { - // useHeaderStyleFix(containerStyle, setContainerStyle, isHome); - - const params = useParams(); - - return ( -
-
- -
- -
-
- - - -
- - -
-
- ); -} - -function DesktopHeader({ - isHome, - menu, - openCart, - title, -}: { - isHome: boolean; - openCart: () => void; - menu?: EnhancedMenu; - title: string; -}) { - const params = useParams(); - const { y } = useWindowScroll(); - return ( -
50 && " shadow-header", - "hidden h-nav lg:flex items-center sticky transition duration-300 backdrop-blur-lg z-40 top-0 justify-between leading-none gap-8", - "w-full px-6 md:px-8 lg:px-12", - )} - > - - -
- - - -
-
- ); -} - -function AccountLink({ className }: { className?: string }) { - const rootData = useRootLoaderData(); - const isLoggedIn = rootData?.isLoggedIn; - - return ( - - }> - }> - {(isLoggedIn) => (isLoggedIn ? : )} - - - - ); -} - -function SearchToggle() { - const { isOpen, closeDrawer, openDrawer } = useDrawer(); - let { pathname } = useLocation(); - useEffect(() => { - if (isOpen) { - closeDrawer(); - } - }, [pathname]); - return ( - <> - - - - - - ); -} - -function CartCount({ - isHome, - openCart, -}: { - isHome: boolean; - openCart: () => void; -}) { - const rootData = useRootLoaderData(); - - return ( - }> - - {(cart) => ( - - )} - - - ); -} - -function Badge({ - openCart, - dark, - count, - cart, -}: { - count: number; - dark: boolean; - openCart: () => void; - cart: any; -}) { - const isHydrated = useIsHydrated(); - - const BadgeCounter = useMemo( - () => ( - <> - -
- {count || 0} -
- - ), - // eslint-disable-next-line react-hooks/exhaustive-deps - [count, dark], - ); - - const { publish } = useAnalytics(); - - function handleOpenCart() { - publish("custom_sidecart_viewed", { cart }); - openCart(); - } - - return isHydrated ? ( - - ) : ( - - {BadgeCounter} - - ); -} function Footer({ menu }: { menu?: EnhancedMenu }) { const isHome = useIsHomePath(); diff --git a/app/modules/Logo.tsx b/app/modules/Logo.tsx index b715599d..3d838cfb 100644 --- a/app/modules/Logo.tsx +++ b/app/modules/Logo.tsx @@ -3,23 +3,25 @@ import { Image } from "@shopify/hydrogen"; import { Link } from "./Link"; -export function Logo() { +export function Logo({ showTransparent }: { showTransparent?: boolean }) { let settings = useThemeSettings(); let logoData = settings?.logoData; + let transparentLogoData = settings?.transparentLogoData; + if (!logoData) { return null; } return (
diff --git a/app/modules/ProductGallery.tsx b/app/modules/ProductGallery.tsx index 97653b0a..67c32048 100644 --- a/app/modules/ProductGallery.tsx +++ b/app/modules/ProductGallery.tsx @@ -19,7 +19,7 @@ export function ProductGallery({ return (
{media.map((med, i) => { const isFirst = i === 0; diff --git a/app/modules/ProductSwimlane.tsx b/app/modules/ProductSwimlane.tsx index f920cb65..3bf47149 100644 --- a/app/modules/ProductSwimlane.tsx +++ b/app/modules/ProductSwimlane.tsx @@ -18,7 +18,7 @@ export function ProductSwimlane({ }: ProductSwimlaneProps) { return (
-
+
{products.nodes.slice(0, count).map((product) => ( void; +}) { + const rootData = useRootLoaderData(); + + return ( + }> + + {(cart) => ( + + )} + + + ); +} + +function Badge({ + openCart, + dark, + count, + cart, +}: { + count: number; + dark: boolean; + openCart: () => void; + cart: any; +}) { + const isHydrated = useIsHydrated(); + + const BadgeCounter = useMemo( + () => ( + <> + +
+ {count || 0} +
+ + ), + // eslint-disable-next-line react-hooks/exhaustive-deps + [count, dark], + ); + + const { publish } = useAnalytics(); + + function handleOpenCart() { + publish("custom_sidecart_viewed", { cart }); + openCart(); + } + + return isHydrated ? ( + + ) : ( + + {BadgeCounter} + + ); +} diff --git a/app/modules/header/DesktopHeader.tsx b/app/modules/header/DesktopHeader.tsx new file mode 100644 index 00000000..016ea4c6 --- /dev/null +++ b/app/modules/header/DesktopHeader.tsx @@ -0,0 +1,109 @@ +import { Await, Link, useLocation } from "@remix-run/react"; +import { useThemeSettings } from "@weaverse/hydrogen"; +import clsx from "clsx"; +import { Suspense, useEffect, useState } from "react"; +import useWindowScroll from "react-use/esm/useWindowScroll"; +import { PredictiveSearch } from "~/components/predictive-search/PredictiveSearch"; +import { type EnhancedMenu } from "~/lib/utils"; +import { useRootLoaderData } from "~/root"; +import { Drawer, useDrawer } from "../Drawer"; +import { IconAccount, IconLogin, IconSearch } from "../Icon"; +import { Logo } from "../Logo"; +import { CartCount } from "./CartCount"; +import { DesktopMenu } from "./menu/DesktopMenu"; + +export function DesktopHeader({ + isHome, + menu, + openCart, + title, +}: { + isHome: boolean; + openCart: () => void; + menu?: EnhancedMenu; + title: string; +}) { + const { y } = useWindowScroll(); + let settings = useThemeSettings(); + let [hovered, setHovered] = useState(false); + let { isOpen, openDrawer, closeDrawer } = useDrawer(); + + let onHover = () => setHovered(true); + let onLeave = () => setHovered(false); + + let enableTransparent = settings?.enableTransparentHeader; + let isTransparent = enableTransparent && y < 50 && !isOpen && !hovered; + return ( +
+
+ + {menu && } +
+ + + +
+
+ ); +} + +function AccountLink({ className }: { className?: string }) { + const rootData = useRootLoaderData(); + const isLoggedIn = rootData?.isLoggedIn; + + return ( + + }> + }> + {(isLoggedIn) => (isLoggedIn ? : )} + + + + ); +} + +function SearchToggle({ isOpen, openDrawer, closeDrawer }: any) { + let { pathname } = useLocation(); + useEffect(() => { + if (isOpen) { + closeDrawer(); + } + }, [pathname]); + return ( + <> + + + + + + ); +} diff --git a/app/modules/header/index.tsx b/app/modules/header/index.tsx new file mode 100644 index 00000000..262d42df --- /dev/null +++ b/app/modules/header/index.tsx @@ -0,0 +1,180 @@ +import { Await, Form, Link, useParams } from "@remix-run/react"; +import { CartForm } from "@shopify/hydrogen"; +import { Suspense, useEffect } from "react"; +import { useCartFetchers } from "~/hooks/useCartFetchers"; +import { useIsHomePath, type EnhancedMenu } from "~/lib/utils"; +import { useRootLoaderData } from "~/root"; +import { Cart } from "../Cart"; +import { CartLoading } from "../CartLoading"; +import { Drawer, useDrawer } from "../Drawer"; +import { IconAccount, IconLogin, IconMenu, IconSearch } from "../Icon"; +import { Logo } from "../Logo"; +import { CartCount } from "./CartCount"; +import { DesktopHeader } from "./DesktopHeader"; +import { MobileMenu } from "./menu/MobileMenu"; + +export function Header({ + title, + menu, +}: { + title: string; + menu?: EnhancedMenu; +}) { + const isHome = useIsHomePath(); + + const { + isOpen: isCartOpen, + openDrawer: openCart, + closeDrawer: closeCart, + } = useDrawer(); + + const { + isOpen: isMenuOpen, + openDrawer: openMenu, + closeDrawer: closeMenu, + } = useDrawer(); + + const addToCartFetchers = useCartFetchers(CartForm.ACTIONS.LinesAdd); + + // toggle cart drawer when adding to cart + useEffect(() => { + if (isCartOpen || !addToCartFetchers.length) return; + openCart(); + }, [addToCartFetchers, isCartOpen, openCart]); + + return ( + <> + + {menu && ( + + )} + + + + ); +} + +function CartDrawer({ + isOpen, + onClose, +}: { + isOpen: boolean; + onClose: () => void; +}) { + const rootData = useRootLoaderData(); + + return ( + +
+ }> + + {(cart) => } + + +
+
+ ); +} + +export function MenuDrawer({ + isOpen, + onClose, + menu, +}: { + isOpen: boolean; + onClose: () => void; + menu: EnhancedMenu; +}) { + return ( + + {} + + ); +} + +function MobileHeader({ + title, + isHome, + openCart, + openMenu, +}: { + title: string; + isHome: boolean; + openCart: () => void; + openMenu: () => void; +}) { + // useHeaderStyleFix(containerStyle, setContainerStyle, isHome); + + const params = useParams(); + + return ( +
+
+ +
+ +
+
+ + + +
+ + +
+
+ ); +} + + +function AccountLink({ className }: { className?: string }) { + const rootData = useRootLoaderData(); + const isLoggedIn = rootData?.isLoggedIn; + + return ( + + }> + }> + {(isLoggedIn) => (isLoggedIn ? : )} + + + + ); +} diff --git a/app/modules/header/menu/DesktopMenu.tsx b/app/modules/header/menu/DesktopMenu.tsx new file mode 100644 index 00000000..f57e09ee --- /dev/null +++ b/app/modules/header/menu/DesktopMenu.tsx @@ -0,0 +1,195 @@ +import { Link } from "@remix-run/react"; +import { Image } from "@shopify/hydrogen"; +import clsx from "clsx"; +import React from "react"; +import { getMaxDepth } from "~/lib/menu"; +import type { SingleMenuItem } from "~/lib/type"; +import type { EnhancedMenu } from "~/lib/utils"; + +const dropdownContentClass = + "absolute overflow-hidden bg-white shadow-md z-10 dropdown-transition border-t"; + +export function DesktopMenu(props: { menu: EnhancedMenu }) { + let { menu } = props; + let items = menu.items as unknown as SingleMenuItem[]; + if (!items) return null; + return ( + + ); +} + +function MultiMenu(props: SingleMenuItem) { + let { title, items, to } = props; + + let renderList = (item: SingleMenuItem, idx: number) => ( +
+
+ + {item.title} + +
+
    + {item.items.map((subItem, ind) => ( +
  • + + {subItem.title} + +
  • + ))} +
+
+ ); + + let renderImageItem = (item: SingleMenuItem, idx: number) => ( +
+ + + +
+ {item.title} +
+
+ ); + return ( + +
+
+
+ {items.map((item, id) => + item.resource && item.items.length === 0 + ? renderImageItem(item, id) + : renderList(item, id), + )} +
+
+
+
+
+ ); +} + +function SingleMenu(props: SingleMenuItem) { + let { title, items, to } = props; + return ( + +
+
+
+
+ + {title} + +
+
    + {items.map((subItem, ind) => ( +
  • + + {subItem.title} + +
  • + ))} +
+
+
+
+
+ ); +} + +function ImageMenu({ title, items, to }: SingleMenuItem) { + return ( + +
+
+
+ {items.map((item, id) => ( + +
+ +
+ {item.title} +
+
+ + ))} +
+
+
+
+ ); +} + +function GroupHeader({ title, to }: { title: string; to: string }) { + return ( +
+ + {title} + +
+ ); +} + +function GroupWrapper(props: { + children?: React.ReactNode; + className?: string; + title: string; + to: string; +}) { + let { children, className, title, to } = props; + return ( +
+ + {children} +
+ ); +} diff --git a/app/modules/menu/MobileMenu.tsx b/app/modules/header/menu/MobileMenu.tsx similarity index 62% rename from app/modules/menu/MobileMenu.tsx rename to app/modules/header/menu/MobileMenu.tsx index c2536e17..d2982c8d 100644 --- a/app/modules/menu/MobileMenu.tsx +++ b/app/modules/header/menu/MobileMenu.tsx @@ -2,39 +2,42 @@ import { Disclosure } from "@headlessui/react"; import { Link } from "@remix-run/react"; import { Image } from "@shopify/hydrogen"; import { IconCaretDown, IconCaretRight } from "~/components/Icons"; -import { Drawer, useDrawer } from "../Drawer"; -import { - Nav_Items, - type ImageItem, - type MultiMenuProps, - type SingleMenuProps, -} from "./defines"; +import { getMaxDepth } from "~/lib/menu"; +import type { EnhancedMenu } from "~/lib/utils"; +import { Drawer, useDrawer } from "~/modules/Drawer"; +import type { SingleMenuItem } from "~/lib/type"; -const MenuByType = { - multi: MultiMenu, - image: ImageMenu, - single: SingleMenu, -}; - -export function MobileMenu() { +export function MobileMenu({ menu }: { menu: EnhancedMenu }) { + let items = menu.items as unknown as SingleMenuItem[]; + console.log("🚀 ~ items:", items); return ( ); } -function MultiMenu(props: MultiMenuProps) { +function MultiMenu(props: SingleMenuItem) { const { isOpen: isMenuOpen, openDrawer: openMenu, closeDrawer: closeMenu, } = useDrawer(); - let { title, items } = props; + let { title, to, items } = props; let content = (
- {item.title} - - {open ? ( - - ) : ( - - )} - + + {item.title} + + {item?.items?.length > 0 && ( + + {open ? ( + + ) : ( + + )} + + )}
{item?.items?.length > 0 ? ( @@ -76,7 +83,6 @@ function MultiMenu(props: MultiMenuProps) { key={ind} to={subItem.to} prefetch="intent" - className="animate-hover" > {subItem.title} @@ -101,7 +107,9 @@ function MultiMenu(props: MultiMenuProps) { role="button" onClick={openMenu} > - {title}{" "} + + {title} +
{content} @@ -109,13 +117,7 @@ function MultiMenu(props: MultiMenuProps) { ); } -function ImageMenu({ - title, - imageItems, -}: { - title: string; - imageItems: ImageItem[]; -}) { +function ImageMenu({ title, items, to }: SingleMenuItem) { const { isOpen: isMenuOpen, openDrawer: openMenu, @@ -131,15 +133,15 @@ function ImageMenu({ bordered >
- {imageItems.map((item, id) => ( + {items.map((item, id) => (
-
+
{item.title}
@@ -155,21 +157,23 @@ function ImageMenu({ role="button" onClick={openMenu} > - {title}{" "} - + + {title} + +
{content}
); } -function SingleMenu(props: SingleMenuProps) { +function SingleMenu(props: SingleMenuItem) { const { isOpen: isMenuOpen, openDrawer: openMenu, closeDrawer: closeMenu, } = useDrawer(); - let { title, items } = props; + let { title, items, to } = props; let content = ( {subItem.title} @@ -204,10 +207,22 @@ function SingleMenu(props: SingleMenuProps) { role="button" onClick={openMenu} > - {title}{" "} - + + {title} + +
{content}
); } + +function ItemHeader({ title, to }: { title: string; to: string }) { + return ( +
+ + {title} + +
+ ); +} diff --git a/app/modules/menu/DesktopMenu.tsx b/app/modules/menu/DesktopMenu.tsx deleted file mode 100644 index d166b68b..00000000 --- a/app/modules/menu/DesktopMenu.tsx +++ /dev/null @@ -1,175 +0,0 @@ -import { Link } from "@remix-run/react"; -import { Image } from "@shopify/hydrogen"; -import { - Nav_Items, - type ImageMenuProps, - type MultiMenuProps, - type SingleMenuProps, -} from "./defines"; -import clsx from "clsx"; - -const MenuByType = { - multi: MultiMenu, - image: ImageMenu, - single: SingleMenu, -}; - -const commonAnimatedClass = - "absolute h-0 opacity-0 overflow-hidden bg-white shadow-md transition-all ease-out group-hover:opacity-100 group-hover:border-t duration-500 group-hover:duration-300 group-hover:z-50 "; - -export function DesktopMenu() { - return ( - - ); -} - -function ItemHeader({ title, to }: { title: string; to: string }) { - return ( -
- -
- ); -} - -function MultiMenu(props: MultiMenuProps) { - let { title, items, to, imageItems } = props; - return ( -
- -
-
-
- {items.map((item, id) => ( -
-
- - {item.title} - -
-
    - {item.items.map((subItem, ind) => ( -
  • - - {subItem.title} - -
  • - ))} -
-
- ))} - {imageItems.map((item, id) => ( -
- - - -
- {item.title} -
-
- ))} -
-
-
-
-
- ); -} - -function SingleMenu(props: SingleMenuProps) { - let { title, items, to } = props; - return ( -
- -
-
-
-
- - {title} - -
-
    - {items.map((subItem, ind) => ( -
  • - - {subItem.title} - -
  • - ))} -
-
-
-
-
- ); -} - -function ImageMenu({ title, imageItems, to }: ImageMenuProps) { - return ( -
- -
-
-
- {imageItems.map((item, id) => ( - -
- -
- {item.title} -
-
- - ))} -
-
-
-
- ); -} diff --git a/app/modules/menu/defines.ts b/app/modules/menu/defines.ts deleted file mode 100644 index 959ff3b1..00000000 --- a/app/modules/menu/defines.ts +++ /dev/null @@ -1,249 +0,0 @@ -interface Item { - title: string; - to: string; -} -interface SingleMenuItem { - title: string; - items: Item[]; - to: string; -} - -export interface ImageItem { - title: string; - data: { - altText: string; - url: string; - width: number; - height: number; - }; - to: string; -} - -export interface MultiMenuProps { - title: string; - items: SingleMenuItem[]; - imageItems: ImageItem[]; - to: string; -} - -export interface SingleMenuProps { - title: string; - items: Item[]; - to: string; -} -export interface ImageMenuProps { - title: string; - imageItems: ImageItem[]; - to: string; -} - -let items = [ - { - title: "Best Sellers", - to: "/best-sellers", - items: [ - { - title: "Black Friday", - to: "/black-friday", - }, - { - title: "History Month", - to: "/history-month", - }, - { - title: "Outlets", - to: "/outlets", - }, - ], - }, - { - title: "SHIRTS & TEES", - to: "/shirts-tees", - items: [ - { - title: "New Arrivals", - to: "/new-arrivals", - }, - { - title: "Tops", - to: "/tops", - }, - { - title: "Jackets", - to: "/jackets", - }, - { - title: "Denims", - to: "/denims", - }, - { - title: "Pants", - to: "/pants", - }, - ], - }, - { - title: "PANTS & JEANS", - to: "/pants-jeans", - items: [ - { - title: "New Arrivals", - to: "/new-arrivals", - }, - { - title: "Scarfs", - to: "/scarfs", - }, - { - title: "Hats", - to: "/hats", - }, - { - title: "Jewelries", - to: "/jewelries", - }, - ], - }, - { - title: "Accessories", - to: "/accessories", - items: [ - { - title: "Bags", - to: "/bags", - }, - { - title: "Earrings", - to: "/earrings", - }, - { - title: "Hats", - to: "/hats", - }, - { - title: "Socks", - to: "/socks", - }, - { - title: "Belts", - to: "/belts", - }, - ], - }, -]; -let imageItems = [ - { - title: "Women's Jackets", - data: { - altText: "Women", - url: "https://cdn.shopify.com/s/files/1/0838/0052/3057/files/woman.jpg?v=1717490382", - width: 860, - height: 1500, - }, - to: "/collections/jackets", - }, - { - title: "Women's Jackets", - data: { - altText: "Women", - url: "https://cdn.shopify.com/s/files/1/0838/0052/3057/files/woman.jpg?v=1717490382", - width: 860, - height: 1500, - }, - to: "/collections/jackets", - }, -]; -let imageMenuItems: ImageItem[] = [ - { - title: "Women's Jackets", - data: { - altText: "Women", - url: "https://cdn.shopify.com/s/files/1/0838/0052/3057/files/woman.jpg?v=1717490382", - width: 860, - height: 1500, - }, - to: "/collections/jackets", - }, - { - title: "Women's Jackets", - data: { - altText: "Women", - url: "https://cdn.shopify.com/s/files/1/0838/0052/3057/files/woman.jpg?v=1717490382", - width: 860, - height: 1500, - }, - to: "/collections/jackets", - }, - { - title: "Women's Jackets", - data: { - altText: "Women", - url: "https://cdn.shopify.com/s/files/1/0838/0052/3057/files/woman.jpg?v=1717490382", - width: 860, - height: 1500, - }, - to: "/collections/jackets", - }, - { - title: "Women's Jackets", - data: { - altText: "Women", - url: "https://cdn.shopify.com/s/files/1/0838/0052/3057/files/woman.jpg?v=1717490382", - width: 860, - height: 1500, - }, - to: "/collections/jackets", - }, -]; - -let popupItems: Item[] = [ - { - title: "Journal", - to: "/journal", - }, - { - title: "Shipping & returns", - to: "/shipping-returns", - }, - { - title: "About us", - to: "/about-us", - }, -]; - -interface NavItem { - title: string; - type: "multi" | "image" | "single"; - items?: SingleMenuItem[] | Item[]; - imageItems?: ImageItem[]; - to: string; -} - -export let Nav_Items: NavItem[] = [ - { - title: "Woman", - type: "multi", - items: items, - imageItems: imageItems, - to: "/women", - }, - { - title: "Men", - type: "multi", - items: items, - imageItems: [], - to: "/men", - }, - { - title: "Accesories", - type: "image", - imageItems: imageMenuItems, - to: "/accessories", - }, - { - title: "Pilot", - type: "single", - items: popupItems, - to: "/pilot", - }, -]; diff --git a/app/modules/product-form/judgeme-review.tsx b/app/modules/product-form/judgeme-review.tsx index d16ad095..a4e27296 100644 --- a/app/modules/product-form/judgeme-review.tsx +++ b/app/modules/product-form/judgeme-review.tsx @@ -60,7 +60,6 @@ export default JudgemeReview; export let schema: HydrogenComponentSchema = { type: "judgeme", title: "Judgeme review", - toolbar: ["general-settings", ["duplicate", "delete"]], inspector: [ { group: "Judgeme", diff --git a/app/root.tsx b/app/root.tsx index 832aac0a..d0596259 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -255,20 +255,47 @@ const LAYOUT_QUERY = `#graphql fragment MenuItem on MenuItem { id resourceId + resource { + ... on Collection { + image { + altText + height + id + url + width + } + } + ... on Product { + image: featuredImage { + altText + height + id + url + width + } + } + } tags title type url } + fragment ChildMenuItem on MenuItem { ...MenuItem } - fragment ParentMenuItem on MenuItem { + fragment ParentMenuItem2 on MenuItem { ...MenuItem items { ...ChildMenuItem } } + fragment ParentMenuItem on MenuItem { + ...MenuItem + items { + ...ParentMenuItem2 + } + } fragment Menu on Menu { id items { diff --git a/app/routes/($locale).api.customer.ts b/app/routes/($locale).api.customer.ts new file mode 100644 index 00000000..fa6612c9 --- /dev/null +++ b/app/routes/($locale).api.customer.ts @@ -0,0 +1,47 @@ +import type { + ActionFunction, + ActionFunctionArgs, +} from "@remix-run/server-runtime"; +import { json } from "@remix-run/server-runtime"; +import type { CustomerCreateMutation } from "storefrontapi.generated"; + +let CUSTOMER_CREATE = `#graphql + mutation customerCreate($input: CustomerCreateInput!) { + customerCreate(input: $input) { + customer { + firstName + lastName + email + phone + acceptsMarketing + } + customerUserErrors { + field + message + code + } + } + } +` as const; + +export let action: ActionFunction = async ({ + request, + context, +}: ActionFunctionArgs) => { + let formData = await request.formData(); + let email = formData.get("email") as string; + let { storefront } = context; + + let { customerCreate, errors: queryError } = + await storefront.mutate(CUSTOMER_CREATE, { + variables: { + input: { email, password: "5hopify" }, + }, + }); + let customer = customerCreate?.customer; + let errors = customerCreate?.customerUserErrors || queryError; + if (errors && errors.length) { + return json({ errors }, { status: 200 }); + } + return json({ customer }, { status: 201 }); +}; diff --git a/app/routes/($locale).api.review.$productHandle.tsx b/app/routes/($locale).api.review.$productHandle.tsx index eef7e14c..655223bc 100644 --- a/app/routes/($locale).api.review.$productHandle.tsx +++ b/app/routes/($locale).api.review.$productHandle.tsx @@ -7,7 +7,7 @@ export async function loader(args: RouteLoaderArgs) { let { params, context } = args; let env = context.env; let handle = params.productHandle; - let api_token = env.JUDGEME_PUBLIC_TOKEN; + let api_token = env.JUDGEME_PRIVATE_API_TOKEN; let shop_domain = env.PUBLIC_STORE_DOMAIN; invariant(handle, "Missing product handle"); let reviews = await getJudgemeReviews(api_token, shop_domain, handle); diff --git a/app/routes/($locale).contact.ts b/app/routes/($locale).contact.ts deleted file mode 100644 index 3f9a5464..00000000 --- a/app/routes/($locale).contact.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { ActionFunction } from "@shopify/remix-oxygen"; - -export let action: ActionFunction = async ({ request, context }) => { - let { env } = context; - const formData = await request.formData(); - let url = `${env.WEAVERSE_HOST}/api/public/mail`; - let res = await fetch(url, { - method: "POST", - headers: { - "Content-Type": "application/json", - "x-api-key": env.WEAVERSE_API_KEY, - }, - body: JSON.stringify({ - // to: env.STORE_EMAIL, - email: formData.get("email"), - name: formData.get("name"), - subject: `New message from ${formData.get("name")}`, - message: formData.get("message"), - }), - }); - let resText = await res.text(); - return new Response(resText, { - status: res.status, - statusText: res.statusText, - headers: { - "Content-Type": "html/text", - }, - }); -}; diff --git a/app/routes/($locale).products.$productHandle.tsx b/app/routes/($locale).products.$productHandle.tsx index 915690e1..2d4ebdf2 100644 --- a/app/routes/($locale).products.$productHandle.tsx +++ b/app/routes/($locale).products.$productHandle.tsx @@ -72,7 +72,7 @@ export async function loader({ params, request, context }: LoaderFunctionArgs) { url: request.url, }); - let judgeme_API_TOKEN = context.env.JUDGEME_PUBLIC_TOKEN; + let judgeme_API_TOKEN = context.env.JUDGEME_PRIVATE_API_TOKEN; let judgemeReviews = null; if (judgeme_API_TOKEN) { let shop_domain = context.env.PUBLIC_STORE_DOMAIN; diff --git a/app/sections/ali-reviews/index.tsx b/app/sections/ali-reviews/index.tsx index 2725dcbb..e99cf1e4 100644 --- a/app/sections/ali-reviews/index.tsx +++ b/app/sections/ali-reviews/index.tsx @@ -8,11 +8,16 @@ import type { SectionProps } from "~/components/Section"; import { Section, layoutInputs } from "~/components/Section"; import { type AliReview } from "./review-item"; -type AliReviewsProps = SectionProps>>; +type AliReviewsData = { + aliReviewsApiKey: string; +}; + +type AliReviewsProps = SectionProps>> & + AliReviewsData; let AliReviewSection = forwardRef( (props, ref) => { - let { children, loaderData, ...rest } = props; + let { children, loaderData, aliReviewsApiKey, ...rest } = props; return (
{children} @@ -22,28 +27,32 @@ let AliReviewSection = forwardRef( ); export type AliReviewsLoaderData = Awaited>; +type AliReviewsAPIPayload = { + data: { reviews: AliReview[]; cursor: string }; + message: string; + status: number; +}; -export let loader = async ({ weaverse }: ComponentLoaderArgs<{}, Env>) => { +export let loader = async ({ + data: { aliReviewsApiKey = "" }, + weaverse, +}: ComponentLoaderArgs) => { let res = await weaverse - .fetchWithCache<{ - data: { reviews: AliReview[]; cursor: string }; - message: string; - status: number; - }>("https://widget-hub-api.alireviews.io/api/public/reviews", { - method: "GET", - headers: { - // https://support.fireapps.io/en/article/ali-reviews-learn-more-about-integration-using-api-key-hklfr0/ - Authorization: `Bearer ${weaverse.env.ALI_REVIEWS_API_KEY ?? ""}`, - "Content-Type": "application/json", + .fetchWithCache( + "https://widget-hub-api.alireviews.io/api/public/reviews", + { + method: "GET", + headers: { + Authorization: `Bearer ${aliReviewsApiKey}`, + "Content-Type": "application/json", + }, }, - }) + ) .catch((err) => { - console.log("🚀 ~ loader ~ err", err); + console.error(err); return { data: { reviews: [], cursor: "" }, message: "", status: 0 }; }); - let { data } = res; - - return data.reviews; + return res?.data?.reviews; }; export default AliReviewSection; @@ -52,6 +61,20 @@ export let schema: HydrogenComponentSchema = { type: "ali-reviews", title: "Ali Reviews box", inspector: [ + { + group: "Integration", + inputs: [ + { + type: "text", + name: "aliReviewsApiKey", + label: "Ali Reviews API key", + defaultValue: "", + placeholder: "Your Ali Reviews API key", + helpText: `Learn how to get your API key from Ali Reviews app.`, + shouldRevalidate: true, + }, + ], + }, { group: "Layout", inputs: layoutInputs.filter((inp) => inp.name !== "borderRadius"), @@ -68,14 +91,13 @@ export let schema: HydrogenComponentSchema = { "paragraph", "button", ], - toolbar: ["general-settings", ["duplicate", "delete"]], presets: { children: [ { type: "heading", content: "Reviews" }, { type: "paragraph", content: - "This section demonstrates how to integrate with third-party apps using their public APIs. Reviews are fetched from Ali Reviews API.", + "This section demonstrates how to integrate with third-party apps using their public APIs. Reviews are fetched from Ali Reviews API on the server side.", }, { type: "ali-reviews--list", diff --git a/app/sections/ali-reviews/review-list.tsx b/app/sections/ali-reviews/review-list.tsx index 4a6a8043..51886b03 100644 --- a/app/sections/ali-reviews/review-list.tsx +++ b/app/sections/ali-reviews/review-list.tsx @@ -194,7 +194,6 @@ export let schema: HydrogenComponentSchema = { ], }, ], - toolbar: ["general-settings", ["duplicate", "delete"]], }; function getReviewsSummary(allReviews: AliReview[]) { diff --git a/app/sections/all-products.tsx b/app/sections/all-products.tsx index fd17be2b..1127b214 100644 --- a/app/sections/all-products.tsx +++ b/app/sections/all-products.tsx @@ -81,7 +81,6 @@ export let schema: HydrogenComponentSchema = { enabledOn: { pages: ["ALL_PRODUCTS"], }, - toolbar: ["general-settings"], inspector: [ { group: "All products", diff --git a/app/sections/blog-post.tsx b/app/sections/blog-post.tsx index f2b061f6..c544ed2b 100644 --- a/app/sections/blog-post.tsx +++ b/app/sections/blog-post.tsx @@ -79,7 +79,6 @@ export let schema: HydrogenComponentSchema = { enabledOn: { pages: ["ARTICLE"], }, - toolbar: ["general-settings"], inspector: [ { group: "Blog post", diff --git a/app/sections/blogs.tsx b/app/sections/blogs.tsx index b1203350..c89da5b8 100644 --- a/app/sections/blogs.tsx +++ b/app/sections/blogs.tsx @@ -135,7 +135,6 @@ export let schema: HydrogenComponentSchema = { enabledOn: { pages: ["BLOG"], }, - toolbar: ["general-settings", ["delete"]], inspector: [ { group: "Blogs", diff --git a/app/sections/collection-banner.tsx b/app/sections/collection-banner.tsx index 27477404..76bf1110 100644 --- a/app/sections/collection-banner.tsx +++ b/app/sections/collection-banner.tsx @@ -98,7 +98,6 @@ export default CollectionBanner; export let schema: HydrogenComponentSchema = { type: "collection-banner", title: "Collection banner", - toolbar: ["general-settings", ["duplicate", "delete"]], enabledOn: { pages: ["COLLECTION"], }, diff --git a/app/sections/collection-filters/index.tsx b/app/sections/collection-filters/index.tsx index 2c3dbb09..bcb7ff8a 100644 --- a/app/sections/collection-filters/index.tsx +++ b/app/sections/collection-filters/index.tsx @@ -116,7 +116,6 @@ export let schema: HydrogenComponentSchema = { enabledOn: { pages: ["COLLECTION"], }, - toolbar: ["general-settings"], inspector: [ { group: "Collection filters", diff --git a/app/sections/collection-list/index.tsx b/app/sections/collection-list/index.tsx index 86c6d717..84a0abbe 100644 --- a/app/sections/collection-list/index.tsx +++ b/app/sections/collection-list/index.tsx @@ -73,7 +73,6 @@ export let schema: HydrogenComponentSchema = { enabledOn: { pages: ["COLLECTION_LIST"], }, - toolbar: ["general-settings"], inspector: [ { group: "Collection list", diff --git a/app/sections/columns-with-images/column.tsx b/app/sections/columns-with-images/column.tsx index ee49fb23..20120482 100644 --- a/app/sections/columns-with-images/column.tsx +++ b/app/sections/columns-with-images/column.tsx @@ -5,30 +5,41 @@ import { type HydrogenComponentSchema, type WeaverseImage, } from "@weaverse/hydrogen"; -import clsx from "clsx"; +import type { VariantProps } from "class-variance-authority"; +import { cva } from "class-variance-authority"; +import type { CSSProperties } from "react"; import { forwardRef } from "react"; import type { ButtonProps } from "~/components/Button"; import Button, { buttonContentInputs } from "~/components/Button"; +let variants = cva("", { + variants: { + size: { + large: "col-span-6", + medium: "col-span-4", + }, + hideOnMobile: { + true: "hidden sm:block", + false: "", + }, + }, +}); + interface ColumnWithImageItemProps - extends Pick, + extends VariantProps, + Pick, HydrogenComponentProps { imageSrc: WeaverseImage; + imageBorderRadius: number; heading: string; content: string; - hideOnMobile: boolean; - size: "large" | "medium"; } -let sizeMap = { - large: "col-span-6", - medium: "col-span-4", -}; - let ColumnWithImageItem = forwardRef( (props, ref) => { let { imageSrc, + imageBorderRadius, heading, content, text, @@ -40,24 +51,23 @@ let ColumnWithImageItem = forwardRef( ...rest } = props; - let imageData = - typeof imageSrc === "object" - ? imageSrc - : { url: imageSrc || IMAGES_PLACEHOLDERS.image, altText: imageSrc }; return (
-
+
{heading &&

{heading}

} - {content &&

{content}

} + {content &&

} {text && (