@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.
Files changed (130) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/dist/frameworks/react/add-ons/powersync/README.md.ejs +26 -0
  3. package/dist/frameworks/react/add-ons/powersync/assets/_dot_env.local.append +3 -0
  4. package/dist/frameworks/react/add-ons/powersync/assets/powersync-vite-plugin.ts +17 -0
  5. package/dist/frameworks/react/add-ons/powersync/assets/src/integrations/powersync/provider.tsx +26 -0
  6. package/dist/frameworks/react/add-ons/powersync/assets/src/lib/powersync/AppSchema.ts +17 -0
  7. package/dist/frameworks/react/add-ons/powersync/assets/src/lib/powersync/BackendConnector.ts +52 -0
  8. package/dist/frameworks/react/add-ons/powersync/assets/src/routes/demo/powersync.tsx +129 -0
  9. package/dist/frameworks/react/add-ons/powersync/info.json +46 -0
  10. package/dist/frameworks/react/add-ons/powersync/package.json.ejs +7 -0
  11. package/dist/frameworks/react/add-ons/powersync/small-logo.svg +6 -0
  12. package/dist/frameworks/react/add-ons/shopify/README.md +86 -0
  13. package/dist/frameworks/react/add-ons/shopify/assets/_dot_env.local.append +19 -0
  14. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/account-nav.tsx.ejs +41 -0
  15. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/add-to-cart-button.tsx +48 -0
  16. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/cart-line-item.tsx +94 -0
  17. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/cart-summary.tsx +111 -0
  18. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/empty-state.tsx +29 -0
  19. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/money.tsx +11 -0
  20. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/product-card.tsx +74 -0
  21. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/product-grid.tsx +24 -0
  22. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/shop-image.tsx +57 -0
  23. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/shop.css +58 -0
  24. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/variant-selector.tsx +79 -0
  25. package/dist/frameworks/react/add-ons/shopify/assets/src/hooks/use-cart.ts +276 -0
  26. package/dist/frameworks/react/add-ons/shopify/assets/src/hooks/use-customer.ts.ejs +22 -0
  27. package/dist/frameworks/react/add-ons/shopify/assets/src/integrations/shopify/header-cart.tsx +37 -0
  28. package/dist/frameworks/react/add-ons/shopify/assets/src/lib/shopify/customer-queries.ts.ejs +228 -0
  29. package/dist/frameworks/react/add-ons/shopify/assets/src/lib/shopify/format.ts +33 -0
  30. package/dist/frameworks/react/add-ons/shopify/assets/src/lib/shopify/queries.ts +684 -0
  31. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.addresses.tsx.ejs +67 -0
  32. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.callback.tsx.ejs +45 -0
  33. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.index.tsx.ejs +70 -0
  34. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.login.tsx.ejs +59 -0
  35. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.logout.tsx.ejs +16 -0
  36. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.orders.$id.tsx.ejs +126 -0
  37. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.orders.tsx.ejs +50 -0
  38. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.tsx.ejs +34 -0
  39. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.cart.tsx +45 -0
  40. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.collections.$handle.tsx +66 -0
  41. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.index.tsx +36 -0
  42. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.pages.$handle.tsx +39 -0
  43. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.policies.$handle.tsx +30 -0
  44. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.products.$handle.tsx +106 -0
  45. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.search.tsx +75 -0
  46. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.tsx +78 -0
  47. package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/cart.functions.ts +207 -0
  48. package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/catalog.functions.ts +244 -0
  49. package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/cookies.ts +29 -0
  50. package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/customer-client.ts.ejs +99 -0
  51. package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/customer-cookies.ts.ejs +49 -0
  52. package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/customer.functions.ts.ejs +168 -0
  53. package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/env.ts +89 -0
  54. package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/oauth.ts.ejs +301 -0
  55. package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/storefront-client.ts +101 -0
  56. package/dist/frameworks/react/add-ons/shopify/info.json +104 -0
  57. package/dist/frameworks/react/add-ons/shopify/package.json +6 -0
  58. package/dist/frameworks/react/add-ons/shopify/small-logo.svg +1 -0
  59. package/dist/frameworks/react/examples/shopify-storefront/README.md +39 -0
  60. package/dist/frameworks/react/examples/shopify-storefront/assets/src/components/FeaturedCollections.tsx +43 -0
  61. package/dist/frameworks/react/examples/shopify-storefront/assets/src/components/ShopHero.tsx +39 -0
  62. package/dist/frameworks/react/examples/shopify-storefront/assets/src/routes/index.tsx +65 -0
  63. package/dist/frameworks/react/examples/shopify-storefront/info.json +18 -0
  64. package/dist/frameworks/react/examples/shopify-storefront/package.json +3 -0
  65. package/dist/frameworks/react/project/base/src/components/Header.tsx.ejs +34 -34
  66. package/package.json +1 -1
  67. package/src/frameworks/react/add-ons/powersync/README.md.ejs +26 -0
  68. package/src/frameworks/react/add-ons/powersync/assets/_dot_env.local.append +3 -0
  69. package/src/frameworks/react/add-ons/powersync/assets/powersync-vite-plugin.ts +17 -0
  70. package/src/frameworks/react/add-ons/powersync/assets/src/integrations/powersync/provider.tsx +26 -0
  71. package/src/frameworks/react/add-ons/powersync/assets/src/lib/powersync/AppSchema.ts +17 -0
  72. package/src/frameworks/react/add-ons/powersync/assets/src/lib/powersync/BackendConnector.ts +52 -0
  73. package/src/frameworks/react/add-ons/powersync/assets/src/routes/demo/powersync.tsx +129 -0
  74. package/src/frameworks/react/add-ons/powersync/info.json +46 -0
  75. package/src/frameworks/react/add-ons/powersync/package.json.ejs +7 -0
  76. package/src/frameworks/react/add-ons/powersync/small-logo.svg +6 -0
  77. package/src/frameworks/react/add-ons/shopify/README.md +86 -0
  78. package/src/frameworks/react/add-ons/shopify/assets/_dot_env.local.append +19 -0
  79. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/account-nav.tsx.ejs +41 -0
  80. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/add-to-cart-button.tsx +48 -0
  81. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/cart-line-item.tsx +94 -0
  82. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/cart-summary.tsx +111 -0
  83. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/empty-state.tsx +29 -0
  84. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/money.tsx +11 -0
  85. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/product-card.tsx +74 -0
  86. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/product-grid.tsx +24 -0
  87. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/shop-image.tsx +57 -0
  88. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/shop.css +58 -0
  89. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/variant-selector.tsx +79 -0
  90. package/src/frameworks/react/add-ons/shopify/assets/src/hooks/use-cart.ts +276 -0
  91. package/src/frameworks/react/add-ons/shopify/assets/src/hooks/use-customer.ts.ejs +22 -0
  92. package/src/frameworks/react/add-ons/shopify/assets/src/integrations/shopify/header-cart.tsx +37 -0
  93. package/src/frameworks/react/add-ons/shopify/assets/src/lib/shopify/customer-queries.ts.ejs +228 -0
  94. package/src/frameworks/react/add-ons/shopify/assets/src/lib/shopify/format.ts +33 -0
  95. package/src/frameworks/react/add-ons/shopify/assets/src/lib/shopify/queries.ts +684 -0
  96. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.addresses.tsx.ejs +67 -0
  97. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.callback.tsx.ejs +45 -0
  98. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.index.tsx.ejs +70 -0
  99. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.login.tsx.ejs +59 -0
  100. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.logout.tsx.ejs +16 -0
  101. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.orders.$id.tsx.ejs +126 -0
  102. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.orders.tsx.ejs +50 -0
  103. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.tsx.ejs +34 -0
  104. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.cart.tsx +45 -0
  105. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.collections.$handle.tsx +66 -0
  106. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.index.tsx +36 -0
  107. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.pages.$handle.tsx +39 -0
  108. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.policies.$handle.tsx +30 -0
  109. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.products.$handle.tsx +106 -0
  110. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.search.tsx +75 -0
  111. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.tsx +78 -0
  112. package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/cart.functions.ts +207 -0
  113. package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/catalog.functions.ts +244 -0
  114. package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/cookies.ts +29 -0
  115. package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/customer-client.ts.ejs +99 -0
  116. package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/customer-cookies.ts.ejs +49 -0
  117. package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/customer.functions.ts.ejs +168 -0
  118. package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/env.ts +89 -0
  119. package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/oauth.ts.ejs +301 -0
  120. package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/storefront-client.ts +101 -0
  121. package/src/frameworks/react/add-ons/shopify/info.json +104 -0
  122. package/src/frameworks/react/add-ons/shopify/package.json +6 -0
  123. package/src/frameworks/react/add-ons/shopify/small-logo.svg +1 -0
  124. package/src/frameworks/react/examples/shopify-storefront/README.md +39 -0
  125. package/src/frameworks/react/examples/shopify-storefront/assets/src/components/FeaturedCollections.tsx +43 -0
  126. package/src/frameworks/react/examples/shopify-storefront/assets/src/components/ShopHero.tsx +39 -0
  127. package/src/frameworks/react/examples/shopify-storefront/assets/src/routes/index.tsx +65 -0
  128. package/src/frameworks/react/examples/shopify-storefront/info.json +18 -0
  129. package/src/frameworks/react/examples/shopify-storefront/package.json +3 -0
  130. 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
+ )