@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,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
+ }