@shopify/cli-hydrogen 3.26.0 → 4.0.0-alpha.1
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/dist/commands/hydrogen/build.js +89 -0
- package/dist/commands/hydrogen/dev.js +116 -0
- package/dist/commands/hydrogen/init.js +42 -0
- package/dist/commands/hydrogen/preview.js +34 -0
- package/dist/hooks/init.js +21 -0
- package/dist/templates/demo-store/.editorconfig +8 -0
- package/dist/templates/demo-store/.eslintignore +4 -0
- package/dist/templates/demo-store/.eslintrc.js +16 -0
- package/dist/templates/demo-store/.graphqlrc.yml +1 -0
- package/dist/templates/demo-store/.prettierignore +2 -0
- package/dist/templates/demo-store/.turbo/turbo-build.log +13 -0
- package/dist/templates/demo-store/app/components/AccountAddressBook.tsx +97 -0
- package/dist/templates/demo-store/app/components/AccountDetails.tsx +41 -0
- package/dist/templates/demo-store/app/components/AddToCartButton.tsx +42 -0
- package/dist/templates/demo-store/app/components/Breadcrumbs.tsx +36 -0
- package/dist/templates/demo-store/app/components/Button.tsx +56 -0
- package/dist/templates/demo-store/app/components/Cart.tsx +431 -0
- package/dist/templates/demo-store/app/components/CartLoading.tsx +50 -0
- package/dist/templates/demo-store/app/components/CountrySelector.tsx +180 -0
- package/dist/templates/demo-store/app/components/Drawer.tsx +115 -0
- package/dist/templates/demo-store/app/components/FeaturedCollections.tsx +54 -0
- package/dist/templates/demo-store/app/components/FeaturedProducts.tsx +116 -0
- package/dist/templates/demo-store/app/components/FeaturedSection.tsx +39 -0
- package/dist/templates/demo-store/app/components/GenericError.tsx +58 -0
- package/dist/templates/demo-store/app/components/Grid.tsx +44 -0
- package/dist/templates/demo-store/app/components/Hero.tsx +136 -0
- package/dist/templates/demo-store/app/components/Icon.tsx +253 -0
- package/dist/templates/demo-store/app/components/Input.tsx +24 -0
- package/dist/templates/demo-store/app/components/Layout.tsx +492 -0
- package/dist/templates/demo-store/app/components/Link.tsx +46 -0
- package/dist/templates/demo-store/app/components/Modal.tsx +46 -0
- package/dist/templates/demo-store/app/components/NotFound.tsx +22 -0
- package/dist/templates/demo-store/app/components/OrderCard.tsx +85 -0
- package/dist/templates/demo-store/app/components/Pagination.tsx +277 -0
- package/dist/templates/demo-store/app/components/ProductCard.tsx +146 -0
- package/dist/templates/demo-store/app/components/ProductGallery.tsx +114 -0
- package/dist/templates/demo-store/app/components/ProductGrid.tsx +93 -0
- package/dist/templates/demo-store/app/components/ProductSwimlane.tsx +30 -0
- package/dist/templates/demo-store/app/components/Skeleton.tsx +24 -0
- package/dist/templates/demo-store/app/components/SortFilter.tsx +411 -0
- package/dist/templates/demo-store/app/components/Text.tsx +192 -0
- package/dist/templates/demo-store/app/components/index.ts +28 -0
- package/dist/templates/demo-store/app/data/countries.ts +194 -0
- package/dist/templates/demo-store/app/data/index.ts +1037 -0
- package/dist/templates/demo-store/app/entry.client.tsx +4 -0
- package/dist/templates/demo-store/app/entry.server.tsx +26 -0
- package/dist/templates/demo-store/app/hooks/useCartFetchers.tsx +14 -0
- package/dist/templates/demo-store/app/hooks/useIsHydrated.tsx +12 -0
- package/dist/templates/demo-store/app/lib/const.ts +10 -0
- package/dist/templates/demo-store/app/lib/placeholders.ts +242 -0
- package/dist/templates/demo-store/app/lib/seo/common.tsx +324 -0
- package/dist/templates/demo-store/app/lib/seo/debugger.tsx +175 -0
- package/dist/templates/demo-store/app/lib/seo/image.tsx +32 -0
- package/dist/templates/demo-store/app/lib/seo/index.ts +4 -0
- package/dist/templates/demo-store/app/lib/seo/seo.tsx +24 -0
- package/dist/templates/demo-store/app/lib/seo/types.ts +70 -0
- package/dist/templates/demo-store/app/lib/session.server.ts +57 -0
- package/dist/templates/demo-store/app/lib/type.ts +21 -0
- package/dist/templates/demo-store/app/lib/utils.ts +310 -0
- package/dist/templates/demo-store/app/root.tsx +282 -0
- package/dist/templates/demo-store/app/routes/$.tsx +7 -0
- package/dist/templates/demo-store/app/routes/$lang/$.tsx +1 -0
- package/dist/templates/demo-store/app/routes/$lang/[robots.txt].tsx +1 -0
- package/dist/templates/demo-store/app/routes/$lang/[sitemap.xml].tsx +1 -0
- package/dist/templates/demo-store/app/routes/$lang/account/__private/address/$id.tsx +1 -0
- package/dist/templates/demo-store/app/routes/$lang/account/__private/edit.tsx +1 -0
- package/dist/templates/demo-store/app/routes/$lang/account/__private/logout.ts +1 -0
- package/dist/templates/demo-store/app/routes/$lang/account/__private/orders.$id.tsx +1 -0
- package/dist/templates/demo-store/app/routes/$lang/account/__public/activate.$id.$activationToken.tsx +6 -0
- package/dist/templates/demo-store/app/routes/$lang/account/__public/login.tsx +7 -0
- package/dist/templates/demo-store/app/routes/$lang/account/__public/recover.tsx +1 -0
- package/dist/templates/demo-store/app/routes/$lang/account/__public/register.tsx +6 -0
- package/dist/templates/demo-store/app/routes/$lang/account/__public/reset.$id.$resetToken.tsx +5 -0
- package/dist/templates/demo-store/app/routes/$lang/account.tsx +1 -0
- package/dist/templates/demo-store/app/routes/$lang/api/countries.tsx +1 -0
- package/dist/templates/demo-store/app/routes/$lang/api/products.tsx +1 -0
- package/dist/templates/demo-store/app/routes/$lang/cart.tsx +1 -0
- package/dist/templates/demo-store/app/routes/$lang/collections/$collectionHandle.tsx +6 -0
- package/dist/templates/demo-store/app/routes/$lang/collections/all.tsx +1 -0
- package/dist/templates/demo-store/app/routes/$lang/collections/index.tsx +1 -0
- package/dist/templates/demo-store/app/routes/$lang/featured-products.tsx +1 -0
- package/dist/templates/demo-store/app/routes/$lang/index.tsx +7 -0
- package/dist/templates/demo-store/app/routes/$lang/journal/$journalHandle.tsx +7 -0
- package/dist/templates/demo-store/app/routes/$lang/journal/index.tsx +1 -0
- package/dist/templates/demo-store/app/routes/$lang/og-image.tsx +1 -0
- package/dist/templates/demo-store/app/routes/$lang/pages/$pageHandle.tsx +1 -0
- package/dist/templates/demo-store/app/routes/$lang/policies/$policyHandle.tsx +1 -0
- package/dist/templates/demo-store/app/routes/$lang/policies/index.tsx +1 -0
- package/dist/templates/demo-store/app/routes/$lang/products/$productHandle.tsx +6 -0
- package/dist/templates/demo-store/app/routes/$lang/products/index.tsx +1 -0
- package/dist/templates/demo-store/app/routes/$lang/search.tsx +6 -0
- package/dist/templates/demo-store/app/routes/[robots.txt].tsx +40 -0
- package/dist/templates/demo-store/app/routes/[sitemap.xml].tsx +198 -0
- package/dist/templates/demo-store/app/routes/account/__private/address/$id.tsx +320 -0
- package/dist/templates/demo-store/app/routes/account/__private/edit.tsx +273 -0
- package/dist/templates/demo-store/app/routes/account/__private/logout.ts +29 -0
- package/dist/templates/demo-store/app/routes/account/__private/orders.$id.tsx +324 -0
- package/dist/templates/demo-store/app/routes/account/__public/activate.$id.$activationToken.tsx +218 -0
- package/dist/templates/demo-store/app/routes/account/__public/login.tsx +197 -0
- package/dist/templates/demo-store/app/routes/account/__public/recover.tsx +144 -0
- package/dist/templates/demo-store/app/routes/account/__public/register.tsx +184 -0
- package/dist/templates/demo-store/app/routes/account/__public/reset.$id.$resetToken.tsx +214 -0
- package/dist/templates/demo-store/app/routes/account.tsx +191 -0
- package/dist/templates/demo-store/app/routes/api/countries.tsx +22 -0
- package/dist/templates/demo-store/app/routes/api/products.tsx +116 -0
- package/dist/templates/demo-store/app/routes/cart.tsx +498 -0
- package/dist/templates/demo-store/app/routes/collections/$collectionHandle.tsx +308 -0
- package/dist/templates/demo-store/app/routes/collections/all.tsx +5 -0
- package/dist/templates/demo-store/app/routes/collections/index.tsx +195 -0
- package/dist/templates/demo-store/app/routes/discounts.$code.tsx +60 -0
- package/dist/templates/demo-store/app/routes/featured-products.tsx +58 -0
- package/dist/templates/demo-store/app/routes/index.tsx +254 -0
- package/dist/templates/demo-store/app/routes/journal/$journalHandle.tsx +147 -0
- package/dist/templates/demo-store/app/routes/journal/index.tsx +150 -0
- package/dist/templates/demo-store/app/routes/og-image.tsx +19 -0
- package/dist/templates/demo-store/app/routes/pages/$pageHandle.tsx +82 -0
- package/dist/templates/demo-store/app/routes/policies/$policyHandle.tsx +117 -0
- package/dist/templates/demo-store/app/routes/policies/index.tsx +104 -0
- package/dist/templates/demo-store/app/routes/products/$productHandle.tsx +561 -0
- package/dist/templates/demo-store/app/routes/products/index.tsx +155 -0
- package/dist/templates/demo-store/app/routes/search.tsx +205 -0
- package/dist/templates/demo-store/app/styles/custom-font.css +13 -0
- package/dist/templates/demo-store/package-lock.json +25515 -0
- package/dist/templates/demo-store/package.json +67 -0
- package/dist/templates/demo-store/playwright.config.ts +109 -0
- package/dist/templates/demo-store/postcss.config.js +10 -0
- package/dist/templates/demo-store/public/favicon.svg +28 -0
- package/dist/templates/demo-store/public/fonts/IBMPlexSerif-Text.woff2 +0 -0
- package/dist/templates/demo-store/public/fonts/IBMPlexSerif-TextItalic.woff2 +0 -0
- package/dist/templates/demo-store/remix.config.js +12 -0
- package/dist/templates/demo-store/remix.env.d.ts +34 -0
- package/dist/templates/demo-store/remix.init/index.ts +15 -0
- package/dist/templates/demo-store/remix.init/package.json +7 -0
- package/dist/templates/demo-store/server.ts +87 -0
- package/dist/templates/demo-store/styles/app.css +182 -0
- package/dist/templates/demo-store/tailwind.config.js +70 -0
- package/dist/templates/demo-store/tests/cart.test.ts +70 -0
- package/dist/templates/demo-store/tests/seo.test.ts +36 -0
- package/dist/templates/demo-store/tests/utils.ts +100 -0
- package/dist/templates/demo-store/tsconfig.json +26 -0
- package/dist/templates/hello-world/.eslintignore +4 -0
- package/dist/templates/hello-world/.eslintrc.js +6 -0
- package/dist/templates/hello-world/.graphqlrc.yml +1 -0
- package/dist/templates/hello-world/.turbo/turbo-build.log +9 -0
- package/dist/templates/hello-world/README.md +20 -0
- package/dist/templates/hello-world/app/components/Layout.tsx +15 -0
- package/dist/templates/hello-world/app/components/index.ts +1 -0
- package/dist/templates/hello-world/app/entry.client.tsx +4 -0
- package/dist/templates/hello-world/app/entry.server.tsx +21 -0
- package/dist/templates/hello-world/app/root.tsx +212 -0
- package/dist/templates/hello-world/app/routes/index.tsx +7 -0
- package/dist/templates/hello-world/app/styles/app.css +38 -0
- package/dist/templates/hello-world/package-lock.json +27641 -0
- package/dist/templates/hello-world/package.json +41 -0
- package/dist/templates/hello-world/public/favicon.svg +28 -0
- package/dist/templates/hello-world/remix.env.d.ts +29 -0
- package/dist/templates/hello-world/server.ts +127 -0
- package/dist/templates/hello-world/tsconfig.json +25 -0
- package/dist/utils/config.js +81 -0
- package/dist/utils/flags.js +15 -0
- package/dist/utils/log.js +20 -0
- package/dist/utils/mini-oxygen.js +70 -0
- package/package.json +27 -64
- package/tmp-create-app.mjs +29 -0
- package/LICENSE +0 -8
- package/README.md +0 -61
- package/dist/cli/commands/hydrogen/add/eslint.d.ts +0 -11
- package/dist/cli/commands/hydrogen/add/eslint.js +0 -26
- package/dist/cli/commands/hydrogen/add/eslint.js.map +0 -1
- package/dist/cli/commands/hydrogen/add/tailwind.d.ts +0 -11
- package/dist/cli/commands/hydrogen/add/tailwind.js +0 -26
- package/dist/cli/commands/hydrogen/add/tailwind.js.map +0 -1
- package/dist/cli/commands/hydrogen/build.d.ts +0 -14
- package/dist/cli/commands/hydrogen/build.js +0 -49
- package/dist/cli/commands/hydrogen/build.js.map +0 -1
- package/dist/cli/commands/hydrogen/deploy.d.ts +0 -19
- package/dist/cli/commands/hydrogen/deploy.js +0 -58
- package/dist/cli/commands/hydrogen/deploy.js.map +0 -1
- package/dist/cli/commands/hydrogen/dev.d.ts +0 -13
- package/dist/cli/commands/hydrogen/dev.js +0 -31
- package/dist/cli/commands/hydrogen/dev.js.map +0 -1
- package/dist/cli/commands/hydrogen/info.d.ts +0 -12
- package/dist/cli/commands/hydrogen/info.js +0 -28
- package/dist/cli/commands/hydrogen/info.js.map +0 -1
- package/dist/cli/commands/hydrogen/preview.d.ts +0 -13
- package/dist/cli/commands/hydrogen/preview.js +0 -46
- package/dist/cli/commands/hydrogen/preview.js.map +0 -1
- package/dist/cli/constants.d.ts +0 -15
- package/dist/cli/constants.js +0 -16
- package/dist/cli/constants.js.map +0 -1
- package/dist/cli/flags.d.ts +0 -4
- package/dist/cli/flags.js +0 -16
- package/dist/cli/flags.js.map +0 -1
- package/dist/cli/models/hydrogen.d.ts +0 -22
- package/dist/cli/models/hydrogen.js +0 -82
- package/dist/cli/models/hydrogen.js.map +0 -1
- package/dist/cli/prompts/git-init.d.ts +0 -1
- package/dist/cli/prompts/git-init.js +0 -16
- package/dist/cli/prompts/git-init.js.map +0 -1
- package/dist/cli/services/build/check-lockfile.d.ts +0 -3
- package/dist/cli/services/build/check-lockfile.js +0 -80
- package/dist/cli/services/build/check-lockfile.js.map +0 -1
- package/dist/cli/services/build.d.ts +0 -14
- package/dist/cli/services/build.js +0 -44
- package/dist/cli/services/build.js.map +0 -1
- package/dist/cli/services/deploy/config.d.ts +0 -4
- package/dist/cli/services/deploy/config.js +0 -49
- package/dist/cli/services/deploy/config.js.map +0 -1
- package/dist/cli/services/deploy/error.d.ts +0 -4
- package/dist/cli/services/deploy/error.js +0 -11
- package/dist/cli/services/deploy/error.js.map +0 -1
- package/dist/cli/services/deploy/graphql/create_deployment.d.ts +0 -10
- package/dist/cli/services/deploy/graphql/create_deployment.js +0 -15
- package/dist/cli/services/deploy/graphql/create_deployment.js.map +0 -1
- package/dist/cli/services/deploy/graphql/upload_deployment.d.ts +0 -1
- package/dist/cli/services/deploy/graphql/upload_deployment.js +0 -16
- package/dist/cli/services/deploy/graphql/upload_deployment.js.map +0 -1
- package/dist/cli/services/deploy/types.d.ts +0 -37
- package/dist/cli/services/deploy/types.js +0 -2
- package/dist/cli/services/deploy/types.js.map +0 -1
- package/dist/cli/services/deploy/upload.d.ts +0 -5
- package/dist/cli/services/deploy/upload.js +0 -81
- package/dist/cli/services/deploy/upload.js.map +0 -1
- package/dist/cli/services/deploy.d.ts +0 -2
- package/dist/cli/services/deploy.js +0 -103
- package/dist/cli/services/deploy.js.map +0 -1
- package/dist/cli/services/dev/check-version.d.ts +0 -1
- package/dist/cli/services/dev/check-version.js +0 -30
- package/dist/cli/services/dev/check-version.js.map +0 -1
- package/dist/cli/services/dev.d.ts +0 -10
- package/dist/cli/services/dev.js +0 -36
- package/dist/cli/services/dev.js.map +0 -1
- package/dist/cli/services/eslint.d.ts +0 -8
- package/dist/cli/services/eslint.js +0 -74
- package/dist/cli/services/eslint.js.map +0 -1
- package/dist/cli/services/info.d.ts +0 -7
- package/dist/cli/services/info.js +0 -131
- package/dist/cli/services/info.js.map +0 -1
- package/dist/cli/services/preview.d.ts +0 -12
- package/dist/cli/services/preview.js +0 -63
- package/dist/cli/services/preview.js.map +0 -1
- package/dist/cli/services/tailwind.d.ts +0 -9
- package/dist/cli/services/tailwind.js +0 -103
- package/dist/cli/services/tailwind.js.map +0 -1
- package/dist/cli/utilities/load-config.d.ts +0 -5
- package/dist/cli/utilities/load-config.js +0 -6
- package/dist/cli/utilities/load-config.js.map +0 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/oclif.manifest.json +0 -1
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import {Link, useLocation, type RouteMatch} from '@remix-run/react';
|
|
2
|
+
import {useSeoConfig, useHeadTags, recursivelyInvokeOrReturn} from './common';
|
|
3
|
+
import {renderToString} from 'react-dom/server';
|
|
4
|
+
|
|
5
|
+
const LABEL_MAP = {
|
|
6
|
+
twitterTags: 'Twitter',
|
|
7
|
+
ogTags: 'Open graph',
|
|
8
|
+
links: 'Links',
|
|
9
|
+
tags: 'Meta',
|
|
10
|
+
LdJson: 'Structured data',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function Badge() {
|
|
14
|
+
return (
|
|
15
|
+
<div className="fixed bottom-5 right-5 divide-y rounded-md bg-white">
|
|
16
|
+
<div className="flex gap-2 px-3 py-3 items-center px-5 py-3">
|
|
17
|
+
<Link
|
|
18
|
+
to="?debug=true"
|
|
19
|
+
reloadDocument
|
|
20
|
+
className="flex-1 text-sm font-bold text-gray-600"
|
|
21
|
+
>
|
|
22
|
+
SEO
|
|
23
|
+
</Link>
|
|
24
|
+
<span className="absolute bottom-0 right-0 h-3 w-3 rounded-full bg-green-500 ring ring-white"></span>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function Debugger() {
|
|
31
|
+
const {seo, matches} = useSeoConfig();
|
|
32
|
+
|
|
33
|
+
const tags = useHeadTags(seo);
|
|
34
|
+
const location = useLocation();
|
|
35
|
+
const debug = new URLSearchParams(location.search).get('debug');
|
|
36
|
+
const debuggerOpen = Boolean(
|
|
37
|
+
debug === 'false' ? false : debug === '' || debug,
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
return debuggerOpen ? (
|
|
41
|
+
<Panel>
|
|
42
|
+
<>
|
|
43
|
+
<span className="font-bold block text-sm py-4 px-4">Config</span>
|
|
44
|
+
{Object.entries(seo).flatMap(([property, value]) => {
|
|
45
|
+
if (typeof value !== 'string') {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
return (
|
|
49
|
+
<Item
|
|
50
|
+
key={`${property}${value}`}
|
|
51
|
+
property={property}
|
|
52
|
+
value={value}
|
|
53
|
+
/>
|
|
54
|
+
);
|
|
55
|
+
})}
|
|
56
|
+
|
|
57
|
+
<div className="py-4 px-4 space-y-4">
|
|
58
|
+
{matches.map(({id, handle, data}: RouteMatch, index: number) => (
|
|
59
|
+
<div key={id}>
|
|
60
|
+
<div className="font-bold text-xs pb-2 ">{id}</div>
|
|
61
|
+
<pre className="overflow-x-scroll whitespace-pre font-mono rounded-sm bg-gray-100 text-[10px] px-4 py-2">
|
|
62
|
+
{JSON.stringify(
|
|
63
|
+
recursivelyInvokeOrReturn(handle?.seo, data),
|
|
64
|
+
null,
|
|
65
|
+
2,
|
|
66
|
+
)}
|
|
67
|
+
</pre>
|
|
68
|
+
</div>
|
|
69
|
+
))}
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<span className="font-bold block text-sm py-4 px-4">Tags</span>
|
|
73
|
+
|
|
74
|
+
{Object.entries(tags).map(([label, entries]) => {
|
|
75
|
+
if (entries.length < 1) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
return (
|
|
79
|
+
<div key={label} className="font-bold text-sm py-4 px-4">
|
|
80
|
+
<div className="font-bold text-xs pb-2">
|
|
81
|
+
{LABEL_MAP[label as keyof typeof LABEL_MAP]}
|
|
82
|
+
</div>
|
|
83
|
+
<pre className="overflow-x-scroll whitespace-pre font-mono rounded-sm bg-gray-100 text-[10px] px-4 py-2">
|
|
84
|
+
{label === 'LdJson'
|
|
85
|
+
? JSON.stringify(entries, null, 2)
|
|
86
|
+
: entries.map(
|
|
87
|
+
(entry: React.ReactElement, index: number) =>
|
|
88
|
+
renderToString(entry) + '\n',
|
|
89
|
+
)}
|
|
90
|
+
</pre>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
})}
|
|
94
|
+
</>
|
|
95
|
+
</Panel>
|
|
96
|
+
) : (
|
|
97
|
+
<Badge />
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function Panel({children}: {children?: React.ReactNode}) {
|
|
102
|
+
return (
|
|
103
|
+
<div className="overflow-y-scroll height max-h-full z-40 fixed w-96 bottom-5 top-5 right-5 divide-y rounded-md bg-white text-gray-600">
|
|
104
|
+
<div className="flex items-center px-4 py-3 ">
|
|
105
|
+
<span className="flex-1 text-sm font-bold text-gray-600">SEO</span>
|
|
106
|
+
<Link to="?" reloadDocument className="text-sm font-bold text-gray-600">
|
|
107
|
+
<svg
|
|
108
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
109
|
+
viewBox="0 0 20 20"
|
|
110
|
+
fill="currentColor"
|
|
111
|
+
className="h-5 w-5"
|
|
112
|
+
>
|
|
113
|
+
<path
|
|
114
|
+
fillRule="evenodd"
|
|
115
|
+
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z"
|
|
116
|
+
clipRule="evenodd"
|
|
117
|
+
/>
|
|
118
|
+
</svg>
|
|
119
|
+
</Link>
|
|
120
|
+
</div>
|
|
121
|
+
{children}
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function Item({
|
|
127
|
+
pass,
|
|
128
|
+
value,
|
|
129
|
+
property,
|
|
130
|
+
}: {
|
|
131
|
+
pass?: boolean;
|
|
132
|
+
value: string;
|
|
133
|
+
property: string;
|
|
134
|
+
}) {
|
|
135
|
+
const icon =
|
|
136
|
+
pass !== false ? (
|
|
137
|
+
<svg
|
|
138
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
139
|
+
className="w-5 h-5 top-0.5 relative text-blue-500"
|
|
140
|
+
viewBox="0 0 20 20"
|
|
141
|
+
fill="currentColor"
|
|
142
|
+
>
|
|
143
|
+
<path
|
|
144
|
+
fillRule="evenodd"
|
|
145
|
+
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
|
|
146
|
+
clipRule="evenodd"
|
|
147
|
+
/>
|
|
148
|
+
</svg>
|
|
149
|
+
) : (
|
|
150
|
+
<svg
|
|
151
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
152
|
+
className="w-5 h-5 top-0.5 relative text-red-500"
|
|
153
|
+
viewBox="0 0 20 20"
|
|
154
|
+
fill="currentColor"
|
|
155
|
+
>
|
|
156
|
+
<path
|
|
157
|
+
fillRule="evenodd"
|
|
158
|
+
d="M13.477 14.89A6 6 0 015.11 6.524l8.367 8.368zm1.414-1.414L6.524 5.11a6 6 0 018.367 8.367zM18 10a8 8 0 11-16 0 8 8 0 0116 0z"
|
|
159
|
+
clipRule="evenodd"
|
|
160
|
+
/>
|
|
161
|
+
</svg>
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<div className="flex items-end px-4 py-3 ">
|
|
166
|
+
{icon}
|
|
167
|
+
<span className="font-mono flex-1 mx-1 text-gray-900 text-[10px]">
|
|
168
|
+
<span className="px-2 py-1 rounded-sm bg-gray-100">{property}</span>
|
|
169
|
+
</span>
|
|
170
|
+
<span className="pl-8 text-right text-xs text-gray-900 text-ellipsis overflow-hidden whitespace-nowrap max-w-full">
|
|
171
|
+
{value}
|
|
172
|
+
</span>
|
|
173
|
+
</div>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import {renderToString} from 'react-dom/server';
|
|
2
|
+
|
|
3
|
+
export async function getShareableImage(
|
|
4
|
+
component: React.ReactElement<any, 'svg'>,
|
|
5
|
+
) {
|
|
6
|
+
try {
|
|
7
|
+
const svg = renderToString(component);
|
|
8
|
+
return new Response(svg, {
|
|
9
|
+
headers: {
|
|
10
|
+
'Content-Type': 'image/svg+xml',
|
|
11
|
+
'Cache-Control': 'public, s-maxage=60',
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
} catch (error) {
|
|
15
|
+
// eslint-disable-next-line no-console
|
|
16
|
+
console.error('Failed to render Share Image:', component, error);
|
|
17
|
+
|
|
18
|
+
return new Response(
|
|
19
|
+
renderToString(
|
|
20
|
+
<svg>
|
|
21
|
+
<text>Default share image</text>
|
|
22
|
+
</svg>,
|
|
23
|
+
),
|
|
24
|
+
{
|
|
25
|
+
headers: {
|
|
26
|
+
'Content-Type': 'image/svg+xml',
|
|
27
|
+
'Cache-Control': 'public, s-maxage=60',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {useSeoConfig, useHeadTags} from './common';
|
|
2
|
+
|
|
3
|
+
export function Seo() {
|
|
4
|
+
const {seo} = useSeoConfig();
|
|
5
|
+
const {tags, ogTags, twitterTags, links, LdJson} = useHeadTags(seo);
|
|
6
|
+
const structuredContent = (
|
|
7
|
+
<script
|
|
8
|
+
type="application/ld+json"
|
|
9
|
+
dangerouslySetInnerHTML={{
|
|
10
|
+
__html: JSON.stringify(LdJson),
|
|
11
|
+
}}
|
|
12
|
+
/>
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<>
|
|
17
|
+
{tags}
|
|
18
|
+
{ogTags}
|
|
19
|
+
{twitterTags}
|
|
20
|
+
{links}
|
|
21
|
+
{structuredContent}
|
|
22
|
+
</>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export type SeoDescriptor = {
|
|
2
|
+
type: 'product' | 'article' | 'page' | 'collection' | 'blog' | 'root';
|
|
3
|
+
site: string;
|
|
4
|
+
title: string;
|
|
5
|
+
titleTemplate?: string;
|
|
6
|
+
bypassTitleTemplate?: boolean;
|
|
7
|
+
defaultTitle: string;
|
|
8
|
+
description: string;
|
|
9
|
+
noindex: boolean;
|
|
10
|
+
nofollow: boolean;
|
|
11
|
+
url: string;
|
|
12
|
+
twitter: Partial<TwitterOptions>;
|
|
13
|
+
openGraph: Partial<OpenGraphOptions>;
|
|
14
|
+
images: Partial<ImageOptions>[];
|
|
15
|
+
alternates: Partial<AlternateOptions>[];
|
|
16
|
+
tags: string[];
|
|
17
|
+
robots: Partial<RobotsOptions>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export interface TwitterOptions {
|
|
21
|
+
card: string;
|
|
22
|
+
site: string;
|
|
23
|
+
description: string;
|
|
24
|
+
handle: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface OpenGraphOptions {
|
|
28
|
+
url: string;
|
|
29
|
+
type: string;
|
|
30
|
+
title: string;
|
|
31
|
+
description: string;
|
|
32
|
+
site: string;
|
|
33
|
+
locale: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface OpenGraphProfileOptions {
|
|
37
|
+
firstName: string;
|
|
38
|
+
lastName: string;
|
|
39
|
+
username: string;
|
|
40
|
+
gender: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface OpenGraphArticleOptions {
|
|
44
|
+
publishedTime: Date;
|
|
45
|
+
modifiedTime: Date;
|
|
46
|
+
expirationTime: Date;
|
|
47
|
+
authors: string[];
|
|
48
|
+
section: string;
|
|
49
|
+
tags: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface RobotsOptions {
|
|
53
|
+
noArchive: boolean;
|
|
54
|
+
noSnippet: boolean;
|
|
55
|
+
maxSnippet: number;
|
|
56
|
+
unAvailableAfter: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface AlternateOptions {
|
|
60
|
+
url: string;
|
|
61
|
+
media: string;
|
|
62
|
+
lang: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface ImageOptions {
|
|
66
|
+
url: string;
|
|
67
|
+
width: number;
|
|
68
|
+
height: number;
|
|
69
|
+
alt: string;
|
|
70
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createCookieSessionStorage,
|
|
3
|
+
type SessionStorage,
|
|
4
|
+
type Session,
|
|
5
|
+
} from '@shopify/remix-oxygen';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* This is a custom session implementation for your Hydrogen shop.
|
|
9
|
+
* Feel free to customize it to your needs, add helper methods, or
|
|
10
|
+
* swap out the cookie-based implementation with something else!
|
|
11
|
+
*/
|
|
12
|
+
export class HydrogenSession {
|
|
13
|
+
constructor(
|
|
14
|
+
private sessionStorage: SessionStorage,
|
|
15
|
+
private session: Session,
|
|
16
|
+
) {}
|
|
17
|
+
|
|
18
|
+
static async init(request: Request, secrets: string[]) {
|
|
19
|
+
const storage = createCookieSessionStorage({
|
|
20
|
+
cookie: {
|
|
21
|
+
name: 'session',
|
|
22
|
+
httpOnly: true,
|
|
23
|
+
path: '/',
|
|
24
|
+
sameSite: 'lax',
|
|
25
|
+
secrets,
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const session = await storage.getSession(request.headers.get('Cookie'));
|
|
30
|
+
|
|
31
|
+
return new this(storage, session);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get(key: string) {
|
|
35
|
+
return this.session.get(key);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
destroy() {
|
|
39
|
+
return this.sessionStorage.destroySession(this.session);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
flash(key: string, value: any) {
|
|
43
|
+
this.session.flash(key, value);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
unset(key: string) {
|
|
47
|
+
this.session.unset(key);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
set(key: string, value: any) {
|
|
51
|
+
this.session.set(key, value);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
commit() {
|
|
55
|
+
return this.sessionStorage.commitSession(this.session);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CountryCode,
|
|
3
|
+
LanguageCode,
|
|
4
|
+
} from '@shopify/hydrogen-react/storefront-api-types';
|
|
5
|
+
|
|
6
|
+
export type Locale = {
|
|
7
|
+
label?: string;
|
|
8
|
+
language: LanguageCode;
|
|
9
|
+
country: CountryCode;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type Localizations = Record<string, Locale>;
|
|
13
|
+
|
|
14
|
+
export enum CartAction {
|
|
15
|
+
ADD_TO_CART = 'ADD_TO_CART',
|
|
16
|
+
REMOVE_FROM_CART = 'REMOVE_FROM_CART',
|
|
17
|
+
UPDATE_CART = 'UPDATE_CART',
|
|
18
|
+
UPDATE_DISCOUNT = 'UPDATE_DISCOUNT',
|
|
19
|
+
UPDATE_BUYER_IDENTITY = 'UPDATE_BUYER_IDENTITY',
|
|
20
|
+
}
|
|
21
|
+
export type CartActions = keyof typeof CartAction;
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import {useLocation, useMatches} from '@remix-run/react';
|
|
2
|
+
import type {
|
|
3
|
+
MenuItem,
|
|
4
|
+
Menu,
|
|
5
|
+
MoneyV2,
|
|
6
|
+
} from '@shopify/hydrogen-react/storefront-api-types';
|
|
7
|
+
|
|
8
|
+
// @ts-expect-error types not available
|
|
9
|
+
import typographicBase from 'typographic-base';
|
|
10
|
+
import {countries} from '~/data/countries';
|
|
11
|
+
import {Locale} from './type';
|
|
12
|
+
|
|
13
|
+
export interface EnhancedMenuItem extends MenuItem {
|
|
14
|
+
to: string;
|
|
15
|
+
target: string;
|
|
16
|
+
isExternal?: boolean;
|
|
17
|
+
items: EnhancedMenuItem[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface EnhancedMenu extends Menu {
|
|
21
|
+
items: EnhancedMenuItem[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function missingClass(string?: string, prefix?: string) {
|
|
25
|
+
if (!string) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const regex = new RegExp(` ?${prefix}`, 'g');
|
|
30
|
+
return string.match(regex) === null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function formatText(input?: string | React.ReactNode) {
|
|
34
|
+
if (!input) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (typeof input !== 'string') {
|
|
39
|
+
return input;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return typographicBase(input, {locale: 'en-us'}).replace(
|
|
43
|
+
/\s([^\s<]+)\s*$/g,
|
|
44
|
+
'\u00A0$1',
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function getExcerpt(text: string) {
|
|
49
|
+
const regex = /<p.*>(.*?)<\/p>/;
|
|
50
|
+
const match = regex.exec(text);
|
|
51
|
+
return match?.length ? match[0] : text;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function isNewArrival(date: string, daysOld = 30) {
|
|
55
|
+
return (
|
|
56
|
+
new Date(date).valueOf() >
|
|
57
|
+
new Date().setDate(new Date().getDate() - daysOld).valueOf()
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function isDiscounted(price: MoneyV2, compareAtPrice: MoneyV2) {
|
|
62
|
+
if (compareAtPrice?.amount > price?.amount) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function resolveToFromType(
|
|
69
|
+
{
|
|
70
|
+
customPrefixes,
|
|
71
|
+
pathname,
|
|
72
|
+
type,
|
|
73
|
+
}: {
|
|
74
|
+
customPrefixes: Record<string, string>;
|
|
75
|
+
pathname?: string;
|
|
76
|
+
type?: string;
|
|
77
|
+
} = {
|
|
78
|
+
customPrefixes: {},
|
|
79
|
+
},
|
|
80
|
+
) {
|
|
81
|
+
if (!pathname || !type) return '';
|
|
82
|
+
|
|
83
|
+
/*
|
|
84
|
+
MenuItemType enum
|
|
85
|
+
@see: https://shopify.dev/api/storefront/unstable/enums/MenuItemType
|
|
86
|
+
*/
|
|
87
|
+
const defaultPrefixes = {
|
|
88
|
+
BLOG: 'blogs',
|
|
89
|
+
COLLECTION: 'collections',
|
|
90
|
+
COLLECTIONS: 'collections', // Collections All (not documented)
|
|
91
|
+
FRONTPAGE: 'frontpage',
|
|
92
|
+
HTTP: '',
|
|
93
|
+
PAGE: 'pages',
|
|
94
|
+
CATALOG: 'collections/all', // Products All
|
|
95
|
+
PRODUCT: 'products',
|
|
96
|
+
SEARCH: 'search',
|
|
97
|
+
SHOP_POLICY: 'policies',
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const pathParts = pathname.split('/');
|
|
101
|
+
const handle = pathParts.pop() || '';
|
|
102
|
+
const routePrefix: Record<string, string> = {
|
|
103
|
+
...defaultPrefixes,
|
|
104
|
+
...customPrefixes,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
switch (true) {
|
|
108
|
+
// special cases
|
|
109
|
+
case type === 'FRONTPAGE':
|
|
110
|
+
return '/';
|
|
111
|
+
|
|
112
|
+
case type === 'ARTICLE': {
|
|
113
|
+
const blogHandle = pathParts.pop();
|
|
114
|
+
return routePrefix.BLOG
|
|
115
|
+
? `/${routePrefix.BLOG}/${blogHandle}/${handle}/`
|
|
116
|
+
: `/${blogHandle}/${handle}/`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
case type === 'COLLECTIONS':
|
|
120
|
+
return `/${routePrefix.COLLECTIONS}`;
|
|
121
|
+
|
|
122
|
+
case type === 'SEARCH':
|
|
123
|
+
return `/${routePrefix.SEARCH}`;
|
|
124
|
+
|
|
125
|
+
case type === 'CATALOG':
|
|
126
|
+
return `/${routePrefix.CATALOG}`;
|
|
127
|
+
|
|
128
|
+
// common cases: BLOG, PAGE, COLLECTION, PRODUCT, SHOP_POLICY, HTTP
|
|
129
|
+
default:
|
|
130
|
+
return routePrefix[type]
|
|
131
|
+
? `/${routePrefix[type]}/${handle}`
|
|
132
|
+
: `/${handle}`;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/*
|
|
137
|
+
Parse each menu link and adding, isExternal, to and target
|
|
138
|
+
*/
|
|
139
|
+
function parseItem(customPrefixes = {}) {
|
|
140
|
+
return function (item: MenuItem): EnhancedMenuItem {
|
|
141
|
+
if (!item?.url || !item?.type) {
|
|
142
|
+
// eslint-disable-next-line no-console
|
|
143
|
+
console.warn('Invalid menu item. Must include a url and type.');
|
|
144
|
+
// @ts-ignore
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// extract path from url because we don't need the origin on internal to attributes
|
|
149
|
+
const {pathname} = new URL(item.url);
|
|
150
|
+
|
|
151
|
+
/*
|
|
152
|
+
Currently the MenuAPI only returns online store urls e.g — xyz.myshopify.com/..
|
|
153
|
+
Note: update logic when API is updated to include the active qualified domain
|
|
154
|
+
*/
|
|
155
|
+
const isInternalLink = /\.myshopify\.com/g.test(item.url);
|
|
156
|
+
|
|
157
|
+
const parsedItem = isInternalLink
|
|
158
|
+
? // internal links
|
|
159
|
+
{
|
|
160
|
+
...item,
|
|
161
|
+
isExternal: false,
|
|
162
|
+
target: '_self',
|
|
163
|
+
to: resolveToFromType({type: item.type, customPrefixes, pathname}),
|
|
164
|
+
}
|
|
165
|
+
: // external links
|
|
166
|
+
{
|
|
167
|
+
...item,
|
|
168
|
+
isExternal: true,
|
|
169
|
+
target: '_blank',
|
|
170
|
+
to: item.url,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
...parsedItem,
|
|
175
|
+
items: item.items?.map(parseItem(customPrefixes)),
|
|
176
|
+
};
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/*
|
|
181
|
+
Recursively adds `to` and `target` attributes to links based on their url
|
|
182
|
+
and resource type.
|
|
183
|
+
It optionally overwrites url paths based on item.type
|
|
184
|
+
*/
|
|
185
|
+
export function parseMenu(menu: Menu, customPrefixes = {}): EnhancedMenu {
|
|
186
|
+
if (!menu?.items) {
|
|
187
|
+
// eslint-disable-next-line no-console
|
|
188
|
+
console.warn('Invalid menu passed to parseMenu');
|
|
189
|
+
// @ts-ignore
|
|
190
|
+
return menu;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
...menu,
|
|
195
|
+
items: menu.items.map(parseItem(customPrefixes)),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export const INPUT_STYLE_CLASSES =
|
|
200
|
+
'appearance-none rounded dark:bg-transparent border focus:border-primary/50 focus:ring-0 w-full py-2 px-3 text-primary/90 placeholder:text-primary/50 leading-tight focus:shadow-outline';
|
|
201
|
+
|
|
202
|
+
export const getInputStyleClasses = (isError?: string | null) => {
|
|
203
|
+
return `${INPUT_STYLE_CLASSES} ${
|
|
204
|
+
isError ? 'border-red-500' : 'border-primary/20'
|
|
205
|
+
}`;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
export function statusMessage(status: string) {
|
|
209
|
+
const translations: Record<string, string> = {
|
|
210
|
+
ATTEMPTED_DELIVERY: 'Attempted delivery',
|
|
211
|
+
CANCELED: 'Canceled',
|
|
212
|
+
CONFIRMED: 'Confirmed',
|
|
213
|
+
DELIVERED: 'Delivered',
|
|
214
|
+
FAILURE: 'Failure',
|
|
215
|
+
FULFILLED: 'Fulfilled',
|
|
216
|
+
IN_PROGRESS: 'In Progress',
|
|
217
|
+
IN_TRANSIT: 'In transit',
|
|
218
|
+
LABEL_PRINTED: 'Label printed',
|
|
219
|
+
LABEL_PURCHASED: 'Label purchased',
|
|
220
|
+
LABEL_VOIDED: 'Label voided',
|
|
221
|
+
MARKED_AS_FULFILLED: 'Marked as fulfilled',
|
|
222
|
+
NOT_DELIVERED: 'Not delivered',
|
|
223
|
+
ON_HOLD: 'On Hold',
|
|
224
|
+
OPEN: 'Open',
|
|
225
|
+
OUT_FOR_DELIVERY: 'Out for delivery',
|
|
226
|
+
PARTIALLY_FULFILLED: 'Partially Fulfilled',
|
|
227
|
+
PENDING_FULFILLMENT: 'Pending',
|
|
228
|
+
PICKED_UP: 'Displayed as Picked up',
|
|
229
|
+
READY_FOR_PICKUP: 'Ready for pickup',
|
|
230
|
+
RESTOCKED: 'Restocked',
|
|
231
|
+
SCHEDULED: 'Scheduled',
|
|
232
|
+
SUBMITTED: 'Submitted',
|
|
233
|
+
UNFULFILLED: 'Unfulfilled',
|
|
234
|
+
};
|
|
235
|
+
try {
|
|
236
|
+
return translations?.[status];
|
|
237
|
+
} catch (error) {
|
|
238
|
+
return status;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Errors can exist in an errors object, or nested in a data field.
|
|
244
|
+
*/
|
|
245
|
+
export function assertApiErrors(data: Record<string, any> | null | undefined) {
|
|
246
|
+
const errorMessage = data?.customerUserErrors?.[0]?.message;
|
|
247
|
+
if (errorMessage) {
|
|
248
|
+
throw new Error(errorMessage);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export const DEFAULT_LOCALE: Locale & {pathPrefix: string} = Object.freeze({
|
|
253
|
+
...countries.default,
|
|
254
|
+
pathPrefix: '',
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
export function getLocaleFromRequest(request: Request): Locale & {
|
|
258
|
+
pathPrefix: string;
|
|
259
|
+
} {
|
|
260
|
+
const url = new URL(request.url);
|
|
261
|
+
const firstPathPart =
|
|
262
|
+
'/' + url.pathname.substring(1).split('/')[0].toLowerCase();
|
|
263
|
+
|
|
264
|
+
return countries[firstPathPart]
|
|
265
|
+
? {
|
|
266
|
+
...countries[firstPathPart],
|
|
267
|
+
pathPrefix: firstPathPart,
|
|
268
|
+
}
|
|
269
|
+
: {
|
|
270
|
+
...countries['default'],
|
|
271
|
+
pathPrefix: '',
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function usePrefixPathWithLocale(path: string) {
|
|
276
|
+
const [root] = useMatches();
|
|
277
|
+
const selectedLocale = root.data?.selectedLocale ?? DEFAULT_LOCALE;
|
|
278
|
+
|
|
279
|
+
return `${selectedLocale.pathPrefix}${
|
|
280
|
+
path.startsWith('/') ? path : '/' + path
|
|
281
|
+
}`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function useIsHomePath() {
|
|
285
|
+
const {pathname} = useLocation();
|
|
286
|
+
const [root] = useMatches();
|
|
287
|
+
const selectedLocale = root.data?.selectedLocale ?? DEFAULT_LOCALE;
|
|
288
|
+
const strippedPathname = pathname.replace(selectedLocale.pathPrefix, '');
|
|
289
|
+
return strippedPathname === '/';
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Validates that a url is local
|
|
294
|
+
* @param url
|
|
295
|
+
* @returns `true` if local `false`if external domain
|
|
296
|
+
*/
|
|
297
|
+
export function isLocalPath(url: string) {
|
|
298
|
+
try {
|
|
299
|
+
// We don't want to redirect cross domain,
|
|
300
|
+
// doing so could create fishing vulnerability
|
|
301
|
+
// If `new URL()` succeeds, it's a fully qualified
|
|
302
|
+
// url which is cross domain. If it fails, it's just
|
|
303
|
+
// a path, which will be the current domain.
|
|
304
|
+
new URL(url);
|
|
305
|
+
} catch (e) {
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return false;
|
|
310
|
+
}
|