@tanstack/create 0.65.0 → 0.66.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 (110) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/dist/frameworks/react/add-ons/shopify/README.md +86 -0
  3. package/dist/frameworks/react/add-ons/shopify/assets/_dot_env.local.append +19 -0
  4. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/account-nav.tsx.ejs +41 -0
  5. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/add-to-cart-button.tsx +48 -0
  6. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/cart-line-item.tsx +94 -0
  7. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/cart-summary.tsx +111 -0
  8. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/empty-state.tsx +29 -0
  9. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/money.tsx +11 -0
  10. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/product-card.tsx +74 -0
  11. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/product-grid.tsx +24 -0
  12. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/shop-image.tsx +57 -0
  13. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/shop.css +58 -0
  14. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/variant-selector.tsx +79 -0
  15. package/dist/frameworks/react/add-ons/shopify/assets/src/hooks/use-cart.ts +276 -0
  16. package/dist/frameworks/react/add-ons/shopify/assets/src/hooks/use-customer.ts.ejs +22 -0
  17. package/dist/frameworks/react/add-ons/shopify/assets/src/integrations/shopify/header-cart.tsx +37 -0
  18. package/dist/frameworks/react/add-ons/shopify/assets/src/lib/shopify/customer-queries.ts.ejs +228 -0
  19. package/dist/frameworks/react/add-ons/shopify/assets/src/lib/shopify/format.ts +33 -0
  20. package/dist/frameworks/react/add-ons/shopify/assets/src/lib/shopify/queries.ts +684 -0
  21. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.addresses.tsx.ejs +67 -0
  22. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.callback.tsx.ejs +45 -0
  23. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.index.tsx.ejs +70 -0
  24. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.login.tsx.ejs +59 -0
  25. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.logout.tsx.ejs +16 -0
  26. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.orders.$id.tsx.ejs +126 -0
  27. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.orders.tsx.ejs +50 -0
  28. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.tsx.ejs +34 -0
  29. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.cart.tsx +45 -0
  30. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.collections.$handle.tsx +66 -0
  31. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.index.tsx +36 -0
  32. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.pages.$handle.tsx +39 -0
  33. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.policies.$handle.tsx +30 -0
  34. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.products.$handle.tsx +106 -0
  35. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.search.tsx +75 -0
  36. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.tsx +78 -0
  37. package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/cart.functions.ts +207 -0
  38. package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/catalog.functions.ts +244 -0
  39. package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/cookies.ts +29 -0
  40. package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/customer-client.ts.ejs +99 -0
  41. package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/customer-cookies.ts.ejs +49 -0
  42. package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/customer.functions.ts.ejs +168 -0
  43. package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/env.ts +89 -0
  44. package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/oauth.ts.ejs +301 -0
  45. package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/storefront-client.ts +101 -0
  46. package/dist/frameworks/react/add-ons/shopify/info.json +104 -0
  47. package/dist/frameworks/react/add-ons/shopify/package.json +6 -0
  48. package/dist/frameworks/react/add-ons/shopify/small-logo.svg +1 -0
  49. package/dist/frameworks/react/examples/shopify-storefront/README.md +39 -0
  50. package/dist/frameworks/react/examples/shopify-storefront/assets/src/components/FeaturedCollections.tsx +43 -0
  51. package/dist/frameworks/react/examples/shopify-storefront/assets/src/components/ShopHero.tsx +39 -0
  52. package/dist/frameworks/react/examples/shopify-storefront/assets/src/routes/index.tsx +65 -0
  53. package/dist/frameworks/react/examples/shopify-storefront/info.json +18 -0
  54. package/dist/frameworks/react/examples/shopify-storefront/package.json +3 -0
  55. package/dist/frameworks/react/project/base/src/components/Header.tsx.ejs +34 -34
  56. package/package.json +1 -1
  57. package/src/frameworks/react/add-ons/shopify/README.md +86 -0
  58. package/src/frameworks/react/add-ons/shopify/assets/_dot_env.local.append +19 -0
  59. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/account-nav.tsx.ejs +41 -0
  60. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/add-to-cart-button.tsx +48 -0
  61. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/cart-line-item.tsx +94 -0
  62. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/cart-summary.tsx +111 -0
  63. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/empty-state.tsx +29 -0
  64. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/money.tsx +11 -0
  65. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/product-card.tsx +74 -0
  66. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/product-grid.tsx +24 -0
  67. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/shop-image.tsx +57 -0
  68. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/shop.css +58 -0
  69. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/variant-selector.tsx +79 -0
  70. package/src/frameworks/react/add-ons/shopify/assets/src/hooks/use-cart.ts +276 -0
  71. package/src/frameworks/react/add-ons/shopify/assets/src/hooks/use-customer.ts.ejs +22 -0
  72. package/src/frameworks/react/add-ons/shopify/assets/src/integrations/shopify/header-cart.tsx +37 -0
  73. package/src/frameworks/react/add-ons/shopify/assets/src/lib/shopify/customer-queries.ts.ejs +228 -0
  74. package/src/frameworks/react/add-ons/shopify/assets/src/lib/shopify/format.ts +33 -0
  75. package/src/frameworks/react/add-ons/shopify/assets/src/lib/shopify/queries.ts +684 -0
  76. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.addresses.tsx.ejs +67 -0
  77. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.callback.tsx.ejs +45 -0
  78. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.index.tsx.ejs +70 -0
  79. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.login.tsx.ejs +59 -0
  80. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.logout.tsx.ejs +16 -0
  81. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.orders.$id.tsx.ejs +126 -0
  82. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.orders.tsx.ejs +50 -0
  83. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.tsx.ejs +34 -0
  84. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.cart.tsx +45 -0
  85. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.collections.$handle.tsx +66 -0
  86. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.index.tsx +36 -0
  87. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.pages.$handle.tsx +39 -0
  88. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.policies.$handle.tsx +30 -0
  89. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.products.$handle.tsx +106 -0
  90. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.search.tsx +75 -0
  91. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.tsx +78 -0
  92. package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/cart.functions.ts +207 -0
  93. package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/catalog.functions.ts +244 -0
  94. package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/cookies.ts +29 -0
  95. package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/customer-client.ts.ejs +99 -0
  96. package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/customer-cookies.ts.ejs +49 -0
  97. package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/customer.functions.ts.ejs +168 -0
  98. package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/env.ts +89 -0
  99. package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/oauth.ts.ejs +301 -0
  100. package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/storefront-client.ts +101 -0
  101. package/src/frameworks/react/add-ons/shopify/info.json +104 -0
  102. package/src/frameworks/react/add-ons/shopify/package.json +6 -0
  103. package/src/frameworks/react/add-ons/shopify/small-logo.svg +1 -0
  104. package/src/frameworks/react/examples/shopify-storefront/README.md +39 -0
  105. package/src/frameworks/react/examples/shopify-storefront/assets/src/components/FeaturedCollections.tsx +43 -0
  106. package/src/frameworks/react/examples/shopify-storefront/assets/src/components/ShopHero.tsx +39 -0
  107. package/src/frameworks/react/examples/shopify-storefront/assets/src/routes/index.tsx +65 -0
  108. package/src/frameworks/react/examples/shopify-storefront/info.json +18 -0
  109. package/src/frameworks/react/examples/shopify-storefront/package.json +3 -0
  110. package/src/frameworks/react/project/base/src/components/Header.tsx.ejs +34 -34
