@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,30 @@
|
|
|
1
|
+
import { createFileRoute, notFound } from '@tanstack/react-router'
|
|
2
|
+
|
|
3
|
+
import { getShopPolicy } from '#/server/shopify/catalog.functions'
|
|
4
|
+
|
|
5
|
+
export const Route = createFileRoute('/shop/policies/$handle')({
|
|
6
|
+
loader: async ({ params }) => {
|
|
7
|
+
const policy = await getShopPolicy({ data: { handle: params.handle } })
|
|
8
|
+
if (!policy) throw notFound()
|
|
9
|
+
return { policy }
|
|
10
|
+
},
|
|
11
|
+
head: ({ loaderData }) => ({
|
|
12
|
+
meta: loaderData
|
|
13
|
+
? [{ title: loaderData.policy.title }]
|
|
14
|
+
: [],
|
|
15
|
+
}),
|
|
16
|
+
component: PolicyRoute,
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
function PolicyRoute() {
|
|
20
|
+
const { policy } = Route.useLoaderData()
|
|
21
|
+
return (
|
|
22
|
+
<article className="space-y-6">
|
|
23
|
+
<h1 className="text-3xl font-medium tracking-tight">{policy.title}</h1>
|
|
24
|
+
<div
|
|
25
|
+
className="shop-prose"
|
|
26
|
+
dangerouslySetInnerHTML={{ __html: policy.body }}
|
|
27
|
+
/>
|
|
28
|
+
</article>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { createFileRoute, notFound } from '@tanstack/react-router'
|
|
3
|
+
|
|
4
|
+
import { AddToCartButton } from '#/components/shop/add-to-cart-button'
|
|
5
|
+
import { Money } from '#/components/shop/money'
|
|
6
|
+
import { ShopImage } from '#/components/shop/shop-image'
|
|
7
|
+
import {
|
|
8
|
+
VariantSelector,
|
|
9
|
+
defaultSelectedOptions,
|
|
10
|
+
findVariant,
|
|
11
|
+
} from '#/components/shop/variant-selector'
|
|
12
|
+
import { getProduct } from '#/server/shopify/catalog.functions'
|
|
13
|
+
|
|
14
|
+
export const Route = createFileRoute('/shop/products/$handle')({
|
|
15
|
+
loader: async ({ params }) => {
|
|
16
|
+
const product = await getProduct({ data: { handle: params.handle } })
|
|
17
|
+
if (!product) throw notFound()
|
|
18
|
+
return { product }
|
|
19
|
+
},
|
|
20
|
+
head: ({ loaderData }) => ({
|
|
21
|
+
meta: loaderData
|
|
22
|
+
? [
|
|
23
|
+
{ title: loaderData.product.seo.title ?? loaderData.product.title },
|
|
24
|
+
loaderData.product.seo.description
|
|
25
|
+
? {
|
|
26
|
+
name: 'description',
|
|
27
|
+
content: loaderData.product.seo.description,
|
|
28
|
+
}
|
|
29
|
+
: { name: 'description', content: '' },
|
|
30
|
+
]
|
|
31
|
+
: [],
|
|
32
|
+
}),
|
|
33
|
+
component: ProductDetailRoute,
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
function ProductDetailRoute() {
|
|
37
|
+
const { product } = Route.useLoaderData()
|
|
38
|
+
const [selected, setSelected] = useState(() =>
|
|
39
|
+
defaultSelectedOptions(product),
|
|
40
|
+
)
|
|
41
|
+
const variant = findVariant(product.variants.nodes, selected)
|
|
42
|
+
const heroImage = variant?.image ?? product.images.nodes[0] ?? null
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<article className="grid gap-10 lg:grid-cols-[1.2fr_1fr]">
|
|
46
|
+
<div className="flex flex-col gap-3">
|
|
47
|
+
{heroImage && (
|
|
48
|
+
<ShopImage
|
|
49
|
+
src={heroImage.url}
|
|
50
|
+
alt={heroImage.altText ?? product.title}
|
|
51
|
+
width={1000}
|
|
52
|
+
height={1250}
|
|
53
|
+
loading="eager"
|
|
54
|
+
sizes="(min-width: 1024px) 50vw, 100vw"
|
|
55
|
+
className="w-full rounded-lg object-cover"
|
|
56
|
+
/>
|
|
57
|
+
)}
|
|
58
|
+
{product.images.nodes.length > 1 && (
|
|
59
|
+
<div className="grid grid-cols-4 gap-2">
|
|
60
|
+
{product.images.nodes.slice(0, 8).map((img) => (
|
|
61
|
+
<ShopImage
|
|
62
|
+
key={img.url}
|
|
63
|
+
src={img.url}
|
|
64
|
+
alt={img.altText ?? product.title}
|
|
65
|
+
width={250}
|
|
66
|
+
height={300}
|
|
67
|
+
className="w-full rounded-md object-cover"
|
|
68
|
+
/>
|
|
69
|
+
))}
|
|
70
|
+
</div>
|
|
71
|
+
)}
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<div className="flex flex-col gap-6 lg:sticky lg:top-8 lg:self-start">
|
|
75
|
+
<div className="space-y-2">
|
|
76
|
+
<h1 className="text-3xl font-medium tracking-tight">
|
|
77
|
+
{product.title}
|
|
78
|
+
</h1>
|
|
79
|
+
{variant && (
|
|
80
|
+
<p className="text-2xl">
|
|
81
|
+
<Money
|
|
82
|
+
amount={variant.price.amount}
|
|
83
|
+
currencyCode={variant.price.currencyCode}
|
|
84
|
+
/>
|
|
85
|
+
</p>
|
|
86
|
+
)}
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<VariantSelector
|
|
90
|
+
product={product}
|
|
91
|
+
selectedOptions={selected}
|
|
92
|
+
onChange={setSelected}
|
|
93
|
+
/>
|
|
94
|
+
|
|
95
|
+
<AddToCartButton product={product} variant={variant} />
|
|
96
|
+
|
|
97
|
+
{product.descriptionHtml && (
|
|
98
|
+
<div
|
|
99
|
+
className="shop-prose mt-2 text-sm"
|
|
100
|
+
dangerouslySetInnerHTML={{ __html: product.descriptionHtml }}
|
|
101
|
+
/>
|
|
102
|
+
)}
|
|
103
|
+
</div>
|
|
104
|
+
</article>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { createFileRoute, useNavigate } from '@tanstack/react-router'
|
|
3
|
+
import * as v from 'valibot'
|
|
4
|
+
|
|
5
|
+
import { ProductGrid } from '#/components/shop/product-grid'
|
|
6
|
+
import { searchProducts } from '#/server/shopify/catalog.functions'
|
|
7
|
+
|
|
8
|
+
const SearchSchema = v.object({
|
|
9
|
+
q: v.optional(v.string(), ''),
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
export const Route = createFileRoute('/shop/search')({
|
|
13
|
+
validateSearch: (search) => v.parse(SearchSchema, search),
|
|
14
|
+
loaderDeps: ({ search }) => ({ q: search.q }),
|
|
15
|
+
loader: async ({ deps }) => {
|
|
16
|
+
if (!deps.q.trim()) {
|
|
17
|
+
return {
|
|
18
|
+
q: '',
|
|
19
|
+
products: [] as Awaited<
|
|
20
|
+
ReturnType<typeof searchProducts>
|
|
21
|
+
>['products'],
|
|
22
|
+
totalCount: 0,
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const result = await searchProducts({
|
|
26
|
+
data: { query: deps.q.trim(), first: 24 },
|
|
27
|
+
})
|
|
28
|
+
return { q: deps.q, products: result.products, totalCount: result.totalCount }
|
|
29
|
+
},
|
|
30
|
+
component: SearchRoute,
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
function SearchRoute() {
|
|
34
|
+
const { q, products, totalCount } = Route.useLoaderData()
|
|
35
|
+
const navigate = useNavigate({ from: Route.fullPath })
|
|
36
|
+
const [draft, setDraft] = useState(q)
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className="space-y-8">
|
|
40
|
+
<h1 className="text-3xl font-medium tracking-tight">Search</h1>
|
|
41
|
+
|
|
42
|
+
<form
|
|
43
|
+
onSubmit={(e) => {
|
|
44
|
+
e.preventDefault()
|
|
45
|
+
navigate({ search: { q: draft } })
|
|
46
|
+
}}
|
|
47
|
+
className="flex gap-2"
|
|
48
|
+
>
|
|
49
|
+
<input
|
|
50
|
+
type="search"
|
|
51
|
+
value={draft}
|
|
52
|
+
onChange={(e) => setDraft(e.target.value)}
|
|
53
|
+
placeholder="Search products"
|
|
54
|
+
autoFocus
|
|
55
|
+
className="flex-1 rounded-full border border-[var(--storefront-line)] bg-transparent px-5 py-3 text-base focus:border-[var(--storefront-accent)] focus:outline-none"
|
|
56
|
+
/>
|
|
57
|
+
<button
|
|
58
|
+
type="submit"
|
|
59
|
+
className="rounded-full bg-[var(--storefront-accent)] px-6 py-3 text-sm font-medium text-[var(--storefront-accent-fg)]"
|
|
60
|
+
>
|
|
61
|
+
Search
|
|
62
|
+
</button>
|
|
63
|
+
</form>
|
|
64
|
+
|
|
65
|
+
{q && (
|
|
66
|
+
<p className="text-sm text-[var(--storefront-fg-muted)]">
|
|
67
|
+
{totalCount} result{totalCount === 1 ? '' : 's'} for{' '}
|
|
68
|
+
<span className="font-medium text-[var(--storefront-fg)]">"{q}"</span>
|
|
69
|
+
</p>
|
|
70
|
+
)}
|
|
71
|
+
|
|
72
|
+
{q && <ProductGrid products={products} />}
|
|
73
|
+
</div>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Link, Outlet, createFileRoute } from '@tanstack/react-router'
|
|
2
|
+
|
|
3
|
+
import '#/components/shop/shop.css'
|
|
4
|
+
import { getCollections } from '#/server/shopify/catalog.functions'
|
|
5
|
+
|
|
6
|
+
export const Route = createFileRoute('/shop')({
|
|
7
|
+
loader: () => getCollections(),
|
|
8
|
+
component: ShopLayout,
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
function ShopLayout() {
|
|
12
|
+
const collections = Route.useLoaderData()
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div className="shop-root min-h-screen">
|
|
16
|
+
<div className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
|
|
17
|
+
<div className="mb-8 flex flex-wrap items-center gap-4 border-b border-[var(--storefront-line)] pb-6">
|
|
18
|
+
<Link
|
|
19
|
+
to="/shop"
|
|
20
|
+
className="text-2xl font-medium tracking-tight no-underline text-[var(--storefront-fg)]"
|
|
21
|
+
>
|
|
22
|
+
Shop
|
|
23
|
+
</Link>
|
|
24
|
+
<div className="ml-auto flex items-center gap-4 text-sm">
|
|
25
|
+
<Link
|
|
26
|
+
to="/shop/search"
|
|
27
|
+
search={{ q: '' }}
|
|
28
|
+
className="hover:underline underline-offset-4"
|
|
29
|
+
>
|
|
30
|
+
Search
|
|
31
|
+
</Link>
|
|
32
|
+
<Link to="/shop/cart" className="hover:underline underline-offset-4">
|
|
33
|
+
Cart
|
|
34
|
+
</Link>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<div className="grid gap-8 lg:grid-cols-[200px_1fr]">
|
|
39
|
+
<aside className="hidden lg:block">
|
|
40
|
+
<nav className="sticky top-8 flex flex-col gap-1 text-sm">
|
|
41
|
+
<p className="mb-2 text-xs font-semibold uppercase tracking-wider text-[var(--storefront-fg-muted)]">
|
|
42
|
+
Collections
|
|
43
|
+
</p>
|
|
44
|
+
<Link
|
|
45
|
+
to="/shop"
|
|
46
|
+
className="rounded-md px-2 py-1.5 text-[var(--storefront-fg-muted)] hover:bg-[var(--storefront-line)]/40 hover:text-[var(--storefront-fg)]"
|
|
47
|
+
activeOptions={{ exact: true }}
|
|
48
|
+
activeProps={{
|
|
49
|
+
className:
|
|
50
|
+
'rounded-md px-2 py-1.5 bg-[var(--storefront-line)]/60 text-[var(--storefront-fg)] font-medium',
|
|
51
|
+
}}
|
|
52
|
+
>
|
|
53
|
+
All products
|
|
54
|
+
</Link>
|
|
55
|
+
{collections.map((collection) => (
|
|
56
|
+
<Link
|
|
57
|
+
key={collection.id}
|
|
58
|
+
to="/shop/collections/$handle"
|
|
59
|
+
params={{ handle: collection.handle }}
|
|
60
|
+
className="rounded-md px-2 py-1.5 text-[var(--storefront-fg-muted)] hover:bg-[var(--storefront-line)]/40 hover:text-[var(--storefront-fg)]"
|
|
61
|
+
activeProps={{
|
|
62
|
+
className:
|
|
63
|
+
'rounded-md px-2 py-1.5 bg-[var(--storefront-line)]/60 text-[var(--storefront-fg)] font-medium',
|
|
64
|
+
}}
|
|
65
|
+
>
|
|
66
|
+
{collection.title}
|
|
67
|
+
</Link>
|
|
68
|
+
))}
|
|
69
|
+
</nav>
|
|
70
|
+
</aside>
|
|
71
|
+
<main>
|
|
72
|
+
<Outlet />
|
|
73
|
+
</main>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { createServerFn } from '@tanstack/react-start'
|
|
2
|
+
import { setResponseHeaders } from '@tanstack/react-start/server'
|
|
3
|
+
import * as v from 'valibot'
|
|
4
|
+
|
|
5
|
+
import { clearCartId, getCartId, setCartId } from '#/server/shopify/cookies'
|
|
6
|
+
import { shopifyServerFetch } from '#/server/shopify/storefront-client'
|
|
7
|
+
import {
|
|
8
|
+
CART_CREATE_MUTATION,
|
|
9
|
+
CART_DISCOUNT_CODES_UPDATE_MUTATION,
|
|
10
|
+
CART_LINES_ADD_MUTATION,
|
|
11
|
+
CART_LINES_REMOVE_MUTATION,
|
|
12
|
+
CART_LINES_UPDATE_MUTATION,
|
|
13
|
+
CART_QUERY,
|
|
14
|
+
type CartCreateResult,
|
|
15
|
+
type CartDetail,
|
|
16
|
+
type CartDiscountCodesUpdateResult,
|
|
17
|
+
type CartLinesAddResult,
|
|
18
|
+
type CartLinesRemoveResult,
|
|
19
|
+
type CartLinesUpdateResult,
|
|
20
|
+
type CartQueryResult,
|
|
21
|
+
type CartUserError,
|
|
22
|
+
} from '#/lib/shopify/queries'
|
|
23
|
+
|
|
24
|
+
export class CartUserErrorsError extends Error {
|
|
25
|
+
constructor(public readonly userErrors: Array<CartUserError>) {
|
|
26
|
+
super(userErrors.map((e) => e.message).join('\n'))
|
|
27
|
+
this.name = 'CartUserErrorsError'
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function throwIfUserErrors(errs: Array<CartUserError>) {
|
|
32
|
+
if (errs.length > 0) throw new CartUserErrorsError(errs)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function setCartResponseHeaders() {
|
|
36
|
+
// Cart is per-user; do not edge-cache.
|
|
37
|
+
setResponseHeaders(
|
|
38
|
+
new Headers({ 'Cache-Control': 'private, no-store, must-revalidate' }),
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function fetchCartById(cartId: string): Promise<CartDetail | null> {
|
|
43
|
+
const result = await shopifyServerFetch<CartQueryResult, { cartId: string }>({
|
|
44
|
+
query: CART_QUERY,
|
|
45
|
+
variables: { cartId },
|
|
46
|
+
})
|
|
47
|
+
return result.cart
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const getCart = createServerFn({ method: 'GET' }).handler(
|
|
51
|
+
async (): Promise<CartDetail | null> => {
|
|
52
|
+
setCartResponseHeaders()
|
|
53
|
+
const cartId = getCartId()
|
|
54
|
+
if (!cartId) return null
|
|
55
|
+
|
|
56
|
+
const cart = await fetchCartById(cartId)
|
|
57
|
+
// If Shopify pruned the cart (expired/abandoned), clear the stale cookie.
|
|
58
|
+
if (!cart) clearCartId()
|
|
59
|
+
return cart
|
|
60
|
+
},
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
export const addToCart = createServerFn({ method: 'POST' })
|
|
64
|
+
.inputValidator(
|
|
65
|
+
v.object({
|
|
66
|
+
variantId: v.string(),
|
|
67
|
+
quantity: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1)), 1),
|
|
68
|
+
}),
|
|
69
|
+
)
|
|
70
|
+
.handler(async ({ data }): Promise<CartDetail> => {
|
|
71
|
+
setCartResponseHeaders()
|
|
72
|
+
const existingCartId = getCartId()
|
|
73
|
+
|
|
74
|
+
if (!existingCartId) {
|
|
75
|
+
const result = await shopifyServerFetch<CartCreateResult>({
|
|
76
|
+
query: CART_CREATE_MUTATION,
|
|
77
|
+
variables: {
|
|
78
|
+
input: {
|
|
79
|
+
lines: [
|
|
80
|
+
{ merchandiseId: data.variantId, quantity: data.quantity },
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
})
|
|
85
|
+
throwIfUserErrors(result.cartCreate.userErrors)
|
|
86
|
+
const cart = result.cartCreate.cart
|
|
87
|
+
if (!cart) throw new Error('Shopify returned no cart after create.')
|
|
88
|
+
setCartId(cart.id)
|
|
89
|
+
return cart
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const result = await shopifyServerFetch<CartLinesAddResult>({
|
|
93
|
+
query: CART_LINES_ADD_MUTATION,
|
|
94
|
+
variables: {
|
|
95
|
+
cartId: existingCartId,
|
|
96
|
+
lines: [{ merchandiseId: data.variantId, quantity: data.quantity }],
|
|
97
|
+
},
|
|
98
|
+
})
|
|
99
|
+
throwIfUserErrors(result.cartLinesAdd.userErrors)
|
|
100
|
+
const cart = result.cartLinesAdd.cart
|
|
101
|
+
// Existing cart was pruned between requests — recover by creating fresh.
|
|
102
|
+
if (!cart) {
|
|
103
|
+
clearCartId()
|
|
104
|
+
const createResult = await shopifyServerFetch<CartCreateResult>({
|
|
105
|
+
query: CART_CREATE_MUTATION,
|
|
106
|
+
variables: {
|
|
107
|
+
input: {
|
|
108
|
+
lines: [{ merchandiseId: data.variantId, quantity: data.quantity }],
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
})
|
|
112
|
+
throwIfUserErrors(createResult.cartCreate.userErrors)
|
|
113
|
+
const newCart = createResult.cartCreate.cart
|
|
114
|
+
if (!newCart)
|
|
115
|
+
throw new Error('Shopify returned no cart after recovery create.')
|
|
116
|
+
setCartId(newCart.id)
|
|
117
|
+
return newCart
|
|
118
|
+
}
|
|
119
|
+
return cart
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
export const updateCartLine = createServerFn({ method: 'POST' })
|
|
123
|
+
.inputValidator(
|
|
124
|
+
v.object({
|
|
125
|
+
lineId: v.string(),
|
|
126
|
+
quantity: v.pipe(v.number(), v.integer(), v.minValue(0)),
|
|
127
|
+
}),
|
|
128
|
+
)
|
|
129
|
+
.handler(async ({ data }): Promise<CartDetail> => {
|
|
130
|
+
setCartResponseHeaders()
|
|
131
|
+
const cartId = getCartId()
|
|
132
|
+
if (!cartId) throw new Error('No cart exists to update.')
|
|
133
|
+
|
|
134
|
+
const result = await shopifyServerFetch<CartLinesUpdateResult>({
|
|
135
|
+
query: CART_LINES_UPDATE_MUTATION,
|
|
136
|
+
variables: {
|
|
137
|
+
cartId,
|
|
138
|
+
lines: [{ id: data.lineId, quantity: data.quantity }],
|
|
139
|
+
},
|
|
140
|
+
})
|
|
141
|
+
throwIfUserErrors(result.cartLinesUpdate.userErrors)
|
|
142
|
+
const cart = result.cartLinesUpdate.cart
|
|
143
|
+
if (!cart) throw new Error('Shopify returned no cart after update.')
|
|
144
|
+
return cart
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
export const removeCartLine = createServerFn({ method: 'POST' })
|
|
148
|
+
.inputValidator(v.object({ lineId: v.string() }))
|
|
149
|
+
.handler(async ({ data }): Promise<CartDetail> => {
|
|
150
|
+
setCartResponseHeaders()
|
|
151
|
+
const cartId = getCartId()
|
|
152
|
+
if (!cartId) throw new Error('No cart exists to remove from.')
|
|
153
|
+
|
|
154
|
+
const result = await shopifyServerFetch<CartLinesRemoveResult>({
|
|
155
|
+
query: CART_LINES_REMOVE_MUTATION,
|
|
156
|
+
variables: { cartId, lineIds: [data.lineId] },
|
|
157
|
+
})
|
|
158
|
+
throwIfUserErrors(result.cartLinesRemove.userErrors)
|
|
159
|
+
const cart = result.cartLinesRemove.cart
|
|
160
|
+
if (!cart) throw new Error('Shopify returned no cart after remove.')
|
|
161
|
+
return cart
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
export const applyDiscountCode = createServerFn({ method: 'POST' })
|
|
165
|
+
.inputValidator(v.object({ code: v.pipe(v.string(), v.minLength(1)) }))
|
|
166
|
+
.handler(async ({ data }): Promise<CartDetail> => {
|
|
167
|
+
setCartResponseHeaders()
|
|
168
|
+
const cartId = getCartId()
|
|
169
|
+
if (!cartId) throw new Error('No cart exists to apply a discount to.')
|
|
170
|
+
const result = await shopifyServerFetch<CartDiscountCodesUpdateResult>({
|
|
171
|
+
query: CART_DISCOUNT_CODES_UPDATE_MUTATION,
|
|
172
|
+
variables: { cartId, discountCodes: [data.code] },
|
|
173
|
+
})
|
|
174
|
+
throwIfUserErrors(result.cartDiscountCodesUpdate.userErrors)
|
|
175
|
+
const cart = result.cartDiscountCodesUpdate.cart
|
|
176
|
+
if (!cart)
|
|
177
|
+
throw new Error('Shopify returned no cart after discount update.')
|
|
178
|
+
// Shopify silently drops invalid codes — surface that to the UI.
|
|
179
|
+
const applied = cart.discountCodes.find(
|
|
180
|
+
(c) => c.code.toLowerCase() === data.code.toLowerCase(),
|
|
181
|
+
)
|
|
182
|
+
if (!applied || !applied.applicable) {
|
|
183
|
+
throw new CartUserErrorsError([
|
|
184
|
+
{
|
|
185
|
+
field: null,
|
|
186
|
+
message: `Discount code "${data.code}" is not valid or not applicable to this cart.`,
|
|
187
|
+
},
|
|
188
|
+
])
|
|
189
|
+
}
|
|
190
|
+
return cart
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
export const removeDiscountCode = createServerFn({ method: 'POST' }).handler(
|
|
194
|
+
async (): Promise<CartDetail> => {
|
|
195
|
+
setCartResponseHeaders()
|
|
196
|
+
const cartId = getCartId()
|
|
197
|
+
if (!cartId) throw new Error('No cart exists.')
|
|
198
|
+
const result = await shopifyServerFetch<CartDiscountCodesUpdateResult>({
|
|
199
|
+
query: CART_DISCOUNT_CODES_UPDATE_MUTATION,
|
|
200
|
+
variables: { cartId, discountCodes: [] },
|
|
201
|
+
})
|
|
202
|
+
throwIfUserErrors(result.cartDiscountCodesUpdate.userErrors)
|
|
203
|
+
const cart = result.cartDiscountCodesUpdate.cart
|
|
204
|
+
if (!cart) throw new Error('Shopify returned no cart after discount clear.')
|
|
205
|
+
return cart
|
|
206
|
+
},
|
|
207
|
+
)
|