@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.
- package/CHANGELOG.md +64 -0
- package/dist/frameworks/react/add-ons/shopify/README.md +86 -0
- package/dist/frameworks/react/add-ons/shopify/assets/_dot_env.local.append +19 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/account-nav.tsx.ejs +41 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/add-to-cart-button.tsx +48 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/cart-line-item.tsx +94 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/cart-summary.tsx +111 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/empty-state.tsx +29 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/money.tsx +11 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/product-card.tsx +74 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/product-grid.tsx +24 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/shop-image.tsx +57 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/shop.css +58 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/variant-selector.tsx +79 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/hooks/use-cart.ts +276 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/hooks/use-customer.ts.ejs +22 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/integrations/shopify/header-cart.tsx +37 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/lib/shopify/customer-queries.ts.ejs +228 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/lib/shopify/format.ts +33 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/lib/shopify/queries.ts +684 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.addresses.tsx.ejs +67 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.callback.tsx.ejs +45 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.index.tsx.ejs +70 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.login.tsx.ejs +59 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.logout.tsx.ejs +16 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.orders.$id.tsx.ejs +126 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.orders.tsx.ejs +50 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.tsx.ejs +34 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.cart.tsx +45 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.collections.$handle.tsx +66 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.index.tsx +36 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.pages.$handle.tsx +39 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.policies.$handle.tsx +30 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.products.$handle.tsx +106 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.search.tsx +75 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.tsx +78 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/cart.functions.ts +207 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/catalog.functions.ts +244 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/cookies.ts +29 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/customer-client.ts.ejs +99 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/customer-cookies.ts.ejs +49 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/customer.functions.ts.ejs +168 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/env.ts +89 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/oauth.ts.ejs +301 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/storefront-client.ts +101 -0
- package/dist/frameworks/react/add-ons/shopify/info.json +104 -0
- package/dist/frameworks/react/add-ons/shopify/package.json +6 -0
- package/dist/frameworks/react/add-ons/shopify/small-logo.svg +1 -0
- package/dist/frameworks/react/examples/shopify-storefront/README.md +39 -0
- package/dist/frameworks/react/examples/shopify-storefront/assets/src/components/FeaturedCollections.tsx +43 -0
- package/dist/frameworks/react/examples/shopify-storefront/assets/src/components/ShopHero.tsx +39 -0
- package/dist/frameworks/react/examples/shopify-storefront/assets/src/routes/index.tsx +65 -0
- package/dist/frameworks/react/examples/shopify-storefront/info.json +18 -0
- package/dist/frameworks/react/examples/shopify-storefront/package.json +3 -0
- package/dist/frameworks/react/project/base/src/components/Header.tsx.ejs +34 -34
- package/package.json +1 -1
- package/src/frameworks/react/add-ons/shopify/README.md +86 -0
- package/src/frameworks/react/add-ons/shopify/assets/_dot_env.local.append +19 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/account-nav.tsx.ejs +41 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/add-to-cart-button.tsx +48 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/cart-line-item.tsx +94 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/cart-summary.tsx +111 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/empty-state.tsx +29 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/money.tsx +11 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/product-card.tsx +74 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/product-grid.tsx +24 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/shop-image.tsx +57 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/shop.css +58 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/variant-selector.tsx +79 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/hooks/use-cart.ts +276 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/hooks/use-customer.ts.ejs +22 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/integrations/shopify/header-cart.tsx +37 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/lib/shopify/customer-queries.ts.ejs +228 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/lib/shopify/format.ts +33 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/lib/shopify/queries.ts +684 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.addresses.tsx.ejs +67 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.callback.tsx.ejs +45 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.index.tsx.ejs +70 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.login.tsx.ejs +59 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.logout.tsx.ejs +16 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.orders.$id.tsx.ejs +126 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.orders.tsx.ejs +50 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.tsx.ejs +34 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.cart.tsx +45 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.collections.$handle.tsx +66 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.index.tsx +36 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.pages.$handle.tsx +39 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.policies.$handle.tsx +30 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.products.$handle.tsx +106 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.search.tsx +75 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.tsx +78 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/cart.functions.ts +207 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/catalog.functions.ts +244 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/cookies.ts +29 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/customer-client.ts.ejs +99 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/customer-cookies.ts.ejs +49 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/customer.functions.ts.ejs +168 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/env.ts +89 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/oauth.ts.ejs +301 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/storefront-client.ts +101 -0
- package/src/frameworks/react/add-ons/shopify/info.json +104 -0
- package/src/frameworks/react/add-ons/shopify/package.json +6 -0
- package/src/frameworks/react/add-ons/shopify/small-logo.svg +1 -0
- package/src/frameworks/react/examples/shopify-storefront/README.md +39 -0
- package/src/frameworks/react/examples/shopify-storefront/assets/src/components/FeaturedCollections.tsx +43 -0
- package/src/frameworks/react/examples/shopify-storefront/assets/src/components/ShopHero.tsx +39 -0
- package/src/frameworks/react/examples/shopify-storefront/assets/src/routes/index.tsx +65 -0
- package/src/frameworks/react/examples/shopify-storefront/info.json +18 -0
- package/src/frameworks/react/examples/shopify-storefront/package.json +3 -0
- package/src/frameworks/react/project/base/src/components/Header.tsx.ejs +34 -34
package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.addresses.tsx.ejs
ADDED
|
@@ -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
|
+
})
|
package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.orders.$id.tsx.ejs
ADDED
|
@@ -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
|
+
}
|