@@ -0,0 +1,67 @@
1
+ <% if (addOnOption.shopify.customerAccount !== 'enabled') { ignoreFile(); return; } %>
2
+ import { createFileRoute } from '@tanstack/react-router'
3
+
4
+ import { getAddresses } from '#/server/shopify/customer.functions'
5
+
6
+ export const Route = createFileRoute('/shop/account/addresses')({
7
+ loader: () => getAddresses(),
8
+ component: AddressesRoute,
9
+ })
10
+
11
+ function AddressesRoute() {
12
+ const { addresses, defaultAddressId } = Route.useLoaderData()
13
+
14
+ if (addresses.length === 0) {
15
+ return (
16
+ <div className="space-y-4">
17
+ <h2 className="text-2xl font-medium tracking-tight">Addresses</h2>
18
+ <div className="rounded-2xl border border-dashed border-[var(--storefront-line)] px-6 py-16 text-center text-[var(--storefront-fg-muted)]">
19
+ No saved addresses yet. Addresses you use at checkout will appear here.
20
+ </div>
21
+ </div>
22
+ )
23
+ }
24
+
25
+ return (
26
+ <div className="space-y-4">
27
+ <h2 className="text-2xl font-medium tracking-tight">Addresses</h2>
28
+ <ul className="grid gap-4 sm:grid-cols-2">
29
+ {addresses.map((address) => (
30
+ <li
31
+ key={address.id}
32
+ className="space-y-1 rounded-2xl border border-[var(--storefront-line)] p-4 text-sm"
33
+ >
34
+ <p className="font-medium">
35
+ {[address.firstName, address.lastName].filter(Boolean).join(' ')}
36
+ {address.id === defaultAddressId && (
37
+ <span className="ml-2 rounded-full bg-[var(--storefront-line)]/60 px-2 py-0.5 text-xs">
38
+ Default
39
+ </span>
40
+ )}
41
+ </p>
42
+ <p className="text-[var(--storefront-fg-muted)]">
43
+ {address.address1}
44
+ {address.address2 && (
45
+ <>
46
+ <br />
47
+ {address.address2}
48
+ </>
49
+ )}
50
+ <br />
51
+ {[address.city, address.zoneCode, address.zip]
52
+ .filter(Boolean)
53
+ .join(', ')}
54
+ <br />
55
+ {address.territoryCode}
56
+ </p>
57
+ {address.phoneNumber && (
58
+ <p className="text-[var(--storefront-fg-muted)]">
59
+ {address.phoneNumber}
60
+ </p>
61
+ )}
62
+ </li>
63
+ ))}
64
+ </ul>
65
+ </div>
66
+ )
67
+ }
@@ -0,0 +1,45 @@
1
+ <% if (addOnOption.shopify.customerAccount !== 'enabled') { ignoreFile(); return; } %>
2
+ import { createFileRoute, redirect } from '@tanstack/react-router'
3
+ import * as v from 'valibot'
4
+
5
+ import { completeLogin } from '#/server/shopify/customer.functions'
6
+
7
+ const CallbackSearch = v.object({
8
+ code: v.optional(v.string()),
9
+ state: v.optional(v.string()),
10
+ error: v.optional(v.string()),
11
+ error_description: v.optional(v.string()),
12
+ })
13
+
14
+ export const Route = createFileRoute('/shop/account/callback')({
15
+ validateSearch: (search) => v.parse(CallbackSearch, search),
16
+ beforeLoad: async ({ search }) => {
17
+ if (search.error || !search.code || !search.state) {
18
+ return { error: search.error_description ?? search.error ?? 'Missing OAuth code/state.' }
19
+ }
20
+ try {
21
+ const { redirectAfter } = await completeLogin({
22
+ data: { code: search.code, state: search.state },
23
+ })
24
+ throw redirect({ to: redirectAfter || '/shop/account' })
25
+ } catch (err) {
26
+ if (err && typeof err === 'object' && 'isRedirect' in err) throw err
27
+ return { error: err instanceof Error ? err.message : String(err) }
28
+ }
29
+ },
30
+ loader: ({ context }) => ({ error: context.error }),
31
+ component: CallbackRoute,
32
+ })
33
+
34
+ function CallbackRoute() {
35
+ const { error } = Route.useLoaderData()
36
+ if (error) {
37
+ return (
38
+ <div className="mx-auto max-w-xl space-y-3 rounded-2xl border border-red-500/40 p-8 text-center">
39
+ <h2 className="text-xl font-medium">Sign-in failed</h2>
40
+ <p className="text-sm text-[var(--storefront-fg-muted)]">{error}</p>
41
+ </div>
42
+ )
43
+ }
44
+ return null
45
+ }
@@ -0,0 +1,70 @@
1
+ <% if (addOnOption.shopify.customerAccount !== 'enabled') { ignoreFile(); return; } %>
2
+ import { Link, createFileRoute } from '@tanstack/react-router'
3
+
4
+ import { Money } from '#/components/shop/money'
5
+ import { getOrders } from '#/server/shopify/customer.functions'
6
+
7
+ export const Route = createFileRoute('/shop/account/')({
8
+ loader: () => getOrders({ data: { first: 5 } }),
9
+ component: AccountOverview,
10
+ })
11
+
12
+ function AccountOverview() {
13
+ const orders = Route.useLoaderData()
14
+ const { customer } = Route.useRouteContext()
15
+
16
+ return (
17
+ <div className="space-y-10">
18
+ <header className="space-y-1">
19
+ <p className="text-sm uppercase tracking-wider text-[var(--storefront-fg-muted)]">
20
+ Welcome back
21
+ </p>
22
+ <h2 className="text-3xl font-medium tracking-tight">
23
+ {customer.firstName ?? customer.emailAddress?.emailAddress ?? 'Customer'}
24
+ </h2>
25
+ </header>
26
+
27
+ <section className="space-y-4">
28
+ <div className="flex items-baseline justify-between">
29
+ <h3 className="text-lg font-medium">Recent orders</h3>
30
+ <Link
31
+ to="/shop/account/orders"
32
+ className="text-sm underline underline-offset-4"
33
+ >
34
+ View all
35
+ </Link>
36
+ </div>
37
+ {orders.nodes.length === 0 ? (
38
+ <p className="text-[var(--storefront-fg-muted)]">No orders yet.</p>
39
+ ) : (
40
+ <ul className="divide-y divide-[var(--storefront-line)] rounded-2xl border border-[var(--storefront-line)]">
41
+ {orders.nodes.map((order) => (
42
+ <li
43
+ key={order.id}
44
+ className="flex items-center justify-between gap-4 px-4 py-3"
45
+ >
46
+ <div>
47
+ <Link
48
+ to="/shop/account/orders/$id"
49
+ params={{ id: encodeURIComponent(order.id) }}
50
+ className="font-medium underline-offset-4 hover:underline"
51
+ >
52
+ {order.name}
53
+ </Link>
54
+ <p className="text-xs text-[var(--storefront-fg-muted)]">
55
+ {new Date(order.processedAt).toLocaleDateString()} ·{' '}
56
+ {order.fulfillmentStatus ?? 'pending'}
57
+ </p>
58
+ </div>
59
+ <Money
60
+ amount={order.totalPrice.amount}
61
+ currencyCode={order.totalPrice.currencyCode}
62
+ />
63
+ </li>
64
+ ))}
65
+ </ul>
66
+ )}
67
+ </section>
68
+ </div>
69
+ )
70
+ }
@@ -0,0 +1,59 @@
1
+ <% if (addOnOption.shopify.customerAccount !== 'enabled') { ignoreFile(); return; } %>
2
+ import { useEffect, useState } from 'react'
3
+ import { createFileRoute } from '@tanstack/react-router'
4
+ import * as v from 'valibot'
5
+
6
+ import { isCustomerAccountConfigured } from '#/server/shopify/env'
7
+ import { startLogin } from '#/server/shopify/customer.functions'
8
+
9
+ const LoginSearch = v.object({ redirect: v.optional(v.string(), '/shop/account') })
10
+
11
+ export const Route = createFileRoute('/shop/account/login')({
12
+ validateSearch: (search) => v.parse(LoginSearch, search),
13
+ loader: () => ({ configured: isCustomerAccountConfigured() }),
14
+ component: LoginRoute,
15
+ })
16
+
17
+ function LoginRoute() {
18
+ const { redirect } = Route.useSearch()
19
+ const { configured } = Route.useLoaderData()
20
+ const [error, setError] = useState<string | null>(null)
21
+
22
+ useEffect(() => {
23
+ if (!configured) return
24
+ startLogin({ data: { redirectAfter: redirect } })
25
+ .then(({ authorizationUrl }) => {
26
+ window.location.assign(authorizationUrl)
27
+ })
28
+ .catch((err) => setError(err instanceof Error ? err.message : String(err)))
29
+ }, [configured, redirect])
30
+
31
+ if (!configured) {
32
+ return (
33
+ <div className="mx-auto max-w-xl space-y-3 rounded-2xl border border-[var(--storefront-line)] p-8 text-center">
34
+ <h2 className="text-xl font-medium">Customer accounts not configured</h2>
35
+ <p className="text-sm text-[var(--storefront-fg-muted)]">
36
+ Set <code>SHOPIFY_CUSTOMER_ACCOUNT_CLIENT_ID</code>,{' '}
37
+ <code>SHOPIFY_CUSTOMER_ACCOUNT_SHOP_ID</code>, and{' '}
38
+ <code>SHOPIFY_SESSION_SECRET</code> in <code>.env.local</code>, then
39
+ register the redirect URI in your Shopify admin.
40
+ </p>
41
+ </div>
42
+ )
43
+ }
44
+
45
+ if (error) {
46
+ return (
47
+ <div className="mx-auto max-w-xl space-y-3 rounded-2xl border border-red-500/40 p-8 text-center">
48
+ <h2 className="text-xl font-medium">Couldn't start sign-in</h2>
49
+ <p className="text-sm text-[var(--storefront-fg-muted)]">{error}</p>
50
+ </div>
51
+ )
52
+ }
53
+
54
+ return (
55
+ <p className="py-16 text-center text-[var(--storefront-fg-muted)]">
56
+ Redirecting to Shopify…
57
+ </p>
58
+ )
59
+ }
@@ -0,0 +1,16 @@
1
+ <% if (addOnOption.shopify.customerAccount !== 'enabled') { ignoreFile(); return; } %>
2
+ import { createFileRoute, redirect } from '@tanstack/react-router'
3
+
4
+ import { logout } from '#/server/shopify/customer.functions'
5
+
6
+ export const Route = createFileRoute('/shop/account/logout')({
7
+ beforeLoad: async () => {
8
+ const { endSessionUrl } = await logout()
9
+ if (endSessionUrl) {
10
+ // Bounce through Shopify's end-session endpoint to clear their session too.
11
+ throw redirect({ href: endSessionUrl })
12
+ }
13
+ throw redirect({ to: '/shop' })
14
+ },
15
+ component: () => null,
16
+ })
@@ -0,0 +1,126 @@
1
+ <% if (addOnOption.shopify.customerAccount !== 'enabled') { ignoreFile(); return; } %>
2
+ import { createFileRoute, notFound } from '@tanstack/react-router'
3
+
4
+ import { Money } from '#/components/shop/money'
5
+ import { ShopImage } from '#/components/shop/shop-image'
6
+ import { getOrder } from '#/server/shopify/customer.functions'
7
+
8
+ export const Route = createFileRoute('/shop/account/orders/$id')({
9
+ loader: async ({ params }) => {
10
+ const order = await getOrder({ data: { id: decodeURIComponent(params.id) } })
11
+ if (!order) throw notFound()
12
+ return { order }
13
+ },
14
+ component: OrderDetailRoute,
15
+ })
16
+
17
+ function OrderDetailRoute() {
18
+ const { order } = Route.useLoaderData()
19
+
20
+ return (
21
+ <article className="space-y-8">
22
+ <header className="space-y-1">
23
+ <h2 className="text-2xl font-medium tracking-tight">{order.name}</h2>
24
+ <p className="text-sm text-[var(--storefront-fg-muted)]">
25
+ Placed {new Date(order.processedAt).toLocaleDateString()} ·{' '}
26
+ {order.fulfillmentStatus ?? 'pending'}
27
+ </p>
28
+ </header>
29
+
30
+ <ul className="divide-y divide-[var(--storefront-line)] rounded-2xl border border-[var(--storefront-line)]">
31
+ {order.lineItems.nodes.map((line) => (
32
+ <li key={line.id} className="flex items-center gap-4 px-4 py-3">
33
+ <ShopImage
34
+ src={line.image?.url}
35
+ alt={line.image?.altText ?? line.title}
36
+ width={64}
37
+ height={80}
38
+ className="h-20 w-16 rounded-md object-cover"
39
+ />
40
+ <div className="flex-1">
41
+ <p className="font-medium">{line.title}</p>
42
+ {line.variantTitle && (
43
+ <p className="text-xs text-[var(--storefront-fg-muted)]">
44
+ {line.variantTitle}
45
+ </p>
46
+ )}
47
+ <p className="text-xs text-[var(--storefront-fg-muted)]">
48
+ Qty {line.quantity}
49
+ </p>
50
+ </div>
51
+ <Money
52
+ amount={line.price.amount}
53
+ currencyCode={line.price.currencyCode}
54
+ />
55
+ </li>
56
+ ))}
57
+ </ul>
58
+
59
+ <div className="grid gap-2 rounded-2xl border border-[var(--storefront-line)] p-4 text-sm">
60
+ {order.subtotal && (
61
+ <div className="flex justify-between">
62
+ <span className="text-[var(--storefront-fg-muted)]">Subtotal</span>
63
+ <Money
64
+ amount={order.subtotal.amount}
65
+ currencyCode={order.subtotal.currencyCode}
66
+ />
67
+ </div>
68
+ )}
69
+ {order.totalShipping && (
70
+ <div className="flex justify-between">
71
+ <span className="text-[var(--storefront-fg-muted)]">Shipping</span>
72
+ <Money
73
+ amount={order.totalShipping.amount}
74
+ currencyCode={order.totalShipping.currencyCode}
75
+ />
76
+ </div>
77
+ )}
78
+ {order.totalTax && (
79
+ <div className="flex justify-between">
80
+ <span className="text-[var(--storefront-fg-muted)]">Tax</span>
81
+ <Money
82
+ amount={order.totalTax.amount}
83
+ currencyCode={order.totalTax.currencyCode}
84
+ />
85
+ </div>
86
+ )}
87
+ <div className="flex justify-between border-t border-[var(--storefront-line)] pt-2 font-medium">
88
+ <span>Total</span>
89
+ <Money
90
+ amount={order.totalPrice.amount}
91
+ currencyCode={order.totalPrice.currencyCode}
92
+ />
93
+ </div>
94
+ </div>
95
+
96
+ {order.shippingAddress && (
97
+ <div className="space-y-1 text-sm">
98
+ <h3 className="font-medium">Shipping address</h3>
99
+ <p className="text-[var(--storefront-fg-muted)]">
100
+ {[order.shippingAddress.firstName, order.shippingAddress.lastName]
101
+ .filter(Boolean)
102
+ .join(' ')}
103
+ <br />
104
+ {order.shippingAddress.address1}
105
+ {order.shippingAddress.address2 && (
106
+ <>
107
+ <br />
108
+ {order.shippingAddress.address2}
109
+ </>
110
+ )}
111
+ <br />
112
+ {[
113
+ order.shippingAddress.city,
114
+ order.shippingAddress.zoneCode,
115
+ order.shippingAddress.zip,
116
+ ]
117
+ .filter(Boolean)
118
+ .join(', ')}
119
+ <br />
120
+ {order.shippingAddress.territoryCode}
121
+ </p>
122
+ </div>
123
+ )}
124
+ </article>
125
+ )
126
+ }
@@ -0,0 +1,50 @@
1
+ <% if (addOnOption.shopify.customerAccount !== 'enabled') { ignoreFile(); return; } %>
2
+ import { Link, createFileRoute } from '@tanstack/react-router'
3
+
4
+ import { Money } from '#/components/shop/money'
5
+ import { getOrders } from '#/server/shopify/customer.functions'
6
+
7
+ export const Route = createFileRoute('/shop/account/orders/')({
8
+ loader: () => getOrders({ data: { first: 50 } }),
9
+ component: OrdersRoute,
10
+ })
11
+
12
+ function OrdersRoute() {
13
+ const orders = Route.useLoaderData()
14
+ if (orders.nodes.length === 0) {
15
+ return (
16
+ <div className="rounded-2xl border border-dashed border-[var(--storefront-line)] px-6 py-16 text-center">
17
+ <p className="text-[var(--storefront-fg-muted)]">No orders yet.</p>
18
+ </div>
19
+ )
20
+ }
21
+ return (
22
+ <div className="space-y-4">
23
+ <h2 className="text-2xl font-medium tracking-tight">Orders</h2>
24
+ <ul className="divide-y divide-[var(--storefront-line)] rounded-2xl border border-[var(--storefront-line)]">
25
+ {orders.nodes.map((order) => (
26
+ <li key={order.id} className="flex items-center justify-between gap-4 px-4 py-4">
27
+ <div>
28
+ <Link
29
+ to="/shop/account/orders/$id"
30
+ params={{ id: encodeURIComponent(order.id) }}
31
+ className="font-medium underline-offset-4 hover:underline"
32
+ >
33
+ {order.name}
34
+ </Link>
35
+ <p className="text-xs text-[var(--storefront-fg-muted)]">
36
+ {new Date(order.processedAt).toLocaleDateString()} ·{' '}
37
+ {order.fulfillmentStatus ?? 'pending'} ·{' '}
38
+ {order.financialStatus ?? '—'}
39
+ </p>
40
+ </div>
41
+ <Money
42
+ amount={order.totalPrice.amount}
43
+ currencyCode={order.totalPrice.currencyCode}
44
+ />
45
+ </li>
46
+ ))}
47
+ </ul>
48
+ </div>
49
+ )
50
+ }
@@ -0,0 +1,34 @@
1
+ <% if (addOnOption.shopify.customerAccount !== 'enabled') { ignoreFile(); return; } %>
2
+ import { Outlet, createFileRoute, redirect } from '@tanstack/react-router'
3
+
4
+ import { AccountNav } from '#/components/shop/account-nav'
5
+ import { getCustomer } from '#/server/shopify/customer.functions'
6
+
7
+ export const Route = createFileRoute('/shop/account')({
8
+ beforeLoad: async ({ location }) => {
9
+ const customer = await getCustomer()
10
+ if (!customer) {
11
+ throw redirect({
12
+ to: '/shop/account/login',
13
+ search: { redirect: location.href },
14
+ })
15
+ }
16
+ return { customer }
17
+ },
18
+ loader: ({ context }) => ({ customer: context.customer }),
19
+ component: AccountLayout,
20
+ })
21
+
22
+ function AccountLayout() {
23
+ return (
24
+ <div className="grid gap-10 lg:grid-cols-[200px_1fr]">
25
+ <aside>
26
+ <h1 className="mb-4 text-xl font-medium">Account</h1>
27
+ <AccountNav />
28
+ </aside>
29
+ <main>
30
+ <Outlet />
31
+ </main>
32
+ </div>
33
+ )
34
+ }
@@ -0,0 +1,45 @@
1
+ import { createFileRoute } from '@tanstack/react-router'
2
+
3
+ import { CartLineItem } from '#/components/shop/cart-line-item'
4
+ import { CartSummary } from '#/components/shop/cart-summary'
5
+ import { EmptyState } from '#/components/shop/empty-state'
6
+ import { CART_QUERY_KEY, useCart } from '#/hooks/use-cart'
7
+ import { getCart } from '#/server/shopify/cart.functions'
8
+
9
+ export const Route = createFileRoute('/shop/cart')({
10
+ // QueryClient is always in context — the shopify add-on dependsOn tanstack-query.
11
+ loader: ({ context }) =>
12
+ context.queryClient.ensureQueryData({
13
+ queryKey: CART_QUERY_KEY,
14
+ queryFn: () => getCart(),
15
+ }),
16
+ component: CartRoute,
17
+ })
18
+
19
+ function CartRoute() {
20
+ const { cart } = useCart()
21
+
22
+ if (!cart || cart.lines.nodes.length === 0) {
23
+ return (
24
+ <EmptyState
25
+ title="Your cart is empty"
26
+ description="Looks like you haven't added anything yet. Browse the shop to find something."
27
+ cta={{ label: 'Browse the shop', to: '/shop' }}
28
+ />
29
+ )
30
+ }
31
+
32
+ return (
33
+ <div className="grid gap-10 lg:grid-cols-[1.6fr_1fr]">
34
+ <section>
35
+ <h1 className="mb-6 text-3xl font-medium tracking-tight">Cart</h1>
36
+ <ul className="border-t border-[var(--storefront-line)]">
37
+ {cart.lines.nodes.map((line) => (
38
+ <CartLineItem key={line.id} line={line} />
39
+ ))}
40
+ </ul>
41
+ </section>
42
+ <CartSummary cart={cart} />
43
+ </div>
44
+ )
45
+ }
@@ -0,0 +1,66 @@
1
+ import { createFileRoute, notFound } from '@tanstack/react-router'
2
+
3
+ import { ProductGrid } from '#/components/shop/product-grid'
4
+ import { ShopImage } from '#/components/shop/shop-image'
5
+ import { getCollection } from '#/server/shopify/catalog.functions'
6
+
7
+ export const Route = createFileRoute('/shop/collections/$handle')({
8
+ loader: async ({ params }) => {
9
+ const collection = await getCollection({
10
+ data: { handle: params.handle, first: 24 },
11
+ })
12
+ if (!collection) throw notFound()
13
+ return { collection }
14
+ },
15
+ head: ({ loaderData }) => ({
16
+ meta: loaderData
17
+ ? [
18
+ {
19
+ title: loaderData.collection.seo.title ?? loaderData.collection.title,
20
+ },
21
+ {
22
+ name: 'description',
23
+ content:
24
+ loaderData.collection.seo.description ??
25
+ loaderData.collection.description ??
26
+ '',
27
+ },
28
+ ]
29
+ : [],
30
+ }),
31
+ component: CollectionRoute,
32
+ })
33
+
34
+ function CollectionRoute() {
35
+ const { collection } = Route.useLoaderData()
36
+
37
+ return (
38
+ <div className="space-y-10">
39
+ <header className="space-y-4">
40
+ {collection.image && (
41
+ <ShopImage
42
+ src={collection.image.url}
43
+ alt={collection.image.altText ?? collection.title}
44
+ width={1400}
45
+ height={500}
46
+ loading="eager"
47
+ sizes="100vw"
48
+ className="aspect-[14/5] w-full rounded-lg object-cover"
49
+ />
50
+ )}
51
+ <div className="space-y-2">
52
+ <h1 className="text-3xl font-medium tracking-tight">
53
+ {collection.title}
54
+ </h1>
55
+ {collection.description && (
56
+ <p className="max-w-2xl text-[var(--storefront-fg-muted)]">
57
+ {collection.description}
58
+ </p>
59
+ )}
60
+ </div>
61
+ </header>
62
+
63
+ <ProductGrid products={collection.products.nodes} />
64
+ </div>
65
+ )
66
+ }
@@ -0,0 +1,36 @@
1
+ import { createFileRoute } from '@tanstack/react-router'
2
+
3
+ import { ProductGrid } from '#/components/shop/product-grid'
4
+ import {
5
+ getProducts,
6
+ getShop,
7
+ } from '#/server/shopify/catalog.functions'
8
+
9
+ export const Route = createFileRoute('/shop/')({
10
+ loader: async () => {
11
+ const [shop, page] = await Promise.all([
12
+ getShop(),
13
+ getProducts({ data: { first: 24, sortKey: 'BEST_SELLING' } }),
14
+ ])
15
+ return { shop, products: page.nodes }
16
+ },
17
+ component: ShopIndex,
18
+ })
19
+
20
+ function ShopIndex() {
21
+ const { shop, products } = Route.useLoaderData()
22
+
23
+ return (
24
+ <div className="space-y-12">
25
+ <header className="space-y-3">
26
+ <h1 className="text-4xl font-medium tracking-tight">{shop.name}</h1>
27
+ {shop.description && (
28
+ <p className="max-w-2xl text-lg text-[var(--storefront-fg-muted)]">
29
+ {shop.description}
30
+ </p>
31
+ )}
32
+ </header>
33
+ <ProductGrid products={products} />
34
+ </div>
35
+ )
36
+ }
@@ -0,0 +1,39 @@
1
+ import { createFileRoute, notFound } from '@tanstack/react-router'
2
+
3
+ import { getPage } from '#/server/shopify/catalog.functions'
4
+
5
+ export const Route = createFileRoute('/shop/pages/$handle')({
6
+ loader: async ({ params }) => {
7
+ const page = await getPage({ data: { handle: params.handle } })
8
+ if (!page) throw notFound()
9
+ return { page }
10
+ },
11
+ head: ({ loaderData }) => ({
12
+ meta: loaderData
13
+ ? [
14
+ { title: loaderData.page.seo.title ?? loaderData.page.title },
15
+ {
16
+ name: 'description',
17
+ content:
18
+ loaderData.page.seo.description ??
19
+ loaderData.page.bodySummary ??
20
+ '',
21
+ },
22
+ ]
23
+ : [],
24
+ }),
25
+ component: PageRoute,
26
+ })
27
+
28
+ function PageRoute() {
29
+ const { page } = Route.useLoaderData()
30
+ return (
31
+ <article className="space-y-6">
32
+ <h1 className="text-3xl font-medium tracking-tight">{page.title}</h1>
33
+ <div
34
+ className="shop-prose"
35
+ dangerouslySetInnerHTML={{ __html: page.body }}
36
+ />
37
+ </article>
38
+ )
39
+ }