@tanstack/create 0.65.0 → 0.67.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +72 -0
- package/dist/frameworks/react/add-ons/powersync/README.md.ejs +26 -0
- package/dist/frameworks/react/add-ons/powersync/assets/_dot_env.local.append +3 -0
- package/dist/frameworks/react/add-ons/powersync/assets/powersync-vite-plugin.ts +17 -0
- package/dist/frameworks/react/add-ons/powersync/assets/src/integrations/powersync/provider.tsx +26 -0
- package/dist/frameworks/react/add-ons/powersync/assets/src/lib/powersync/AppSchema.ts +17 -0
- package/dist/frameworks/react/add-ons/powersync/assets/src/lib/powersync/BackendConnector.ts +52 -0
- package/dist/frameworks/react/add-ons/powersync/assets/src/routes/demo/powersync.tsx +129 -0
- package/dist/frameworks/react/add-ons/powersync/info.json +46 -0
- package/dist/frameworks/react/add-ons/powersync/package.json.ejs +7 -0
- package/dist/frameworks/react/add-ons/powersync/small-logo.svg +6 -0
- package/dist/frameworks/react/add-ons/shopify/README.md +86 -0
- package/dist/frameworks/react/add-ons/shopify/assets/_dot_env.local.append +19 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/account-nav.tsx.ejs +41 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/add-to-cart-button.tsx +48 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/cart-line-item.tsx +94 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/cart-summary.tsx +111 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/empty-state.tsx +29 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/money.tsx +11 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/product-card.tsx +74 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/product-grid.tsx +24 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/shop-image.tsx +57 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/shop.css +58 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/variant-selector.tsx +79 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/hooks/use-cart.ts +276 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/hooks/use-customer.ts.ejs +22 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/integrations/shopify/header-cart.tsx +37 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/lib/shopify/customer-queries.ts.ejs +228 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/lib/shopify/format.ts +33 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/lib/shopify/queries.ts +684 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.addresses.tsx.ejs +67 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.callback.tsx.ejs +45 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.index.tsx.ejs +70 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.login.tsx.ejs +59 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.logout.tsx.ejs +16 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.orders.$id.tsx.ejs +126 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.orders.tsx.ejs +50 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.tsx.ejs +34 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.cart.tsx +45 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.collections.$handle.tsx +66 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.index.tsx +36 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.pages.$handle.tsx +39 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.policies.$handle.tsx +30 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.products.$handle.tsx +106 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.search.tsx +75 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.tsx +78 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/cart.functions.ts +207 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/catalog.functions.ts +244 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/cookies.ts +29 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/customer-client.ts.ejs +99 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/customer-cookies.ts.ejs +49 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/customer.functions.ts.ejs +168 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/env.ts +89 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/oauth.ts.ejs +301 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/storefront-client.ts +101 -0
- package/dist/frameworks/react/add-ons/shopify/info.json +104 -0
- package/dist/frameworks/react/add-ons/shopify/package.json +6 -0
- package/dist/frameworks/react/add-ons/shopify/small-logo.svg +1 -0
- package/dist/frameworks/react/examples/shopify-storefront/README.md +39 -0
- package/dist/frameworks/react/examples/shopify-storefront/assets/src/components/FeaturedCollections.tsx +43 -0
- package/dist/frameworks/react/examples/shopify-storefront/assets/src/components/ShopHero.tsx +39 -0
- package/dist/frameworks/react/examples/shopify-storefront/assets/src/routes/index.tsx +65 -0
- package/dist/frameworks/react/examples/shopify-storefront/info.json +18 -0
- package/dist/frameworks/react/examples/shopify-storefront/package.json +3 -0
- package/dist/frameworks/react/project/base/src/components/Header.tsx.ejs +34 -34
- package/package.json +1 -1
- package/src/frameworks/react/add-ons/powersync/README.md.ejs +26 -0
- package/src/frameworks/react/add-ons/powersync/assets/_dot_env.local.append +3 -0
- package/src/frameworks/react/add-ons/powersync/assets/powersync-vite-plugin.ts +17 -0
- package/src/frameworks/react/add-ons/powersync/assets/src/integrations/powersync/provider.tsx +26 -0
- package/src/frameworks/react/add-ons/powersync/assets/src/lib/powersync/AppSchema.ts +17 -0
- package/src/frameworks/react/add-ons/powersync/assets/src/lib/powersync/BackendConnector.ts +52 -0
- package/src/frameworks/react/add-ons/powersync/assets/src/routes/demo/powersync.tsx +129 -0
- package/src/frameworks/react/add-ons/powersync/info.json +46 -0
- package/src/frameworks/react/add-ons/powersync/package.json.ejs +7 -0
- package/src/frameworks/react/add-ons/powersync/small-logo.svg +6 -0
- package/src/frameworks/react/add-ons/shopify/README.md +86 -0
- package/src/frameworks/react/add-ons/shopify/assets/_dot_env.local.append +19 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/account-nav.tsx.ejs +41 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/add-to-cart-button.tsx +48 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/cart-line-item.tsx +94 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/cart-summary.tsx +111 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/empty-state.tsx +29 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/money.tsx +11 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/product-card.tsx +74 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/product-grid.tsx +24 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/shop-image.tsx +57 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/shop.css +58 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/variant-selector.tsx +79 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/hooks/use-cart.ts +276 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/hooks/use-customer.ts.ejs +22 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/integrations/shopify/header-cart.tsx +37 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/lib/shopify/customer-queries.ts.ejs +228 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/lib/shopify/format.ts +33 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/lib/shopify/queries.ts +684 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.addresses.tsx.ejs +67 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.callback.tsx.ejs +45 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.index.tsx.ejs +70 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.login.tsx.ejs +59 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.logout.tsx.ejs +16 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.orders.$id.tsx.ejs +126 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.orders.tsx.ejs +50 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.tsx.ejs +34 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.cart.tsx +45 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.collections.$handle.tsx +66 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.index.tsx +36 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.pages.$handle.tsx +39 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.policies.$handle.tsx +30 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.products.$handle.tsx +106 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.search.tsx +75 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.tsx +78 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/cart.functions.ts +207 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/catalog.functions.ts +244 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/cookies.ts +29 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/customer-client.ts.ejs +99 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/customer-cookies.ts.ejs +49 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/customer.functions.ts.ejs +168 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/env.ts +89 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/oauth.ts.ejs +301 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/storefront-client.ts +101 -0
- package/src/frameworks/react/add-ons/shopify/info.json +104 -0
- package/src/frameworks/react/add-ons/shopify/package.json +6 -0
- package/src/frameworks/react/add-ons/shopify/small-logo.svg +1 -0
- package/src/frameworks/react/examples/shopify-storefront/README.md +39 -0
- package/src/frameworks/react/examples/shopify-storefront/assets/src/components/FeaturedCollections.tsx +43 -0
- package/src/frameworks/react/examples/shopify-storefront/assets/src/components/ShopHero.tsx +39 -0
- package/src/frameworks/react/examples/shopify-storefront/assets/src/routes/index.tsx +65 -0
- package/src/frameworks/react/examples/shopify-storefront/info.json +18 -0
- package/src/frameworks/react/examples/shopify-storefront/package.json +3 -0
- package/src/frameworks/react/project/base/src/components/Header.tsx.ejs +34 -34
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { Link } from '@tanstack/react-router'
|
|
2
|
+
|
|
3
|
+
import { Money } from '#/components/shop/money'
|
|
4
|
+
import { ShopImage } from '#/components/shop/shop-image'
|
|
5
|
+
import { useRemoveCartLine, useUpdateCartLine } from '#/hooks/use-cart'
|
|
6
|
+
import type { CartLineDetail } from '#/lib/shopify/queries'
|
|
7
|
+
|
|
8
|
+
type CartLineItemProps = {
|
|
9
|
+
line: CartLineDetail
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function CartLineItem({ line }: CartLineItemProps) {
|
|
13
|
+
const update = useUpdateCartLine()
|
|
14
|
+
const remove = useRemoveCartLine()
|
|
15
|
+
const merch = line.merchandise
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<li className="flex gap-4 border-b border-[var(--storefront-line)] py-6">
|
|
19
|
+
<Link
|
|
20
|
+
to="/shop/products/$handle"
|
|
21
|
+
params={{ handle: merch.product.handle }}
|
|
22
|
+
className="flex-shrink-0"
|
|
23
|
+
>
|
|
24
|
+
<ShopImage
|
|
25
|
+
src={merch.image?.url}
|
|
26
|
+
alt={merch.image?.altText ?? merch.product.title}
|
|
27
|
+
width={120}
|
|
28
|
+
height={150}
|
|
29
|
+
className="h-[150px] w-[120px] rounded-md object-cover"
|
|
30
|
+
/>
|
|
31
|
+
</Link>
|
|
32
|
+
<div className="flex flex-1 flex-col gap-2">
|
|
33
|
+
<div className="flex items-start justify-between gap-4">
|
|
34
|
+
<div>
|
|
35
|
+
<Link
|
|
36
|
+
to="/shop/products/$handle"
|
|
37
|
+
params={{ handle: merch.product.handle }}
|
|
38
|
+
className="text-base font-medium no-underline text-[var(--storefront-fg)]"
|
|
39
|
+
>
|
|
40
|
+
{merch.product.title}
|
|
41
|
+
</Link>
|
|
42
|
+
{merch.title && merch.title !== 'Default Title' && (
|
|
43
|
+
<p className="text-sm text-[var(--storefront-fg-muted)]">
|
|
44
|
+
{merch.selectedOptions.map((o) => o.value).join(' · ')}
|
|
45
|
+
</p>
|
|
46
|
+
)}
|
|
47
|
+
</div>
|
|
48
|
+
<Money
|
|
49
|
+
amount={line.cost.totalAmount.amount}
|
|
50
|
+
currencyCode={line.cost.totalAmount.currencyCode}
|
|
51
|
+
className="text-base font-medium"
|
|
52
|
+
/>
|
|
53
|
+
</div>
|
|
54
|
+
<div className="mt-auto flex items-center justify-between gap-4">
|
|
55
|
+
<div className="inline-flex items-center rounded-full border border-[var(--storefront-line)]">
|
|
56
|
+
<button
|
|
57
|
+
type="button"
|
|
58
|
+
aria-label="Decrease quantity"
|
|
59
|
+
disabled={update.isPending || line.quantity <= 1}
|
|
60
|
+
onClick={() =>
|
|
61
|
+
update.mutate({ lineId: line.id, quantity: line.quantity - 1 })
|
|
62
|
+
}
|
|
63
|
+
className="px-3 py-1.5 text-base disabled:opacity-40"
|
|
64
|
+
>
|
|
65
|
+
−
|
|
66
|
+
</button>
|
|
67
|
+
<span className="min-w-[2rem] text-center text-sm">
|
|
68
|
+
{line.quantity}
|
|
69
|
+
</span>
|
|
70
|
+
<button
|
|
71
|
+
type="button"
|
|
72
|
+
aria-label="Increase quantity"
|
|
73
|
+
disabled={update.isPending}
|
|
74
|
+
onClick={() =>
|
|
75
|
+
update.mutate({ lineId: line.id, quantity: line.quantity + 1 })
|
|
76
|
+
}
|
|
77
|
+
className="px-3 py-1.5 text-base disabled:opacity-40"
|
|
78
|
+
>
|
|
79
|
+
+
|
|
80
|
+
</button>
|
|
81
|
+
</div>
|
|
82
|
+
<button
|
|
83
|
+
type="button"
|
|
84
|
+
onClick={() => remove.mutate({ lineId: line.id })}
|
|
85
|
+
disabled={remove.isPending}
|
|
86
|
+
className="text-sm text-[var(--storefront-fg-muted)] underline underline-offset-4 hover:text-[var(--storefront-fg)] disabled:opacity-40"
|
|
87
|
+
>
|
|
88
|
+
Remove
|
|
89
|
+
</button>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</li>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
|
|
3
|
+
import { Money } from '#/components/shop/money'
|
|
4
|
+
import {
|
|
5
|
+
useApplyDiscountCode,
|
|
6
|
+
useRemoveDiscountCode,
|
|
7
|
+
} from '#/hooks/use-cart'
|
|
8
|
+
import type { CartDetail } from '#/lib/shopify/queries'
|
|
9
|
+
|
|
10
|
+
type CartSummaryProps = {
|
|
11
|
+
cart: CartDetail
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function CartSummary({ cart }: CartSummaryProps) {
|
|
15
|
+
const [code, setCode] = useState('')
|
|
16
|
+
const apply = useApplyDiscountCode()
|
|
17
|
+
const remove = useRemoveDiscountCode()
|
|
18
|
+
const appliedCode = cart.discountCodes.find((c) => c.applicable)
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<aside className="flex flex-col gap-4 rounded-2xl border border-[var(--storefront-line)] p-6">
|
|
22
|
+
<h2 className="text-lg font-medium">Order summary</h2>
|
|
23
|
+
|
|
24
|
+
<div className="flex justify-between text-sm">
|
|
25
|
+
<span>Subtotal</span>
|
|
26
|
+
<Money
|
|
27
|
+
amount={cart.cost.subtotalAmount.amount}
|
|
28
|
+
currencyCode={cart.cost.subtotalAmount.currencyCode}
|
|
29
|
+
/>
|
|
30
|
+
</div>
|
|
31
|
+
{cart.cost.totalTaxAmount && Number(cart.cost.totalTaxAmount.amount) > 0 && (
|
|
32
|
+
<div className="flex justify-between text-sm text-[var(--storefront-fg-muted)]">
|
|
33
|
+
<span>Estimated tax</span>
|
|
34
|
+
<Money
|
|
35
|
+
amount={cart.cost.totalTaxAmount.amount}
|
|
36
|
+
currencyCode={cart.cost.totalTaxAmount.currencyCode}
|
|
37
|
+
/>
|
|
38
|
+
</div>
|
|
39
|
+
)}
|
|
40
|
+
<p className="text-sm text-[var(--storefront-fg-muted)]">
|
|
41
|
+
Shipping calculated at checkout.
|
|
42
|
+
</p>
|
|
43
|
+
|
|
44
|
+
<form
|
|
45
|
+
onSubmit={(e) => {
|
|
46
|
+
e.preventDefault()
|
|
47
|
+
if (code.trim()) {
|
|
48
|
+
apply.mutate(
|
|
49
|
+
{ code: code.trim() },
|
|
50
|
+
{ onSuccess: () => setCode('') },
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
}}
|
|
54
|
+
className="flex flex-col gap-2"
|
|
55
|
+
>
|
|
56
|
+
<div className="flex gap-2">
|
|
57
|
+
<input
|
|
58
|
+
type="text"
|
|
59
|
+
value={code}
|
|
60
|
+
onChange={(e) => setCode(e.target.value)}
|
|
61
|
+
placeholder="Discount code"
|
|
62
|
+
className="min-w-0 flex-1 rounded-full border border-[var(--storefront-line)] bg-transparent px-4 py-2 text-sm focus:border-[var(--storefront-accent)] focus:outline-none"
|
|
63
|
+
/>
|
|
64
|
+
<button
|
|
65
|
+
type="submit"
|
|
66
|
+
disabled={apply.isPending || !code.trim()}
|
|
67
|
+
className="rounded-full border border-[var(--storefront-line)] px-4 py-2 text-sm font-medium hover:border-[var(--storefront-accent)] disabled:opacity-40"
|
|
68
|
+
>
|
|
69
|
+
Apply
|
|
70
|
+
</button>
|
|
71
|
+
</div>
|
|
72
|
+
{apply.error && (
|
|
73
|
+
<p className="text-xs text-red-600">{(apply.error as Error).message}</p>
|
|
74
|
+
)}
|
|
75
|
+
{appliedCode && (
|
|
76
|
+
<div className="flex items-center justify-between rounded-full bg-[var(--storefront-line)]/40 px-4 py-2 text-xs">
|
|
77
|
+
<span>
|
|
78
|
+
Applied: <strong>{appliedCode.code}</strong>
|
|
79
|
+
</span>
|
|
80
|
+
<button
|
|
81
|
+
type="button"
|
|
82
|
+
onClick={() => remove.mutate()}
|
|
83
|
+
disabled={remove.isPending}
|
|
84
|
+
className="underline underline-offset-2 disabled:opacity-40"
|
|
85
|
+
>
|
|
86
|
+
Remove
|
|
87
|
+
</button>
|
|
88
|
+
</div>
|
|
89
|
+
)}
|
|
90
|
+
</form>
|
|
91
|
+
|
|
92
|
+
<div className="flex justify-between border-t border-[var(--storefront-line)] pt-4 text-base font-medium">
|
|
93
|
+
<span>Total</span>
|
|
94
|
+
<Money
|
|
95
|
+
amount={cart.cost.totalAmount.amount}
|
|
96
|
+
currencyCode={cart.cost.totalAmount.currencyCode}
|
|
97
|
+
/>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<a
|
|
101
|
+
href={cart.checkoutUrl}
|
|
102
|
+
className="block w-full rounded-full bg-[var(--storefront-accent)] px-6 py-3.5 text-center text-sm font-medium text-[var(--storefront-accent-fg)] no-underline transition hover:opacity-90"
|
|
103
|
+
>
|
|
104
|
+
Checkout
|
|
105
|
+
</a>
|
|
106
|
+
<p className="text-center text-xs text-[var(--storefront-fg-muted)]">
|
|
107
|
+
Secure checkout powered by Shopify
|
|
108
|
+
</p>
|
|
109
|
+
</aside>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Link } from '@tanstack/react-router'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
|
|
4
|
+
type EmptyStateProps = {
|
|
5
|
+
title: string
|
|
6
|
+
description?: string
|
|
7
|
+
cta?: { label: string; to: string }
|
|
8
|
+
children?: ReactNode
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function EmptyState({ title, description, cta, children }: EmptyStateProps) {
|
|
12
|
+
return (
|
|
13
|
+
<div className="flex flex-col items-center gap-4 rounded-2xl border border-dashed border-[var(--storefront-line)] px-6 py-16 text-center">
|
|
14
|
+
<h2 className="text-xl font-medium">{title}</h2>
|
|
15
|
+
{description && (
|
|
16
|
+
<p className="max-w-md text-[var(--storefront-fg-muted)]">{description}</p>
|
|
17
|
+
)}
|
|
18
|
+
{children}
|
|
19
|
+
{cta && (
|
|
20
|
+
<Link
|
|
21
|
+
to={cta.to}
|
|
22
|
+
className="rounded-full border border-[var(--storefront-fg)] px-5 py-2 text-sm font-medium no-underline text-[var(--storefront-fg)]"
|
|
23
|
+
>
|
|
24
|
+
{cta.label}
|
|
25
|
+
</Link>
|
|
26
|
+
)}
|
|
27
|
+
</div>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { formatMoney } from '#/lib/shopify/format'
|
|
2
|
+
|
|
3
|
+
type MoneyProps = {
|
|
4
|
+
amount: string | number
|
|
5
|
+
currencyCode: string
|
|
6
|
+
className?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function Money({ amount, currencyCode, className }: MoneyProps) {
|
|
10
|
+
return <span className={className}>{formatMoney(amount, currencyCode)}</span>
|
|
11
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Link } from '@tanstack/react-router'
|
|
2
|
+
|
|
3
|
+
import { Money } from '#/components/shop/money'
|
|
4
|
+
import { ShopImage } from '#/components/shop/shop-image'
|
|
5
|
+
import type { ProductListItem } from '#/lib/shopify/queries'
|
|
6
|
+
|
|
7
|
+
type ProductCardProps = {
|
|
8
|
+
product: ProductListItem
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function ProductCard({ product }: ProductCardProps) {
|
|
12
|
+
const minPrice = product.priceRange.minVariantPrice
|
|
13
|
+
const maxPrice = product.priceRange.maxVariantPrice
|
|
14
|
+
const compareAt = product.compareAtPriceRange.minVariantPrice
|
|
15
|
+
const onSale =
|
|
16
|
+
Number(compareAt.amount) > 0 &&
|
|
17
|
+
Number(compareAt.amount) > Number(minPrice.amount)
|
|
18
|
+
const soldOut = !product.variants.nodes.some((v) => v.availableForSale)
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<Link
|
|
22
|
+
to="/shop/products/$handle"
|
|
23
|
+
params={{ handle: product.handle }}
|
|
24
|
+
className="group flex flex-col gap-3 no-underline text-[var(--storefront-fg)]"
|
|
25
|
+
>
|
|
26
|
+
<div
|
|
27
|
+
className="relative overflow-hidden bg-[var(--storefront-line)]"
|
|
28
|
+
style={{ aspectRatio: '4 / 5', borderRadius: 'var(--storefront-radius)' }}
|
|
29
|
+
>
|
|
30
|
+
<ShopImage
|
|
31
|
+
src={product.featuredImage?.url}
|
|
32
|
+
alt={product.featuredImage?.altText ?? product.title}
|
|
33
|
+
width={600}
|
|
34
|
+
height={750}
|
|
35
|
+
sizes="(min-width: 1024px) 25vw, (min-width: 640px) 33vw, 50vw"
|
|
36
|
+
className="h-full w-full object-cover transition-transform duration-500 group-hover:scale-[1.03]"
|
|
37
|
+
/>
|
|
38
|
+
{soldOut && (
|
|
39
|
+
<span className="absolute left-3 top-3 rounded-full bg-[var(--storefront-bg)]/90 px-2.5 py-1 text-xs font-medium uppercase tracking-wide">
|
|
40
|
+
Sold out
|
|
41
|
+
</span>
|
|
42
|
+
)}
|
|
43
|
+
{!soldOut && onSale && (
|
|
44
|
+
<span className="absolute left-3 top-3 rounded-full bg-[var(--storefront-accent)] px-2.5 py-1 text-xs font-medium uppercase tracking-wide text-[var(--storefront-accent-fg)]">
|
|
45
|
+
Sale
|
|
46
|
+
</span>
|
|
47
|
+
)}
|
|
48
|
+
</div>
|
|
49
|
+
<div className="flex flex-col gap-1">
|
|
50
|
+
<h3 className="text-sm font-medium leading-snug">{product.title}</h3>
|
|
51
|
+
<p className="text-sm text-[var(--storefront-fg-muted)]">
|
|
52
|
+
{onSale && (
|
|
53
|
+
<span className="mr-2 line-through">
|
|
54
|
+
<Money
|
|
55
|
+
amount={compareAt.amount}
|
|
56
|
+
currencyCode={compareAt.currencyCode}
|
|
57
|
+
/>
|
|
58
|
+
</span>
|
|
59
|
+
)}
|
|
60
|
+
<Money amount={minPrice.amount} currencyCode={minPrice.currencyCode} />
|
|
61
|
+
{Number(maxPrice.amount) > Number(minPrice.amount) && (
|
|
62
|
+
<>
|
|
63
|
+
{' – '}
|
|
64
|
+
<Money
|
|
65
|
+
amount={maxPrice.amount}
|
|
66
|
+
currencyCode={maxPrice.currencyCode}
|
|
67
|
+
/>
|
|
68
|
+
</>
|
|
69
|
+
)}
|
|
70
|
+
</p>
|
|
71
|
+
</div>
|
|
72
|
+
</Link>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ProductCard } from '#/components/shop/product-card'
|
|
2
|
+
import type { ProductListItem } from '#/lib/shopify/queries'
|
|
3
|
+
|
|
4
|
+
type ProductGridProps = {
|
|
5
|
+
products: ReadonlyArray<ProductListItem>
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function ProductGrid({ products }: ProductGridProps) {
|
|
9
|
+
if (products.length === 0) {
|
|
10
|
+
return (
|
|
11
|
+
<p className="py-16 text-center text-[var(--storefront-fg-muted)]">
|
|
12
|
+
No products yet.
|
|
13
|
+
</p>
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div className="grid grid-cols-2 gap-x-4 gap-y-10 sm:grid-cols-3 lg:grid-cols-4">
|
|
19
|
+
{products.map((product) => (
|
|
20
|
+
<ProductCard key={product.id} product={product} />
|
|
21
|
+
))}
|
|
22
|
+
</div>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { shopifyImageUrl } from '#/lib/shopify/format'
|
|
2
|
+
|
|
3
|
+
type ShopImageProps = {
|
|
4
|
+
src: string | null | undefined
|
|
5
|
+
alt: string | null | undefined
|
|
6
|
+
width: number
|
|
7
|
+
height?: number
|
|
8
|
+
className?: string
|
|
9
|
+
sizes?: string
|
|
10
|
+
loading?: 'eager' | 'lazy'
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function ShopImage({
|
|
14
|
+
src,
|
|
15
|
+
alt,
|
|
16
|
+
width,
|
|
17
|
+
height,
|
|
18
|
+
className,
|
|
19
|
+
sizes,
|
|
20
|
+
loading = 'lazy',
|
|
21
|
+
}: ShopImageProps) {
|
|
22
|
+
if (!src) {
|
|
23
|
+
return (
|
|
24
|
+
<div
|
|
25
|
+
aria-hidden
|
|
26
|
+
className={className}
|
|
27
|
+
style={{
|
|
28
|
+
background: 'var(--storefront-line)',
|
|
29
|
+
aspectRatio: height ? `${width} / ${height}` : '1',
|
|
30
|
+
}}
|
|
31
|
+
/>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const transformed = shopifyImageUrl(src, { width, height, format: 'webp' })
|
|
36
|
+
const srcSet = [1, 2]
|
|
37
|
+
.map((dpr) => {
|
|
38
|
+
const w = width * dpr
|
|
39
|
+
const h = height ? height * dpr : undefined
|
|
40
|
+
return `${shopifyImageUrl(src, { width: w, height: h, format: 'webp' })} ${dpr}x`
|
|
41
|
+
})
|
|
42
|
+
.join(', ')
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<img
|
|
46
|
+
src={transformed}
|
|
47
|
+
srcSet={srcSet}
|
|
48
|
+
sizes={sizes}
|
|
49
|
+
alt={alt ?? ''}
|
|
50
|
+
width={width}
|
|
51
|
+
height={height}
|
|
52
|
+
loading={loading}
|
|
53
|
+
decoding="async"
|
|
54
|
+
className={className}
|
|
55
|
+
/>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Shop theme tokens. Override these six variables in your own CSS to re-theme
|
|
3
|
+
* the storefront without touching components.
|
|
4
|
+
*/
|
|
5
|
+
:root {
|
|
6
|
+
--storefront-bg: #ffffff;
|
|
7
|
+
--storefront-fg: #0a0a0a;
|
|
8
|
+
--storefront-fg-muted: #525252;
|
|
9
|
+
--storefront-line: #e5e5e5;
|
|
10
|
+
--storefront-accent: #0a0a0a;
|
|
11
|
+
--storefront-accent-fg: #ffffff;
|
|
12
|
+
--storefront-radius: 0.5rem;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@media (prefers-color-scheme: dark) {
|
|
16
|
+
:root:not([data-theme='light']) {
|
|
17
|
+
--storefront-bg: #0a0a0a;
|
|
18
|
+
--storefront-fg: #fafafa;
|
|
19
|
+
--storefront-fg-muted: #a3a3a3;
|
|
20
|
+
--storefront-line: #262626;
|
|
21
|
+
--storefront-accent: #fafafa;
|
|
22
|
+
--storefront-accent-fg: #0a0a0a;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
:root[data-theme='dark'] {
|
|
27
|
+
--storefront-bg: #0a0a0a;
|
|
28
|
+
--storefront-fg: #fafafa;
|
|
29
|
+
--storefront-fg-muted: #a3a3a3;
|
|
30
|
+
--storefront-line: #262626;
|
|
31
|
+
--storefront-accent: #fafafa;
|
|
32
|
+
--storefront-accent-fg: #0a0a0a;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.shop-root {
|
|
36
|
+
background: var(--storefront-bg);
|
|
37
|
+
color: var(--storefront-fg);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.shop-prose {
|
|
41
|
+
max-width: 65ch;
|
|
42
|
+
line-height: 1.7;
|
|
43
|
+
}
|
|
44
|
+
.shop-prose h1,
|
|
45
|
+
.shop-prose h2,
|
|
46
|
+
.shop-prose h3 {
|
|
47
|
+
font-weight: 600;
|
|
48
|
+
letter-spacing: -0.02em;
|
|
49
|
+
margin-top: 1.5em;
|
|
50
|
+
}
|
|
51
|
+
.shop-prose p {
|
|
52
|
+
margin: 0.75em 0;
|
|
53
|
+
}
|
|
54
|
+
.shop-prose a {
|
|
55
|
+
color: var(--storefront-accent);
|
|
56
|
+
text-decoration: underline;
|
|
57
|
+
text-underline-offset: 2px;
|
|
58
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { ProductDetail, ProductDetailVariant } from '#/lib/shopify/queries'
|
|
2
|
+
|
|
3
|
+
type VariantSelectorProps = {
|
|
4
|
+
product: ProductDetail
|
|
5
|
+
selectedOptions: Record<string, string>
|
|
6
|
+
onChange: (next: Record<string, string>) => void
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function VariantSelector({
|
|
10
|
+
product,
|
|
11
|
+
selectedOptions,
|
|
12
|
+
onChange,
|
|
13
|
+
}: VariantSelectorProps) {
|
|
14
|
+
if (product.options.length === 1 && product.options[0]?.values.length === 1) {
|
|
15
|
+
return null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className="space-y-5">
|
|
20
|
+
{product.options.map((option) => (
|
|
21
|
+
<div key={option.id}>
|
|
22
|
+
<div className="mb-2 flex items-baseline justify-between">
|
|
23
|
+
<h3 className="text-sm font-medium">{option.name}</h3>
|
|
24
|
+
<span className="text-sm text-[var(--storefront-fg-muted)]">
|
|
25
|
+
{selectedOptions[option.name]}
|
|
26
|
+
</span>
|
|
27
|
+
</div>
|
|
28
|
+
<div className="flex flex-wrap gap-2">
|
|
29
|
+
{option.values.map((value) => {
|
|
30
|
+
const selected = selectedOptions[option.name] === value
|
|
31
|
+
const wouldBe = { ...selectedOptions, [option.name]: value }
|
|
32
|
+
const matchingVariant = findVariant(product.variants.nodes, wouldBe)
|
|
33
|
+
const available = matchingVariant?.availableForSale ?? false
|
|
34
|
+
return (
|
|
35
|
+
<button
|
|
36
|
+
key={value}
|
|
37
|
+
type="button"
|
|
38
|
+
onClick={() => onChange(wouldBe)}
|
|
39
|
+
disabled={!available && !selected}
|
|
40
|
+
className={[
|
|
41
|
+
'min-w-[3rem] rounded-full border px-4 py-2 text-sm transition',
|
|
42
|
+
selected
|
|
43
|
+
? 'border-[var(--storefront-accent)] bg-[var(--storefront-accent)] text-[var(--storefront-accent-fg)]'
|
|
44
|
+
: 'border-[var(--storefront-line)] hover:border-[var(--storefront-accent)]',
|
|
45
|
+
!available && !selected
|
|
46
|
+
? 'cursor-not-allowed opacity-40 line-through'
|
|
47
|
+
: '',
|
|
48
|
+
].join(' ')}
|
|
49
|
+
>
|
|
50
|
+
{value}
|
|
51
|
+
</button>
|
|
52
|
+
)
|
|
53
|
+
})}
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
))}
|
|
57
|
+
</div>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function findVariant(
|
|
62
|
+
variants: ReadonlyArray<ProductDetailVariant>,
|
|
63
|
+
selected: Record<string, string>,
|
|
64
|
+
): ProductDetailVariant | undefined {
|
|
65
|
+
return variants.find((variant) =>
|
|
66
|
+
variant.selectedOptions.every(
|
|
67
|
+
(opt) => selected[opt.name] === opt.value,
|
|
68
|
+
),
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function defaultSelectedOptions(
|
|
73
|
+
product: ProductDetail,
|
|
74
|
+
): Record<string, string> {
|
|
75
|
+
const firstAvailable = product.variants.nodes.find((v) => v.availableForSale)
|
|
76
|
+
const source = firstAvailable ?? product.variants.nodes[0]
|
|
77
|
+
if (!source) return {}
|
|
78
|
+
return Object.fromEntries(source.selectedOptions.map((o) => [o.name, o.value]))
|
|
79
|
+
}
|