Skip to content

Commit e3ebec5

Browse files
authored
Merge pull request #265 from Weaverse/dev
v3.5.0
2 parents 6fce996 + a8439cc commit e3ebec5

53 files changed

Lines changed: 4531 additions & 4291 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,8 @@ WEAVERSE_PROJECT_ID=clptu3l4p001sxfsn1u9jzqnm
2323
# Custom metafields & metaobjects
2424
METAOBJECT_COLORS_TYPE="shopify--color-pattern"
2525
CUSTOM_COLLECTION_BANNER_METAFIELD="custom.collection_banner"
26+
27+
28+
# Shopify Inbox
29+
## https://github.com/juanpprieto/hydrogen-chat-inbox?tab=readme-ov-file#1-find-your-shopify-inbox-shop-id
30+
PUBLIC_SHOPIFY_INBOX_SHOP_ID=your-shopify-inbox-shop-id

app/components/button.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export let variants = cva(
99
[
1010
"inline-flex items-center justify-center rounded-none relative",
1111
"text-base leading-tight font-normal whitespace-nowrap",
12-
"focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50",
12+
"focus-visible:outline-none disabled:cursor-not-allowed disabled:!opacity-50",
1313
"transition-colors",
1414
],
1515
{
@@ -62,7 +62,7 @@ export let variants = cva(
6262
defaultVariants: {
6363
variant: "primary",
6464
},
65-
}
65+
},
6666
);
6767

6868
export interface ButtonStyleProps {
@@ -148,7 +148,7 @@ function Spinner() {
148148
let style = { "--duration": "500ms" } as React.CSSProperties;
149149
return (
150150
<span
151-
className="button-spinner absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2"
151+
className="[&~*]:invisible absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2"
152152
style={style}
153153
>
154154
<CircleNotch className="animate-spin w-5 h-5" />

app/components/product/add-to-cart-button.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { useEffect } from "react";
1515
import { useMemo } from "react";
1616
import { Button } from "~/components/button";
1717
import { openCartDrawer } from "~/components/layout/cart-drawer";
18+
import { cn } from "~/utils/cn";
1819
import { DEFAULT_LOCALE } from "~/utils/const";
1920

2021
export function AddToCartButton({
@@ -50,7 +51,10 @@ export function AddToCartButton({
5051
/>
5152
<Button
5253
type="submit"
53-
className={className}
54+
className={cn(
55+
"hover:text-[--btn-primary-text] hover:bg-[--btn-primary-bg]",
56+
className,
57+
)}
5458
disabled={disabled ?? fetcher.state !== "idle"}
5559
onClick={openCartDrawer}
5660
{...props}
@@ -108,7 +112,7 @@ function AddToCartAnalytics({
108112
try {
109113
if (cartInputs.inputs.analytics) {
110114
let dataInForm: unknown = JSON.parse(
111-
String(cartInputs.inputs.analytics)
115+
String(cartInputs.inputs.analytics),
112116
);
113117
Object.assign(cartData, dataInForm);
114118
}

app/components/product/badges.tsx

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useMoney as parseMoney } from "@shopify/hydrogen";
1+
import { useMoney } from "@shopify/hydrogen";
22
import type { MoneyV2 } from "@shopify/hydrogen/storefront-api-types";
33
import { useThemeSettings } from "@weaverse/hydrogen";
44
import { clsx } from "clsx";
@@ -23,7 +23,7 @@ function Badge({
2323
borderRadius: `${badgeBorderRadius}px`,
2424
textTransform: badgeTextTransform,
2525
}}
26-
className={clsx("px-1.5 py-1 uppercase text-sm leading-none", className)}
26+
className={clsx("px-1.5 py-1 uppercase text-sm", className)}
2727
>
2828
{text}
2929
</span>
@@ -76,8 +76,9 @@ export function SaleBadge({
7676
}: { price: MoneyV2; compareAtPrice: MoneyV2; className?: string }) {
7777
let { saleBadgeText = "Sale", saleBadgeColor } = useThemeSettings();
7878
let { amount, percentage } = calculateDiscount(price, compareAtPrice);
79+
let discountAmount = useMoney({ amount, currencyCode: price.currencyCode });
7980
let text = saleBadgeText
80-
.replace("[amount]", amount)
81+
.replace("[amount]", discountAmount.withoutTrailingZeros)
8182
.replace("[percentage]", percentage);
8283

8384
if (percentage !== "0") {
@@ -92,23 +93,13 @@ export function SaleBadge({
9293
return null;
9394
}
9495

95-
function isNewArrival(date: string, daysOld = 30) {
96-
return (
97-
new Date(date).valueOf() >
98-
new Date().setDate(new Date().getDate() - daysOld).valueOf()
99-
);
100-
}
101-
10296
function calculateDiscount(price: MoneyV2, compareAtPrice: MoneyV2) {
10397
if (price?.amount && compareAtPrice?.amount) {
10498
let priceNumber = Number(price.amount);
10599
let compareAtPriceNumber = Number(compareAtPrice.amount);
106100
if (compareAtPriceNumber > priceNumber) {
107101
return {
108-
amount: parseMoney({
109-
amount: String(compareAtPriceNumber - priceNumber),
110-
currencyCode: price.currencyCode,
111-
}).withoutTrailingZeros,
102+
amount: String(compareAtPriceNumber - priceNumber),
112103
percentage: Math.round(
113104
((compareAtPriceNumber - priceNumber) / compareAtPriceNumber) * 100,
114105
).toString(),
@@ -117,3 +108,10 @@ function calculateDiscount(price: MoneyV2, compareAtPrice: MoneyV2) {
117108
}
118109
return { amount: "0", percentage: "0" };
119110
}
111+
112+
function isNewArrival(date: string, daysOld = 30) {
113+
return (
114+
new Date(date).valueOf() >
115+
new Date().setDate(new Date().getDate() - daysOld).valueOf()
116+
);
117+
}

app/components/product/judgeme-review.tsx

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { useParentInstance } from "@weaverse/hydrogen";
77
import { forwardRef, useEffect } from "react";
88
import { StarRating } from "~/components/star-rating";
99
import { usePrefixPathWithLocale } from "~/hooks/use-prefix-path-with-locale";
10+
import type { loader as productRouteLoader } from "~/routes/($locale).products.$productHandle";
1011

1112
type JudgemeReviewsData = {
1213
rating: number;
@@ -16,32 +17,22 @@ type JudgemeReviewsData = {
1617

1718
let JudgemeReview = forwardRef<HTMLDivElement, HydrogenComponentProps>(
1819
(props, ref) => {
19-
let loaderData = useLoaderData<{
20-
judgemeReviews: JudgemeReviewsData;
21-
}>();
22-
let judgemeReviews = loaderData?.judgemeReviews;
20+
let { productReviews } = useLoaderData<typeof productRouteLoader>();
2321
let { load, data: fetchData } = useFetcher<JudgemeReviewsData>();
2422
let context = useParentInstance();
2523
let handle = context?.data?.product?.handle;
2624
let api = usePrefixPathWithLocale(`/api/review/${handle}`);
2725

2826
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
2927
useEffect(() => {
30-
if (judgemeReviews || !handle) return;
28+
if (productReviews || !handle) return;
3129
load(api);
3230
// eslint-disable-next-line react-hooks/exhaustive-deps
3331
}, [handle, api]);
3432

35-
let data = judgemeReviews || fetchData;
33+
let data = productReviews || fetchData;
3634

3735
if (!data) return null;
38-
if (data.error) {
39-
return (
40-
<div {...props} ref={ref}>
41-
{data.error}
42-
</div>
43-
);
44-
}
4536

4637
let rating = Math.round((data.rating || 0) * 100) / 100;
4738
let reviewNumber = data.reviewNumber || 0;
@@ -54,7 +45,7 @@ let JudgemeReview = forwardRef<HTMLDivElement, HydrogenComponentProps>(
5445
</div>
5546
</div>
5647
);
57-
}
48+
},
5849
);
5950

6051
export default JudgemeReview;
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import { ArrowLeft, ArrowRight, VideoCamera, X } from "@phosphor-icons/react";
2+
import * as Dialog from "@radix-ui/react-dialog";
3+
import * as VisuallyHidden from "@radix-ui/react-visually-hidden";
4+
import clsx from "clsx";
5+
import type {
6+
MediaFragment,
7+
Media_MediaImage_Fragment,
8+
Media_Video_Fragment,
9+
} from "storefront-api.generated";
10+
import { Button } from "~/components/button";
11+
import { Image } from "~/components/image";
12+
import { ScrollArea } from "~/components/scroll-area";
13+
import { cn } from "~/utils/cn";
14+
import { getImageAspectRatio } from "~/utils/image";
15+
16+
export function ZoomModal({
17+
media,
18+
zoomMediaId,
19+
setZoomMediaId,
20+
open,
21+
onOpenChange,
22+
}: {
23+
media: MediaFragment[];
24+
zoomMediaId: string;
25+
setZoomMediaId: (id: string) => void;
26+
open: boolean;
27+
onOpenChange: (open: boolean) => void;
28+
}) {
29+
let zoomMedia = media.find((med) => med.id === zoomMediaId);
30+
let zoomMediaIndex = media.findIndex((med) => med.id === zoomMediaId);
31+
let nextMedia = media[zoomMediaIndex + 1] ?? media[0];
32+
let prevMedia = media[zoomMediaIndex - 1] ?? media[media.length - 1];
33+
34+
return (
35+
<Dialog.Root open={open} onOpenChange={onOpenChange}>
36+
<Dialog.Portal>
37+
<Dialog.Overlay
38+
className="fixed inset-0 bg-white data-[state=open]:animate-fade-in z-10"
39+
style={{ "--fade-in-duration": "100ms" } as React.CSSProperties}
40+
/>
41+
<Dialog.Content
42+
className={clsx([
43+
"fixed inset-0 w-screen z-10",
44+
"data-[state=open]:animate-slide-up",
45+
])}
46+
style={
47+
{
48+
"--slide-up-from": "20px",
49+
"--slide-up-duration": "300ms",
50+
} as React.CSSProperties
51+
}
52+
aria-describedby={undefined}
53+
>
54+
<div className="w-full h-full flex items-center justify-center bg-[--color-background] relative">
55+
<VisuallyHidden.Root asChild>
56+
<Dialog.Title>Product media zoom</Dialog.Title>
57+
</VisuallyHidden.Root>
58+
<div className="hidden md:block absolute top-10 left-8">
59+
<ScrollArea className="max-h-[700px]" size="sm">
60+
<div className="w-24 pr-2 space-y-2">
61+
{media.map(({ id, previewImage, alt, mediaContentType }) => {
62+
return (
63+
<div
64+
key={id}
65+
className={cn(
66+
"relative",
67+
"p-1 border rounded-md transition-colors cursor-pointer border-transparent !h-auto",
68+
zoomMediaId === id && "border-line",
69+
)}
70+
onClick={() => setZoomMediaId(id)}
71+
>
72+
<Image
73+
data={{
74+
...previewImage,
75+
altText: alt || "Product image zoom",
76+
}}
77+
loading="lazy"
78+
width={200}
79+
aspectRatio="1/1"
80+
className="object-cover w-full h-auto rounded"
81+
sizes="auto"
82+
/>
83+
{mediaContentType === "VIDEO" && (
84+
<div className="absolute bottom-2 right-2 bg-gray-900 text-white p-0.5">
85+
<VideoCamera className="w-4 h-4" />
86+
</div>
87+
)}
88+
</div>
89+
);
90+
})}
91+
</div>
92+
</ScrollArea>
93+
</div>
94+
<ZoomMedia media={zoomMedia} />
95+
<Dialog.Close className="absolute top-4 right-4 z-1">
96+
<X className="w-6 h-6" />
97+
</Dialog.Close>
98+
<div className="flex items-center gap-2 justify-center absolute bottom-10 left-10 md:left-auto right-10">
99+
<Button
100+
variant="secondary"
101+
className="rounded border-line-subtle"
102+
onClick={() => setZoomMediaId(prevMedia.id)}
103+
>
104+
<ArrowLeft className="w-4.5 h-4.5" />
105+
</Button>
106+
<Button
107+
variant="secondary"
108+
className="rounded border-line-subtle"
109+
onClick={() => setZoomMediaId(nextMedia.id)}
110+
>
111+
<ArrowRight className="w-4.5 h-4.5" />
112+
</Button>
113+
</div>
114+
</div>
115+
</Dialog.Content>
116+
</Dialog.Portal>
117+
</Dialog.Root>
118+
);
119+
}
120+
121+
function ZoomMedia({ media }: { media: MediaFragment }) {
122+
if (!media) return null;
123+
if (media.mediaContentType === "IMAGE") {
124+
let { image, alt } = media as Media_MediaImage_Fragment;
125+
return (
126+
<Image
127+
data={{ ...image, altText: alt || "Product image zoom" }}
128+
loading="lazy"
129+
className="object-cover max-w-[95vw] w-auto h-auto md:h-full max-h-screen rounded"
130+
width={4096}
131+
aspectRatio={getImageAspectRatio(image, "adapt")}
132+
sizes="auto"
133+
/>
134+
);
135+
}
136+
if (media.mediaContentType === "VIDEO") {
137+
let mediaVideo = media as Media_Video_Fragment;
138+
return (
139+
<video controls className="h-auto md:h-full object-cover rounded">
140+
<track kind="captions" />
141+
<source src={mediaVideo.sources[0].url} type="video/mp4" />
142+
</video>
143+
);
144+
}
145+
return null;
146+
}

0 commit comments

Comments
 (0)